@gjsify/cli 0.52.0 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.gjs.mjs +351 -305
- package/lib/actions/build.d.ts +22 -0
- package/lib/actions/build.js +37 -9
- package/lib/bundler-pick.d.ts +72 -1
- package/lib/bundler-pick.js +175 -17
- package/lib/cli-app.js +7 -2
- package/lib/commands/blueprint/index.d.ts +4 -0
- package/lib/commands/blueprint/index.js +15 -0
- package/lib/commands/blueprint/types.d.ts +8 -0
- package/lib/commands/blueprint/types.js +124 -0
- package/lib/commands/devtools.d.ts +21 -0
- package/lib/commands/devtools.js +146 -0
- package/lib/commands/env.d.ts +31 -0
- package/lib/commands/env.js +114 -0
- package/lib/commands/exec.d.ts +10 -0
- package/lib/commands/exec.js +208 -0
- package/lib/commands/foreach.js +5 -97
- package/lib/commands/format.js +26 -6
- package/lib/commands/index.d.ts +5 -0
- package/lib/commands/index.js +5 -0
- package/lib/commands/install.d.ts +2 -2
- package/lib/commands/install.js +5 -5
- package/lib/commands/self-update.js +2 -1
- package/lib/commands/ship.js +51 -8
- package/lib/commands/test.d.ts +18 -9
- package/lib/commands/test.js +56 -11
- package/lib/commands/tsc.js +4 -4
- package/lib/commands/upgrade.d.ts +15 -0
- package/lib/commands/upgrade.js +76 -2
- package/lib/commands/webext/index.d.ts +2 -0
- package/lib/commands/webext/index.js +15 -0
- package/lib/commands/webext/subcommands.d.ts +16 -0
- package/lib/commands/webext/subcommands.js +204 -0
- package/lib/types/config-data.d.ts +1 -1
- package/lib/utils/bin-shim.d.ts +8 -2
- package/lib/utils/bin-shim.js +6 -5
- package/lib/utils/bundle-inputs.d.ts +84 -0
- package/lib/utils/bundle-inputs.js +268 -0
- package/lib/utils/check-system-deps.d.ts +4 -2
- package/lib/utils/check-system-deps.js +21 -26
- package/lib/utils/dev-link.d.ts +9 -6
- package/lib/utils/dev-link.js +71 -19
- package/lib/utils/exec-bin.d.ts +137 -0
- package/lib/utils/exec-bin.js +321 -0
- package/lib/utils/gi-runtime-paths.js +3 -2
- package/lib/utils/gitignore.d.ts +15 -0
- package/lib/utils/gitignore.js +178 -0
- package/lib/utils/gjsify-shim.d.ts +47 -0
- package/lib/utils/gjsify-shim.js +97 -8
- package/lib/utils/install-backend-native.d.ts +19 -1
- package/lib/utils/install-backend-native.js +93 -7
- package/lib/utils/install-lock.js +4 -25
- package/lib/utils/native-skip-scan.d.ts +20 -0
- package/lib/utils/native-skip-scan.js +343 -0
- package/lib/utils/node-script.js +1 -1
- package/lib/utils/oxc-resolve.js +46 -5
- package/lib/utils/process-table.d.ts +22 -0
- package/lib/utils/process-table.js +188 -0
- package/lib/utils/prune-prefix.js +1 -1
- package/lib/utils/run-gjs.d.ts +15 -1
- package/lib/utils/run-gjs.js +16 -7
- package/lib/utils/run-node.d.ts +2 -0
- package/lib/utils/run-node.js +2 -1
- package/lib/utils/runtimes.js +14 -13
- package/lib/utils/ship/app-runtime.d.ts +7 -5
- package/lib/utils/ship/app-runtime.js +11 -8
- package/lib/utils/ship/appimage.d.ts +9 -0
- package/lib/utils/ship/appimage.js +2 -1
- package/lib/utils/ship/deb.d.ts +10 -0
- package/lib/utils/ship/deb.js +7 -0
- package/lib/utils/ship/depends.d.ts +37 -3
- package/lib/utils/ship/depends.js +21 -9
- package/lib/utils/ship/gi-namespaces.d.ts +16 -0
- package/lib/utils/ship/gi-namespaces.js +64 -1
- package/lib/utils/ship/gzip.js +1 -1
- package/lib/utils/ship/icons.d.ts +6 -0
- package/lib/utils/ship/icons.js +6 -5
- package/lib/utils/ship/launcher.js +2 -2
- package/lib/utils/ship/layout.js +25 -2
- package/lib/utils/ship/rpm.d.ts +5 -0
- package/lib/utils/ship/rpm.js +42 -0
- package/lib/utils/ship/settings.js +6 -0
- package/lib/utils/ship/signing.d.ts +7 -2
- package/lib/utils/ship/signing.js +59 -32
- package/lib/utils/ship/stage-manifest.d.ts +14 -0
- package/lib/utils/ship/stage-manifest.js +7 -0
- package/lib/utils/spawn.d.ts +13 -0
- package/lib/utils/spawn.js +10 -5
- package/lib/utils/watch-loop.d.ts +15 -1
- package/lib/utils/watch-loop.js +9 -5
- package/lib/utils/webext/build.d.ts +42 -0
- package/lib/utils/webext/build.js +120 -0
- package/lib/utils/webext/config.d.ts +72 -0
- package/lib/utils/webext/config.js +234 -0
- package/lib/utils/webext/icons.d.ts +24 -0
- package/lib/utils/webext/icons.js +83 -0
- package/lib/utils/webext/launch.d.ts +32 -0
- package/lib/utils/webext/launch.js +77 -0
- package/lib/utils/webext/manifest.d.ts +53 -0
- package/lib/utils/webext/manifest.js +219 -0
- package/lib/utils/webext/pages.d.ts +26 -0
- package/lib/utils/webext/pages.js +61 -0
- package/lib/utils/webext/targets.d.ts +13 -0
- package/lib/utils/webext/targets.js +48 -0
- package/lib/utils/win32-command.d.ts +17 -0
- package/lib/utils/win32-command.js +21 -6
- package/package.json +20 -19
package/lib/commands/foreach.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// matching yarn's interactive flow. Exit code is non-zero if any child
|
|
10
10
|
// process failed; first failure's stderr is forwarded.
|
|
11
11
|
import { spawnToCompletion } from '../utils/spawn.js';
|
|
12
|
-
import {
|
|
12
|
+
import { collectDescendants } from '../utils/process-table.js';
|
|
13
13
|
import { cpus } from 'node:os';
|
|
14
14
|
import { affectedClosure, buildDependencyGraph, discoverWorkspaces, filterWorkspaces, topologicalSort, } from '@gjsify/workspace';
|
|
15
15
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
@@ -34,101 +34,6 @@ const KILL_GRACE_MS = 5_000;
|
|
|
34
34
|
// killed child whose grandchildren inherit (and keep open) the stdio pipe
|
|
35
35
|
// never emits 'close', so we must not wait on it unboundedly.
|
|
36
36
|
const DRAIN_DEADLINE_MS = 15_000;
|
|
37
|
-
// Read the direct child PIDs of `pid` from /proc/<pid>/task/*/children
|
|
38
|
-
// (Linux, CONFIG_PROC_CHILDREN — standard on every mainstream kernel). When
|
|
39
|
-
// the per-task children files are unavailable, fall back to one full
|
|
40
|
-
// /proc/*/stat scan building a ppid→children map. Both paths use only
|
|
41
|
-
// `node:fs` reads, so they behave identically under Node and GJS
|
|
42
|
-
// (@gjsify/fs) — unlike a process-group kill, which GJS cannot issue
|
|
43
|
-
// (`@gjsify/process.kill` shells out `kill <sig> <pid>` where a negative
|
|
44
|
-
// PID parses as an option, and Gio.Subprocess has no group-signal API).
|
|
45
|
-
function readDirectChildren(pid) {
|
|
46
|
-
let taskIds;
|
|
47
|
-
try {
|
|
48
|
-
taskIds = readdirSync(`/proc/${pid}/task`);
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
// Process already gone (common) or /proc unavailable.
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
const kids = [];
|
|
55
|
-
let readAny = false;
|
|
56
|
-
for (const tid of taskIds) {
|
|
57
|
-
try {
|
|
58
|
-
const data = readFileSync(`/proc/${pid}/task/${tid}/children`, 'utf-8');
|
|
59
|
-
readAny = true;
|
|
60
|
-
for (const tok of data.trim().split(/\s+/)) {
|
|
61
|
-
const n = Number(tok);
|
|
62
|
-
if (Number.isInteger(n) && n > 0)
|
|
63
|
-
kids.push(n);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
// Thread vanished mid-walk, or kernel lacks CONFIG_PROC_CHILDREN.
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return readAny ? kids : null;
|
|
71
|
-
}
|
|
72
|
-
// Fallback: scan every /proc/<pid>/stat once and build ppid → children.
|
|
73
|
-
// O(#processes) small reads — fine for the rare kill path.
|
|
74
|
-
function buildPpidMap() {
|
|
75
|
-
const map = new Map();
|
|
76
|
-
let entries = [];
|
|
77
|
-
try {
|
|
78
|
-
entries = readdirSync('/proc');
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
return map;
|
|
82
|
-
}
|
|
83
|
-
for (const entry of entries) {
|
|
84
|
-
const pid = Number(entry);
|
|
85
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
86
|
-
continue;
|
|
87
|
-
try {
|
|
88
|
-
const stat = readFileSync(`/proc/${pid}/stat`, 'utf-8');
|
|
89
|
-
// comm (field 2) may contain spaces/parens — parse after last ')'.
|
|
90
|
-
const rest = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
|
|
91
|
-
const ppid = Number(rest[1]);
|
|
92
|
-
if (!Number.isInteger(ppid) || ppid <= 0)
|
|
93
|
-
continue;
|
|
94
|
-
const siblings = map.get(ppid);
|
|
95
|
-
if (siblings)
|
|
96
|
-
siblings.push(pid);
|
|
97
|
-
else
|
|
98
|
-
map.set(ppid, [pid]);
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
// Process exited between readdir and read — fine.
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return map;
|
|
105
|
-
}
|
|
106
|
-
// Snapshot every transitive descendant of `pid`, breadth-first (so
|
|
107
|
-
// reversing the list yields deepest-first order for signalling).
|
|
108
|
-
function collectDescendants(pid) {
|
|
109
|
-
const result = [];
|
|
110
|
-
const seen = new Set([pid]);
|
|
111
|
-
const queue = [pid];
|
|
112
|
-
let ppidMap = null;
|
|
113
|
-
while (queue.length > 0) {
|
|
114
|
-
const cur = queue.shift();
|
|
115
|
-
let kids = readDirectChildren(cur);
|
|
116
|
-
if (kids === null) {
|
|
117
|
-
// children files unreadable — use (and lazily build) the
|
|
118
|
-
// full-scan fallback for this and subsequent levels.
|
|
119
|
-
ppidMap ??= buildPpidMap();
|
|
120
|
-
kids = ppidMap.get(cur) ?? [];
|
|
121
|
-
}
|
|
122
|
-
for (const kid of kids) {
|
|
123
|
-
if (seen.has(kid))
|
|
124
|
-
continue;
|
|
125
|
-
seen.add(kid);
|
|
126
|
-
result.push(kid);
|
|
127
|
-
queue.push(kid);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return result;
|
|
131
|
-
}
|
|
132
37
|
// Signal an already-collected descendant set deepest-first, then the direct
|
|
133
38
|
// child itself. Snapshot-then-signal (not signal-while-walking) so killing
|
|
134
39
|
// the parent can't orphan grandchildren before we enumerated them. The
|
|
@@ -157,7 +62,10 @@ function killActiveChildren() {
|
|
|
157
62
|
// The direct child is `npm run <script>` — killing only it leaves the
|
|
158
63
|
// npm-spawned shell → gjs/node build grandchildren alive (observed: an
|
|
159
64
|
// orphaned gjs bundler at 100% CPU for 19+ min after a fail-fast kill).
|
|
160
|
-
// So terminate the whole process TREE of each child
|
|
65
|
+
// So terminate the whole process TREE of each child — walked by
|
|
66
|
+
// `utils/process-table.ts` (procfs on Linux, the OS process list elsewhere).
|
|
67
|
+
// A process-group kill would be simpler and GJS cannot issue one:
|
|
68
|
+
// Gio.Subprocess has no group-signal API.
|
|
161
69
|
const termSnapshots = new Map();
|
|
162
70
|
for (const child of activeChildren) {
|
|
163
71
|
const descendants = child.pid ? collectDescendants(child.pid) : [];
|
package/lib/commands/format.js
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
// optionalDependency, keeping the Node-free runtime promise for the bundled
|
|
8
8
|
// CLI intact (the formatter runs in dev/CI, not in shipped GJS apps).
|
|
9
9
|
//
|
|
10
|
-
// oxfmt formats JS/TS
|
|
11
|
-
//
|
|
12
|
-
//
|
|
10
|
+
// oxfmt formats JS/TS, JSON, TOML, CSS and GraphQL in both builds; the
|
|
11
|
+
// languages it hands to its Prettier host (Markdown, HTML, Vue, YAML, …) are
|
|
12
|
+
// napi-only — the native GJS build drops them, which the skip scan below
|
|
13
|
+
// reports.
|
|
13
14
|
//
|
|
14
15
|
// A BARE `gjsify format` WRITES. `--check` is the read-only CI mode and
|
|
15
16
|
// `--no-write` the read-only local one; there is no flagless report mode.
|
|
@@ -24,11 +25,12 @@
|
|
|
24
25
|
// tuned for GJS/GNOME projects (4-space, single-quote, printWidth 120,
|
|
25
26
|
// trailing-comma all, semicolons always, arrow parens always).
|
|
26
27
|
import { existsSync, writeFileSync } from 'node:fs';
|
|
27
|
-
import { resolve } from 'node:path';
|
|
28
|
-
import { OxcNotFoundError, findOxfmtConfig, loadOxfmtTemplate, loadOxlintTemplate, printOxcNotFound, runOxfmt, setOxcExitCode, } from '../utils/oxc-resolve.js';
|
|
28
|
+
import { relative, resolve } from 'node:path';
|
|
29
|
+
import { OxcNotFoundError, findOxfmtConfig, loadOxfmtTemplate, loadOxlintTemplate, printOxcNotFound, runOxfmt, setOxcExitCode, shouldUseNativeOxfmt, } from '../utils/oxc-resolve.js';
|
|
30
|
+
import { scanForNativeSkips } from '../utils/native-skip-scan.js';
|
|
29
31
|
export const formatCommand = {
|
|
30
32
|
command: 'format [paths..]',
|
|
31
|
-
description: 'Format
|
|
33
|
+
description: 'Format source files via oxfmt (Markdown/YAML/HTML/Vue need the napi build).',
|
|
32
34
|
builder: (yargs) => {
|
|
33
35
|
return yargs
|
|
34
36
|
.positional('paths', {
|
|
@@ -90,6 +92,24 @@ export const formatCommand = {
|
|
|
90
92
|
if (configPath)
|
|
91
93
|
oxfmtArgs.push('--config', resolve(configPath));
|
|
92
94
|
oxfmtArgs.push(...paths);
|
|
95
|
+
// Under GJS the native oxfmt formats everything but the files napi oxfmt
|
|
96
|
+
// hands to its Prettier host (Markdown, HTML, Vue, YAML, …), and it drops
|
|
97
|
+
// those without a word. `--check` FAILS on them, so a green check that
|
|
98
|
+
// checked nothing is impossible — asked about exactly the files oxfmt
|
|
99
|
+
// itself would walk with this config, never `node_modules` or ignored ones.
|
|
100
|
+
if (args.check && (await shouldUseNativeOxfmt())) {
|
|
101
|
+
const scan = scanForNativeSkips(paths, cwd, { configPath });
|
|
102
|
+
if (scan.skipped.length > 0) {
|
|
103
|
+
const list = scan.skipped.map((f) => ` ${relative(cwd, f) || f}`).join('\n');
|
|
104
|
+
console.error(`[gjsify format] ERROR: ${scan.skipped.length} file(s) skipped — ` +
|
|
105
|
+
`the native oxfmt (GJS) has no Prettier host for them (Markdown/HTML/Vue/YAML/…):\n` +
|
|
106
|
+
`${list}\n` +
|
|
107
|
+
` The format check is INCOMPLETE. Run \`gjsify format --check\` under Node to check these ` +
|
|
108
|
+
`files, or add them to \`ignorePatterns\`.`);
|
|
109
|
+
setOxcExitCode(1);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
93
113
|
try {
|
|
94
114
|
const code = await runOxfmt(oxfmtArgs, { cwd, verbose: args.verbose });
|
|
95
115
|
setOxcExitCode(code);
|
package/lib/commands/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from './build.js';
|
|
2
2
|
export * from './clear.js';
|
|
3
3
|
export * from './copy.js';
|
|
4
|
+
export * from './env.js';
|
|
5
|
+
export * from './exec.js';
|
|
4
6
|
export * from './test.js';
|
|
5
7
|
export * from './run.js';
|
|
6
8
|
export * from './info.js';
|
|
@@ -39,4 +41,7 @@ export * from './affected.js';
|
|
|
39
41
|
export * from './storybook.js';
|
|
40
42
|
export * from './dev.js';
|
|
41
43
|
export * from './debug.js';
|
|
44
|
+
export * from './devtools.js';
|
|
42
45
|
export * from './browse.js';
|
|
46
|
+
export { webextCommand } from './webext/index.js';
|
|
47
|
+
export { blueprintCommand, blueprintTypesCommand } from './blueprint/index.js';
|
package/lib/commands/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from './build.js';
|
|
2
2
|
export * from './clear.js';
|
|
3
3
|
export * from './copy.js';
|
|
4
|
+
export * from './env.js';
|
|
5
|
+
export * from './exec.js';
|
|
4
6
|
export * from './test.js';
|
|
5
7
|
export * from './run.js';
|
|
6
8
|
export * from './info.js';
|
|
@@ -39,4 +41,7 @@ export * from './affected.js';
|
|
|
39
41
|
export * from './storybook.js';
|
|
40
42
|
export * from './dev.js';
|
|
41
43
|
export * from './debug.js';
|
|
44
|
+
export * from './devtools.js';
|
|
42
45
|
export * from './browse.js';
|
|
46
|
+
export { webextCommand } from './webext/index.js';
|
|
47
|
+
export { blueprintCommand, blueprintTypesCommand } from './blueprint/index.js';
|
|
@@ -83,8 +83,8 @@ export interface EnsureProjectGjsEngineDeps {
|
|
|
83
83
|
*
|
|
84
84
|
* The engine is an OPTIONAL PEER of `@gjsify/cli` on purpose — a plain
|
|
85
85
|
* `npm install @gjsify/cli` on Node must not fetch prebuilds — but nobody resolves
|
|
86
|
-
* it for a project: npm 7+ skips optional peers and the native backend
|
|
87
|
-
*
|
|
86
|
+
* it for a project: npm 7+ skips optional peers and so does the native backend
|
|
87
|
+
* (it installs REQUIRED peers only). Under GJS there is no npm `rolldown`
|
|
88
88
|
* fallback, so `gjsify build` had nothing to load.
|
|
89
89
|
*
|
|
90
90
|
* Four conditions, and (b) is the one worth reading:
|
package/lib/commands/install.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// `node_modules/` and symlinks `workspace:` refs to their source. Members with
|
|
11
11
|
// conflicting ranges of the same external still share ONE hoisted copy; the
|
|
12
12
|
// resolver warns naming both ranges, both requesters and the winner (per-member
|
|
13
|
-
// dedup is the open Phase D.8 item in status/open-todos.md).
|
|
13
|
+
// dedup is the open Phase D.8 item in status/open-todos/README.md).
|
|
14
14
|
//
|
|
15
15
|
// Concurrency (ADR 0001): every prefix mutation runs under a per-prefix
|
|
16
16
|
// cross-process lock (utils/install-lock.ts), so installs into different
|
|
@@ -641,7 +641,7 @@ function syncLockfileRequested(cwd, specs) {
|
|
|
641
641
|
*
|
|
642
642
|
* Hoisting is deliberately minimal — members with conflicting ranges of the same
|
|
643
643
|
* dep share one hoisted copy and the resolver warns. Per-member dedup and nested
|
|
644
|
-
* `node_modules/` are the open Phase D.8 item in status/open-todos.md.
|
|
644
|
+
* `node_modules/` are the open Phase D.8 item in status/open-todos/README.md.
|
|
645
645
|
*/
|
|
646
646
|
async function workspaceInstall(cwd, args, signal) {
|
|
647
647
|
// Held for the WHOLE flow: bin shims, workspace symlinks and the external
|
|
@@ -1272,7 +1272,7 @@ async function installGlobalAndLink(specs, opts) {
|
|
|
1272
1272
|
// A global install of `@gjsify/cli` must also lay down the GJS bundler engine
|
|
1273
1273
|
// + the sibling format/CSS bridges. They are OPTIONAL PEERS of the CLI (so a
|
|
1274
1274
|
// plain `npm install @gjsify/cli` on Node does not force a prebuild) and the
|
|
1275
|
-
// native backend
|
|
1275
|
+
// native backend, like npm ≥ 7, installs required peers only, so without this
|
|
1276
1276
|
// they never arrive and `gjsify build` hard-fails under GJS with "no usable
|
|
1277
1277
|
// bundler engine". Pinned to the CLI's resolved version so they move in
|
|
1278
1278
|
// lockstep with the bundle; a platform with no published prebuild warns.
|
|
@@ -1333,8 +1333,8 @@ function reportLinkedBins(created, binDir) {
|
|
|
1333
1333
|
*
|
|
1334
1334
|
* The engine is an OPTIONAL PEER of `@gjsify/cli` on purpose — a plain
|
|
1335
1335
|
* `npm install @gjsify/cli` on Node must not fetch prebuilds — but nobody resolves
|
|
1336
|
-
* it for a project: npm 7+ skips optional peers and the native backend
|
|
1337
|
-
*
|
|
1336
|
+
* it for a project: npm 7+ skips optional peers and so does the native backend
|
|
1337
|
+
* (it installs REQUIRED peers only). Under GJS there is no npm `rolldown`
|
|
1338
1338
|
* fallback, so `gjsify build` had nothing to load.
|
|
1339
1339
|
*
|
|
1340
1340
|
* Four conditions, and (b) is the one worth reading:
|
|
@@ -27,6 +27,7 @@ import { fetchPackument } from '@gjsify/npm-registry';
|
|
|
27
27
|
import { installPackages, makeProgressReporter } from '../utils/install-backend.js';
|
|
28
28
|
import { classifyInstall } from '../utils/install-provenance.js';
|
|
29
29
|
import { defaultGlobalLayout, globalLayoutIsDefault, hasBundlerEngineInstalled, installGjsEnginePackages, linkGlobalBins, resolveBinOnPath, } from '../utils/install-global.js';
|
|
30
|
+
import { pathWithoutSelfShim } from '../utils/gjsify-shim.js';
|
|
30
31
|
import { resolveHostPlatform } from '../utils/platform-check.js';
|
|
31
32
|
import { pruneAfterInstall } from '../utils/prune-prefix.js';
|
|
32
33
|
const PACKAGE_NAME = '@gjsify/cli';
|
|
@@ -210,7 +211,7 @@ export const selfUpdateCommand = {
|
|
|
210
211
|
const verdict = verifyPathResolution({
|
|
211
212
|
binName: BIN_NAME,
|
|
212
213
|
binDir: layout.binDir,
|
|
213
|
-
resolvedBin: resolveBinOnPath(BIN_NAME),
|
|
214
|
+
resolvedBin: resolveBinOnPath(BIN_NAME, { pathValue: pathWithoutSelfShim() }),
|
|
214
215
|
runningVersion: currentVersion,
|
|
215
216
|
targetVersion: target,
|
|
216
217
|
});
|
package/lib/commands/ship.js
CHANGED
|
@@ -45,7 +45,7 @@ import { compileSchemasForPayload, compileSchemasForStage } from '../utils/ship/
|
|
|
45
45
|
import { buildMsi, MSI_PAYLOAD_DIR } from '../utils/ship/msi.js';
|
|
46
46
|
import { buildZip, zipEntriesFromPayload } from '../utils/ship/zip.js';
|
|
47
47
|
import { assertEntryRunsUnder } from '../utils/ship/entry-interpreter.js';
|
|
48
|
-
import {
|
|
48
|
+
import { scanGiRequirements } from '../utils/ship/gi-namespaces.js';
|
|
49
49
|
import { assertLauncherMatchesInterpreter, assertPayloadMatchesArch, buildTimestamp, isArchIndependent, linuxInstallDependent, readPayloadFacts, } from '../utils/ship/payload.js';
|
|
50
50
|
import { assertOverlayIsLicensed, planOverlay, planStage } from '../utils/ship/plan.js';
|
|
51
51
|
import { assertHostCanSign, notarizeArtifact, stapleArtifact, resolveNotaryPlan, resolveSignPlan, signPayload, } from '../utils/ship/signing.js';
|
|
@@ -537,9 +537,20 @@ async function assemble(args) {
|
|
|
537
537
|
// `gi://` specifiers are what the emitted `Depends:` is derived from
|
|
538
538
|
// (ADR 0024 § 6), and the packing host has the staged copy but no way to
|
|
539
539
|
// tell which staged file is the entry.
|
|
540
|
-
const
|
|
541
|
-
|
|
540
|
+
const gi = scanGiRequirements(bundleSource);
|
|
541
|
+
const namespaces = gi.namespaces;
|
|
542
|
+
const optionalNamespaces = gi.optional;
|
|
543
|
+
if (args.verbose) {
|
|
544
|
+
const hard = namespaces.filter((ns) => !optionalNamespaces.includes(ns));
|
|
542
545
|
console.log(`${LOG} gi namespaces: ${namespaces.join(', ') || '(none)'}`);
|
|
546
|
+
// The split is worth its own line whenever it is non-empty: an author who
|
|
547
|
+
// wrote `&optional` and sees the namespace under the plain list has learned
|
|
548
|
+
// nothing about whether their package will declare it.
|
|
549
|
+
if (optionalNamespaces.length > 0) {
|
|
550
|
+
console.log(`${LOG} gi optional namespaces: ${optionalNamespaces.join(', ')}`);
|
|
551
|
+
console.log(`${LOG} gi required namespaces: ${hard.join(', ') || '(none)'}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
543
554
|
// PLACED, like the payload, and through the same map. `planOverlay` answers a
|
|
544
555
|
// question about the FORMAT (`share/doc/<pkg>/copyright` for Debian policy,
|
|
545
556
|
// `share/licenses/<pkg>/LICENSE` for rpm) in the prefix-relative shape every
|
|
@@ -578,8 +589,9 @@ async function assemble(args) {
|
|
|
578
589
|
// into a Debian `Depends:`, at exit 0).
|
|
579
590
|
if (format.depends === null)
|
|
580
591
|
continue;
|
|
581
|
-
deriveDepends(format.depends, {
|
|
592
|
+
const derived = deriveDepends(format.depends, {
|
|
582
593
|
namespaces,
|
|
594
|
+
optionalNamespaces,
|
|
583
595
|
hasIcons: facts.hasIcons,
|
|
584
596
|
hasSchemas: facts.hasSchemas,
|
|
585
597
|
interpreter: settings.app,
|
|
@@ -589,6 +601,17 @@ async function assemble(args) {
|
|
|
589
601
|
minGjsVersion: settings.minGjsVersion,
|
|
590
602
|
minNodeVersion: settings.minNodeVersion,
|
|
591
603
|
});
|
|
604
|
+
// Reported here and not inside `deriveDepends`, which is also called by
|
|
605
|
+
// `packOne` on a host that has no project — and this is the only place that
|
|
606
|
+
// does. An optional namespace nobody mapped produces NO package field, and
|
|
607
|
+
// the app runs without the typelib either way, so nothing downstream will
|
|
608
|
+
// ever say so again.
|
|
609
|
+
for (const namespace of derived.unmappedOptional) {
|
|
610
|
+
console.warn(`${LOG} gi://${namespace} is imported optionally, but no ${format.depends} package is known ` +
|
|
611
|
+
'to ship its typelib, so no Recommends entry names it. The package builds and runs ' +
|
|
612
|
+
`without it. Add the row if the namespace should be recommended: \`gjsify.ship.typelibPackages.` +
|
|
613
|
+
`${namespace} = { "${format.depends}": "…" }\`.`);
|
|
614
|
+
}
|
|
592
615
|
// One warning per package, for the interpreter it actually declares. A
|
|
593
616
|
// floor warning about a dependency this package does not emit is noise,
|
|
594
617
|
// and a noisy warning is the one nobody reads when it matters.
|
|
@@ -602,7 +625,16 @@ async function assemble(args) {
|
|
|
602
625
|
// is sometimes packable elsewhere and sometimes not is a worse contract
|
|
603
626
|
// than one that always is, and it is what lets the e2e suite compare the
|
|
604
627
|
// one-process artifact with the two-phase one.
|
|
605
|
-
const manifest = writeStageManifest({
|
|
628
|
+
const manifest = writeStageManifest({
|
|
629
|
+
stageDir,
|
|
630
|
+
settings,
|
|
631
|
+
formats,
|
|
632
|
+
mtime,
|
|
633
|
+
namespaces,
|
|
634
|
+
optionalNamespaces,
|
|
635
|
+
staged,
|
|
636
|
+
overlay,
|
|
637
|
+
});
|
|
606
638
|
if (args.stage) {
|
|
607
639
|
// `(none)` spelled out, because an empty list printed as nothing after
|
|
608
640
|
// "formats " reads as a truncated line rather than as a fact. WHICH fact
|
|
@@ -638,6 +670,7 @@ async function assemble(args) {
|
|
|
638
670
|
stageDir,
|
|
639
671
|
outRoot,
|
|
640
672
|
namespaces,
|
|
673
|
+
optionalNamespaces,
|
|
641
674
|
mtime,
|
|
642
675
|
sign,
|
|
643
676
|
notarize,
|
|
@@ -799,6 +832,9 @@ async function finishFromStage(args, fromStage) {
|
|
|
799
832
|
for (const file of manifest.staged)
|
|
800
833
|
console.log(`${LOG} ${file.path}`);
|
|
801
834
|
console.log(`${LOG} gi namespaces: ${manifest.namespaces.join(', ') || '(none)'}`);
|
|
835
|
+
if ((manifest.optionalNamespaces ?? []).length > 0) {
|
|
836
|
+
console.log(`${LOG} gi optional namespaces: ${manifest.optionalNamespaces?.join(', ')}`);
|
|
837
|
+
}
|
|
802
838
|
}
|
|
803
839
|
// The stage's own directory is not written to — `writeStage` wipes what it
|
|
804
840
|
// is pointed at, and pointing it at an arriving stage would delete the
|
|
@@ -832,6 +868,7 @@ async function finishFromStage(args, fromStage) {
|
|
|
832
868
|
stageDir,
|
|
833
869
|
outRoot,
|
|
834
870
|
namespaces: manifest.namespaces,
|
|
871
|
+
optionalNamespaces: manifest.optionalNamespaces ?? [],
|
|
835
872
|
mtime,
|
|
836
873
|
sign,
|
|
837
874
|
notarize,
|
|
@@ -858,10 +895,11 @@ async function packOne(input) {
|
|
|
858
895
|
// this answers used to be answered from the project's file lists, which are
|
|
859
896
|
// absolute paths on the build host and therefore unavailable here.
|
|
860
897
|
const facts = readPayloadFacts(assembled);
|
|
861
|
-
const
|
|
862
|
-
? []
|
|
898
|
+
const derived = format.depends === null
|
|
899
|
+
? { requires: [], recommends: [], unmappedOptional: [] }
|
|
863
900
|
: deriveDepends(format.depends, {
|
|
864
901
|
namespaces: input.namespaces,
|
|
902
|
+
optionalNamespaces: input.optionalNamespaces,
|
|
865
903
|
hasIcons: facts.hasIcons,
|
|
866
904
|
hasSchemas: facts.hasSchemas,
|
|
867
905
|
interpreter: settings.app,
|
|
@@ -871,6 +909,8 @@ async function packOne(input) {
|
|
|
871
909
|
minGjsVersion: settings.minGjsVersion,
|
|
872
910
|
minNodeVersion: settings.minNodeVersion,
|
|
873
911
|
});
|
|
912
|
+
const depends = derived.requires;
|
|
913
|
+
const recommends = derived.recommends;
|
|
874
914
|
// `--arch` is a CLAIM about the payload and nothing else compared it to one:
|
|
875
915
|
// an x86-64 `.so` staged with `--arch arm64` packed at exit 0 and `rpm -qp
|
|
876
916
|
// --qf %{ARCH}` confirmed `aarch64`, because rpm reads the header and the
|
|
@@ -956,7 +996,9 @@ async function packOne(input) {
|
|
|
956
996
|
})
|
|
957
997
|
: assembled;
|
|
958
998
|
const archLabel = format.archName(input.arch, isArchIndependent(payload));
|
|
959
|
-
|
|
999
|
+
// `recommends` rides the shared input object, so a format that does not write the
|
|
1000
|
+
// field (every non-deb/rpm row) simply ignores an extra property it never reads.
|
|
1001
|
+
const common = { settings, payload, prefix: format.prefix, depends, recommends, archLabel, mtime };
|
|
960
1002
|
const outDir = join(outRoot, 'out');
|
|
961
1003
|
mkdirSync(outDir, { recursive: true });
|
|
962
1004
|
const target = join(outDir, format.fileName(settings, archLabel));
|
|
@@ -1010,6 +1052,7 @@ async function packOne(input) {
|
|
|
1010
1052
|
minGjsVersion: settings.minGjsVersion,
|
|
1011
1053
|
minNodeVersion: settings.minNodeVersion,
|
|
1012
1054
|
namespaces: input.namespaces,
|
|
1055
|
+
optionalNamespaces: input.optionalNamespaces,
|
|
1013
1056
|
bundledTypelibs: facts.bundledTypelibs,
|
|
1014
1057
|
});
|
|
1015
1058
|
const appDir = appDirFor(outRoot);
|
package/lib/commands/test.d.ts
CHANGED
|
@@ -10,16 +10,25 @@ interface TestOptions {
|
|
|
10
10
|
}
|
|
11
11
|
export declare const testCommand: Command<unknown, TestOptions>;
|
|
12
12
|
/**
|
|
13
|
-
* True when `outfile` is at least as new as every
|
|
14
|
-
*
|
|
13
|
+
* True when `outfile` is at least as new as every input of the build that
|
|
14
|
+
* wrote it.
|
|
15
15
|
*
|
|
16
|
-
* The set is
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
16
|
+
* The set is the union of two answers, because neither covers the question
|
|
17
|
+
* alone: {@link packageBuildInputs} — what may this PACKAGE's build read, the
|
|
18
|
+
* same walk the build cache hashes — and the manifest the build itself wrote
|
|
19
|
+
* beside `outfile` ({@link readBundleInputs}), what this build DID read. The
|
|
20
|
+
* second is what reaches a workspace package through a `node_modules` symlink
|
|
21
|
+
* into a sibling directory: the package walk is blind to it by construction,
|
|
22
|
+
* so an edit there left the bundle "fresh" and the run reported on code that
|
|
23
|
+
* was no longer on disk — a stale green, worse than a slow build. The first is
|
|
24
|
+
* what the walk alone never managed: it was `dirname(entry)`, `tests/` for any
|
|
25
|
+
* package whose suite lives there, so `src/**` was never seen and `gjsify test`
|
|
26
|
+
* reran the previous build and reported on it, in both directions inside one
|
|
27
|
+
* dependency bump (#1651).
|
|
28
|
+
*
|
|
29
|
+
* A missing manifest is NOT an answer: it is the pre-recorder shape, and the
|
|
30
|
+
* package walk then decides alone — never weaker than before, and the next
|
|
31
|
+
* build writes one.
|
|
23
32
|
*
|
|
24
33
|
* `outdir` is subtracted as this command's own output; `entry` is added
|
|
25
34
|
* because `--entry ../shared/test.mts` may point outside the package.
|
package/lib/commands/test.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// exit codes.
|
|
8
8
|
import { existsSync, statSync } from 'node:fs';
|
|
9
9
|
import { join, resolve, relative, sep } from 'node:path';
|
|
10
|
+
import { newestBundleInputMtimeMs, readBundleInputs, createBundleInputsRecorder } from '../utils/bundle-inputs.js';
|
|
10
11
|
import { newestDepSignalMtimeMs, newestInputMtimeMs, packageBuildInputs } from '../utils/package-inputs.js';
|
|
11
12
|
import { isRuntimeAvailable, RUNTIMES } from '../utils/runtimes.js';
|
|
12
13
|
import { nodeBinary } from '../utils/run-node.js';
|
|
@@ -180,13 +181,39 @@ async function buildTestBundle(entry, outfile, runtime, verbose) {
|
|
|
180
181
|
// bundler.output.file from the surrounding project don't redirect the
|
|
181
182
|
// bundle elsewhere.
|
|
182
183
|
configData.library = { ...configData.library };
|
|
184
|
+
// Record what this build READ, so the next `isFresh` can judge against the
|
|
185
|
+
// bundle's real dependencies rather than this package's tree. A recorder
|
|
186
|
+
// rides the same plugin chain a user's `bundler.plugins` entry does, and
|
|
187
|
+
// the auto-globals pre-build pass therefore feeds it too — a superset of
|
|
188
|
+
// the final graph, which errs towards a rebuild and never towards a stale
|
|
189
|
+
// green. Appended last so it observes whatever the earlier hooks loaded.
|
|
190
|
+
const recorder = createBundleInputsRecorder(outfile);
|
|
183
191
|
configData.bundler = {
|
|
184
192
|
...configData.bundler,
|
|
185
193
|
input: [entry],
|
|
186
194
|
output: { ...configData.bundler?.output, file: outfile },
|
|
195
|
+
plugins: [...(configData.bundler?.plugins ?? []), recorder.plugin],
|
|
187
196
|
};
|
|
188
197
|
const action = new BuildAction(configData);
|
|
189
|
-
|
|
198
|
+
// `onWatchFiles` is the half the transform observer cannot reach: a file a
|
|
199
|
+
// plugin read itself and declared (a stylesheet's `@import` chain). The
|
|
200
|
+
// recorder takes the union — npm reports its module set too, the native
|
|
201
|
+
// engine reports nothing but the declarations, and each engine alone would
|
|
202
|
+
// leave a class of input unrecorded.
|
|
203
|
+
await action.start({
|
|
204
|
+
app: runtime,
|
|
205
|
+
library: false,
|
|
206
|
+
onWatchFiles: (watch) => recorder.addWatchFiles(watch),
|
|
207
|
+
});
|
|
208
|
+
// Written AFTER the build resolves, and that ordering is the whole of what
|
|
209
|
+
// a failure leaves behind: a build that fails BEFORE writing an artifact
|
|
210
|
+
// (parse, resolve, a plugin hook) throws out of `start()` and never reaches
|
|
211
|
+
// this line, so the previous manifest still describes the previous bundle.
|
|
212
|
+
// A failure in a POST-WRITE gate is the other case and is not ours to
|
|
213
|
+
// paper over: `actions/build.ts` has already replaced the bundle, the
|
|
214
|
+
// manifest is rewritten beside it, and both then describe the same build —
|
|
215
|
+
// the next run re-runs the failing bundle rather than the one before it.
|
|
216
|
+
recorder.write();
|
|
190
217
|
}
|
|
191
218
|
/** Run a single test bundle and reject on non-zero exit. */
|
|
192
219
|
async function runTestBundle(outfile, runtime) {
|
|
@@ -206,16 +233,25 @@ async function runTestBundle(outfile, runtime) {
|
|
|
206
233
|
throw new Error(`node exited with ${describeExit(result)}`);
|
|
207
234
|
}
|
|
208
235
|
/**
|
|
209
|
-
* True when `outfile` is at least as new as every
|
|
210
|
-
*
|
|
236
|
+
* True when `outfile` is at least as new as every input of the build that
|
|
237
|
+
* wrote it.
|
|
238
|
+
*
|
|
239
|
+
* The set is the union of two answers, because neither covers the question
|
|
240
|
+
* alone: {@link packageBuildInputs} — what may this PACKAGE's build read, the
|
|
241
|
+
* same walk the build cache hashes — and the manifest the build itself wrote
|
|
242
|
+
* beside `outfile` ({@link readBundleInputs}), what this build DID read. The
|
|
243
|
+
* second is what reaches a workspace package through a `node_modules` symlink
|
|
244
|
+
* into a sibling directory: the package walk is blind to it by construction,
|
|
245
|
+
* so an edit there left the bundle "fresh" and the run reported on code that
|
|
246
|
+
* was no longer on disk — a stale green, worse than a slow build. The first is
|
|
247
|
+
* what the walk alone never managed: it was `dirname(entry)`, `tests/` for any
|
|
248
|
+
* package whose suite lives there, so `src/**` was never seen and `gjsify test`
|
|
249
|
+
* reran the previous build and reported on it, in both directions inside one
|
|
250
|
+
* dependency bump (#1651).
|
|
211
251
|
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* the bundle "fresh" and `gjsify test` reran the previous build and reported
|
|
216
|
-
* on it. That went both ways inside one dependency bump — a green run
|
|
217
|
-
* measuring a version that was no longer installed, then a red one after the
|
|
218
|
-
* source had already been repaired (#1651).
|
|
252
|
+
* A missing manifest is NOT an answer: it is the pre-recorder shape, and the
|
|
253
|
+
* package walk then decides alone — never weaker than before, and the next
|
|
254
|
+
* build writes one.
|
|
219
255
|
*
|
|
220
256
|
* `outdir` is subtracted as this command's own output; `entry` is added
|
|
221
257
|
* because `--entry ../shared/test.mts` may point outside the package.
|
|
@@ -229,7 +265,16 @@ export function isFresh(outfile, entry, cwd, outdir) {
|
|
|
229
265
|
const outdirRel = relative(cwd, outdir).split(sep).join('/');
|
|
230
266
|
if (outdirRel !== '' && !outdirRel.startsWith('../'))
|
|
231
267
|
extraOutputs.push(outdirRel);
|
|
232
|
-
const
|
|
268
|
+
const bundleInputs = readBundleInputs(outfile);
|
|
269
|
+
// A build whose engine could report NO watch list never entered a file
|
|
270
|
+
// only a PLUGIN read — a stylesheet's `@import` chain, an inlined
|
|
271
|
+
// `readFileSync` — into the set below, and no mtime comparison can
|
|
272
|
+
// recover it. Judging such a bundle fresh IS the stale green, so the
|
|
273
|
+
// answer is no: rebuild, on every run, and say why. `bundler-pick.ts`
|
|
274
|
+
// prints the skew once; the manifest carries it across processes.
|
|
275
|
+
if (bundleInputs !== null && !bundleInputs.watchList)
|
|
276
|
+
return false;
|
|
277
|
+
const newest = Math.max(newestInputMtimeMs(packageBuildInputs(cwd, { extraOutputs })), bundleInputs === null ? 0 : newestBundleInputMtimeMs(bundleInputs.inputs), statSync(entry).mtimeMs,
|
|
233
278
|
// A dependency bump changes no file in the package — and that is
|
|
234
279
|
// the shape #1651 was found in. `node_modules` is far too large
|
|
235
280
|
// to stat per run, so the lockfile stands in for it.
|
package/lib/commands/tsc.js
CHANGED
|
@@ -128,10 +128,10 @@ export const tscCommand = {
|
|
|
128
128
|
// Run upstream `typescript` under Node — the Node fallback.
|
|
129
129
|
//
|
|
130
130
|
// `nodeBinary()`, NEVER a bare `process.execPath`: under the GJS bundle
|
|
131
|
-
// `process.execPath` is the GJS interpreter (
|
|
132
|
-
// `
|
|
133
|
-
// TypeScript's CommonJS CLI to
|
|
134
|
-
// reference — or, where execPath resolves to a gjsify launcher instead,
|
|
131
|
+
// `process.execPath` is the GJS interpreter (`hostExecPath()`:
|
|
132
|
+
// `/proc/self/exe` on Linux, the PATH lookup of `gjs` on macOS), so
|
|
133
|
+
// `spawn(process.execPath, [tsc.js])` hands TypeScript's CommonJS CLI to
|
|
134
|
+
// GJS, which dies on the first `module` reference — or, where execPath resolves to a gjsify launcher instead,
|
|
135
135
|
// re-executes the CLI with the tsc entry as an unknown argument. Both
|
|
136
136
|
// were reached from a COLD tree: `@gjsify/tsc`'s `dist/tsc.gjs.mjs` is a
|
|
137
137
|
// build output, and `build:infra` builds `@gjsify/create-app` (its first
|
|
@@ -29,6 +29,21 @@ export declare const upgradeCommand: Command<unknown, UpgradeOptions>;
|
|
|
29
29
|
* Returns the number of offending declarations so the caller can exit once for both arms.
|
|
30
30
|
*/
|
|
31
31
|
export declare function reportInexactRanges(groups: readonly DependencyGroup[]): number;
|
|
32
|
+
/**
|
|
33
|
+
* Check that every lockfile resolution satisfies the ranges declared for it.
|
|
34
|
+
*
|
|
35
|
+
* `gjsify upgrade --check` compares declared ranges against each other across
|
|
36
|
+
* workspaces. It does NOT compare the resolution against any of them, so a
|
|
37
|
+
* lockfile entry that violates every declaration in the repo passes silently.
|
|
38
|
+
*
|
|
39
|
+
* This check reads the lockfile, resolves each entry's version, and verifies
|
|
40
|
+
* it satisfies at least one declared range for that dependency.
|
|
41
|
+
*
|
|
42
|
+
* @param groups dependency groups with declared ranges
|
|
43
|
+
* @param cwd working directory
|
|
44
|
+
* @returns number of violations
|
|
45
|
+
*/
|
|
46
|
+
export declare function checkLockfileRangeViolations(groups: readonly DependencyGroup[], cwd: string): number;
|
|
32
47
|
/**
|
|
33
48
|
* The range a dependency will be WRITTEN at.
|
|
34
49
|
*
|