@gjsify/cli 0.51.1 → 0.53.0

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 (126) hide show
  1. package/dist/cli.gjs.mjs +539 -493
  2. package/lib/actions/build.d.ts +22 -0
  3. package/lib/actions/build.js +37 -9
  4. package/lib/bundler-pick.d.ts +37 -1
  5. package/lib/bundler-pick.js +108 -17
  6. package/lib/cli-app.js +7 -2
  7. package/lib/commands/affected-classify.d.ts +7 -1
  8. package/lib/commands/affected-classify.js +66 -9
  9. package/lib/commands/affected-script-refs.d.ts +20 -0
  10. package/lib/commands/affected-script-refs.js +218 -0
  11. package/lib/commands/affected.js +24 -2
  12. package/lib/commands/env.d.ts +31 -0
  13. package/lib/commands/env.js +114 -0
  14. package/lib/commands/exec.d.ts +10 -0
  15. package/lib/commands/exec.js +208 -0
  16. package/lib/commands/flatpak/diff.js +8 -2
  17. package/lib/commands/flatpak/init.js +4 -1
  18. package/lib/commands/flatpak/sources.js +4 -3
  19. package/lib/commands/flatpak/sync-flathub.d.ts +2 -0
  20. package/lib/commands/flatpak/sync-flathub.js +149 -7
  21. package/lib/commands/flatpak/utils.d.ts +10 -0
  22. package/lib/commands/flatpak/utils.js +10 -0
  23. package/lib/commands/foreach.js +5 -97
  24. package/lib/commands/format.js +26 -6
  25. package/lib/commands/index.d.ts +4 -0
  26. package/lib/commands/index.js +4 -0
  27. package/lib/commands/install.d.ts +2 -2
  28. package/lib/commands/install.js +81 -5
  29. package/lib/commands/link.d.ts +13 -0
  30. package/lib/commands/link.js +243 -0
  31. package/lib/commands/onboard.js +18 -2
  32. package/lib/commands/publish.d.ts +34 -0
  33. package/lib/commands/publish.js +153 -8
  34. package/lib/commands/self-update.js +2 -1
  35. package/lib/commands/ship.js +19 -1
  36. package/lib/commands/test.d.ts +18 -9
  37. package/lib/commands/test.js +56 -11
  38. package/lib/commands/tsc.js +4 -4
  39. package/lib/commands/upgrade.d.ts +15 -0
  40. package/lib/commands/upgrade.js +76 -2
  41. package/lib/commands/webext/index.d.ts +2 -0
  42. package/lib/commands/webext/index.js +15 -0
  43. package/lib/commands/webext/subcommands.d.ts +16 -0
  44. package/lib/commands/webext/subcommands.js +204 -0
  45. package/lib/types/config-data.d.ts +2 -2
  46. package/lib/utils/bin-shim.d.ts +8 -2
  47. package/lib/utils/bin-shim.js +6 -5
  48. package/lib/utils/bundle-inputs.d.ts +84 -0
  49. package/lib/utils/bundle-inputs.js +268 -0
  50. package/lib/utils/check-system-deps.d.ts +5 -19
  51. package/lib/utils/check-system-deps.js +31 -94
  52. package/lib/utils/dev-link.d.ts +225 -0
  53. package/lib/utils/dev-link.js +684 -0
  54. package/lib/utils/exec-bin.d.ts +137 -0
  55. package/lib/utils/exec-bin.js +321 -0
  56. package/lib/utils/gi-runtime-paths.js +3 -2
  57. package/lib/utils/gitignore.d.ts +15 -0
  58. package/lib/utils/gitignore.js +178 -0
  59. package/lib/utils/gjsify-shim.d.ts +47 -0
  60. package/lib/utils/gjsify-shim.js +97 -8
  61. package/lib/utils/hang-watchdog.d.ts +95 -0
  62. package/lib/utils/hang-watchdog.js +194 -0
  63. package/lib/utils/install-backend-native.d.ts +33 -1
  64. package/lib/utils/install-backend-native.js +184 -20
  65. package/lib/utils/install-extraneous.d.ts +90 -0
  66. package/lib/utils/install-extraneous.js +120 -0
  67. package/lib/utils/install-lock.js +4 -25
  68. package/lib/utils/install-tarball-cache.d.ts +37 -9
  69. package/lib/utils/install-tarball-cache.js +59 -14
  70. package/lib/utils/native-skip-scan.d.ts +20 -0
  71. package/lib/utils/native-skip-scan.js +343 -0
  72. package/lib/utils/node-script.js +1 -1
  73. package/lib/utils/normalize-bundler-options.js +11 -0
  74. package/lib/utils/oxc-resolve.js +42 -3
  75. package/lib/utils/process-table.d.ts +22 -0
  76. package/lib/utils/process-table.js +188 -0
  77. package/lib/utils/prune-prefix.js +1 -1
  78. package/lib/utils/publish-pins.d.ts +38 -0
  79. package/lib/utils/publish-pins.js +47 -0
  80. package/lib/utils/publish-readback.d.ts +103 -17
  81. package/lib/utils/publish-readback.js +326 -47
  82. package/lib/utils/run-gjs.d.ts +15 -1
  83. package/lib/utils/run-gjs.js +98 -13
  84. package/lib/utils/run-node.d.ts +2 -0
  85. package/lib/utils/run-node.js +2 -1
  86. package/lib/utils/runtimes.js +14 -13
  87. package/lib/utils/ship/gzip.js +1 -1
  88. package/lib/utils/ship/icns.d.ts +14 -0
  89. package/lib/utils/ship/icns.js +83 -0
  90. package/lib/utils/ship/ico.d.ts +25 -0
  91. package/lib/utils/ship/ico.js +120 -0
  92. package/lib/utils/ship/icons.d.ts +197 -0
  93. package/lib/utils/ship/icons.js +399 -0
  94. package/lib/utils/ship/launcher.js +16 -1
  95. package/lib/utils/ship/layout.d.ts +32 -0
  96. package/lib/utils/ship/layout.js +112 -38
  97. package/lib/utils/ship/msi.d.ts +1 -1
  98. package/lib/utils/ship/msi.js +30 -5
  99. package/lib/utils/ship/pe-launcher.d.ts +18 -0
  100. package/lib/utils/ship/pe-launcher.js +125 -4
  101. package/lib/utils/ship/plan.d.ts +1 -34
  102. package/lib/utils/ship/plan.js +7 -56
  103. package/lib/utils/ship/plist.d.ts +20 -10
  104. package/lib/utils/ship/plist.js +29 -12
  105. package/lib/utils/ship/settings.js +3 -2
  106. package/lib/utils/spawn.d.ts +13 -0
  107. package/lib/utils/spawn.js +10 -5
  108. package/lib/utils/watch-loop.d.ts +15 -1
  109. package/lib/utils/watch-loop.js +9 -5
  110. package/lib/utils/webext/build.d.ts +42 -0
  111. package/lib/utils/webext/build.js +120 -0
  112. package/lib/utils/webext/config.d.ts +72 -0
  113. package/lib/utils/webext/config.js +234 -0
  114. package/lib/utils/webext/icons.d.ts +24 -0
  115. package/lib/utils/webext/icons.js +83 -0
  116. package/lib/utils/webext/launch.d.ts +32 -0
  117. package/lib/utils/webext/launch.js +77 -0
  118. package/lib/utils/webext/manifest.d.ts +53 -0
  119. package/lib/utils/webext/manifest.js +219 -0
  120. package/lib/utils/webext/pages.d.ts +26 -0
  121. package/lib/utils/webext/pages.js +61 -0
  122. package/lib/utils/webext/targets.d.ts +13 -0
  123. package/lib/utils/webext/targets.js +36 -0
  124. package/lib/utils/win32-command.d.ts +17 -0
  125. package/lib/utils/win32-command.js +21 -6
  126. package/package.json +20 -20
@@ -19,6 +19,7 @@
19
19
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
20
20
  import { dirname, join, resolve } from 'node:path';
21
21
  import { Buffer } from 'node:buffer';
22
+ import { FLATPAK_SOURCES_FILE } from './utils.js';
22
23
  /** Checksum algorithms flatpak-builder accepts on a `file` source. */
23
24
  const FLATPAK_ALGOS = new Set(['sha256', 'sha512']);
24
25
  /** Parse an SRI integrity (`sha512-<base64>`) into `{ algorithm, hex }`. Mirrors
@@ -219,7 +220,7 @@ export const flatpakSourcesCommand = {
219
220
  .option('out', {
220
221
  description: 'Output JSON sources file',
221
222
  type: 'string',
222
- default: 'gjsify-sources.json',
223
+ default: FLATPAK_SOURCES_FILE,
223
224
  normalize: true,
224
225
  })
225
226
  .option('cache-root', {
@@ -277,7 +278,7 @@ export const flatpakSourcesCommand = {
277
278
  }
278
279
  // Stable order (by url) so the generated file diffs cleanly across runs.
279
280
  const sources = [...byHex.values()].sort((a, b) => (a.url < b.url ? -1 : a.url > b.url ? 1 : 0));
280
- const out = resolve(cwd, args.out ?? 'gjsify-sources.json');
281
+ const out = resolve(cwd, args.out ?? FLATPAK_SOURCES_FILE);
281
282
  mkdirSync(dirname(out), { recursive: true });
282
283
  writeFileSync(out, JSON.stringify(sources, null, 2) + '\n');
283
284
  console.log(`[gjsify flatpak sources] wrote ${out} — ${sources.length} tarball source(s) from ${type} lockfile` +
@@ -285,7 +286,7 @@ export const flatpakSourcesCommand = {
285
286
  (skippedSri ? `, ${skippedSri} without a usable integrity skipped` : '') +
286
287
  (skippedAlgo ? `, ${skippedAlgo} non-sha256/512 skipped` : ''));
287
288
  if (args.printModule) {
288
- console.error(buildModuleSnippet(args.out ?? 'gjsify-sources.json', cacheRoot));
289
+ console.error(buildModuleSnippet(args.out ?? FLATPAK_SOURCES_FILE, cacheRoot));
289
290
  }
290
291
  },
291
292
  };
@@ -1,6 +1,7 @@
1
1
  import type { Command } from '../../types/index.js';
2
2
  interface SyncFlathubOptions {
3
3
  version?: string;
4
+ sourcesFile?: string;
4
5
  appId?: string;
5
6
  flathubRepo?: string;
6
7
  commit?: string;
@@ -22,5 +23,6 @@ export declare function editManifest(original: string, args: {
22
23
  tag: string;
23
24
  commit: string;
24
25
  sourceIndex?: number;
26
+ sourcesFile?: string;
25
27
  }): string;
26
28
  export {};
@@ -11,7 +11,8 @@
11
11
  // 3. Resolve commit SHA via `git rev-list -n 1 <version>`
12
12
  // 4. Clone (or update) the flathub tracking-repo into $XDG_CACHE_HOME
13
13
  // 5. Surgically edit the manifest: modules[].sources[].{tag,commit}
14
- // plus x-checker-data block (inject if missing)
14
+ // plus x-checker-data block (inject if missing), and carry over the
15
+ // offline source list (see `offline sources` below)
15
16
  // 6. Create a branch, commit the changes
16
17
  // 7. (unless --no-pr) push + open a PR via `gh pr create`
17
18
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
@@ -21,7 +22,7 @@ import { execFile } from 'node:child_process';
21
22
  import { describeExit, spawnToCompletion } from '../../utils/spawn.js';
22
23
  import { promisify } from 'node:util';
23
24
  import { Config } from '../../config.js';
24
- import { readPackageJson } from './utils.js';
25
+ import { FLATPAK_SOURCES_FILE, readPackageJson } from './utils.js';
25
26
  const execFileAsync = promisify(execFile);
26
27
  export const flatpakSyncFlathubCommand = {
27
28
  command: 'sync-flathub',
@@ -51,6 +52,12 @@ export const flatpakSyncFlathubCommand = {
51
52
  .option('branch', {
52
53
  description: 'Branch name in the flathub-repo. Default: `update-to-<version>`.',
53
54
  type: 'string',
55
+ })
56
+ .option('sources-file', {
57
+ description: 'Generated offline tarball list to carry into the Flathub repo and name in the manifest. ' +
58
+ `Default: \`${FLATPAK_SOURCES_FILE}\`. Skipped when the tag does not carry it.`,
59
+ type: 'string',
60
+ default: FLATPAK_SOURCES_FILE,
54
61
  })
55
62
  .option('source-index', {
56
63
  description: 'Index into modules[0].sources[] to update (when manifest has multiple sources). Default: first `type: git` source.',
@@ -105,13 +112,33 @@ export const flatpakSyncFlathubCommand = {
105
112
  if (!existsSync(manifestPath)) {
106
113
  throw new Error(`[gjsify flatpak sync-flathub] Flathub manifest not found at ${manifestPath} — wrong appId / wrong flathub-repo?`);
107
114
  }
115
+ // Read AT THE TAG, not from the working tree: the manifest is about to pin
116
+ // that tag, and a list taken from an edited checkout would describe a tree
117
+ // nobody can get. Absent is the ordinary case for an app that installs
118
+ // online or vendors its dependencies, so it is a skip and not an error.
119
+ const sourcesFile = args.sourcesFile ?? FLATPAK_SOURCES_FILE;
120
+ const offline = await readSourceListAtTag(cwd, version, sourcesFile, args.verbose);
121
+ let offlineCopied = false;
122
+ if (offline !== null) {
123
+ const dest = join(cloneDir, sourcesFile);
124
+ offlineCopied = !existsSync(dest) || readFileSync(dest, 'utf-8') !== offline;
125
+ if (offlineCopied) {
126
+ writeFileSync(dest, offline, 'utf-8');
127
+ console.log(`[gjsify flatpak sync-flathub] ${sourcesFile} copied from ${version}`);
128
+ }
129
+ }
108
130
  const original = readFileSync(manifestPath, 'utf-8');
109
131
  const updated = editManifest(original, {
110
132
  tag: version,
111
133
  commit: commitSha,
112
134
  sourceIndex: args.sourceIndex,
135
+ sourcesFile: offline !== null ? sourcesFile : undefined,
113
136
  });
114
- if (updated === original) {
137
+ // NOT `updated === original` alone: the tarball list changes whenever a
138
+ // dependency does, so a release can need a PR with the pin unmoved. That
139
+ // test used to be the only one, and it would have returned "nothing to do"
140
+ // over a Flathub repo carrying a stale list.
141
+ if (updated === original && !offlineCopied) {
115
142
  console.log(`[gjsify flatpak sync-flathub] manifest already at ${version} — nothing to do.`);
116
143
  return;
117
144
  }
@@ -130,6 +157,88 @@ export const flatpakSyncFlathubCommand = {
130
157
  },
131
158
  };
132
159
  // ─── Internal helpers ────────────────────────────────────────────────────
160
+ /**
161
+ * The generated source list as of `tag`, or null when the tag does not carry one.
162
+ *
163
+ * `git show <tag>:<path>` rather than reading the working tree, so the file that
164
+ * travels to Flathub is the one the pinned commit actually contains.
165
+ *
166
+ * WHAT COMES BACK IS CHECKED, not just whether the command succeeded. A missing
167
+ * path makes real `git show` exit non-zero, so the obvious version of this
168
+ * function looks complete — and it treats an EMPTY answer as "the file exists
169
+ * and is empty", which then writes an empty file into the Flathub repo and
170
+ * names it in the manifest. That is strictly worse than skipping: the build
171
+ * still cannot install, and now the manifest says it can. Caught by the
172
+ * sync-flathub E2E, whose git stub answers every unknown subcommand with exit 0
173
+ * and no output.
174
+ *
175
+ * A blank answer is a skip. Content that is not a non-empty array of sources is
176
+ * an ERROR, because this file has exactly one producer (`gjsify flatpak
177
+ * sources`) and anything else means something upstream is broken.
178
+ */
179
+ async function readSourceListAtTag(cwd, tag, path, verbose) {
180
+ let raw;
181
+ try {
182
+ const { stdout } = await execFileAsync('git', ['show', `${tag}:${path}`], { cwd, maxBuffer: 64 * 1024 * 1024 });
183
+ raw = stdout;
184
+ }
185
+ catch (error) {
186
+ await assertGitMeantAbsent(cwd, tag, path, error);
187
+ if (verbose)
188
+ console.log(`[gjsify flatpak sync-flathub] ${path} not present at ${tag} — skipping`);
189
+ return null;
190
+ }
191
+ if (raw.trim() === '') {
192
+ if (verbose)
193
+ console.log(`[gjsify flatpak sync-flathub] ${path} is empty at ${tag} — skipping`);
194
+ return null;
195
+ }
196
+ let parsed;
197
+ try {
198
+ parsed = JSON.parse(raw);
199
+ }
200
+ catch (error) {
201
+ throw new Error(`[gjsify flatpak sync-flathub] ${path} at ${tag} is not JSON: ` +
202
+ (error instanceof Error ? error.message : String(error)));
203
+ }
204
+ if (!Array.isArray(parsed) || parsed.length === 0) {
205
+ throw new Error(`[gjsify flatpak sync-flathub] ${path} at ${tag} is not a non-empty array of flatpak sources`);
206
+ }
207
+ return raw;
208
+ }
209
+ /**
210
+ * Throw unless a failed `git show <tag>:<path>` really did mean "that tag does
211
+ * not carry that file".
212
+ *
213
+ * MEASURED, and it is why the failure cannot be read on its own: path absent,
214
+ * unknown tag and not-a-git-repository are ALL exit 128, and git translates the
215
+ * message (`Pfad … existiert nicht in …` on a German host), so neither the
216
+ * status nor the text separates them. Taken for absence, a tag nobody fetched
217
+ * moves the pin and leaves the Flathub repo's old list in place — the failure
218
+ * this file exists to prevent, now silent. `--commit <sha>` is what puts it in
219
+ * reach: it is the one path on which nothing else resolves the tag.
220
+ *
221
+ * The tag is therefore asked about separately, and a SHA is believed rather
222
+ * than an exit code — the same lesson as the read above, since a `git` that
223
+ * answers 0 with nothing must not pass for a tag that exists.
224
+ */
225
+ async function assertGitMeantAbsent(cwd, tag, path, error) {
226
+ const code = error?.code;
227
+ if (typeof code !== 'number') {
228
+ // Not git's verdict at all: ENOENT is no `git` on PATH, and
229
+ // ERR_CHILD_PROCESS_STDIO_MAXBUFFER a list past the cap set above.
230
+ throw new Error(`[gjsify flatpak sync-flathub] could not run \`git show ${tag}:${path}\` — ` +
231
+ (error instanceof Error ? error.message : String(error)));
232
+ }
233
+ // `--quiet` exits 1 rather than printing for a name it cannot resolve.
234
+ const sha = await execFileAsync('git', ['rev-parse', '--verify', '--quiet', `${tag}^{commit}`], { cwd })
235
+ .then(({ stdout }) => stdout.trim())
236
+ .catch(() => '');
237
+ if (!sha) {
238
+ throw new Error(`[gjsify flatpak sync-flathub] ${tag} does not resolve in ${cwd}, so whether it carries ${path} is unanswered. ` +
239
+ 'Run `git fetch --tags`, pass --version <an existing tag>, or run from the app checkout.');
240
+ }
241
+ }
133
242
  async function resolveLatestTag(cwd, verbose) {
134
243
  try {
135
244
  const { stdout } = await execFileAsync('git', ['describe', '--tags', '--abbrev=0'], { cwd });
@@ -258,14 +367,17 @@ export function editManifest(original, args) {
258
367
  if (sources.length === 0) {
259
368
  throw new Error('[gjsify flatpak sync-flathub] modules[0] has no sources');
260
369
  }
261
- let idx = args.sourceIndex ?? sources.findIndex((s) => s.type === 'git');
370
+ const isObject = (s) => typeof s === 'object';
371
+ let idx = args.sourceIndex ?? sources.findIndex((s) => isObject(s) && s.type === 'git');
262
372
  if (idx < 0 || idx >= sources.length) {
263
373
  throw new Error(`[gjsify flatpak sync-flathub] no git source found in modules[0].sources (use --source-index <n>)`);
264
374
  }
265
- const source = sources[idx];
266
- if (source.type !== 'git') {
267
- throw new Error(`[gjsify flatpak sync-flathub] modules[0].sources[${idx}].type is "${source.type}", expected "git"`);
375
+ const entry = sources[idx];
376
+ if (!isObject(entry) || entry.type !== 'git') {
377
+ const what = isObject(entry) ? `type is "${entry.type}"` : `is the file reference "${entry}"`;
378
+ throw new Error(`[gjsify flatpak sync-flathub] modules[0].sources[${idx}] ${what}, expected a git source`);
268
379
  }
380
+ const source = entry;
269
381
  source.tag = args.tag;
270
382
  source.commit = args.commit;
271
383
  if (!source['x-checker-data']) {
@@ -275,12 +387,42 @@ export function editManifest(original, args) {
275
387
  'version-scheme': 'semantic',
276
388
  };
277
389
  }
390
+ // THE OFFLINE TARBALL LIST, which is the half of a Flathub bump that is easy
391
+ // to forget because nothing points at it. Flathub builds with the network
392
+ // unshared, so an app whose install reads a generated source list cannot
393
+ // build from the git checkout alone; the list has to live in the Flathub repo
394
+ // and be named here. Repointing tag and commit alone produces a manifest that
395
+ // looks correct and cannot build, and the error it eventually gives is
396
+ // whatever the app's build system says when its install fails.
397
+ //
398
+ // Measured: Learn6502 0.8.0 was the first release after its vendored
399
+ // dependency cache was dropped, and its Flathub build died on exactly this.
400
+ if (args.sourcesFile && !namesFile(sources, args.sourcesFile)) {
401
+ sources.push(args.sourcesFile);
402
+ }
278
403
  // Detect the original indent (2 vs 4 spaces) by inspecting the second
279
404
  // line — Flathub manifests are all 2-space in practice, but some
280
405
  // older ones might be 4. Preserve original convention.
281
406
  const indent = detectIndent(original);
282
407
  return JSON.stringify(manifest, null, indent) + (original.endsWith('\n') ? '\n' : '');
283
408
  }
409
+ /**
410
+ * Is `file` already among the module's bare-string sources?
411
+ *
412
+ * Compared with `./` stripped, because flatpak-builder resolves a string source
413
+ * against the manifest's own directory: `./gjsify-sources.json` and
414
+ * `gjsify-sources.json` are one file. Compared as written, a manifest holding
415
+ * the dotted spelling grows a second reference and the same tarballs are
416
+ * spliced in twice — the duplicate `dest`/`dest-filename` pairs `gjsify flatpak
417
+ * sources` dedupes its own output to avoid.
418
+ */
419
+ function namesFile(sources, file) {
420
+ const want = stripDotSlash(file);
421
+ return sources.some((entry) => typeof entry === 'string' && stripDotSlash(entry) === want);
422
+ }
423
+ function stripDotSlash(ref) {
424
+ return ref.replace(/^\.\//, '');
425
+ }
284
426
  function detectIndent(json) {
285
427
  const match = json.match(/^\{\n( +)/);
286
428
  if (match)
@@ -1,3 +1,13 @@
1
+ /**
2
+ * The generated offline tarball list, by the name both halves have to agree on.
3
+ *
4
+ * `gjsify flatpak sources` WRITES it; `gjsify flatpak sync-flathub` carries it
5
+ * into the Flathub repo and names it in the manifest. Each spelled the default
6
+ * itself, so changing one would have left the other reading a file nobody
7
+ * writes — and that shows up as a Flathub build which cannot install, four
8
+ * steps from its cause.
9
+ */
10
+ export declare const FLATPAK_SOURCES_FILE = "gjsify-sources.json";
1
11
  /** Read package.json from a directory. Throws a helpful error if missing/invalid. */
2
12
  export declare function readPackageJson(dir: string): Record<string, unknown>;
3
13
  /** True if a name string looks like a reverse-DNS Flatpak app id. */
@@ -5,6 +5,16 @@
5
5
  // header. What is left here is what only these subcommands ask.
6
6
  import { readFileSync } from 'node:fs';
7
7
  import { resolve } from 'node:path';
8
+ /**
9
+ * The generated offline tarball list, by the name both halves have to agree on.
10
+ *
11
+ * `gjsify flatpak sources` WRITES it; `gjsify flatpak sync-flathub` carries it
12
+ * into the Flathub repo and names it in the manifest. Each spelled the default
13
+ * itself, so changing one would have left the other reading a file nobody
14
+ * writes — and that shows up as a Flathub build which cannot install, four
15
+ * steps from its cause.
16
+ */
17
+ export const FLATPAK_SOURCES_FILE = 'gjsify-sources.json';
8
18
  /** Read package.json from a directory. Throws a helpful error if missing/invalid. */
9
19
  export function readPackageJson(dir) {
10
20
  const path = resolve(dir, 'package.json');
@@ -9,7 +9,7 @@
9
9
  // matching yarn's interactive flow. Exit code is non-zero if any child
10
10
  // process failed; first failure's stderr is forwarded.
11
11
  import { spawnToCompletion } from '../utils/spawn.js';
12
- import { readFileSync, readdirSync } from 'node:fs';
12
+ import { collectDescendants } from '../utils/process-table.js';
13
13
  import { cpus } from 'node:os';
14
14
  import { affectedClosure, buildDependencyGraph, discoverWorkspaces, filterWorkspaces, topologicalSort, } from '@gjsify/workspace';
15
15
  import { findWorkspaceRoot } from '../utils/workspace-root.js';
@@ -34,101 +34,6 @@ const KILL_GRACE_MS = 5_000;
34
34
  // killed child whose grandchildren inherit (and keep open) the stdio pipe
35
35
  // never emits 'close', so we must not wait on it unboundedly.
36
36
  const DRAIN_DEADLINE_MS = 15_000;
37
- // Read the direct child PIDs of `pid` from /proc/<pid>/task/*/children
38
- // (Linux, CONFIG_PROC_CHILDREN — standard on every mainstream kernel). When
39
- // the per-task children files are unavailable, fall back to one full
40
- // /proc/*/stat scan building a ppid→children map. Both paths use only
41
- // `node:fs` reads, so they behave identically under Node and GJS
42
- // (@gjsify/fs) — unlike a process-group kill, which GJS cannot issue
43
- // (`@gjsify/process.kill` shells out `kill <sig> <pid>` where a negative
44
- // PID parses as an option, and Gio.Subprocess has no group-signal API).
45
- function readDirectChildren(pid) {
46
- let taskIds;
47
- try {
48
- taskIds = readdirSync(`/proc/${pid}/task`);
49
- }
50
- catch {
51
- // Process already gone (common) or /proc unavailable.
52
- return null;
53
- }
54
- const kids = [];
55
- let readAny = false;
56
- for (const tid of taskIds) {
57
- try {
58
- const data = readFileSync(`/proc/${pid}/task/${tid}/children`, 'utf-8');
59
- readAny = true;
60
- for (const tok of data.trim().split(/\s+/)) {
61
- const n = Number(tok);
62
- if (Number.isInteger(n) && n > 0)
63
- kids.push(n);
64
- }
65
- }
66
- catch {
67
- // Thread vanished mid-walk, or kernel lacks CONFIG_PROC_CHILDREN.
68
- }
69
- }
70
- return readAny ? kids : null;
71
- }
72
- // Fallback: scan every /proc/<pid>/stat once and build ppid → children.
73
- // O(#processes) small reads — fine for the rare kill path.
74
- function buildPpidMap() {
75
- const map = new Map();
76
- let entries = [];
77
- try {
78
- entries = readdirSync('/proc');
79
- }
80
- catch {
81
- return map;
82
- }
83
- for (const entry of entries) {
84
- const pid = Number(entry);
85
- if (!Number.isInteger(pid) || pid <= 0)
86
- continue;
87
- try {
88
- const stat = readFileSync(`/proc/${pid}/stat`, 'utf-8');
89
- // comm (field 2) may contain spaces/parens — parse after last ')'.
90
- const rest = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
91
- const ppid = Number(rest[1]);
92
- if (!Number.isInteger(ppid) || ppid <= 0)
93
- continue;
94
- const siblings = map.get(ppid);
95
- if (siblings)
96
- siblings.push(pid);
97
- else
98
- map.set(ppid, [pid]);
99
- }
100
- catch {
101
- // Process exited between readdir and read — fine.
102
- }
103
- }
104
- return map;
105
- }
106
- // Snapshot every transitive descendant of `pid`, breadth-first (so
107
- // reversing the list yields deepest-first order for signalling).
108
- function collectDescendants(pid) {
109
- const result = [];
110
- const seen = new Set([pid]);
111
- const queue = [pid];
112
- let ppidMap = null;
113
- while (queue.length > 0) {
114
- const cur = queue.shift();
115
- let kids = readDirectChildren(cur);
116
- if (kids === null) {
117
- // children files unreadable — use (and lazily build) the
118
- // full-scan fallback for this and subsequent levels.
119
- ppidMap ??= buildPpidMap();
120
- kids = ppidMap.get(cur) ?? [];
121
- }
122
- for (const kid of kids) {
123
- if (seen.has(kid))
124
- continue;
125
- seen.add(kid);
126
- result.push(kid);
127
- queue.push(kid);
128
- }
129
- }
130
- return result;
131
- }
132
37
  // Signal an already-collected descendant set deepest-first, then the direct
133
38
  // child itself. Snapshot-then-signal (not signal-while-walking) so killing
134
39
  // the parent can't orphan grandchildren before we enumerated them. The
@@ -157,7 +62,10 @@ function killActiveChildren() {
157
62
  // The direct child is `npm run <script>` — killing only it leaves the
158
63
  // npm-spawned shell → gjs/node build grandchildren alive (observed: an
159
64
  // orphaned gjs bundler at 100% CPU for 19+ min after a fail-fast kill).
160
- // So terminate the whole process TREE of each child via a /proc walk.
65
+ // So terminate the whole process TREE of each child — walked by
66
+ // `utils/process-table.ts` (procfs on Linux, the OS process list elsewhere).
67
+ // A process-group kill would be simpler and GJS cannot issue one:
68
+ // Gio.Subprocess has no group-signal API.
161
69
  const termSnapshots = new Map();
162
70
  for (const child of activeChildren) {
163
71
  const descendants = child.pid ? collectDescendants(child.pid) : [];
@@ -7,9 +7,10 @@
7
7
  // optionalDependency, keeping the Node-free runtime promise for the bundled
8
8
  // CLI intact (the formatter runs in dev/CI, not in shipped GJS apps).
9
9
  //
10
- // oxfmt formats JS/TS (+TOML) only. CSS/JSON formatting that the old Biome
11
- // toolchain handled is intentionally DROPPED in the oxc migration — no other
12
- // formatter is wired up for those file types.
10
+ // oxfmt formats JS/TS, JSON, TOML, CSS and GraphQL in both builds; the
11
+ // languages it hands to its Prettier host (Markdown, HTML, Vue, YAML, …) are
12
+ // napi-only — the native GJS build drops them, which the skip scan below
13
+ // reports.
13
14
  //
14
15
  // A BARE `gjsify format` WRITES. `--check` is the read-only CI mode and
15
16
  // `--no-write` the read-only local one; there is no flagless report mode.
@@ -24,11 +25,12 @@
24
25
  // tuned for GJS/GNOME projects (4-space, single-quote, printWidth 120,
25
26
  // trailing-comma all, semicolons always, arrow parens always).
26
27
  import { existsSync, writeFileSync } from 'node:fs';
27
- import { resolve } from 'node:path';
28
- import { OxcNotFoundError, findOxfmtConfig, loadOxfmtTemplate, loadOxlintTemplate, printOxcNotFound, runOxfmt, setOxcExitCode, } from '../utils/oxc-resolve.js';
28
+ import { relative, resolve } from 'node:path';
29
+ import { OxcNotFoundError, findOxfmtConfig, loadOxfmtTemplate, loadOxlintTemplate, printOxcNotFound, runOxfmt, setOxcExitCode, shouldUseNativeOxfmt, } from '../utils/oxc-resolve.js';
30
+ import { scanForNativeSkips } from '../utils/native-skip-scan.js';
29
31
  export const formatCommand = {
30
32
  command: 'format [paths..]',
31
- description: 'Format JS/TS source files via oxfmt (CSS/JSON formatting is not supported).',
33
+ description: 'Format source files via oxfmt (Markdown/YAML/HTML/Vue need the napi build).',
32
34
  builder: (yargs) => {
33
35
  return yargs
34
36
  .positional('paths', {
@@ -90,6 +92,24 @@ export const formatCommand = {
90
92
  if (configPath)
91
93
  oxfmtArgs.push('--config', resolve(configPath));
92
94
  oxfmtArgs.push(...paths);
95
+ // Under GJS the native oxfmt formats everything but the files napi oxfmt
96
+ // hands to its Prettier host (Markdown, HTML, Vue, YAML, …), and it drops
97
+ // those without a word. `--check` FAILS on them, so a green check that
98
+ // checked nothing is impossible — asked about exactly the files oxfmt
99
+ // itself would walk with this config, never `node_modules` or ignored ones.
100
+ if (args.check && (await shouldUseNativeOxfmt())) {
101
+ const scan = scanForNativeSkips(paths, cwd, { configPath });
102
+ if (scan.skipped.length > 0) {
103
+ const list = scan.skipped.map((f) => ` ${relative(cwd, f) || f}`).join('\n');
104
+ console.error(`[gjsify format] ERROR: ${scan.skipped.length} file(s) skipped — ` +
105
+ `the native oxfmt (GJS) has no Prettier host for them (Markdown/HTML/Vue/YAML/…):\n` +
106
+ `${list}\n` +
107
+ ` The format check is INCOMPLETE. Run \`gjsify format --check\` under Node to check these ` +
108
+ `files, or add them to \`ignorePatterns\`.`);
109
+ setOxcExitCode(1);
110
+ return;
111
+ }
112
+ }
93
113
  try {
94
114
  const code = await runOxfmt(oxfmtArgs, { cwd, verbose: args.verbose });
95
115
  setOxcExitCode(code);
@@ -1,6 +1,8 @@
1
1
  export * from './build.js';
2
2
  export * from './clear.js';
3
3
  export * from './copy.js';
4
+ export * from './env.js';
5
+ export * from './exec.js';
4
6
  export * from './test.js';
5
7
  export * from './run.js';
6
8
  export * from './info.js';
@@ -15,6 +17,7 @@ export { flatpakCommand } from './flatpak/index.js';
15
17
  export * from './ship.js';
16
18
  export * from './dlx.js';
17
19
  export * from './install.js';
20
+ export * from './link.js';
18
21
  export * from './foreach.js';
19
22
  export * from './workspace.js';
20
23
  export * from './pack.js';
@@ -39,3 +42,4 @@ export * from './storybook.js';
39
42
  export * from './dev.js';
40
43
  export * from './debug.js';
41
44
  export * from './browse.js';
45
+ export { webextCommand } from './webext/index.js';
@@ -1,6 +1,8 @@
1
1
  export * from './build.js';
2
2
  export * from './clear.js';
3
3
  export * from './copy.js';
4
+ export * from './env.js';
5
+ export * from './exec.js';
4
6
  export * from './test.js';
5
7
  export * from './run.js';
6
8
  export * from './info.js';
@@ -15,6 +17,7 @@ export { flatpakCommand } from './flatpak/index.js';
15
17
  export * from './ship.js';
16
18
  export * from './dlx.js';
17
19
  export * from './install.js';
20
+ export * from './link.js';
18
21
  export * from './foreach.js';
19
22
  export * from './workspace.js';
20
23
  export * from './pack.js';
@@ -39,3 +42,4 @@ export * from './storybook.js';
39
42
  export * from './dev.js';
40
43
  export * from './debug.js';
41
44
  export * from './browse.js';
45
+ export { webextCommand } from './webext/index.js';
@@ -83,8 +83,8 @@ export interface EnsureProjectGjsEngineDeps {
83
83
  *
84
84
  * The engine is an OPTIONAL PEER of `@gjsify/cli` on purpose — a plain
85
85
  * `npm install @gjsify/cli` on Node must not fetch prebuilds — but nobody resolves
86
- * it for a project: npm 7+ skips optional peers and the native backend does not
87
- * resolve `peerDependencies` at all. Under GJS there is no npm `rolldown`
86
+ * it for a project: npm 7+ skips optional peers and so does the native backend
87
+ * (it installs REQUIRED peers only). Under GJS there is no npm `rolldown`
88
88
  * fallback, so `gjsify build` had nothing to load.
89
89
  *
90
90
  * Four conditions, and (b) is the one worth reading: