@gjsify/cli 0.52.0 → 0.54.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 (107) hide show
  1. package/dist/cli.gjs.mjs +351 -305
  2. package/lib/actions/build.d.ts +22 -0
  3. package/lib/actions/build.js +37 -9
  4. package/lib/bundler-pick.d.ts +72 -1
  5. package/lib/bundler-pick.js +175 -17
  6. package/lib/cli-app.js +7 -2
  7. package/lib/commands/blueprint/index.d.ts +4 -0
  8. package/lib/commands/blueprint/index.js +15 -0
  9. package/lib/commands/blueprint/types.d.ts +8 -0
  10. package/lib/commands/blueprint/types.js +124 -0
  11. package/lib/commands/devtools.d.ts +21 -0
  12. package/lib/commands/devtools.js +146 -0
  13. package/lib/commands/env.d.ts +31 -0
  14. package/lib/commands/env.js +114 -0
  15. package/lib/commands/exec.d.ts +10 -0
  16. package/lib/commands/exec.js +208 -0
  17. package/lib/commands/foreach.js +5 -97
  18. package/lib/commands/format.js +26 -6
  19. package/lib/commands/index.d.ts +5 -0
  20. package/lib/commands/index.js +5 -0
  21. package/lib/commands/install.d.ts +2 -2
  22. package/lib/commands/install.js +5 -5
  23. package/lib/commands/self-update.js +2 -1
  24. package/lib/commands/ship.js +51 -8
  25. package/lib/commands/test.d.ts +18 -9
  26. package/lib/commands/test.js +56 -11
  27. package/lib/commands/tsc.js +4 -4
  28. package/lib/commands/upgrade.d.ts +15 -0
  29. package/lib/commands/upgrade.js +76 -2
  30. package/lib/commands/webext/index.d.ts +2 -0
  31. package/lib/commands/webext/index.js +15 -0
  32. package/lib/commands/webext/subcommands.d.ts +16 -0
  33. package/lib/commands/webext/subcommands.js +204 -0
  34. package/lib/types/config-data.d.ts +1 -1
  35. package/lib/utils/bin-shim.d.ts +8 -2
  36. package/lib/utils/bin-shim.js +6 -5
  37. package/lib/utils/bundle-inputs.d.ts +84 -0
  38. package/lib/utils/bundle-inputs.js +268 -0
  39. package/lib/utils/check-system-deps.d.ts +4 -2
  40. package/lib/utils/check-system-deps.js +21 -26
  41. package/lib/utils/dev-link.d.ts +9 -6
  42. package/lib/utils/dev-link.js +71 -19
  43. package/lib/utils/exec-bin.d.ts +137 -0
  44. package/lib/utils/exec-bin.js +321 -0
  45. package/lib/utils/gi-runtime-paths.js +3 -2
  46. package/lib/utils/gitignore.d.ts +15 -0
  47. package/lib/utils/gitignore.js +178 -0
  48. package/lib/utils/gjsify-shim.d.ts +47 -0
  49. package/lib/utils/gjsify-shim.js +97 -8
  50. package/lib/utils/install-backend-native.d.ts +19 -1
  51. package/lib/utils/install-backend-native.js +93 -7
  52. package/lib/utils/install-lock.js +4 -25
  53. package/lib/utils/native-skip-scan.d.ts +20 -0
  54. package/lib/utils/native-skip-scan.js +343 -0
  55. package/lib/utils/node-script.js +1 -1
  56. package/lib/utils/oxc-resolve.js +46 -5
  57. package/lib/utils/process-table.d.ts +22 -0
  58. package/lib/utils/process-table.js +188 -0
  59. package/lib/utils/prune-prefix.js +1 -1
  60. package/lib/utils/run-gjs.d.ts +15 -1
  61. package/lib/utils/run-gjs.js +16 -7
  62. package/lib/utils/run-node.d.ts +2 -0
  63. package/lib/utils/run-node.js +2 -1
  64. package/lib/utils/runtimes.js +14 -13
  65. package/lib/utils/ship/app-runtime.d.ts +7 -5
  66. package/lib/utils/ship/app-runtime.js +11 -8
  67. package/lib/utils/ship/appimage.d.ts +9 -0
  68. package/lib/utils/ship/appimage.js +2 -1
  69. package/lib/utils/ship/deb.d.ts +10 -0
  70. package/lib/utils/ship/deb.js +7 -0
  71. package/lib/utils/ship/depends.d.ts +37 -3
  72. package/lib/utils/ship/depends.js +21 -9
  73. package/lib/utils/ship/gi-namespaces.d.ts +16 -0
  74. package/lib/utils/ship/gi-namespaces.js +64 -1
  75. package/lib/utils/ship/gzip.js +1 -1
  76. package/lib/utils/ship/icons.d.ts +6 -0
  77. package/lib/utils/ship/icons.js +6 -5
  78. package/lib/utils/ship/launcher.js +2 -2
  79. package/lib/utils/ship/layout.js +25 -2
  80. package/lib/utils/ship/rpm.d.ts +5 -0
  81. package/lib/utils/ship/rpm.js +42 -0
  82. package/lib/utils/ship/settings.js +6 -0
  83. package/lib/utils/ship/signing.d.ts +7 -2
  84. package/lib/utils/ship/signing.js +59 -32
  85. package/lib/utils/ship/stage-manifest.d.ts +14 -0
  86. package/lib/utils/ship/stage-manifest.js +7 -0
  87. package/lib/utils/spawn.d.ts +13 -0
  88. package/lib/utils/spawn.js +10 -5
  89. package/lib/utils/watch-loop.d.ts +15 -1
  90. package/lib/utils/watch-loop.js +9 -5
  91. package/lib/utils/webext/build.d.ts +42 -0
  92. package/lib/utils/webext/build.js +120 -0
  93. package/lib/utils/webext/config.d.ts +72 -0
  94. package/lib/utils/webext/config.js +234 -0
  95. package/lib/utils/webext/icons.d.ts +24 -0
  96. package/lib/utils/webext/icons.js +83 -0
  97. package/lib/utils/webext/launch.d.ts +32 -0
  98. package/lib/utils/webext/launch.js +77 -0
  99. package/lib/utils/webext/manifest.d.ts +53 -0
  100. package/lib/utils/webext/manifest.js +219 -0
  101. package/lib/utils/webext/pages.d.ts +26 -0
  102. package/lib/utils/webext/pages.js +61 -0
  103. package/lib/utils/webext/targets.d.ts +13 -0
  104. package/lib/utils/webext/targets.js +48 -0
  105. package/lib/utils/win32-command.d.ts +17 -0
  106. package/lib/utils/win32-command.js +21 -6
  107. package/package.json +20 -19
@@ -7,7 +7,8 @@
7
7
  // is the first layout that is NOT a prefix substitution: the launcher, the bundle,
8
8
  // the shared libraries and the data tree each move somewhere different, and on
9
9
  // macOS the native files leave the bundle directory altogether for
10
- // `Contents/Frameworks`.
10
+ // `Contents/Resources/native` — a path Apple's own layout does not have, for the
11
+ // measured reason {@link layoutForOs}'s darwin row carries.
11
12
  //
12
13
  // So the layout is a MAP from the prefix-relative plan to a stage-relative path,
13
14
  // and `planStage` keeps producing the prefix-relative plan for all three. That
@@ -168,12 +169,34 @@ export const LAYOUTS = {
168
169
  // everything that is not, and `Contents/Frameworks` the dylibs — which is
169
170
  // the split that makes this more than a prefix: the `gi/` directory that
170
171
  // sits INSIDE `lib/<name>/` on Linux leaves the bundle directory entirely.
172
+ //
173
+ // …AND THE NATIVE DIRECTORY IS NOT APPLE'S, because `Contents/Frameworks`
174
+ // is RESERVED FOR NESTED CODE and one directory of it is loadable code
175
+ // that cannot be there. Measured on darwin-arm64 / macOS 27 with the
176
+ // published `@gjsify/gtk-runtime-darwin-arm64` staged by M2b: `codesign`
177
+ // refuses the BUNDLE SEAL with *"bundle format unrecognized, invalid, or
178
+ // unsuitable / In subcomponent: …/gtk/lib/gstreamer-1.0"*, so
179
+ // `gjsify ship darwin --sign` cannot sign a real GTK application at all —
180
+ // and it does so at exit 1 with the payload half-signed. The trigger is not
181
+ // gstreamer: it is any directory under `Contents/Frameworks` whose last
182
+ // dot-separated component parses as a version, because `codesign` reads
183
+ // `foo-1.0`, `foo1.0` and `Versions/1.0` as a VERSIONED NESTED BUNDLE
184
+ // (`<name>-<version>` is a framework's shape) and then finds no
185
+ // `Contents/Info.plist` inside. The relocated closure carries three:
186
+ // `girepository-1.0`, `gdk-pixbuf-2.0/2.10.0` and `lib/gstreamer-1.0`.
187
+ //
188
+ // The same three under `Contents/Resources` seal and verify at exit 0, which
189
+ // is the whole measurement. So `native` is the ONE seam that moves: the
190
+ // relocated closure, the addon, the app's own `gi/` dylibs and both launchers'
191
+ // `GI_TYPELIB_PATH`/`GJSIFY_GI_LIBRARY_PATH` all read it, so the tree and
192
+ // every path that names it move together. Library validation is unaffected —
193
+ // it follows the SIGNATURE (§ `signing.ts`'s darwin row), not the directory.
171
194
  dirs: (identity) => {
172
195
  const app = appBundleDir(identity);
173
196
  return {
174
197
  launcher: `${app}/Contents/MacOS`,
175
198
  bundle: `${app}/Contents/Resources/lib`,
176
- native: `${app}/Contents/Frameworks`,
199
+ native: `${app}/Contents/Resources/native`,
177
200
  data: `${app}/Contents/Resources/share`,
178
201
  other: `${app}/Contents/Resources`,
179
202
  };
@@ -5,6 +5,11 @@ export interface RpmInputs {
5
5
  payload: readonly PayloadEntry[];
6
6
  prefix: string;
7
7
  depends: readonly string[];
8
+ /**
9
+ * Soft dependencies, written as the `RECOMMEND*` tags — an `&optional` typelib
10
+ * (ADR 0087), which the package installs with and runs without.
11
+ */
12
+ recommends?: readonly string[];
8
13
  /** RPM architecture (`x86_64`, `noarch`, …). */
9
14
  archLabel: string;
10
15
  mtime: number;
@@ -54,6 +54,17 @@ const TAG = {
54
54
  REQUIREFLAGS: 1048,
55
55
  REQUIRENAME: 1049,
56
56
  REQUIREVERSION: 1050,
57
+ // The recommend trio. NOT the 1099/1046/1047 of rpm ≤ 4.18, which shared ONE
58
+ // index space with `REQUIRENAME`; since 4.19 each dependency tag carries its
59
+ // own arrays. MEASURED against a package `rpmbuild` produced and read back with
60
+ // `rpm` 6.0.2: RECOMMENDNAME is a STRING_ARRAY of its own right after
61
+ // PROVIDEVERSION, not an extension of the four REQUIRE entries. The numbers are
62
+ // in the 5000s for that reason and a wrong one is invisible here — rpm either
63
+ // ignores the tag or refuses the header, and `rpm -qp --recommends` is what
64
+ // says which (e2e `ship`).
65
+ RECOMMENDNAME: 5046,
66
+ RECOMMENDVERSION: 5047,
67
+ RECOMMENDFLAGS: 5048,
57
68
  RPMVERSION: 1064,
58
69
  POSTINPROG: 1086,
59
70
  POSTUNPROG: 1088,
@@ -225,6 +236,10 @@ function mainHeaderEntries(inputs, files, cpio, compressed) {
225
236
  // From the PAYLOAD, not the settings — see the same call in `deb.ts`.
226
237
  const scripts = renderRpmScriptlets(readPayloadFacts(inputs.payload), inputs.prefix);
227
238
  const requires = buildRequires(inputs.depends, scripts, readShebangInterpreters(inputs.payload));
239
+ // OMITTED, not written empty: rpm rejects a zero-count array (the same rule
240
+ // `encodeValue` enforces), so a package with no `&optional` namespace carries
241
+ // no RECOMMEND tag at all.
242
+ const recommends = buildRecommends(inputs.recommends ?? []);
228
243
  const entries = [
229
244
  { tag: TAG.HEADERI18NTABLE, type: RpmType.STRING_ARRAY, value: ['C'] },
230
245
  { tag: TAG.NAME, type: RpmType.STRING, value: settings.binaryName },
@@ -277,6 +292,13 @@ function mainHeaderEntries(inputs, files, cpio, compressed) {
277
292
  { tag: TAG.REQUIRENAME, type: RpmType.STRING_ARRAY, value: requires.names },
278
293
  { tag: TAG.REQUIREFLAGS, type: RpmType.INT32, value: requires.flags },
279
294
  { tag: TAG.REQUIREVERSION, type: RpmType.STRING_ARRAY, value: requires.versions },
295
+ ...(recommends.names.length > 0
296
+ ? [
297
+ { tag: TAG.RECOMMENDNAME, type: RpmType.STRING_ARRAY, value: recommends.names },
298
+ { tag: TAG.RECOMMENDFLAGS, type: RpmType.INT32, value: recommends.flags },
299
+ { tag: TAG.RECOMMENDVERSION, type: RpmType.STRING_ARRAY, value: recommends.versions },
300
+ ]
301
+ : []),
280
302
  { tag: TAG.PAYLOADFORMAT, type: RpmType.STRING, value: 'cpio' },
281
303
  { tag: TAG.PAYLOADCOMPRESSOR, type: RpmType.STRING, value: 'gzip' },
282
304
  // Write-side metadata only — no reader acts on it (rpm-format.md § 6.5).
@@ -383,6 +405,26 @@ function buildRequires(depends, scripts, interpreters) {
383
405
  }
384
406
  return { names, flags, versions };
385
407
  }
408
+ /**
409
+ * The three parallel `Recommends` arrays — the soft half of
410
+ * {@link buildRequires}, same relation spelling, no `RPMLIB` and no sense bits.
411
+ *
412
+ * `0` is the right flags value and not a missing one: rpm reads an unversioned
413
+ * recommendation as "any version", which is what an `&optional` typelib package
414
+ * means (the app asks for the NAMESPACE, never for a version of the package).
415
+ */
416
+ function buildRecommends(recommends) {
417
+ const names = [];
418
+ const flags = [];
419
+ const versions = [];
420
+ for (const depend of recommends) {
421
+ const parsed = parseDepend(depend);
422
+ names.push(parsed.name);
423
+ flags.push(senseFor(parsed.relation));
424
+ versions.push(parsed.version ?? '');
425
+ }
426
+ return { names, flags, versions };
427
+ }
386
428
  function senseFor(relation) {
387
429
  switch (relation) {
388
430
  case '>=':
@@ -166,6 +166,12 @@ export function resolveShipSettings(input) {
166
166
  throw new Error('gjsify ship: no licence — set `license` in package.json (an SPDX id, e.g. "MIT") ' +
167
167
  'or `gjsify.ship.license.project`. Both deb and rpm carry it as a required field.');
168
168
  }
169
+ // Written BACK, not read twice: the packers read `settings.license` and the AppStream
170
+ // renderers read `metadata.license.project`, so resolving the `package.json#license`
171
+ // fallback into the first alone left `<project_license>` empty in the staged metainfo
172
+ // and warned about a field the project had answered. The returned metadata is what every
173
+ // renderer reads, so this is where the one answer has to land.
174
+ metadata.license = { ...metadata.license, project: license };
169
175
  const maintainer = ship.maintainer ?? formatAuthor(pkg.author) ?? formatDeveloper(metadata);
170
176
  if (!maintainer) {
171
177
  throw new Error('gjsify ship: no maintainer — set `author` in package.json ("Name <you@example.com>"), ' +
@@ -30,14 +30,19 @@ export interface SignerDescriptor {
30
30
  /** argv after {@link tool}, for ONE file. */
31
31
  args: (input: SignArgs) => readonly string[];
32
32
  /**
33
- * The entitlements this signer asks for, or `[]` where the OS has none.
33
+ * The entitlements this signer asks for, given the identity in hand.
34
+ *
35
+ * A FUNCTION, not a list, and the darwin row's second argument is what makes
36
+ * that necessary: the set depends on whether the identity is a real one, and
37
+ * one entitlement is exactly wrong in one of the two directions. It is a
38
+ * measured difference, not a preference — see {@link SIGNERS}.darwin.
34
39
  *
35
40
  * `[]` is an ANSWER and not an omission — the same rule `Layout.metadata`
36
41
  * states. Windows has no equivalent concept at all, and a signer that granted
37
42
  * nothing on macOS would be a different decision from one that has nothing to
38
43
  * grant.
39
44
  */
40
- entitlements: readonly string[];
45
+ entitlements: (adhoc: boolean) => readonly string[];
41
46
  /**
42
47
  * Does this signer seal the ARTIFACT ROOT after signing the images inside it?
43
48
  *
@@ -18,7 +18,10 @@
18
18
  // an ad-hoc `LC_CODE_SIGNATURE` already (they must — `install_name_tool`
19
19
  // invalidates the original during relocation). So the darwin leg re-signs
20
20
  // every image, and the result is new BYTES for the packer rather than a
21
- // container around the old ones.
21
+ // container around the old ones. § A16's corollary of this — "the same
22
+ // identity everywhere, so library validation has nothing to object to" — is
23
+ // measured FALSE for the ad-hoc identity, and the darwin row's
24
+ // `entitlements` is where that correction lives.
22
25
  // 4. THE ORDER IS STRUCTURAL, not conventional (§ A17). `readStage` compares
23
26
  // each file's SIZE against `.gjsify-ship-stage.json`, and a size is no more
24
27
  // re-sign-proof than a digest. BOTH halves are measured and they agree:
@@ -66,10 +69,10 @@
66
69
  // `.dmg` is stapled and the `.zip` is told what a user has to do instead.
67
70
  //
68
71
  // WHAT IS STILL NOT DONE, deliberately: `signtool` is never given a timestamp URL
69
- // (see the win32 row), and the entitlement § A16 is about —
70
- // `com.apple.security.cs.disable-library-validation` — is NOT granted, because
71
- // § A4's re-sign of every image in the closure is the design of record and
72
- // granting the entitlement would quietly make it look unnecessary.
72
+ // (see the win32 row), and the named-identity half of § A16 is untested because it
73
+ // needs a certificate — the GRANT of `disable-library-validation` is the measured
74
+ // ad-hoc answer, and § A4's re-sign of every image stays the design of record for
75
+ // the Developer ID that notarisation will actually require.
73
76
  import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
74
77
  import { dirname, join, sep } from 'node:path';
75
78
  import { isOnPath } from '../check-system-deps.js';
@@ -123,32 +126,52 @@ export const SIGNERS = {
123
126
  ...(adhoc ? [] : ['--timestamp']),
124
127
  file,
125
128
  ],
126
- // FOUR OF THE REFERENCE'S SIX, and both omissions are decisions:
129
+ // FIVE OF THE REFERENCE'S SIX for an ad-hoc identity, FOUR for a real one,
130
+ // and the difference is the one § A16 left open — now MEASURED, on
131
+ // darwin-arm64 with the published `@gjsify/gtk-runtime-darwin-arm64` staged
132
+ // into the bundle (`utils/ship/layout.ts` carries the rest of that run).
127
133
  //
128
- // * `com.apple.security.cs.get-task-allow` is granted by
129
- // `refs/node/tools/osx-entitlements.plist:15` and is a DEBUGGING
130
- // entitlement — it lets another process attach to this one. Apple's
131
- // notarisation rules refuse a Developer-ID artifact carrying it, so
132
- // shipping it would trade a working debug build for an artifact that
133
- // cannot be distributed. The reference signs its own build; we sign what
134
- // a stranger downloads.
135
- // * `com.apple.security.cs.disable-library-validation` is § A16's open
136
- // question and stays open. § A4's design of record is that every Mach-O
137
- // in the closure is re-signed with the SAME identity as the launcher, so
138
- // library validation has nothing to object to — and granting the
139
- // entitlement anyway would make that re-sign look optional the first
140
- // time somebody reads this list.
134
+ // § A16's premise was that library validation has nothing to object to
135
+ // because every Mach-O carries the SAME identity as the launcher. That is
136
+ // true for a Developer ID and FALSE for `--sign -`: an ad-hoc signature has
137
+ // no team at all, so the running process and the dylib it dlopens report
138
+ // different team IDs and the load is refused —
141
139
  //
142
- // The four that remain are what a shipped V8 needs under a hardened
143
- // runtime, plus the one our own loader story needs:
144
- // `@gjsify/node-gi`'s `maybeReexecForGtkRuntime` sets `DYLD_*` for its
145
- // child on darwin, and a hardened process may not do that without
146
- // `allow-dyld-environment-variables`.
147
- entitlements: [
140
+ // dlopen(…/prebuilds/darwin-arm64/node_gi.node): code signature in
141
+ // <378B2FDA-…> '…node_gi.node' not valid for use in process: mapping
142
+ // process and mapped file (non-platform) have different Team IDs
143
+ //
144
+ // — at `require('@gjsify/node-gi')`, the FIRST thing a shipped `.app` does,
145
+ // with every image correctly signed. Same bundle, same signer, same argv,
146
+ // one more entitlement: `interpreter:`/`gtk-runtime:`/`chrome: ok`/
147
+ // `render: 480x320`/`done: 0`.
148
+ //
149
+ // So it is granted WHERE IT IS NEEDED, which is ad-hoc only. A Developer ID
150
+ // signs the whole closure with one team and needs nothing, and Apple's
151
+ // notarisation rules read a `disable-library-validation` entitlement on a
152
+ // Developer-ID artifact as shipping unsigned code — so granting it there
153
+ // would trade a distributable artifact for a locally runnable one. The
154
+ // GRANT therefore stays what § A16 asked to know: § A4's re-sign of every
155
+ // image is the design of record, and the entitlement is what makes the
156
+ // ad-hoc proof of it RUNNABLE rather than a signature `codesign` accepts and
157
+ // dyld refuses.
158
+ //
159
+ // The four both share are what a shipped V8 needs under a hardened runtime,
160
+ // plus the one our own loader story needs: `@gjsify/node-gi`'s
161
+ // `maybeReexecForGtkRuntime` sets `DYLD_*` for its child on darwin, and a
162
+ // hardened process may not do that without `allow-dyld-environment-variables`.
163
+ //
164
+ // `get-task-allow` is absent from both, and that is a decision:
165
+ // `refs/node/tools/osx-entitlements.plist:15` grants it, it is a DEBUGGING
166
+ // entitlement — it lets another process attach to this one — and Apple's
167
+ // notarisation rules refuse a Developer-ID artifact carrying it. The
168
+ // reference signs its own build; we sign what a stranger downloads.
169
+ entitlements: (adhoc) => [
148
170
  'com.apple.security.cs.allow-jit',
149
171
  'com.apple.security.cs.allow-unsigned-executable-memory',
150
172
  'com.apple.security.cs.disable-executable-page-protection',
151
173
  'com.apple.security.cs.allow-dyld-environment-variables',
174
+ ...(adhoc ? ['com.apple.security.cs.disable-library-validation'] : []),
152
175
  ],
153
176
  // The `.app` is a bundle, and a bundle's signature is the seal over its
154
177
  // whole tree — the images alone leave `codesign --verify` with nothing to
@@ -156,9 +179,10 @@ export const SIGNERS = {
156
179
  sealsBundle: true,
157
180
  sealAddPrefix: (bundleRoot) => `${bundleRoot}/Contents/_CodeSignature/`,
158
181
  configKey: 'gjsify.ship.sign.darwin.identity',
159
- gap: 'the images are signed and the bundle is sealed; UNVERIFIED end to end — no macOS host in this ' +
160
- 'repository has run a `--sign` over a real `.app`, and ADR 0024 § A16 still leaves library ' +
161
- 'validation unmeasured',
182
+ gap: 'the images are signed and the bundle is sealed, and the seal survives both the `.zip` and the ' +
183
+ '`.dmg` round trip; § A16 stands measured in ONE direction only — library validation refuses an ' +
184
+ 'ad-hoc-signed dylib, so the ad-hoc identity grants `disable-library-validation` while a named ' +
185
+ 'one does not, and no run in this repository has ever held the named one',
162
186
  },
163
187
  win32: {
164
188
  layoutOs: 'win32',
@@ -180,8 +204,10 @@ export const SIGNERS = {
180
204
  // Windows has no equivalent concept. An Authenticode signature carries no
181
205
  // capability claims — what a hardened runtime and an entitlement do on
182
206
  // macOS is done on Windows by the manifest and by AppContainer, neither of
183
- // which this signer touches.
184
- entitlements: [],
207
+ // which this signer touches. `() => []` rather than `[]` because the field is
208
+ // a function of the identity (see the darwin row), and this one ignores the
209
+ // argument instead of reading `adhoc` out of it.
210
+ entitlements: () => [],
185
211
  // A Windows program directory is a DIRECTORY and nothing more: there is no
186
212
  // manifest to seal and no per-directory signature format. `signtool` signs
187
213
  // images, one at a time, which is what the loop above already does.
@@ -303,9 +329,10 @@ export async function signPayload(input) {
303
329
  writePayload(workDir, payload, '');
304
330
  const adhoc = input.identity === ADHOC_IDENTITY;
305
331
  let entitlements;
306
- if (signer.entitlements.length > 0) {
332
+ const granted = signer.entitlements(adhoc);
333
+ if (granted.length > 0) {
307
334
  mkdirSync(dirname(input.entitlementsPath), { recursive: true });
308
- writeFileSync(input.entitlementsPath, renderEntitlements(signer.entitlements));
335
+ writeFileSync(input.entitlementsPath, renderEntitlements(granted));
309
336
  entitlements = input.entitlementsPath;
310
337
  }
311
338
  const run = async (file, what) => {
@@ -102,6 +102,18 @@ export interface StageManifest {
102
102
  mtime: number;
103
103
  /** GI namespaces the bundle imports, `Ns-Version` where the specifier pins one. */
104
104
  namespaces: string[];
105
+ /**
106
+ * The subset of {@link namespaces} the BUNDLE marks optional (ADR 0087).
107
+ *
108
+ * OPTIONAL on read, deliberately, and that is the {STAGE_SCHEMA_VERSION} header's
109
+ * "a field an older reader would simply ignore" case: a stage written before the
110
+ * split has no such key, and answering "nothing is optional" makes every
111
+ * namespace a `Depends:` again — today's behaviour, and the SAFE direction. A
112
+ * newer reader packing an OLDER stage therefore over-declares instead of dropping
113
+ * a requirement the artifact cannot run without, which is the one error here that
114
+ * ships a package that dies at first launch.
115
+ */
116
+ optionalNamespaces?: string[];
105
117
  settings: PackSettings;
106
118
  staged: StageFileRecord[];
107
119
  overlay: Partial<Record<FormatId, StageOverlayFile[]>>;
@@ -113,6 +125,8 @@ export interface StageManifestInput {
113
125
  formats: readonly FormatDescriptor[];
114
126
  mtime: number;
115
127
  namespaces: readonly string[];
128
+ /** The optional subset of {@link namespaces} — see {@link StageManifest}. */
129
+ optionalNamespaces?: readonly string[];
116
130
  staged: readonly StagedFile[];
117
131
  /** Per-format overlay, as `planOverlay` produced it. */
118
132
  overlay: ReadonlyMap<FormatId, readonly StagedFile[]>;
@@ -187,6 +187,7 @@ export function writeStageManifest(input) {
187
187
  formats: input.formats.map((format) => format.id),
188
188
  mtime: input.mtime,
189
189
  namespaces: [...input.namespaces],
190
+ optionalNamespaces: [...(input.optionalNamespaces ?? [])],
190
191
  settings: toPackSettings(input.settings),
191
192
  staged: input.staged.map((file) => ({
192
193
  path: file.path,
@@ -276,6 +277,12 @@ export function readStageManifest(stageDir) {
276
277
  }),
277
278
  mtime: expectInteger(data.mtime, at('mtime')),
278
279
  namespaces: expectArray(data.namespaces, at('namespaces')).map((entry, index) => expectString(entry, at(`namespaces[${index}]`))),
280
+ // Absent is the empty set, not an error — see the field's doc comment.
281
+ ...(data.optionalNamespaces === undefined
282
+ ? {}
283
+ : {
284
+ optionalNamespaces: expectArray(data.optionalNamespaces, at('optionalNamespaces')).map((entry, index) => expectString(entry, at(`optionalNamespaces[${index}]`))),
285
+ }),
279
286
  settings: readPackSettings(record(data.settings, at('settings')), at('settings')),
280
287
  staged: expectArray(data.staged, at('staged')).map((entry, index) => {
281
288
  const file = record(entry, at(`staged[${index}]`));
@@ -1,4 +1,5 @@
1
1
  import type { ChildProcess } from 'node:child_process';
2
+ import { type Win32Invocation } from './win32-command.js';
2
3
  /**
3
4
  * What the calling command does once this spawn settles.
4
5
  *
@@ -82,6 +83,18 @@ export interface SpawnCompletion {
82
83
  /** The child's stderr — only under `stdio: 'capture'`, `''` when it wrote none. */
83
84
  stderr?: string;
84
85
  }
86
+ /**
87
+ * Rewrite a bare command for Windows (see `utils/win32-command.ts`). A no-op on
88
+ * every other platform, and on win32 for anything already carrying a path or
89
+ * extension. `env` is the CHILD's, because its `PATH` is what must be searched.
90
+ *
91
+ * Exported for the synchronous PROBES that cannot go through
92
+ * {@link spawnToCompletion} (`<tool> --version` in `check-system-deps.ts`, the
93
+ * runtime probe in `runtimes.ts`): a raw `execFileSync('deno', …)` is ENOENT on
94
+ * Windows when deno came from `npm i -g deno`, which installs a `.cmd` shim, so
95
+ * `--runtime deno` was refused as "not installed" on a host where `deno` runs.
96
+ */
97
+ export declare function toSpawnable(cmd: string, args: readonly string[], env?: NodeJS.ProcessEnv): Win32Invocation;
85
98
  /**
86
99
  * Run `cmd args…` to completion and resolve with its exit code and signal.
87
100
  *
@@ -60,7 +60,7 @@
60
60
  //
61
61
  // ## What is spawned still matters — the Windows rewrite
62
62
  //
63
- // This helper also carries the Windows command rewrite (`forWin32`), which is a
63
+ // This helper also carries the Windows command rewrite (`toSpawnable`), which is a
64
64
  // second reason to come through it, and the one that applies OUTSIDE the CLI too:
65
65
  //
66
66
  // • a real executable (`git`, `gh`, `node`, `gjs`, `flatpak`, `process.execPath`)
@@ -93,10 +93,15 @@ function resolveEnv(opts) {
93
93
  /**
94
94
  * Rewrite a bare command for Windows (see `utils/win32-command.ts`). A no-op on
95
95
  * every other platform, and on win32 for anything already carrying a path or
96
- * extension. Applied to BOTH paths below so the blocking and streaming branches
97
- * agree; `env` is the CHILD's, because its `PATH` is what must be searched.
96
+ * extension. `env` is the CHILD's, because its `PATH` is what must be searched.
97
+ *
98
+ * Exported for the synchronous PROBES that cannot go through
99
+ * {@link spawnToCompletion} (`<tool> --version` in `check-system-deps.ts`, the
100
+ * runtime probe in `runtimes.ts`): a raw `execFileSync('deno', …)` is ENOENT on
101
+ * Windows when deno came from `npm i -g deno`, which installs a `.cmd` shim, so
102
+ * `--runtime deno` was refused as "not installed" on a host where `deno` runs.
98
103
  */
99
- function forWin32(cmd, args, env) {
104
+ export function toSpawnable(cmd, args, env) {
100
105
  const rewritten = resolveWin32Command(cmd, args, {
101
106
  platform: process.platform,
102
107
  env: env ?? process.env,
@@ -132,7 +137,7 @@ export function spawnToCompletion(cmd, args, opts) {
132
137
  // must not touch it — see `SpawnToCompletionOptions.shell`.
133
138
  const invocation = opts.shell
134
139
  ? { cmd, args: [...args], windowsVerbatimArguments: false }
135
- : forWin32(cmd, args, env);
140
+ : toSpawnable(cmd, args, env);
136
141
  if (mustBlock(opts)) {
137
142
  // `'inherit'` is NOT forwarded through `@gjsify/child_process`'s GJS
138
143
  // spawnSync path (it drives a private main context via
@@ -23,9 +23,23 @@ export declare function spawnSupervised(cmd: string, cmdArgs: string[], env?: No
23
23
  * "this project").
24
24
  */
25
25
  export declare function spawnBundleSupervised(runtime: ExampleRuntime, bundlePath: string, cwd: string, subject: string): Promise<ChildProcess>;
26
+ /** One path the loop watches — a directory recursively, or the directory's own entries only. */
27
+ export interface WatchTarget {
28
+ path: string;
29
+ recursive: boolean;
30
+ }
26
31
  export interface WatchLoopOptions {
27
- /** Directory watched recursively. */
32
+ /** Directory watched recursively — the label and the default when `watch` is not given. */
28
33
  dir: string;
34
+ /**
35
+ * Several paths instead of `dir` alone. `gjsify webext dev` needs it: its
36
+ * sources sit in sibling top-level directories next to `node_modules`, and a
37
+ * recursive watch on the project root costs one `Gio.FileMonitor` per
38
+ * directory under GJS, `node_modules` included.
39
+ */
40
+ watch?: readonly WatchTarget[];
41
+ /** An absolute changed path the loop must not rebuild for (build output, caches). */
42
+ ignore?: (changed: string) => boolean;
29
43
  /** How `dir` is spelled in the loop's own log lines. Defaults to `dir`. */
30
44
  dirLabel?: string;
31
45
  /** Prefix for the loop's log lines: `[<label>] …`. */
@@ -125,7 +125,7 @@ export function isSelfWrite(dir, output, filename) {
125
125
  * fix, and a dev loop that dies on a syntax error is a dev loop nobody uses.
126
126
  */
127
127
  export async function runWatchLoop(opts) {
128
- const { dir, label, prepare, build, spawnChild, output, debounceMs = 200 } = opts;
128
+ const { dir, label, prepare, build, spawnChild, output, ignore, debounceMs = 200 } = opts;
129
129
  // Say so BEFORE the first build: `gjsify run`'s in-process dispatch acts on
130
130
  // this the moment the handler resolves, and a first build that throws still
131
131
  // leaves the loop watching.
@@ -164,13 +164,16 @@ export async function runWatchLoop(opts) {
164
164
  };
165
165
  await rebuild();
166
166
  let debounce = null;
167
- const watcher = watch(dir, { recursive: true }, (_event, filename) => {
168
- if (isSelfWrite(dir, output, typeof filename === 'string' ? filename : null))
167
+ const watchers = (opts.watch ?? [{ path: dir, recursive: true }]).map((target) => watch(target.path, { recursive: target.recursive }, (_event, filename) => {
168
+ const name = typeof filename === 'string' ? filename : null;
169
+ if (isSelfWrite(target.path, output, name))
170
+ return;
171
+ if (ignore && name !== null && ignore(resolve(target.path, name)))
169
172
  return;
170
173
  if (debounce)
171
174
  clearTimeout(debounce);
172
175
  debounce = setTimeout(() => void rebuild(), debounceMs);
173
- });
176
+ }));
174
177
  // Under GJS nothing else keeps us here. An `fs.watch` handle is a Gio file
175
178
  // monitor, not a Node handle count, and the supervised spawn's own
176
179
  // `ensureMainLoop()` declines to arm anything: it is reached from a
@@ -198,7 +201,8 @@ export async function runWatchLoop(opts) {
198
201
  if (debounce)
199
202
  clearTimeout(debounce);
200
203
  child?.kill();
201
- watcher.close();
204
+ for (const watcher of watchers)
205
+ watcher.close();
202
206
  process.exit(0);
203
207
  };
204
208
  process.on('SIGINT', shutdown);
@@ -0,0 +1,42 @@
1
+ import { type ZipEntry } from '../ship/zip.js';
2
+ import type { RasterizeInput } from '../ship/icons.js';
3
+ import type { ResolvedWebext } from './config.js';
4
+ import type { WebextTarget } from './targets.js';
5
+ export interface BundleRequest {
6
+ entry: string;
7
+ outfile: string;
8
+ format: 'esm' | 'iife';
9
+ define: Record<string, string>;
10
+ minify: boolean;
11
+ }
12
+ export interface WebextBuildOptions {
13
+ config: ResolvedWebext;
14
+ targets: readonly WebextTarget[];
15
+ /** Absolute; the target folders are `<outDir>/<target>`. */
16
+ outDir: string;
17
+ mode: 'production' | 'development';
18
+ /** Effective defines: the config's, then the command line's. */
19
+ define: Record<string, string>;
20
+ /**
21
+ * Overwrite files instead of starting from an empty folder. `dev` needs it:
22
+ * deleting the folder `web-ext run` watches makes Firefox unload the
23
+ * extension mid-rebuild (measured in beifahrer).
24
+ */
25
+ inPlace: boolean;
26
+ zip: boolean;
27
+ /** Bundle one entry. Injectable so the assembly is testable without a bundler. */
28
+ bundle: (request: BundleRequest) => Promise<void>;
29
+ rasterize?: (input: RasterizeInput) => Promise<Map<number, Uint8Array>>;
30
+ log?: (line: string) => void;
31
+ }
32
+ export interface WebextBuildResult {
33
+ /** target id → absolute folder. */
34
+ folders: Map<string, string>;
35
+ /** target id → absolute zip path (only with `zip`). */
36
+ zips: Map<string, string>;
37
+ }
38
+ /** One `SOURCE_DATE_EPOCH`-aware timestamp for every entry, so a rebuild zips to the same bytes. */
39
+ export declare function zipTimestamp(env?: NodeJS.ProcessEnv): number;
40
+ /** Every file under `dir`, as archive entries relative to it. */
41
+ export declare function zipEntriesOf(dir: string): ZipEntry[];
42
+ export declare function buildWebext(opts: WebextBuildOptions): Promise<WebextBuildResult>;