diff --git a/lib/esm-transformer.ts b/lib/esm-transformer.ts index 318b46fe..0956d264 100644 --- a/lib/esm-transformer.ts +++ b/lib/esm-transformer.ts @@ -207,6 +207,24 @@ function replaceImportMetaObject(code: string): string { return code.replace(/const import_meta\s*=\s*\{\s*\};/, shimImplementation); } +/** + * Rewrite relative `.mjs` require paths to `.js` in CJS output + * + * When esbuild transforms ESM to CJS, it converts `import './foo.mjs'` to `require('./foo.mjs')`. + * Since the packer renames `.mjs` files to `.js` in the snapshot, the require paths must be + * updated to match. This handles the rewriting at build time. + * + * @param code - The CJS code after esbuild transformation + * @returns Code with relative .mjs require paths rewritten to .js + */ +export function rewriteMjsRequirePaths(code: string): string { + // Match require("./path.mjs") or require('../path.mjs') with relative paths only + return code.replace( + /require\((["'])(\.\.?\/[^"']*?)\.mjs\1\)/g, + 'require($1$2.js$1)', + ); +} + /** * Transform ESM code to CommonJS using esbuild * This allows ESM modules to be compiled to bytecode via vm.Script @@ -395,7 +413,7 @@ export function transformESMtoCJS( // Inject import.meta shims after esbuild transformation if needed let finalCode = result.code; if (usesImportMeta) { - finalCode = replaceImportMetaObject(result.code); + finalCode = replaceImportMetaObject(finalCode); } return { diff --git a/lib/walker.ts b/lib/walker.ts index 94d287ea..2b2429fc 100644 --- a/lib/walker.ts +++ b/lib/walker.ts @@ -26,7 +26,7 @@ import { pc } from './colors'; import { follow } from './follow'; import { log, wasReported } from './log'; import * as detector from './detector'; -import { transformESMtoCJS } from './esm-transformer'; +import { transformESMtoCJS, rewriteMjsRequirePaths } from './esm-transformer'; import { ConfigDictionary, FileRecord, @@ -75,6 +75,10 @@ const strictVerify = Boolean(process.env.PKG_STRICT_VER); const win32 = process.platform === 'win32'; +// Extensions to try when resolving modules +// Includes .mjs to support ESM files that get transformed to .js +const MODULE_RESOLVE_EXTENSIONS = ['.js', '.json', '.node', '.mjs']; + /** * Checks if a module is a core module * module.isBuiltin is available in Node.js 16.17.0 or later. Use that if available @@ -826,7 +830,8 @@ class Walker { // it is not enough because 'typos.json' // is not taken in require('./typos') // in 'normalize-package-data/lib/fixer.js' - extensions: ['.js', '.json', '.node'], + // Also include .mjs to support ESM files that get transformed to .js + extensions: MODULE_RESOLVE_EXTENSIONS, catchReadFile, catchPackageFilter, }); @@ -865,7 +870,7 @@ class Walker { try { newFile2 = await follow(derivative.alias, { basedir: path.dirname(record.file), - extensions: ['.js', '.json', '.node'], + extensions: MODULE_RESOLVE_EXTENSIONS, ignoreFile: newPackage.packageJson, }); if (strictVerify) { @@ -1116,6 +1121,15 @@ class Walker { const derivatives2: Derivative[] = []; stepDetect(record, marker, derivatives2); await this.stepDerivatives(record, marker, derivatives2); + + // After dependencies are resolved, rewrite .mjs require paths to .js + // since the packer renames .mjs files to .js in the snapshot + if (record.wasTransformed && record.body) { + record.body = Buffer.from( + rewriteMjsRequirePaths(record.body.toString('utf8')), + 'utf8', + ); + } } } diff --git a/test/.gitignore b/test/.gitignore index c2658d7d..58922d9e 100644 --- a/test/.gitignore +++ b/test/.gitignore @@ -1 +1,2 @@ node_modules/ +run-time/ diff --git a/test/test-50-fs-runtime-layer-2/main.js b/test/test-50-fs-runtime-layer-2/main.js index 35ad06fc..a3b51431 100644 --- a/test/test-50-fs-runtime-layer-2/main.js +++ b/test/test-50-fs-runtime-layer-2/main.js @@ -25,12 +25,16 @@ function bitty(version) { (64 * /^(node|v)?16/.test(version)) | (128 * /^(node|v)?18/.test(version)) ); + // Node 20+ not included: bootstrap.js error handling has version-specific + // branches only up to Node 18, so error messages won't match on newer versions. } const version1 = process.version; const version2 = target; -if (bitty(version1) === bitty(version2)) { +// Only run when both versions are recognized and match. +// Unrecognized versions (Node 20+) return 0, so the test is skipped. +if (bitty(version1) !== 0 && bitty(version1) === bitty(version2)) { let left, right; utils.mkdirp.sync(path.dirname(output)); diff --git a/test/test-52-esm-internal-imports/esm-module/module.mjs b/test/test-52-esm-internal-imports/esm-module/module.mjs new file mode 100644 index 00000000..e73b85a7 --- /dev/null +++ b/test/test-52-esm-internal-imports/esm-module/module.mjs @@ -0,0 +1,3 @@ +export function testFunction() { + console.log("If you see this, it actually worked!"); +} diff --git a/test/test-52-esm-internal-imports/esm-module/package.json b/test/test-52-esm-internal-imports/esm-module/package.json new file mode 100644 index 00000000..b5550829 --- /dev/null +++ b/test/test-52-esm-internal-imports/esm-module/package.json @@ -0,0 +1,6 @@ +{ + "name": "test-52-esm-internal-imports", + "version": "1.0.0", + "type": "module", + "private": true +} diff --git a/test/test-52-esm-internal-imports/esm-module/script.mjs b/test/test-52-esm-internal-imports/esm-module/script.mjs new file mode 100644 index 00000000..956ac0c3 --- /dev/null +++ b/test/test-52-esm-internal-imports/esm-module/script.mjs @@ -0,0 +1,2 @@ +import { testFunction } from './module.mjs'; +testFunction(); diff --git a/test/test-52-esm-internal-imports/main.js b/test/test-52-esm-internal-imports/main.js new file mode 100644 index 00000000..fe6be48e --- /dev/null +++ b/test/test-52-esm-internal-imports/main.js @@ -0,0 +1,51 @@ +#!/usr/bin/env node + +'use strict'; + +const path = require('path'); +const assert = require('assert'); +const utils = require('../utils.js'); + +assert(!module.parent); +assert(__dirname === process.cwd()); + +const target = process.argv[2] || 'host'; +const input = './esm-module/script.mjs'; +const output = './run-time/test-output.exe'; + +console.log('Testing ESM internal imports (.mjs importing .mjs)...'); + +let left, right; +utils.mkdirp.sync(path.dirname(output)); + +// Run with node first to get expected output +console.log('Running with node...'); +left = utils.spawn.sync('node', [path.basename(input)], { + cwd: path.dirname(input), +}); +console.log('Node output:', left); + +// Package with pkg +console.log('Packaging with pkg...'); +utils.pkg.sync(['--target', target, '--output', output, input], { + stdio: 'inherit', +}); +console.log('Packaging succeeded'); + +// Run packaged version +console.log('Running packaged version...'); +right = utils.spawn.sync('./' + path.basename(output), [], { + cwd: path.dirname(output), +}); +console.log('Packaged output:', right); + +// Verify outputs match +assert.strictEqual( + left.trim(), + right.trim(), + 'Outputs should match between node and pkg', +); + +console.log('Test passed: ESM internal imports work correctly'); + +utils.vacuum.sync(path.dirname(output)); diff --git a/test/test-53-esm-nested-imports/esm-module/entry.mjs b/test/test-53-esm-nested-imports/esm-module/entry.mjs new file mode 100644 index 00000000..99eaf8c3 --- /dev/null +++ b/test/test-53-esm-nested-imports/esm-module/entry.mjs @@ -0,0 +1,3 @@ +import { level1Function } from './level1.mjs'; +console.log('Entry point called'); +level1Function(); diff --git a/test/test-53-esm-nested-imports/esm-module/level1.mjs b/test/test-53-esm-nested-imports/esm-module/level1.mjs new file mode 100644 index 00000000..444d61b9 --- /dev/null +++ b/test/test-53-esm-nested-imports/esm-module/level1.mjs @@ -0,0 +1,6 @@ +import { level2Function } from './level2.mjs'; + +export function level1Function() { + console.log('Level 1 function called'); + level2Function(); +} diff --git a/test/test-53-esm-nested-imports/esm-module/level2.mjs b/test/test-53-esm-nested-imports/esm-module/level2.mjs new file mode 100644 index 00000000..fbfbcaa6 --- /dev/null +++ b/test/test-53-esm-nested-imports/esm-module/level2.mjs @@ -0,0 +1,3 @@ +export function level2Function() { + console.log('Level 2 function called - nested imports work!'); +} diff --git a/test/test-53-esm-nested-imports/esm-module/package.json b/test/test-53-esm-nested-imports/esm-module/package.json new file mode 100644 index 00000000..f7fce2b0 --- /dev/null +++ b/test/test-53-esm-nested-imports/esm-module/package.json @@ -0,0 +1,6 @@ +{ + "name": "test-53-esm-nested-imports", + "version": "1.0.0", + "type": "module", + "private": true +} diff --git a/test/test-53-esm-nested-imports/main.js b/test/test-53-esm-nested-imports/main.js new file mode 100644 index 00000000..a29fce55 --- /dev/null +++ b/test/test-53-esm-nested-imports/main.js @@ -0,0 +1,53 @@ +#!/usr/bin/env node + +'use strict'; + +const path = require('path'); +const assert = require('assert'); +const utils = require('../utils.js'); + +assert(!module.parent); +assert(__dirname === process.cwd()); + +const target = process.argv[2] || 'host'; +const input = './esm-module/entry.mjs'; +const output = './run-time/test-output.exe'; + +console.log( + 'Testing ESM nested imports (.mjs importing .mjs importing .mjs)...', +); + +let left, right; +utils.mkdirp.sync(path.dirname(output)); + +// Run with node first to get expected output +console.log('Running with node...'); +left = utils.spawn.sync('node', [path.basename(input)], { + cwd: path.dirname(input), +}); +console.log('Node output:', left); + +// Package with pkg +console.log('Packaging with pkg...'); +utils.pkg.sync(['--target', target, '--output', output, input], { + stdio: 'inherit', +}); +console.log('Packaging succeeded'); + +// Run packaged version +console.log('Running packaged version...'); +right = utils.spawn.sync('./' + path.basename(output), [], { + cwd: path.dirname(output), +}); +console.log('Packaged output:', right); + +// Verify outputs match +assert.strictEqual( + left.trim(), + right.trim(), + 'Outputs should match between node and pkg', +); + +console.log('Test passed: ESM nested imports work correctly'); + +utils.vacuum.sync(path.dirname(output));