@@ -85,16 +85,27 @@ export async function transpileTypeScript(mainFilePath, typescript) {
8585 skipLibCheck : true ,
8686 } )
8787
88- // Check if the code uses CommonJS globals
88+ // Check if the code uses CommonJS globals.
89+ //
90+ // A bare `require` or `module` identifier counts, not just `require(...)` and
91+ // `module.exports`: `if (require.main === module)` is the standard entrypoint idiom,
92+ // and without a shim the transpiled file throws "require is not defined in ES module
93+ // scope". Quoted occurrences (`from 'module'`) are specifiers, not references. A file
94+ // that declares its own binding is left alone, so the shim can never redeclare it.
95+ const references = name => new RegExp ( `(?<!['"\`])\\b${ name } \\b(?!['"\`])` ) . test ( jsContent )
96+ const declaresOwn = name => new RegExp ( `\\b(?:const|let|var|function|class)\\s+${ name } \\b|\\bimport\\s+${ name } \\b` ) . test ( jsContent )
97+ const needsShim = name => references ( name ) && ! declaresOwn ( name )
98+
8999 const usesCommonJSGlobals = / _ _ d i r n a m e | _ _ f i l e n a m e / . test ( jsContent )
90- const usesRequire = / \b r e q u i r e \s * \( / . test ( jsContent )
100+ const usesRequire = needsShim ( 'require' )
101+ const usesModule = needsShim ( 'module' )
91102 const usesModuleExports = / \b ( m o d u l e \. e x p o r t s | e x p o r t s \. ) / . test ( jsContent )
92103
93- if ( usesCommonJSGlobals || usesRequire || usesModuleExports ) {
104+ if ( usesCommonJSGlobals || usesRequire || usesModule || usesModuleExports ) {
94105 // Inject ESM equivalents at the top of the file
95106 let esmGlobals = ''
96107
97- if ( usesRequire || usesModuleExports ) {
108+ if ( usesRequire ) {
98109 // IMPORTANT: Use the original .ts file path as the base for require()
99110 // This ensures dynamic require() calls work with relative paths from the original file location
100111 const originalFileUrl = `file://${ filePath . replace ( / \\ / g, '/' ) } `
@@ -131,7 +142,11 @@ const require = (id) => {
131142 }
132143};
133144
134- const module = { exports: {} };
145+ `
146+ }
147+
148+ if ( usesModule || usesModuleExports ) {
149+ esmGlobals += `const module = { exports: {} };
135150const exports = module.exports;
136151
137152`
@@ -189,8 +204,9 @@ const __dirname = __dirname_fn(__filename);
189204 // Transpile this file
190205 let jsContent = transpileTS ( filePath )
191206
192- // Find all TypeScript imports in this file (both ESM imports and require() calls)
207+ // Find all TypeScript imports in this file (static imports, dynamic import() and require() calls)
193208 const importRegex = / f r o m \s + [ ' " ] ( [ ^ ' " ] + ?) [ ' " ] / g
209+ const dynamicImportRegex = / \b i m p o r t \s * \( \s * [ ' " ] ( [ ^ ' " ] + ?) [ ' " ] \s * \) / g
194210 const requireRegex = / r e q u i r e \s * \( \s * [ ' " ] ( [ ^ ' " ] + ?) [ ' " ] \s * \) / g
195211 let match
196212 const imports = [ ]
@@ -199,6 +215,10 @@ const __dirname = __dirname_fn(__filename);
199215 imports . push ( { path : match [ 1 ] , type : 'import' } )
200216 }
201217
218+ while ( ( match = dynamicImportRegex . exec ( jsContent ) ) !== null ) {
219+ imports . push ( { path : match [ 1 ] , type : 'import' } )
220+ }
221+
202222 while ( ( match = requireRegex . exec ( jsContent ) ) !== null ) {
203223 imports . push ( { path : match [ 1 ] , type : 'require' } )
204224 }
@@ -260,85 +280,76 @@ const __dirname = __dirname_fn(__filename);
260280 }
261281 }
262282
263- // After all dependencies are transpiled, rewrite imports in this file
264- jsContent = jsContent . replace (
265- / f r o m \s + [ ' " ] ( [ ^ ' " ] + ? ) [ ' " ] / g ,
266- ( match , importPath ) => {
267- let resolvedPath = importPath
268- const originalExt = path . extname ( importPath )
283+ // Resolve one ESM specifier to the temp file its source was transpiled into.
284+ // Returns null when the specifier must be left untouched — bare package names,
285+ // or paths that resolve to nothing we emitted.
286+ const resolveEsmSpecifier = importPath => {
287+ let resolvedPath = importPath
288+ const originalExt = path . extname ( importPath )
269289
270- // Check if this is a path alias
271- const resolvedAlias = resolveTsPathAlias ( importPath , tsConfig , configDir )
272- if ( resolvedAlias ) {
273- resolvedPath = resolvedAlias
274- } else if ( importPath . startsWith ( '.' ) ) {
275- resolvedPath = path . resolve ( fileBaseDir , importPath )
276- } else {
277- return match
278- }
290+ // Check if this is a path alias
291+ const resolvedAlias = resolveTsPathAlias ( importPath , tsConfig , configDir )
292+ if ( resolvedAlias ) {
293+ resolvedPath = resolvedAlias
294+ } else if ( importPath . startsWith ( '.' ) ) {
295+ resolvedPath = path . resolve ( fileBaseDir , importPath )
296+ } else {
297+ return null
298+ }
279299
280- // If resolved path is a directory, try index.ts
281- if ( fs . existsSync ( resolvedPath ) && fs . statSync ( resolvedPath ) . isDirectory ( ) ) {
282- const indexPath = path . join ( resolvedPath , 'index.ts' )
283- if ( fs . existsSync ( indexPath ) && transpiledFiles . has ( indexPath ) ) {
284- const tempFile = transpiledFiles . get ( indexPath )
285- const relPath = path . relative ( fileBaseDir , tempFile ) . replace ( / \\ / g, '/' )
286- if ( ! relPath . startsWith ( '.' ) ) {
287- return `from './${ relPath } '`
288- }
289- return `from '${ relPath } '`
290- }
291- }
300+ const toRelative = tempFile => {
301+ const relPath = path . relative ( fileBaseDir , tempFile ) . replace ( / \\ / g, '/' )
302+ return relPath . startsWith ( '.' ) ? relPath : `./${ relPath } `
303+ }
292304
293- // Handle .js extension that might be .ts
294- if ( resolvedPath . endsWith ( '.js' ) ) {
295- const tsVersion = resolvedPath . replace ( / \. j s $ / , '.ts' )
296- if ( transpiledFiles . has ( tsVersion ) ) {
297- const tempFile = transpiledFiles . get ( tsVersion )
298- const relPath = path . relative ( fileBaseDir , tempFile ) . replace ( / \\ / g, '/' )
299- if ( ! relPath . startsWith ( '.' ) ) {
300- return `from './${ relPath } '`
301- }
302- return `from '${ relPath } '`
303- }
304- return match
305+ // If resolved path is a directory, try index.ts
306+ if ( fs . existsSync ( resolvedPath ) && fs . statSync ( resolvedPath ) . isDirectory ( ) ) {
307+ const indexPath = path . join ( resolvedPath , 'index.ts' )
308+ if ( fs . existsSync ( indexPath ) && transpiledFiles . has ( indexPath ) ) {
309+ return toRelative ( transpiledFiles . get ( indexPath ) )
305310 }
311+ }
306312
307- // Try with .ts extension
308- const tsPath = resolvedPath . endsWith ( '.ts' ) ? resolvedPath : resolvedPath + '.ts'
313+ // Handle .js extension that might be .ts
314+ if ( resolvedPath . endsWith ( '.js' ) ) {
315+ const tsVersion = resolvedPath . replace ( / \. j s $ / , '.ts' )
316+ return transpiledFiles . has ( tsVersion ) ? toRelative ( transpiledFiles . get ( tsVersion ) ) : null
317+ }
309318
310- // If we transpiled this file, use the temp file
311- if ( transpiledFiles . has ( tsPath ) ) {
312- const tempFile = transpiledFiles . get ( tsPath )
313- const relPath = path . relative ( fileBaseDir , tempFile ) . replace ( / \\ / g, '/' )
314- if ( ! relPath . startsWith ( '.' ) ) {
315- return `from './${ relPath } '`
316- }
317- return `from '${ relPath } '`
318- }
319+ // Try with .ts extension
320+ const tsPath = resolvedPath . endsWith ( '.ts' ) ? resolvedPath : resolvedPath + '.ts'
321+ if ( transpiledFiles . has ( tsPath ) ) {
322+ return toRelative ( transpiledFiles . get ( tsPath ) )
323+ }
319324
320- // Try index.ts for directory imports
321- const indexTsPath = path . join ( resolvedPath , 'index.ts' )
322- if ( transpiledFiles . has ( indexTsPath ) ) {
323- const tempFile = transpiledFiles . get ( indexTsPath )
324- const relPath = path . relative ( fileBaseDir , tempFile ) . replace ( / \\ / g, '/' )
325- if ( ! relPath . startsWith ( '.' ) ) {
326- return `from './${ relPath } '`
327- }
328- return `from '${ relPath } '`
329- }
325+ // Try index.ts for directory imports
326+ const indexTsPath = path . join ( resolvedPath , 'index.ts' )
327+ if ( transpiledFiles . has ( indexTsPath ) ) {
328+ return toRelative ( transpiledFiles . get ( indexTsPath ) )
329+ }
330330
331- // If the import doesn't have a standard module extension, add .js for ESM compatibility
332- const standardExtensions = [ '.js' , '.mjs' , '.cjs' , '.json' , '.node' ]
333- const hasStandardExtension = standardExtensions . includes ( originalExt . toLowerCase ( ) )
331+ // If the import doesn't have a standard module extension, add .js for ESM compatibility
332+ const standardExtensions = [ '.js' , '.mjs' , '.cjs' , '.json' , '.node' ]
333+ if ( ! standardExtensions . includes ( originalExt . toLowerCase ( ) ) ) {
334+ return `${ importPath } .js`
335+ }
334336
335- if ( ! hasStandardExtension ) {
336- return match . replace ( importPath , importPath + '.js' )
337- }
337+ return null
338+ }
338339
339- return match
340- }
341- )
340+ // After all dependencies are transpiled, rewrite imports in this file
341+ jsContent = jsContent . replace ( / f r o m \s + [ ' " ] ( [ ^ ' " ] + ?) [ ' " ] / g, ( match , importPath ) => {
342+ const resolved = resolveEsmSpecifier ( importPath )
343+ return resolved === null ? match : `from '${ resolved } '`
344+ } )
345+
346+ // Dynamic import() resolves exactly like a static import. Without this rewrite,
347+ // `await import('./module')` survives transpilation still pointing at a `.ts` file
348+ // that was never emitted, and fails with ERR_MODULE_NOT_FOUND at runtime.
349+ jsContent = jsContent . replace ( / ( \b i m p o r t \s * \( \s * ) [ ' " ] ( [ ^ ' " ] + ?) [ ' " ] ( \s * \) ) / g, ( match , open , importPath , close ) => {
350+ const resolved = resolveEsmSpecifier ( importPath )
351+ return resolved === null ? match : `${ open } '${ resolved } '${ close } `
352+ } )
342353
343354 // Also rewrite require() calls to point to transpiled TypeScript files
344355 jsContent = jsContent . replace (
0 commit comments