Skip to content

Commit

Permalink
Merge pull request #727 from cm-ayf/register-esm-use-ts-resolver
Browse files Browse the repository at this point in the history
@swc-node/register/esm use TypeScript resolver
  • Loading branch information
Brooooooklyn committed Sep 26, 2023
2 parents 6d46780 + e34f006 commit 1ea2734
Showing 1 changed file with 75 additions and 80 deletions.
155 changes: 75 additions & 80 deletions packages/register/esm.mts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { constants as FSConstants, promises as fs } from 'fs'
import { isAbsolute, join, parse } from 'path'
import { fileURLToPath, pathToFileURL } from 'url'

import ts from 'typescript'
Expand All @@ -9,101 +7,98 @@ import { readDefaultTsConfig } from '../lib/read-default-tsconfig.js'
// @ts-expect-error
import { compile } from '../lib/register.js'

type ResolveFn = (
interface ResolveContext {
conditions: string[]
parentURL: string | undefined
}
interface ResolveResult {
format?: string
shortCircuit?: boolean
url: string
}
type ResolveArgs = [
specifier: string,
context?: { conditions: string[]; parentURL: string | undefined },
defaultResolve?: ResolveFn,
) => Promise<{ url: string }>

const DEFAULT_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts']
context?: ResolveContext,
nextResolve?: (...args: ResolveArgs) => Promise<ResolveResult>,
]
type ResolveFn = (...args: Required<ResolveArgs>) => Promise<ResolveResult>

const TRANSFORM_MAP = new Map<string, string>()
const tsconfig: ts.CompilerOptions = readDefaultTsConfig()
tsconfig.module = ts.ModuleKind.ESNext

async function checkRequestURL(parentURL: string, requestURL: string) {
const { dir, name, ext } = parse(requestURL)
const parentDir = join(parentURL.startsWith('file://') ? fileURLToPath(parentURL) : parentURL, '..')
if (ext && ext !== '.js' && ext !== '.mjs') {
try {
const url = join(parentDir, requestURL)
await fs.access(url, FSConstants.R_OK)
return url
} catch (e) {
// ignore
}
} else {
for (const ext of DEFAULT_EXTENSIONS) {
try {
const url = join(parentDir, dir, `${name}${ext}`)
await fs.access(url, FSConstants.R_OK)
return url
} catch (e) {
// ignore
}
try {
const url = join(parentDir, requestURL, `index${ext}`)
await fs.access(url, FSConstants.R_OK)
return url
} catch (e) {
// ignore
}
}
}
const moduleResolutionCache = ts.createModuleResolutionCache(ts.sys.getCurrentDirectory(), (x) => x, tsconfig)
const host: ts.ModuleResolutionHost = {
fileExists: ts.sys.fileExists,
readFile: ts.sys.readFile,
}
const EXTENSIONS: string[] = [ts.Extension.Ts, ts.Extension.Tsx, ts.Extension.Mts]

export const resolve: ResolveFn = async (specifier, context, nextResolve) => {
const rawUrl = TRANSFORM_MAP.get(specifier)
if (rawUrl) {
return { url: new URL(rawUrl).href, format: 'module', shortCircuit: true }
}
const { parentURL } = context ?? {}
if (parentURL && TRANSFORM_MAP.has(parentURL) && specifier.startsWith('.')) {
const existedURL = await checkRequestURL(parentURL, specifier)
if (existedURL) {
const { href: url } = pathToFileURL(existedURL)
TRANSFORM_MAP.set(url, existedURL)
return {
url: new URL(url).href,
shortCircuit: true,
format: 'module',
}
// entrypoint
if (!context.parentURL) {
return {
format: EXTENSIONS.some((ext) => specifier.endsWith(ext)) ? 'ts' : undefined,
url: specifier,
shortCircuit: true,
}
}
if (DEFAULT_EXTENSIONS.some((ext) => specifier.endsWith(ext))) {
specifier = specifier.startsWith('file://') ? specifier : pathToFileURL(specifier).toString()
const newUrl = `${specifier}.mjs`
TRANSFORM_MAP.set(newUrl, fileURLToPath(specifier))

// import/require from external library
if (context.parentURL.includes('/node_modules/')) {
return nextResolve(specifier)
}

const { resolvedModule } = ts.resolveModuleName(
specifier,
fileURLToPath(context.parentURL),
tsconfig,
host,
moduleResolutionCache,
)

// import from local project to local project TS file
if (
resolvedModule &&
!resolvedModule.resolvedFileName.includes('/node_modules/') &&
EXTENSIONS.includes(resolvedModule.extension)
) {
return {
format: 'ts',
url: pathToFileURL(resolvedModule.resolvedFileName).href,
shortCircuit: true,
url: new URL(newUrl).href,
format: 'module',
}
}
if (parentURL && isAbsolute(parentURL)) {
return nextResolve!(specifier, {
...context!,
parentURL: pathToFileURL(parentURL).toString(),
})
}
return nextResolve!(specifier)

// import from local project to either:
// - something TS couldn't resolve
// - external library
// - local project non-TS file
return nextResolve(specifier)
}

type LoadFn = (
url: string,
context: { format: string },
defaultLoad: LoadFn,
) => Promise<{ format: string; source: string | ArrayBuffer | SharedArrayBuffer | Uint8Array }>
interface LoadContext {
conditions: string[]
format: string | null | undefined
}
interface LoadResult {
format: string
shortCircuit?: boolean
source: string | ArrayBuffer | SharedArrayBuffer | Uint8Array
}
type LoadArgs = [url: string, context: LoadContext, nextLoad?: (...args: LoadArgs) => Promise<LoadResult>]
type LoadFn = (...args: Required<LoadArgs>) => Promise<LoadResult>

export const load: LoadFn = async (url, context, defaultLoad) => {
const filePath = TRANSFORM_MAP.get(url)
if (filePath) {
const tsconfig: ts.CompilerOptions = readDefaultTsConfig()
tsconfig.module = ts.ModuleKind.ESNext
const code = await compile(await fs.readFile(filePath, 'utf8'), filePath, tsconfig, true)
export const load: LoadFn = async (url, context, nextLoad) => {
if (context.format === 'ts') {
const { source } = await nextLoad(url, context)
const code = typeof source === 'string' ? source : Buffer.from(source).toString()
const compiled = await compile(code, url, tsconfig, true)
return {
format: context.format,
source: code,
format: 'module',
source: compiled,
shortCircuit: true,
}
} else {
return nextLoad(url, context)
}
return defaultLoad(url, context, defaultLoad)
}

0 comments on commit 1ea2734

Please sign in to comment.