@gjsify/cli 0.4.42 → 0.4.44

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.
@@ -1,14 +1,19 @@
1
- // Phase D-2.B.5b — runtime bundler pick.
1
+ // Runtime bundler pick (Phase D-2.B.5b → D-3.1 runtime-aware default).
2
2
  //
3
- // Default behavior: call npm `rolldown(opts).write(opts.output)` exactly
4
- // as before. Setting `GJSIFY_BUNDLER=native` opts into the
5
- // `@gjsify/rolldown-native` Vala/Rust prebuild via `bundleWithPlugins()`.
3
+ // The engine is chosen by `shouldUseNative()` per host runtime:
4
+ // - Node: npm `rolldown(opts).write(opts.output)`.
5
+ // - GJS: `@gjsify/rolldown-native` (the Vala/Rust prebuild) via
6
+ // `bundleWithPlugins()`. npm `rolldown` is a Rust N-API addon
7
+ // that CANNOT load under GJS, so the native engine is the GJS
8
+ // DEFAULT — no env var needed (this is the Node-free build
9
+ // chain's bundler). This writes the BundleOutput to disk
10
+ // (`runNativeBundle`), replicating npm rolldown's `.write()`
11
+ // incl. nested chunk/asset subdirectories.
6
12
  //
7
- // The native path is only attempted under GJS — under Node it falls
8
- // back to npm rolldown silently. Under GJS, if the prebuild isn't
9
- // loadable for the running architecture, we throw so the caller
10
- // notices the configuration mismatch instead of silently using a
11
- // different code path.
13
+ // `GJSIFY_BUNDLER=native|npm` is an explicit override: `native` forces
14
+ // the native engine and throws if its prebuild isn't loadable for the
15
+ // running architecture (instead of silently switching engines); `npm`
16
+ // forces the npm crate (Node only).
12
17
  //
13
18
  // Plugin shape conversion: rolldown plugins may declare hooks either
14
19
  // as bare functions (`load(id)`) or as `{filter, handler}` objects.
@@ -264,10 +269,23 @@ async function runNativeBundle(finalOpts) {
264
269
  if (outDir)
265
270
  await fs.mkdir(outDir, { recursive: true });
266
271
  for (const item of result.output) {
272
+ // A single `--outfile` build collapses to that exact path; otherwise
273
+ // each chunk/asset lands at `outDir/<fileName>`. Nested fileNames
274
+ // (e.g. `_virtual/_rolldown/runtime.js` from a `--library` /
275
+ // code-split build) live in subdirectories npm rolldown's `.write()`
276
+ // creates implicitly — replicate that here, or `writeFile` throws
277
+ // ENOENT on the missing parent (regression: `--library` under the
278
+ // native GJS engine). Also write text assets for `.write()` parity.
279
+ const target = outputCfg.file && item.type === 'chunk' && result.output.length === 1
280
+ ? outputCfg.file
281
+ : path.join(outDir, item.fileName);
282
+ await fs.mkdir(path.dirname(target), { recursive: true });
267
283
  if (item.type === 'chunk') {
268
- const target = outputCfg.file && result.output.length === 1 ? outputCfg.file : path.join(outDir, item.fileName);
269
284
  await fs.writeFile(target, item.code, 'utf8');
270
285
  }
286
+ else if (item.sourceText !== undefined) {
287
+ await fs.writeFile(target, item.sourceText, 'utf8');
288
+ }
271
289
  }
272
290
  // Synthesize the RolldownOutput shape downstream code touches.
273
291
  return synthRolldownOutput(result);
@@ -10,15 +10,19 @@
10
10
  // Classifier table (first-match wins) handles the cases that aren't a
11
11
  // straightforward "file lives in workspace X":
12
12
  //
13
- // 1. GLOBAL_TRIGGERS — change touches infra the classifier itself
14
- // depends on (`@gjsify/workspace`, `@gjsify/cli`, the bundler
15
- // plugins, the lockfile, root tsconfig / package.json, this very
16
- // workflow file). Emits `global=true` → CI must run the full
17
- // suite. We can't trust the closure when the algorithm that
18
- // computes it just changed.
13
+ // 1. IGNORE — pure-docs / *.md / website / refs/ submodule / unrelated
14
+ // workflow files / LICENSE / .gitignore. Discarded FIRST, before every
15
+ // other rule: an ignored file is never build-relevant, not even inside
16
+ // a GLOBAL_TRIGGERS directory (e.g. `packages/infra/cli/README.md` must
17
+ // not force a full run just because `packages/infra/cli/` is a global
18
+ // trigger). Ignore wins over global.
19
19
  //
20
- // 2. IGNORE — pure-docs / website / refs/ submodule / unrelated
21
- // workflow files. Discard; do not contribute to seeds.
20
+ // 2. GLOBAL_TRIGGERS — a non-ignored change touching infra the classifier
21
+ // itself depends on (`@gjsify/workspace`, `@gjsify/cli`, the bundler
22
+ // plugins, the lockfile, root tsconfig / package.json, this very
23
+ // workflow file). Emits `global=true` → CI must run the full suite. We
24
+ // can't trust the closure when the algorithm that computes it just
25
+ // changed.
22
26
  //
23
27
  // 3. TEST_ONLY — every changed file under ONE workspace is a spec
24
28
  // file, e2e fixture, or integration test. Seed = that workspace
@@ -133,22 +137,11 @@ function classifyAndExpand(workspaces, rootDir, changedFiles) {
133
137
  skipAll: true,
134
138
  };
135
139
  }
136
- // Global triggers short-circuit immediately.
137
- for (const f of files) {
138
- for (const re of GLOBAL_TRIGGERS) {
139
- if (re.test(f)) {
140
- return {
141
- global: true,
142
- reason: `global-trigger ${re.source} matched ${f}`,
143
- workspaces: workspaces.map((w) => w.name),
144
- runE2E: true,
145
- runIntegration: true,
146
- skipAll: false,
147
- };
148
- }
149
- }
150
- }
151
- // Drop ignored files; collect the remainder.
140
+ // Drop ignored files FIRST — before the global-trigger check. An ignored
141
+ // file (docs, *.md, LICENSE, .gitignore, …) is never build-relevant, not
142
+ // even inside a GLOBAL_TRIGGERS directory: `packages/infra/cli/README.md`
143
+ // must NOT force a full run just because `packages/infra/cli/` is a global
144
+ // trigger. Ignore wins over global.
152
145
  const remaining = [];
153
146
  for (const f of files) {
154
147
  if (IGNORE.some((re) => re.test(f)))
@@ -165,6 +158,21 @@ function classifyAndExpand(workspaces, rootDir, changedFiles) {
165
158
  skipAll: true,
166
159
  };
167
160
  }
161
+ // Global triggers short-circuit — checked on the non-ignored remainder.
162
+ for (const f of remaining) {
163
+ for (const re of GLOBAL_TRIGGERS) {
164
+ if (re.test(f)) {
165
+ return {
166
+ global: true,
167
+ reason: `global-trigger ${re.source} matched ${f}`,
168
+ workspaces: workspaces.map((w) => w.name),
169
+ runE2E: true,
170
+ runIntegration: true,
171
+ skipAll: false,
172
+ };
173
+ }
174
+ }
175
+ }
168
176
  // Map files → workspaces. Files outside any workspace stay in `unmatched`.
169
177
  const { matched, unmatched } = workspacesForChangedFiles(workspaces, rootDir, remaining);
170
178
  // Unmatched-but-not-ignored files are suspicious enough to fall back to
@@ -2,10 +2,11 @@ import type { Command } from '../../types/index.js';
2
2
  import { flatpakInitCommand } from './init.js';
3
3
  import { flatpakBuildCommand } from './build.js';
4
4
  import { flatpakDepsCommand } from './deps.js';
5
+ import { flatpakSourcesCommand } from './sources.js';
5
6
  import { flatpakCiCommand } from './ci.js';
6
7
  import { flatpakCheckCommand } from './check.js';
7
8
  import { flatpakSyncFlathubCommand } from './sync-flathub.js';
8
9
  import { flatpakDiffCommand } from './diff.js';
9
10
  import { flatpakReleaseCommand } from './release.js';
10
11
  export declare const flatpakCommand: Command;
11
- export { flatpakInitCommand, flatpakBuildCommand, flatpakDepsCommand, flatpakCiCommand, flatpakCheckCommand, flatpakSyncFlathubCommand, flatpakDiffCommand, flatpakReleaseCommand, };
12
+ export { flatpakInitCommand, flatpakBuildCommand, flatpakDepsCommand, flatpakSourcesCommand, flatpakCiCommand, flatpakCheckCommand, flatpakSyncFlathubCommand, flatpakDiffCommand, flatpakReleaseCommand, };
@@ -6,6 +6,7 @@
6
6
  import { flatpakInitCommand } from './init.js';
7
7
  import { flatpakBuildCommand } from './build.js';
8
8
  import { flatpakDepsCommand } from './deps.js';
9
+ import { flatpakSourcesCommand } from './sources.js';
9
10
  import { flatpakCiCommand } from './ci.js';
10
11
  import { flatpakCheckCommand } from './check.js';
11
12
  import { flatpakSyncFlathubCommand } from './sync-flathub.js';
@@ -13,12 +14,13 @@ import { flatpakDiffCommand } from './diff.js';
13
14
  import { flatpakReleaseCommand } from './release.js';
14
15
  export const flatpakCommand = {
15
16
  command: 'flatpak <subcommand>',
16
- description: 'Flatpak toolchain: init/build/deps/ci/check/sync-flathub/diff/release subcommands for shipping GJS apps and CLIs as Flatpaks.',
17
+ description: 'Flatpak toolchain: init/build/deps/sources/ci/check/sync-flathub/diff/release subcommands for shipping GJS apps and CLIs as Flatpaks.',
17
18
  builder: (yargs) => {
18
19
  return yargs
19
20
  .command(flatpakInitCommand.command, flatpakInitCommand.description, flatpakInitCommand.builder, flatpakInitCommand.handler)
20
21
  .command(flatpakBuildCommand.command, flatpakBuildCommand.description, flatpakBuildCommand.builder, flatpakBuildCommand.handler)
21
22
  .command(flatpakDepsCommand.command, flatpakDepsCommand.description, flatpakDepsCommand.builder, flatpakDepsCommand.handler)
23
+ .command(flatpakSourcesCommand.command, flatpakSourcesCommand.description, flatpakSourcesCommand.builder, flatpakSourcesCommand.handler)
22
24
  .command(flatpakCiCommand.command, flatpakCiCommand.description, flatpakCiCommand.builder, flatpakCiCommand.handler)
23
25
  .command(flatpakCheckCommand.command, flatpakCheckCommand.description, flatpakCheckCommand.builder, flatpakCheckCommand.handler)
24
26
  .command(flatpakSyncFlathubCommand.command, flatpakSyncFlathubCommand.description, flatpakSyncFlathubCommand.builder, flatpakSyncFlathubCommand.handler)
@@ -28,4 +30,4 @@ export const flatpakCommand = {
28
30
  .strict();
29
31
  },
30
32
  };
31
- export { flatpakInitCommand, flatpakBuildCommand, flatpakDepsCommand, flatpakCiCommand, flatpakCheckCommand, flatpakSyncFlathubCommand, flatpakDiffCommand, flatpakReleaseCommand, };
33
+ export { flatpakInitCommand, flatpakBuildCommand, flatpakDepsCommand, flatpakSourcesCommand, flatpakCiCommand, flatpakCheckCommand, flatpakSyncFlathubCommand, flatpakDiffCommand, flatpakReleaseCommand, };
@@ -0,0 +1,11 @@
1
+ import type { Command } from '../../types/index.js';
2
+ type LockfileType = 'gjsify' | 'npm' | 'yarn' | 'pnpm';
3
+ interface FlatpakSourcesOptions {
4
+ lockfile?: string;
5
+ type?: LockfileType;
6
+ out?: string;
7
+ cacheRoot?: string;
8
+ printModule?: boolean;
9
+ }
10
+ export declare const flatpakSourcesCommand: Command<unknown, FlatpakSourcesOptions>;
11
+ export {};
@@ -0,0 +1,318 @@
1
+ // `gjsify flatpak sources` — turn a lockfile into a flatpak-builder `sources`
2
+ // array so a Flathub build (which forbids network access) can vendor every npm
3
+ // tarball offline.
4
+ //
5
+ // Auto-detects the lockfile format — gjsify's own `gjsify-lock.json`, npm's
6
+ // `package-lock.json`, classic `yarn.lock`, or pnpm's `pnpm-lock.yaml` — so it
7
+ // is a Node-free, dependency-free replacement for the Python
8
+ // `flatpak-node-generator` that `gjsify flatpak deps` wraps (one tool, no pipx).
9
+ //
10
+ // Each locked tarball becomes a flatpak `file` source that downloads straight
11
+ // into gjsify's content-addressed tarball cache layout:
12
+ //
13
+ // $XDG_CACHE_HOME/gjsify/tarballs/v1/<algo>/<hex[0:2]>/<full-hex>.tgz
14
+ //
15
+ // (the exact path `install-tarball-cache.ts` computes from the SRI integrity).
16
+ // flatpak-builder fetches them in its network-allowed download phase; the build
17
+ // itself then runs `gjsify install --immutable` fully offline, finding every
18
+ // tarball in the pre-populated cache.
19
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
20
+ import { dirname, join, resolve } from 'node:path';
21
+ import { Buffer } from 'node:buffer';
22
+ /** Checksum algorithms flatpak-builder accepts on a `file` source. */
23
+ const FLATPAK_ALGOS = new Set(['sha256', 'sha512']);
24
+ /** Parse an SRI integrity (`sha512-<base64>`) into `{ algorithm, hex }`. Mirrors
25
+ * `install-tarball-cache.ts` so the emitted cache paths match byte-for-byte. */
26
+ function parseSri(integrity) {
27
+ if (!integrity)
28
+ return null;
29
+ const dash = integrity.indexOf('-');
30
+ if (dash <= 0 || dash === integrity.length - 1)
31
+ return null;
32
+ const algorithm = integrity.slice(0, dash);
33
+ const b64 = integrity.slice(dash + 1).replace(/=+$/, '');
34
+ let hex;
35
+ try {
36
+ hex = Buffer.from(b64, 'base64').toString('hex');
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ if (hex.length < 4)
42
+ return null;
43
+ return { algorithm, hex };
44
+ }
45
+ /** gjsify-lock.json + package-lock.json share npm's v2/v3 `packages` shape:
46
+ * a map keyed by install path, each with `resolved` + `integrity`. Falls back
47
+ * to npm's v1 nested `dependencies` tree. */
48
+ function parseNpmLike(json) {
49
+ const out = [];
50
+ const root = json;
51
+ const packages = root.packages;
52
+ if (packages && typeof packages === 'object') {
53
+ for (const entry of Object.values(packages)) {
54
+ if (entry?.resolved && entry.integrity)
55
+ out.push({ url: entry.resolved, integrity: entry.integrity });
56
+ }
57
+ if (out.length > 0)
58
+ return out;
59
+ }
60
+ // npm lockfileVersion 1: nested `dependencies` tree.
61
+ if (root.dependencies && typeof root.dependencies === 'object')
62
+ collectV1Deps(root.dependencies, out);
63
+ return out;
64
+ }
65
+ function collectV1Deps(deps, out) {
66
+ for (const node of Object.values(deps)) {
67
+ if (node?.resolved && node.integrity)
68
+ out.push({ url: node.resolved, integrity: node.integrity });
69
+ if (node?.dependencies)
70
+ collectV1Deps(node.dependencies, out);
71
+ }
72
+ }
73
+ /** Classic (v1) `yarn.lock`: blank-line-separated blocks, each with a
74
+ * `resolved "<url>#<sha1>"` and (yarn ≥1.x) an `integrity sha512-…` line.
75
+ * Yarn Berry (v2+) is rejected — its `checksum` is a zip-cache hash, not a
76
+ * tarball SRI, so the tarball can't be verified from the lockfile. */
77
+ function parseYarnClassic(text) {
78
+ if (/^__metadata:/m.test(text)) {
79
+ throw new Error('gjsify flatpak sources: Yarn Berry (v2+) yarn.lock is not supported — its `checksum` is ' +
80
+ 'a cache hash, not a tarball SRI. Run `gjsify install` to produce a gjsify-lock.json, ' +
81
+ 'or use a classic (v1) yarn.lock.');
82
+ }
83
+ const out = [];
84
+ let noIntegrity = 0;
85
+ for (const block of text.split(/\n\s*\n/)) {
86
+ const rm = block.match(/^\s+resolved\s+"([^"]+)"/m);
87
+ if (!rm)
88
+ continue;
89
+ const url = rm[1].replace(/#.*$/, ''); // strip the trailing #<sha1> fragment
90
+ const im = block.match(/^\s+integrity\s+(\S+)/m);
91
+ if (!im) {
92
+ noIntegrity++;
93
+ continue;
94
+ }
95
+ out.push({ url, integrity: im[1] });
96
+ }
97
+ if (noIntegrity > 0) {
98
+ console.warn(`[gjsify flatpak sources] ${noIntegrity} yarn.lock entr(y/ies) had no integrity (pre-1.x format) ` +
99
+ 'and were skipped — re-lock with a modern yarn or use `gjsify install`.');
100
+ }
101
+ return out;
102
+ }
103
+ /** pnpm-lock.yaml: each `packages:` entry's `resolution.integrity` is the
104
+ * tarball SRI; the URL is `resolution.tarball` or reconstructed from the key.
105
+ * Handles both v9 (`name@version`) and v5/6 (`/name/version`) key shapes.
106
+ *
107
+ * A small targeted scanner instead of a full YAML parser: pnpm always writes
108
+ * the resolution inline (`resolution: {integrity: …}`), so this needs no extra
109
+ * dependency and stays bundle-light. It reads ONLY the top-level `packages:`
110
+ * block (not `snapshots:`/`importers:`), keys at exactly 2-space indent. */
111
+ function parsePnpm(text) {
112
+ const out = [];
113
+ let inPackages = false;
114
+ let currentKey = null;
115
+ for (const raw of text.split('\n')) {
116
+ const line = raw.replace(/\r$/, '');
117
+ if (/^packages:\s*$/.test(line)) {
118
+ inPackages = true;
119
+ continue;
120
+ }
121
+ // A non-indented, non-comment line ends the `packages:` section.
122
+ if (inPackages && /^[^\s#]/.test(line))
123
+ inPackages = false;
124
+ if (!inPackages)
125
+ continue;
126
+ // Package key: exactly 2-space indent, quoted or bare, ending in ':'.
127
+ const keyM = line.match(/^ {2}(?:'([^']+)'|"([^"]+)"|([^\s#][^:]*)):\s*$/);
128
+ if (keyM) {
129
+ currentKey = keyM[1] ?? keyM[2] ?? keyM[3] ?? null;
130
+ continue;
131
+ }
132
+ // Inline resolution: `resolution: {integrity: sha…, tarball: …}`.
133
+ const resM = line.match(/resolution:\s*\{([^}]*)\}/);
134
+ if (resM && currentKey) {
135
+ const intM = resM[1].match(/integrity:\s*(sha\d+-[A-Za-z0-9+/=]+)/);
136
+ if (!intM)
137
+ continue;
138
+ const tarM = resM[1].match(/tarball:\s*([^\s,}]+)/);
139
+ const url = tarM ? tarM[1] : reconstructNpmTarballUrl(currentKey);
140
+ if (url)
141
+ out.push({ url, integrity: intM[1] });
142
+ }
143
+ }
144
+ return out;
145
+ }
146
+ /** Reconstruct the canonical npm tarball URL from a pnpm package key. Returns
147
+ * null for non-registry refs (aliases, git, …). */
148
+ function reconstructNpmTarballUrl(key) {
149
+ let name;
150
+ let version;
151
+ if (key.startsWith('/')) {
152
+ // v5/6: /name/version or /@scope/name/version, optional `_peer` suffix.
153
+ const k = key.slice(1).replace(/_.*$/, '');
154
+ const idx = k.lastIndexOf('/');
155
+ if (idx <= 0)
156
+ return null;
157
+ name = k.slice(0, idx);
158
+ version = k.slice(idx + 1);
159
+ }
160
+ else {
161
+ // v9: name@version, optional `(peer@x)` suffixes.
162
+ const k = key.replace(/\(.*\)$/, '');
163
+ const at = k.lastIndexOf('@');
164
+ if (at <= 0)
165
+ return null;
166
+ name = k.slice(0, at);
167
+ version = k.slice(at + 1);
168
+ }
169
+ // Only plain registry semver versions — skip aliases / protocols.
170
+ if (!name || !version || /[@:/]/.test(version))
171
+ return null;
172
+ const basename = name.slice(name.lastIndexOf('/') + 1);
173
+ return `https://registry.npmjs.org/${name}/-/${basename}-${version}.tgz`;
174
+ }
175
+ function detectType(file) {
176
+ const base = file.toLowerCase();
177
+ if (base.endsWith('pnpm-lock.yaml'))
178
+ return 'pnpm';
179
+ if (base.endsWith('yarn.lock'))
180
+ return 'yarn';
181
+ if (base.endsWith('package-lock.json'))
182
+ return 'npm';
183
+ return 'gjsify';
184
+ }
185
+ function defaultLockfile(cwd) {
186
+ for (const f of ['gjsify-lock.json', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml']) {
187
+ if (existsSync(join(cwd, f)))
188
+ return f;
189
+ }
190
+ return 'gjsify-lock.json'; // surfaces a clear "not found" later
191
+ }
192
+ function parseLockfile(path, type) {
193
+ const text = readFileSync(path, 'utf-8');
194
+ if (type === 'yarn')
195
+ return parseYarnClassic(text);
196
+ if (type === 'pnpm')
197
+ return parsePnpm(text);
198
+ let json;
199
+ try {
200
+ json = JSON.parse(text);
201
+ }
202
+ catch (err) {
203
+ throw new Error(`gjsify flatpak sources: ${path} is not valid JSON: ${err.message}`);
204
+ }
205
+ return parseNpmLike(json);
206
+ }
207
+ // --- command ---------------------------------------------------------------
208
+ export const flatpakSourcesCommand = {
209
+ command: 'sources',
210
+ description: 'Generate an offline flatpak-builder `sources` array from any lockfile (gjsify-lock.json / package-lock.json / yarn.lock / pnpm-lock.yaml) for a no-network Flathub build.',
211
+ builder: (yargs) => {
212
+ return yargs
213
+ .option('lockfile', {
214
+ description: 'Path to the lockfile. Default: the first of gjsify-lock.json / package-lock.json / yarn.lock / pnpm-lock.yaml found in cwd.',
215
+ type: 'string',
216
+ normalize: true,
217
+ })
218
+ .option('type', {
219
+ description: 'Lockfile format. Default: detected from the filename.',
220
+ choices: ['gjsify', 'npm', 'yarn', 'pnpm'],
221
+ })
222
+ .option('out', {
223
+ description: 'Output JSON sources file',
224
+ type: 'string',
225
+ default: 'gjsify-sources.json',
226
+ normalize: true,
227
+ })
228
+ .option('cache-root', {
229
+ description: 'Relative dir the tarballs download into; point XDG_CACHE_HOME here in the build. ' +
230
+ 'Tarballs land at <cache-root>/gjsify/tarballs/v1/<algo>/<shard>/<hex>.tgz.',
231
+ type: 'string',
232
+ default: 'flatpak-gjsify-cache',
233
+ })
234
+ .option('print-module', {
235
+ description: 'Also print a ready-to-paste flatpak manifest module snippet to stderr',
236
+ type: 'boolean',
237
+ default: false,
238
+ });
239
+ },
240
+ handler: async (args) => {
241
+ const cwd = process.cwd();
242
+ const lockfile = resolve(cwd, args.lockfile ?? defaultLockfile(cwd));
243
+ if (!existsSync(lockfile)) {
244
+ throw new Error(`gjsify flatpak sources: lockfile ${lockfile} not found. ` +
245
+ 'Run `gjsify install` (or npm/yarn/pnpm) first, or pass --lockfile.');
246
+ }
247
+ const type = args.type ?? detectType(lockfile);
248
+ const tarballs = parseLockfile(lockfile, type);
249
+ const cacheRoot = args.cacheRoot ?? 'flatpak-gjsify-cache';
250
+ // Dedupe by content hash — the same tarball is locked at many install
251
+ // paths; flatpak rejects duplicate dest/filename pairs.
252
+ const byHex = new Map();
253
+ let skippedSri = 0;
254
+ let skippedAlgo = 0;
255
+ for (const { url, integrity } of tarballs) {
256
+ const sri = parseSri(integrity);
257
+ if (!sri) {
258
+ skippedSri++;
259
+ continue;
260
+ }
261
+ if (!FLATPAK_ALGOS.has(sri.algorithm)) {
262
+ skippedAlgo++;
263
+ continue;
264
+ }
265
+ const key = `${sri.algorithm}:${sri.hex}`;
266
+ if (byHex.has(key))
267
+ continue;
268
+ const source = {
269
+ type: 'file',
270
+ url,
271
+ dest: `${cacheRoot}/gjsify/tarballs/v1/${sri.algorithm}/${sri.hex.slice(0, 2)}`,
272
+ 'dest-filename': `${sri.hex}.tgz`,
273
+ };
274
+ // FLATPAK_ALGOS gates this to exactly sha256 / sha512.
275
+ if (sri.algorithm === 'sha512')
276
+ source.sha512 = sri.hex;
277
+ else
278
+ source.sha256 = sri.hex;
279
+ byHex.set(key, source);
280
+ }
281
+ // Stable order (by url) so the generated file diffs cleanly across runs.
282
+ const sources = [...byHex.values()].sort((a, b) => (a.url < b.url ? -1 : a.url > b.url ? 1 : 0));
283
+ const out = resolve(cwd, args.out ?? 'gjsify-sources.json');
284
+ mkdirSync(dirname(out), { recursive: true });
285
+ writeFileSync(out, JSON.stringify(sources, null, 2) + '\n');
286
+ console.log(`[gjsify flatpak sources] wrote ${out} — ${sources.length} tarball source(s) from ${type} lockfile` +
287
+ ` (${tarballs.length} locked entr${tarballs.length === 1 ? 'y' : 'ies'})` +
288
+ (skippedSri ? `, ${skippedSri} without a usable integrity skipped` : '') +
289
+ (skippedAlgo ? `, ${skippedAlgo} non-sha256/512 skipped` : ''));
290
+ if (args.printModule) {
291
+ console.error(buildModuleSnippet(args.out ?? 'gjsify-sources.json', cacheRoot));
292
+ }
293
+ },
294
+ };
295
+ /** A copy-pasteable flatpak manifest module that consumes the generated
296
+ * sources file and runs a fully offline `gjsify install`. */
297
+ function buildModuleSnippet(sourcesFile, cacheRoot) {
298
+ return [
299
+ '',
300
+ '# Add the generated sources to your app module and point XDG_CACHE_HOME',
301
+ '# at the vendored cache before running gjsify offline. Example module:',
302
+ '#',
303
+ '# {',
304
+ '# "name": "my-app",',
305
+ '# "buildsystem": "simple",',
306
+ '# "build-commands": [',
307
+ `# "export XDG_CACHE_HOME=\\"$(pwd)/${cacheRoot}\\"",`,
308
+ '# "gjsify install --immutable",',
309
+ '# "gjsify build src/main.ts --app gjs --outfile /app/bin/my-app"',
310
+ '# ],',
311
+ '# "sources": [',
312
+ '# { "type": "dir", "path": "." },',
313
+ `# "${sourcesFile}"`,
314
+ '# ]',
315
+ '# }',
316
+ '',
317
+ ].join('\n');
318
+ }
@@ -3,6 +3,7 @@ interface SelfUpdateOptions {
3
3
  check?: boolean;
4
4
  force?: boolean;
5
5
  tag: string;
6
+ skipDeps?: boolean;
6
7
  }
7
8
  export declare const selfUpdateCommand: Command<unknown, SelfUpdateOptions>;
8
9
  export {};
@@ -36,6 +36,13 @@ export const selfUpdateCommand = {
36
36
  description: 'npm dist-tag or pinned version to install (e.g. `latest`, `next`, `0.5.0`).',
37
37
  type: 'string',
38
38
  default: 'latest',
39
+ })
40
+ .option('skip-deps', {
41
+ description: 'Update only the @gjsify/cli bundle itself; do not pull its runtime dependencies ' +
42
+ '(rolldown, lightningcss, @gjsify/tsc, the native gi:// bridges, …). Faster, but can ' +
43
+ 'leave the on-disk runtime deps stale relative to the new bundle.',
44
+ type: 'boolean',
45
+ default: false,
39
46
  }),
40
47
  handler: async (args) => {
41
48
  const layout = defaultGlobalLayout();
@@ -80,21 +87,33 @@ export const selfUpdateCommand = {
80
87
  process.exit(1);
81
88
  return;
82
89
  }
83
- console.log(`Installing ${PACKAGE_NAME}@${target} ...`);
84
- // `@gjsify/cli` is a self-contained GJS bundle: every workspace dep
85
- // (`@gjsify/node-polyfills`, `@gjsify/v8`, …) is bundled into the
86
- // published `dist/cli.gjs.mjs` artifact and must NOT be resolved as
87
- // a separate npm package. `skipDeps: true` limits the native install
88
- // backend to fetching the top-level tarball only, avoiding spurious
89
- // packument requests for workspace-internal packages that are not
90
- // published to the public registry (which previously caused a
91
- // 406 Not Acceptable → fatal crash on the `@gjsify/v8` packument).
90
+ // `@gjsify/cli` ships two runtimes from one package: the Node `bin`
91
+ // (`lib/index.js`, which imports its deps from `node_modules`) and the
92
+ // self-contained GJS bundle (`dist/cli.gjs.mjs`, which inlines them).
93
+ // The published `dependencies` therefore must cover everything `lib/**`
94
+ // resolves at runtime PLUS the pieces only the GJS runtime loads from
95
+ // disk (the `gi://` native typelib bridges via the launcher's
96
+ // GI_TYPELIB_PATH, `rolldown`/`lightningcss` native addons, the
97
+ // separate `@gjsify/tsc` bundle, `@gjsify/create-app`'s disk templates).
98
+ //
99
+ // By DEFAULT self-update now resolves that full production `dependencies`
100
+ // tree (`skipDeps: false`) so the on-disk runtime deps move in lockstep
101
+ // with the freshly-installed bundle instead of skewing behind it. This is
102
+ // safe today because every `@gjsify/*` package — including the
103
+ // `@gjsify/v8` that once 404'd → 406 and forced `skipDeps: true`
104
+ // unconditionally — is now published to the public registry.
105
+ //
106
+ // `--skip-deps` restores the old fast path: fetch only the top-level
107
+ // `@gjsify/cli` tarball and re-link the bin, leaving runtime deps untouched.
108
+ const pullDeps = !args.skipDeps;
109
+ const depNote = pullDeps ? ' + runtime dependencies' : ' (bundle only)';
110
+ console.log(`Installing ${PACKAGE_NAME}@${target}${depNote} ...`);
92
111
  try {
93
112
  await installPackages({
94
113
  prefix: layout.prefix,
95
114
  specs: [`${PACKAGE_NAME}@${target}`],
96
115
  verbose: false,
97
- skipDeps: true,
116
+ skipDeps: !pullDeps,
98
117
  });
99
118
  }
100
119
  catch (err) {
@@ -114,6 +133,12 @@ export const selfUpdateCommand = {
114
133
  }
115
134
  }
116
135
  console.log(`\nUpdated ${PACKAGE_NAME} to v${target}.`);
136
+ if (pullDeps) {
137
+ console.log('Runtime dependencies were resolved alongside the bundle.');
138
+ }
139
+ else {
140
+ console.log('Bundle-only update (--skip-deps): on-disk runtime dependencies were left unchanged.');
141
+ }
117
142
  },
118
143
  };
119
144
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.4.42",
3
+ "version": "0.4.44",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -120,19 +120,19 @@
120
120
  "cli"
121
121
  ],
122
122
  "dependencies": {
123
- "@gjsify/buffer": "^0.4.42",
124
- "@gjsify/create-app": "^0.4.42",
125
- "@gjsify/node-globals": "^0.4.42",
126
- "@gjsify/node-polyfills": "^0.4.42",
127
- "@gjsify/npm-registry": "^0.4.42",
128
- "@gjsify/resolve-npm": "^0.4.42",
129
- "@gjsify/rolldown-plugin-gjsify": "^0.4.42",
130
- "@gjsify/rolldown-plugin-pnp": "^0.4.42",
131
- "@gjsify/semver": "^0.4.42",
132
- "@gjsify/tar": "^0.4.42",
133
- "@gjsify/tsc": "^0.4.42",
134
- "@gjsify/web-polyfills": "^0.4.42",
135
- "@gjsify/workspace": "^0.4.42",
123
+ "@gjsify/buffer": "^0.4.44",
124
+ "@gjsify/create-app": "^0.4.44",
125
+ "@gjsify/node-globals": "^0.4.44",
126
+ "@gjsify/node-polyfills": "^0.4.44",
127
+ "@gjsify/npm-registry": "^0.4.44",
128
+ "@gjsify/resolve-npm": "^0.4.44",
129
+ "@gjsify/rolldown-plugin-gjsify": "^0.4.44",
130
+ "@gjsify/rolldown-plugin-pnp": "^0.4.44",
131
+ "@gjsify/semver": "^0.4.44",
132
+ "@gjsify/tar": "^0.4.44",
133
+ "@gjsify/tsc": "^0.4.44",
134
+ "@gjsify/web-polyfills": "^0.4.44",
135
+ "@gjsify/workspace": "^0.4.44",
136
136
  "cosmiconfig": "^9.0.1",
137
137
  "get-tsconfig": "^4.14.0",
138
138
  "pkg-types": "^2.3.1",
@@ -140,12 +140,12 @@
140
140
  "yargs": "^18.0.0"
141
141
  },
142
142
  "devDependencies": {
143
- "@gjsify/unit": "^0.4.42",
143
+ "@gjsify/unit": "^0.4.44",
144
144
  "@types/yargs": "^17.0.35",
145
145
  "typescript": "^6.0.3"
146
146
  },
147
147
  "peerDependencies": {
148
- "@gjsify/rolldown-native": "^0.4.42"
148
+ "@gjsify/rolldown-native": "^0.4.44"
149
149
  },
150
150
  "peerDependenciesMeta": {
151
151
  "@gjsify/rolldown-native": {