diff --git a/README.md b/README.md index 7de68fb6..c17a2ace 100644 --- a/README.md +++ b/README.md @@ -422,6 +422,37 @@ await exec(['app.js', '--target', 'host', '--output', 'app.exe']); // do something with app.exe, run, test, upload, deploy, etc ``` +## ECMAScript Modules (ESM) Support + +Starting from version **6.13.0**, pkg has improved support for ECMAScript Modules (ESM). Most ESM features are now automatically transformed to CommonJS during the packaging process. + +### Supported ESM Features + +The following ESM features are now supported and will work in your packaged executables: + +- **`import` and `export` statements** - Automatically transformed to `require()` and `module.exports` +- **Top-level `await`** - Wrapped in an async IIFE to work in CommonJS context +- **Top-level `for await...of`** - Wrapped in an async IIFE to work in CommonJS context +- **`import.meta.url`** - Polyfilled to provide the file URL of the current module +- **`import.meta.dirname`** - Polyfilled to provide the directory path (Node.js 20.11+ property) +- **`import.meta.filename`** - Polyfilled to provide the file path (Node.js 20.11+ property) + +### Known Limitations + +While most ESM features work, there are some limitations to be aware of: + +1. **Modules with both top-level await and exports**: Modules that use `export` statements alongside top-level `await` cannot be wrapped in an async IIFE and will not be transformed to bytecode. These modules will be included as source code instead. + +2. **`import.meta.main`** and other custom properties: Only the standard `import.meta` properties listed above are polyfilled. Custom properties added by your code or other tools may not work as expected. + +3. **Dynamic imports**: `import()` expressions work but may have limitations depending on the module being imported. + +### Best Practices + +- For entry point scripts (the main file you're packaging), feel free to use top-level await +- For library modules that will be imported by other code, avoid using both exports and top-level await together +- Test your packaged executable to ensure all ESM features work as expected in your specific use case + ## Use custom Node.js binary In case you want to use custom node binary, you can set `PKG_NODE_PATH` environment variable to the path of the node binary you want to use and `pkg` will use it instead of the default one. @@ -535,43 +566,45 @@ or Note: make sure not to use --debug flag in production. ### Injecting Windows Executable Metadata After Packaging -Executables created with `pkg` are based on a Node.js binary and, by default, -inherit its embedded metadata – such as version number, product name, company -name, icon, and description. This can be misleading or unpolished in + +Executables created with `pkg` are based on a Node.js binary and, by default, +inherit its embedded metadata – such as version number, product name, company +name, icon, and description. This can be misleading or unpolished in distributed applications. There are two ways to customize the metadata of the resulting `.exe`: + 1. **Use a custom Node.js binary** with your own metadata already embedded. See: [Use Custom Node.js Binary](#use-custom-nodejs-binary) -2. **Post-process the generated executable** using - [`resedit`](https://www.npmjs.com/package/resedit), a Node.js-compatible - tool for modifying Windows executable resources. This allows injecting +2. **Post-process the generated executable** using + [`resedit`](https://www.npmjs.com/package/resedit), a Node.js-compatible + tool for modifying Windows executable resources. This allows injecting correct version info, icons, copyright, and more. This section focuses on the second approach: post-processing the packaged -binary using [`resedit`](https://www.npmjs.com/package/resedit). +binary using [`resedit`](https://www.npmjs.com/package/resedit). > ⚠️ Other tools may corrupt the executable, resulting in runtime errors such as -> `Pkg: Error reading from file.` – +> `Pkg: Error reading from file.` – > [`resedit`](https://www.npmjs.com/package/resedit) has proven to work reliably > with `pkg`-generated binaries. Below is a working example for post-processing an `.exe` file using the Node.js API of [`resedit`](https://www.npmjs.com/package/resedit): ```ts -import * as ResEdit from "resedit"; -import * as fs from "fs"; -import * as path from "path"; +import * as ResEdit from 'resedit'; +import * as fs from 'fs'; +import * as path from 'path'; // Set your inputs: -const exePath = "dist/my-tool.exe"; // Path to the generated executable -const outputPath = exePath; // Overwrite or use a different path -const version = "1.2.3"; // Your application version +const exePath = 'dist/my-tool.exe'; // Path to the generated executable +const outputPath = exePath; // Overwrite or use a different path +const version = '1.2.3'; // Your application version -const lang = 1033; // en-US -const codepage = 1200; // Unicode +const lang = 1033; // en-US +const codepage = 1200; // Unicode const exeData = fs.readFileSync(exePath); const exe = ResEdit.NtExecutable.from(exeData); @@ -580,19 +613,22 @@ const res = ResEdit.NtExecutableResource.from(exe); const viList = ResEdit.Resource.VersionInfo.fromEntries(res.entries); const vi = viList[0]; -const [major, minor, patch] = version.split("."); +const [major, minor, patch] = version.split('.'); vi.setFileVersion(Number(major), Number(minor), Number(patch), 0, lang); vi.setProductVersion(Number(major), Number(minor), Number(patch), 0, lang); -vi.setStringValues({ lang, codepage }, { - FileDescription: "ACME CLI Tool", - ProductName: "ACME Application", - CompanyName: "ACME Corporation", - ProductVersion: version, - FileVersion: version, - OriginalFilename: path.basename(exePath), - LegalCopyright: `© ${new Date().getFullYear()} ACME Corporation` -}); +vi.setStringValues( + { lang, codepage }, + { + FileDescription: 'ACME CLI Tool', + ProductName: 'ACME Application', + CompanyName: 'ACME Corporation', + ProductVersion: version, + FileVersion: version, + OriginalFilename: path.basename(exePath), + LegalCopyright: `© ${new Date().getFullYear()} ACME Corporation`, + }, +); vi.outputToResourceEntries(res.entries); res.outputResource(exe); @@ -610,6 +646,7 @@ The following command examples inject an icon and metadata into the executable `dist/bin/app.exe`. - **Example (PowerShell on Windows)** + ```powershell npx resedit dist/bin/app.exe dist/bin/app_with_metadata.exe ` --icon 1,dist/favicon.ico ` diff --git a/lib/esm-transformer.ts b/lib/esm-transformer.ts index 52c941d0..130a2a48 100644 --- a/lib/esm-transformer.ts +++ b/lib/esm-transformer.ts @@ -16,6 +16,15 @@ interface UnsupportedFeature { column: number | null; } +/** + * Wrapper for top-level await support + * Wraps code in an async IIFE to allow top-level await in CommonJS + */ +const ASYNC_IIFE_WRAPPER = { + prefix: '(async () => {\n', + suffix: '\n})()', +}; + /** * Check if code contains import.meta usage * @@ -57,20 +66,23 @@ function hasImportMeta(code: string): boolean { } /** - * Detect ESM features that cannot be safely transformed to CommonJS + * Detect ESM features that require special handling or cannot be transformed * These include: - * - Top-level await (no CJS equivalent) + * - Top-level await (can be handled with async IIFE wrapper) * - * Note: import.meta is now supported via polyfills and is no longer unsupported + * Note: import.meta is now supported via polyfills and is no longer in the unsupported list * * @param code - The ESM source code to check * @param filename - The filename for error reporting - * @returns Array of unsupported features found, or null if parse fails + * @returns Object with arrays of features requiring special handling */ -function detectUnsupportedESMFeatures( +function detectESMFeatures( code: string, filename: string, -): UnsupportedFeature[] | null { +): { + topLevelAwait: UnsupportedFeature[]; + unsupportedFeatures: UnsupportedFeature[]; +} | null { try { const ast = babel.parse(code, { sourceType: 'module', @@ -81,11 +93,12 @@ function detectUnsupportedESMFeatures( return null; } + const topLevelAwait: UnsupportedFeature[] = []; const unsupportedFeatures: UnsupportedFeature[] = []; // @ts-expect-error Type mismatch due to @babel/types version in @types/babel__traverse traverse(ast as t.File, { - // Detect top-level await + // Detect top-level await - can be handled with async IIFE wrapper AwaitExpression(path: NodePath) { // Check if await is at top level (not inside a function) let parent: NodePath | null = path.parentPath; @@ -106,7 +119,7 @@ function detectUnsupportedESMFeatures( } if (isTopLevel) { - unsupportedFeatures.push({ + topLevelAwait.push({ feature: 'top-level await', line: path.node.loc?.start.line ?? null, column: path.node.loc?.start.column ?? null, @@ -114,7 +127,7 @@ function detectUnsupportedESMFeatures( } }, - // Detect for-await-of at top level + // Detect for-await-of at top level - can be handled with async IIFE wrapper ForOfStatement(path: NodePath) { if (path.node.await) { let parent: NodePath | null = path.parentPath; @@ -135,7 +148,7 @@ function detectUnsupportedESMFeatures( } if (isTopLevel) { - unsupportedFeatures.push({ + topLevelAwait.push({ feature: 'top-level for-await-of', line: path.node.loc?.start.line ?? null, column: path.node.loc?.start.column ?? null, @@ -145,11 +158,11 @@ function detectUnsupportedESMFeatures( }, }); - return unsupportedFeatures; + return { topLevelAwait, unsupportedFeatures }; } catch (error) { // If we can't parse, return null to let the transform attempt proceed log.debug( - `Could not parse ${filename} to detect unsupported ESM features: ${ + `Could not parse ${filename} to detect ESM features: ${ error instanceof Error ? error.message : String(error) }`, ); @@ -218,11 +231,16 @@ export function transformESMtoCJS( }; } - // First, check for unsupported ESM features that can't be safely transformed - const unsupportedFeatures = detectUnsupportedESMFeatures(code, filename); + // First, check for ESM features that need special handling + const esmFeatures = detectESMFeatures(code, filename); - if (unsupportedFeatures && unsupportedFeatures.length > 0) { - const featureList = unsupportedFeatures + // Handle truly unsupported features (import.meta) + if ( + esmFeatures && + esmFeatures.unsupportedFeatures && + esmFeatures.unsupportedFeatures.length > 0 + ) { + const featureList = esmFeatures.unsupportedFeatures .map((f) => { const location = f.line !== null ? ` at line ${f.line}` : ''; return ` - ${f.feature}${location}`; @@ -251,18 +269,123 @@ export function transformESMtoCJS( }; } + // Check if we need to wrap in async IIFE for top-level await + const hasTopLevelAwait = + esmFeatures && + esmFeatures.topLevelAwait && + esmFeatures.topLevelAwait.length > 0; + + let codeToTransform = code; + + // If top-level await is detected, we need to wrap in async IIFE + // But we must handle imports and exports specially + if (hasTopLevelAwait) { + try { + // Parse the code to check for exports and collect imports + const ast = babel.parse(code, { + sourceType: 'module', + plugins: [], + }); + + let hasExports = false; + const codeLines = code.split('\n'); + const importLineIndices = new Set(); + + // @ts-expect-error Type mismatch due to @babel/types version + traverse(ast as t.File, { + ExportNamedDeclaration() { + hasExports = true; + }, + ExportDefaultDeclaration() { + hasExports = true; + }, + ExportAllDeclaration() { + hasExports = true; + }, + ImportDeclaration(path: NodePath) { + // Track import statements by line number + const { loc } = path.node; + if (loc) { + const { start, end } = loc; + for (let i = start.line; i <= end.line; i += 1) { + importLineIndices.add(i - 1); // Convert to 0-based index + } + } + }, + }); + + if (hasExports) { + // If the file has exports, we can't wrap it in an IIFE + // because exports need to be synchronous and at the top level. + log.warn( + `Module ${filename} has both top-level await and export statements. ` + + `This combination cannot be safely transformed to CommonJS in pkg's ESM transformer. ` + + `The original source code will be used as-is; depending on the package visibility and build configuration, ` + + `bytecode compilation may fail and the module may need to be loaded from source or be skipped.`, + ); + return { + code, + isTransformed: false, + }; + } + + // If there are imports, extract them to keep outside the async IIFE + if (importLineIndices.size > 0) { + const imports: string[] = []; + const rest: string[] = []; + + codeLines.forEach((line, index) => { + if (importLineIndices.has(index)) { + imports.push(line); + } else { + rest.push(line); + } + }); + + // Reconstruct: imports at top, then async IIFE wrapping the rest + codeToTransform = `${imports.join('\n')}\n${ASYNC_IIFE_WRAPPER.prefix}${rest.join('\n')}${ASYNC_IIFE_WRAPPER.suffix}`; + + log.debug( + `Wrapping ${filename} in async IIFE with imports extracted to top level`, + ); + } else { + // No imports, wrap everything + codeToTransform = + ASYNC_IIFE_WRAPPER.prefix + code + ASYNC_IIFE_WRAPPER.suffix; + + log.debug( + `Wrapping ${filename} in async IIFE to support top-level await`, + ); + } + } catch (parseError) { + // If we can't parse, wrap everything and hope for the best + codeToTransform = + ASYNC_IIFE_WRAPPER.prefix + code + ASYNC_IIFE_WRAPPER.suffix; + + log.warn( + `Could not parse ${filename} to detect exports/imports (${ + parseError instanceof Error ? parseError.message : String(parseError) + }). ` + + `Wrapping entire code in async IIFE - this may fail if the module has export or import statements.`, + ); + } + } + // Check if code uses import.meta before transformation const usesImportMeta = hasImportMeta(code); try { - const result = esbuild.transformSync(code, { + // Build esbuild options + const esbuildOptions: esbuild.TransformOptions = { loader: 'js', format: 'cjs', target: 'node18', sourcemap: false, minify: false, keepNames: true, - }); + }; + + const result = esbuild.transformSync(codeToTransform, esbuildOptions); if (!result || !result.code) { log.warn(`esbuild transform returned no code for ${filename}`); diff --git a/lib/packer.ts b/lib/packer.ts index 6230060b..2a6787b2 100644 --- a/lib/packer.ts +++ b/lib/packer.ts @@ -67,7 +67,12 @@ export default function packer({ }: PackerOptions) { const stripes: Stripe[] = []; - for (const snap in records) { + // If the entrypoint was a .mjs file that got transformed, update its extension + if (records[entrypoint]?.wasTransformed && entrypoint.endsWith('.mjs')) { + entrypoint = `${entrypoint.slice(0, -4)}.js`; + } + + for (let snap in records) { if (records[snap]) { const record = records[snap]; const { file } = record; @@ -76,6 +81,12 @@ export default function packer({ continue; } + // If .mjs file was transformed to CJS, rename it to .js in the snapshot + // This prevents Node.js from treating it as an ES module + if (record.wasTransformed && snap.endsWith('.mjs')) { + snap = `${snap.slice(0, -4)}.js`; + } + assert(record[STORE_STAT], 'packer: no STORE_STAT'); assert( record[STORE_BLOB] || diff --git a/lib/types.ts b/lib/types.ts index 66039569..9699ae23 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -3,6 +3,7 @@ import type { log } from './log'; export interface FileRecord { file: string; body?: Buffer | string; + wasTransformed?: boolean; // Track if .mjs was transformed to CJS // This could be improved a bit. making this stricter opens up a lot of // changes that need to be made throughout the code though // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/lib/walker.ts b/lib/walker.ts index fc28cdd4..3442f349 100644 --- a/lib/walker.ts +++ b/lib/walker.ts @@ -1099,6 +1099,10 @@ class Walker { ); if (result.isTransformed) { record.body = Buffer.from(result.code, 'utf8'); + // Mark .mjs files as transformed so packer can rename them to .js + if (record.file.endsWith('.mjs')) { + record.wasTransformed = true; + } } } catch (error) { const message = diff --git a/test/test-50-esm-unsupported/main.js b/test/test-50-esm-unsupported/main.js index 1859c777..fc31c3f4 100644 --- a/test/test-50-esm-unsupported/main.js +++ b/test/test-50-esm-unsupported/main.js @@ -48,7 +48,7 @@ console.log('\n=== Test 1: import.meta support ==='); utils.filesAfter(before, newcomers); } -// Test 2: top-level await detection +// Test 2: top-level await support (should now work!) console.log('\n=== Test 2: top-level await ==='); { const input = './test-top-level-await.mjs'; @@ -58,24 +58,28 @@ console.log('\n=== Test 2: top-level await ==='); const before = utils.filesBefore(newcomers); utils.mkdirp.sync(path.dirname(output)); - const result = utils.pkg.sync( + // Package the file with top-level await + utils.pkg.sync( ['--target', target, '--output', output, input], ['inherit', 'pipe', 'inherit'], ); - // Verify warning was emitted + // Run the executable and verify it works + const execResult = utils.spawn.sync('./' + path.basename(output), [], { + cwd: path.dirname(output), + }); + assert( - result.includes('top-level await') || - result.includes('Cannot transform ESM module'), - 'Should warn about top-level await usage', + execResult.includes('Top-level await completed'), + 'Should successfully execute top-level await code', ); - console.log('✓ top-level await detection working'); + console.log('✓ top-level await now supported'); // Cleanup utils.filesAfter(before, newcomers); } -// Test 3: top-level for-await-of detection +// Test 3: top-level for-await-of support (should now work!) console.log('\n=== Test 3: top-level for-await-of ==='); { const input = './test-for-await-of.mjs'; @@ -85,25 +89,29 @@ console.log('\n=== Test 3: top-level for-await-of ==='); const before = utils.filesBefore(newcomers); utils.mkdirp.sync(path.dirname(output)); - const result = utils.pkg.sync( + // Package the file with top-level for-await-of + utils.pkg.sync( ['--target', target, '--output', output, input], ['inherit', 'pipe', 'inherit'], ); - // Verify warning was emitted + // Run the executable and verify it works + const execResult = utils.spawn.sync('./' + path.basename(output), [], { + cwd: path.dirname(output), + }); + assert( - result.includes('for-await-of') || - result.includes('Cannot transform ESM module'), - 'Should warn about top-level for-await-of usage', + execResult.includes('Top-level for-await-of completed'), + 'Should successfully execute top-level for-await-of code', ); - console.log('✓ top-level for-await-of detection working'); + console.log('✓ top-level for-await-of now supported'); // Cleanup utils.filesAfter(before, newcomers); } -// Test 4: multiple unsupported features detection -console.log('\n=== Test 4: multiple unsupported features ==='); +// Test 4: multiple ESM features working together +console.log('\n=== Test 4: multiple ESM features ==='); { const input = './test-multiple-features.mjs'; const output = './run-time/test-multiple.exe'; @@ -112,38 +120,38 @@ console.log('\n=== Test 4: multiple unsupported features ==='); const before = utils.filesBefore(newcomers); utils.mkdirp.sync(path.dirname(output)); - const result = utils.pkg.sync( + utils.pkg.sync( ['--target', target, '--output', output, input], ['inherit', 'pipe', 'inherit'], ); - // 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) + // Verify executable was created successfully (all features now supported) assert( - !hasImportMeta, - 'Should NOT warn about import.meta (it is now supported)', + existsSync(output), + 'Executable should be created with all ESM features', ); - // But top-level await and for-await-of should still warn + // Run the executable and verify it works + const execResult = utils.spawn.sync(`./${path.basename(output)}`, [], { + cwd: path.dirname(output), + }); + assert( - hasTopLevelAwait || hasForAwaitOf || hasGeneralWarning, - 'Should warn about truly unsupported features (top-level await, for-await-of)', + execResult.includes('ok with multiple features'), + 'Should execute successfully with all ESM features', ); - console.log('✓ Multiple features detection working'); - 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); + console.log( + '✓ Multiple ESM features working together (import.meta + top-level await + for-await-of)', + ); // Cleanup utils.filesAfter(before, newcomers); } +console.log('\n✅ ESM features test completed!'); +console.log(' - import.meta is now supported with polyfills'); +console.log(' - top-level await is now supported with async IIFE wrapper'); console.log( - '\n✅ All ESM features correctly handled! (import.meta now supported, top-level await/for-await-of still unsupported)', + ' - top-level for-await-of is now supported with async IIFE wrapper', ); diff --git a/test/test-50-esm-unsupported/test-for-await-of.mjs b/test/test-50-esm-unsupported/test-for-await-of.mjs index ec97847d..78d5924a 100644 --- a/test/test-50-esm-unsupported/test-for-await-of.mjs +++ b/test/test-50-esm-unsupported/test-for-await-of.mjs @@ -5,13 +5,10 @@ async function* generateNumbers() { yield 3; } -// Top-level for-await-of - not allowed in CJS +// Top-level for-await-of - now supported with async IIFE wrapper for await (const num of generateNumbers()) { console.log('Number:', num); } console.log('Top-level for-await-of completed'); -export default function test() { - return 'ok'; -} diff --git a/test/test-50-esm-unsupported/test-multiple-features.mjs b/test/test-50-esm-unsupported/test-multiple-features.mjs index f3f5d809..c4fe33a6 100644 --- a/test/test-50-esm-unsupported/test-multiple-features.mjs +++ b/test/test-50-esm-unsupported/test-multiple-features.mjs @@ -17,6 +17,5 @@ for await (const item of generateItems()) { console.log('Item:', item); } -export default function test() { - return 'ok with multiple features'; -} +console.log('ok with multiple features'); + diff --git a/test/test-50-esm-unsupported/test-top-level-await.mjs b/test/test-50-esm-unsupported/test-top-level-await.mjs index de420931..429b97eb 100644 --- a/test/test-50-esm-unsupported/test-top-level-await.mjs +++ b/test/test-50-esm-unsupported/test-top-level-await.mjs @@ -1,11 +1,8 @@ // Test file with top-level await const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); -// Top-level await - not allowed in CJS +// Top-level await - now supported with async IIFE wrapper await delay(100); console.log('Top-level await completed'); -export default function test() { - return 'ok'; -} diff --git a/test/test-50-top-level-await/main.js b/test/test-50-top-level-await/main.js new file mode 100644 index 00000000..dd2983e8 --- /dev/null +++ b/test/test-50-top-level-await/main.js @@ -0,0 +1,83 @@ +#!/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 host = 'node' + utils.getNodeMajorVersion(); +const target = process.argv[2] || host; + +console.log('Testing top-level await support with esbuild...'); + +// Test 1: Top-level await without imports +console.log('\n=== Test 1: Top-level await without imports ==='); +{ + const input = './test-x-index.mjs'; + const output = './test-output.exe'; + + // Package the file with top-level await + utils.pkg.sync(['--target', target, '--output', output, input]); + + // Run the packaged executable + const right = utils.spawn.sync('./' + path.basename(output), [], { + cwd: path.dirname(output), + }); + + // Expected output + const expected = + [ + 'Top-level await completed', + 'Number: 1', + 'Number: 2', + 'Number: 3', + 'For-await-of completed', + ].join('\n') + '\n'; + + assert.strictEqual(right, expected, 'Top-level await should work correctly'); + + console.log('✅ Top-level await test passed!'); + + utils.vacuum.sync(output); +} + +// Test 2: Top-level await WITH imports +console.log('\n=== Test 2: Top-level await with imports ==='); +{ + const input = './test-x-with-imports.mjs'; + const output = './test-output-imports.exe'; + + // Package the file with top-level await and imports + utils.pkg.sync(['--target', target, '--output', output, input]); + + // Run the packaged executable + const right = utils.spawn.sync('./' + path.basename(output), [], { + cwd: path.dirname(output), + }); + + // Expected output + const expected = + [ + 'Top-level await with imports completed', + 'Item: item1', + 'Item: item2', + 'Item: item3', + 'For-await-of with imports completed', + ].join('\n') + '\n'; + + assert.strictEqual( + right, + expected, + 'Top-level await with imports should work correctly', + ); + + console.log('✅ Top-level await with imports test passed!'); + + utils.vacuum.sync(output); +} + +console.log('\n✅ All top-level await tests passed!'); diff --git a/test/test-50-top-level-await/test-x-index.mjs b/test/test-50-top-level-await/test-x-index.mjs new file mode 100644 index 00000000..80e34a7f --- /dev/null +++ b/test/test-50-top-level-await/test-x-index.mjs @@ -0,0 +1,20 @@ +// Test file with top-level await +const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +// Top-level await should work with async IIFE wrapper +await delay(100); + +console.log('Top-level await completed'); + +// Also test for-await-of at top level +async function* generateNumbers() { + yield 1; + yield 2; + yield 3; +} + +for await (const num of generateNumbers()) { + console.log(`Number: ${num}`); +} + +console.log('For-await-of completed'); diff --git a/test/test-50-top-level-await/test-x-with-imports.mjs b/test/test-50-top-level-await/test-x-with-imports.mjs new file mode 100644 index 00000000..8aeb925f --- /dev/null +++ b/test/test-50-top-level-await/test-x-with-imports.mjs @@ -0,0 +1,20 @@ +// Test file with top-level await AND import statements +import { setTimeout } from 'timers/promises'; + +// Top-level await with imports should work +await setTimeout(100); + +console.log('Top-level await with imports completed'); + +// Also test for-await-of at top level with imports +async function* generateData() { + yield 'item1'; + yield 'item2'; + yield 'item3'; +} + +for await (const item of generateData()) { + console.log(`Item: ${item}`); +} + +console.log('For-await-of with imports completed');