@gjsify/cli 0.40.0 → 0.42.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.
- package/README.md +79 -33
- package/dist/cli.gjs.mjs +1043 -404
- package/lib/actions/build.d.ts +14 -0
- package/lib/actions/build.js +54 -7
- package/lib/bundler-pick.js +10 -2
- package/lib/cli-app.js +3 -1
- package/lib/commands/browse.js +1 -1
- package/lib/commands/create.d.ts +3 -0
- package/lib/commands/create.js +49 -6
- package/lib/commands/debug.js +1 -1
- package/lib/commands/dev.d.ts +13 -0
- package/lib/commands/dev.js +132 -0
- package/lib/commands/dlx.js +1 -1
- package/lib/commands/index.d.ts +2 -0
- package/lib/commands/index.js +2 -0
- package/lib/commands/install.d.ts +1 -0
- package/lib/commands/install.js +32 -1
- package/lib/commands/prune.d.ts +11 -0
- package/lib/commands/prune.js +84 -0
- package/lib/commands/run.js +38 -85
- package/lib/commands/self-update.d.ts +1 -0
- package/lib/commands/self-update.js +55 -0
- package/lib/commands/ship.js +6 -1
- package/lib/commands/showcase.js +1 -1
- package/lib/commands/storybook.js +17 -74
- package/lib/commands/test.js +12 -16
- package/lib/commands/tsc.js +96 -45
- package/lib/config.js +4 -0
- package/lib/templates/install.mjs.tmpl +8 -3
- package/lib/types/config-data.d.ts +27 -0
- package/lib/utils/app-metadata.js +27 -6
- package/lib/utils/check-system-deps.d.ts +2 -2
- package/lib/utils/check-system-deps.js +92 -13
- package/lib/utils/daemon-command.d.ts +7 -0
- package/lib/utils/daemon-command.js +30 -0
- package/lib/utils/dev-plan.d.ts +52 -0
- package/lib/utils/dev-plan.js +117 -0
- package/lib/utils/gi-runtime-paths.d.ts +27 -0
- package/lib/utils/gi-runtime-paths.js +84 -0
- package/lib/utils/gi-typelib.d.ts +49 -0
- package/lib/utils/gi-typelib.js +160 -0
- package/lib/utils/install-provenance.d.ts +44 -0
- package/lib/utils/install-provenance.js +106 -0
- package/lib/utils/node-script.d.ts +4 -1
- package/lib/utils/node-script.js +27 -3
- package/lib/utils/npm-oidc.js +8 -5
- package/lib/utils/prune-prefix.d.ts +83 -0
- package/lib/utils/prune-prefix.js +333 -0
- package/lib/utils/run-gjs.d.ts +12 -1
- package/lib/utils/run-gjs.js +24 -31
- package/lib/utils/run-node.d.ts +9 -4
- package/lib/utils/run-node.js +34 -41
- package/lib/utils/ship/depends.d.ts +5 -0
- package/lib/utils/ship/depends.js +38 -0
- package/lib/utils/ship/discover.js +106 -1
- package/lib/utils/ship/launcher.js +27 -4
- package/lib/utils/ship/payload.d.ts +35 -0
- package/lib/utils/ship/payload.js +84 -0
- package/lib/utils/ship/plan.js +20 -0
- package/lib/utils/ship/settings.d.ts +8 -1
- package/lib/utils/ship/settings.js +13 -1
- package/lib/utils/ship/types.d.ts +19 -0
- package/lib/utils/simple-command.d.ts +16 -0
- package/lib/utils/simple-command.js +77 -0
- package/lib/utils/spawn.d.ts +7 -1
- package/lib/utils/spawn.js +34 -7
- package/lib/utils/system-gi.d.ts +24 -0
- package/lib/utils/system-gi.js +14 -3
- package/lib/utils/watch-loop.d.ts +91 -0
- package/lib/utils/watch-loop.js +207 -0
- package/package.json +27 -18
- package/lib/templates/app/desktop.tmpl +0 -10
package/lib/utils/npm-oidc.js
CHANGED
|
@@ -80,11 +80,14 @@ export async function fetchGithubOidcToken(audience, log) {
|
|
|
80
80
|
throw new OidcUnavailableError('GitHub Actions OIDC env vars (ACTIONS_ID_TOKEN_REQUEST_{URL,TOKEN}) not set. ' +
|
|
81
81
|
'The calling workflow needs `permissions: id-token: write`.', 'no-env');
|
|
82
82
|
}
|
|
83
|
-
// Append the audience by STRING CONCATENATION, as @actions/core's getIDToken does.
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
83
|
+
// Append the audience by STRING CONCATENATION, as @actions/core's getIDToken does.
|
|
84
|
+
//
|
|
85
|
+
// fixed upstream in gjsify: `URL.searchParams.set` + `.href` used to DROP the parameter under
|
|
86
|
+
// GJS — the mutation never reached the serialisation — so GitHub issued an id-token with the
|
|
87
|
+
// DEFAULT audience (the repo API URL) and npm rejected an otherwise-valid token with 401:
|
|
88
|
+
// repository/workflow_ref/sub all matched the Trusted Publisher, only `aud` was wrong. The
|
|
89
|
+
// URL live view now works, so the ban is lifted; the concatenation stays because it is what
|
|
90
|
+
// the reference implementation does, not because the alternative is broken.
|
|
88
91
|
const sep = url.includes('?') ? '&' : '?';
|
|
89
92
|
const requestUrl = `${url}${sep}audience=${encodeURIComponent(audience)}`;
|
|
90
93
|
log?.(`gjsify oidc: GET ${requestUrl.replace(bearer, '<bearer>')}`);
|
|
@@ -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
|
+
}
|
package/lib/utils/run-gjs.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type NativeEnv } from './detect-native-packages.js';
|
|
2
|
+
import { type SpawnCompletionContract } from './spawn.js';
|
|
2
3
|
/**
|
|
3
4
|
* Pure env computation: the typelib + shared-library search paths {@link runGjsBundle} would
|
|
4
5
|
* inject into the spawned `gjs`, plus the formatted prefix for the `$ …` echo. Which library
|
|
@@ -15,6 +16,16 @@ export declare function computeNativeEnvForBundle(bundlePath: string, cwd?: stri
|
|
|
15
16
|
* bundle's own node_modules tree.
|
|
16
17
|
*/
|
|
17
18
|
export interface RunGjsBundleOptions {
|
|
19
|
+
/**
|
|
20
|
+
* The teardown contract of the command that will end this process — the same field,
|
|
21
|
+
* and the same reason, `spawnToCompletion` demands. Required rather than defaulted:
|
|
22
|
+
* absent, a caller silently inherited the side that ARMS the GJS main loop.
|
|
23
|
+
*
|
|
24
|
+
* Distinct from {@link exitOnSuccess}, which asks WHO performs the exit. `gjsify test`
|
|
25
|
+
* is `'exit'` — its handler exits at the end of `commands/test.ts` — while leaving
|
|
26
|
+
* `exitOnSuccess` off, because an exit HERE would truncate its multi-runtime loop.
|
|
27
|
+
*/
|
|
28
|
+
completion: SpawnCompletionContract;
|
|
18
29
|
/**
|
|
19
30
|
* Exit this process with code 0 once the child succeeds. Under GJS, `ensureMainLoop()`
|
|
20
31
|
* (armed by spawn) keeps the parent's GLib loop alive after the child exits, so a TERMINAL
|
|
@@ -31,4 +42,4 @@ export interface RunGjsBundleOptions {
|
|
|
31
42
|
*/
|
|
32
43
|
quiet?: boolean;
|
|
33
44
|
}
|
|
34
|
-
export declare function runGjsBundle(bundlePath: string, extraArgs
|
|
45
|
+
export declare function runGjsBundle(bundlePath: string, extraArgs: string[] | undefined, options: RunGjsBundleOptions): Promise<void>;
|
package/lib/utils/run-gjs.js
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
//
|
|
10
10
|
// Env composition is split out as the pure `computeNativeEnvForBundle()` so the e2e tests can
|
|
11
11
|
// assert the env without spawning gjs.
|
|
12
|
-
import { spawn } from 'node:child_process';
|
|
13
12
|
import { dirname, resolve } from 'node:path';
|
|
14
13
|
import { detectNativePackages, buildNativeEnv } from './detect-native-packages.js';
|
|
14
|
+
import { describeExit, spawnToCompletion } from './spawn.js';
|
|
15
15
|
/**
|
|
16
16
|
* Pure env computation: the typelib + shared-library search paths {@link runGjsBundle} would
|
|
17
17
|
* inject into the spawned `gjs`, plus the formatted prefix for the `$ …` echo. Which library
|
|
@@ -40,7 +40,7 @@ export function computeNativeEnvForBundle(bundlePath, cwd = process.cwd(), inher
|
|
|
40
40
|
.join(' ');
|
|
41
41
|
return { env, envPrefix };
|
|
42
42
|
}
|
|
43
|
-
export async function runGjsBundle(bundlePath, extraArgs = [], options
|
|
43
|
+
export async function runGjsBundle(bundlePath, extraArgs = [], options) {
|
|
44
44
|
const { env: nativeEnv, envPrefix } = computeNativeEnvForBundle(bundlePath);
|
|
45
45
|
const env = {
|
|
46
46
|
...process.env,
|
|
@@ -52,36 +52,29 @@ export async function runGjsBundle(bundlePath, extraArgs = [], options = {}) {
|
|
|
52
52
|
const gjsCommand = ['gjs', ...gjsArgs.map((a) => (a.includes(' ') ? `"${a}"` : a))].join(' ');
|
|
53
53
|
if (!options.quiet)
|
|
54
54
|
console.error(`$ ${envPrefix ? `${envPrefix} ` : ''}${gjsCommand}`);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}).catch((err) => {
|
|
55
|
+
// The CHILD's own exit code is re-raised verbatim; a spawn error (no `gjs` on PATH) has
|
|
56
|
+
// none of its own and falls back to 1. Collapsing every failure to 1 lost information
|
|
57
|
+
// callers act on — a chain reading `$?` to tell "tool failed" from "tool says no" saw 1
|
|
58
|
+
// for both.
|
|
59
|
+
const failed = (code) => {
|
|
60
|
+
// `process.exitCode` is set BEFORE exiting because `gjsify run <script>` dispatches a
|
|
61
|
+
// nested `gjsify run <bundle>` IN PROCESS: the outer command reads `process.exitCode`
|
|
62
|
+
// to propagate. Without it a failing `test:gjs` returned 0 through the `test` chain,
|
|
63
|
+
// and through `gjsify foreach test` in CI. The `return` guards a fall-through exit.
|
|
64
|
+
process.exitCode = code;
|
|
65
|
+
return process.exit(code);
|
|
66
|
+
};
|
|
67
|
+
let result;
|
|
68
|
+
try {
|
|
69
|
+
result = await spawnToCompletion('gjs', gjsArgs, { completion: options.completion, env });
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
73
72
|
console.error(err.message);
|
|
74
|
-
failed
|
|
75
|
-
}
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// execution returns to the caller, which propagates by reading `process.exitCode`.
|
|
80
|
-
// Without it, `gjsify run <script>` wrapping `gjsify run <bundle>` swallowed a non-zero
|
|
81
|
-
// gjs exit and returned 0 — masking a failing `test:gjs` in the `test` chain, and via
|
|
82
|
-
// `gjsify foreach test` in CI. The `return` guards a fall-through second `exit`.
|
|
83
|
-
process.exitCode = childCode;
|
|
84
|
-
return process.exit(childCode);
|
|
73
|
+
return failed(1);
|
|
74
|
+
}
|
|
75
|
+
if (result.code !== 0) {
|
|
76
|
+
console.error(`gjs exited with ${describeExit(result)}`);
|
|
77
|
+
return failed(result.code ?? 1);
|
|
85
78
|
}
|
|
86
79
|
// See RunGjsBundleOptions.exitOnSuccess — terminal callers opt in,
|
|
87
80
|
// mid-flow callers (test.ts's runtime loop) keep the process alive.
|
package/lib/utils/run-node.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ExampleRuntime } from './runtimes.js';
|
|
2
|
+
import { type SpawnCompletionContract } from './spawn.js';
|
|
2
3
|
/**
|
|
3
4
|
* Walk up from `startDir` for `<node_modules>/@gjsify/node-gi`, returning its package directory
|
|
4
5
|
* or null. A plain fs walk because Node's own resolver is not available under the GJS-bundled CLI.
|
|
@@ -19,6 +20,8 @@ export declare function resolveNodeGiForBundle(bundlePath: string, cwd: string):
|
|
|
19
20
|
/** Pick the node executable: the current one when on Node, else PATH `node`. */
|
|
20
21
|
export declare function nodeBinary(): string;
|
|
21
22
|
export interface RunNodeBundleOptions {
|
|
23
|
+
/** The caller's teardown contract — see `RunGjsBundleOptions.completion`. */
|
|
24
|
+
completion: SpawnCompletionContract;
|
|
22
25
|
/** Exit this process with code 0 once the child succeeds (terminal callers). */
|
|
23
26
|
exitOnSuccess?: boolean;
|
|
24
27
|
/** Suppress the `$ <env> node …` echo — see `RunGjsBundleOptions.quiet`. */
|
|
@@ -28,12 +31,14 @@ export interface RunNodeBundleOptions {
|
|
|
28
31
|
* Run a `--app node` bundle on Node.js with the native typelib env set (same detection as
|
|
29
32
|
* {@link runGjsBundle}). Throws when `@gjsify/node-gi` is not resolvable.
|
|
30
33
|
*/
|
|
31
|
-
export declare function runNodeBundle(bundlePath: string, extraArgs
|
|
34
|
+
export declare function runNodeBundle(bundlePath: string, extraArgs: string[] | undefined, options: RunNodeBundleOptions): Promise<void>;
|
|
32
35
|
/**
|
|
33
36
|
* Whether a `--app node` bundle actually consumes `@gjsify/node-gi` — kept EXTERNAL, so a bundle
|
|
34
37
|
* using gi:// imports the shim by name. A plain Node app does NOT, and must run on node/bun/deno
|
|
35
|
-
* WITHOUT node-gi installed, which is why the check is conditional
|
|
36
|
-
*
|
|
38
|
+
* WITHOUT node-gi installed, which is why the check is conditional rather than a flat "this is a
|
|
39
|
+
* node bundle, demand node-gi". The supervised launcher (`utils/watch-loop.ts`) asks the same
|
|
40
|
+
* question for the same reason: `gjsify dev` on a web-server template builds a node bundle that
|
|
41
|
+
* never reaches a typelib.
|
|
37
42
|
*/
|
|
38
43
|
export declare function bundleRequiresNodeGi(bundlePath: string): boolean;
|
|
39
44
|
/**
|
|
@@ -44,4 +49,4 @@ export declare function bundleRequiresNodeGi(bundlePath: string): boolean;
|
|
|
44
49
|
*
|
|
45
50
|
* `gjs` is intentionally NOT handled here — the caller dispatches it to {@link runGjsBundle}.
|
|
46
51
|
*/
|
|
47
|
-
export declare function runRuntimeBundle(runtime: Exclude<ExampleRuntime, 'gjs'>, bundlePath: string, extraArgs
|
|
52
|
+
export declare function runRuntimeBundle(runtime: Exclude<ExampleRuntime, 'gjs'>, bundlePath: string, extraArgs: string[] | undefined, options: RunNodeBundleOptions): Promise<void>;
|