@gjsify/cli 0.41.0 → 0.43.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 +1068 -405
- package/lib/actions/build.d.ts +20 -0
- package/lib/actions/build.js +118 -15
- package/lib/bundler-pick.js +10 -2
- package/lib/cli-app.js +2 -1
- package/lib/commands/browse.js +1 -1
- 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 +1 -0
- package/lib/commands/index.js +1 -0
- package/lib/commands/run.js +38 -85
- package/lib/commands/ship.d.ts +2 -0
- package/lib/commands/ship.js +285 -88
- 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/types/config-data.d.ts +59 -0
- package/lib/utils/app-metadata.js +27 -6
- package/lib/utils/check-system-deps.js +49 -7
- 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/gjs-bundle-guard.d.ts +57 -3
- package/lib/utils/gjs-bundle-guard.js +122 -13
- package/lib/utils/jsx-config.d.ts +45 -0
- package/lib/utils/jsx-config.js +98 -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/onboard-probe.d.ts +23 -0
- package/lib/utils/onboard-probe.js +65 -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/deb.d.ts +3 -3
- package/lib/utils/ship/deb.js +6 -2
- package/lib/utils/ship/depends.d.ts +14 -6
- package/lib/utils/ship/depends.js +45 -4
- package/lib/utils/ship/discover.js +178 -7
- package/lib/utils/ship/formats.js +9 -1
- package/lib/utils/ship/launcher.js +27 -4
- package/lib/utils/ship/mime.d.ts +19 -0
- package/lib/utils/ship/mime.js +89 -0
- package/lib/utils/ship/payload.d.ts +111 -0
- package/lib/utils/ship/payload.js +174 -0
- package/lib/utils/ship/plan.d.ts +30 -1
- package/lib/utils/ship/plan.js +74 -2
- package/lib/utils/ship/rpm.d.ts +3 -3
- package/lib/utils/ship/rpm.js +20 -3
- package/lib/utils/ship/scripts.d.ts +14 -4
- package/lib/utils/ship/scripts.js +26 -8
- package/lib/utils/ship/settings.d.ts +8 -1
- package/lib/utils/ship/settings.js +26 -1
- package/lib/utils/ship/stage-manifest.d.ts +154 -0
- package/lib/utils/ship/stage-manifest.js +408 -0
- package/lib/utils/ship/stage-writer.d.ts +30 -3
- package/lib/utils/ship/stage-writer.js +76 -9
- package/lib/utils/ship/types.d.ts +110 -18
- package/lib/utils/ship/types.js +5 -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 +19 -18
- package/lib/templates/app/desktop.tmpl +0 -10
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
/** One
|
|
2
|
-
export interface
|
|
1
|
+
/** One planned path and the mode the PLAN gives it. */
|
|
2
|
+
export interface StagedMode {
|
|
3
3
|
/** Prefix-relative path, e.g. `bin/learn6502`, `share/applications/x.desktop`. */
|
|
4
4
|
path: string;
|
|
5
5
|
/** POSIX mode bits. */
|
|
6
6
|
mode: number;
|
|
7
|
+
}
|
|
8
|
+
/** One file in the payload, addressed relative to the install prefix. */
|
|
9
|
+
export interface StagedFile extends StagedMode {
|
|
7
10
|
/** Where the bytes come from. */
|
|
8
11
|
source: {
|
|
9
12
|
kind: 'text';
|
|
@@ -13,6 +16,18 @@ export interface StagedFile {
|
|
|
13
16
|
path: string;
|
|
14
17
|
};
|
|
15
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* What {@link readStage} checks a staged tree against.
|
|
21
|
+
*
|
|
22
|
+
* `bytes` is present only when the plan came from a stage manifest, i.e. when
|
|
23
|
+
* the tree travelled between hosts and there is something to compare against.
|
|
24
|
+
* The single-host run plans and writes in one process, so there is no transfer
|
|
25
|
+
* to check and no second opinion to check it with.
|
|
26
|
+
*/
|
|
27
|
+
export interface StagePlanEntry extends StagedMode {
|
|
28
|
+
/** Size the manifest recorded for this path, when one was recorded. */
|
|
29
|
+
bytes?: number;
|
|
30
|
+
}
|
|
16
31
|
/** A packed artifact on disk. */
|
|
17
32
|
export interface ShipArtifact {
|
|
18
33
|
format: FormatId;
|
|
@@ -49,17 +64,61 @@ export interface FormatDescriptor {
|
|
|
49
64
|
* arm64 machine it runs on perfectly. One `.so` in the payload flips it.
|
|
50
65
|
*/
|
|
51
66
|
archName: (arch: string, archIndependent: boolean) => string;
|
|
52
|
-
/** Artifact filename, given the
|
|
53
|
-
fileName: (settings:
|
|
67
|
+
/** Artifact filename, given the pack-time settings and the resolved arch label. */
|
|
68
|
+
fileName: (settings: PackSettings, archLabel: string) => string;
|
|
54
69
|
}
|
|
55
|
-
/**
|
|
56
|
-
export interface
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
70
|
+
/** One shared-mime-info type definition (`gjsify.ship.mimeTypes`). */
|
|
71
|
+
export interface ShipMimeType {
|
|
72
|
+
type: string;
|
|
73
|
+
comment: string;
|
|
74
|
+
globs?: string[];
|
|
75
|
+
subClassOf?: string;
|
|
76
|
+
genericIcon?: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The half of the configuration that survives the host boundary.
|
|
80
|
+
*
|
|
81
|
+
* `gjsify ship --from-stage` runs on a machine that has the staged tree and
|
|
82
|
+
* NOTHING else: no project directory, no `package.json`, no built bundle. So
|
|
83
|
+
* the two phases exchange exactly this object, serialised into the stage's
|
|
84
|
+
* `.gjsify-ship-stage.json`, and that is why the split is a TYPE rather than a
|
|
85
|
+
* convention — the packers take a `PackSettings`, so a `settings.bundlePath`
|
|
86
|
+
* read added to `deb.ts` or `rpm.ts` is a compile error until the field is
|
|
87
|
+
* moved here, and moving it here is what puts it in the manifest.
|
|
88
|
+
*
|
|
89
|
+
* Every field below has a MEASURED reader in the pack path
|
|
90
|
+
* (`deb.ts`, `rpm.ts`, `depends.ts`, `formats.ts#fileName`). Nothing is here
|
|
91
|
+
* "because ship needs it": a settings dump is what ADR 0024 § A2 rejects, and
|
|
92
|
+
* a dumped field that happens to be an absolute path is a path the packing host
|
|
93
|
+
* cannot open.
|
|
94
|
+
*
|
|
95
|
+
* NOT here, deliberately, and each with the reason it is not:
|
|
96
|
+
* - `projectDir`, `outDir` — nothing in the pack path reads either; the
|
|
97
|
+
* finishing host has its own output root.
|
|
98
|
+
* - `bundlePath`, `bundleDir` — build-tree paths. Their three readers are all
|
|
99
|
+
* phase 1: the build stamp (carried as the manifest's `mtime`), the
|
|
100
|
+
* launcher's exec target (already rendered into the staged `bin/<name>`),
|
|
101
|
+
* and the GI-namespace scan (carried as the manifest's `namespaces`).
|
|
102
|
+
* - `iconFiles`, `schemaFiles`, `typelibFiles` — the pack path reads them only
|
|
103
|
+
* to ask what the PAYLOAD contains, which the payload answers itself; see
|
|
104
|
+
* `readPayloadFacts`.
|
|
105
|
+
* - `licenseFile`, `extraFiles` — phase 1 reads their bytes into the plan.
|
|
106
|
+
* - `localeFiles` — same, and the reason is worth stating because the field
|
|
107
|
+
* arrived (#1263) AFTER this split was written and carries a build-host
|
|
108
|
+
* `abs` path: `planStage` reads each catalogue into the staged tree at
|
|
109
|
+
* `share/locale/<rel>`, and no packer reads the field. So translations
|
|
110
|
+
* cross in the payload, where they belong, and the `abs` path does not
|
|
111
|
+
* cross at all. This list being explicit is what caught that.
|
|
112
|
+
* - `appId`, `name`, `kind`, `execArgs` — read by the planner and the metadata
|
|
113
|
+
* renderers, all of which have run by the time a stage exists.
|
|
114
|
+
* - `arch` — carried by the manifest's `target`, which is also the value a
|
|
115
|
+
* mismatched stage is refused on, so it lives in exactly one place.
|
|
116
|
+
* - `mimeTypes` — phase 1 renders the shared-mime-info document into the
|
|
117
|
+
* staged tree, and the one pack-time reader asks whether that FILE is in the
|
|
118
|
+
* payload (`readPayloadFacts#hasMimeTypes`), which is the same question one
|
|
119
|
+
* host later. Carrying the declarations would put the answer in two places.
|
|
120
|
+
*/
|
|
121
|
+
export interface PackSettings {
|
|
63
122
|
/** Package name: the `bin/` entry, the deb `Package:`, the rpm `Name:`. */
|
|
64
123
|
binaryName: string;
|
|
65
124
|
/** Upstream version, normalised for package managers. */
|
|
@@ -74,15 +133,11 @@ export interface ShipSettings {
|
|
|
74
133
|
description: string[];
|
|
75
134
|
/** SPDX identifier of the project licence. */
|
|
76
135
|
license: string;
|
|
77
|
-
/** Absolute path to the licence file, when one was found. */
|
|
78
|
-
licenseFile?: string;
|
|
79
136
|
homepage?: string;
|
|
80
137
|
/** deb `Section:`. */
|
|
81
138
|
section: string;
|
|
82
139
|
/** rpm `Group:`. */
|
|
83
140
|
group: string;
|
|
84
|
-
/** `'app'` stages a desktop entry and an icon; `'cli'` stages neither. */
|
|
85
|
-
kind: 'app' | 'cli';
|
|
86
141
|
/** Extra runtime dependencies per format, appended to the derived set. */
|
|
87
142
|
extraDepends: {
|
|
88
143
|
deb: string[];
|
|
@@ -93,6 +148,21 @@ export interface ShipSettings {
|
|
|
93
148
|
deb: string;
|
|
94
149
|
rpm: string;
|
|
95
150
|
}>;
|
|
151
|
+
/** Minimum GJS the emitted dependency asks for. */
|
|
152
|
+
minGjsVersion: string;
|
|
153
|
+
}
|
|
154
|
+
/** The resolved, fully defaulted configuration for one `gjsify ship` run. */
|
|
155
|
+
export interface ShipSettings extends PackSettings {
|
|
156
|
+
/** Absolute path to the project being shipped. */
|
|
157
|
+
projectDir: string;
|
|
158
|
+
/** Reverse-DNS application id — desktop entry, metainfo and icon basename. */
|
|
159
|
+
appId: string;
|
|
160
|
+
/** Human-readable display name. */
|
|
161
|
+
name: string;
|
|
162
|
+
/** Absolute path to the licence file, when one was found. */
|
|
163
|
+
licenseFile?: string;
|
|
164
|
+
/** `'app'` stages a desktop entry and an icon; `'cli'` stages neither. */
|
|
165
|
+
kind: 'app' | 'cli';
|
|
96
166
|
/** Absolute path to the built bundle that `bin/<binaryName>` executes. */
|
|
97
167
|
bundlePath: string;
|
|
98
168
|
/** Absolute path of the directory staged wholesale into `lib/<binaryName>/`. */
|
|
@@ -101,6 +171,30 @@ export interface ShipSettings {
|
|
|
101
171
|
iconFiles: string[];
|
|
102
172
|
/** Absolute paths of `*.gschema.xml` files to install. */
|
|
103
173
|
schemaFiles: string[];
|
|
174
|
+
/**
|
|
175
|
+
* File types this package defines system-wide (shared-mime-info). Empty for a package that only
|
|
176
|
+
* HANDLES existing types — that needs no document of its own.
|
|
177
|
+
*/
|
|
178
|
+
mimeTypes: ShipMimeType[];
|
|
179
|
+
/**
|
|
180
|
+
* Typelibs + shared libraries the package carries itself (`gjsify.ship.bundledTypelibs`).
|
|
181
|
+
*
|
|
182
|
+
* gjsify's own GI libraries arrive as npm prebuilds, not distro packages, so an app using one
|
|
183
|
+
* has nothing to depend on and must ship the files — with the launcher pointing GI at them.
|
|
184
|
+
*/
|
|
185
|
+
typelibFiles: string[];
|
|
186
|
+
/**
|
|
187
|
+
* Compiled gettext catalogues (`gjsify.ship.localeDir`), each keeping its
|
|
188
|
+
* `<lang>/LC_MESSAGES/<domain>.mo` path so `share/locale/` comes out in the layout
|
|
189
|
+
* `bindtextdomain` reads.
|
|
190
|
+
*
|
|
191
|
+
* `rel` rather than a bare list of absolute paths, because the LAYOUT is the whole point here:
|
|
192
|
+
* a `.mo` staged by basename alone lands where no catalogue lookup will ever look at it.
|
|
193
|
+
*/
|
|
194
|
+
localeFiles: {
|
|
195
|
+
rel: string;
|
|
196
|
+
abs: string;
|
|
197
|
+
}[];
|
|
104
198
|
/** `<dest relative to prefix>` → absolute source path. */
|
|
105
199
|
extraFiles: Record<string, string>;
|
|
106
200
|
/** Arguments appended to the launcher's `exec` line. */
|
|
@@ -109,6 +203,4 @@ export interface ShipSettings {
|
|
|
109
203
|
outDir: string;
|
|
110
204
|
/** Target architecture in `process.arch` spelling. */
|
|
111
205
|
arch: string;
|
|
112
|
-
/** Minimum GJS the emitted dependency asks for. */
|
|
113
|
-
minGjsVersion: string;
|
|
114
206
|
}
|
package/lib/utils/ship/types.js
CHANGED
|
@@ -7,4 +7,9 @@
|
|
|
7
7
|
// (`refs/gtkx`, MPL-2.0, reimplemented not copied) expresses the same
|
|
8
8
|
// distinction as `if (target === 'deb')` inside its stager, and pays for it:
|
|
9
9
|
// adding a fifth target there means editing six files.
|
|
10
|
+
//
|
|
11
|
+
// ADR 0024 § A2 splits the command in two — `--stage` assembles anywhere,
|
|
12
|
+
// `--from-stage` packs on the host that owns the format — and that split shows
|
|
13
|
+
// up here as the {@link PackSettings} / {@link ShipSettings} pair. Read the
|
|
14
|
+
// comment on {@link PackSettings} before adding a field to either.
|
|
10
15
|
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokenize `cmd`, or return `null` if the string contains anything the shell
|
|
3
|
+
* would treat specially — operators (`&& | ; < > &`), substitutions (`$(...)` /
|
|
4
|
+
* backticks / `$VAR`), unquoted globs/expansions (`* ? { } [ ] ~`), comments
|
|
5
|
+
* (`#`), or an unterminated quote — in which case the caller MUST use the real
|
|
6
|
+
* shell instead. Quotes are honoured so a quoted glob (`'src/**'`) survives as a
|
|
7
|
+
* single literal token, exactly as the shell would hand it to `gjsify` (which
|
|
8
|
+
* does its own glob expansion).
|
|
9
|
+
*/
|
|
10
|
+
export declare function tokenizeSimpleCommand(cmd: string): string[] | null;
|
|
11
|
+
/**
|
|
12
|
+
* The argv of a script that is a single `gjsify <subcommand> …` command — i.e.
|
|
13
|
+
* the tokens after `gjsify` — or `null` when it is anything else. `null` is the
|
|
14
|
+
* signal to hand the literal to a shell.
|
|
15
|
+
*/
|
|
16
|
+
export declare function gjsifyCommandArgv(literal: string): string[] | null;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Quote-aware tokenizer for a package.json script that is a SIMPLE command line.
|
|
2
|
+
//
|
|
3
|
+
// Lifted out of `commands/run.ts` when `gjsify dev` became the second caller:
|
|
4
|
+
// both need to know whether a declared script is a plain `gjsify <subcommand> …`
|
|
5
|
+
// they may take apart, or something only a shell can execute. The predicate is
|
|
6
|
+
// deliberately conservative in ONE direction — a `null` costs the caller its
|
|
7
|
+
// fast path, a wrong token list would run a different command than the script
|
|
8
|
+
// says.
|
|
9
|
+
/**
|
|
10
|
+
* Tokenize `cmd`, or return `null` if the string contains anything the shell
|
|
11
|
+
* would treat specially — operators (`&& | ; < > &`), substitutions (`$(...)` /
|
|
12
|
+
* backticks / `$VAR`), unquoted globs/expansions (`* ? { } [ ] ~`), comments
|
|
13
|
+
* (`#`), or an unterminated quote — in which case the caller MUST use the real
|
|
14
|
+
* shell instead. Quotes are honoured so a quoted glob (`'src/**'`) survives as a
|
|
15
|
+
* single literal token, exactly as the shell would hand it to `gjsify` (which
|
|
16
|
+
* does its own glob expansion).
|
|
17
|
+
*/
|
|
18
|
+
export function tokenizeSimpleCommand(cmd) {
|
|
19
|
+
const tokens = [];
|
|
20
|
+
let cur = '';
|
|
21
|
+
let has = false;
|
|
22
|
+
let quote = null;
|
|
23
|
+
for (let i = 0; i < cmd.length; i++) {
|
|
24
|
+
const c = cmd[i];
|
|
25
|
+
if (quote === "'") {
|
|
26
|
+
if (c === "'")
|
|
27
|
+
quote = null;
|
|
28
|
+
else
|
|
29
|
+
cur += c;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (quote === '"') {
|
|
33
|
+
if (c === '"')
|
|
34
|
+
quote = null;
|
|
35
|
+
else if (c === '\\' && (cmd[i + 1] === '"' || cmd[i + 1] === '\\'))
|
|
36
|
+
cur += cmd[++i];
|
|
37
|
+
else if (c === '$' || c === '`')
|
|
38
|
+
return null; // substitution inside "…"
|
|
39
|
+
else
|
|
40
|
+
cur += c;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (c === "'" || c === '"') {
|
|
44
|
+
quote = c;
|
|
45
|
+
has = true;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (c === ' ' || c === '\t') {
|
|
49
|
+
if (has) {
|
|
50
|
+
tokens.push(cur);
|
|
51
|
+
cur = '';
|
|
52
|
+
has = false;
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if ('|&;<>`$()\\\n\r*?{}[]~#!'.includes(c))
|
|
57
|
+
return null;
|
|
58
|
+
cur += c;
|
|
59
|
+
has = true;
|
|
60
|
+
}
|
|
61
|
+
if (quote)
|
|
62
|
+
return null; // unterminated quote
|
|
63
|
+
if (has)
|
|
64
|
+
tokens.push(cur);
|
|
65
|
+
return tokens;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The argv of a script that is a single `gjsify <subcommand> …` command — i.e.
|
|
69
|
+
* the tokens after `gjsify` — or `null` when it is anything else. `null` is the
|
|
70
|
+
* signal to hand the literal to a shell.
|
|
71
|
+
*/
|
|
72
|
+
export function gjsifyCommandArgv(literal) {
|
|
73
|
+
const tokens = tokenizeSimpleCommand(literal);
|
|
74
|
+
if (!tokens || tokens.length < 2 || tokens[0] !== 'gjsify')
|
|
75
|
+
return null;
|
|
76
|
+
return tokens.slice(1);
|
|
77
|
+
}
|
package/lib/utils/spawn.d.ts
CHANGED
|
@@ -6,10 +6,16 @@ import type { ChildProcess } from 'node:child_process';
|
|
|
6
6
|
* main loop, so the streaming async `spawn` is safe on both hosts.
|
|
7
7
|
* - `'return'` — the command returns and lets the CLI unwind. Under GJS that must
|
|
8
8
|
* not leave a main loop armed, so the child runs on the blocking `spawnSync` path.
|
|
9
|
+
* - `'daemon'` — the command settles nothing: it stays alive supervising the child,
|
|
10
|
+
* on a main loop it holds ITSELF under GJS (`holdMainLoop()`; see the module header
|
|
11
|
+
* for why this spawn cannot arm one for it). The caller takes the handle through
|
|
12
|
+
* {@link SpawnToCompletionOptions.onSpawn} and does NOT await the returned promise,
|
|
13
|
+
* so "once this spawn settles" has no answer for it. Distinct from `'exit'` because
|
|
14
|
+
* a supervisor has no exit to be safe by; it routes to the same streaming path.
|
|
9
15
|
*
|
|
10
16
|
* See the module header for the full rationale.
|
|
11
17
|
*/
|
|
12
|
-
export type SpawnCompletionContract = 'exit' | 'return';
|
|
18
|
+
export type SpawnCompletionContract = 'exit' | 'return' | 'daemon';
|
|
13
19
|
export interface SpawnToCompletionOptions {
|
|
14
20
|
/** The caller's teardown contract. Required — see {@link SpawnCompletionContract}. */
|
|
15
21
|
completion: SpawnCompletionContract;
|
package/lib/utils/spawn.js
CHANGED
|
@@ -24,6 +24,16 @@
|
|
|
24
24
|
// | Node | anything | async `spawn` (streaming) |
|
|
25
25
|
// | GJS | `process.exit(…)` | async `spawn` (streaming) |
|
|
26
26
|
// | GJS | a normal `return` | blocking `spawnSync` (captured) |
|
|
27
|
+
// | GJS | never — it supervises | async `spawn` (streaming) |
|
|
28
|
+
//
|
|
29
|
+
// The fourth row is `completion: 'daemon'`: a command like `gjsify storybook
|
|
30
|
+
// --watch` neither exits nor unwinds, so an armed loop is not a leak there but the
|
|
31
|
+
// thing keeping it alive. It takes the handle through `onSpawn` and never awaits the
|
|
32
|
+
// completion promise. What it must NOT do is expect this spawn to arm that loop —
|
|
33
|
+
// `ensureMainLoop()` declines at `GLib.main_depth() !== 0`, and a supervisor reaches
|
|
34
|
+
// its spawn from a continuation GJS resumed inside the main-context spin it runs
|
|
35
|
+
// while the entry module's top-level await is pending, i.e. at depth 1. A daemon
|
|
36
|
+
// holds the loop itself, through `holdMainLoop()` (`utils/watch-loop.ts` does).
|
|
27
37
|
//
|
|
28
38
|
// which is why `gjsify foreach`/`workspace`/`check` and `runGjsBundle` run hundreds
|
|
29
39
|
// of async-spawned children under GJS without hanging: each ends its handler with
|
|
@@ -31,19 +41,36 @@
|
|
|
31
41
|
// REQUIRED {@link SpawnToCompletionOptions.completion} field — a new call site must
|
|
32
42
|
// state which side of the table it is on rather than inherit a wrong default.
|
|
33
43
|
//
|
|
34
|
-
// ##
|
|
44
|
+
// ## Under `packages/infra/cli/src/`, a raw async `spawn` is never fine
|
|
45
|
+
//
|
|
46
|
+
// The contract above binds only code that USES this helper, and one import bypassed
|
|
47
|
+
// it for years — `utils/run-lifecycle-script.ts` reached for `node:child_process`
|
|
48
|
+
// while its own header asserted the opposite, and `gjsify pack` parked for 5m30s
|
|
49
|
+
// after finishing its work on any package with a `prepack`, the production
|
|
50
|
+
// `gjsify run publish:app` chain included (#1010).
|
|
51
|
+
//
|
|
52
|
+
// So the rule is enforced rather than written down: `scripts/check-spawn-teardown-
|
|
53
|
+
// contract.mjs` fails on a value import of async `spawn` from `node:child_process`
|
|
54
|
+
// anywhere under `packages/infra/cli/src/` outside this file, unless the site
|
|
55
|
+
// carries an entry in `scripts/spawn-teardown-exceptions.mjs`. Deliberately narrow
|
|
56
|
+
// to async `spawn`: `spawnSync` blocks and arms nothing, and `exec`/`execFile` stay
|
|
57
|
+
// out until the `gjsify gsettings` observation (an `execFile` command that compiles
|
|
58
|
+
// a schema and exits in 0.45 s although `_execImpl` also calls `ensureMainLoop`) is
|
|
59
|
+
// explained rather than generalised from.
|
|
60
|
+
//
|
|
61
|
+
// ## What is spawned still matters — the Windows rewrite
|
|
35
62
|
//
|
|
36
|
-
// This helper also carries the Windows command rewrite (`forWin32`),
|
|
37
|
-
//
|
|
63
|
+
// This helper also carries the Windows command rewrite (`forWin32`), which is a
|
|
64
|
+
// second reason to come through it, and the one that applies OUTSIDE the CLI too:
|
|
38
65
|
//
|
|
39
66
|
// • a real executable (`git`, `gh`, `node`, `gjs`, `flatpak`, `process.execPath`)
|
|
40
|
-
//
|
|
67
|
+
// resolves on Windows via `.exe`, so a rewrite is not what it needs.
|
|
41
68
|
// • `shell: true` is fine — Node routes it through `%COMSPEC%`, which finds
|
|
42
69
|
// `.cmd` shims.
|
|
43
70
|
// • a PACKAGE MANAGER or any `node_modules/.bin` entry by BARE NAME (`npm`,
|
|
44
|
-
// `yarn`, `npx`, `tsc`, `oxlint`, a `pm` from `detectPackageManager()`)
|
|
45
|
-
//
|
|
46
|
-
// ENOENT and `spawn('npm.cmd')` is EINVAL
|
|
71
|
+
// `yarn`, `npx`, `tsc`, `oxlint`, a `pm` from `detectPackageManager()`) cannot
|
|
72
|
+
// work without it: on Windows those are `.cmd` shims, where `spawn('npm')` is
|
|
73
|
+
// ENOENT and `spawn('npm.cmd')` is EINVAL.
|
|
47
74
|
//
|
|
48
75
|
// The third case is not hypothetical, and each instance was invisible to Linux and
|
|
49
76
|
// macOS CI: raw spawns left `commands/check.ts` exiting 1 with no diagnostic,
|
package/lib/utils/system-gi.d.ts
CHANGED
|
@@ -9,6 +9,30 @@ export interface SystemGiOptions {
|
|
|
9
9
|
/** `pkg-config`'s `.pc` search directories; defaults to {@link pkgConfigSearchDirs}. */
|
|
10
10
|
searchDirs?: (env: Record<string, string | undefined>) => string[];
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Prefixes a macOS GI stack is installed under in practice — probed, never
|
|
14
|
+
* assumed: each is accepted only when it actually holds a `girepository-1.0/`.
|
|
15
|
+
*
|
|
16
|
+
* Load-bearing rather than belt-and-braces: `brew install gtk4` installs RUNTIME
|
|
17
|
+
* dependencies only, so a user running a published showcase routinely has the
|
|
18
|
+
* whole GTK stack and no `pkg-config` at all (a BUILD dependency of those
|
|
19
|
+
* formulae). The pkg-config source below is the general mechanism — it finds a
|
|
20
|
+
* jhbuild or otherwise bespoke prefix — but it cannot be the only one.
|
|
21
|
+
*
|
|
22
|
+
* Exported because a `--app gjs` BUNDLE has to carry the candidates rather than this
|
|
23
|
+
* function's answer: it runs on a host the build machine never saw, so the probe moves
|
|
24
|
+
* into the emitted prologue (`gi-runtime-paths.ts`). `@gjsify/utils`' copy of this rule
|
|
25
|
+
* exports it too; node-gi's does not, and the mirror suite compares OUTPUTS, not surface.
|
|
26
|
+
*/
|
|
27
|
+
export declare const PROBED_GI_LIBDIRS: Record<string, readonly string[]>;
|
|
28
|
+
/**
|
|
29
|
+
* The subdir GI installs typelibs into — historically `1.0` even for the girepository-2.0 API.
|
|
30
|
+
*
|
|
31
|
+
* Exported so `gi-typelib.ts` shares it rather than keeping a third copy of the
|
|
32
|
+
* one string this whole layout rule turns on. Both mirrors export it, so the
|
|
33
|
+
* agreement suite still compares two files that differ only in language.
|
|
34
|
+
*/
|
|
35
|
+
export declare const TYPELIB_SUBDIR = "girepository-1.0";
|
|
12
36
|
/** Split an OS search-path variable, dropping empty segments. */
|
|
13
37
|
export declare function splitSearchPath(value: string | undefined, separator?: string): string[];
|
|
14
38
|
/**
|
package/lib/utils/system-gi.js
CHANGED
|
@@ -62,12 +62,23 @@ const { basename, dirname, join } = posix;
|
|
|
62
62
|
* whole GTK stack and no `pkg-config` at all (a BUILD dependency of those
|
|
63
63
|
* formulae). The pkg-config source below is the general mechanism — it finds a
|
|
64
64
|
* jhbuild or otherwise bespoke prefix — but it cannot be the only one.
|
|
65
|
+
*
|
|
66
|
+
* Exported because a `--app gjs` BUNDLE has to carry the candidates rather than this
|
|
67
|
+
* function's answer: it runs on a host the build machine never saw, so the probe moves
|
|
68
|
+
* into the emitted prologue (`gi-runtime-paths.ts`). `@gjsify/utils`' copy of this rule
|
|
69
|
+
* exports it too; node-gi's does not, and the mirror suite compares OUTPUTS, not surface.
|
|
65
70
|
*/
|
|
66
|
-
const PROBED_GI_LIBDIRS = {
|
|
71
|
+
export const PROBED_GI_LIBDIRS = {
|
|
67
72
|
darwin: ['/opt/homebrew/lib', '/usr/local/lib', '/opt/local/lib'],
|
|
68
73
|
};
|
|
69
|
-
/**
|
|
70
|
-
|
|
74
|
+
/**
|
|
75
|
+
* The subdir GI installs typelibs into — historically `1.0` even for the girepository-2.0 API.
|
|
76
|
+
*
|
|
77
|
+
* Exported so `gi-typelib.ts` shares it rather than keeping a third copy of the
|
|
78
|
+
* one string this whole layout rule turns on. Both mirrors export it, so the
|
|
79
|
+
* agreement suite still compares two files that differ only in language.
|
|
80
|
+
*/
|
|
81
|
+
export const TYPELIB_SUBDIR = 'girepository-1.0';
|
|
71
82
|
/** Memoized `pkg-config` answer: the spawn happens at most once per process. */
|
|
72
83
|
let pkgConfigDirsCache = null;
|
|
73
84
|
/** Split an OS search-path variable, dropping empty segments. */
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
import { type ExampleRuntime } from './runtimes.js';
|
|
3
|
+
/**
|
|
4
|
+
* Start a child the caller SUPERVISES rather than waits for.
|
|
5
|
+
*
|
|
6
|
+
* `completion: 'daemon'` is the third row of the teardown table: a watch loop
|
|
7
|
+
* neither exits nor unwinds, so the armed GJS main loop is not a leak here —
|
|
8
|
+
* it is what keeps the session alive. The handle arrives synchronously through
|
|
9
|
+
* `onSpawn`; the completion promise is deliberately never awaited (it settles
|
|
10
|
+
* when the child is killed for the next rebuild), only its REJECTION is routed
|
|
11
|
+
* out, so a missing interpreter still surfaces as a rebuild failure.
|
|
12
|
+
*/
|
|
13
|
+
export declare function spawnSupervised(cmd: string, cmdArgs: string[], env?: NodeJS.ProcessEnv): Promise<ChildProcess>;
|
|
14
|
+
/**
|
|
15
|
+
* Launch a built bundle as a supervised child on `runtime`, wired the way
|
|
16
|
+
* `gjsify run` would launch it for real: `gjs -m` for the `--app gjs` bundle,
|
|
17
|
+
* the `--app node` bundle on node/bun/deno, and in BOTH cases the native
|
|
18
|
+
* typelib/library environment for any `@gjsify` native package the bundle
|
|
19
|
+
* reaches. The env is not optional on the gjs arm either — a watch loop that
|
|
20
|
+
* skipped it would run an app that `gjsify run` can start and this cannot.
|
|
21
|
+
*
|
|
22
|
+
* `subject` names what is being run in the node-gi diagnostic ("the storybook",
|
|
23
|
+
* "this project").
|
|
24
|
+
*/
|
|
25
|
+
export declare function spawnBundleSupervised(runtime: ExampleRuntime, bundlePath: string, cwd: string, subject: string): Promise<ChildProcess>;
|
|
26
|
+
export interface WatchLoopOptions {
|
|
27
|
+
/** Directory watched recursively. */
|
|
28
|
+
dir: string;
|
|
29
|
+
/** How `dir` is spelled in the loop's own log lines. Defaults to `dir`. */
|
|
30
|
+
dirLabel?: string;
|
|
31
|
+
/** Prefix for the loop's log lines: `[<label>] …`. */
|
|
32
|
+
label: string;
|
|
33
|
+
/** Regenerate derived inputs before each build (e.g. a generated entry). */
|
|
34
|
+
prepare?: () => void | Promise<void>;
|
|
35
|
+
/** Build the bundle. Runtime-agnostic — an in-process re-entry of `build`. */
|
|
36
|
+
build: () => Promise<void>;
|
|
37
|
+
/** Launch the freshly built bundle, or `null` to rebuild without launching. */
|
|
38
|
+
spawnChild: (() => Promise<ChildProcess>) | null;
|
|
39
|
+
/**
|
|
40
|
+
* Absolute path the build WRITES, when one is known. Changes to it never
|
|
41
|
+
* trigger a rebuild — see {@link isSelfWrite} for why the loop otherwise
|
|
42
|
+
* feeds itself.
|
|
43
|
+
*/
|
|
44
|
+
output?: string;
|
|
45
|
+
/** Quiet window after a change before rebuilding. */
|
|
46
|
+
debounceMs?: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Whether a watcher event is the loop's OWN output rather than a source edit.
|
|
50
|
+
*
|
|
51
|
+
* Without this the loop feeds itself whenever the bundle lands inside the
|
|
52
|
+
* watched tree, which is the DEFAULT arrangement rather than an exotic one: the
|
|
53
|
+
* watch dir defaults to the entry point's directory, so `gjsify dev index.ts` on
|
|
54
|
+
* a project building to `dist/` watches the project root and every build it runs
|
|
55
|
+
* is a change under it. Measured before this filter: one edit to `index.ts`
|
|
56
|
+
* started a rebuild every ~5 s that never stopped.
|
|
57
|
+
*
|
|
58
|
+
* Two shapes, because a build writes more than the file it is named after:
|
|
59
|
+
*
|
|
60
|
+
* - the bundle itself and anything sharing its stem (`out.mjs.map`), which the
|
|
61
|
+
* bundler writes beside it in the same pass;
|
|
62
|
+
* - the whole output DIRECTORY — but only when it is not the watched directory
|
|
63
|
+
* itself. `--outfile dist/out.mjs` under a watch dir of `.` puts chunks and
|
|
64
|
+
* assets there too; an `--outfile` written straight into the watched dir has
|
|
65
|
+
* no directory to exclude that would not also exclude the sources.
|
|
66
|
+
*
|
|
67
|
+
* `filename` is what `fs.watch` reports, relative to `dir` — `null` on the
|
|
68
|
+
* platforms that cannot name the changed path, where the honest answer is "no
|
|
69
|
+
* idea", i.e. rebuild.
|
|
70
|
+
*
|
|
71
|
+
* BOTH operands are resolved before they are compared. Resolving only the
|
|
72
|
+
* changed path is right on POSIX for as long as every caller happens to pass an
|
|
73
|
+
* already-absolute `output`, and silently wrong the moment one does not: on
|
|
74
|
+
* win32 `resolve()` also rewrites the separators and picks up the drive, so the
|
|
75
|
+
* two sides stop being the same spelling of the same file and every self-write
|
|
76
|
+
* reads as a source edit — which is the feedback loop this filter exists to
|
|
77
|
+
* stop. Caught by the win32 leg; on Linux all five cases passed.
|
|
78
|
+
*/
|
|
79
|
+
export declare function isSelfWrite(dir: string, output: string | undefined, filename: string | null): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Run the loop: build + launch once, then rebuild + relaunch on every change
|
|
82
|
+
* under `dir`. Returns once the watcher is armed; the process is then held open
|
|
83
|
+
* — by the watcher handle on Node, by the GLib main loop on GJS — so there is
|
|
84
|
+
* nothing left to await. That the RESOLUTION is not completion is the one thing
|
|
85
|
+
* a caller has to know, which is why it is also declared through
|
|
86
|
+
* `markDaemonCommand()` for the callers that cannot see this signature.
|
|
87
|
+
*
|
|
88
|
+
* A build failure is reported and the loop KEEPS WATCHING: the next save is the
|
|
89
|
+
* fix, and a dev loop that dies on a syntax error is a dev loop nobody uses.
|
|
90
|
+
*/
|
|
91
|
+
export declare function runWatchLoop(opts: WatchLoopOptions): Promise<void>;
|