@yao-pkg/pkg 6.13.0 → 6.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -422,6 +422,37 @@ await exec(['app.js', '--target', 'host', '--output', 'app.exe']);
422
422
  // do something with app.exe, run, test, upload, deploy, etc
423
423
  ```
424
424
 
425
+ ## ECMAScript Modules (ESM) Support
426
+
427
+ 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.
428
+
429
+ ### Supported ESM Features
430
+
431
+ The following ESM features are now supported and will work in your packaged executables:
432
+
433
+ - **`import` and `export` statements** - Automatically transformed to `require()` and `module.exports`
434
+ - **Top-level `await`** - Wrapped in an async IIFE to work in CommonJS context
435
+ - **Top-level `for await...of`** - Wrapped in an async IIFE to work in CommonJS context
436
+ - **`import.meta.url`** - Polyfilled to provide the file URL of the current module
437
+ - **`import.meta.dirname`** - Polyfilled to provide the directory path (Node.js 20.11+ property)
438
+ - **`import.meta.filename`** - Polyfilled to provide the file path (Node.js 20.11+ property)
439
+
440
+ ### Known Limitations
441
+
442
+ While most ESM features work, there are some limitations to be aware of:
443
+
444
+ 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.
445
+
446
+ 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.
447
+
448
+ 3. **Dynamic imports**: `import()` expressions work but may have limitations depending on the module being imported.
449
+
450
+ ### Best Practices
451
+
452
+ - For entry point scripts (the main file you're packaging), feel free to use top-level await
453
+ - For library modules that will be imported by other code, avoid using both exports and top-level await together
454
+ - Test your packaged executable to ensure all ESM features work as expected in your specific use case
455
+
425
456
  ## Use custom Node.js binary
426
457
 
427
458
  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
535
566
  Note: make sure not to use --debug flag in production.
536
567
 
537
568
  ### Injecting Windows Executable Metadata After Packaging
538
- Executables created with `pkg` are based on a Node.js binary and, by default,
539
- inherit its embedded metadata such as version number, product name, company
540
- name, icon, and description. This can be misleading or unpolished in
569
+
570
+ Executables created with `pkg` are based on a Node.js binary and, by default,
571
+ inherit its embedded metadata such as version number, product name, company
572
+ name, icon, and description. This can be misleading or unpolished in
541
573
  distributed applications.
542
574
 
543
575
  There are two ways to customize the metadata of the resulting `.exe`:
576
+
544
577
  1. **Use a custom Node.js binary** with your own metadata already embedded.
545
578
  See: [Use Custom Node.js Binary](#use-custom-nodejs-binary)
546
579
 
547
- 2. **Post-process the generated executable** using
548
- [`resedit`](https://www.npmjs.com/package/resedit), a Node.js-compatible
549
- tool for modifying Windows executable resources. This allows injecting
580
+ 2. **Post-process the generated executable** using
581
+ [`resedit`](https://www.npmjs.com/package/resedit), a Node.js-compatible
582
+ tool for modifying Windows executable resources. This allows injecting
550
583
  correct version info, icons, copyright,
551
584
  and more.
552
585
 
553
586
  This section focuses on the second approach: post-processing the packaged
554
- binary using [`resedit`](https://www.npmjs.com/package/resedit).
587
+ binary using [`resedit`](https://www.npmjs.com/package/resedit).
555
588
 
556
589
  > ⚠️ Other tools may corrupt the executable, resulting in runtime errors such as
557
- > `Pkg: Error reading from file.` –
590
+ > `Pkg: Error reading from file.` –
558
591
  > [`resedit`](https://www.npmjs.com/package/resedit) has proven to work reliably
559
592
  > with `pkg`-generated binaries.
560
593
 
561
594
  Below is a working example for post-processing an `.exe` file using the Node.js API of [`resedit`](https://www.npmjs.com/package/resedit):
562
595
 
563
596
  ```ts
564
- import * as ResEdit from "resedit";
565
- import * as fs from "fs";
566
- import * as path from "path";
597
+ import * as ResEdit from 'resedit';
598
+ import * as fs from 'fs';
599
+ import * as path from 'path';
567
600
 
568
601
  // Set your inputs:
569
- const exePath = "dist/my-tool.exe"; // Path to the generated executable
570
- const outputPath = exePath; // Overwrite or use a different path
571
- const version = "1.2.3"; // Your application version
602
+ const exePath = 'dist/my-tool.exe'; // Path to the generated executable
603
+ const outputPath = exePath; // Overwrite or use a different path
604
+ const version = '1.2.3'; // Your application version
572
605
 
573
- const lang = 1033; // en-US
574
- const codepage = 1200; // Unicode
606
+ const lang = 1033; // en-US
607
+ const codepage = 1200; // Unicode
575
608
 
576
609
  const exeData = fs.readFileSync(exePath);
577
610
  const exe = ResEdit.NtExecutable.from(exeData);
@@ -580,19 +613,22 @@ const res = ResEdit.NtExecutableResource.from(exe);
580
613
  const viList = ResEdit.Resource.VersionInfo.fromEntries(res.entries);
581
614
  const vi = viList[0];
582
615
 
583
- const [major, minor, patch] = version.split(".");
616
+ const [major, minor, patch] = version.split('.');
584
617
  vi.setFileVersion(Number(major), Number(minor), Number(patch), 0, lang);
585
618
  vi.setProductVersion(Number(major), Number(minor), Number(patch), 0, lang);
586
619
 
587
- vi.setStringValues({ lang, codepage }, {
588
- FileDescription: "ACME CLI Tool",
589
- ProductName: "ACME Application",
590
- CompanyName: "ACME Corporation",
591
- ProductVersion: version,
592
- FileVersion: version,
593
- OriginalFilename: path.basename(exePath),
594
- LegalCopyright: `© ${new Date().getFullYear()} ACME Corporation`
595
- });
620
+ vi.setStringValues(
621
+ { lang, codepage },
622
+ {
623
+ FileDescription: 'ACME CLI Tool',
624
+ ProductName: 'ACME Application',
625
+ CompanyName: 'ACME Corporation',
626
+ ProductVersion: version,
627
+ FileVersion: version,
628
+ OriginalFilename: path.basename(exePath),
629
+ LegalCopyright: `© ${new Date().getFullYear()} ACME Corporation`,
630
+ },
631
+ );
596
632
 
597
633
  vi.outputToResourceEntries(res.entries);
598
634
  res.outputResource(exe);
@@ -610,6 +646,7 @@ The following command examples inject an icon and metadata into the executable
610
646
  `dist/bin/app.exe`.
611
647
 
612
648
  - **Example (PowerShell on Windows)**
649
+
613
650
  ```powershell
614
651
  npx resedit dist/bin/app.exe dist/bin/app_with_metadata.exe `
615
652
  --icon 1,dist/favicon.ico `
@@ -33,39 +33,72 @@ const esbuild = __importStar(require("esbuild"));
33
33
  const log_1 = require("./log");
34
34
  const common_1 = require("./common");
35
35
  /**
36
- * Detect ESM features that cannot be safely transformed to CommonJS
37
- * These include:
38
- * - Top-level await (no CJS equivalent)
39
- * - import.meta (no CJS equivalent)
36
+ * Wrapper for top-level await support
37
+ * Wraps code in an async IIFE to allow top-level await in CommonJS
38
+ */
39
+ const ASYNC_IIFE_WRAPPER = {
40
+ prefix: '(async () => {\n',
41
+ suffix: '\n})()',
42
+ };
43
+ /**
44
+ * Check if code contains import.meta usage
40
45
  *
41
46
  * @param code - The ESM source code to check
42
- * @param filename - The filename for error reporting
43
- * @returns Array of unsupported features found, or null if parse fails
47
+ * @returns true if import.meta is used, false otherwise
44
48
  */
45
- function detectUnsupportedESMFeatures(code, filename) {
49
+ function hasImportMeta(code) {
46
50
  try {
47
51
  const ast = babel.parse(code, {
48
52
  sourceType: 'module',
49
53
  plugins: [],
50
54
  });
51
55
  if (!ast) {
52
- return null;
56
+ return false;
53
57
  }
54
- const unsupportedFeatures = [];
58
+ let found = false;
55
59
  // @ts-expect-error Type mismatch due to @babel/types version in @types/babel__traverse
56
60
  (0, traverse_1.default)(ast, {
57
61
  // Detect import.meta usage
58
62
  MetaProperty(path) {
59
63
  if (path.node.meta.name === 'import' &&
60
64
  path.node.property.name === 'meta') {
61
- unsupportedFeatures.push({
62
- feature: 'import.meta',
63
- line: path.node.loc?.start.line ?? null,
64
- column: path.node.loc?.start.column ?? null,
65
- });
65
+ found = true;
66
+ path.stop(); // Stop traversal once found
66
67
  }
67
68
  },
68
- // Detect top-level await
69
+ });
70
+ return found;
71
+ }
72
+ catch (error) {
73
+ // If we can't parse, assume no import.meta
74
+ return false;
75
+ }
76
+ }
77
+ /**
78
+ * Detect ESM features that require special handling or cannot be transformed
79
+ * These include:
80
+ * - Top-level await (can be handled with async IIFE wrapper)
81
+ *
82
+ * Note: import.meta is now supported via polyfills and is no longer in the unsupported list
83
+ *
84
+ * @param code - The ESM source code to check
85
+ * @param filename - The filename for error reporting
86
+ * @returns Object with arrays of features requiring special handling
87
+ */
88
+ function detectESMFeatures(code, filename) {
89
+ try {
90
+ const ast = babel.parse(code, {
91
+ sourceType: 'module',
92
+ plugins: [],
93
+ });
94
+ if (!ast) {
95
+ return null;
96
+ }
97
+ const topLevelAwait = [];
98
+ const unsupportedFeatures = [];
99
+ // @ts-expect-error Type mismatch due to @babel/types version in @types/babel__traverse
100
+ (0, traverse_1.default)(ast, {
101
+ // Detect top-level await - can be handled with async IIFE wrapper
69
102
  AwaitExpression(path) {
70
103
  // Check if await is at top level (not inside a function)
71
104
  let parent = path.parentPath;
@@ -82,14 +115,14 @@ function detectUnsupportedESMFeatures(code, filename) {
82
115
  parent = parent.parentPath;
83
116
  }
84
117
  if (isTopLevel) {
85
- unsupportedFeatures.push({
118
+ topLevelAwait.push({
86
119
  feature: 'top-level await',
87
120
  line: path.node.loc?.start.line ?? null,
88
121
  column: path.node.loc?.start.column ?? null,
89
122
  });
90
123
  }
91
124
  },
92
- // Detect for-await-of at top level
125
+ // Detect for-await-of at top level - can be handled with async IIFE wrapper
93
126
  ForOfStatement(path) {
94
127
  if (path.node.await) {
95
128
  let parent = path.parentPath;
@@ -106,7 +139,7 @@ function detectUnsupportedESMFeatures(code, filename) {
106
139
  parent = parent.parentPath;
107
140
  }
108
141
  if (isTopLevel) {
109
- unsupportedFeatures.push({
142
+ topLevelAwait.push({
110
143
  feature: 'top-level for-await-of',
111
144
  line: path.node.loc?.start.line ?? null,
112
145
  column: path.node.loc?.start.column ?? null,
@@ -115,14 +148,51 @@ function detectUnsupportedESMFeatures(code, filename) {
115
148
  }
116
149
  },
117
150
  });
118
- return unsupportedFeatures;
151
+ return { topLevelAwait, unsupportedFeatures };
119
152
  }
120
153
  catch (error) {
121
154
  // If we can't parse, return null to let the transform attempt proceed
122
- log_1.log.debug(`Could not parse ${filename} to detect unsupported ESM features: ${error instanceof Error ? error.message : String(error)}`);
155
+ log_1.log.debug(`Could not parse ${filename} to detect ESM features: ${error instanceof Error ? error.message : String(error)}`);
123
156
  return null;
124
157
  }
125
158
  }
159
+ /**
160
+ * Replace esbuild's empty import_meta object with a proper implementation
161
+ *
162
+ * When esbuild transforms ESM to CJS, it converts `import.meta` to a `const import_meta = {}`.
163
+ * This function replaces that empty object with a proper implementation of import.meta properties.
164
+ *
165
+ * Shims provided:
166
+ * - import.meta.url: File URL of the current module
167
+ * - import.meta.dirname: Directory path of the current module (Node.js 20.11+)
168
+ * - import.meta.filename: File path of the current module (Node.js 20.11+)
169
+ *
170
+ * Based on approach from tsup and esbuild discussions
171
+ * @see https://github.com/egoist/tsup/blob/main/assets/cjs_shims.js
172
+ * @see https://github.com/evanw/esbuild/issues/3839
173
+ *
174
+ * @param code - The transformed CJS code from esbuild
175
+ * @returns Code with import_meta properly implemented
176
+ */
177
+ function replaceImportMetaObject(code) {
178
+ // esbuild generates: const import_meta = {};
179
+ // We need to replace this with a proper implementation
180
+ // Note: We use getters to ensure values are computed at runtime in the correct context
181
+ const shimImplementation = `const import_meta = {
182
+ get url() {
183
+ return require('url').pathToFileURL(__filename).href;
184
+ },
185
+ get dirname() {
186
+ return __dirname;
187
+ },
188
+ get filename() {
189
+ return __filename;
190
+ }
191
+ };`;
192
+ // Replace esbuild's empty import_meta object with our implementation
193
+ // Match: const import_meta = {};
194
+ return code.replace(/const import_meta\s*=\s*\{\s*\};/, shimImplementation);
195
+ }
126
196
  /**
127
197
  * Transform ESM code to CommonJS using esbuild
128
198
  * This allows ESM modules to be compiled to bytecode via vm.Script
@@ -141,10 +211,13 @@ function transformESMtoCJS(code, filename) {
141
211
  isTransformed: false,
142
212
  };
143
213
  }
144
- // First, check for unsupported ESM features that can't be safely transformed
145
- const unsupportedFeatures = detectUnsupportedESMFeatures(code, filename);
146
- if (unsupportedFeatures && unsupportedFeatures.length > 0) {
147
- const featureList = unsupportedFeatures
214
+ // First, check for ESM features that need special handling
215
+ const esmFeatures = detectESMFeatures(code, filename);
216
+ // Handle truly unsupported features (import.meta)
217
+ if (esmFeatures &&
218
+ esmFeatures.unsupportedFeatures &&
219
+ esmFeatures.unsupportedFeatures.length > 0) {
220
+ const featureList = esmFeatures.unsupportedFeatures
148
221
  .map((f) => {
149
222
  const location = f.line !== null ? ` at line ${f.line}` : '';
150
223
  return ` - ${f.feature}${location}`;
@@ -169,15 +242,101 @@ function transformESMtoCJS(code, filename) {
169
242
  isTransformed: false,
170
243
  };
171
244
  }
245
+ // Check if we need to wrap in async IIFE for top-level await
246
+ const hasTopLevelAwait = esmFeatures &&
247
+ esmFeatures.topLevelAwait &&
248
+ esmFeatures.topLevelAwait.length > 0;
249
+ let codeToTransform = code;
250
+ // If top-level await is detected, we need to wrap in async IIFE
251
+ // But we must handle imports and exports specially
252
+ if (hasTopLevelAwait) {
253
+ try {
254
+ // Parse the code to check for exports and collect imports
255
+ const ast = babel.parse(code, {
256
+ sourceType: 'module',
257
+ plugins: [],
258
+ });
259
+ let hasExports = false;
260
+ const codeLines = code.split('\n');
261
+ const importLineIndices = new Set();
262
+ // @ts-expect-error Type mismatch due to @babel/types version
263
+ (0, traverse_1.default)(ast, {
264
+ ExportNamedDeclaration() {
265
+ hasExports = true;
266
+ },
267
+ ExportDefaultDeclaration() {
268
+ hasExports = true;
269
+ },
270
+ ExportAllDeclaration() {
271
+ hasExports = true;
272
+ },
273
+ ImportDeclaration(path) {
274
+ // Track import statements by line number
275
+ const { loc } = path.node;
276
+ if (loc) {
277
+ const { start, end } = loc;
278
+ for (let i = start.line; i <= end.line; i += 1) {
279
+ importLineIndices.add(i - 1); // Convert to 0-based index
280
+ }
281
+ }
282
+ },
283
+ });
284
+ if (hasExports) {
285
+ // If the file has exports, we can't wrap it in an IIFE
286
+ // because exports need to be synchronous and at the top level.
287
+ log_1.log.warn(`Module ${filename} has both top-level await and export statements. ` +
288
+ `This combination cannot be safely transformed to CommonJS in pkg's ESM transformer. ` +
289
+ `The original source code will be used as-is; depending on the package visibility and build configuration, ` +
290
+ `bytecode compilation may fail and the module may need to be loaded from source or be skipped.`);
291
+ return {
292
+ code,
293
+ isTransformed: false,
294
+ };
295
+ }
296
+ // If there are imports, extract them to keep outside the async IIFE
297
+ if (importLineIndices.size > 0) {
298
+ const imports = [];
299
+ const rest = [];
300
+ codeLines.forEach((line, index) => {
301
+ if (importLineIndices.has(index)) {
302
+ imports.push(line);
303
+ }
304
+ else {
305
+ rest.push(line);
306
+ }
307
+ });
308
+ // Reconstruct: imports at top, then async IIFE wrapping the rest
309
+ codeToTransform = `${imports.join('\n')}\n${ASYNC_IIFE_WRAPPER.prefix}${rest.join('\n')}${ASYNC_IIFE_WRAPPER.suffix}`;
310
+ log_1.log.debug(`Wrapping ${filename} in async IIFE with imports extracted to top level`);
311
+ }
312
+ else {
313
+ // No imports, wrap everything
314
+ codeToTransform =
315
+ ASYNC_IIFE_WRAPPER.prefix + code + ASYNC_IIFE_WRAPPER.suffix;
316
+ log_1.log.debug(`Wrapping ${filename} in async IIFE to support top-level await`);
317
+ }
318
+ }
319
+ catch (parseError) {
320
+ // If we can't parse, wrap everything and hope for the best
321
+ codeToTransform =
322
+ ASYNC_IIFE_WRAPPER.prefix + code + ASYNC_IIFE_WRAPPER.suffix;
323
+ log_1.log.warn(`Could not parse ${filename} to detect exports/imports (${parseError instanceof Error ? parseError.message : String(parseError)}). ` +
324
+ `Wrapping entire code in async IIFE - this may fail if the module has export or import statements.`);
325
+ }
326
+ }
327
+ // Check if code uses import.meta before transformation
328
+ const usesImportMeta = hasImportMeta(code);
172
329
  try {
173
- const result = esbuild.transformSync(code, {
330
+ // Build esbuild options
331
+ const esbuildOptions = {
174
332
  loader: 'js',
175
333
  format: 'cjs',
176
334
  target: 'node18',
177
335
  sourcemap: false,
178
336
  minify: false,
179
337
  keepNames: true,
180
- });
338
+ };
339
+ const result = esbuild.transformSync(codeToTransform, esbuildOptions);
181
340
  if (!result || !result.code) {
182
341
  log_1.log.warn(`esbuild transform returned no code for ${filename}`);
183
342
  return {
@@ -185,8 +344,13 @@ function transformESMtoCJS(code, filename) {
185
344
  isTransformed: false,
186
345
  };
187
346
  }
347
+ // Inject import.meta shims after esbuild transformation if needed
348
+ let finalCode = result.code;
349
+ if (usesImportMeta) {
350
+ finalCode = replaceImportMetaObject(result.code);
351
+ }
188
352
  return {
189
- code: result.code,
353
+ code: finalCode,
190
354
  isTransformed: true,
191
355
  };
192
356
  }
package/lib-es5/packer.js CHANGED
@@ -27,13 +27,22 @@ function hasAnyStore(record) {
27
27
  }
28
28
  function packer({ records, entrypoint, bytecode, }) {
29
29
  const stripes = [];
30
- for (const snap in records) {
30
+ // If the entrypoint was a .mjs file that got transformed, update its extension
31
+ if (records[entrypoint]?.wasTransformed && entrypoint.endsWith('.mjs')) {
32
+ entrypoint = `${entrypoint.slice(0, -4)}.js`;
33
+ }
34
+ for (let snap in records) {
31
35
  if (records[snap]) {
32
36
  const record = records[snap];
33
37
  const { file } = record;
34
38
  if (!hasAnyStore(record)) {
35
39
  continue;
36
40
  }
41
+ // If .mjs file was transformed to CJS, rename it to .js in the snapshot
42
+ // This prevents Node.js from treating it as an ES module
43
+ if (record.wasTransformed && snap.endsWith('.mjs')) {
44
+ snap = `${snap.slice(0, -4)}.js`;
45
+ }
37
46
  (0, assert_1.default)(record[common_1.STORE_STAT], 'packer: no STORE_STAT');
38
47
  (0, assert_1.default)(record[common_1.STORE_BLOB] ||
39
48
  record[common_1.STORE_CONTENT] ||
package/lib-es5/walker.js CHANGED
@@ -852,6 +852,10 @@ class Walker {
852
852
  const result = (0, esm_transformer_1.transformESMtoCJS)(record.body.toString('utf8'), record.file);
853
853
  if (result.isTransformed) {
854
854
  record.body = Buffer.from(result.code, 'utf8');
855
+ // Mark .mjs files as transformed so packer can rename them to .js
856
+ if (record.file.endsWith('.mjs')) {
857
+ record.wasTransformed = true;
858
+ }
855
859
  }
856
860
  }
857
861
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yao-pkg/pkg",
3
- "version": "6.13.0",
3
+ "version": "6.13.1",
4
4
  "description": "Package your Node.js project into an executable",
5
5
  "main": "lib-es5/index.js",
6
6
  "license": "MIT",