@gjsify/cli 0.52.0 → 0.53.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 +329 -298
- package/lib/actions/build.d.ts +22 -0
- package/lib/actions/build.js +37 -9
- package/lib/bundler-pick.d.ts +37 -1
- package/lib/bundler-pick.js +108 -17
- package/lib/cli-app.js +5 -2
- 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 +3 -0
- package/lib/commands/index.js +3 -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/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 +42 -3
- 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/gzip.js +1 -1
- package/lib/utils/ship/icons.d.ts +6 -0
- package/lib/utils/ship/icons.js +6 -5
- 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 +36 -0
- package/lib/utils/win32-command.d.ts +17 -0
- package/lib/utils/win32-command.js +21 -6
- package/package.json +19 -19
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// `gjsify exec [--runtime <r>] [--rebuild] <bin> [args…]` — run an installed npm bin
|
|
2
|
+
// on the runtime gjsify itself is running on (ADR 0076).
|
|
3
|
+
//
|
|
4
|
+
// node / bun / deno host → the bin's npm entry, unchanged, on that runtime.
|
|
5
|
+
// gjs host → the package's own `gjsify.bin` bundle if it ships one,
|
|
6
|
+
// else the npm entry REBUILT `--app gjs` (Node APIs mapped
|
|
7
|
+
// to `@gjsify/*`, `.node` addons routed through
|
|
8
|
+
// `@gjsify/napi`), cached by content, run with `gjs -m`.
|
|
9
|
+
//
|
|
10
|
+
// A failed rebuild is an error that carries the bundler's diagnostics. There is no
|
|
11
|
+
// silent fallback to `node` under GJS: the host may have none, and a tool that
|
|
12
|
+
// "worked" on a runtime the user did not ask for hides exactly the gap this command
|
|
13
|
+
// exists to expose. `--runtime node` is the explicit, documented escape hatch.
|
|
14
|
+
import { existsSync, readdirSync, renameSync, rmSync } from 'node:fs';
|
|
15
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
16
|
+
import { pathToFileURL } from 'node:url';
|
|
17
|
+
import { hostRuntime } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
18
|
+
import { doubleDashArgs } from '../utils/double-dash-args.js';
|
|
19
|
+
import { ExecBinNotFoundError, ExecPlanError, entryWithinPackage, execArtifactPath, execCacheKey, execCacheRoot, findProjectLockfile, hashFile, planExec, resolveExecBin, splitExecArgv, } from '../utils/exec-bin.js';
|
|
20
|
+
import { cliPackageDir, cliVersion } from '../utils/publish-headers.js';
|
|
21
|
+
import { runGjsBundle } from '../utils/run-gjs.js';
|
|
22
|
+
import { runRuntimeBundle } from '../utils/run-node.js';
|
|
23
|
+
export class ExecRebuildError extends Error {
|
|
24
|
+
constructor(bin, entry, cause, verbose) {
|
|
25
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
26
|
+
super(`gjsify exec: rebuilding "${bin.binName}" (${bin.pkgName}@${bin.pkgVersion}) for GJS failed.\n` +
|
|
27
|
+
` entry: ${entry}\n` +
|
|
28
|
+
'Nothing was run — gjsify exec never falls back to another runtime on its own. To run it on Node ' +
|
|
29
|
+
`explicitly: \`gjsify exec --runtime node ${bin.binName}\`.\n` +
|
|
30
|
+
// The warnings were silenced for the rebuild; an error can point back at one
|
|
31
|
+
// (UNRESOLVED_IMPORT behind a load-check failure), so say how to get them.
|
|
32
|
+
(verbose ? '' : `Re-run with \`gjsify exec --verbose ${bin.binName}\` for the bundler's warnings.\n`) +
|
|
33
|
+
`Bundler diagnostics:\n${detail}`, { cause });
|
|
34
|
+
this.name = 'ExecRebuildError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** The nearest `node_modules` walking up from `start`, or null. */
|
|
38
|
+
function nearestNodeModules(start) {
|
|
39
|
+
let dir = resolve(start);
|
|
40
|
+
for (;;) {
|
|
41
|
+
const candidate = join(dir, 'node_modules');
|
|
42
|
+
if (existsSync(candidate))
|
|
43
|
+
return candidate;
|
|
44
|
+
const parent = dirname(dir);
|
|
45
|
+
if (parent === dir)
|
|
46
|
+
return null;
|
|
47
|
+
dir = parent;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Rebuild `entry` as a `--app gjs` bundle and return its path, reusing a cached one
|
|
52
|
+
* whose content key still matches (see `execCacheKey`).
|
|
53
|
+
*
|
|
54
|
+
* Written to a private temp directory and RENAMED into place, so two concurrent
|
|
55
|
+
* `gjsify exec` runs never read a half-written bundle: the loser of the rename race
|
|
56
|
+
* uses the winner's artifact, which has the same key and therefore the same inputs.
|
|
57
|
+
*/
|
|
58
|
+
async function rebuildForGjs(bin, entry, opts) {
|
|
59
|
+
const nodeModules = bin.nodeModules ?? nearestNodeModules(process.cwd()) ?? join(bin.pkgDir, 'node_modules');
|
|
60
|
+
const lockfile = findProjectLockfile(dirname(nodeModules));
|
|
61
|
+
const key = execCacheKey({
|
|
62
|
+
pkgName: bin.pkgName,
|
|
63
|
+
pkgVersion: bin.pkgVersion,
|
|
64
|
+
entry: entryWithinPackage(bin, entry),
|
|
65
|
+
cliVersion: cliVersion(),
|
|
66
|
+
lockfileHash: lockfile ? hashFile(lockfile) : null,
|
|
67
|
+
});
|
|
68
|
+
const outfile = execArtifactPath(execCacheRoot(nodeModules), bin, entry, bin.installed ? key : 'local');
|
|
69
|
+
const reusable = bin.installed && !opts.rebuild;
|
|
70
|
+
if (reusable && existsSync(outfile)) {
|
|
71
|
+
if (opts.verbose)
|
|
72
|
+
console.error(`gjsify exec: reusing ${outfile}`);
|
|
73
|
+
return outfile;
|
|
74
|
+
}
|
|
75
|
+
console.error(`gjsify exec: rebuilding ${bin.pkgName}@${bin.pkgVersion} "${bin.binName}" for GJS…`);
|
|
76
|
+
const finalDir = dirname(outfile);
|
|
77
|
+
const tmpDir = `${finalDir}.tmp-${process.pid.toString(16)}-${Date.now().toString(16)}`;
|
|
78
|
+
const tmpOut = join(tmpDir, basename(outfile));
|
|
79
|
+
try {
|
|
80
|
+
const { BuildAction } = await import('../actions/build.js');
|
|
81
|
+
await BuildAction.bundleFileForGjs(entry, tmpOut, {
|
|
82
|
+
verbose: opts.verbose,
|
|
83
|
+
// A bin is executed, never imported.
|
|
84
|
+
preserveDefaultExport: false,
|
|
85
|
+
// An installed dependency's `import.meta.url` is rewritten per module by
|
|
86
|
+
// the node_modules path rewriter; the project's OWN bin is not under
|
|
87
|
+
// node_modules, so its entry keeps its source URL the way `--node-script`
|
|
88
|
+
// does it.
|
|
89
|
+
...(bin.installed ? {} : { define: { 'import.meta.url': JSON.stringify(pathToFileURL(entry).href) } }),
|
|
90
|
+
// The CLI is bundling on the user's behalf: the project installed wxt,
|
|
91
|
+
// not `@gjsify/fs`, so the polyfills come from beside the running CLI.
|
|
92
|
+
toolchainAnchor: BuildAction.runningCliFile(),
|
|
93
|
+
// Every polyfill of a rebuilt bin comes from the toolchain, so the per-import
|
|
94
|
+
// "did not resolve from the project" notice is the rule here, not a symptom —
|
|
95
|
+
// dozens of lines ahead of `--version`. Warnings only: a failing build still
|
|
96
|
+
// rejects with its diagnostics, and `--verbose` shows them all.
|
|
97
|
+
...(opts.verbose ? {} : { logLevel: 'silent' }),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
102
|
+
throw new ExecRebuildError(bin, entry, err, opts.verbose);
|
|
103
|
+
}
|
|
104
|
+
if (reusable && existsSync(outfile)) {
|
|
105
|
+
// A concurrent run with the same key finished first; its artifact is ours.
|
|
106
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
107
|
+
return outfile;
|
|
108
|
+
}
|
|
109
|
+
// Whatever sits under this name now is stale: a forced rebuild, or a directory
|
|
110
|
+
// left by an older layout without the file this one expects.
|
|
111
|
+
rmSync(finalDir, { recursive: true, force: true });
|
|
112
|
+
try {
|
|
113
|
+
renameSync(tmpDir, finalDir);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
// Lost the race to a concurrent run with the same key — its bundle is ours.
|
|
117
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
118
|
+
if (!existsSync(outfile))
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
pruneStaleArtifacts(dirname(finalDir), bin, basename(finalDir));
|
|
122
|
+
return outfile;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Drop earlier artifacts of the SAME package version: a rebuild under a new CLI or a
|
|
126
|
+
* moved lockfile leaves the old key behind, and nothing else would ever read it.
|
|
127
|
+
* Best-effort — housekeeping must not fail the run it follows.
|
|
128
|
+
*/
|
|
129
|
+
function pruneStaleArtifacts(cacheRoot, bin, keep) {
|
|
130
|
+
const prefix = `${bin.pkgName}@${bin.pkgVersion}`.replace(/[^a-zA-Z0-9._@-]/g, '_');
|
|
131
|
+
let names;
|
|
132
|
+
try {
|
|
133
|
+
names = readdirSync(cacheRoot);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
for (const name of names) {
|
|
139
|
+
if (name === keep || !name.startsWith(`${prefix}-`) || name.includes('.tmp-'))
|
|
140
|
+
continue;
|
|
141
|
+
rmSync(join(cacheRoot, name), { recursive: true, force: true });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export const execCommand = {
|
|
145
|
+
command: 'exec <entries..>',
|
|
146
|
+
description: 'Run an installed npm bin on the runtime gjsify runs on: `gjsify exec [--runtime gjs|node|bun|deno] [--rebuild] [--verbose] <bin> [args…]`. ' +
|
|
147
|
+
'On Node/Bun/Deno the bin runs unchanged; on GJS it is rebuilt `--app gjs` once, cached under node_modules/.cache/gjsify/exec/, and run with gjs. ' +
|
|
148
|
+
"Options go BEFORE the bin; everything after it is the bin's own.",
|
|
149
|
+
builder: (yargs) => yargs
|
|
150
|
+
.positional('entries', {
|
|
151
|
+
description: 'gjsify exec options, then the bin name and its arguments.',
|
|
152
|
+
type: 'string',
|
|
153
|
+
array: true,
|
|
154
|
+
})
|
|
155
|
+
// The bin owns its whole flag namespace, as with `gjsify env`/`run`:
|
|
156
|
+
// `gjsify exec prettier --version` must reach prettier.
|
|
157
|
+
.version(false)
|
|
158
|
+
.help(false)
|
|
159
|
+
.parserConfiguration({
|
|
160
|
+
'populate--': true,
|
|
161
|
+
'parse-positional-numbers': false,
|
|
162
|
+
'unknown-options-as-args': true,
|
|
163
|
+
}),
|
|
164
|
+
handler: async (args) => {
|
|
165
|
+
let invocation;
|
|
166
|
+
let bin;
|
|
167
|
+
let plan;
|
|
168
|
+
try {
|
|
169
|
+
invocation = splitExecArgv(args.entries ?? [], doubleDashArgs(args));
|
|
170
|
+
bin = resolveExecBin(invocation.bin, process.cwd());
|
|
171
|
+
plan = planExec(bin, invocation.runtime ?? hostRuntime());
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
if (err instanceof ExecBinNotFoundError) {
|
|
175
|
+
console.error(err.message);
|
|
176
|
+
// The shell's "command not found", which is what this is.
|
|
177
|
+
return process.exit(127);
|
|
178
|
+
}
|
|
179
|
+
if (err instanceof ExecPlanError) {
|
|
180
|
+
console.error(err.message);
|
|
181
|
+
return process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
const launch = {
|
|
186
|
+
completion: 'exit',
|
|
187
|
+
exitOnSuccess: true,
|
|
188
|
+
quiet: !invocation.verbose,
|
|
189
|
+
quietExit: true,
|
|
190
|
+
};
|
|
191
|
+
if (plan.kind === 'direct') {
|
|
192
|
+
await runRuntimeBundle(plan.runtime, plan.file, invocation.args, launch);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (plan.kind === 'gjs-bundle') {
|
|
196
|
+
await runGjsBundle(plan.file, invocation.args, launch);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const bundle = await rebuildForGjs(bin, plan.entry, {
|
|
200
|
+
rebuild: invocation.rebuild,
|
|
201
|
+
verbose: invocation.verbose,
|
|
202
|
+
});
|
|
203
|
+
// The rebuild took its polyfills from beside the CLI, so their native prebuilds
|
|
204
|
+
// (typelibs, shared libraries) are found there too.
|
|
205
|
+
const cliDir = cliPackageDir();
|
|
206
|
+
await runGjsBundle(bundle, invocation.args, { ...launch, nativeRoots: cliDir ? [cliDir] : [] });
|
|
207
|
+
},
|
|
208
|
+
};
|
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';
|
|
@@ -40,3 +42,4 @@ export * from './storybook.js';
|
|
|
40
42
|
export * from './dev.js';
|
|
41
43
|
export * from './debug.js';
|
|
42
44
|
export * from './browse.js';
|
|
45
|
+
export { webextCommand } from './webext/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';
|
|
@@ -40,3 +42,4 @@ export * from './storybook.js';
|
|
|
40
42
|
export * from './dev.js';
|
|
41
43
|
export * from './debug.js';
|
|
42
44
|
export * from './browse.js';
|
|
45
|
+
export { webextCommand } from './webext/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/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
|