@gjsify/cli 0.4.28 → 0.4.30

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 (82) hide show
  1. package/dist/cli.gjs.mjs +132 -132
  2. package/lib/actions/barrels-generate.js +1 -5
  3. package/lib/actions/build.d.ts +3 -3
  4. package/lib/actions/build.js +56 -64
  5. package/lib/bundler-pick.d.ts +3 -3
  6. package/lib/bundler-pick.js +5 -6
  7. package/lib/commands/build.d.ts +1 -1
  8. package/lib/commands/build.js +37 -31
  9. package/lib/commands/check.js +3 -3
  10. package/lib/commands/create.d.ts +1 -1
  11. package/lib/commands/dlx.d.ts +1 -1
  12. package/lib/commands/fix.js +33 -23
  13. package/lib/commands/flatpak/build.js +6 -2
  14. package/lib/commands/flatpak/check.js +9 -3
  15. package/lib/commands/flatpak/ci.js +1 -2
  16. package/lib/commands/flatpak/deps.js +1 -2
  17. package/lib/commands/flatpak/diff.js +2 -6
  18. package/lib/commands/flatpak/init.js +19 -19
  19. package/lib/commands/flatpak/release.js +2 -2
  20. package/lib/commands/flatpak/scaffold.js +3 -11
  21. package/lib/commands/flatpak/sync-flathub.js +5 -9
  22. package/lib/commands/flatpak/utils.js +1 -6
  23. package/lib/commands/foreach.d.ts +1 -1
  24. package/lib/commands/foreach.js +5 -14
  25. package/lib/commands/format.js +54 -41
  26. package/lib/commands/generate-installer.d.ts +1 -1
  27. package/lib/commands/gettext.d.ts +1 -1
  28. package/lib/commands/gettext.js +8 -15
  29. package/lib/commands/gresource.d.ts +1 -1
  30. package/lib/commands/gresource.js +8 -13
  31. package/lib/commands/gsettings.d.ts +1 -1
  32. package/lib/commands/gsettings.js +7 -8
  33. package/lib/commands/info.d.ts +1 -1
  34. package/lib/commands/install.d.ts +1 -1
  35. package/lib/commands/install.js +45 -13
  36. package/lib/commands/lint.d.ts +1 -1
  37. package/lib/commands/lint.js +22 -22
  38. package/lib/commands/pack.d.ts +1 -1
  39. package/lib/commands/pack.js +29 -17
  40. package/lib/commands/publish.d.ts +1 -1
  41. package/lib/commands/publish.js +17 -18
  42. package/lib/commands/run.d.ts +1 -1
  43. package/lib/commands/run.js +2 -6
  44. package/lib/commands/self-update.d.ts +1 -1
  45. package/lib/commands/self-update.js +1 -3
  46. package/lib/commands/showcase.d.ts +1 -1
  47. package/lib/commands/showcase.js +1 -1
  48. package/lib/commands/system-check.d.ts +1 -1
  49. package/lib/commands/system-check.js +8 -11
  50. package/lib/commands/test.js +12 -8
  51. package/lib/commands/uninstall.d.ts +1 -1
  52. package/lib/commands/uninstall.js +1 -3
  53. package/lib/commands/upgrade.d.ts +1 -1
  54. package/lib/commands/upgrade.js +109 -120
  55. package/lib/commands/workspace.d.ts +1 -1
  56. package/lib/commands/workspace.js +1 -3
  57. package/lib/config.js +18 -13
  58. package/lib/index.js +3 -1
  59. package/lib/templates/install.mjs.tmpl +20 -14
  60. package/lib/templates/oxfmtrc.tmpl +54 -0
  61. package/lib/templates/oxlintrc.json.tmpl +35 -0
  62. package/lib/types/command.d.ts +1 -1
  63. package/lib/types/config-data.d.ts +23 -13
  64. package/lib/types/cosmiconfig-result.d.ts +1 -1
  65. package/lib/utils/check-system-deps.js +10 -4
  66. package/lib/utils/detect-native-packages.js +1 -1
  67. package/lib/utils/dlx-cache.js +2 -7
  68. package/lib/utils/install-backend-native.d.ts +2 -2
  69. package/lib/utils/install-backend-native.js +112 -58
  70. package/lib/utils/install-backend.js +2 -1
  71. package/lib/utils/install-global.js +1 -3
  72. package/lib/utils/normalize-bundler-options.js +52 -17
  73. package/lib/utils/oxc-resolve.d.ts +63 -0
  74. package/lib/utils/oxc-resolve.js +264 -0
  75. package/lib/utils/pkg-json-edit.js +1 -6
  76. package/lib/utils/run-gjs.js +1 -4
  77. package/lib/utils/run-lifecycle-script.js +3 -7
  78. package/lib/utils/workspace-root.js +3 -1
  79. package/package.json +17 -17
  80. package/lib/templates/biome.json.tmpl +0 -79
  81. package/lib/utils/biome-resolve.d.ts +0 -47
  82. package/lib/utils/biome-resolve.js +0 -204
@@ -13,18 +13,18 @@
13
13
  //
14
14
  // Out of scope (still deferred): peerDependencies validation,
15
15
  // lifecycle scripts, git/file specs.
16
- import * as fs from "node:fs";
17
- import * as path from "node:path";
18
- import * as os from "node:os";
19
- import { Range, SemVer, maxSatisfying, satisfies, } from "@gjsify/semver";
20
- import { DEFAULT_REGISTRY, fetchPackument, fetchTarball, parseNpmrc, } from "@gjsify/npm-registry";
21
- import { extractTarball } from "@gjsify/tar";
22
- const DEFAULT_CONCURRENCY = Number(process.env.GJSIFY_INSTALL_CONCURRENCY ?? "8") || 8;
23
- const LOCKFILE_NAME = "gjsify-lock.json";
16
+ import * as fs from 'node:fs';
17
+ import * as path from 'node:path';
18
+ import * as os from 'node:os';
19
+ import { Range, SemVer, maxSatisfying, satisfies } from '@gjsify/semver';
20
+ import { DEFAULT_REGISTRY, fetchPackument, fetchTarball, parseNpmrc, } from '@gjsify/npm-registry';
21
+ import { extractTarball } from '@gjsify/tar';
22
+ const DEFAULT_CONCURRENCY = Number(process.env.GJSIFY_INSTALL_CONCURRENCY ?? '8') || 8;
23
+ const LOCKFILE_NAME = 'gjsify-lock.json';
24
24
  const LOCKFILE_VERSION = 2;
25
25
  export async function installPackagesNative(opts) {
26
26
  if (opts.specs.length === 0) {
27
- throw new Error("installPackagesNative: empty specs list");
27
+ throw new Error('installPackagesNative: empty specs list');
28
28
  }
29
29
  fs.mkdirSync(opts.prefix, { recursive: true });
30
30
  const npmrc = await loadNpmrc(opts);
@@ -47,25 +47,25 @@ export async function installPackagesNative(opts) {
47
47
  throw new Error(`install: --immutable but ${lockfilePath} is stale.\n${drift}\n` +
48
48
  `Re-run \`gjsify install\` (without --immutable) to refresh the lockfile.`);
49
49
  }
50
- log("install: --immutable, using lockfile (%d package(s))", Object.keys(existingLock.packages).length);
50
+ log('install: --immutable, using lockfile (%d package(s))', Object.keys(existingLock.packages).length);
51
51
  nodes = lockfileToNodes(existingLock);
52
52
  }
53
53
  else if (existingLock && lockfileMatchesRequest(existingLock, opts.specs)) {
54
- log("install: using lockfile (%d package(s))", Object.keys(existingLock.packages).length);
54
+ log('install: using lockfile (%d package(s))', Object.keys(existingLock.packages).length);
55
55
  nodes = lockfileToNodes(existingLock);
56
56
  }
57
57
  else {
58
- log("install: resolving %d top-level spec(s) → %s", opts.specs.length, opts.prefix);
58
+ log('install: resolving %d top-level spec(s) → %s', opts.specs.length, opts.prefix);
59
59
  nodes = await resolveDeps(opts.specs, npmrc, log, opts.overrides, opts.skipDeps);
60
60
  if (opts.lockfile) {
61
61
  writeLockfile(lockfilePath, opts.specs, nodes);
62
- log("install: wrote %s (%d entries)", LOCKFILE_NAME, nodes.length);
62
+ log('install: wrote %s (%d entries)', LOCKFILE_NAME, nodes.length);
63
63
  }
64
64
  }
65
- log("install: downloading %d tarball(s)", nodes.length);
65
+ log('install: downloading %d tarball(s)', nodes.length);
66
66
  await downloadAndExtractAll(nodes, opts.prefix, npmrc, log);
67
67
  await linkBins(nodes, opts.prefix, log);
68
- log("install: done");
68
+ log('install: done');
69
69
  // Surface the top-level requested packages so callers can update
70
70
  // package.json with the resolved version (mirrors `npm install --save`
71
71
  // behavior). Sub-deps are not included.
@@ -94,14 +94,14 @@ function topLevelResolutions(specs, nodes) {
94
94
  return out;
95
95
  }
96
96
  function parseSpecName(spec) {
97
- if (spec.startsWith("@")) {
98
- const slash = spec.indexOf("/");
97
+ if (spec.startsWith('@')) {
98
+ const slash = spec.indexOf('/');
99
99
  if (slash === -1)
100
100
  return spec;
101
- const at = spec.indexOf("@", slash + 1);
101
+ const at = spec.indexOf('@', slash + 1);
102
102
  return at === -1 ? spec : spec.slice(0, at);
103
103
  }
104
- const at = spec.indexOf("@");
104
+ const at = spec.indexOf('@');
105
105
  return at === -1 ? spec : spec.slice(0, at);
106
106
  }
107
107
  /**
@@ -128,7 +128,7 @@ async function resolveDeps(specs, npmrc, log, overrides, skipDeps) {
128
128
  return range;
129
129
  if (override === range)
130
130
  return range;
131
- log("install: override %s %s → %s", name, range, override);
131
+ log('install: override %s %s → %s', name, range, override);
132
132
  return override;
133
133
  };
134
134
  const packumentCache = new Map();
@@ -139,7 +139,7 @@ async function resolveDeps(specs, npmrc, log, overrides, skipDeps) {
139
139
  const fresh = fetchPackument(name, {
140
140
  npmrc,
141
141
  onRetry: ({ attempt, error, delayMs }) => {
142
- log("packument %s: retry %d after %dms (%s)", name, attempt, delayMs, errMsg(error));
142
+ log('packument %s: retry %d after %dms (%s)', name, attempt, delayMs, errMsg(error));
143
143
  },
144
144
  });
145
145
  packumentCache.set(name, fresh);
@@ -200,13 +200,23 @@ async function resolveDeps(specs, npmrc, log, overrides, skipDeps) {
200
200
  if (installPath === `node_modules/${edge.name}`) {
201
201
  root.set(edge.name, node);
202
202
  }
203
- log("resolve: %s@%s ← %s (at %s)", edge.name, version, edge.range, installPath);
203
+ log('resolve: %s@%s ← %s (at %s)', edge.name, version, edge.range, installPath);
204
204
  if (!skipDeps) {
205
205
  for (const [depName, depRange] of Object.entries(node.dependencies)) {
206
- queue.push({ from: installPath, name: depName, range: applyOverride(depName, depRange), required: true });
206
+ queue.push({
207
+ from: installPath,
208
+ name: depName,
209
+ range: applyOverride(depName, depRange),
210
+ required: true,
211
+ });
207
212
  }
208
213
  for (const [depName, depRange] of Object.entries(node.optionalDependencies)) {
209
- queue.push({ from: installPath, name: depName, range: applyOverride(depName, depRange), required: false });
214
+ queue.push({
215
+ from: installPath,
216
+ name: depName,
217
+ range: applyOverride(depName, depRange),
218
+ required: false,
219
+ });
210
220
  }
211
221
  }
212
222
  }
@@ -214,7 +224,7 @@ async function resolveDeps(specs, npmrc, log, overrides, skipDeps) {
214
224
  // Optional deps that fail to resolve are skipped — yarn/npm
215
225
  // behavior. Required deps re-throw.
216
226
  if (!edge.required) {
217
- log("resolve: optional dep %s@%s skipped (%s)", edge.name, edge.range, e.message);
227
+ log('resolve: optional dep %s@%s skipped (%s)', edge.name, edge.range, e.message);
218
228
  continue;
219
229
  }
220
230
  throw e;
@@ -245,12 +255,12 @@ function findVisible(requesterPath, name, byPath) {
245
255
  // eslint-disable-next-line no-constant-condition
246
256
  while (true) {
247
257
  // Find the deepest `/node_modules/<pkg>` in `p`, strip it.
248
- const idx = p.lastIndexOf("/node_modules/");
258
+ const idx = p.lastIndexOf('/node_modules/');
249
259
  if (idx < 0)
250
260
  break;
251
261
  p = p.slice(0, idx);
252
262
  candidates.push(`${p}/node_modules/${name}`);
253
- if (p === "")
263
+ if (p === '')
254
264
  break;
255
265
  }
256
266
  }
@@ -301,10 +311,10 @@ function readLockfile(lockfilePath) {
301
311
  if (!fs.existsSync(lockfilePath))
302
312
  return null;
303
313
  try {
304
- const parsed = JSON.parse(fs.readFileSync(lockfilePath, "utf-8"));
314
+ const parsed = JSON.parse(fs.readFileSync(lockfilePath, 'utf-8'));
305
315
  if (parsed.lockfileVersion !== LOCKFILE_VERSION)
306
316
  return null;
307
- if (!parsed.packages || typeof parsed.packages !== "object")
317
+ if (!parsed.packages || typeof parsed.packages !== 'object')
308
318
  return null;
309
319
  return parsed;
310
320
  }
@@ -330,7 +340,7 @@ function writeLockfile(lockfilePath, specs, nodes) {
330
340
  requested: [...specs],
331
341
  packages,
332
342
  };
333
- fs.writeFileSync(lockfilePath, JSON.stringify(lockfile, null, 2) + "\n");
343
+ fs.writeFileSync(lockfilePath, JSON.stringify(lockfile, null, 2) + '\n');
334
344
  }
335
345
  function lockfileToNodes(lockfile) {
336
346
  return Object.entries(lockfile.packages).map(([installPath, entry]) => ({
@@ -349,8 +359,8 @@ function lockfileToNodes(lockfile) {
349
359
  function nameFromInstallPath(installPath) {
350
360
  // Last `node_modules/` boundary, then the rest is the package name
351
361
  // (single segment unscoped, or `@scope/pkg` scoped).
352
- const idx = installPath.lastIndexOf("/node_modules/");
353
- const after = idx < 0 ? installPath.replace(/^node_modules\//, "") : installPath.slice(idx + "/node_modules/".length);
362
+ const idx = installPath.lastIndexOf('/node_modules/');
363
+ const after = idx < 0 ? installPath.replace(/^node_modules\//, '') : installPath.slice(idx + '/node_modules/'.length);
354
364
  return after;
355
365
  }
356
366
  function lockfileMatchesRequest(lockfile, specs) {
@@ -381,10 +391,10 @@ function describeLockfileDrift(lockfile, specs) {
381
391
  return null;
382
392
  const lines = [];
383
393
  if (added.length > 0)
384
- lines.push(` + ${added.sort().join("\n + ")}`);
394
+ lines.push(` + ${added.sort().join('\n + ')}`);
385
395
  if (removed.length > 0)
386
- lines.push(` - ${removed.sort().join("\n - ")}`);
387
- return lines.join("\n");
396
+ lines.push(` - ${removed.sort().join('\n - ')}`);
397
+ return lines.join('\n');
388
398
  }
389
399
  // Exported for unit-testing — keep the function name + signature
390
400
  // stable, the install-backend itself still calls it via the local
@@ -400,25 +410,25 @@ export function parseSpec(raw) {
400
410
  // shipped only prereleases (4.0.0-rc.17 is the `latest` tag, no
401
411
  // stable 4.x yet) and `*` was selecting the abandoned 3.3.0
402
412
  // instead.
403
- if (raw.startsWith("@")) {
404
- const slash = raw.indexOf("/");
413
+ if (raw.startsWith('@')) {
414
+ const slash = raw.indexOf('/');
405
415
  if (slash < 0)
406
416
  throw new Error(`Invalid spec (scoped name without slash): ${raw}`);
407
- const at = raw.indexOf("@", slash);
417
+ const at = raw.indexOf('@', slash);
408
418
  if (at < 0)
409
- return { name: raw, range: "latest" };
410
- return { name: raw.slice(0, at), range: raw.slice(at + 1) || "latest" };
419
+ return { name: raw, range: 'latest' };
420
+ return { name: raw.slice(0, at), range: raw.slice(at + 1) || 'latest' };
411
421
  }
412
- const at = raw.indexOf("@");
422
+ const at = raw.indexOf('@');
413
423
  if (at < 0)
414
- return { name: raw, range: "latest" };
415
- return { name: raw.slice(0, at), range: raw.slice(at + 1) || "latest" };
424
+ return { name: raw, range: 'latest' };
425
+ return { name: raw.slice(0, at), range: raw.slice(at + 1) || 'latest' };
416
426
  }
417
427
  // Exported for unit-testing. Internal API.
418
428
  export function pickVersion(packument, range) {
419
429
  // dist-tag fast path: `latest`, `next`, ...
420
- if (packument["dist-tags"][range])
421
- return packument["dist-tags"][range];
430
+ if (packument['dist-tags'][range])
431
+ return packument['dist-tags'][range];
422
432
  // Validate range early so a typo fails loudly.
423
433
  let parsedRange;
424
434
  try {
@@ -442,8 +452,7 @@ async function downloadAndExtractAll(nodes, prefix, npmrc, log) {
442
452
  // Sort by install-path depth ascending so parents extract before
443
453
  // children. Extracting a parent on top of an existing child would
444
454
  // wipe out the child.
445
- const queue = [...nodes].sort((a, b) => depth(a.installPath) - depth(b.installPath) ||
446
- (a.installPath < b.installPath ? -1 : 1));
455
+ const queue = [...nodes].sort((a, b) => depth(a.installPath) - depth(b.installPath) || (a.installPath < b.installPath ? -1 : 1));
447
456
  const workers = [];
448
457
  const concurrency = Math.max(1, Math.min(DEFAULT_CONCURRENCY, queue.length));
449
458
  // Parents (depth 1) are extracted serially first to avoid concurrent
@@ -477,22 +486,69 @@ async function downloadAndExtractAll(nodes, prefix, npmrc, log) {
477
486
  }
478
487
  async function extractOne(node, prefix, npmrc, log) {
479
488
  const dest = path.join(prefix, node.installPath);
480
- log("fetch: %s@%s %s (→ %s)", node.name, node.version, node.tarballUrl, node.installPath);
489
+ // Defense-in-depth against the workspace-source-wipe data-loss bug:
490
+ // every extractable node MUST land inside a `node_modules/` directory.
491
+ // The resolver only ever produces `installPath`s of that shape, so this
492
+ // can only fail if a workspace package leaked into the fetch/extract
493
+ // queue (the root cause fixed in `workspaceInstall`). Refusing here means
494
+ // a regression in the resolver can never again `rmSync` a working-tree
495
+ // source dir — the realpath check additionally rejects a `dest` that
496
+ // resolves THROUGH a symlink into a directory outside node_modules.
497
+ assertNodeModulesDest(dest, node);
498
+ log('fetch: %s@%s ← %s (→ %s)', node.name, node.version, node.tarballUrl, node.installPath);
481
499
  const bytes = await fetchTarball(node.tarballUrl, {
482
500
  npmrc,
483
501
  integrity: node.integrity,
484
502
  onRetry: ({ attempt, error, delayMs }) => {
485
- log("tarball %s@%s: retry %d after %dms (%s)", node.name, node.version, attempt, delayMs, errMsg(error));
503
+ log('tarball %s@%s: retry %d after %dms (%s)', node.name, node.version, attempt, delayMs, errMsg(error));
486
504
  },
487
505
  });
488
506
  fs.rmSync(dest, { recursive: true, force: true });
489
507
  fs.mkdirSync(dest, { recursive: true });
490
508
  await extractTarball(bytes, dest);
491
509
  }
510
+ /**
511
+ * Guard: a tarball may only be extracted into a `node_modules/` directory.
512
+ *
513
+ * Two checks, both belt-and-suspenders against ever wiping a working-tree
514
+ * source dir (the install-deletes-workspace-sources data-loss bug):
515
+ *
516
+ * 1. The logical `installPath` must contain a `node_modules` path segment.
517
+ * The resolver always produces such paths; a workspace package that
518
+ * slipped into the queue would not.
519
+ * 2. If `dest` already exists and resolves (via symlink) to a directory
520
+ * whose REAL path is not under a `node_modules/` segment, refuse. This
521
+ * catches the case where `node_modules/<name>` is a symlink to a
522
+ * workspace's source tree — `rmSync(dest, { recursive: true })` would
523
+ * then delete the link's target contents.
524
+ */
525
+ function assertNodeModulesDest(dest, node) {
526
+ const segments = dest.split(path.sep);
527
+ if (!segments.includes('node_modules')) {
528
+ throw new Error(`gjsify install: refusing to extract ${node.name}@${node.version} into ${dest} — ` +
529
+ `target is not inside a node_modules/ directory. This would overwrite working-tree files. ` +
530
+ `A workspace package likely leaked into the fetch queue (it must be symlinked, not fetched).`);
531
+ }
532
+ let real;
533
+ try {
534
+ real = fs.realpathSync(dest);
535
+ }
536
+ catch {
537
+ // `dest` doesn't exist yet (fresh install) — nothing to resolve, the
538
+ // logical-path check above is sufficient.
539
+ return;
540
+ }
541
+ const realSegments = real.split(path.sep);
542
+ if (!realSegments.includes('node_modules')) {
543
+ throw new Error(`gjsify install: refusing to extract ${node.name}@${node.version} — ${dest} resolves to ${real}, ` +
544
+ `which is outside any node_modules/ directory (likely a symlink to a workspace source tree). ` +
545
+ `Extracting here would delete working-tree source files.`);
546
+ }
547
+ }
492
548
  function depth(installPath) {
493
549
  // Count `node_modules/` segments to know nesting depth.
494
550
  // `node_modules/foo` = 1, `node_modules/foo/node_modules/bar` = 2, etc.
495
- return installPath.split("/node_modules/").length;
551
+ return installPath.split('/node_modules/').length;
496
552
  }
497
553
  async function linkBins(nodes, prefix, log) {
498
554
  // Only root-level packages publish bins into the top-level
@@ -500,7 +556,7 @@ async function linkBins(nodes, prefix, log) {
500
556
  // direct dependents through the nested .bin (npm matches this) — we
501
557
  // omit nested-bin linking for now since no consumer of the install
502
558
  // backend depends on it (gjsify's own use cases all hit root bins).
503
- const binDir = path.join(prefix, "node_modules", ".bin");
559
+ const binDir = path.join(prefix, 'node_modules', '.bin');
504
560
  let created = 0;
505
561
  for (const node of nodes) {
506
562
  if (!node.bin)
@@ -536,15 +592,13 @@ async function linkBins(nodes, prefix, log) {
536
592
  }
537
593
  }
538
594
  if (created > 0)
539
- log("bin: linked %d entry(ies) under .bin/", created);
595
+ log('bin: linked %d entry(ies) under .bin/', created);
540
596
  }
541
597
  function normalizeBin(pkgName, bin) {
542
598
  const out = new Map();
543
- if (typeof bin === "string") {
599
+ if (typeof bin === 'string') {
544
600
  // String form is shorthand for `{ <last-segment-of-pkgName>: <bin> }`.
545
- const baseName = pkgName.startsWith("@")
546
- ? pkgName.slice(pkgName.indexOf("/") + 1)
547
- : pkgName;
601
+ const baseName = pkgName.startsWith('@') ? pkgName.slice(pkgName.indexOf('/') + 1) : pkgName;
548
602
  out.set(baseName, bin);
549
603
  return out;
550
604
  }
@@ -565,11 +619,11 @@ async function loadNpmrc(opts) {
565
619
  // workspace-root one too; the gjsify project-local case is what users
566
620
  // hit most often (mock-registry tests, scoped-registry overrides), so
567
621
  // we cover that explicitly.
568
- for (const candidate of [path.join(home, ".npmrc"), path.join(opts.prefix, ".npmrc")]) {
622
+ for (const candidate of [path.join(home, '.npmrc'), path.join(opts.prefix, '.npmrc')]) {
569
623
  if (!fs.existsSync(candidate))
570
624
  continue;
571
625
  try {
572
- const projectParsed = parseNpmrc(fs.readFileSync(candidate, "utf-8"));
626
+ const projectParsed = parseNpmrc(fs.readFileSync(candidate, 'utf-8'));
573
627
  parsed = { ...parsed, ...projectParsed, scopes: { ...parsed.scopes, ...projectParsed.scopes } };
574
628
  }
575
629
  catch (e) {
@@ -40,7 +40,8 @@ async function installViaNpm({ prefix, specs, verbose, registry }) {
40
40
  '--no-package-lock',
41
41
  '--no-audit',
42
42
  '--no-fund',
43
- '--prefix', prefix,
43
+ '--prefix',
44
+ prefix,
44
45
  ...(verbose ? ['--loglevel', 'verbose'] : ['--loglevel', 'warn']),
45
46
  ...specs,
46
47
  ];
@@ -161,9 +161,7 @@ function pickBinMap(pkgName, pkgJson) {
161
161
  function normalizeBin(pkgName, bin) {
162
162
  const out = new Map();
163
163
  if (typeof bin === 'string') {
164
- const baseName = pkgName.startsWith('@')
165
- ? pkgName.slice(pkgName.indexOf('/') + 1)
166
- : pkgName;
164
+ const baseName = pkgName.startsWith('@') ? pkgName.slice(pkgName.indexOf('/') + 1) : pkgName;
167
165
  out.set(baseName, bin);
168
166
  return out;
169
167
  }
@@ -2,10 +2,46 @@
2
2
  // field on `.gjsifyrc.js` / `package.json#gjsify` into the equivalent
3
3
  // `bundler?: RolldownOptions` shape. Logs a single warning per build.
4
4
  //
5
- // Drop in 0.5.0.
5
+ // Also handles the top-level `bundler.define` alias: Rolldown reads
6
+ // `transform.define`, not a top-level `define`. A user who flat-renames
7
+ // `esbuild: { define: {...} }` → `bundler: { define: {...} }` puts `define`
8
+ // at the top level where Rolldown silently ignores it, causing
9
+ // `ReferenceError: <TOKEN> is not defined` at GJS load time. We auto-map
10
+ // top-level `bundler.define` into `bundler.transform.define` and emit a
11
+ // one-time warning so the user can correct their config.
12
+ //
13
+ // Drop `esbuild` shim in 0.5.0; the `bundler.define` alias is permanent.
6
14
  let warnedOnce = false;
15
+ let warnedDefineOnce = false;
7
16
  export function normalizeBundlerOptions(configData) {
8
- const fromBundler = (configData.bundler ?? {});
17
+ const raw = (configData.bundler ?? {});
18
+ // --- Top-level bundler.define alias ------------------------------------------
19
+ // Rolldown only reads `transform.define`; a top-level `define` key is silently
20
+ // ignored. Detect it, warn once, and move it into `transform.define` so the
21
+ // user's flat esbuild→bundler rename of `define` Just Works.
22
+ let fromBundler = raw;
23
+ if (typeof raw['define'] === 'object' &&
24
+ raw['define'] !== null) {
25
+ if (!warnedDefineOnce) {
26
+ warnedDefineOnce = true;
27
+ // eslint-disable-next-line no-console
28
+ console.warn("[gjsify] WARNING: 'bundler.define' is not a valid Rolldown option and would be " +
29
+ "silently ignored — it has been auto-mapped to 'bundler.transform.define'. " +
30
+ "Move 'define' under 'bundler.transform.define' in your config to suppress " +
31
+ 'this warning and avoid a ReferenceError at GJS load time.');
32
+ }
33
+ const { define: topLevelDefine, ...rest } = raw;
34
+ fromBundler = {
35
+ ...rest,
36
+ transform: {
37
+ ...rest.transform,
38
+ define: {
39
+ ...topLevelDefine,
40
+ ...rest.transform?.define,
41
+ },
42
+ },
43
+ };
44
+ }
9
45
  if (!configData.esbuild)
10
46
  return fromBundler;
11
47
  if (!warnedOnce) {
@@ -22,19 +58,19 @@ export function normalizeBundlerOptions(configData) {
22
58
  // user-provided config and `input` must survive the merge.
23
59
  const out = { ...fromEsbuild, ...fromBundler };
24
60
  if (fromEsbuild.output || fromBundler.output) {
25
- out.output = { ...(fromEsbuild.output ?? {}), ...(fromBundler.output ?? {}) };
61
+ out.output = { ...fromEsbuild.output, ...fromBundler.output };
26
62
  }
27
63
  if (fromEsbuild.transform || fromBundler.transform) {
28
- out.transform = { ...(fromEsbuild.transform ?? {}), ...(fromBundler.transform ?? {}) };
64
+ out.transform = { ...fromEsbuild.transform, ...fromBundler.transform };
29
65
  if (fromEsbuild.transform?.define || fromBundler.transform?.define) {
30
66
  out.transform.define = {
31
- ...(fromEsbuild.transform?.define ?? {}),
32
- ...(fromBundler.transform?.define ?? {}),
67
+ ...fromEsbuild.transform?.define,
68
+ ...fromBundler.transform?.define,
33
69
  };
34
70
  }
35
71
  }
36
72
  if (fromEsbuild.resolve || fromBundler.resolve) {
37
- out.resolve = { ...(fromEsbuild.resolve ?? {}), ...(fromBundler.resolve ?? {}) };
73
+ out.resolve = { ...fromEsbuild.resolve, ...fromBundler.resolve };
38
74
  }
39
75
  return out;
40
76
  }
@@ -54,10 +90,7 @@ function legacyEsbuildToRolldown(esb) {
54
90
  output.minify = esb.minify;
55
91
  if (esb.sourcemap !== undefined) {
56
92
  // esbuild has 'external' / 'both' which Rolldown doesn't — coerce to boolean.
57
- output.sourcemap =
58
- esb.sourcemap === 'inline'
59
- ? 'inline'
60
- : Boolean(esb.sourcemap);
93
+ output.sourcemap = esb.sourcemap === 'inline' ? 'inline' : Boolean(esb.sourcemap);
61
94
  }
62
95
  if (esb.banner?.js !== undefined)
63
96
  output.banner = esb.banner.js;
@@ -80,10 +113,12 @@ function legacyEsbuildToRolldown(esb) {
80
113
  out.transform = transform;
81
114
  if (Object.keys(resolve).length > 0)
82
115
  out.resolve = resolve;
83
- // Discarded silently:
116
+ // Discarded (handled elsewhere):
84
117
  // esb.inject — esbuild's array-of-side-effect-files; surfaced at the
85
118
  // CLI layer instead, via input expansion.
86
- // esb.loader — Rolldown infers module types from extensions natively.
119
+ // esb.loader — replaced by top-level `gjsify.loaders` (see ConfigData);
120
+ // migration: `esbuild.loader: { '.png': 'dataurl', '.glsl': 'text' }`
121
+ // → `loaders: { '.png': 'dataurl', '.glsl': 'text' }`.
87
122
  return out;
88
123
  }
89
124
  /**
@@ -108,16 +143,16 @@ export function mergeBundlerOptions(base, overrides) {
108
143
  const { input: _ignoredInput, external: _ignoredExternal, ...overridesRest } = overrides;
109
144
  const out = { ...base, ...overridesRest };
110
145
  if (base.output || overrides.output) {
111
- out.output = { ...(base.output ?? {}), ...(overrides.output ?? {}) };
146
+ out.output = { ...base.output, ...overrides.output };
112
147
  }
113
148
  if (base.transform || overrides.transform) {
114
- out.transform = { ...(base.transform ?? {}), ...(overrides.transform ?? {}) };
149
+ out.transform = { ...base.transform, ...overrides.transform };
115
150
  if (base.transform?.define || overrides.transform?.define) {
116
- out.transform.define = { ...(base.transform?.define ?? {}), ...(overrides.transform?.define ?? {}) };
151
+ out.transform.define = { ...base.transform?.define, ...overrides.transform?.define };
117
152
  }
118
153
  }
119
154
  if (base.resolve || overrides.resolve) {
120
- out.resolve = { ...(base.resolve ?? {}), ...(overrides.resolve ?? {}) };
155
+ out.resolve = { ...base.resolve, ...overrides.resolve };
121
156
  }
122
157
  return out;
123
158
  }
@@ -0,0 +1,63 @@
1
+ export type OxcTool = 'oxlint' | 'oxfmt';
2
+ /**
3
+ * Resolve the absolute path to a tool's Node ESM launcher
4
+ * (`node_modules/<tool>/bin/<tool>`). Walks cwd → workspace-root → parents.
5
+ *
6
+ * Throws {@link OxcNotFoundError} with a clear install hint when not found.
7
+ */
8
+ export declare function findOxcLauncher(tool: OxcTool, cwd?: string): string;
9
+ export declare class OxcNotFoundError extends Error {
10
+ tool: OxcTool;
11
+ cwd: string;
12
+ constructor(tool: OxcTool, cwd: string);
13
+ }
14
+ /**
15
+ * Walk up from a starting directory to find the nearest oxlint config
16
+ * (`.oxlintrc.json`). Returns absolute path or null.
17
+ */
18
+ export declare function findOxlintConfig(cwd?: string): string | null;
19
+ /**
20
+ * Walk up from a starting directory to find the nearest oxfmt config
21
+ * (`.oxfmtrc` / `.oxfmtrc.json`). Returns absolute path or null.
22
+ */
23
+ export declare function findOxfmtConfig(cwd?: string): string | null;
24
+ export interface RunOxcOptions {
25
+ cwd?: string;
26
+ verbose?: boolean;
27
+ }
28
+ /**
29
+ * Spawn an oxc tool (oxlint / oxfmt) via its Node ESM launcher. Inherits
30
+ * stdio so the tool's own output (diagnostics, reformatted files, summary
31
+ * lines) reaches the user.
32
+ *
33
+ * The launcher is run with the current Node executable (`process.execPath`)
34
+ * so the JS plugin host is available — required for oxlint's `jsPlugins`.
35
+ *
36
+ * Returns the exit code; never throws on non-zero exit (callers check it).
37
+ */
38
+ export declare function runOxc(tool: OxcTool, args: string[], opts?: RunOxcOptions): Promise<number>;
39
+ /** Convenience wrappers. */
40
+ export declare function runOxlint(args: string[], opts?: RunOxcOptions): Promise<number>;
41
+ export declare function runOxfmt(args: string[], opts?: RunOxcOptions): Promise<number>;
42
+ /**
43
+ * Lazy-load the embedded `.oxlintrc.json` scaffold template. The
44
+ * static-read-inliner matches this `readFileSync(new URL(<lit>,
45
+ * import.meta.url), 'utf-8')` shape at build time and inlines the file into
46
+ * the GJS bundle, so the template is available without runtime file I/O
47
+ * against the install dir. (Same mechanism the old `loadBiomeTemplate()`
48
+ * relied on — the shape must stay exactly this for the inliner to fire.)
49
+ */
50
+ export declare function loadOxlintTemplate(): string;
51
+ /** Lazy-load the embedded `.oxfmtrc` scaffold template (same inliner shape). */
52
+ export declare function loadOxfmtTemplate(): string;
53
+ /** Helper for callers to surface the install hint to the user cleanly. */
54
+ export declare function printOxcNotFound(err: OxcNotFoundError): void;
55
+ /**
56
+ * Has the given oxc tool's npm package (or its companion) been declared in the
57
+ * project's dependencies? Useful as a cheap pre-flight check — `gjsify flatpak
58
+ * init`'s post-format hook uses this to decide whether to auto-format outputs.
59
+ *
60
+ * Checks for `oxfmt` by default (the formatter), since that is what the
61
+ * post-format hook would invoke.
62
+ */
63
+ export declare function hasOxcDevDep(cwd?: string, tool?: OxcTool): boolean;