Skip to content
Merged
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
20 changes: 19 additions & 1 deletion lib/esm-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
20 changes: 17 additions & 3 deletions lib/walker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
});
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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',
);
}
}
}

Expand Down
1 change: 1 addition & 0 deletions test/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules/
run-time/
6 changes: 5 additions & 1 deletion test/test-50-fs-runtime-layer-2/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down
3 changes: 3 additions & 0 deletions test/test-52-esm-internal-imports/esm-module/module.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function testFunction() {
console.log("If you see this, it actually worked!");
}
6 changes: 6 additions & 0 deletions test/test-52-esm-internal-imports/esm-module/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "test-52-esm-internal-imports",
"version": "1.0.0",
"type": "module",
"private": true
}
2 changes: 2 additions & 0 deletions test/test-52-esm-internal-imports/esm-module/script.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { testFunction } from './module.mjs';
testFunction();
51 changes: 51 additions & 0 deletions test/test-52-esm-internal-imports/main.js
Original file line number Diff line number Diff line change
@@ -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));
3 changes: 3 additions & 0 deletions test/test-53-esm-nested-imports/esm-module/entry.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { level1Function } from './level1.mjs';
console.log('Entry point called');
level1Function();
6 changes: 6 additions & 0 deletions test/test-53-esm-nested-imports/esm-module/level1.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { level2Function } from './level2.mjs';

export function level1Function() {
console.log('Level 1 function called');
level2Function();
}
3 changes: 3 additions & 0 deletions test/test-53-esm-nested-imports/esm-module/level2.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function level2Function() {
console.log('Level 2 function called - nested imports work!');
}
6 changes: 6 additions & 0 deletions test/test-53-esm-nested-imports/esm-module/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "test-53-esm-nested-imports",
"version": "1.0.0",
"type": "module",
"private": true
}
53 changes: 53 additions & 0 deletions test/test-53-esm-nested-imports/main.js
Original file line number Diff line number Diff line change
@@ -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));