@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.
Files changed (89) hide show
  1. package/lib/app/browser.d.ts +17 -0
  2. package/lib/app/browser.js +77 -0
  3. package/lib/app/gjs.d.ts +27 -0
  4. package/lib/app/gjs.js +211 -0
  5. package/lib/app/index.d.ts +6 -0
  6. package/lib/app/index.js +3 -0
  7. package/lib/app/node.d.ts +17 -0
  8. package/lib/app/node.js +102 -0
  9. package/lib/globals.d.ts +4 -0
  10. package/lib/globals.js +9 -0
  11. package/lib/index.d.ts +17 -0
  12. package/lib/index.js +15 -0
  13. package/lib/library/index.d.ts +2 -0
  14. package/lib/library/index.js +1 -0
  15. package/lib/library/lib.d.ts +16 -0
  16. package/lib/library/lib.js +118 -0
  17. package/lib/plugin.d.ts +25 -0
  18. package/lib/plugin.js +67 -0
  19. package/lib/plugins/alias.d.ts +5 -0
  20. package/lib/plugins/alias.js +45 -0
  21. package/lib/plugins/css-as-string.d.ts +2 -0
  22. package/lib/plugins/css-as-string.js +34 -0
  23. package/lib/plugins/gjs-imports-empty.d.ts +2 -0
  24. package/lib/plugins/gjs-imports-empty.js +26 -0
  25. package/lib/plugins/process-stub.d.ts +28 -0
  26. package/lib/plugins/process-stub.js +60 -0
  27. package/lib/plugins/rewrite-node-modules-paths.d.ts +38 -0
  28. package/lib/plugins/rewrite-node-modules-paths.js +132 -0
  29. package/lib/plugins/shebang.d.ts +8 -0
  30. package/lib/plugins/shebang.js +26 -0
  31. package/lib/shims/console-gjs.d.ts +24 -0
  32. package/lib/shims/console-gjs.js +24 -0
  33. package/lib/types/app.d.ts +1 -0
  34. package/lib/types/app.js +1 -0
  35. package/lib/types/index.d.ts +3 -0
  36. package/lib/types/index.js +3 -0
  37. package/lib/types/plugin-options.d.ts +46 -0
  38. package/lib/types/plugin-options.js +1 -0
  39. package/lib/types/resolve-alias-options.d.ts +2 -0
  40. package/lib/types/resolve-alias-options.js +1 -0
  41. package/lib/utils/alias.d.ts +12 -0
  42. package/lib/utils/alias.js +29 -0
  43. package/lib/utils/auto-globals.d.ts +72 -0
  44. package/lib/utils/auto-globals.js +193 -0
  45. package/lib/utils/detect-free-globals.d.ts +18 -0
  46. package/lib/utils/detect-free-globals.js +268 -0
  47. package/lib/utils/entry-points.d.ts +2 -0
  48. package/lib/utils/entry-points.js +38 -0
  49. package/lib/utils/extension.d.ts +1 -0
  50. package/lib/utils/extension.js +7 -0
  51. package/lib/utils/index.d.ts +7 -0
  52. package/lib/utils/index.js +7 -0
  53. package/lib/utils/inline-static-reads.d.ts +11 -0
  54. package/lib/utils/inline-static-reads.js +549 -0
  55. package/lib/utils/merge.d.ts +2 -0
  56. package/lib/utils/merge.js +23 -0
  57. package/lib/utils/scan-globals.d.ts +32 -0
  58. package/lib/utils/scan-globals.js +85 -0
  59. package/package.json +68 -0
  60. package/src/app/browser.ts +102 -0
  61. package/src/app/gjs.ts +260 -0
  62. package/src/app/index.ts +6 -0
  63. package/src/app/node.ts +128 -0
  64. package/src/globals.ts +11 -0
  65. package/src/index.ts +32 -0
  66. package/src/library/index.ts +2 -0
  67. package/src/library/lib.ts +142 -0
  68. package/src/plugin.ts +91 -0
  69. package/src/plugins/alias.ts +53 -0
  70. package/src/plugins/css-as-string.ts +37 -0
  71. package/src/plugins/gjs-imports-empty.ts +29 -0
  72. package/src/plugins/process-stub.ts +91 -0
  73. package/src/plugins/rewrite-node-modules-paths.ts +169 -0
  74. package/src/plugins/shebang.ts +33 -0
  75. package/src/shims/console-gjs.ts +25 -0
  76. package/src/types/app.ts +1 -0
  77. package/src/types/index.ts +3 -0
  78. package/src/types/plugin-options.ts +48 -0
  79. package/src/types/resolve-alias-options.ts +1 -0
  80. package/src/utils/alias.ts +46 -0
  81. package/src/utils/auto-globals.ts +283 -0
  82. package/src/utils/detect-free-globals.ts +278 -0
  83. package/src/utils/entry-points.ts +48 -0
  84. package/src/utils/extension.ts +7 -0
  85. package/src/utils/index.ts +7 -0
  86. package/src/utils/inline-static-reads.ts +541 -0
  87. package/src/utils/merge.ts +22 -0
  88. package/src/utils/scan-globals.ts +91 -0
  89. 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,2 @@
1
+ /** Deep merge objects (replaces lodash.merge) */
2
+ export declare function merge<T extends Record<string, any>>(target: T, ...sources: Record<string, any>[]): T;
@@ -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>;