From c4d170edd69a3b80321ad52ee53d469763269f7f Mon Sep 17 00:00:00 2001 From: Stephan Schreiber Date: Wed, 7 Feb 2024 14:52:36 +0100 Subject: [PATCH] Feat: support Node's --experimental-default-type flag --- .vscode/launch.json | 26 ++++++++++++++++++++++++++ rollup.config.js | 2 +- source/ambient.d.ts | 32 ++++++++++++++++++-------------- source/cjs-hooks.ts | 17 ++++++++--------- source/cjs-transform.cts | 12 ++++++------ source/esm-hooks.ts | 21 +++++++++++---------- source/index.ts | 23 +++++++++++++++++++---- 7 files changed, 89 insertions(+), 44 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..de2e434 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Program", + "type": "node", + "request": "launch", + "skipFiles": [ + "/**", + "**/node_modules/**" + ], + "runtimeArgs": [ + "--import=./lib/index.js", + "--experimental-default-type module" + ], + "program": "zz_test.js", + "outFiles": [ + "${workspaceFolder}/lib/*.js" + ], + "sourceMaps": true + } + ] +} diff --git a/rollup.config.js b/rollup.config.js index bf9044e..d0537e1 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -19,7 +19,7 @@ export default defineConfig([ dir: 'lib', format: 'esm', generatedCode: 'es2015', - sourcemap: "hidden", + sourcemap: true, sourcemapExcludeSources: true }, plugins: [ diff --git a/source/ambient.d.ts b/source/ambient.d.ts index a6b16cf..13ac4d0 100644 --- a/source/ambient.d.ts +++ b/source/ambient.d.ts @@ -1,23 +1,27 @@ declare global { - namespace NodeJS { - interface Module { - _compile(code: string, filename: string): string - } + type ModuleType = 'commonjs' | 'module' - interface NodeError { - code: string - } + // In package.json. + interface PkgType { + type?: ModuleType + } - type ModuleType = 'commonjs' | 'module' - interface PkgType { - type?: ModuleType - } - } + // The data passed to the initialize() hook. + interface InitializeHookData { + self: string + defaultModuleType: ModuleType + } + + namespace NodeJS { + interface Module { + _compile(code: string, filename: string): string + } + } } declare module 'module' { - export const _extensions: NodeJS.RequireExtensions; + export const _extensions: NodeJS.RequireExtensions } // This file needs to be a module -export {} +export { } diff --git a/source/cjs-hooks.ts b/source/cjs-hooks.ts index 96936ee..771c98f 100644 --- a/source/cjs-hooks.ts +++ b/source/cjs-hooks.ts @@ -4,7 +4,7 @@ import { readFileSync } from 'node:fs' const require = createRequire(import.meta.url) -function transpile(m: Module, format: NodeJS.ModuleType, filePath: string) { +function transpile(m: Module, format: ModuleType, filePath: string) { // Notes: // - This function is called by the CJS loader so it must be sync. // - We lazy-load Sucrase as the CJS loader may well never be used @@ -18,8 +18,8 @@ function transpile(m: Module, format: NodeJS.ModuleType, filePath: string) { } const unknownType = Symbol() -const pkgTypeCache = new Map() -function nearestPackageType(file: string): NodeJS.ModuleType { +const pkgTypeCache = new Map() +function nearestPackageType(file: string, defaultType: ModuleType): ModuleType { for ( let current = path.dirname(file), previous: string | undefined = undefined; previous !== current; @@ -30,13 +30,13 @@ function nearestPackageType(file: string): NodeJS.ModuleType { if (!format) { try { const data = readFileSync(pkgFile, 'utf-8') - const { type } = JSON.parse(data) as NodeJS.PkgType + const { type } = JSON.parse(data) as PkgType format = type === 'module' || type ==='commonjs' ? type : unknownType } catch(err) { - const { code } = err as NodeJS.NodeError + const { code } = err as NodeJS.ErrnoException if (code !== 'ENOENT') console.error(err) format = unknownType @@ -49,12 +49,11 @@ function nearestPackageType(file: string): NodeJS.ModuleType { return format } - // TODO: decide default format based on --experimental-default-type - return 'commonjs' + return defaultType } -export function install_cjs_hooks() { - Module._extensions['.ts'] = (m, filename) => transpile(m, nearestPackageType(filename), filename) +export function install_cjs_hooks(defaultType: ModuleType) { + Module._extensions['.ts'] = (m, filename) => transpile(m, nearestPackageType(filename, defaultType), filename) Module._extensions['.cts'] = (m, filename) => transpile(m, 'commonjs', filename) Module._extensions['.mts'] = (m, filename) => transpile(m, 'module', filename) } diff --git a/source/cjs-transform.cts b/source/cjs-transform.cts index e528146..c022b36 100644 --- a/source/cjs-transform.cts +++ b/source/cjs-transform.cts @@ -1,11 +1,11 @@ -import { transform as sucrase, type Transform } from 'sucrase' +import { transform as sucrase, type Transform, type TransformResult } from 'sucrase' -const transforms: Record = { +const transforms: Record = { commonjs: [ 'typescript', 'imports' ], module: [ 'typescript' ] } -export function transform(source: string, format: NodeJS.ModuleType, filePath: string) { +export function transform(source: string, format: ModuleType, filePath: string) { const { code, sourceMap } = sucrase(source, { filePath, transforms: transforms[format], @@ -16,10 +16,10 @@ export function transform(source: string, format: NodeJS.ModuleType, filePath: s sourceMapOptions: { compiledFilename: filePath } - }) + }) as Required - sourceMap!.sourceRoot = '' - sourceMap!.sources = [ filePath ] + sourceMap.sourceRoot = '' + sourceMap.sources = [ filePath ] // sourceMap.sourcesContent = [ source ] return code + '\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,' diff --git a/source/esm-hooks.ts b/source/esm-hooks.ts index 1a71468..99bfe1d 100644 --- a/source/esm-hooks.ts +++ b/source/esm-hooks.ts @@ -1,13 +1,15 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' import { readFile } from 'node:fs/promises' -import { type InitializeHook, type ResolveHook, type LoadHook, type ModuleSource, createRequire } from 'node:module' +import { createRequire, type InitializeHook, type ResolveHook, type LoadHook } from 'node:module' const { transform } = createRequire(import.meta.url)('./cjs-transform.cjs') as typeof import('./cjs-transform.cjs') let self: string -export const initialize: InitializeHook = data => { - self = data +let defaultModuleType: ModuleType +export const initialize: InitializeHook = data => { + self = data.self + defaultModuleType = data.defaultModuleType } export const resolve: ResolveHook = async (specifier, context, nextResolve) => { @@ -37,8 +39,8 @@ export const resolve: ResolveHook = async (specifier, context, nextResolve) => { } const unknownType = Symbol() -const pkgTypeCache = new Map() -async function nearestPackageType(file: string): Promise { +const pkgTypeCache = new Map() +async function nearestPackageType(file: string): Promise { for ( let current = path.dirname(file), previous: string | undefined = undefined; previous !== current; @@ -48,9 +50,9 @@ async function nearestPackageType(file: string): Promise { let format = pkgTypeCache.get(pkgFile) if (!format) { format = await readFile(pkgFile, 'utf-8') - .then(data => (JSON.parse(data) as NodeJS.PkgType).type ?? unknownType) + .then(data => (JSON.parse(data) as PkgType).type ?? unknownType) .catch(err => { - const { code } = err as NodeJS.NodeError + const { code } = err as NodeJS.ErrnoException if (code !== 'ENOENT') console.error(err) return unknownType @@ -62,8 +64,7 @@ async function nearestPackageType(file: string): Promise { return format } - // TODO: decide default format based on --experimental-default-type - return 'commonjs' + return defaultModuleType } export const load: LoadHook = async (url, context, nextLoad) => { @@ -77,7 +78,7 @@ export const load: LoadHook = async (url, context, nextLoad) => { // Determine the output format based on the file's extension // or the nearest package.json's `type` field. const filePath = fileURLToPath(url) - const format: NodeJS.ModuleType = ( + const format: ModuleType = ( ext[1] === '.ts' ? await nearestPackageType(filePath) : ext[1] === '.mts' diff --git a/source/index.ts b/source/index.ts index f52521d..acb3ce6 100644 --- a/source/index.ts +++ b/source/index.ts @@ -11,16 +11,31 @@ if ( || (major === 20 && minor >= 6) || (major === 18 && minor >= 19) ) { - const self = import.meta.url + + // Determine the default module type. + let defaultModuleType: ModuleType = 'commonjs' + const argc = process.execArgv.findIndex(arg => arg.startsWith('--experimental-default-type')) + if (argc >= 0) { + const argv = process.execArgv[argc].split('=') + const type = argv.length === 1 + ? process.execArgv[argc + 1] + : argv[1] + if (type === 'module' || type === 'commonjs') + defaultModuleType = type + } // Install the esm hooks -- those are run in a worker thread. - Module.register('./esm-hooks.js', { + const self = import.meta.url + Module.register('./esm-hooks.js', { parentURL: self, - data: self + data: { + self, + defaultModuleType + } }) // Install the cjs hooks -- those are run synchronously in the main thread. - install_cjs_hooks() + install_cjs_hooks(defaultModuleType) // Enable source map support. process.setSourceMapsEnabled(true)