From 0eb4e911f31c478400c7f31027f218123450d108 Mon Sep 17 00:00:00 2001 From: Hiroki Osame Date: Fri, 7 Jun 2024 11:53:02 +0900 Subject: [PATCH] feat(esm/api): `tsImport()` to support loading CommonJS files --- docs/node/ts-import.md | 4 ---- src/cjs/api/module-resolve-filename.ts | 7 ++++++- src/esm/api/ts-import.ts | 6 ++++++ src/esm/hook/load.ts | 6 +++++- tests/specs/api.ts | 11 ++++++++++- 5 files changed, 27 insertions(+), 7 deletions(-) diff --git a/docs/node/ts-import.md b/docs/node/ts-import.md index d9e8491a9..3a214d0e4 100644 --- a/docs/node/ts-import.md +++ b/docs/node/ts-import.md @@ -8,10 +8,6 @@ The current file path must be passed in as the second argument to resolve the im Since this is designed for one-time use, it does not cache loaded modules. -::: warning Caveat -CommonJS files are currently not enhanced due to this [Node.js bug](https://github.com/nodejs/node/issues/51327). -::: - ## ESM usage ```js diff --git a/src/cjs/api/module-resolve-filename.ts b/src/cjs/api/module-resolve-filename.ts index a801f3aef..61629c41f 100644 --- a/src/cjs/api/module-resolve-filename.ts +++ b/src/cjs/api/module-resolve-filename.ts @@ -26,8 +26,13 @@ export const interopCjsExports = ( } const searchParams = new URLSearchParams(request.slice(queryIndex + 1)); - const realPath = searchParams.get('filePath'); + let realPath = searchParams.get('filePath'); if (realPath) { + const namespace = searchParams.get('namespace'); + if (namespace) { + realPath += `?namespace=${encodeURIComponent(namespace)}`; + } + // The CJS module cache needs to be updated with the actual path for export parsing to work // https://github.com/nodejs/node/blob/v22.2.0/lib/internal/modules/esm/translators.js#L338 Module._cache[realPath] = Module._cache[request]; diff --git a/src/esm/api/ts-import.ts b/src/esm/api/ts-import.ts index e0f486993..02268f4c7 100644 --- a/src/esm/api/ts-import.ts +++ b/src/esm/api/ts-import.ts @@ -1,3 +1,4 @@ +import { register as cjsRegister } from '../../cjs/api/index.js'; import { register, type TsconfigOptions } from './register.js'; type Options = { @@ -20,6 +21,11 @@ const tsImport = ( const parentURL = isOptionsString ? options : options.parentURL; const namespace = Date.now().toString(); + // Keep registered for hanging require() calls + cjsRegister({ + namespace, + }); + /** * We don't want to unregister this after load since there can be child import() calls * that need TS support diff --git a/src/esm/hook/load.ts b/src/esm/hook/load.ts index 82f4d16de..085672efd 100644 --- a/src/esm/hook/load.ts +++ b/src/esm/hook/load.ts @@ -29,7 +29,8 @@ export const load: LoadHook = async ( return nextLoad(url, context); } - if (data.namespace && data.namespace !== getNamespace(url)) { + const urlNamespace = getNamespace(url); + if (data.namespace && data.namespace !== urlNamespace) { return nextLoad(url, context); } @@ -76,6 +77,9 @@ export const load: LoadHook = async ( exports.map(exported => exported.n).filter(name => name !== 'default').join(',') }}`; const parameters = new URLSearchParams({ filePath }); + if (urlNamespace) { + parameters.set('namespace', urlNamespace); + } loaded.responseURL = `data:text/javascript,${encodeURIComponent(cjsExports)}?${parameters.toString()}`; } diff --git a/tests/specs/api.ts b/tests/specs/api.ts index e7b752b7c..10bea57f5 100644 --- a/tests/specs/api.ts +++ b/tests/specs/api.ts @@ -23,6 +23,7 @@ const tsFiles = { export const foo = \`foo \${bar}\` as string export const async = setTimeout(10).then(() => require('./async')).catch((error) => error); `, + 'cts.cts': 'export const cts = \'cts\' as string', 'bar.ts': 'export type A = 1; export { bar } from "pkg"', 'async.ts': 'export default "async"', 'node_modules/pkg': { @@ -507,6 +508,9 @@ export default testSuite(({ describe }, node: NodeApis) => { const { message } = await tsImport('./file.ts', import.meta.url); console.log(message); + const cts = await tsImport('./cts.cts', import.meta.url).then(m => m.cts, err => err.constructor.name); + console.log(cts); + const { message: message2 } = await tsImport('./file.ts?with-query', import.meta.url); console.log(message2); @@ -522,7 +526,12 @@ export default testSuite(({ describe }, node: NodeApis) => { nodePath: node.path, nodeOptions: [], }); - expect(stdout).toMatch(/Fails as expected 1\nfoo bar file\.ts\?tsx-namespace=\d+\nfoo bar file\.ts\?with-query=&tsx-namespace=\d+\nFails as expected 2/); + + if (node.supports.cjsInterop) { + expect(stdout).toMatch(/Fails as expected 1\nfoo bar file\.ts\?tsx-namespace=\d+\ncts\nfoo bar file\.ts\?with-query=&tsx-namespace=\d+\nFails as expected 2/); + } else { + expect(stdout).toMatch(/Fails as expected 1\nfoo bar file\.ts\?tsx-namespace=\d+\nSyntaxError\nfoo bar file\.ts\?with-query=&tsx-namespace=\d+\nFails as expected 2/); + } }); test('commonjs', async () => {