diff --git a/src/esm/hook/load.ts b/src/esm/hook/load.ts index 085672efd..33918aeff 100644 --- a/src/esm/hook/load.ts +++ b/src/esm/hook/load.ts @@ -10,7 +10,6 @@ import { parent } from '../../utils/ipc/client.js'; import type { Message } from '../types.js'; import { fileMatcher } from '../../utils/tsconfig.js'; import { isJsonPattern, tsExtensionsPattern } from '../../utils/path-utils.js'; -import { parseEsm } from '../../utils/es-module-lexer.js'; import { getNamespace } from './utils.js'; import { data } from './initialize.js'; @@ -69,20 +68,35 @@ export const load: LoadHook = async ( loaded.format === 'commonjs' && isFeatureSupported(esmLoadReadFile) && loaded.responseURL?.startsWith('file:') // Could be data: + && !filePath.endsWith('.cjs') // CJS syntax doesn't need to be transformed for interop ) { - const code = await readFile(new URL(url), 'utf8'); - const [, exports] = parseEsm(code); - if (exports.length > 0) { - const cjsExports = `module.exports={${ - 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()}`; - } + /** + * es or cjs module lexer unfortunately cannot be used because it doesn't support + * typescript syntax + * + * While the full code is transformed, only the exports are used for parsing. + * In fact, the code can't even run because imports cannot be resolved relative + * from the data: URL. + * + * TODO: extract exports only + */ + const transformed = await transform( + await readFile(new URL(url), 'utf8'), + filePath, + { + format: 'cjs', + // CJS Annotations for Node + platform: 'node', + // TODO: disable source maps + }, + ); + + const parameters = new URLSearchParams({ filePath }); + if (urlNamespace) { + parameters.set('namespace', urlNamespace); + } + loaded.responseURL = `data:text/javascript,${encodeURIComponent(transformed.code)}?${parameters.toString()}`; return loaded; } diff --git a/src/utils/debug.ts b/src/utils/debug.ts index a119d931a..e5afebe43 100644 --- a/src/utils/debug.ts +++ b/src/utils/debug.ts @@ -4,7 +4,10 @@ import { writeSync } from 'node:fs'; export const log = ( ...args: any[] ) => { - writeSync(1, `${inspect(args, { colors: true })}\n\n`); + writeSync( + 1, + `${args.map(argument => inspect(argument, { colors: true })).join(' ')}\n\n`, + ); }; export const time = <T extends (...args: any[]) => unknown>( diff --git a/tests/specs/api.ts b/tests/specs/api.ts index 10bea57f5..d3e3e3e2a 100644 --- a/tests/specs/api.ts +++ b/tests/specs/api.ts @@ -23,7 +23,9 @@ 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', + 'exports-no.cts': 'console.log("cts loaded" as string)', + 'exports-yes.cts': 'module.exports.cts = require("./esm-syntax.js").default as string', + 'esm-syntax.js': 'export default "cts export"', 'bar.ts': 'export type A = 1; export { bar } from "pkg"', 'async.ts': 'export default "async"', 'node_modules/pkg': { @@ -508,7 +510,10 @@ 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); + // Loads cts vis CJS namespace even if there are no exports + await tsImport('./exports-no.cts', import.meta.url).catch((error) => console.log(error.constructor.name)) + + const cts = await tsImport('./exports-yes.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); @@ -528,9 +533,9 @@ export default testSuite(({ describe }, node: NodeApis) => { }); 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/); + expect(stdout).toMatch(/Fails as expected 1\nfoo bar file\.ts\?tsx-namespace=\d+\ncts loaded\ncts export\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/); + expect(stdout).toMatch(/Fails as expected 1\nfoo bar file\.ts\?tsx-namespace=\d+\nSyntaxError\nSyntaxError\nfoo bar file\.ts\?with-query=&tsx-namespace=\d+\nFails as expected 2/); } });