Skip to content

Commit

Permalink
feat: try to resolve format for absolute path import
Browse files Browse the repository at this point in the history
  • Loading branch information
yeliex authored and Brooooooklyn committed Jun 28, 2024
1 parent 92f05d4 commit 86fb5d2
Showing 1 changed file with 131 additions and 15 deletions.
146 changes: 131 additions & 15 deletions packages/register/esm.mts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { readFile } from 'fs/promises'
import { createRequire, type LoadFnOutput, type LoadHook, type ResolveFnOutput, type ResolveHook } from 'node:module'
import { extname } from 'path'
import { fileURLToPath, parse as parseUrl, pathToFileURL } from 'url'

import debugFactory from 'debug'
import ts from 'typescript'


// @ts-expect-error
import { readDefaultTsConfig } from '../lib/read-default-tsconfig.js'
// @ts-expect-error
Expand All @@ -28,13 +29,110 @@ const addShortCircuitSignal = <T extends ResolveFnOutput | LoadFnOutput>(input:
}
}

const INTERNAL_MODULE_PATTERN = /^(data|node|nodejs):/
interface PackageJson {
name: string
version: string
type?: 'module' | 'commonjs'
main?: string
}

const packageJSONCache = new Map<string, undefined | PackageJson>()

const readFileIfExists = async (path: string) => {
try {
const content = await readFile(path, 'utf-8')

return JSON.parse(content)
} catch (e) {
// eslint-disable-next-line no-undef
if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
return undefined
}

throw e
}
}

const readPackageJSON = async (path: string) => {
if (packageJSONCache.has(path)) {
return packageJSONCache.get(path)
}

const res = (await readFileIfExists(path)) as PackageJson
packageJSONCache.set(path, res)
return res
}

const getPackageForFile = async (url: string) => {
// use URL instead path.resolve to handle relative path
let packageJsonURL = new URL('./package.json', url)

// eslint-disable-next-line no-constant-condition
while (true) {
const path = fileURLToPath(packageJsonURL)

// for special case by some package manager
if (path.endsWith('node_modules/package.json')) {
break
}

const packageJson = await readPackageJSON(path)

if (!packageJson) {
const lastPath = packageJsonURL.pathname
packageJsonURL = new URL('../package.json', packageJsonURL)

// root level /package.json
if (packageJsonURL.pathname === lastPath) {
break
}

continue
}

if (packageJson.type && packageJson.type !== 'module' && packageJson.type !== 'commonjs') {
packageJson.type = undefined
}

return packageJson
}

return undefined
}

export const getPackageType = async (url: string) => {
const packageJson = await getPackageForFile(url)

return packageJson?.type ?? undefined
}

const INTERNAL_MODULE_PATTERN = /^(node|nodejs):/

const EXTENSION_MODULE_MAP = {
'.mjs': 'module',
'.cjs': 'commonjs',
'.ts': 'module',
'.mts': 'module',
'.cts': 'commonjs',
'.json': 'json',
'.wasm': 'wasm',
'.node': 'commonjs',
} as const

export const resolve: ResolveHook = async (specifier, context, nextResolve) => {
debug('resolve', specifier, JSON.stringify(context))

if (INTERNAL_MODULE_PATTERN.test(specifier)) {
debug('resolved original caused by internal format', specifier)
debug('skip resolve: internal format', specifier)

return addShortCircuitSignal({
url: specifier,
format: 'builtin',
})
}

if (specifier.startsWith('data:')) {
debug('skip resolve: data url', specifier)

return addShortCircuitSignal({
url: specifier,
Expand All @@ -45,15 +143,29 @@ export const resolve: ResolveHook = async (specifier, context, nextResolve) => {

// as entrypoint, just return specifier
if (!context.parentURL || parsedUrl.protocol === 'file:') {
debug('resolved original caused by protocol', specifier)
debug('skip resolve: absolute path or entrypoint', specifier)

let format: ResolveFnOutput['format'] = null

const specifierPath = fileURLToPath(specifier)
const ext = extname(specifierPath)

if (ext === '.js') {
format = (await getPackageType(specifier)) === 'module' ? 'module' : 'commonjs'
} else {
format = EXTENSION_MODULE_MAP[ext as keyof typeof EXTENSION_MODULE_MAP]
}

return addShortCircuitSignal({
url: specifier,
format: 'module',
format,
})
}

// import attributes, support json currently
if (context.importAttributes?.type) {
debug('skip resolve: import attributes', specifier)

return addShortCircuitSignal(await nextResolve(specifier))
}

Expand All @@ -71,7 +183,7 @@ export const resolve: ResolveHook = async (specifier, context, nextResolve) => {
!resolvedModule.resolvedFileName.includes('/node_modules/') &&
AVAILABLE_TS_EXTENSION_PATTERN.test(resolvedModule.resolvedFileName)
) {
debug('resolved by typescript', specifier, resolvedModule.resolvedFileName)
debug('resolved: typescript', specifier, resolvedModule.resolvedFileName)

return addShortCircuitSignal({
...context,
Expand All @@ -83,14 +195,14 @@ export const resolve: ResolveHook = async (specifier, context, nextResolve) => {
try {
// files could not resolved by typescript or resolved as dts, fallback to use node resolver
const res = await nextResolve(specifier)
debug('fallback resolved by node', specifier, res.url, res.format)
debug('resolved: fallback node', specifier, res.url, res.format)
return addShortCircuitSignal(res)
} catch (resolveError) {
// fallback to cjs resolve as may import non-esm files
try {
const resolution = pathToFileURL(createRequire(process.cwd()).resolve(specifier)).toString()

debug('resolved by node commonjs', specifier, resolution)
debug('resolved: fallback commonjs', specifier, resolution)

return addShortCircuitSignal({
format: 'commonjs',
Expand All @@ -112,25 +224,29 @@ const tsconfigForSWCNode = {
export const load: LoadHook = async (url, context, nextLoad) => {
debug('load', url, JSON.stringify(context))

if (url.startsWith('data:')) {
debug('skip load: data url', url)

return nextLoad(url, context)
}

if (['builtin', 'json', 'wasm'].includes(context.format)) {
debug('load original caused by internal format', url)
debug('loaded: internal format', url)
return nextLoad(url, context)
}

const { source, format } = await nextLoad(url, {
...context,
})
const { source, format: resolvedFormat } = await nextLoad(url, context)

debug('loaded', url, format)
debug('loaded', url, resolvedFormat)

const code = !source || typeof source === 'string' ? source : Buffer.from(source).toString()
const compiled = await compile(code, url, tsconfigForSWCNode, true)

debug('compiled', url, format)
debug('compiled', url, resolvedFormat)

return addShortCircuitSignal({
// for lazy: ts-node think format would undefined, actually it should not, keep it as original temporarily
format,
format: resolvedFormat,
source: compiled,
})
}

0 comments on commit 86fb5d2

Please sign in to comment.