esm-loader.mjs 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. // ESM loader for module-alias
  2. // Provides resolve hooks for ES modules
  3. import { readFileSync, existsSync, statSync } from 'node:fs'
  4. import { join, resolve as pathResolve, dirname } from 'node:path'
  5. import { fileURLToPath, pathToFileURL } from 'node:url'
  6. let aliases = {}
  7. let moduleDirectories = []
  8. let base = process.cwd()
  9. let initialized = false
  10. export function init (options = {}) {
  11. if (initialized) return
  12. const thisDir = dirname(fileURLToPath(import.meta.url))
  13. const candidatePaths = options.base
  14. ? [pathResolve(options.base)]
  15. : [join(thisDir, '../..'), process.cwd()]
  16. let pkg
  17. for (const candidate of candidatePaths) {
  18. try {
  19. pkg = JSON.parse(readFileSync(join(candidate, 'package.json'), 'utf8'))
  20. base = candidate
  21. break
  22. } catch (e) {
  23. // Continue to next candidate
  24. }
  25. }
  26. if (!pkg) {
  27. console.warn('[module-alias] Unable to find package.json in:', candidatePaths.join(', '))
  28. initialized = true
  29. return
  30. }
  31. // Load _moduleAliases
  32. const pkgAliases = pkg._moduleAliases || {}
  33. for (const alias in pkgAliases) {
  34. const target = pkgAliases[alias]
  35. aliases[alias] = target.startsWith('/') ? target : join(base, target)
  36. }
  37. // Load _moduleDirectories
  38. if (Array.isArray(pkg._moduleDirectories)) {
  39. moduleDirectories = pkg._moduleDirectories
  40. .filter(d => d !== 'node_modules')
  41. .map(d => join(base, d))
  42. }
  43. initialized = true
  44. }
  45. export function isPathMatchesAlias (path, alias) {
  46. // Matching /^alias(\/|$)/
  47. if (path.indexOf(alias) === 0) {
  48. if (path.length === alias.length) return true
  49. if (path[alias.length] === '/') return true
  50. }
  51. return false
  52. }
  53. export function resolveAlias (specifier, parentURL) {
  54. init() // Ensure initialized
  55. // Sort aliases by length (longest first) for correct matching
  56. const sortedAliases = Object.keys(aliases).sort((a, b) => b.length - a.length)
  57. for (const alias of sortedAliases) {
  58. if (isPathMatchesAlias(specifier, alias)) {
  59. const target = aliases[alias]
  60. // Function-based resolver
  61. if (typeof target === 'function') {
  62. const parentPath = parentURL ? fileURLToPath(parentURL) : process.cwd()
  63. const result = target(parentPath, specifier, alias)
  64. if (!result || typeof result !== 'string') {
  65. throw new Error('[module-alias] Custom handler must return path')
  66. }
  67. return result
  68. }
  69. // String path - join target with remainder of specifier
  70. return join(target, specifier.slice(alias.length))
  71. }
  72. }
  73. // Check moduleDirectories
  74. for (const dir of moduleDirectories) {
  75. const modulePath = join(dir, specifier)
  76. // Check for directory with index.mjs/index.js
  77. if (existsSync(join(modulePath, 'index.mjs'))) {
  78. return join(modulePath, 'index.mjs')
  79. }
  80. if (existsSync(join(modulePath, 'index.js'))) {
  81. return join(modulePath, 'index.js')
  82. }
  83. // Check for file with extension
  84. if (existsSync(modulePath + '.mjs')) {
  85. return modulePath + '.mjs'
  86. }
  87. if (existsSync(modulePath + '.js')) {
  88. return modulePath + '.js'
  89. }
  90. // Check for exact file
  91. if (existsSync(modulePath) && !statSync(modulePath).isDirectory()) {
  92. return modulePath
  93. }
  94. }
  95. return null
  96. }
  97. export function addAlias (alias, target) {
  98. aliases[alias] = target
  99. }
  100. export function addAliases (aliasMap) {
  101. for (const alias in aliasMap) {
  102. addAlias(alias, aliasMap[alias])
  103. }
  104. }
  105. export function addPath (path) {
  106. moduleDirectories.push(path)
  107. }
  108. export function reset () {
  109. aliases = {}
  110. moduleDirectories = []
  111. base = process.cwd()
  112. initialized = false
  113. }
  114. // For Node 18-21: async loader hooks
  115. export async function resolve (specifier, context, nextResolve) {
  116. const resolved = resolveAlias(specifier, context.parentURL)
  117. if (resolved) {
  118. // If absolute path, convert to file URL
  119. if (resolved.startsWith('/')) {
  120. return { url: pathToFileURL(resolved).href, shortCircuit: true }
  121. }
  122. // Otherwise let Node resolve it (could be npm package)
  123. return nextResolve(resolved, context)
  124. }
  125. return nextResolve(specifier, context)
  126. }
  127. export async function initialize (data) {
  128. init(data || {})
  129. }