@gjsify/cli 0.51.1 → 0.52.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 +505 -490
- package/lib/cli-app.js +3 -1
- package/lib/commands/affected-classify.d.ts +7 -1
- package/lib/commands/affected-classify.js +66 -9
- package/lib/commands/affected-script-refs.d.ts +20 -0
- package/lib/commands/affected-script-refs.js +218 -0
- package/lib/commands/affected.js +24 -2
- package/lib/commands/flatpak/diff.js +8 -2
- package/lib/commands/flatpak/init.js +4 -1
- package/lib/commands/flatpak/sources.js +4 -3
- package/lib/commands/flatpak/sync-flathub.d.ts +2 -0
- package/lib/commands/flatpak/sync-flathub.js +149 -7
- package/lib/commands/flatpak/utils.d.ts +10 -0
- package/lib/commands/flatpak/utils.js +10 -0
- package/lib/commands/index.d.ts +1 -0
- package/lib/commands/index.js +1 -0
- package/lib/commands/install.js +76 -0
- package/lib/commands/link.d.ts +13 -0
- package/lib/commands/link.js +243 -0
- package/lib/commands/onboard.js +18 -2
- package/lib/commands/publish.d.ts +34 -0
- package/lib/commands/publish.js +153 -8
- package/lib/commands/ship.js +19 -1
- package/lib/types/config-data.d.ts +1 -1
- package/lib/utils/check-system-deps.d.ts +1 -17
- package/lib/utils/check-system-deps.js +10 -68
- package/lib/utils/dev-link.d.ts +222 -0
- package/lib/utils/dev-link.js +632 -0
- package/lib/utils/hang-watchdog.d.ts +95 -0
- package/lib/utils/hang-watchdog.js +194 -0
- package/lib/utils/install-backend-native.d.ts +14 -0
- package/lib/utils/install-backend-native.js +91 -13
- package/lib/utils/install-extraneous.d.ts +90 -0
- package/lib/utils/install-extraneous.js +120 -0
- package/lib/utils/install-tarball-cache.d.ts +37 -9
- package/lib/utils/install-tarball-cache.js +59 -14
- package/lib/utils/normalize-bundler-options.js +11 -0
- package/lib/utils/publish-pins.d.ts +38 -0
- package/lib/utils/publish-pins.js +47 -0
- package/lib/utils/publish-readback.d.ts +103 -17
- package/lib/utils/publish-readback.js +326 -47
- package/lib/utils/run-gjs.js +82 -6
- package/lib/utils/ship/icns.d.ts +14 -0
- package/lib/utils/ship/icns.js +83 -0
- package/lib/utils/ship/ico.d.ts +25 -0
- package/lib/utils/ship/ico.js +120 -0
- package/lib/utils/ship/icons.d.ts +191 -0
- package/lib/utils/ship/icons.js +398 -0
- package/lib/utils/ship/launcher.js +16 -1
- package/lib/utils/ship/layout.d.ts +32 -0
- package/lib/utils/ship/layout.js +112 -38
- package/lib/utils/ship/msi.d.ts +1 -1
- package/lib/utils/ship/msi.js +30 -5
- package/lib/utils/ship/pe-launcher.d.ts +18 -0
- package/lib/utils/ship/pe-launcher.js +125 -4
- package/lib/utils/ship/plan.d.ts +1 -34
- package/lib/utils/ship/plan.js +7 -56
- package/lib/utils/ship/plist.d.ts +20 -10
- package/lib/utils/ship/plist.js +29 -12
- package/lib/utils/ship/settings.js +3 -2
- package/package.json +20 -20
package/lib/cli-app.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import yargs from 'yargs';
|
|
19
19
|
import { classifyCliFailure } from './cli-fail.js';
|
|
20
20
|
import { cliVersion } from './utils/publish-headers.js';
|
|
21
|
-
import { buildCommand as build, clearCommand as clear, copyCommand as copy, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, shipCommand as ship, dlxCommand as dlx, installCommand as install, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, pruneCommand as prune, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, devCommand as dev, debugCommand as debug, browseCommand as browse, } from './commands/index.js';
|
|
21
|
+
import { buildCommand as build, clearCommand as clear, copyCommand as copy, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, shipCommand as ship, dlxCommand as dlx, installCommand as install, linkCommand as link, unlinkCommand as unlink, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, pruneCommand as prune, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, devCommand as dev, debugCommand as debug, browseCommand as browse, } from './commands/index.js';
|
|
22
22
|
import { APP_NAME } from './constants.js';
|
|
23
23
|
import { isBun, isDeno, isNode, gjsSystemVersion } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
24
24
|
import { ensureGjsifyShimOnPath } from './utils/gjsify-shim.js';
|
|
@@ -93,6 +93,8 @@ export async function runCli(argv) {
|
|
|
93
93
|
.wrap(cli.terminalWidth())
|
|
94
94
|
.command(create.command, create.description, create.builder, create.handler)
|
|
95
95
|
.command(install.command, install.description, install.builder, install.handler)
|
|
96
|
+
.command(link.command, link.description, link.builder, link.handler)
|
|
97
|
+
.command(unlink.command, unlink.description, unlink.builder, unlink.handler)
|
|
96
98
|
.command(build.command, build.description, build.builder, build.handler)
|
|
97
99
|
.command(clear.command, clear.description, clear.builder, clear.handler)
|
|
98
100
|
.command(copy.command, copy.description, copy.builder, copy.handler)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Workspace } from '@gjsify/workspace';
|
|
2
|
+
import type { ScriptReferrers } from './affected-script-refs.js';
|
|
2
3
|
export interface ClassifyResult {
|
|
3
4
|
/** Cannot decide precisely — caller should run full suite. */
|
|
4
5
|
global: boolean;
|
|
@@ -13,4 +14,9 @@ export interface ClassifyResult {
|
|
|
13
14
|
/** Diff was empty AND no triggers fired → CI can skip the whole job. */
|
|
14
15
|
skipAll: boolean;
|
|
15
16
|
}
|
|
16
|
-
|
|
17
|
+
/**
|
|
18
|
+
* @param readersOf who reads a root `scripts/*` file. Omitted, a changed root script stays
|
|
19
|
+
* `unmatched` and forces the full run — the behaviour before the lookup existed, and what
|
|
20
|
+
* the command falls back to when it cannot list the tree.
|
|
21
|
+
*/
|
|
22
|
+
export declare function classifyAndExpand(workspaces: readonly Workspace[], changedFiles: readonly string[], readersOf?: ScriptReferrers): ClassifyResult;
|
|
@@ -49,8 +49,10 @@ const GLOBAL_TRIGGERS = [
|
|
|
49
49
|
*/
|
|
50
50
|
const OTHER_WORKFLOW_INPUTS = [
|
|
51
51
|
// Each sibling workflow's own definition. `main.yml` is deliberately absent: it
|
|
52
|
-
// is a GLOBAL_TRIGGER instead.
|
|
53
|
-
|
|
52
|
+
// is a GLOBAL_TRIGGER instead. The three OS-suite workflows run unfiltered on every
|
|
53
|
+
// PR, so an edit to one is measured by that run; `webview2-probe.yml` is absent on
|
|
54
|
+
// purpose, being dispatch-only — nothing would measure an edit to it on the PR.
|
|
55
|
+
/^\.github\/workflows\/(deploy-docs|commitlint|release|release-cut|audit-runtimes|prebuilds|node-gi|napi|cli-cross-platform|build-ci-image|cancel-pr-runs|macos-suites|windows-suites|gtk-os-suites)\.yml$/,
|
|
54
56
|
// `prebuilds.yml`'s toolchain (#838): the QEMU-emulated build script and the
|
|
55
57
|
// classifier deciding which native packages a prebuild run rebuilds. It is in
|
|
56
58
|
// both of that workflow's `paths:` filters; `main.yml` builds no prebuild and
|
|
@@ -168,7 +170,51 @@ const SCRIPT_COUPLINGS = [
|
|
|
168
170
|
why: 'tests/e2e/react-native-devtools is the only external observer of AppRegistry.runApplication',
|
|
169
171
|
},
|
|
170
172
|
];
|
|
171
|
-
|
|
173
|
+
/** A root script whose readers can stand in for it (see `affected-script-refs.ts`). */
|
|
174
|
+
const ROOT_SCRIPT = /^scripts\/[^/]+$/;
|
|
175
|
+
function isResolvableScript(f) {
|
|
176
|
+
return (ROOT_SCRIPT.test(f) &&
|
|
177
|
+
!GLOBAL_TRIGGERS.some((re) => re.test(f)) &&
|
|
178
|
+
!SCRIPT_COUPLINGS.some((c) => c.match.test(f)));
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Replace each changed root script by the files that read it, recursively through scripts
|
|
182
|
+
* that read scripts. Readers go back through IGNORE here and through GLOBAL / workspace
|
|
183
|
+
* mapping in the caller, so a reader this classifier does not model still forces the full
|
|
184
|
+
* run, and a script a GLOBAL_TRIGGER reads (`audit-runtimes.mjs`) stays in and fires it.
|
|
185
|
+
*/
|
|
186
|
+
function resolveRootScripts(files, readersOf) {
|
|
187
|
+
const out = new Set();
|
|
188
|
+
const queue = [];
|
|
189
|
+
for (const f of files) {
|
|
190
|
+
if (isResolvableScript(f))
|
|
191
|
+
queue.push(f);
|
|
192
|
+
else
|
|
193
|
+
out.add(f);
|
|
194
|
+
}
|
|
195
|
+
const seen = new Set();
|
|
196
|
+
while (queue.length > 0) {
|
|
197
|
+
const s = queue.shift();
|
|
198
|
+
if (seen.has(s))
|
|
199
|
+
continue;
|
|
200
|
+
seen.add(s);
|
|
201
|
+
for (const r of readersOf(s)) {
|
|
202
|
+
if (IGNORE.some((re) => re.test(r)))
|
|
203
|
+
continue;
|
|
204
|
+
if (isResolvableScript(r))
|
|
205
|
+
queue.push(r);
|
|
206
|
+
else
|
|
207
|
+
out.add(r);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { files: [...out], scripts: seen.size };
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* @param readersOf who reads a root `scripts/*` file. Omitted, a changed root script stays
|
|
214
|
+
* `unmatched` and forces the full run — the behaviour before the lookup existed, and what
|
|
215
|
+
* the command falls back to when it cannot list the tree.
|
|
216
|
+
*/
|
|
217
|
+
export function classifyAndExpand(workspaces, changedFiles, readersOf) {
|
|
172
218
|
const files = changedFiles.map((f) => f.replace(/\\/g, '/')).filter((f) => f.length > 0);
|
|
173
219
|
if (files.length === 0) {
|
|
174
220
|
return {
|
|
@@ -182,16 +228,26 @@ export function classifyAndExpand(workspaces, changedFiles) {
|
|
|
182
228
|
}
|
|
183
229
|
// Ignored files go FIRST, before the global-trigger check: ignore wins over
|
|
184
230
|
// global, so `packages/infra/cli/README.md` must not force a full run.
|
|
185
|
-
|
|
231
|
+
let remaining = [];
|
|
186
232
|
for (const f of files) {
|
|
187
233
|
if (IGNORE.some((re) => re.test(f)))
|
|
188
234
|
continue;
|
|
189
235
|
remaining.push(f);
|
|
190
236
|
}
|
|
237
|
+
// Every verdict below says when it was reached through a script's readers, so a
|
|
238
|
+
// surprising selection can be traced back to the lookup rather than the diff.
|
|
239
|
+
let note = '';
|
|
240
|
+
if (readersOf) {
|
|
241
|
+
const resolved = resolveRootScripts(remaining, readersOf);
|
|
242
|
+
if (resolved.scripts > 0) {
|
|
243
|
+
remaining = resolved.files;
|
|
244
|
+
note = `; ${resolved.scripts} root script(s) resolved to their readers`;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
191
247
|
if (remaining.length === 0) {
|
|
192
248
|
return {
|
|
193
249
|
global: false,
|
|
194
|
-
reason: 'ignored-only',
|
|
250
|
+
reason: 'ignored-only' + note,
|
|
195
251
|
workspaces: [],
|
|
196
252
|
runE2E: false,
|
|
197
253
|
runIntegration: false,
|
|
@@ -204,7 +260,7 @@ export function classifyAndExpand(workspaces, changedFiles) {
|
|
|
204
260
|
if (re.test(f)) {
|
|
205
261
|
return {
|
|
206
262
|
global: true,
|
|
207
|
-
reason: `global-trigger ${re.source} matched ${f}`,
|
|
263
|
+
reason: `global-trigger ${re.source} matched ${f}${note}`,
|
|
208
264
|
workspaces: workspaces.map((w) => w.name),
|
|
209
265
|
runE2E: true,
|
|
210
266
|
runIntegration: true,
|
|
@@ -254,7 +310,7 @@ export function classifyAndExpand(workspaces, changedFiles) {
|
|
|
254
310
|
if (stillUnmatched.length > 0) {
|
|
255
311
|
return {
|
|
256
312
|
global: true,
|
|
257
|
-
reason: `unmatched files (${stillUnmatched.length}): ${stillUnmatched.slice(0, 3).join(', ')}${stillUnmatched.length > 3 ? '…' : ''}`,
|
|
313
|
+
reason: `unmatched files (${stillUnmatched.length}): ${stillUnmatched.slice(0, 3).join(', ')}${stillUnmatched.length > 3 ? '…' : ''}${note}`,
|
|
258
314
|
workspaces: workspaces.map((w) => w.name),
|
|
259
315
|
runE2E: true,
|
|
260
316
|
runIntegration: true,
|
|
@@ -274,7 +330,7 @@ export function classifyAndExpand(workspaces, changedFiles) {
|
|
|
274
330
|
const touchedIntegration = remaining.some((f) => f.startsWith('tests/integration/'));
|
|
275
331
|
return {
|
|
276
332
|
global: false,
|
|
277
|
-
reason: `test-only (${remaining.length} file(s) in ${only})`,
|
|
333
|
+
reason: `test-only (${remaining.length} file(s) in ${only})${note}`,
|
|
278
334
|
workspaces: [only],
|
|
279
335
|
// `|| couplingRunE2E`: this shortcut skips CLOSURE EXPANSION, and a
|
|
280
336
|
// declared tier is not part of the closure. Dropping it here made a
|
|
@@ -323,7 +379,8 @@ export function classifyAndExpand(workspaces, changedFiles) {
|
|
|
323
379
|
return {
|
|
324
380
|
global: false,
|
|
325
381
|
reason: `closure (${closure.size} ws from ${matched.size} seed(s))` +
|
|
326
|
-
(couplingSeeds.size > 0 ? `, ${couplingSeeds.size} via script-coupling` : '')
|
|
382
|
+
(couplingSeeds.size > 0 ? `, ${couplingSeeds.size} via script-coupling` : '') +
|
|
383
|
+
note,
|
|
327
384
|
workspaces: [...closure].sort(),
|
|
328
385
|
runE2E,
|
|
329
386
|
runIntegration,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Every file that names `scriptPath`, as repo-relative paths. */
|
|
2
|
+
export type ScriptReferrers = (scriptPath: string) => readonly string[];
|
|
3
|
+
/**
|
|
4
|
+
* The file-name pattern for a script: `adwaita-elements.mjs` is also read as
|
|
5
|
+
* `adwaita-elements.d.mts` (a TS import of the `.mjs` resolves to its declaration) and a
|
|
6
|
+
* `.d.mts` change is read through its `.mjs`. The leading boundary excludes `-` so
|
|
7
|
+
* `value-types.mjs` does not match `generate-value-types.mjs`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function scriptNamePattern(scriptPath: string): RegExp;
|
|
10
|
+
/**
|
|
11
|
+
* Build the reader lookup over `texts` (repo-relative path → raw content). Pure: the command
|
|
12
|
+
* feeds it `git ls-files`, the spec feeds it a fixture map.
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildScriptReferrers(texts: ReadonlyMap<string, string>): ScriptReferrers;
|
|
15
|
+
/**
|
|
16
|
+
* Every tracked text file under `rootDir`, for {@link buildScriptReferrers}. Throws when git
|
|
17
|
+
* cannot list the tree — the caller then classifies without the lookup, which is the old,
|
|
18
|
+
* conservative behaviour (a root script lands in `unmatched`).
|
|
19
|
+
*/
|
|
20
|
+
export declare function readTrackedTexts(rootDir: string): Map<string, string>;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Who READS a root `scripts/*` file — the edge the `affected` closure walk has no way to see.
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS. `scripts/` belongs to no workspace, so every change there used to land in
|
|
5
|
+
// `unmatched` and buy a FULL `main.yml` run. Measured over the 60 PRs before #1774: 33 went
|
|
6
|
+
// full for that reason alone, and most of them touched only a website or gallery gate that
|
|
7
|
+
// `audit-runtimes.yml` runs and `main.yml` never opens. The full run was never wrong — it was
|
|
8
|
+
// the classifier saying "I cannot tell" about a question the tree answers: a script matters to
|
|
9
|
+
// `main.yml` exactly when something `main.yml` builds or runs names it.
|
|
10
|
+
//
|
|
11
|
+
// So a changed root script is REPLACED by the files that name it, and those go through the
|
|
12
|
+
// ordinary IGNORE → GLOBAL → workspace mapping. A spec that imports it seeds its workspace; a
|
|
13
|
+
// workspace build script that runs it seeds that workspace; a sibling workflow that runs it is
|
|
14
|
+
// IGNOREd like the workflow itself; `.github/actions`, `tests/e2e/**` or anything else the
|
|
15
|
+
// classifier does not model still forces the full run. A script that names another script
|
|
16
|
+
// hands the question on (`affected-classify.ts` walks that recursively).
|
|
17
|
+
//
|
|
18
|
+
// THE MATCH IS DELIBERATELY LOOSE, because it may only fail towards running MORE. It is the
|
|
19
|
+
// script's file name (stem plus a JS/TS extension, `.d.mts` included), not an import
|
|
20
|
+
// statement: `join(root, 'scripts', 'x.mjs')` and `node scripts/x.mjs` are both reads, and a
|
|
21
|
+
// parser for "is this an import" would miss the first. The price is that an error message
|
|
22
|
+
// naming a script counts as a reader too — measured, that turns some script changes back
|
|
23
|
+
// into full runs, which is the direction this is allowed to be wrong in. Comment lines are
|
|
24
|
+
// dropped first; without that, the prose cross-references in this repository chain every
|
|
25
|
+
// script to every other one and the whole mechanism collapses back into "run everything".
|
|
26
|
+
//
|
|
27
|
+
// TWO READERS ARE NARROWED, and each narrowing is a claim about `main.yml`'s shape:
|
|
28
|
+
// · `main.yml` itself counts only from a job that (transitively) `needs: changes`. A job
|
|
29
|
+
// that does not — `tree-checks` today — runs on every PR whatever this classifier says,
|
|
30
|
+
// so a script it runs cannot be skipped by anything decided here.
|
|
31
|
+
// · the root `package.json` counts only when the script is named outside `scripts`, or by
|
|
32
|
+
// a lifecycle/build/test entry, or by an entry another root script or a gated `main.yml`
|
|
33
|
+
// job runs. The `check:*` conveniences that only a person or a sibling workflow invokes
|
|
34
|
+
// are not `main.yml` inputs.
|
|
35
|
+
//
|
|
36
|
+
// NOT SEEN, stated so nobody reads this as more: a reader that builds the file name at run
|
|
37
|
+
// time, and a tool that walks `scripts/` as a directory. The second shape exists
|
|
38
|
+
// (`generate-status.mjs`, `check-comment-budget.mjs`) but only in jobs outside `main.yml`'s
|
|
39
|
+
// gated set. Push-to-main and the nightly still run the FULL suite.
|
|
40
|
+
import { spawnSync } from 'node:child_process';
|
|
41
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
42
|
+
import { join, posix } from 'node:path';
|
|
43
|
+
const MAIN_WORKFLOW = '.github/workflows/main.yml';
|
|
44
|
+
const ROOT_MANIFEST = 'package.json';
|
|
45
|
+
/** Larger files are data (lockfile, generated corpora), never a script's reader. */
|
|
46
|
+
const MAX_TEXT_BYTES = 2 * 1024 * 1024;
|
|
47
|
+
/**
|
|
48
|
+
* A root package.json entry whose run is part of installing, building or testing the tree.
|
|
49
|
+
* Not `check:*`: those are single-gate conveniences (`node scripts/check-….mjs`), and the
|
|
50
|
+
* one `main.yml` does run (`check:examples`) is caught by name in the workflow instead.
|
|
51
|
+
*/
|
|
52
|
+
const LIFECYCLE_SCRIPT = /^(pre|post)?(install|prepare|build|test)(:|$)/;
|
|
53
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
54
|
+
/**
|
|
55
|
+
* Comment lines out: `#` for YAML, shell, git and Docker files, `//` + block-comment lines
|
|
56
|
+
* for everything else. `#` is not stripped from JS, where a line may open with a private
|
|
57
|
+
* field.
|
|
58
|
+
*/
|
|
59
|
+
function stripCommentLines(path, text) {
|
|
60
|
+
const hashComments = /\.(ya?ml|sh|bash|py|toml)$|(^|\/)\.git[a-z]*$|Dockerfile$/.test(path);
|
|
61
|
+
const comment = hashComments ? /^\s*#/ : /^\s*(\/\/|\/\*|\*)/;
|
|
62
|
+
return text
|
|
63
|
+
.split('\n')
|
|
64
|
+
.filter((line) => !comment.test(line))
|
|
65
|
+
.join('\n');
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The file-name pattern for a script: `adwaita-elements.mjs` is also read as
|
|
69
|
+
* `adwaita-elements.d.mts` (a TS import of the `.mjs` resolves to its declaration) and a
|
|
70
|
+
* `.d.mts` change is read through its `.mjs`. The leading boundary excludes `-` so
|
|
71
|
+
* `value-types.mjs` does not match `generate-value-types.mjs`.
|
|
72
|
+
*/
|
|
73
|
+
export function scriptNamePattern(scriptPath) {
|
|
74
|
+
// A git path, always `/`-separated whatever the host: POSIX basename, not the host's.
|
|
75
|
+
const base = posix.basename(scriptPath);
|
|
76
|
+
const js = /^(.*?)(\.d)?\.[mc]?[jt]sx?$/.exec(base);
|
|
77
|
+
const tail = js ? `${escapeRe(js[1])}(\\.d)?\\.[mc]?[jt]sx?\\b` : `${escapeRe(base)}\\b`;
|
|
78
|
+
return new RegExp(`(^|[^\\w.-])${tail}`);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Jobs of `main.yml` that depend, directly or through `needs:`, on the `changes` job — the
|
|
82
|
+
* ones the classifier can switch off. Comment lines are already gone: `setup`'s comments
|
|
83
|
+
* NAME `needs.changes.outputs.*`, and counting that made `tree-checks` look gated.
|
|
84
|
+
*/
|
|
85
|
+
function gatedJobs(lines) {
|
|
86
|
+
const needs = new Map();
|
|
87
|
+
const mentionsChanges = new Set();
|
|
88
|
+
for (const { job, text } of lines) {
|
|
89
|
+
if (!job)
|
|
90
|
+
continue;
|
|
91
|
+
if (!needs.has(job))
|
|
92
|
+
needs.set(job, []);
|
|
93
|
+
const n = /^ {4}needs:\s*\[(.*)\]/.exec(text) ?? /^ {4}needs:\s*([A-Za-z0-9_-]+)\s*$/.exec(text);
|
|
94
|
+
if (n)
|
|
95
|
+
needs.set(job, n[1]
|
|
96
|
+
.split(',')
|
|
97
|
+
.map((d) => d.trim())
|
|
98
|
+
.filter(Boolean));
|
|
99
|
+
// A block-list `needs:` is a spelling this parse does not read, so the job counts
|
|
100
|
+
// as gated: misreading one here could only ever skip less.
|
|
101
|
+
if (/needs\.changes\b/.test(text) || /^ {4}needs:\s*$/.test(text))
|
|
102
|
+
mentionsChanges.add(job);
|
|
103
|
+
}
|
|
104
|
+
const gated = new Set(['changes', ...mentionsChanges]);
|
|
105
|
+
let grew = true;
|
|
106
|
+
while (grew) {
|
|
107
|
+
grew = false;
|
|
108
|
+
for (const [j, deps] of needs) {
|
|
109
|
+
if (!gated.has(j) && deps.some((d) => gated.has(d))) {
|
|
110
|
+
gated.add(j);
|
|
111
|
+
grew = true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// `changes` itself runs the classifier; it is not switched off by it.
|
|
116
|
+
gated.delete('changes');
|
|
117
|
+
return gated;
|
|
118
|
+
}
|
|
119
|
+
function workflowLines(text) {
|
|
120
|
+
const lines = text.split('\n');
|
|
121
|
+
const out = [];
|
|
122
|
+
let job;
|
|
123
|
+
let inJobs = false;
|
|
124
|
+
for (const line of lines) {
|
|
125
|
+
if (/^jobs:\s*$/.test(line))
|
|
126
|
+
inJobs = true;
|
|
127
|
+
else if (/^\S/.test(line))
|
|
128
|
+
inJobs = false;
|
|
129
|
+
const key = inJobs ? /^ {2}([A-Za-z0-9_-]+):\s*$/.exec(line) : null;
|
|
130
|
+
if (key)
|
|
131
|
+
job = key[1];
|
|
132
|
+
if (/^\s*#/.test(line))
|
|
133
|
+
continue;
|
|
134
|
+
out.push({ job: inJobs ? job : undefined, text: line });
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Build the reader lookup over `texts` (repo-relative path → raw content). Pure: the command
|
|
140
|
+
* feeds it `git ls-files`, the spec feeds it a fixture map.
|
|
141
|
+
*/
|
|
142
|
+
export function buildScriptReferrers(texts) {
|
|
143
|
+
const stripped = new Map();
|
|
144
|
+
for (const [path, text] of texts) {
|
|
145
|
+
if (path === MAIN_WORKFLOW || path === ROOT_MANIFEST)
|
|
146
|
+
continue;
|
|
147
|
+
stripped.set(path, stripCommentLines(path, text));
|
|
148
|
+
}
|
|
149
|
+
const workflowText = texts.get(MAIN_WORKFLOW);
|
|
150
|
+
const workflow = workflowText === undefined ? [] : workflowLines(workflowText);
|
|
151
|
+
const gated = gatedJobs(workflow);
|
|
152
|
+
// Top-level workflow keys (`env:`) reach every job, so they count as gated too.
|
|
153
|
+
const gatedWorkflowNames = (re) => workflow.some((l) => re.test(l.text) && (l.job === undefined || gated.has(l.job)));
|
|
154
|
+
let manifest;
|
|
155
|
+
const manifestText = texts.get(ROOT_MANIFEST);
|
|
156
|
+
if (manifestText !== undefined)
|
|
157
|
+
manifest = JSON.parse(manifestText);
|
|
158
|
+
const manifestReads = (re) => {
|
|
159
|
+
if (!manifest)
|
|
160
|
+
return false;
|
|
161
|
+
for (const [k, v] of Object.entries(manifest)) {
|
|
162
|
+
if (k !== 'scripts' && re.test(JSON.stringify(v)))
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
const scripts = manifest.scripts ?? {};
|
|
166
|
+
for (const [key, value] of Object.entries(scripts)) {
|
|
167
|
+
if (!re.test(value))
|
|
168
|
+
continue;
|
|
169
|
+
if (LIFECYCLE_SCRIPT.test(key))
|
|
170
|
+
return true;
|
|
171
|
+
const byName = new RegExp(`(^|[\\s"'])${escapeRe(key)}($|[\\s"'&;|])`);
|
|
172
|
+
if (Object.entries(scripts).some(([other, v]) => other !== key && byName.test(v)))
|
|
173
|
+
return true;
|
|
174
|
+
if (gatedWorkflowNames(byName))
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
};
|
|
179
|
+
return (scriptPath) => {
|
|
180
|
+
const re = scriptNamePattern(scriptPath);
|
|
181
|
+
const readers = [];
|
|
182
|
+
for (const [path, text] of stripped) {
|
|
183
|
+
if (path !== scriptPath && re.test(text))
|
|
184
|
+
readers.push(path);
|
|
185
|
+
}
|
|
186
|
+
if (gatedWorkflowNames(re))
|
|
187
|
+
readers.push(MAIN_WORKFLOW);
|
|
188
|
+
if (manifestReads(re))
|
|
189
|
+
readers.push(ROOT_MANIFEST);
|
|
190
|
+
return readers.sort();
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Every tracked text file under `rootDir`, for {@link buildScriptReferrers}. Throws when git
|
|
195
|
+
* cannot list the tree — the caller then classifies without the lookup, which is the old,
|
|
196
|
+
* conservative behaviour (a root script lands in `unmatched`).
|
|
197
|
+
*/
|
|
198
|
+
export function readTrackedTexts(rootDir) {
|
|
199
|
+
const r = spawnSync('git', ['ls-files', '-z'], { cwd: rootDir, encoding: 'utf8' });
|
|
200
|
+
if (r.status !== 0)
|
|
201
|
+
throw new Error(`git ls-files failed (${r.status}): ${String(r.stderr).trim()}`);
|
|
202
|
+
const texts = new Map();
|
|
203
|
+
for (const path of r.stdout.split('\0')) {
|
|
204
|
+
if (!path || path.startsWith('refs/'))
|
|
205
|
+
continue;
|
|
206
|
+
const abs = join(rootDir, path);
|
|
207
|
+
// A gitlink (submodule) is listed but is a directory; a deleted-but-staged path is
|
|
208
|
+
// listed and absent. Neither can name a script.
|
|
209
|
+
const st = statSync(abs, { throwIfNoEntry: false });
|
|
210
|
+
if (!st?.isFile() || st.size > MAX_TEXT_BYTES)
|
|
211
|
+
continue;
|
|
212
|
+
const text = readFileSync(abs, 'utf8');
|
|
213
|
+
if (text.includes('\0'))
|
|
214
|
+
continue;
|
|
215
|
+
texts.set(path, text);
|
|
216
|
+
}
|
|
217
|
+
return texts;
|
|
218
|
+
}
|
package/lib/commands/affected.js
CHANGED
|
@@ -58,6 +58,7 @@ import { appendFileSync } from 'node:fs';
|
|
|
58
58
|
import { readStdinLines } from '../utils/stdin.js';
|
|
59
59
|
import { discoverWorkspaces } from '@gjsify/workspace';
|
|
60
60
|
import { classifyAndExpand } from './affected-classify.js';
|
|
61
|
+
import { buildScriptReferrers, readTrackedTexts } from './affected-script-refs.js';
|
|
61
62
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
62
63
|
export const affectedCommand = {
|
|
63
64
|
command: 'affected',
|
|
@@ -92,15 +93,36 @@ export const affectedCommand = {
|
|
|
92
93
|
const changedFiles = args['changed-from-stdin']
|
|
93
94
|
? readStdinLines()
|
|
94
95
|
: runGitDiff(rootDir, args.base ?? 'origin/main', args.head ?? 'HEAD');
|
|
95
|
-
const result = classifyAndExpand(workspaces, changedFiles);
|
|
96
|
+
const result = classifyAndExpand(workspaces, changedFiles, scriptReadersFor(rootDir, changedFiles));
|
|
96
97
|
emit(args.format, result);
|
|
97
98
|
},
|
|
98
99
|
};
|
|
100
|
+
/**
|
|
101
|
+
* The root-script reader lookup, built only when the diff has a root script in it — reading
|
|
102
|
+
* the tree costs a second or two and most diffs do not need it. `undefined` when git cannot
|
|
103
|
+
* list the tree (a fixture without a repository): the classifier then treats a root script
|
|
104
|
+
* as it always did, as `unmatched`, which forces the full run.
|
|
105
|
+
*/
|
|
106
|
+
function scriptReadersFor(rootDir, changedFiles) {
|
|
107
|
+
if (!changedFiles.some((f) => /^scripts\/[^/]+$/.test(f.replace(/\\/g, '/'))))
|
|
108
|
+
return undefined;
|
|
109
|
+
try {
|
|
110
|
+
return buildScriptReferrers(readTrackedTexts(rootDir));
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
process.stderr.write(`gjsify affected: cannot index the tree for script readers (${String(err)}); root scripts stay unmatched\n`);
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
99
117
|
function runGitDiff(cwd, base, head) {
|
|
100
118
|
// `base...head` lists changed paths on `head` relative to the MERGE-BASE, which
|
|
101
119
|
// matches what a GitHub PR diff shows and survives stacked PRs without picking up
|
|
102
120
|
// commits from base.
|
|
103
|
-
|
|
121
|
+
// `--no-renames`: with rename detection on (git's default since 2.9), a pure move lists
|
|
122
|
+
// only the DESTINATION. Moving `packages/node/os/src/x.ts` to `docs/x.ts` then reads as
|
|
123
|
+
// a docs-only change and skips every suite that would have seen `@gjsify/os` break.
|
|
124
|
+
// Without detection a move is a delete plus an add, and the deleted side seeds as usual.
|
|
125
|
+
const r = spawnSync('git', ['diff', '--no-renames', '--name-only', `${base}...${head}`], {
|
|
104
126
|
cwd,
|
|
105
127
|
encoding: 'utf8',
|
|
106
128
|
});
|
|
@@ -125,10 +125,16 @@ async function loadFlathubSource(args, sourceIndex) {
|
|
|
125
125
|
const sources = modules[0]?.sources ?? [];
|
|
126
126
|
if (sources.length === 0)
|
|
127
127
|
return null;
|
|
128
|
-
const idx = sourceIndex ?? sources.findIndex((s) => s
|
|
128
|
+
const idx = sourceIndex ?? sources.findIndex((s) => isSourceObject(s) && s.type === 'git');
|
|
129
129
|
if (idx < 0 || idx >= sources.length)
|
|
130
130
|
return null;
|
|
131
|
-
|
|
131
|
+
// A `--source-index` aimed at a file reference has no tag to compare, which
|
|
132
|
+
// the caller already reports as a manifest without one.
|
|
133
|
+
const entry = sources[idx];
|
|
134
|
+
return isSourceObject(entry) ? entry : null;
|
|
135
|
+
}
|
|
136
|
+
function isSourceObject(entry) {
|
|
137
|
+
return typeof entry === 'object' && entry !== null;
|
|
132
138
|
}
|
|
133
139
|
async function fetchFlathubManifest(flathubRepo, appId, verbose) {
|
|
134
140
|
for (const branch of ['master', 'main']) {
|
|
@@ -153,7 +153,10 @@ export const flatpakInitCommand = {
|
|
|
153
153
|
// `flatpak.extraModules` — prepended to the meson default.
|
|
154
154
|
// Right shape for meson-built GTK apps
|
|
155
155
|
// that want a few extra sibling modules
|
|
156
|
-
// (e.g.
|
|
156
|
+
// (e.g. a GI library the runtime lacks). NOT
|
|
157
|
+
// blueprint-compiler: ADR 0053 clause 5 parses
|
|
158
|
+
// `.blp` in the bundler, so a manifest that
|
|
159
|
+
// builds it builds a tool nothing runs.
|
|
157
160
|
const modules = [];
|
|
158
161
|
if (flatpak.modules?.length) {
|
|
159
162
|
modules.push(...flatpak.modules);
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
20
20
|
import { dirname, join, resolve } from 'node:path';
|
|
21
21
|
import { Buffer } from 'node:buffer';
|
|
22
|
+
import { FLATPAK_SOURCES_FILE } from './utils.js';
|
|
22
23
|
/** Checksum algorithms flatpak-builder accepts on a `file` source. */
|
|
23
24
|
const FLATPAK_ALGOS = new Set(['sha256', 'sha512']);
|
|
24
25
|
/** Parse an SRI integrity (`sha512-<base64>`) into `{ algorithm, hex }`. Mirrors
|
|
@@ -219,7 +220,7 @@ export const flatpakSourcesCommand = {
|
|
|
219
220
|
.option('out', {
|
|
220
221
|
description: 'Output JSON sources file',
|
|
221
222
|
type: 'string',
|
|
222
|
-
default:
|
|
223
|
+
default: FLATPAK_SOURCES_FILE,
|
|
223
224
|
normalize: true,
|
|
224
225
|
})
|
|
225
226
|
.option('cache-root', {
|
|
@@ -277,7 +278,7 @@ export const flatpakSourcesCommand = {
|
|
|
277
278
|
}
|
|
278
279
|
// Stable order (by url) so the generated file diffs cleanly across runs.
|
|
279
280
|
const sources = [...byHex.values()].sort((a, b) => (a.url < b.url ? -1 : a.url > b.url ? 1 : 0));
|
|
280
|
-
const out = resolve(cwd, args.out ??
|
|
281
|
+
const out = resolve(cwd, args.out ?? FLATPAK_SOURCES_FILE);
|
|
281
282
|
mkdirSync(dirname(out), { recursive: true });
|
|
282
283
|
writeFileSync(out, JSON.stringify(sources, null, 2) + '\n');
|
|
283
284
|
console.log(`[gjsify flatpak sources] wrote ${out} — ${sources.length} tarball source(s) from ${type} lockfile` +
|
|
@@ -285,7 +286,7 @@ export const flatpakSourcesCommand = {
|
|
|
285
286
|
(skippedSri ? `, ${skippedSri} without a usable integrity skipped` : '') +
|
|
286
287
|
(skippedAlgo ? `, ${skippedAlgo} non-sha256/512 skipped` : ''));
|
|
287
288
|
if (args.printModule) {
|
|
288
|
-
console.error(buildModuleSnippet(args.out ??
|
|
289
|
+
console.error(buildModuleSnippet(args.out ?? FLATPAK_SOURCES_FILE, cacheRoot));
|
|
289
290
|
}
|
|
290
291
|
},
|
|
291
292
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Command } from '../../types/index.js';
|
|
2
2
|
interface SyncFlathubOptions {
|
|
3
3
|
version?: string;
|
|
4
|
+
sourcesFile?: string;
|
|
4
5
|
appId?: string;
|
|
5
6
|
flathubRepo?: string;
|
|
6
7
|
commit?: string;
|
|
@@ -22,5 +23,6 @@ export declare function editManifest(original: string, args: {
|
|
|
22
23
|
tag: string;
|
|
23
24
|
commit: string;
|
|
24
25
|
sourceIndex?: number;
|
|
26
|
+
sourcesFile?: string;
|
|
25
27
|
}): string;
|
|
26
28
|
export {};
|