diff --git a/lib/get-esm-exports.mjs b/lib/get-esm-exports.mjs index 2da2db8..9549fd5 100644 --- a/lib/get-esm-exports.mjs +++ b/lib/get-esm-exports.mjs @@ -90,16 +90,6 @@ function decodeExportName (name) { } } -// es-module-lexer reports a bare `export * from ` only as an import with no -// matching export entry, indistinguishable from `import ` except by the -// statement text. This matches that text to rewrite it as the transitive -// `* from ` marker the interpreting code recognizes. `export * as ns -// from` binds a real name and is reported as a normal export, so it must not -// match here. `GAP` allows whitespace and comments between the tokens, the way -// the parser does (e.g. `export /* c */ * from`). -const GAP = '(?:\\s|/\\*[^]*?\\*/|//[^\\n]*\\n)*' -const STAR_REEXPORT = new RegExp(`^export${GAP}\\*${GAP}from`) - /** * Lexes ESM source code with es-module-lexer and builds a list of exported * identifiers. In the baseline case the list is the simple identifier names as @@ -133,17 +123,15 @@ export default function getEsmExports (moduleSource) { */ export function lexEsm (moduleSource) { const exportNames = new Set() - const [imports, exports, , hasModuleSyntax] = parse(moduleSource) + const [, exports, , hasModuleSyntax] = parse(moduleSource) for (const exported of exports) { - exportNames.add(decodeExportName(exported.n)) - } + if (exported.typeOnly) continue - // Bare `export * from ` re-exports report no export name; reconstruct - // the transitive marker from the import statement that carries the specifier. - for (const imported of imports) { - if (STAR_REEXPORT.test(moduleSource.slice(imported.ss, imported.se))) { - exportNames.add(`* from ${imported.n}`) + if (exported.type === 'reexport-all') { + exportNames.add(`* from ${exported.from}`) + } else { + exportNames.add(decodeExportName(exported.name)) } } diff --git a/lib/get-exports.mjs b/lib/get-exports.mjs index 05a11e8..8bd4749 100644 --- a/lib/get-exports.mjs +++ b/lib/get-exports.mjs @@ -11,11 +11,6 @@ import { LOAD } from './io.mjs' const nodeMajor = Number(process.versions.node.split('.')[0]) export const hasModuleExportsCJSDefault = nodeMajor >= 23 -// Resolve `stripTypeScriptTypes` (Node >= 22.13) via `getBuiltinModule` rather -// than a static named import (throws on older runtimes) or `require` (re-enters -// iitm's own loader hooks). `undefined` on runtimes that lack it. -const stripTypeScriptTypes = process.getBuiltinModule?.('module')?.stripTypeScriptTypes - let parserInitialized = false // The CJS export scanner is backed by WebAssembly. `initSync` compiles it @@ -270,11 +265,14 @@ export function * getExports (url, context) { try { let moduleFormat = format - if (format === 'module-typescript' || format === 'commonjs-typescript') { + if (format === 'module-typescript') { + moduleFormat = 'module' + } else if (format === 'commonjs-typescript') { + const stripTypeScriptTypes = process.getBuiltinModule?.('module')?.stripTypeScriptTypes if (stripTypeScriptTypes !== undefined) { source = stripTypeScriptTypes(source, { mode: 'strip' }) } - moduleFormat = format === 'module-typescript' ? 'module' : 'commonjs' + moduleFormat = 'commonjs' } if (moduleFormat === 'commonjs') { diff --git a/package.json b/package.json index e011819..b68a46b 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ }, "dependencies": { "cjs-module-lexer": "^2.2.0", - "es-module-lexer": "^2.2.0", + "es-module-lexer": "github:BridgeAR/es-module-lexer#c3d8cdac91995d8bf6eff84d2165e4a66fdd7a4f", "module-details-from-path": "^1.0.4" } } diff --git a/test/fixtures/typescript-cjs-hook.cts b/test/fixtures/typescript-cjs-hook.cts index b92a9f2..f766d08 100644 --- a/test/fixtures/typescript-cjs-hook.cts +++ b/test/fixtures/typescript-cjs-hook.cts @@ -1,5 +1,6 @@ interface Shape { kind: string } type Label = string +export type ExportedLabel = string const epsilon: number = 5 diff --git a/test/get-esm-exports/v20-get-esm-exports.mjs b/test/get-esm-exports/v20-get-esm-exports.mjs index bd220cc..eb092e9 100644 --- a/test/get-esm-exports/v20-get-esm-exports.mjs +++ b/test/get-esm-exports/v20-get-esm-exports.mjs @@ -23,6 +23,11 @@ fixture.split('\n').forEach(line => { console.log(`${mod}\n ✅ contains exports: ${testStr}`) }) +assert.deepEqual(Array.from(getEsmExports('export type { Type } from "module-name"')), []) +assert.deepEqual(Array.from(getEsmExports('export type * from "module-name"')), []) +assert.deepEqual(Array.from(getEsmExports('export const alpha: number = 1, beta: string = "two"')), ['alpha', 'beta']) +assert.deepEqual(Array.from(getEsmExports('const type = 1; export { type }')), ['type']) + // // Generate fixture data // fixture.split('\n').forEach(line => { // if (!line.includes('export ')) { diff --git a/test/register/v22.15-sync-register-hooks.mjs b/test/register/v22.15-sync-register-hooks.mjs index 2e8e7b8..9feeba7 100644 --- a/test/register/v22.15-sync-register-hooks.mjs +++ b/test/register/v22.15-sync-register-hooks.mjs @@ -73,6 +73,7 @@ nodeModule.registerHooks({ register() let somethingHooked = false +let typescriptCjsExports let wrapFailureHooked = false // No module filter: match fixtures by file name, mirroring test/hook/static-import.mjs. @@ -82,6 +83,9 @@ new Hook((exports, name) => { somethingHooked = true exports.foo += 15 } + if (typeof name === 'string' && name.endsWith('/typescript-cjs-hook.cts')) { + typescriptCjsExports = exports + } if (typeof name === 'string' && /sync-wrap-failure/.test(name)) { wrapFailureHooked = true } @@ -95,6 +99,11 @@ ok(somethingHooked, 'sync hook should have run for something.mjs') strictEqual(namespace.foo, 57, 'hook-mutated named export should be visible through the wrapper') strictEqual(typeof namespace.default, 'function', 'default export should be preserved') +const typescriptCjs = await import('../fixtures/typescript-cjs-hook.cts') +strictEqual(typescriptCjs.default.epsilon, 5) +strictEqual(typescriptCjs.default.zeta({ kind: 'square' }), 'square') +ok(typescriptCjsExports, 'sync hook should instrument commonjs-typescript with type-only exports') + // When IITM fails to wrap, it falls back to the upstream loader: the module // still loads, but it cannot be Hook'ed because it was never wrapped. // @ts-expect-error - resolved by the in-process loader above