Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 6 additions & 18 deletions lib/get-esm-exports.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -90,16 +90,6 @@ function decodeExportName (name) {
}
}

// es-module-lexer reports a bare `export * from <mod>` only as an import with no
// matching export entry, indistinguishable from `import <mod>` except by the
// statement text. This matches that text to rewrite it as the transitive
// `* from <specifier>` 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
Expand Down Expand Up @@ -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 <mod>` 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))
}
}

Expand Down
12 changes: 5 additions & 7 deletions lib/get-exports.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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') {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
1 change: 1 addition & 0 deletions test/fixtures/typescript-cjs-hook.cts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
interface Shape { kind: string }
type Label = string
export type ExportedLabel = string

const epsilon: number = 5

Expand Down
5 changes: 5 additions & 0 deletions test/get-esm-exports/v20-get-esm-exports.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ')) {
Expand Down
9 changes: 9 additions & 0 deletions test/register/v22.15-sync-register-hooks.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
}
Expand All @@ -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
Expand Down