@gjsify/cli 0.39.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.
- package/dist/cli.gjs.mjs +202 -181
- package/lib/cli-app.js +12 -25
- package/lib/commands/create.d.ts +3 -0
- package/lib/commands/create.js +49 -6
- package/lib/commands/flatpak/check.js +1 -1
- package/lib/commands/flatpak/ci.js +1 -1
- package/lib/commands/flatpak/diff.js +1 -1
- package/lib/commands/flatpak/init.js +1 -1
- package/lib/commands/flatpak/scaffold.d.ts +2 -4
- package/lib/commands/flatpak/scaffold.js +25 -298
- package/lib/commands/flatpak/sync-flathub.js +1 -1
- package/lib/commands/foreach.js +34 -20
- 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/self-update.d.ts +1 -0
- package/lib/commands/self-update.js +55 -0
- package/lib/commands/ship.d.ts +11 -0
- package/lib/commands/ship.js +217 -0
- package/lib/config.d.ts +12 -0
- package/lib/config.js +15 -0
- package/lib/templates/install.mjs.tmpl +8 -3
- package/lib/types/config-data.d.ts +136 -46
- package/lib/utils/app-metadata.d.ts +51 -0
- package/lib/utils/app-metadata.js +338 -0
- package/lib/utils/check-system-deps.d.ts +2 -2
- package/lib/utils/check-system-deps.js +43 -6
- package/lib/utils/cli-runtime-closure.d.ts +23 -0
- package/lib/utils/cli-runtime-closure.js +274 -0
- package/lib/utils/install-provenance.d.ts +44 -0
- package/lib/utils/install-provenance.js +106 -0
- package/lib/utils/prune-prefix.d.ts +83 -0
- package/lib/utils/prune-prefix.js +333 -0
- package/lib/utils/publish-headers.d.ts +13 -0
- package/lib/utils/publish-headers.js +30 -4
- package/lib/utils/ship/ar.d.ts +21 -0
- package/lib/utils/ship/ar.js +57 -0
- package/lib/utils/ship/bytes.d.ts +4 -0
- package/lib/utils/ship/bytes.js +23 -0
- package/lib/utils/ship/cpio.d.ts +22 -0
- package/lib/utils/ship/cpio.js +93 -0
- package/lib/utils/ship/deb.d.ts +24 -0
- package/lib/utils/ship/deb.js +118 -0
- package/lib/utils/ship/depends.d.ts +62 -0
- package/lib/utils/ship/depends.js +221 -0
- package/lib/utils/ship/discover.d.ts +21 -0
- package/lib/utils/ship/discover.js +129 -0
- package/lib/utils/ship/formats.d.ts +5 -0
- package/lib/utils/ship/formats.js +83 -0
- package/lib/utils/ship/gi-namespaces.d.ts +7 -0
- package/lib/utils/ship/gi-namespaces.js +49 -0
- package/lib/utils/ship/gzip.d.ts +2 -0
- package/lib/utils/ship/gzip.js +21 -0
- package/lib/utils/ship/launcher.d.ts +7 -0
- package/lib/utils/ship/launcher.js +37 -0
- package/lib/utils/ship/payload.d.ts +30 -0
- package/lib/utils/ship/payload.js +59 -0
- package/lib/utils/ship/plan.d.ts +34 -0
- package/lib/utils/ship/plan.js +167 -0
- package/lib/utils/ship/rpm-header.d.ts +29 -0
- package/lib/utils/ship/rpm-header.js +170 -0
- package/lib/utils/ship/rpm.d.ts +12 -0
- package/lib/utils/ship/rpm.js +377 -0
- package/lib/utils/ship/scripts.d.ts +31 -0
- package/lib/utils/ship/scripts.js +76 -0
- package/lib/utils/ship/settings.d.ts +60 -0
- package/lib/utils/ship/settings.js +184 -0
- package/lib/utils/ship/stage-writer.d.ts +17 -0
- package/lib/utils/ship/stage-writer.js +49 -0
- package/lib/utils/ship/types.d.ts +114 -0
- package/lib/utils/ship/types.js +10 -0
- package/lib/utils/ship/version.d.ts +17 -0
- package/lib/utils/ship/version.js +55 -0
- package/package.json +27 -18
- /package/lib/templates/{flatpak → app}/desktop.tmpl +0 -0
|
@@ -18,6 +18,7 @@ import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
|
18
18
|
// The zero-dependency subpath, deliberately: importing the plugin root would
|
|
19
19
|
// pull vite/execa/minify-xml into a system CHECK.
|
|
20
20
|
import { resolveBlueprintCompiler } from '@gjsify/vite-plugin-blueprint/resolve';
|
|
21
|
+
import { isNode } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
21
22
|
/** Run a binary and return its stdout trimmed, or null if it fails. */
|
|
22
23
|
function tryExecFile(binary, args) {
|
|
23
24
|
try {
|
|
@@ -378,13 +379,41 @@ export function runAllChecks(cwd) {
|
|
|
378
379
|
];
|
|
379
380
|
}
|
|
380
381
|
/**
|
|
381
|
-
*
|
|
382
|
-
*
|
|
382
|
+
* Is a Node.js runtime available — asked of the host rather than asserted.
|
|
383
|
+
*
|
|
384
|
+
* This row used to be the literal `{found: true, version: process.version}`, which is
|
|
385
|
+
* a lie on three of the four hosts: `@gjsify/process` stubs `process.version` to
|
|
386
|
+
* `"v20.0.0"` for npm compatibility (Bun and Deno fake `process.versions.node` for the
|
|
387
|
+
* same reason), so `gjsify system-check` under GJS reported `✓ Node.js (v20.0.0)` on a
|
|
388
|
+
* machine with no Node on it — under the NODE-FREE toolchain, whose entire premise is
|
|
389
|
+
* that there is none. `cli-app.ts`'s `runtimeLabel()` already had to know better; one
|
|
390
|
+
* question gets one answer, so this asks the same module.
|
|
391
|
+
*
|
|
392
|
+
* Under Node the running process IS the answer and its version cannot be faked from
|
|
393
|
+
* inside; elsewhere it is a PATH question whose answer may legitimately be "no".
|
|
394
|
+
*
|
|
395
|
+
* `isNode()` rather than `hostRuntime() === 'node'`: the latter RETURNS `'node'` as its
|
|
396
|
+
* fallback for an unrecognised host, which would put an exotic runtime's possibly-stubbed
|
|
397
|
+
* `process.version` on the trusted branch. `isNode()` demands positive evidence.
|
|
398
|
+
*
|
|
399
|
+
* `optional`, never `required`: `gjsify showcase` and `gjsify install` both treat a
|
|
400
|
+
* missing `required` row as fatal, and a GJS showcase needs no Node. The old row could
|
|
401
|
+
* not have failed either gate — it was constant — so nothing that used to pass now
|
|
402
|
+
* fails; what changes is only that the printed line is true.
|
|
403
|
+
*/
|
|
404
|
+
function checkNodeRuntime() {
|
|
405
|
+
if (isNode()) {
|
|
406
|
+
return { id: 'nodejs', name: 'Node.js', found: true, version: process.version, severity: 'optional' };
|
|
407
|
+
}
|
|
408
|
+
return checkBinary('nodejs', 'Node.js', 'node', ['--version'], 'optional');
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Minimal checks needed to run any GJS example (the GJS binary; Node is reported but
|
|
412
|
+
* not required). Used by `gjsify showcase` for examples that have no native deps.
|
|
383
413
|
*/
|
|
384
414
|
export function runMinimalChecks() {
|
|
385
415
|
const results = [];
|
|
386
|
-
|
|
387
|
-
results.push({ id: 'nodejs', name: 'Node.js', found: true, version: process.version, severity: 'required' });
|
|
416
|
+
results.push(checkNodeRuntime());
|
|
388
417
|
// GJS
|
|
389
418
|
results.push(checkBinary('gjs', 'GJS', 'gjs', ['--version'], 'required', (out) => out.replace(/^GJS\s+/i, '').split('\n')[0] ?? out));
|
|
390
419
|
// The one win32 system dependency that is real and was undiagnosed (#997).
|
|
@@ -528,6 +557,7 @@ function runOptionalChecks(needed, cwd) {
|
|
|
528
557
|
// Entries with multiple space-separated packages are intentional (one dep needs multiple system pkgs).
|
|
529
558
|
const PM_PACKAGES = {
|
|
530
559
|
apt: {
|
|
560
|
+
nodejs: 'nodejs',
|
|
531
561
|
gjs: 'gjs',
|
|
532
562
|
'blueprint-compiler': 'blueprint-compiler',
|
|
533
563
|
'pkg-config': 'pkg-config',
|
|
@@ -553,6 +583,7 @@ const PM_PACKAGES = {
|
|
|
553
583
|
epoxy: 'libepoxy-dev',
|
|
554
584
|
},
|
|
555
585
|
dnf: {
|
|
586
|
+
nodejs: 'nodejs',
|
|
556
587
|
gjs: 'gjs',
|
|
557
588
|
'blueprint-compiler': 'blueprint-compiler',
|
|
558
589
|
'pkg-config': 'pkgconf-pkg-config',
|
|
@@ -578,6 +609,7 @@ const PM_PACKAGES = {
|
|
|
578
609
|
epoxy: 'libepoxy-devel',
|
|
579
610
|
},
|
|
580
611
|
pacman: {
|
|
612
|
+
nodejs: 'nodejs',
|
|
581
613
|
gjs: 'gjs',
|
|
582
614
|
'blueprint-compiler': 'blueprint-compiler',
|
|
583
615
|
'pkg-config': 'pkgconf',
|
|
@@ -603,6 +635,7 @@ const PM_PACKAGES = {
|
|
|
603
635
|
epoxy: 'libepoxy',
|
|
604
636
|
},
|
|
605
637
|
zypper: {
|
|
638
|
+
nodejs: 'nodejs',
|
|
606
639
|
gjs: 'gjs',
|
|
607
640
|
'blueprint-compiler': 'blueprint-compiler',
|
|
608
641
|
'pkg-config': 'pkg-config',
|
|
@@ -628,6 +661,7 @@ const PM_PACKAGES = {
|
|
|
628
661
|
epoxy: 'libepoxy-devel',
|
|
629
662
|
},
|
|
630
663
|
apk: {
|
|
664
|
+
nodejs: 'nodejs',
|
|
631
665
|
gjs: 'gjs',
|
|
632
666
|
'blueprint-compiler': 'blueprint-compiler',
|
|
633
667
|
'pkg-config': 'pkgconf',
|
|
@@ -675,6 +709,7 @@ const PM_PACKAGES = {
|
|
|
675
709
|
// Recorded here so the next reader does not "fix" the gap by guessing a name;
|
|
676
710
|
// both are genuine platform gaps, not table omissions.
|
|
677
711
|
brew: {
|
|
712
|
+
nodejs: 'node',
|
|
678
713
|
gjs: 'gjs',
|
|
679
714
|
'blueprint-compiler': 'blueprint-compiler',
|
|
680
715
|
'pkg-config': 'pkgconf',
|
|
@@ -750,8 +785,10 @@ export function buildInstallCommand(pm, missing) {
|
|
|
750
785
|
'mingw-w64-ucrt-x86_64-libadwaita # inside MSYS2; found automatically afterwards');
|
|
751
786
|
continue;
|
|
752
787
|
}
|
|
753
|
-
|
|
754
|
-
|
|
788
|
+
// No `continue` for `nodejs`. It USED to be unmissable — the row was the
|
|
789
|
+
// literal `found: true` — and since it became a real probe it can report
|
|
790
|
+
// absent under GJS, so skipping it here printed "Missing optional: Node.js"
|
|
791
|
+
// above an install command that did not install it.
|
|
755
792
|
const pkg = pkgMap[dep.id];
|
|
756
793
|
if (pkg)
|
|
757
794
|
pkgs.push(pkg);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Workspace } from '@gjsify/workspace';
|
|
2
|
+
export declare function moduleSpecifiers(code: string): string[];
|
|
3
|
+
export interface CliRuntimeClosureOptions {
|
|
4
|
+
/**
|
|
5
|
+
* File to start the walk from. Defaults to the running CLI's Node entry
|
|
6
|
+
* (`bin.gjsify`), which is what a spawned child loads.
|
|
7
|
+
*/
|
|
8
|
+
entry?: string | null;
|
|
9
|
+
/** The CLI's own manifest, for its optional peers. Read from disk by default. */
|
|
10
|
+
cliManifest?: {
|
|
11
|
+
peerDependencies?: Record<string, string>;
|
|
12
|
+
} | null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Workspace packages the running CLI imports, so a parallel sweep can build them
|
|
16
|
+
* first instead of underneath itself.
|
|
17
|
+
*
|
|
18
|
+
* Empty is the correct answer in three real situations and none of them is an
|
|
19
|
+
* error: a consumer project (its `@gjsify/*` deps are not workspaces, so nothing
|
|
20
|
+
* the sweep builds can tear), a relocated bundle with no reachable manifest, and
|
|
21
|
+
* a CLI whose entry has not been built.
|
|
22
|
+
*/
|
|
23
|
+
export declare function cliRuntimeClosure(workspaces: readonly Workspace[], options?: CliRuntimeClosureOptions): Set<string>;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// Which workspace packages does the RUNNING CLI import at runtime?
|
|
2
|
+
//
|
|
3
|
+
// WHY THE SCHEDULER NEEDS THIS
|
|
4
|
+
//
|
|
5
|
+
// `gjsify foreach <script> -p` spawns children, and every child is another
|
|
6
|
+
// `gjsify` process: it BOOTS the CLI before it runs anything. So while a parallel
|
|
7
|
+
// sweep rebuilds the workspace, each child is reading the very `lib/` trees its
|
|
8
|
+
// siblings are writing. Measured on the macOS leg (run 31130155911,
|
|
9
|
+
// darwin-arm64), on the first full build that leg ever reached:
|
|
10
|
+
//
|
|
11
|
+
// [gjsify foreach] start @gjsify/native-platform (49/160 done, 3 in flight)
|
|
12
|
+
// [gjsify foreach] start @gjsify/npm-registry (50/160 done, 3 in flight)
|
|
13
|
+
// [@gjsify/native-platform] ERR_MODULE_NOT_FOUND
|
|
14
|
+
// .../npm-registry/lib/esm/_virtual/_rolldown/runtime.js
|
|
15
|
+
// imported from .../npm-registry/lib/esm/auth.js
|
|
16
|
+
//
|
|
17
|
+
// `auth.js` was on disk and its rolldown chunk was not — a torn write, not a
|
|
18
|
+
// corrupt build. `foreach` uses this set to build those packages BEFORE the
|
|
19
|
+
// parallel sweep starts, so nothing rewrites them while a child boots.
|
|
20
|
+
//
|
|
21
|
+
// WHY IT IS DERIVED AND NOT LISTED
|
|
22
|
+
//
|
|
23
|
+
// The four names a root script once carried by hand (`semver`, `npm-registry`,
|
|
24
|
+
// `tar`, `workspace`) were never the whole answer, and a hand-written list is the
|
|
25
|
+
// same defect one level up: it is right until an import changes. Two measured
|
|
26
|
+
// alternatives were rejected before this one:
|
|
27
|
+
//
|
|
28
|
+
// * the MANIFEST closure of `@gjsify/cli` (transitive workspace `dependencies`)
|
|
29
|
+
// selects 110 of the ~160 packages a full sweep builds — it counts the
|
|
30
|
+
// umbrella polyfill metas the CLI declares for the alias layer and never
|
|
31
|
+
// imports under Node. Serialising 110 packages is not a fix, it is the end of
|
|
32
|
+
// parallelism;
|
|
33
|
+
// * a regex over the sources reports 27, because `commands/storybook.ts` emits
|
|
34
|
+
// GENERATED CODE containing `import { … } from '@gjsify/storybook';` inside a
|
|
35
|
+
// template literal. A text scan cannot tell a program from a string, and that
|
|
36
|
+
// one string drags the whole GTK storybook subtree into the prefix.
|
|
37
|
+
//
|
|
38
|
+
// So the graph is PARSED, from the entry the children actually boot, and the
|
|
39
|
+
// answer was checked against ground truth: a Node loader hook recording every
|
|
40
|
+
// `file:` URL `gjsify --version` loads reports exactly the ten packages this walk
|
|
41
|
+
// returns (see `cli-runtime-closure.spec.ts` for the shape the walk must keep).
|
|
42
|
+
//
|
|
43
|
+
// THE ONE THING A PARSE CANNOT SEE
|
|
44
|
+
//
|
|
45
|
+
// A COMPUTED dynamic import. `bundler-pick.ts` loads the bundler engine as
|
|
46
|
+
// `await import(target)` where `target` is a resolved file URL, and
|
|
47
|
+
// `rolldown-plugin-gjsify`'s `css-as-string.ts` does the same for lightningcss —
|
|
48
|
+
// deliberately, so tsc and rolldown do not resolve an optional peer at build
|
|
49
|
+
// time. Those are invisible to any static walk, and they are invisible to npm for
|
|
50
|
+
// the same reason, which is exactly why the convention is to declare them as
|
|
51
|
+
// optional PEER dependencies. So the peers of every package in the graph are
|
|
52
|
+
// added: one declaration closes both blind spots, and it stays a declaration the
|
|
53
|
+
// package owns rather than a list this file keeps.
|
|
54
|
+
import * as acorn from 'acorn';
|
|
55
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
56
|
+
import { dirname, join, resolve as resolvePath } from 'node:path';
|
|
57
|
+
import { cliPackageDir } from './publish-headers.js';
|
|
58
|
+
/**
|
|
59
|
+
* Split a bare specifier into a WORKSPACE package name + its `exports` key, or
|
|
60
|
+
* `null` when it names no workspace.
|
|
61
|
+
*
|
|
62
|
+
* Matched against the workspace index rather than against the `@gjsify/` scope: a
|
|
63
|
+
* scope literal would be a second place that has to be edited when the project is
|
|
64
|
+
* vendored, forked or renamed, and the question here is only ever "is this one of
|
|
65
|
+
* OUR packages". Scoped names take two segments, unscoped one.
|
|
66
|
+
*/
|
|
67
|
+
function splitWorkspaceSpecifier(specifier, byName) {
|
|
68
|
+
if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.includes(':'))
|
|
69
|
+
return null;
|
|
70
|
+
const parts = specifier.split('/');
|
|
71
|
+
const candidates = specifier.startsWith('@') ? [2] : [1];
|
|
72
|
+
for (const depth of candidates) {
|
|
73
|
+
const name = parts.slice(0, depth).join('/');
|
|
74
|
+
if (!byName.has(name))
|
|
75
|
+
continue;
|
|
76
|
+
const rest = parts.slice(depth).join('/');
|
|
77
|
+
return { name, subpath: rest ? `./${rest}` : '.' };
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The string a module specifier node names, or `null` when it is computed.
|
|
83
|
+
*
|
|
84
|
+
* A substitution-free TEMPLATE LITERAL is as static as a quoted string, and
|
|
85
|
+
* skipping it is not hypothetical: the minifier rewrites `await
|
|
86
|
+
* import('gi://GLib?version=2.0')` to a backtick form, so a Literal-only reader
|
|
87
|
+
* silently loses every dynamic import in a built bundle.
|
|
88
|
+
*/
|
|
89
|
+
function literalSpecifier(node) {
|
|
90
|
+
if (node?.type === 'Literal')
|
|
91
|
+
return typeof node.value === 'string' ? node.value : null;
|
|
92
|
+
if (node?.type === 'TemplateLiteral' && node.expressions?.length === 0) {
|
|
93
|
+
const cooked = node.quasis?.[0]?.value?.cooked;
|
|
94
|
+
return typeof cooked === 'string' ? cooked : null;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
export function moduleSpecifiers(code) {
|
|
99
|
+
let ast;
|
|
100
|
+
try {
|
|
101
|
+
ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module', allowHashBang: true });
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
const out = [];
|
|
107
|
+
const visit = (node) => {
|
|
108
|
+
if (!node || typeof node !== 'object')
|
|
109
|
+
return;
|
|
110
|
+
if (Array.isArray(node)) {
|
|
111
|
+
for (const child of node)
|
|
112
|
+
visit(child);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const n = node;
|
|
116
|
+
if (n.type === 'ImportDeclaration' ||
|
|
117
|
+
n.type === 'ExportNamedDeclaration' ||
|
|
118
|
+
n.type === 'ExportAllDeclaration' ||
|
|
119
|
+
n.type === 'ImportExpression') {
|
|
120
|
+
const specifier = literalSpecifier(n.source);
|
|
121
|
+
if (specifier !== null)
|
|
122
|
+
out.push(specifier);
|
|
123
|
+
}
|
|
124
|
+
for (const [key, value] of Object.entries(n)) {
|
|
125
|
+
if (key === 'type' || key === 'start' || key === 'end' || key === 'loc')
|
|
126
|
+
continue;
|
|
127
|
+
visit(value);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
visit(ast);
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
/** The file a relative specifier names, probing the extensionless spellings Node accepts. */
|
|
134
|
+
function resolveRelative(fromFile, specifier) {
|
|
135
|
+
const base = resolvePath(dirname(fromFile), specifier);
|
|
136
|
+
for (const candidate of [base, `${base}.js`, `${base}.mjs`, join(base, 'index.js')]) {
|
|
137
|
+
try {
|
|
138
|
+
if (statSync(candidate).isFile())
|
|
139
|
+
return candidate;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
/* not this spelling */
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The file a workspace package's `<subpath>` resolves to, read from its own
|
|
149
|
+
* manifest. `subpath` is `'.'` for a bare package specifier.
|
|
150
|
+
*
|
|
151
|
+
* The `exports` conditions are walked in the order the CHILD's runtime resolves
|
|
152
|
+
* them; `main`/`module` is the pre-`exports` fallback. Deliberately NOT
|
|
153
|
+
* `import.meta.resolve`: that answers only for the runtime doing the asking, and
|
|
154
|
+
* this walk must give the same answer from Node and from GJS, from a spec with a
|
|
155
|
+
* fixture tree as from a real install. Resolving the SUBPATH rather than the
|
|
156
|
+
* package root matters — `@gjsify/rolldown-plugin-gjsify/runtime` is a leaf, and
|
|
157
|
+
* substituting the root barrel for it would drag in everything the barrel
|
|
158
|
+
* re-exports and nothing imports.
|
|
159
|
+
*/
|
|
160
|
+
function workspaceEntry(ws, subpath) {
|
|
161
|
+
const manifest = ws.manifest;
|
|
162
|
+
const pick = (node) => {
|
|
163
|
+
if (typeof node === 'string')
|
|
164
|
+
return node;
|
|
165
|
+
if (!node || typeof node !== 'object' || Array.isArray(node))
|
|
166
|
+
return null;
|
|
167
|
+
const record = node;
|
|
168
|
+
for (const condition of ['node', 'import', 'default', 'gjs']) {
|
|
169
|
+
if (condition in record) {
|
|
170
|
+
const picked = pick(record[condition]);
|
|
171
|
+
if (picked)
|
|
172
|
+
return picked;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
};
|
|
177
|
+
let target = null;
|
|
178
|
+
if (typeof manifest.exports === 'string')
|
|
179
|
+
target = subpath === '.' ? manifest.exports : null;
|
|
180
|
+
else if (manifest.exports && typeof manifest.exports === 'object') {
|
|
181
|
+
target = pick(manifest.exports[subpath]);
|
|
182
|
+
}
|
|
183
|
+
if (!target && subpath === '.') {
|
|
184
|
+
target = typeof manifest.module === 'string' ? manifest.module : null;
|
|
185
|
+
target ??= typeof manifest.main === 'string' ? manifest.main : null;
|
|
186
|
+
}
|
|
187
|
+
if (!target)
|
|
188
|
+
return null;
|
|
189
|
+
const file = resolvePath(ws.location, target);
|
|
190
|
+
return existsSync(file) ? file : null;
|
|
191
|
+
}
|
|
192
|
+
/** `peerDependencies` of `manifest` that name a workspace package. */
|
|
193
|
+
function workspacePeers(manifest, byName) {
|
|
194
|
+
return Object.keys(manifest.peerDependencies ?? {}).filter((name) => byName.has(name));
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Workspace packages the running CLI imports, so a parallel sweep can build them
|
|
198
|
+
* first instead of underneath itself.
|
|
199
|
+
*
|
|
200
|
+
* Empty is the correct answer in three real situations and none of them is an
|
|
201
|
+
* error: a consumer project (its `@gjsify/*` deps are not workspaces, so nothing
|
|
202
|
+
* the sweep builds can tear), a relocated bundle with no reachable manifest, and
|
|
203
|
+
* a CLI whose entry has not been built.
|
|
204
|
+
*/
|
|
205
|
+
export function cliRuntimeClosure(workspaces, options = {}) {
|
|
206
|
+
const byName = new Map(workspaces.map((ws) => [ws.name, ws]));
|
|
207
|
+
const found = new Set();
|
|
208
|
+
let entry = options.entry;
|
|
209
|
+
let cliManifest = options.cliManifest;
|
|
210
|
+
if (entry === undefined || cliManifest === undefined) {
|
|
211
|
+
const packageDir = cliPackageDir();
|
|
212
|
+
let manifest = null;
|
|
213
|
+
if (packageDir) {
|
|
214
|
+
try {
|
|
215
|
+
manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'));
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
/* unreadable — the closure degrades to empty, which is a no-op prefix */
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
cliManifest ??= manifest;
|
|
222
|
+
if (entry === undefined) {
|
|
223
|
+
// The npm `bin` entry, NOT `gjsify.bin`: the GJS bundle is self-contained,
|
|
224
|
+
// so walking it would report nothing, while a GJS child still loads the
|
|
225
|
+
// engine facades through the peers below. The `lib/` graph is a superset
|
|
226
|
+
// of what either entry reaches, and one code path beats two.
|
|
227
|
+
const bin = manifest?.bin;
|
|
228
|
+
const relative = typeof bin === 'string' ? bin : (bin?.['gjsify'] ?? manifest?.main);
|
|
229
|
+
entry = packageDir && relative ? resolvePath(packageDir, relative) : null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
for (const peer of workspacePeers(cliManifest ?? {}, byName))
|
|
233
|
+
found.add(peer);
|
|
234
|
+
if (!entry || !existsSync(entry))
|
|
235
|
+
return found;
|
|
236
|
+
const seen = new Set();
|
|
237
|
+
const queue = [entry];
|
|
238
|
+
while (queue.length > 0) {
|
|
239
|
+
const file = queue.shift();
|
|
240
|
+
if (seen.has(file))
|
|
241
|
+
continue;
|
|
242
|
+
seen.add(file);
|
|
243
|
+
let code;
|
|
244
|
+
try {
|
|
245
|
+
code = readFileSync(file, 'utf8');
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
for (const specifier of moduleSpecifiers(code)) {
|
|
251
|
+
if (specifier.startsWith('.')) {
|
|
252
|
+
const next = resolveRelative(file, specifier);
|
|
253
|
+
if (next)
|
|
254
|
+
queue.push(next);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const split = splitWorkspaceSpecifier(specifier, byName);
|
|
258
|
+
const ws = split ? byName.get(split.name) : undefined;
|
|
259
|
+
if (!split || !ws)
|
|
260
|
+
continue;
|
|
261
|
+
found.add(split.name);
|
|
262
|
+
for (const peer of workspacePeers(ws.manifest, byName))
|
|
263
|
+
found.add(peer);
|
|
264
|
+
// Queued per SUBPATH, not per package: `seen` dedupes files, so a
|
|
265
|
+
// package imported through two subpaths is walked through both. A
|
|
266
|
+
// package-level visited set would stop at whichever subpath came
|
|
267
|
+
// first and miss what the other one reaches.
|
|
268
|
+
const next = workspaceEntry(ws, split.subpath);
|
|
269
|
+
if (next)
|
|
270
|
+
queue.push(next);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return found;
|
|
274
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Where the running CLI came from. */
|
|
2
|
+
export type InstallProvenanceKind =
|
|
3
|
+
/** `install.mjs` or `gjsify install -g` — the layout `self-update` manages. */
|
|
4
|
+
'xdg-global'
|
|
5
|
+
/** Inside a Flatpak sandbox. */
|
|
6
|
+
| 'flatpak'
|
|
7
|
+
/** A distro package (`.deb`/`.rpm`) staged under a system prefix. */
|
|
8
|
+
| 'system-package'
|
|
9
|
+
/** `npm install -g` (or yarn/pnpm global) — a `node_modules` tree elsewhere. */
|
|
10
|
+
| 'npm-global'
|
|
11
|
+
/** A checkout, a container image, an unpacked tarball — anything else. */
|
|
12
|
+
| 'unknown';
|
|
13
|
+
export interface InstallProvenance {
|
|
14
|
+
kind: InstallProvenanceKind;
|
|
15
|
+
/** What the decision was made ON, so a wrong verdict is debuggable. */
|
|
16
|
+
evidence: string;
|
|
17
|
+
/** How the user should update, when it is not `self-update`'s job. */
|
|
18
|
+
updateWith?: string;
|
|
19
|
+
/** True when `self-update` must refuse rather than write the XDG prefix. */
|
|
20
|
+
managedElsewhere: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface ProvenanceInput {
|
|
23
|
+
/** Absolute path of the running CLI's own package directory. */
|
|
24
|
+
selfDir: string;
|
|
25
|
+
/** `defaultGlobalLayout().prefix` — the layout `self-update` owns. */
|
|
26
|
+
xdgPrefix: string;
|
|
27
|
+
env?: Record<string, string | undefined>;
|
|
28
|
+
platform?: NodeJS.Platform;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Classify an installation from its path and environment.
|
|
32
|
+
*
|
|
33
|
+
* ORDER MATTERS and is not alphabetical:
|
|
34
|
+
*
|
|
35
|
+
* 1. **Flatpak first.** Its `/app` prefix would also satisfy no other test, but
|
|
36
|
+
* `FLATPAK_ID` is the unambiguous signal and is present even when the tree is
|
|
37
|
+
* mounted somewhere unusual.
|
|
38
|
+
* 2. **XDG before system.** The XDG prefix is under `$HOME`, so it can never be
|
|
39
|
+
* `/usr` — but a user whose `$HOME` is `/opt/someuser` exists, and asking the
|
|
40
|
+
* narrow question first means that user is not told to run `dnf`.
|
|
41
|
+
* 3. **System before npm.** A `.deb` stages a `node_modules`-shaped tree under
|
|
42
|
+
* `/usr/lib`, so the npm test alone would claim it.
|
|
43
|
+
*/
|
|
44
|
+
export declare function classifyInstall(input: ProvenanceInput): InstallProvenance;
|
|
@@ -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;
|