@gjsify/rolldown-plugin-gjsify 0.3.14
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/lib/app/browser.d.ts +17 -0
- package/lib/app/browser.js +77 -0
- package/lib/app/gjs.d.ts +27 -0
- package/lib/app/gjs.js +211 -0
- package/lib/app/index.d.ts +6 -0
- package/lib/app/index.js +3 -0
- package/lib/app/node.d.ts +17 -0
- package/lib/app/node.js +102 -0
- package/lib/globals.d.ts +4 -0
- package/lib/globals.js +9 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.js +15 -0
- package/lib/library/index.d.ts +2 -0
- package/lib/library/index.js +1 -0
- package/lib/library/lib.d.ts +16 -0
- package/lib/library/lib.js +118 -0
- package/lib/plugin.d.ts +25 -0
- package/lib/plugin.js +67 -0
- package/lib/plugins/alias.d.ts +5 -0
- package/lib/plugins/alias.js +45 -0
- package/lib/plugins/css-as-string.d.ts +2 -0
- package/lib/plugins/css-as-string.js +34 -0
- package/lib/plugins/gjs-imports-empty.d.ts +2 -0
- package/lib/plugins/gjs-imports-empty.js +26 -0
- package/lib/plugins/process-stub.d.ts +28 -0
- package/lib/plugins/process-stub.js +60 -0
- package/lib/plugins/rewrite-node-modules-paths.d.ts +38 -0
- package/lib/plugins/rewrite-node-modules-paths.js +132 -0
- package/lib/plugins/shebang.d.ts +8 -0
- package/lib/plugins/shebang.js +26 -0
- package/lib/shims/console-gjs.d.ts +24 -0
- package/lib/shims/console-gjs.js +24 -0
- package/lib/types/app.d.ts +1 -0
- package/lib/types/app.js +1 -0
- package/lib/types/index.d.ts +3 -0
- package/lib/types/index.js +3 -0
- package/lib/types/plugin-options.d.ts +46 -0
- package/lib/types/plugin-options.js +1 -0
- package/lib/types/resolve-alias-options.d.ts +2 -0
- package/lib/types/resolve-alias-options.js +1 -0
- package/lib/utils/alias.d.ts +12 -0
- package/lib/utils/alias.js +29 -0
- package/lib/utils/auto-globals.d.ts +72 -0
- package/lib/utils/auto-globals.js +193 -0
- package/lib/utils/detect-free-globals.d.ts +18 -0
- package/lib/utils/detect-free-globals.js +268 -0
- package/lib/utils/entry-points.d.ts +2 -0
- package/lib/utils/entry-points.js +38 -0
- package/lib/utils/extension.d.ts +1 -0
- package/lib/utils/extension.js +7 -0
- package/lib/utils/index.d.ts +7 -0
- package/lib/utils/index.js +7 -0
- package/lib/utils/inline-static-reads.d.ts +11 -0
- package/lib/utils/inline-static-reads.js +549 -0
- package/lib/utils/merge.d.ts +2 -0
- package/lib/utils/merge.js +23 -0
- package/lib/utils/scan-globals.d.ts +32 -0
- package/lib/utils/scan-globals.js +85 -0
- package/package.json +68 -0
- package/src/app/browser.ts +102 -0
- package/src/app/gjs.ts +260 -0
- package/src/app/index.ts +6 -0
- package/src/app/node.ts +128 -0
- package/src/globals.ts +11 -0
- package/src/index.ts +32 -0
- package/src/library/index.ts +2 -0
- package/src/library/lib.ts +142 -0
- package/src/plugin.ts +91 -0
- package/src/plugins/alias.ts +53 -0
- package/src/plugins/css-as-string.ts +37 -0
- package/src/plugins/gjs-imports-empty.ts +29 -0
- package/src/plugins/process-stub.ts +91 -0
- package/src/plugins/rewrite-node-modules-paths.ts +169 -0
- package/src/plugins/shebang.ts +33 -0
- package/src/shims/console-gjs.ts +25 -0
- package/src/types/app.ts +1 -0
- package/src/types/index.ts +3 -0
- package/src/types/plugin-options.ts +48 -0
- package/src/types/resolve-alias-options.ts +1 -0
- package/src/utils/alias.ts +46 -0
- package/src/utils/auto-globals.ts +283 -0
- package/src/utils/detect-free-globals.ts +278 -0
- package/src/utils/entry-points.ts +48 -0
- package/src/utils/extension.ts +7 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/inline-static-reads.ts +541 -0
- package/src/utils/merge.ts +22 -0
- package/src/utils/scan-globals.ts +91 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
// Build-time inlining of statically-resolvable filesystem reads.
|
|
2
|
+
//
|
|
3
|
+
// Many node_modules packages locate their own resources (own package.json,
|
|
4
|
+
// locales, themes, ...) via `import.meta.url`-relative reads:
|
|
5
|
+
//
|
|
6
|
+
// const pkg = JSON.parse(readFileSync(
|
|
7
|
+
// new URL("../package.json", import.meta.url),
|
|
8
|
+
// "utf8",
|
|
9
|
+
// ));
|
|
10
|
+
//
|
|
11
|
+
// In a bundled GJS executable, `import.meta.url` no longer points at the
|
|
12
|
+
// original `node_modules/<pkg>/<file>` location, so the read fails with
|
|
13
|
+
// ENOENT once the bundle leaves the build site (gjsify dlx, manual move,
|
|
14
|
+
// CI artifact download, …).
|
|
15
|
+
//
|
|
16
|
+
// The clean fix is to evaluate the static expressions at build time and
|
|
17
|
+
// replace the entire `readFileSync(...)` (or `readdirSync(...)`, or the
|
|
18
|
+
// `JSON.parse(readFileSync(...))` composition) with a literal containing
|
|
19
|
+
// the file contents. The bundle is then a single self-contained file that
|
|
20
|
+
// behaves exactly like the original — same return value, same errors on
|
|
21
|
+
// missing files — but with no runtime dependency on the build-site layout.
|
|
22
|
+
//
|
|
23
|
+
// Patterns handled:
|
|
24
|
+
//
|
|
25
|
+
// readFileSync(<URL-derived-path>, "utf8" | "utf-8" | { encoding: "utf8" })
|
|
26
|
+
// → string literal
|
|
27
|
+
// readFileSync(<URL-derived-path>) → Uint8Array literal
|
|
28
|
+
// readdirSync(<URL-derived-path>) → array literal of names
|
|
29
|
+
// JSON.parse(readFileSync(...)) → object literal
|
|
30
|
+
// existsSync(<URL-derived-path>) → boolean literal
|
|
31
|
+
//
|
|
32
|
+
// Path expressions are evaluated against `import.meta.url` of the source
|
|
33
|
+
// file at build time, supporting compositions of:
|
|
34
|
+
//
|
|
35
|
+
// new URL(<lit>, import.meta.url) base resolution
|
|
36
|
+
// <expr>.href, <expr>.pathname property access
|
|
37
|
+
// fileURLToPath(<URL-expr>) url → fs path
|
|
38
|
+
// path.{join,dirname,resolve,basename,relative}(...) path arithmetic
|
|
39
|
+
// string-literal + string-literal concatenation
|
|
40
|
+
//
|
|
41
|
+
// Anything not statically resolvable is left untouched — the legacy
|
|
42
|
+
// `import.meta.url` rewriter still applies as a fallback.
|
|
43
|
+
import * as acorn from 'acorn';
|
|
44
|
+
import * as walk from 'acorn-walk';
|
|
45
|
+
import { dirname, join, resolve, basename, relative, extname } from 'node:path';
|
|
46
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
47
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
48
|
+
/**
|
|
49
|
+
* Run the inliner on a source string. Returns the rewritten source (or the
|
|
50
|
+
* original string when no inlining applied) and the count of edits applied.
|
|
51
|
+
*
|
|
52
|
+
* Safe to call on any JS source. Files that don't reference `readFileSync` /
|
|
53
|
+
* `readdirSync` / `existsSync` skip the AST parse entirely (cheap fast path).
|
|
54
|
+
*/
|
|
55
|
+
export function inlineStaticReads(src, sourceFilePath) {
|
|
56
|
+
if (!src.includes('readFileSync') &&
|
|
57
|
+
!src.includes('readdirSync') &&
|
|
58
|
+
!src.includes('existsSync')) {
|
|
59
|
+
return { contents: src, inlined: 0 };
|
|
60
|
+
}
|
|
61
|
+
let ast;
|
|
62
|
+
try {
|
|
63
|
+
ast = acorn.parse(src, {
|
|
64
|
+
ecmaVersion: 'latest',
|
|
65
|
+
sourceType: 'module',
|
|
66
|
+
allowAwaitOutsideFunction: true,
|
|
67
|
+
allowReturnOutsideFunction: true,
|
|
68
|
+
allowImportExportEverywhere: true,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Source isn't valid JS (CJS source with shebangs, mixed module
|
|
73
|
+
// syntax, ...). Skip; the rest of the rewriter still runs.
|
|
74
|
+
return { contents: src, inlined: 0 };
|
|
75
|
+
}
|
|
76
|
+
const ctx = {
|
|
77
|
+
sourceUrl: pathToFileURL(sourceFilePath).href,
|
|
78
|
+
};
|
|
79
|
+
const edits = [];
|
|
80
|
+
walk.simple(ast, {
|
|
81
|
+
CallExpression(node) {
|
|
82
|
+
const edit = tryInlineCall(node, ctx, src);
|
|
83
|
+
if (edit)
|
|
84
|
+
edits.push(edit);
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
if (edits.length === 0)
|
|
88
|
+
return { contents: src, inlined: 0 };
|
|
89
|
+
// The walker visits both outer and inner CallExpressions, so a successful
|
|
90
|
+
// match on `JSON.parse(readFileSync(...))` produces an edit AT the same
|
|
91
|
+
// time that the inner `readFileSync(...)` also produces one. Applying both
|
|
92
|
+
// would corrupt the output. Keep only edits that are not contained in any
|
|
93
|
+
// other edit (= outermost wins).
|
|
94
|
+
const outermost = [];
|
|
95
|
+
edits.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
96
|
+
for (const e of edits) {
|
|
97
|
+
const last = outermost[outermost.length - 1];
|
|
98
|
+
if (last && e.start >= last.start && e.end <= last.end)
|
|
99
|
+
continue; // nested
|
|
100
|
+
outermost.push(e);
|
|
101
|
+
}
|
|
102
|
+
// Apply right-to-left so earlier offsets remain valid.
|
|
103
|
+
outermost.sort((a, b) => b.start - a.start);
|
|
104
|
+
let out = src;
|
|
105
|
+
for (const e of outermost) {
|
|
106
|
+
out = out.slice(0, e.start) + e.replacement + out.slice(e.end);
|
|
107
|
+
}
|
|
108
|
+
return { contents: out, inlined: outermost.length };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Try to inline a single `CallExpression`. Returns an edit on success, or
|
|
112
|
+
* `undefined` if the call doesn't match an inlinable pattern or the path
|
|
113
|
+
* couldn't be resolved or the file doesn't exist.
|
|
114
|
+
*/
|
|
115
|
+
function tryInlineCall(node, ctx, src) {
|
|
116
|
+
const callee = node.callee;
|
|
117
|
+
// `JSON.parse(readFileSync(<path>, "utf8"))` — collapse the whole
|
|
118
|
+
// composition. Recognising it specifically lets us emit a parsed-JSON
|
|
119
|
+
// object literal instead of a `JSON.parse('…')` string-then-parse pair,
|
|
120
|
+
// which esbuild can dead-code-eliminate against.
|
|
121
|
+
if (callee.type === 'MemberExpression' &&
|
|
122
|
+
!callee.computed &&
|
|
123
|
+
callee.object.type === 'Identifier' && callee.object.name === 'JSON' &&
|
|
124
|
+
callee.property.type === 'Identifier' && callee.property.name === 'parse' &&
|
|
125
|
+
node.arguments.length >= 1 &&
|
|
126
|
+
node.arguments[0].type === 'CallExpression') {
|
|
127
|
+
const inner = node.arguments[0];
|
|
128
|
+
const innerEdit = tryInlineReadFile(inner, ctx, /*forceTextEncoding*/ true);
|
|
129
|
+
if (innerEdit !== undefined) {
|
|
130
|
+
// `innerEdit` is the literal source for the read result (a JSON
|
|
131
|
+
// string). Parse and re-emit as a JS-literal expression so the
|
|
132
|
+
// surrounding code sees an object directly.
|
|
133
|
+
try {
|
|
134
|
+
const parsed = JSON.parse(JSON.parse(innerEdit));
|
|
135
|
+
return {
|
|
136
|
+
start: node.start,
|
|
137
|
+
end: node.end,
|
|
138
|
+
replacement: jsLiteral(parsed),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Fall through — leave the original call alone.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const calleeName = identifierName(callee);
|
|
147
|
+
if (calleeName === 'readFileSync') {
|
|
148
|
+
const replacement = tryInlineReadFile(node, ctx, /*forceTextEncoding*/ false);
|
|
149
|
+
if (replacement !== undefined) {
|
|
150
|
+
return { start: node.start, end: node.end, replacement };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (calleeName === 'readdirSync') {
|
|
154
|
+
const path = evalPathExpr(node.arguments[0], ctx);
|
|
155
|
+
if (path && existsSyncSafe(path) && isDirectorySafe(path)) {
|
|
156
|
+
try {
|
|
157
|
+
const names = readdirSync(path);
|
|
158
|
+
return {
|
|
159
|
+
start: node.start,
|
|
160
|
+
end: node.end,
|
|
161
|
+
replacement: jsLiteral(names),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
/* skip */
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (calleeName === 'existsSync') {
|
|
170
|
+
const path = evalPathExpr(node.arguments[0], ctx);
|
|
171
|
+
if (path !== undefined) {
|
|
172
|
+
return {
|
|
173
|
+
start: node.start,
|
|
174
|
+
end: node.end,
|
|
175
|
+
replacement: existsSyncSafe(path) ? 'true' : 'false',
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// `createRequire(<URL>)` from `node:module` returns a CJS-style require.
|
|
180
|
+
// In a bundled GJS executable, the deps that the runtime require would
|
|
181
|
+
// resolve are already inlined by esbuild, so the require() function is
|
|
182
|
+
// typically dead code. The createRequire CALL itself runs at module init,
|
|
183
|
+
// and Node's implementation rejects the rewritten URLs we produce when
|
|
184
|
+
// they don't point at an existing file (yargs-parser's `createRequire(
|
|
185
|
+
// import.meta.url)` blows up because the rewritten URL refers to a Yarn
|
|
186
|
+
// PnP zip path that doesn't exist outside the PnP runtime).
|
|
187
|
+
//
|
|
188
|
+
// Replace the call with a stub function: assignment succeeds, the bundle
|
|
189
|
+
// boots, and any actual `require()` invocation produces a clear error
|
|
190
|
+
// instead of an obscure URL-validation crash. Only fires when the URL
|
|
191
|
+
// argument can be statically resolved AND points at a non-existent file
|
|
192
|
+
// — the common case is exactly the broken one.
|
|
193
|
+
if (calleeName === 'createRequire') {
|
|
194
|
+
const path = evalPathExpr(node.arguments[0], ctx);
|
|
195
|
+
// Stub the call when:
|
|
196
|
+
// - the resolved path doesn't exist on disk (build site), OR
|
|
197
|
+
// - the path contains a `.zip/` segment (Yarn PnP virtual zip,
|
|
198
|
+
// where Node's PnP hooks make `existsSync` return true at build
|
|
199
|
+
// time but the path doesn't exist under GJS at runtime).
|
|
200
|
+
const isZip = path !== undefined && path.includes('.zip/');
|
|
201
|
+
if (path !== undefined && (isZip || !existsSyncSafe(path))) {
|
|
202
|
+
return {
|
|
203
|
+
start: node.start,
|
|
204
|
+
end: node.end,
|
|
205
|
+
replacement: `(() => { ` +
|
|
206
|
+
`const _r = (id) => { throw new Error("[gjsify] createRequire stub: '" + id + "' was not bundled (anchor path: " + ${jsStringLiteral(path)} + ")"); }; ` +
|
|
207
|
+
`_r.resolve = _r; _r.cache = {}; _r.extensions = {}; _r.main = void 0; ` +
|
|
208
|
+
`return _r; ` +
|
|
209
|
+
`})()`,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Inline a `readFileSync(<path>, <enc>?)` call to a string or byte literal.
|
|
217
|
+
* Returns the source replacement, or `undefined` to leave the call alone.
|
|
218
|
+
*
|
|
219
|
+
* `forceTextEncoding`: caller (JSON.parse wrapper) demands an utf-8 read
|
|
220
|
+
* regardless of whether the syntactic argument provides an encoding.
|
|
221
|
+
*/
|
|
222
|
+
function tryInlineReadFile(node, ctx, forceTextEncoding) {
|
|
223
|
+
if (node.arguments.length < 1)
|
|
224
|
+
return undefined;
|
|
225
|
+
const path = evalPathExpr(node.arguments[0], ctx);
|
|
226
|
+
if (!path)
|
|
227
|
+
return undefined;
|
|
228
|
+
if (!existsSyncSafe(path) || isDirectorySafe(path))
|
|
229
|
+
return undefined;
|
|
230
|
+
let encoding;
|
|
231
|
+
if (forceTextEncoding) {
|
|
232
|
+
encoding = 'utf8';
|
|
233
|
+
}
|
|
234
|
+
else if (node.arguments.length >= 2) {
|
|
235
|
+
encoding = evalEncodingExpr(node.arguments[1]);
|
|
236
|
+
if (encoding === undefined)
|
|
237
|
+
return undefined; // unknown → bail
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
if (encoding) {
|
|
241
|
+
const text = readFileSync(path, encoding);
|
|
242
|
+
return jsStringLiteral(text);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
// Binary read → emit a Uint8Array constructor over a number array.
|
|
246
|
+
// Buffer-vs-Uint8Array semantic difference is mostly irrelevant in
|
|
247
|
+
// bundled GJS code (Buffer is polyfilled on top of Uint8Array).
|
|
248
|
+
const bytes = readFileSync(path);
|
|
249
|
+
return `new Uint8Array([${Array.from(bytes).join(',')}])`;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Statically evaluate a node we expect to produce a filesystem path string.
|
|
258
|
+
* Returns the absolute path or `undefined` if any step is non-static.
|
|
259
|
+
*
|
|
260
|
+
* Recursively understands compositions of:
|
|
261
|
+
* - string literals, template literals (no expressions), `+` concatenation
|
|
262
|
+
* - `new URL(<lit>, <base-url-expr>)`
|
|
263
|
+
* - `<URL-expr>.href`, `<URL-expr>.pathname`
|
|
264
|
+
* - `fileURLToPath(<URL-expr>)` / `pathToFileURL(<path>).href`
|
|
265
|
+
* - `(path.)?{join,dirname,resolve,basename,relative,extname}(...)` over static args
|
|
266
|
+
* - `import.meta.url` (resolved against ctx.sourceUrl)
|
|
267
|
+
* - bare identifier `__dirname` / `__filename` (resolved against ctx.sourceUrl)
|
|
268
|
+
*
|
|
269
|
+
* Returns a path string OR a URL string, depending on context — callers
|
|
270
|
+
* that need a path use `evalPathExpr`, callers that need a URL use
|
|
271
|
+
* `evalUrlExpr`. They both come from the same recursive evaluator.
|
|
272
|
+
*/
|
|
273
|
+
function evalPathExpr(node, ctx) {
|
|
274
|
+
const v = evalExpr(node, ctx);
|
|
275
|
+
if (v instanceof URL) {
|
|
276
|
+
if (v.protocol === 'file:')
|
|
277
|
+
return fileURLToPath(v);
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
if (typeof v !== 'string')
|
|
281
|
+
return undefined;
|
|
282
|
+
if (v.startsWith('file://'))
|
|
283
|
+
return fileURLToPath(v);
|
|
284
|
+
if (v.startsWith('/'))
|
|
285
|
+
return v;
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
function evalExpr(node, ctx) {
|
|
289
|
+
if (!node)
|
|
290
|
+
return undefined;
|
|
291
|
+
switch (node.type) {
|
|
292
|
+
case 'Literal':
|
|
293
|
+
if (typeof node.value === 'string') {
|
|
294
|
+
return node.value;
|
|
295
|
+
}
|
|
296
|
+
return undefined;
|
|
297
|
+
case 'TemplateLiteral': {
|
|
298
|
+
const tl = node;
|
|
299
|
+
if (tl.expressions.length > 0)
|
|
300
|
+
return undefined;
|
|
301
|
+
return tl.quasis.map((q) => q.value.cooked ?? '').join('');
|
|
302
|
+
}
|
|
303
|
+
case 'BinaryExpression': {
|
|
304
|
+
const be = node;
|
|
305
|
+
if (be.operator !== '+')
|
|
306
|
+
return undefined;
|
|
307
|
+
const l = evalExpr(be.left, ctx);
|
|
308
|
+
const r = evalExpr(be.right, ctx);
|
|
309
|
+
if (typeof l !== 'string' || typeof r !== 'string')
|
|
310
|
+
return undefined;
|
|
311
|
+
return l + r;
|
|
312
|
+
}
|
|
313
|
+
case 'Identifier': {
|
|
314
|
+
const id = node;
|
|
315
|
+
if (id.name === '__dirname')
|
|
316
|
+
return fileURLToPath(new URL('.', ctx.sourceUrl));
|
|
317
|
+
if (id.name === '__filename')
|
|
318
|
+
return fileURLToPath(ctx.sourceUrl);
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
case 'MemberExpression': {
|
|
322
|
+
const me = node;
|
|
323
|
+
// import.meta.url
|
|
324
|
+
if (me.object.type === 'MetaProperty' &&
|
|
325
|
+
me.object.meta.name === 'import' &&
|
|
326
|
+
me.object.property.name === 'meta' &&
|
|
327
|
+
me.property.type === 'Identifier' &&
|
|
328
|
+
me.property.name === 'url') {
|
|
329
|
+
return ctx.sourceUrl;
|
|
330
|
+
}
|
|
331
|
+
// <expr>.href / .pathname
|
|
332
|
+
if (!me.computed && me.property.type === 'Identifier') {
|
|
333
|
+
const obj = evalExpr(me.object, ctx);
|
|
334
|
+
const prop = me.property.name;
|
|
335
|
+
if (obj instanceof URL) {
|
|
336
|
+
if (prop === 'href')
|
|
337
|
+
return obj.href;
|
|
338
|
+
if (prop === 'pathname')
|
|
339
|
+
return obj.pathname;
|
|
340
|
+
}
|
|
341
|
+
if (typeof obj === 'string') {
|
|
342
|
+
if (prop === 'href')
|
|
343
|
+
return obj; // already a URL string
|
|
344
|
+
if (prop === 'pathname') {
|
|
345
|
+
try {
|
|
346
|
+
return new URL(obj).pathname;
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
case 'NewExpression': {
|
|
357
|
+
const ne = node;
|
|
358
|
+
const calleeName = identifierName(ne.callee);
|
|
359
|
+
if (calleeName === 'URL') {
|
|
360
|
+
if (ne.arguments.length === 0)
|
|
361
|
+
return undefined;
|
|
362
|
+
const first = evalExpr(ne.arguments[0], ctx);
|
|
363
|
+
if (typeof first !== 'string')
|
|
364
|
+
return undefined;
|
|
365
|
+
if (ne.arguments.length === 1) {
|
|
366
|
+
try {
|
|
367
|
+
return new URL(first);
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const base = evalExpr(ne.arguments[1], ctx);
|
|
374
|
+
const baseStr = base instanceof URL ? base.href : (typeof base === 'string' ? base : undefined);
|
|
375
|
+
if (!baseStr)
|
|
376
|
+
return undefined;
|
|
377
|
+
try {
|
|
378
|
+
return new URL(first, baseStr);
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return undefined;
|
|
385
|
+
}
|
|
386
|
+
case 'CallExpression': {
|
|
387
|
+
const ce = node;
|
|
388
|
+
const name = identifierName(ce.callee);
|
|
389
|
+
if (name === 'fileURLToPath') {
|
|
390
|
+
const arg = evalExpr(ce.arguments[0], ctx);
|
|
391
|
+
const url = arg instanceof URL ? arg.href : (typeof arg === 'string' ? arg : undefined);
|
|
392
|
+
if (!url)
|
|
393
|
+
return undefined;
|
|
394
|
+
try {
|
|
395
|
+
return fileURLToPath(url);
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (name === 'pathToFileURL') {
|
|
402
|
+
const arg = evalExpr(ce.arguments[0], ctx);
|
|
403
|
+
if (typeof arg !== 'string')
|
|
404
|
+
return undefined;
|
|
405
|
+
try {
|
|
406
|
+
return pathToFileURL(arg);
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (name === 'join' || name === 'resolve') {
|
|
413
|
+
const args = [];
|
|
414
|
+
for (const a of ce.arguments) {
|
|
415
|
+
const v = evalExpr(a, ctx);
|
|
416
|
+
if (typeof v !== 'string')
|
|
417
|
+
return undefined;
|
|
418
|
+
args.push(v);
|
|
419
|
+
}
|
|
420
|
+
return name === 'join' ? join(...args) : resolve(...args);
|
|
421
|
+
}
|
|
422
|
+
if (name === 'dirname' || name === 'basename' || name === 'extname') {
|
|
423
|
+
const v = evalExpr(ce.arguments[0], ctx);
|
|
424
|
+
if (typeof v !== 'string')
|
|
425
|
+
return undefined;
|
|
426
|
+
if (name === 'dirname')
|
|
427
|
+
return dirname(v);
|
|
428
|
+
if (name === 'basename') {
|
|
429
|
+
const ext = ce.arguments.length >= 2 ? evalExpr(ce.arguments[1], ctx) : undefined;
|
|
430
|
+
return basename(v, typeof ext === 'string' ? ext : undefined);
|
|
431
|
+
}
|
|
432
|
+
if (name === 'extname')
|
|
433
|
+
return extname(v);
|
|
434
|
+
}
|
|
435
|
+
if (name === 'relative') {
|
|
436
|
+
const a = evalExpr(ce.arguments[0], ctx);
|
|
437
|
+
const b = evalExpr(ce.arguments[1], ctx);
|
|
438
|
+
if (typeof a !== 'string' || typeof b !== 'string')
|
|
439
|
+
return undefined;
|
|
440
|
+
return relative(a, b);
|
|
441
|
+
}
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Evaluate an encoding argument to its canonical string form.
|
|
449
|
+
* "utf8" / "utf-8" → "utf8"
|
|
450
|
+
* { encoding: "utf8" } → "utf8"
|
|
451
|
+
* anything else → undefined (caller leaves the call alone)
|
|
452
|
+
*/
|
|
453
|
+
function evalEncodingExpr(node) {
|
|
454
|
+
if (!node)
|
|
455
|
+
return undefined;
|
|
456
|
+
if (node.type === 'Literal') {
|
|
457
|
+
const v = node.value;
|
|
458
|
+
if (typeof v === 'string')
|
|
459
|
+
return canonicalEncoding(v);
|
|
460
|
+
return undefined;
|
|
461
|
+
}
|
|
462
|
+
if (node.type === 'ObjectExpression') {
|
|
463
|
+
for (const p of node.properties) {
|
|
464
|
+
if (p.type !== 'Property' || p.computed)
|
|
465
|
+
continue;
|
|
466
|
+
const key = p.key.type === 'Identifier'
|
|
467
|
+
? p.key.name
|
|
468
|
+
: p.key.type === 'Literal' ? String(p.key.value) : undefined;
|
|
469
|
+
if (key !== 'encoding')
|
|
470
|
+
continue;
|
|
471
|
+
if (p.value.type === 'Literal' && typeof p.value.value === 'string') {
|
|
472
|
+
return canonicalEncoding(p.value.value);
|
|
473
|
+
}
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
function canonicalEncoding(v) {
|
|
480
|
+
const lc = v.toLowerCase();
|
|
481
|
+
if (lc === 'utf8' || lc === 'utf-8')
|
|
482
|
+
return 'utf8';
|
|
483
|
+
if (lc === 'ascii')
|
|
484
|
+
return 'ascii';
|
|
485
|
+
if (lc === 'latin1' || lc === 'binary')
|
|
486
|
+
return 'latin1';
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Get the leaf identifier name of a callee. Recognises:
|
|
491
|
+
* `foo` → "foo"
|
|
492
|
+
* `path.foo` → "foo"
|
|
493
|
+
* `node:path.foo` → "foo" (rare)
|
|
494
|
+
* `fs.foo` / `fs.promises.foo` → "foo"
|
|
495
|
+
* Returns `undefined` for computed/dynamic callees.
|
|
496
|
+
*/
|
|
497
|
+
function identifierName(node) {
|
|
498
|
+
if (!node)
|
|
499
|
+
return undefined;
|
|
500
|
+
if (node.type === 'Identifier')
|
|
501
|
+
return node.name;
|
|
502
|
+
if (node.type === 'MemberExpression' && !node.computed) {
|
|
503
|
+
const me = node;
|
|
504
|
+
if (me.property.type === 'Identifier')
|
|
505
|
+
return me.property.name;
|
|
506
|
+
}
|
|
507
|
+
return undefined;
|
|
508
|
+
}
|
|
509
|
+
/** Produce a JS source-fragment for a value the inliner produced. */
|
|
510
|
+
function jsLiteral(v) {
|
|
511
|
+
if (typeof v === 'string')
|
|
512
|
+
return jsStringLiteral(v);
|
|
513
|
+
if (typeof v === 'number')
|
|
514
|
+
return Number.isFinite(v) ? String(v) : 'null';
|
|
515
|
+
if (typeof v === 'boolean')
|
|
516
|
+
return v ? 'true' : 'false';
|
|
517
|
+
if (v === null)
|
|
518
|
+
return 'null';
|
|
519
|
+
if (Array.isArray(v))
|
|
520
|
+
return '[' + v.map(jsLiteral).join(',') + ']';
|
|
521
|
+
if (typeof v === 'object') {
|
|
522
|
+
const parts = [];
|
|
523
|
+
for (const [k, val] of Object.entries(v)) {
|
|
524
|
+
parts.push(`${jsStringLiteral(k)}:${jsLiteral(val)}`);
|
|
525
|
+
}
|
|
526
|
+
return '{' + parts.join(',') + '}';
|
|
527
|
+
}
|
|
528
|
+
return 'undefined';
|
|
529
|
+
}
|
|
530
|
+
/** JSON.stringify is the safest way to escape arbitrary strings into JS. */
|
|
531
|
+
function jsStringLiteral(s) {
|
|
532
|
+
return JSON.stringify(s);
|
|
533
|
+
}
|
|
534
|
+
function existsSyncSafe(path) {
|
|
535
|
+
try {
|
|
536
|
+
return existsSync(path);
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function isDirectorySafe(path) {
|
|
543
|
+
try {
|
|
544
|
+
return statSync(path).isDirectory();
|
|
545
|
+
}
|
|
546
|
+
catch {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Deep merge objects (replaces lodash.merge) */
|
|
2
|
+
export function merge(target, ...sources) {
|
|
3
|
+
for (const source of sources) {
|
|
4
|
+
if (!source)
|
|
5
|
+
continue;
|
|
6
|
+
for (const key of Object.keys(source)) {
|
|
7
|
+
const targetVal = target[key];
|
|
8
|
+
const sourceVal = source[key];
|
|
9
|
+
if (sourceVal !== undefined) {
|
|
10
|
+
if (isPlainObject(targetVal) && isPlainObject(sourceVal)) {
|
|
11
|
+
merge(targetVal, sourceVal);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
target[key] = sourceVal;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return target;
|
|
20
|
+
}
|
|
21
|
+
function isPlainObject(val) {
|
|
22
|
+
return typeof val === 'object' && val !== null && !Array.isArray(val) && Object.getPrototypeOf(val) === Object.prototype;
|
|
23
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a `--globals` CLI argument into the set of `/register` subpaths
|
|
3
|
+
* that must be injected into the build.
|
|
4
|
+
*
|
|
5
|
+
* The argument is a comma-separated list of identifiers or group names.
|
|
6
|
+
* Group names (`node`, `web`, `dom`) expand to all identifiers in that group.
|
|
7
|
+
* Unknown tokens are silently ignored. Empty or whitespace-only input returns
|
|
8
|
+
* an empty set.
|
|
9
|
+
*
|
|
10
|
+
* Examples:
|
|
11
|
+
* resolveGlobalsList('fetch,Buffer,process')
|
|
12
|
+
* → Set { 'fetch/register', '@gjsify/buffer/register', '@gjsify/node-globals/register' }
|
|
13
|
+
*
|
|
14
|
+
* resolveGlobalsList('node,web')
|
|
15
|
+
* → Set { '@gjsify/buffer/register', '@gjsify/node-globals/register', 'fetch/register', … }
|
|
16
|
+
*
|
|
17
|
+
* resolveGlobalsList('')
|
|
18
|
+
* → Set { }
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveGlobalsList(globalsArg: string): Set<string>;
|
|
21
|
+
/**
|
|
22
|
+
* Write a stub ESM file with `import` statements for the given register
|
|
23
|
+
* paths and return its absolute path, suitable for passing to esbuild's
|
|
24
|
+
* `inject` option via the plugin's `autoGlobalsInject` field.
|
|
25
|
+
*
|
|
26
|
+
* The file lives inside `<cwd>/node_modules/.cache/gjsify/` so esbuild's
|
|
27
|
+
* module resolver can follow the bare specifiers in the generated imports.
|
|
28
|
+
*
|
|
29
|
+
* The file name is hashed by content so repeated builds with the same
|
|
30
|
+
* set reuse the same file (no churn, idempotent on disk).
|
|
31
|
+
*/
|
|
32
|
+
export declare function writeRegisterInjectFile(registerPaths: Set<string>, cwd?: string): Promise<string | null>;
|