@gjsify/cli 0.40.0 → 0.41.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.
@@ -0,0 +1,106 @@
1
+ // How was THIS gjsify installed?
2
+ //
3
+ // `self-update` re-runs the `gjsify install -g` pipeline, which writes into the
4
+ // user-global XDG prefix. That is correct for an install that came from
5
+ // `install.mjs` or `gjsify install -g`, and WRONG for one a package manager owns:
6
+ // it would lay a second copy into `~/.local` that shadows — or is shadowed by —
7
+ // the one apt/dnf/flatpak is tracking, and the package manager would then be
8
+ // managing a file the user no longer runs. The next `apt upgrade` reverts or
9
+ // diverges, silently, and "I updated it and nothing changed" is the report.
10
+ //
11
+ // Before `gjsify ship` there was only one non-XDG case worth naming (`npm -g`),
12
+ // and the command warned and continued. Now that gjsify ships as `.deb`, `.rpm`
13
+ // and Flatpak, continuing is the defect. So the question gets a real answer, and
14
+ // the answer carries the command the user should run instead.
15
+ //
16
+ // Everything here is derived from where the RUNNING code sits, never from a
17
+ // build-time constant: one tarball is unpacked by all of these routes, so a flag
18
+ // baked at build time would describe the builder rather than the installation.
19
+ import { posix, win32 } from 'node:path';
20
+ /** System prefixes a distro package stages into. POSIX only — win32 handled separately. */
21
+ const SYSTEM_PREFIXES = ['/usr/', '/opt/', '/snap/'];
22
+ /** Inside a Flatpak the whole app tree lives under this prefix. */
23
+ const FLATPAK_PREFIX = '/app/';
24
+ /**
25
+ * The TARGET platform's path module, never the host's.
26
+ *
27
+ * Same rule as `pathFor` in `install-global.ts`, and for the same reason: this
28
+ * function ACCEPTS a `platform` and must therefore answer for it. Reading the
29
+ * host's `node:path` instead makes every POSIX-shaped case unanswerable from a
30
+ * Windows runner — which is exactly what happened. Four of these tests went red on
31
+ * the win32 leg, on `main`, because `resolve('/app/lib')` there is `C:\app\lib`
32
+ * and `sep` is a backslash.
33
+ */
34
+ function pathFor(platform) {
35
+ return platform === 'win32' ? win32 : posix;
36
+ }
37
+ /** Does `child` sit inside `parent`? Both must be absolute and already resolved. */
38
+ function isInside(child, parent, platform) {
39
+ const p = pathFor(platform);
40
+ if (!p.isAbsolute(child) || !p.isAbsolute(parent))
41
+ return false;
42
+ const withSep = parent.endsWith(p.sep) ? parent : parent + p.sep;
43
+ return child === parent || child.startsWith(withSep);
44
+ }
45
+ /**
46
+ * Classify an installation from its path and environment.
47
+ *
48
+ * ORDER MATTERS and is not alphabetical:
49
+ *
50
+ * 1. **Flatpak first.** Its `/app` prefix would also satisfy no other test, but
51
+ * `FLATPAK_ID` is the unambiguous signal and is present even when the tree is
52
+ * mounted somewhere unusual.
53
+ * 2. **XDG before system.** The XDG prefix is under `$HOME`, so it can never be
54
+ * `/usr` — but a user whose `$HOME` is `/opt/someuser` exists, and asking the
55
+ * narrow question first means that user is not told to run `dnf`.
56
+ * 3. **System before npm.** A `.deb` stages a `node_modules`-shaped tree under
57
+ * `/usr/lib`, so the npm test alone would claim it.
58
+ */
59
+ export function classifyInstall(input) {
60
+ const env = input.env ?? process.env;
61
+ const platform = input.platform ?? process.platform;
62
+ const p = pathFor(platform);
63
+ const selfDir = p.resolve(input.selfDir);
64
+ if (env.FLATPAK_ID || isInside(selfDir, FLATPAK_PREFIX, platform)) {
65
+ const id = env.FLATPAK_ID ?? 'io.github.gjsify.Cli';
66
+ return {
67
+ kind: 'flatpak',
68
+ evidence: env.FLATPAK_ID ? `FLATPAK_ID=${env.FLATPAK_ID}` : `running from ${FLATPAK_PREFIX}`,
69
+ updateWith: `flatpak update ${id}`,
70
+ managedElsewhere: true,
71
+ };
72
+ }
73
+ if (isInside(selfDir, p.resolve(input.xdgPrefix), platform)) {
74
+ return {
75
+ kind: 'xdg-global',
76
+ evidence: `running from the user-global prefix ${input.xdgPrefix}`,
77
+ managedElsewhere: false,
78
+ };
79
+ }
80
+ if (platform !== 'win32' && SYSTEM_PREFIXES.some((pfx) => isInside(selfDir, pfx, platform))) {
81
+ return {
82
+ kind: 'system-package',
83
+ evidence: `running from the system prefix ${selfDir}`,
84
+ // Not `apt` or `dnf` specifically: the same path serves both, and
85
+ // guessing wrong sends the user to a package manager that does not
86
+ // know this file. The release assets are named because they are the
87
+ // one answer that is true on every distribution.
88
+ updateWith: 'your distribution package manager (`apt upgrade` / `dnf upgrade` / …), or install the newer package from ' +
89
+ 'https://github.com/gjsify/gjsify/releases/latest',
90
+ managedElsewhere: true,
91
+ };
92
+ }
93
+ if (selfDir.split(p.sep).includes('node_modules')) {
94
+ return {
95
+ kind: 'npm-global',
96
+ evidence: `running from a node_modules tree at ${selfDir}`,
97
+ updateWith: 'npm install -g @gjsify/cli@latest (or switch to the Node-free bootstrap)',
98
+ managedElsewhere: true,
99
+ };
100
+ }
101
+ return {
102
+ kind: 'unknown',
103
+ evidence: `running from ${selfDir}, which matches no known install layout`,
104
+ managedElsewhere: false,
105
+ };
106
+ }
@@ -0,0 +1,83 @@
1
+ import { type PlatformDeclaration, type PlatformTarget, type PlatformVerdict } from './platform-check.js';
2
+ /** One package directory found under a prefix's `node_modules` tree. */
3
+ export interface InstalledPackage {
4
+ /** `name` from its manifest, or the directory path when the manifest is unreadable. */
5
+ name: string;
6
+ version: string;
7
+ /** Absolute path to the package directory. */
8
+ dir: string;
9
+ /**
10
+ * A symlink, i.e. a workspace source tree linked into `node_modules` rather than
11
+ * a copy the installer extracted. Never pruned and never descended: the installer
12
+ * refuses to remove one for the same reason, and following it would walk the
13
+ * user's own sources.
14
+ */
15
+ linked: boolean;
16
+ /** The `os`/`cpu`/`libc` slice, absent when the manifest declares none. */
17
+ platform?: PlatformDeclaration;
18
+ }
19
+ export interface PruneEntry {
20
+ pkg: InstalledPackage;
21
+ /** Apparent size of the package directory, summed from `statSync().size`. */
22
+ bytes: number;
23
+ verdict: PlatformVerdict;
24
+ }
25
+ export interface PrunePlan {
26
+ prefix: string;
27
+ target: PlatformTarget;
28
+ /** How many package directories were examined, so an empty plan is distinguishable from an empty scan. */
29
+ scanned: number;
30
+ entries: PruneEntry[];
31
+ bytes: number;
32
+ }
33
+ export interface PruneResult {
34
+ plan: PrunePlan;
35
+ removed: PruneEntry[];
36
+ failed: Array<{
37
+ entry: PruneEntry;
38
+ error: string;
39
+ }>;
40
+ bytes: number;
41
+ /** `.bin` entries removed because they were symlinks with no target left. */
42
+ binLinks: string[];
43
+ }
44
+ /** Every package directory under `<prefix>/node_modules`, at any depth. */
45
+ export declare function scanPrefix(prefix: string): InstalledPackage[];
46
+ /**
47
+ * The packages `target` cannot use. Pure — no filesystem, no `process.*` — so every
48
+ * OS/arch/libc branch is decidable from any host.
49
+ */
50
+ export declare function planPlatformPrune(pkgs: readonly InstalledPackage[], target: PlatformTarget): PruneEntry[];
51
+ /** Apparent size of a directory tree — what the files claim, not what the block device spent. */
52
+ export declare function dirBytes(dir: string): number;
53
+ export declare function planPrune(opts: {
54
+ prefix: string;
55
+ target: PlatformTarget;
56
+ }): PrunePlan;
57
+ /**
58
+ * Remove a plan's entries.
59
+ *
60
+ * `remove` is injected so the failure path is testable without a filesystem that
61
+ * can fail on demand. Nothing here throws: this runs as housekeeping at the end of
62
+ * an install, and an install that already succeeded must not be reported as failed
63
+ * because a directory could not be unlinked.
64
+ */
65
+ export declare function executePrune(plan: PrunePlan, opts?: {
66
+ dryRun?: boolean;
67
+ remove?: (dir: string) => void;
68
+ }): PruneResult;
69
+ export declare function formatPruneReport(result: PruneResult, opts: {
70
+ dryRun: boolean;
71
+ verbose: boolean;
72
+ }): string;
73
+ /** Why an automatic pass declined to run, or null when it may proceed. */
74
+ export declare function automaticPruneRefusal(env: Record<string, string | undefined>, immutable: boolean): string | null;
75
+ /**
76
+ * The automatic pass: prune what this install would not have placed, report one
77
+ * line, and never fail the install that called it.
78
+ */
79
+ export declare function pruneAfterInstall(prefix: string, target: PlatformTarget, opts?: {
80
+ env?: Record<string, string | undefined>;
81
+ immutable?: boolean;
82
+ hint?: string;
83
+ }): void;
@@ -0,0 +1,333 @@
1
+ // Remove what an install prefix holds that THIS host cannot use.
2
+ //
3
+ // An install prefix is a write-only union today: `applyPlatformFilter` keeps a
4
+ // foreign-platform package from being installed, and nothing ever removes one that
5
+ // an EARLIER install placed. Measured on a workstation whose user-global prefix was
6
+ // created in May and whose CLI was current: 638 MB, of which `@rolldown` alone was
7
+ // 258 MB across sixteen platform bindings on a host that can load exactly one.
8
+ //
9
+ // The residue is ACCRETION, not a live filter defect, and the on-disk versions are
10
+ // what prove it: the foreign `@rolldown/binding-*` dirs were a minor version behind
11
+ // the `rolldown` that pins all of them, and every foreign `@gjsify/*-<os>-<arch>`
12
+ // was at the release that first SHIPPED the filter. They were written by the
13
+ // previous CLI, during the self-update that installed the fix. A filter cannot
14
+ // remove what predates it, which is why this exists as its own pass.
15
+ //
16
+ // ## What this is allowed to decide
17
+ //
18
+ // One rule, and it is a pure manifest read: npm's own `os`/`cpu`/`libc` fields,
19
+ // through the SAME `checkPlatform` the installer filters with — so a pruned prefix
20
+ // converges on what a fresh install would have placed, rather than on this module's
21
+ // opinion. A package that declares nothing is never touched: `@rolldown/binding-
22
+ // wasm32-wasi` is unusable here and says so nowhere, and guessing from its name is
23
+ // how a prune starts deleting things it cannot justify.
24
+ //
25
+ // Reachability ("nothing installed points at this any more") would catch that one
26
+ // and more, but it needs a record of what the prefix was ASSEMBLED FROM, which no
27
+ // prefix carries yet. Uncertain means keep — see ADR 0025 and
28
+ // `status/open-todos.md`.
29
+ import { existsSync, lstatSync, readFileSync, readdirSync, rmSync, statSync, unlinkSync } from 'node:fs';
30
+ import { join } from 'node:path';
31
+ import { checkPlatform, declaresPlatform, describePlatformTarget, readPlatformOverrides, } from './platform-check.js';
32
+ /** Directory entries under `node_modules` that are never packages. */
33
+ function isReservedEntry(name) {
34
+ // `.bin` holds launchers, `.gjsify-install-lock` is the LIVE cross-process lock
35
+ // directory — removing it mid-install would hand the prefix to a second writer.
36
+ return name.startsWith('.');
37
+ }
38
+ function readManifest(dir) {
39
+ try {
40
+ const raw = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
41
+ const platform = {
42
+ os: raw.os,
43
+ cpu: raw.cpu,
44
+ libc: raw.libc,
45
+ };
46
+ return {
47
+ name: typeof raw.name === 'string' ? raw.name : undefined,
48
+ version: typeof raw.version === 'string' ? raw.version : undefined,
49
+ platform: declaresPlatform(platform) ? platform : undefined,
50
+ };
51
+ }
52
+ catch {
53
+ // An unreadable or half-written manifest declares nothing, which makes the
54
+ // package un-prunable by the only rule this module has. That is the safe
55
+ // direction: the one thing worse than 638 MB of residue is deleting a
56
+ // package because its manifest could not be parsed.
57
+ return null;
58
+ }
59
+ }
60
+ function collect(nodeModules, out) {
61
+ let entries;
62
+ try {
63
+ entries = readdirSync(nodeModules, { withFileTypes: true });
64
+ }
65
+ catch {
66
+ return;
67
+ }
68
+ for (const entry of entries) {
69
+ if (isReservedEntry(entry.name))
70
+ continue;
71
+ const full = join(nodeModules, entry.name);
72
+ if (entry.name.startsWith('@')) {
73
+ let scoped;
74
+ try {
75
+ scoped = readdirSync(full, { withFileTypes: true });
76
+ }
77
+ catch {
78
+ continue;
79
+ }
80
+ for (const inner of scoped) {
81
+ if (isReservedEntry(inner.name))
82
+ continue;
83
+ addPackage(join(full, inner.name), out);
84
+ }
85
+ continue;
86
+ }
87
+ addPackage(full, out);
88
+ }
89
+ }
90
+ function addPackage(dir, out) {
91
+ let linked = false;
92
+ try {
93
+ linked = lstatSync(dir).isSymbolicLink();
94
+ }
95
+ catch {
96
+ return;
97
+ }
98
+ const manifest = readManifest(dir);
99
+ out.push({
100
+ name: manifest?.name ?? dir,
101
+ version: manifest?.version ?? '0.0.0',
102
+ dir,
103
+ linked,
104
+ platform: manifest?.platform,
105
+ });
106
+ // A linked package is the user's own source tree — its `node_modules` is not
107
+ // this prefix's to prune.
108
+ if (!linked)
109
+ collect(join(dir, 'node_modules'), out);
110
+ }
111
+ /** Every package directory under `<prefix>/node_modules`, at any depth. */
112
+ export function scanPrefix(prefix) {
113
+ const out = [];
114
+ collect(join(prefix, 'node_modules'), out);
115
+ return out;
116
+ }
117
+ /**
118
+ * The packages `target` cannot use. Pure — no filesystem, no `process.*` — so every
119
+ * OS/arch/libc branch is decidable from any host.
120
+ */
121
+ export function planPlatformPrune(pkgs, target) {
122
+ const entries = [];
123
+ for (const pkg of pkgs) {
124
+ if (pkg.linked || !pkg.platform)
125
+ continue;
126
+ const verdict = checkPlatform(pkg.platform, target);
127
+ if (!verdict.ok)
128
+ entries.push({ pkg, bytes: 0, verdict });
129
+ }
130
+ return entries;
131
+ }
132
+ /** Apparent size of a directory tree — what the files claim, not what the block device spent. */
133
+ export function dirBytes(dir) {
134
+ let total = 0;
135
+ const stack = [dir];
136
+ while (stack.length > 0) {
137
+ const current = stack.pop();
138
+ let entries;
139
+ try {
140
+ entries = readdirSync(current, { withFileTypes: true });
141
+ }
142
+ catch {
143
+ continue;
144
+ }
145
+ for (const entry of entries) {
146
+ const full = join(current, entry.name);
147
+ if (entry.isDirectory()) {
148
+ stack.push(full);
149
+ }
150
+ else if (entry.isFile()) {
151
+ try {
152
+ total += statSync(full).size;
153
+ }
154
+ catch {
155
+ // A file that vanished mid-walk contributes nothing; a size is a
156
+ // report, and refusing to produce one over a race would be worse.
157
+ }
158
+ }
159
+ }
160
+ }
161
+ return total;
162
+ }
163
+ export function planPrune(opts) {
164
+ const pkgs = scanPrefix(opts.prefix);
165
+ const entries = planPlatformPrune(pkgs, opts.target);
166
+ for (const entry of entries)
167
+ entry.bytes = dirBytes(entry.pkg.dir);
168
+ return {
169
+ prefix: opts.prefix,
170
+ target: opts.target,
171
+ scanned: pkgs.length,
172
+ entries,
173
+ bytes: entries.reduce((sum, e) => sum + e.bytes, 0),
174
+ };
175
+ }
176
+ /**
177
+ * Remove a plan's entries.
178
+ *
179
+ * `remove` is injected so the failure path is testable without a filesystem that
180
+ * can fail on demand. Nothing here throws: this runs as housekeeping at the end of
181
+ * an install, and an install that already succeeded must not be reported as failed
182
+ * because a directory could not be unlinked.
183
+ */
184
+ export function executePrune(plan, opts = {}) {
185
+ const removed = [];
186
+ const failed = [];
187
+ const binLinks = [];
188
+ // `maxRetries` mirrors the installer's own removal: on Windows a handle held by
189
+ // a virus scanner or an indexer makes the first unlink fail and the third work.
190
+ const remove = opts.remove ?? ((dir) => rmSync(dir, { recursive: true, force: true, maxRetries: 10 }));
191
+ if (!opts.dryRun) {
192
+ for (const entry of plan.entries) {
193
+ try {
194
+ remove(entry.pkg.dir);
195
+ removed.push(entry);
196
+ }
197
+ catch (err) {
198
+ failed.push({ entry, error: err.message });
199
+ }
200
+ }
201
+ binLinks.push(...pruneDeadBinLinks(plan.prefix));
202
+ }
203
+ return {
204
+ plan,
205
+ removed,
206
+ failed,
207
+ bytes: removed.reduce((sum, e) => sum + e.bytes, 0),
208
+ binLinks,
209
+ };
210
+ }
211
+ /**
212
+ * Drop `.bin` entries that are SYMLINKS whose target is gone.
213
+ *
214
+ * Only symlinks, and only dangling ones: that is the case where deadness is
215
+ * provable from the filesystem alone. A `.bin` entry that is a real file may be a
216
+ * launcher script this prefix still needs, and deciding that would mean reading it —
217
+ * a second, weaker rule for a few hundred bytes.
218
+ */
219
+ function pruneDeadBinLinks(prefix) {
220
+ const binDir = join(prefix, 'node_modules', '.bin');
221
+ const dropped = [];
222
+ let entries;
223
+ try {
224
+ entries = readdirSync(binDir, { withFileTypes: true });
225
+ }
226
+ catch {
227
+ return dropped;
228
+ }
229
+ for (const entry of entries) {
230
+ const full = join(binDir, entry.name);
231
+ try {
232
+ if (!lstatSync(full).isSymbolicLink())
233
+ continue;
234
+ if (existsSync(full))
235
+ continue;
236
+ unlinkSync(full);
237
+ dropped.push(entry.name);
238
+ }
239
+ catch {
240
+ // Same reasoning as `executePrune`: housekeeping never fails the caller.
241
+ }
242
+ }
243
+ return dropped;
244
+ }
245
+ function humanBytes(bytes) {
246
+ if (bytes < 1024)
247
+ return `${bytes} B`;
248
+ const units = ['KB', 'MB', 'GB'];
249
+ let value = bytes / 1024;
250
+ let unit = 0;
251
+ while (value >= 1024 && unit < units.length - 1) {
252
+ value /= 1024;
253
+ unit += 1;
254
+ }
255
+ return `${value.toFixed(1)} ${units[unit]}`;
256
+ }
257
+ /** How many entries a non-verbose report lists before summarising the rest. */
258
+ const REPORT_HEAD = 10;
259
+ export function formatPruneReport(result, opts) {
260
+ const { plan } = result;
261
+ const lines = [
262
+ `${plan.prefix} · target ${describePlatformTarget(plan.target)} · scanned ${plan.scanned} package(s)`,
263
+ ];
264
+ if (plan.entries.length === 0) {
265
+ // Not "every package declares a platform this host can use": most declare
266
+ // none at all, and saying otherwise reports a check that did not happen.
267
+ lines.push('nothing to prune — no installed package declares a platform this target cannot use.');
268
+ return lines.join('\n');
269
+ }
270
+ const shown = opts.verbose ? plan.entries : plan.entries.slice(0, REPORT_HEAD);
271
+ for (const entry of shown) {
272
+ const required = JSON.stringify(entry.verdict.required);
273
+ lines.push(` ${entry.pkg.name}@${entry.pkg.version} ${humanBytes(entry.bytes)} requires ${required}`);
274
+ }
275
+ if (shown.length < plan.entries.length) {
276
+ lines.push(` … and ${plan.entries.length - shown.length} more (--verbose to list them)`);
277
+ }
278
+ // The byte figure is APPARENT size, summed from the files themselves. `du`
279
+ // reports allocated blocks and will disagree; saying so here is cheaper than
280
+ // the bug report that difference otherwise produces.
281
+ lines.push(opts.dryRun
282
+ ? `would free ${humanBytes(plan.bytes)} across ${plan.entries.length} package(s) (apparent size)`
283
+ : `freed ${humanBytes(result.bytes)} across ${result.removed.length} package(s) (apparent size)`);
284
+ if (result.binLinks.length > 0) {
285
+ lines.push(`removed ${result.binLinks.length} dangling .bin link(s): ${result.binLinks.join(', ')}`);
286
+ }
287
+ for (const f of result.failed) {
288
+ lines.push(`FAILED ${f.entry.pkg.name}: ${f.error}`);
289
+ }
290
+ return lines.join('\n');
291
+ }
292
+ /** Why an automatic pass declined to run, or null when it may proceed. */
293
+ export function automaticPruneRefusal(env, immutable) {
294
+ // THE data-loss guard. `--os/--cpu/--libc` are legal on `gjsify install -g`, and
295
+ // they reach here as npm config keys. Pruning against a target the USER TYPED
296
+ // would make `gjsify install -g foo --os=darwin` delete every linux package in
297
+ // the real shared prefix — the engine set, the bundler bindings, the CLI's own.
298
+ // An explicit `gjsify prune --os=darwin` stays legal; it is a request, not a
299
+ // side effect.
300
+ if (Object.keys(readPlatformOverrides(env)).length > 0) {
301
+ return 'the platform target was overridden on the command line';
302
+ }
303
+ // `--immutable` promises the tree is not modified. Resolution-only would be a
304
+ // defensible reading, but it is not the one the flag's users have, and
305
+ // `ensureProjectGjsEngine` already declines under it for the same reason.
306
+ if (immutable)
307
+ return '--immutable';
308
+ return null;
309
+ }
310
+ /**
311
+ * The automatic pass: prune what this install would not have placed, report one
312
+ * line, and never fail the install that called it.
313
+ */
314
+ export function pruneAfterInstall(prefix, target, opts = {}) {
315
+ try {
316
+ const refusal = automaticPruneRefusal(opts.env ?? process.env, opts.immutable ?? false);
317
+ if (refusal)
318
+ return;
319
+ const plan = planPrune({ prefix, target });
320
+ if (plan.entries.length === 0)
321
+ return;
322
+ const result = executePrune(plan);
323
+ if (result.removed.length === 0)
324
+ return;
325
+ const hint = opts.hint ? ` (${opts.hint} to review)` : '';
326
+ console.log(`gjsify: pruned ${result.removed.length} package(s) this host cannot use, ` +
327
+ `freeing ${humanBytes(result.bytes)}${hint}`);
328
+ }
329
+ catch (err) {
330
+ if (process.env.GJSIFY_DEBUG)
331
+ console.error(`gjsify: prune skipped — ${err.message}`);
332
+ }
333
+ }
@@ -106,6 +106,23 @@ const TYPELIB_PACKAGES = {
106
106
  'Vte-3.91': { deb: 'gir1.2-vte-3.91', rpm: 'vte291-gtk4' },
107
107
  'Manette-0.2': { deb: 'gir1.2-manette-0.2', rpm: 'libmanette' },
108
108
  };
109
+ /**
110
+ * A typelib the payload CARRIES, rather than one the system provides.
111
+ *
112
+ * `@gjsify/*`'s native bridges ship their own `.typelib` inside the package —
113
+ * `GjsifyHttpSoupBridge`, which `@gjsify/http`'s server pulls in unconditionally,
114
+ * plus `GjsifyHttp2`, `GjsifySabNative` and the rest of the `*-native` set. No
115
+ * distribution has ever packaged a `gir1.2-gjsifyhttpsoupbridge` and none can:
116
+ * the file is in the tarball being built. So these are not a gap in the table.
117
+ * Mapping one would emit a dependency apt and dnf must fail to resolve, and the
118
+ * module header's rule — an unmapped namespace FAILS the build — is about a
119
+ * SYSTEM library nobody declared, which is the opposite situation.
120
+ *
121
+ * Excluding them is a precondition for widening the namespace scanner rather
122
+ * than a refinement of it: without this, `gjsify ship` throws on every project
123
+ * whose bundle reaches one, which includes every `@gjsify/http` server.
124
+ */
125
+ const BUNDLED_TYPELIB = /^Gjsify[A-Z]/;
109
126
  /**
110
127
  * The `Depends:` / `Requires:` list for one format.
111
128
  *
@@ -115,6 +132,8 @@ export function deriveDepends(format, inputs) {
115
132
  const out = [`gjs >= ${inputs.minGjsVersion ?? DEFAULT_GJS_FLOOR}`];
116
133
  const unmapped = [];
117
134
  for (const namespace of [...new Set(inputs.namespaces)].sort()) {
135
+ if (BUNDLED_TYPELIB.test(namespace))
136
+ continue;
118
137
  const entry = lookupTypelib(namespace, inputs.typelibPackages);
119
138
  if (entry === undefined) {
120
139
  unmapped.push(namespace);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -96,6 +96,14 @@
96
96
  }
97
97
  ]
98
98
  },
99
+ "ship": {
100
+ "binaryName": "gjsify",
101
+ "bundle": "dist/cli.gjs.mjs",
102
+ "targets": [
103
+ "deb",
104
+ "rpm"
105
+ ]
106
+ },
99
107
  "tier": 1,
100
108
  "defineFromPackageJson": {
101
109
  "__PACKAGE_VERSION__": {
@@ -134,20 +142,20 @@
134
142
  "cli"
135
143
  ],
136
144
  "dependencies": {
137
- "@gjsify/buffer": "^0.40.0",
138
- "@gjsify/create-app": "^0.40.0",
139
- "@gjsify/node-globals": "^0.40.0",
140
- "@gjsify/node-polyfills": "^0.40.0",
141
- "@gjsify/npm-registry": "^0.40.0",
142
- "@gjsify/resolve-npm": "^0.40.0",
143
- "@gjsify/rolldown-plugin-gjsify": "^0.40.0",
144
- "@gjsify/vite-plugin-blueprint": "^0.40.0",
145
- "@gjsify/rolldown-plugin-pnp": "^0.40.0",
146
- "@gjsify/semver": "^0.40.0",
147
- "@gjsify/tar": "^0.40.0",
148
- "@gjsify/tsc": "^0.40.0",
149
- "@gjsify/web-polyfills": "^0.40.0",
150
- "@gjsify/workspace": "^0.40.0",
145
+ "@gjsify/buffer": "^0.41.0",
146
+ "@gjsify/create-app": "^0.41.0",
147
+ "@gjsify/node-globals": "^0.41.0",
148
+ "@gjsify/node-polyfills": "^0.41.0",
149
+ "@gjsify/npm-registry": "^0.41.0",
150
+ "@gjsify/resolve-npm": "^0.41.0",
151
+ "@gjsify/rolldown-plugin-gjsify": "^0.41.0",
152
+ "@gjsify/vite-plugin-blueprint": "^0.41.0",
153
+ "@gjsify/rolldown-plugin-pnp": "^0.41.0",
154
+ "@gjsify/semver": "^0.41.0",
155
+ "@gjsify/tar": "^0.41.0",
156
+ "@gjsify/tsc": "^0.41.0",
157
+ "@gjsify/web-polyfills": "^0.41.0",
158
+ "@gjsify/workspace": "^0.41.0",
151
159
  "acorn": "^8.17.0",
152
160
  "cosmiconfig": "^9.0.2",
153
161
  "get-tsconfig": "^4.14.0",
@@ -156,13 +164,13 @@
156
164
  "yargs": "^18.0.0"
157
165
  },
158
166
  "devDependencies": {
159
- "@gjsify/unit": "^0.40.0",
167
+ "@gjsify/unit": "^0.41.0",
160
168
  "@types/yargs": "^17.0.35",
161
169
  "typescript": "^6.0.3"
162
170
  },
163
171
  "peerDependencies": {
164
- "@gjsify/oxfmt-native": "^0.40.0",
165
- "@gjsify/rolldown-native": "^0.40.0"
172
+ "@gjsify/oxfmt-native": "^0.41.0",
173
+ "@gjsify/rolldown-native": "^0.41.0"
166
174
  },
167
175
  "peerDependenciesMeta": {
168
176
  "@gjsify/oxfmt-native": {