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
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
lib-es5
node_modules
dist
test/test-51-esm-import-meta/esm-module/test-import-meta-basic.js
107 changes: 91 additions & 16 deletions lib/esm-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,52 @@ interface UnsupportedFeature {
column: number | null;
}

/**
* Check if code contains import.meta usage
*
* @param code - The ESM source code to check
* @returns true if import.meta is used, false otherwise
*/
function hasImportMeta(code: string): boolean {
try {
const ast = babel.parse(code, {
sourceType: 'module',
plugins: [],
});

if (!ast) {
return false;
}

let found = false;

// @ts-expect-error Type mismatch due to @babel/types version in @types/babel__traverse
traverse(ast as t.File, {
// Detect import.meta usage
MetaProperty(path: NodePath<t.MetaProperty>) {
if (
path.node.meta.name === 'import' &&
path.node.property.name === 'meta'
) {
found = true;
path.stop(); // Stop traversal once found
}
},
});

return found;
} catch (error) {
// If we can't parse, assume no import.meta
return false;
}
}

/**
* Detect ESM features that cannot be safely transformed to CommonJS
* These include:
* - Top-level await (no CJS equivalent)
* - import.meta (no CJS equivalent)
*
* Note: import.meta is now supported via polyfills and is no longer unsupported
*
* @param code - The ESM source code to check
* @param filename - The filename for error reporting
Expand All @@ -44,20 +85,6 @@ function detectUnsupportedESMFeatures(

// @ts-expect-error Type mismatch due to @babel/types version in @types/babel__traverse
traverse(ast as t.File, {
// Detect import.meta usage
MetaProperty(path: NodePath<t.MetaProperty>) {
if (
path.node.meta.name === 'import' &&
path.node.property.name === 'meta'
) {
unsupportedFeatures.push({
feature: 'import.meta',
line: path.node.loc?.start.line ?? null,
column: path.node.loc?.start.column ?? null,
});
}
},

// Detect top-level await
AwaitExpression(path: NodePath<t.AwaitExpression>) {
// Check if await is at top level (not inside a function)
Expand Down Expand Up @@ -130,6 +157,45 @@ function detectUnsupportedESMFeatures(
}
}

/**
* Replace esbuild's empty import_meta object with a proper implementation
*
* When esbuild transforms ESM to CJS, it converts `import.meta` to a `const import_meta = {}`.
* This function replaces that empty object with a proper implementation of import.meta properties.
*
* Shims provided:
* - import.meta.url: File URL of the current module
* - import.meta.dirname: Directory path of the current module (Node.js 20.11+)
* - import.meta.filename: File path of the current module (Node.js 20.11+)
*
* Based on approach from tsup and esbuild discussions
* @see https://github.com/egoist/tsup/blob/main/assets/cjs_shims.js
* @see https://github.com/evanw/esbuild/issues/3839
*
* @param code - The transformed CJS code from esbuild
* @returns Code with import_meta properly implemented
*/
function replaceImportMetaObject(code: string): string {
// esbuild generates: const import_meta = {};
// We need to replace this with a proper implementation
// Note: We use getters to ensure values are computed at runtime in the correct context
const shimImplementation = `const import_meta = {
get url() {
return require('url').pathToFileURL(__filename).href;
},
get dirname() {
return __dirname;
},
get filename() {
return __filename;
}
};`;

// Replace esbuild's empty import_meta object with our implementation
// Match: const import_meta = {};
return code.replace(/const import_meta\s*=\s*\{\s*\};/, shimImplementation);
}

/**
* Transform ESM code to CommonJS using esbuild
* This allows ESM modules to be compiled to bytecode via vm.Script
Expand Down Expand Up @@ -185,6 +251,9 @@ export function transformESMtoCJS(
};
}

// Check if code uses import.meta before transformation
const usesImportMeta = hasImportMeta(code);

try {
const result = esbuild.transformSync(code, {
loader: 'js',
Expand All @@ -203,8 +272,14 @@ export function transformESMtoCJS(
};
}

// Inject import.meta shims after esbuild transformation if needed
let finalCode = result.code;
if (usesImportMeta) {
finalCode = replaceImportMetaObject(result.code);
}

return {
code: result.code,
code: finalCode,
isTransformed: true,
};
} catch (error) {
Expand Down
44 changes: 30 additions & 14 deletions test/test-50-esm-unsupported/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@

const path = require('path');
const assert = require('assert');
const { existsSync } = require('fs');
const utils = require('../utils.js');

assert(!module.parent);
assert(__dirname === process.cwd());

const target = process.argv[2] || 'host';

console.log('Testing unsupported ESM features detection...');
console.log('Testing ESM features detection and transformation...');

// Test 1: import.meta detection
console.log('\n=== Test 1: import.meta ===');
// Test 1: import.meta support (should now work without warnings)
console.log('\n=== Test 1: import.meta support ===');
{
const input = './test-import-meta.mjs';
const output = './run-time/test-import-meta.exe';
Expand All @@ -23,19 +24,25 @@ console.log('\n=== Test 1: import.meta ===');
const before = utils.filesBefore(newcomers);
utils.mkdirp.sync(path.dirname(output));

// Capture stdout to check for warnings
// Capture stdout to check that no warnings are emitted
const result = utils.pkg.sync(
['--target', target, '--output', output, input],
['inherit', 'pipe', 'inherit'],
);

// Verify warning was emitted
// Verify NO warning was emitted (import.meta should now be supported)
assert(
result.includes('import.meta') ||
result.includes('Cannot transform ESM module'),
'Should warn about import.meta usage',
!result.includes('import.meta') &&
!result.includes('Cannot transform ESM module'),
'Should NOT warn about import.meta usage (it is now supported)',
);

// Verify the executable was created
assert(existsSync(output), 'Executable should be created successfully');

console.log(
'✓ import.meta support working (no warnings, executable created)',
);
console.log('✓ import.meta detection working');

// Cleanup
utils.filesAfter(before, newcomers);
Expand Down Expand Up @@ -110,24 +117,33 @@ console.log('\n=== Test 4: multiple unsupported features ===');
['inherit', 'pipe', 'inherit'],
);

// Verify multiple warnings were emitted
// Verify warnings were emitted only for truly unsupported features
const hasImportMeta = result.includes('import.meta');
const hasTopLevelAwait = result.includes('top-level await');
const hasForAwaitOf = result.includes('for-await-of');
const hasGeneralWarning = result.includes('Cannot transform ESM module');

// import.meta should NOT trigger a warning anymore (it's now supported)
assert(
!hasImportMeta,
'Should NOT warn about import.meta (it is now supported)',
);

// But top-level await and for-await-of should still warn
assert(
hasImportMeta || hasTopLevelAwait || hasForAwaitOf || hasGeneralWarning,
'Should warn about multiple unsupported features',
hasTopLevelAwait || hasForAwaitOf || hasGeneralWarning,
'Should warn about truly unsupported features (top-level await, for-await-of)',
);

console.log('✓ Multiple features detection working');
console.log(' - import.meta detected:', hasImportMeta);
console.log(' - import.meta detected:', hasImportMeta, '(should be false)');
console.log(' - top-level await detected:', hasTopLevelAwait);
console.log(' - top-level for-await-of detected:', hasForAwaitOf);

// Cleanup
utils.filesAfter(before, newcomers);
}

console.log('\n✅ All unsupported ESM features correctly detected!');
console.log(
'\n✅ All ESM features correctly handled! (import.meta now supported, top-level await/for-await-of still unsupported)',
);
5 changes: 5 additions & 0 deletions test/test-51-esm-import-meta/esm-module/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "test-51-esm-import-meta",
"version": "1.0.0",
"type": "module"
}
34 changes: 34 additions & 0 deletions test/test-51-esm-import-meta/esm-module/test-import-meta-basic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Test file to verify import.meta properties work correctly
console.log('Testing import.meta properties...');

// Test import.meta.url
if (!import.meta.url) {
console.error('FAIL: import.meta.url is not defined');
process.exit(1);
}

if (!import.meta.url.startsWith('file://')) {
console.error('FAIL: import.meta.url should start with file://');
console.error('Got:', import.meta.url);
process.exit(1);
}

console.log('✓ import.meta.url works:', import.meta.url);

// Test import.meta.dirname
if (typeof import.meta.dirname === 'undefined') {
console.error('FAIL: import.meta.dirname is not defined');
process.exit(1);
}

console.log('✓ import.meta.dirname works:', import.meta.dirname);

// Test import.meta.filename
if (typeof import.meta.filename === 'undefined') {
console.error('FAIL: import.meta.filename is not defined');
process.exit(1);
}

console.log('✓ import.meta.filename works:', import.meta.filename);

console.log('\n✅ All import.meta properties work correctly!');
87 changes: 87 additions & 0 deletions test/test-51-esm-import-meta/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/usr/bin/env node

'use strict';

const path = require('path');
const assert = require('assert');
const { spawnSync } = require('child_process');
const utils = require('../utils.js');

assert(!module.parent);
assert(__dirname === process.cwd());

const target = process.argv[2] || 'host';

console.log('Testing import.meta support in packaged executables...');

// Test: Package and run an ESM module that uses import.meta
console.log('\n=== Test: import.meta properties ===');
{
const input = './esm-module/test-import-meta-basic.js';
const output = './run-time/test-import-meta-basic.exe';
const newcomers = ['run-time/test-import-meta-basic.exe'];

const before = utils.filesBefore(newcomers);
utils.mkdirp.sync(path.dirname(output));

// Package the executable
const buildResult = utils.pkg.sync(
['--target', target, '--output', output, input],
['inherit', 'pipe', 'inherit'],
);

// Should NOT warn about import.meta
assert(
!buildResult.includes('import.meta') &&
!buildResult.includes('Cannot transform ESM module'),
'Should NOT warn about import.meta usage',
);

console.log('✓ Packaging succeeded without warnings');

// Run the executable and check output
const runResult = spawnSync(output, [], {
encoding: 'utf8',
timeout: 10000,
});

console.log('Executable output:');
console.log(runResult.stdout);

if (runResult.stderr) {
console.log('Executable stderr:');
console.log(runResult.stderr);
}

assert(
runResult.status === 0,
`Executable should exit with code 0, got ${runResult.status}`,
);

assert(
runResult.stdout.includes('import.meta.url works'),
'Should show import.meta.url working',
);

assert(
runResult.stdout.includes('import.meta.dirname works'),
'Should show import.meta.dirname working',
);

assert(
runResult.stdout.includes('import.meta.filename works'),
'Should show import.meta.filename working',
);

assert(
runResult.stdout.includes('All import.meta properties work correctly'),
'Should show success message',
);

console.log('✓ Executable runs correctly with import.meta support');

// Cleanup
utils.filesAfter(before, newcomers);
}

console.log('\n✅ All import.meta tests passed!');