@yao-pkg/pkg 6.12.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 +63 -26
- package/lib-es5/common.js +94 -1
- package/lib-es5/detector.js +4 -2
- package/lib-es5/esm-transformer.js +366 -0
- package/lib-es5/follow.js +154 -55
- package/lib-es5/index.js +10 -8
- package/lib-es5/mach-o.js +1 -1
- package/lib-es5/options.js +2 -1
- package/lib-es5/packer.js +11 -2
- package/lib-es5/producer.js +3 -4
- package/lib-es5/resolver.js +142 -0
- package/lib-es5/sea.js +13 -6
- package/lib-es5/walker.js +131 -21
- package/package.json +6 -4
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
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
|
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
|
|
565
|
-
import * as fs from
|
|
566
|
-
import * as path from
|
|
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 =
|
|
570
|
-
const outputPath = exePath;
|
|
571
|
-
const 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;
|
|
574
|
-
const codepage = 1200;
|
|
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(
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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 `
|
package/lib-es5/common.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.toNormalizedRealPath = exports.removeUplevels = exports.stripSnapshot = exports.insideSnapshot = exports.snapshotify = exports.substituteDenominator = exports.retrieveDenominator = exports.isDotNODE = exports.isDotJSON = exports.isDotJS = exports.isPackageJson = exports.normalizePath = exports.isRootPath = exports.ALIAS_AS_RESOLVABLE = exports.ALIAS_AS_RELATIVE = exports.STORE_STAT = exports.STORE_LINKS = exports.STORE_CONTENT = exports.STORE_BLOB = void 0;
|
|
6
|
+
exports.isESMFile = exports.isESMPackage = exports.toNormalizedRealPath = exports.removeUplevels = exports.stripSnapshot = exports.insideSnapshot = exports.snapshotify = exports.substituteDenominator = exports.retrieveDenominator = exports.unlikelyJavascript = exports.isDotNODE = exports.isDotJSON = exports.isDotJS = exports.isPackageJson = exports.normalizePath = exports.isRootPath = exports.ALIAS_AS_RESOLVABLE = exports.ALIAS_AS_RELATIVE = exports.STORE_STAT = exports.STORE_LINKS = exports.STORE_CONTENT = exports.STORE_BLOB = void 0;
|
|
7
7
|
const assert_1 = __importDefault(require("assert"));
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
@@ -93,6 +93,19 @@ function isDotNODE(file) {
|
|
|
93
93
|
return path_1.default.extname(file) === '.node';
|
|
94
94
|
}
|
|
95
95
|
exports.isDotNODE = isDotNODE;
|
|
96
|
+
function unlikelyJavascript(file) {
|
|
97
|
+
const ext = path_1.default.extname(file);
|
|
98
|
+
// Check single extensions
|
|
99
|
+
if (['.css', '.html', '.json', '.vue'].includes(ext)) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
// Check for .d.ts files (compound extension)
|
|
103
|
+
if (file.endsWith('.d.ts')) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
exports.unlikelyJavascript = unlikelyJavascript;
|
|
96
109
|
function replaceSlashes(file, slash) {
|
|
97
110
|
if (/^.:\\/.test(file)) {
|
|
98
111
|
if (slash === '/') {
|
|
@@ -232,4 +245,84 @@ function toNormalizedRealPath(requestPath) {
|
|
|
232
245
|
return file;
|
|
233
246
|
}
|
|
234
247
|
exports.toNormalizedRealPath = toNormalizedRealPath;
|
|
248
|
+
/**
|
|
249
|
+
* Find the nearest package.json file by walking up the directory tree
|
|
250
|
+
* @param filePath - Starting file path
|
|
251
|
+
* @returns Path to package.json or null if not found
|
|
252
|
+
*/
|
|
253
|
+
function findNearestPackageJson(filePath) {
|
|
254
|
+
let dir = path_1.default.dirname(filePath);
|
|
255
|
+
const { root } = path_1.default.parse(dir);
|
|
256
|
+
while (dir !== root) {
|
|
257
|
+
const packageJsonPath = path_1.default.join(dir, 'package.json');
|
|
258
|
+
if (fs_1.default.existsSync(packageJsonPath)) {
|
|
259
|
+
return packageJsonPath;
|
|
260
|
+
}
|
|
261
|
+
dir = path_1.default.dirname(dir);
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
// Caches for ESM detection performance optimization
|
|
266
|
+
const packageJsonCache = new Map();
|
|
267
|
+
const esmPackageCache = new Map();
|
|
268
|
+
/**
|
|
269
|
+
* Check if a package.json indicates an ESM package
|
|
270
|
+
* @param packageJsonPath - Path to package.json
|
|
271
|
+
* @returns true if "type": "module" is set
|
|
272
|
+
*/
|
|
273
|
+
function isESMPackage(packageJsonPath) {
|
|
274
|
+
// Check cache first
|
|
275
|
+
if (esmPackageCache.has(packageJsonPath)) {
|
|
276
|
+
return esmPackageCache.get(packageJsonPath);
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
const content = fs_1.default.readFileSync(packageJsonPath, 'utf8');
|
|
280
|
+
const pkg = JSON.parse(content);
|
|
281
|
+
const result = pkg.type === 'module';
|
|
282
|
+
esmPackageCache.set(packageJsonPath, result);
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
esmPackageCache.set(packageJsonPath, false);
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
exports.isESMPackage = isESMPackage;
|
|
291
|
+
/**
|
|
292
|
+
* Determine if a file should be treated as ESM
|
|
293
|
+
* Based on file extension and nearest package.json "type" field
|
|
294
|
+
*
|
|
295
|
+
* @param filePath - The file path to check
|
|
296
|
+
* @returns true if file should be treated as ESM
|
|
297
|
+
*/
|
|
298
|
+
function isESMFile(filePath) {
|
|
299
|
+
// .mjs files are always ESM
|
|
300
|
+
if (filePath.endsWith('.mjs')) {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
// .cjs files are never ESM
|
|
304
|
+
if (filePath.endsWith('.cjs')) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
// For .js files, check nearest package.json for "type": "module"
|
|
308
|
+
if (filePath.endsWith('.js')) {
|
|
309
|
+
const dir = path_1.default.dirname(filePath);
|
|
310
|
+
// Check cache first
|
|
311
|
+
if (packageJsonCache.has(dir)) {
|
|
312
|
+
const cached = packageJsonCache.get(dir);
|
|
313
|
+
if (cached) {
|
|
314
|
+
return isESMPackage(cached);
|
|
315
|
+
}
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
// Compute and cache
|
|
319
|
+
const packageJsonPath = findNearestPackageJson(filePath);
|
|
320
|
+
packageJsonCache.set(dir, packageJsonPath);
|
|
321
|
+
if (packageJsonPath) {
|
|
322
|
+
return isESMPackage(packageJsonPath);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
exports.isESMFile = isESMFile;
|
|
235
328
|
//# sourceMappingURL=common.js.map
|
package/lib-es5/detector.js
CHANGED
|
@@ -86,6 +86,7 @@ function reconstructSpecifiers(specs) {
|
|
|
86
86
|
return defaults.join(', ');
|
|
87
87
|
}
|
|
88
88
|
function reconstruct(node) {
|
|
89
|
+
// @ts-expect-error Type mismatch due to @babel/types version in @types/babel__generator
|
|
89
90
|
let v = (0, generator_1.default)(node, { comments: false }).code.replace(/\n/g, '');
|
|
90
91
|
let v2;
|
|
91
92
|
while (true) {
|
|
@@ -403,13 +404,14 @@ function parse(body) {
|
|
|
403
404
|
});
|
|
404
405
|
}
|
|
405
406
|
exports.parse = parse;
|
|
406
|
-
function detect(body, visitor) {
|
|
407
|
+
function detect(body, visitor, file) {
|
|
407
408
|
let json;
|
|
408
409
|
try {
|
|
409
410
|
json = parse(body);
|
|
410
411
|
}
|
|
411
412
|
catch (error) {
|
|
412
|
-
|
|
413
|
+
const fileInfo = file ? ` in ${file}` : '';
|
|
414
|
+
log_1.log.warn(`Babel parse has failed: ${error.message}${fileInfo}`);
|
|
413
415
|
}
|
|
414
416
|
if (!json) {
|
|
415
417
|
return;
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.transformESMtoCJS = void 0;
|
|
30
|
+
const babel = __importStar(require("@babel/parser"));
|
|
31
|
+
const traverse_1 = __importDefault(require("@babel/traverse"));
|
|
32
|
+
const esbuild = __importStar(require("esbuild"));
|
|
33
|
+
const log_1 = require("./log");
|
|
34
|
+
const common_1 = require("./common");
|
|
35
|
+
/**
|
|
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
|
|
45
|
+
*
|
|
46
|
+
* @param code - The ESM source code to check
|
|
47
|
+
* @returns true if import.meta is used, false otherwise
|
|
48
|
+
*/
|
|
49
|
+
function hasImportMeta(code) {
|
|
50
|
+
try {
|
|
51
|
+
const ast = babel.parse(code, {
|
|
52
|
+
sourceType: 'module',
|
|
53
|
+
plugins: [],
|
|
54
|
+
});
|
|
55
|
+
if (!ast) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
let found = false;
|
|
59
|
+
// @ts-expect-error Type mismatch due to @babel/types version in @types/babel__traverse
|
|
60
|
+
(0, traverse_1.default)(ast, {
|
|
61
|
+
// Detect import.meta usage
|
|
62
|
+
MetaProperty(path) {
|
|
63
|
+
if (path.node.meta.name === 'import' &&
|
|
64
|
+
path.node.property.name === 'meta') {
|
|
65
|
+
found = true;
|
|
66
|
+
path.stop(); // Stop traversal once found
|
|
67
|
+
}
|
|
68
|
+
},
|
|
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
|
|
102
|
+
AwaitExpression(path) {
|
|
103
|
+
// Check if await is at top level (not inside a function)
|
|
104
|
+
let parent = path.parentPath;
|
|
105
|
+
let isTopLevel = true;
|
|
106
|
+
while (parent) {
|
|
107
|
+
if (parent.isFunctionDeclaration() ||
|
|
108
|
+
parent.isFunctionExpression() ||
|
|
109
|
+
parent.isArrowFunctionExpression() ||
|
|
110
|
+
parent.isObjectMethod() ||
|
|
111
|
+
parent.isClassMethod()) {
|
|
112
|
+
isTopLevel = false;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
parent = parent.parentPath;
|
|
116
|
+
}
|
|
117
|
+
if (isTopLevel) {
|
|
118
|
+
topLevelAwait.push({
|
|
119
|
+
feature: 'top-level await',
|
|
120
|
+
line: path.node.loc?.start.line ?? null,
|
|
121
|
+
column: path.node.loc?.start.column ?? null,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
// Detect for-await-of at top level - can be handled with async IIFE wrapper
|
|
126
|
+
ForOfStatement(path) {
|
|
127
|
+
if (path.node.await) {
|
|
128
|
+
let parent = path.parentPath;
|
|
129
|
+
let isTopLevel = true;
|
|
130
|
+
while (parent) {
|
|
131
|
+
if (parent.isFunctionDeclaration() ||
|
|
132
|
+
parent.isFunctionExpression() ||
|
|
133
|
+
parent.isArrowFunctionExpression() ||
|
|
134
|
+
parent.isObjectMethod() ||
|
|
135
|
+
parent.isClassMethod()) {
|
|
136
|
+
isTopLevel = false;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
parent = parent.parentPath;
|
|
140
|
+
}
|
|
141
|
+
if (isTopLevel) {
|
|
142
|
+
topLevelAwait.push({
|
|
143
|
+
feature: 'top-level for-await-of',
|
|
144
|
+
line: path.node.loc?.start.line ?? null,
|
|
145
|
+
column: path.node.loc?.start.column ?? null,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
return { topLevelAwait, unsupportedFeatures };
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
// If we can't parse, return null to let the transform attempt proceed
|
|
155
|
+
log_1.log.debug(`Could not parse ${filename} to detect ESM features: ${error instanceof Error ? error.message : String(error)}`);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
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
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Transform ESM code to CommonJS using esbuild
|
|
198
|
+
* This allows ESM modules to be compiled to bytecode via vm.Script
|
|
199
|
+
* Uses Babel parser for detecting unsupported ESM features, then esbuild for fast transformation
|
|
200
|
+
*
|
|
201
|
+
* @param code - The ESM source code to transform
|
|
202
|
+
* @param filename - The filename for error reporting
|
|
203
|
+
* @returns Object with transformed code and success flag
|
|
204
|
+
*/
|
|
205
|
+
function transformESMtoCJS(code, filename) {
|
|
206
|
+
// Skip files that are unlikely to be JavaScript (e.g., .d.ts, .json, .css)
|
|
207
|
+
// to avoid Babel parse errors
|
|
208
|
+
if ((0, common_1.unlikelyJavascript)(filename)) {
|
|
209
|
+
return {
|
|
210
|
+
code,
|
|
211
|
+
isTransformed: false,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
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
|
|
221
|
+
.map((f) => {
|
|
222
|
+
const location = f.line !== null ? ` at line ${f.line}` : '';
|
|
223
|
+
return ` - ${f.feature}${location}`;
|
|
224
|
+
})
|
|
225
|
+
.join('\n');
|
|
226
|
+
const errorMessage = [
|
|
227
|
+
`Cannot transform ESM module ${filename} to CommonJS:`,
|
|
228
|
+
`The following ESM features have no CommonJS equivalent:`,
|
|
229
|
+
featureList,
|
|
230
|
+
'',
|
|
231
|
+
'These features are not supported when compiling to bytecode.',
|
|
232
|
+
'Consider one of the following:',
|
|
233
|
+
' 1. Refactor to avoid these features',
|
|
234
|
+
' 2. Use --no-bytecode flag to keep the module as source code',
|
|
235
|
+
' 3. Mark the package as public to distribute with sources',
|
|
236
|
+
].join('\n');
|
|
237
|
+
log_1.log.warn(errorMessage);
|
|
238
|
+
// Return untransformed code rather than throwing
|
|
239
|
+
// This allows the file to be included as content instead of bytecode
|
|
240
|
+
return {
|
|
241
|
+
code,
|
|
242
|
+
isTransformed: false,
|
|
243
|
+
};
|
|
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);
|
|
329
|
+
try {
|
|
330
|
+
// Build esbuild options
|
|
331
|
+
const esbuildOptions = {
|
|
332
|
+
loader: 'js',
|
|
333
|
+
format: 'cjs',
|
|
334
|
+
target: 'node18',
|
|
335
|
+
sourcemap: false,
|
|
336
|
+
minify: false,
|
|
337
|
+
keepNames: true,
|
|
338
|
+
};
|
|
339
|
+
const result = esbuild.transformSync(codeToTransform, esbuildOptions);
|
|
340
|
+
if (!result || !result.code) {
|
|
341
|
+
log_1.log.warn(`esbuild transform returned no code for ${filename}`);
|
|
342
|
+
return {
|
|
343
|
+
code,
|
|
344
|
+
isTransformed: false,
|
|
345
|
+
};
|
|
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
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
code: finalCode,
|
|
354
|
+
isTransformed: true,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
log_1.log.warn(`Failed to transform ESM to CJS for ${filename}: ${error instanceof Error ? error.message : String(error)}`);
|
|
359
|
+
return {
|
|
360
|
+
code,
|
|
361
|
+
isTransformed: false,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
exports.transformESMtoCJS = transformESMtoCJS;
|
|
366
|
+
//# sourceMappingURL=esm-transformer.js.map
|