@gjsify/cli 0.51.0 → 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 +46 -4
- 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/appimage.d.ts +24 -4
- package/lib/utils/ship/appimage.js +51 -6
- package/lib/utils/ship/formats.js +44 -11
- 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/schemas.d.ts +46 -0
- package/lib/utils/ship/schemas.js +92 -11
- package/lib/utils/ship/settings.js +3 -2
- package/package.json +20 -20
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The half of `--immutable` that was missing: every locked package is present AND
|
|
3
|
+
* NOTHING ELSE IS.
|
|
4
|
+
*
|
|
5
|
+
* The installer only ever ADDED. Handed a `node_modules` carrying a package the
|
|
6
|
+
* lockfile never mentions, `--immutable` extracted whatever was missing, left the
|
|
7
|
+
* stranger in place and exited 0 — a green install over a tree its own lockfile does
|
|
8
|
+
* not describe.
|
|
9
|
+
*
|
|
10
|
+
* THE INCIDENT (gjsify#1683, 2026-09-15). CI caches `node_modules` keyed on
|
|
11
|
+
* `hashFiles('gjsify-lock.json')` with a prefix fallback `restore-keys:
|
|
12
|
+
* node-modules-v1-`. An earlier run of a `@girs` bump genuinely nested
|
|
13
|
+
* `node_modules/@girs/gtk-4.0/node_modules/@girs/graphene-1.0` and saved that tree.
|
|
14
|
+
* The fix push changed the lockfile → new key → miss → the FALLBACK restored the
|
|
15
|
+
* nested tree. `--immutable` added what was missing, pruned nothing, and `tsc` then
|
|
16
|
+
* saw one type under two identities: TS2883. Worse, the still-nested tree was saved
|
|
17
|
+
* under the NEW key, so no plain re-run could go green — the failure reproduced
|
|
18
|
+
* itself until the caches were deleted by hand. Nothing about the PR's code was
|
|
19
|
+
* wrong, and any lockfile-changing PR could poison itself the same way.
|
|
20
|
+
*
|
|
21
|
+
* WHY NAMING, NOT DELETING. The peers split: `npm ci` removes `node_modules` first,
|
|
22
|
+
* yarn's `--immutable` fails on a mismatch. This follows yarn, for three reasons.
|
|
23
|
+
* The flag's own name promises not to mutate, and this repo already refuses
|
|
24
|
+
* automatic deletion under it ({@link automaticPruneRefusal} returns `'--immutable'`).
|
|
25
|
+
* A verifier that deletes can delete the wrong thing — a `link`ed dep, a package
|
|
26
|
+
* some other tool put there — while a verifier that reports cannot. And the failure
|
|
27
|
+
* mode is asymmetric: a wrong deletion loses work, a wrong refusal costs one red run
|
|
28
|
+
* carrying the exact paths and the fix.
|
|
29
|
+
*
|
|
30
|
+
* That choice has a price, paid in `.github/actions/gjsify-setup/action.yml`: with
|
|
31
|
+
* this check fatal, a restored FOREIGN tree stops being silent corruption and
|
|
32
|
+
* becomes a hard failure on every lockfile-changing PR. So the `restore-keys`
|
|
33
|
+
* fallback is gone — see the comment there. The two decisions are one decision.
|
|
34
|
+
*/
|
|
35
|
+
import { relative, sep } from 'node:path';
|
|
36
|
+
/** `/`-separated, no trailing slash — lockfile key shape on every host. */
|
|
37
|
+
function normalizePath(p) {
|
|
38
|
+
const slashed = sep === '/' ? p : p.split(sep).join('/');
|
|
39
|
+
return slashed.endsWith('/') ? slashed.slice(0, -1) : slashed;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Package directories under `prefix` that no entry of `expected` accounts for.
|
|
43
|
+
*
|
|
44
|
+
* Pure: it takes the scan rows ({@link scanPrefix}) and the resolved set rather than
|
|
45
|
+
* reading either, so every branch — nested placements, symlinks, subtree collapsing —
|
|
46
|
+
* is decidable without a filesystem.
|
|
47
|
+
*
|
|
48
|
+
* Three things are deliberately NOT extraneous:
|
|
49
|
+
* - a SYMLINK, which is a workspace source tree linked in by `workspaceInstall`
|
|
50
|
+
* and by construction absent from the lockfile (`scanPrefix` does not descend
|
|
51
|
+
* into one either, so the user's own sources are never walked);
|
|
52
|
+
* - a dot-entry like `.bin` or `.cache`, already dropped by the scan;
|
|
53
|
+
* - a package the lockfile describes but the PLATFORM filter skipped on this host.
|
|
54
|
+
* It is described, so it is not a stranger; removing the foreign-platform residue
|
|
55
|
+
* is `prune-prefix.ts`'s separate job under a separate rule.
|
|
56
|
+
*
|
|
57
|
+
* Descendants of an extraneous directory are collapsed away: reporting the parent
|
|
58
|
+
* says everything, and the nested `@girs` tree of the incident would otherwise list
|
|
59
|
+
* its whole subtree.
|
|
60
|
+
*/
|
|
61
|
+
export function findExtraneous(opts) {
|
|
62
|
+
const described = new Set(opts.expected.map((n) => normalizePath(n.installPath)));
|
|
63
|
+
const found = [];
|
|
64
|
+
for (const pkg of opts.installed) {
|
|
65
|
+
if (pkg.linked)
|
|
66
|
+
continue;
|
|
67
|
+
const installPath = normalizePath(relative(opts.prefix, pkg.dir));
|
|
68
|
+
// Outside the prefix, or the prefix itself: not this install's to judge.
|
|
69
|
+
if (installPath === '' || installPath.startsWith('..'))
|
|
70
|
+
continue;
|
|
71
|
+
if (described.has(installPath))
|
|
72
|
+
continue;
|
|
73
|
+
found.push({ installPath, name: pkg.name, version: pkg.version });
|
|
74
|
+
}
|
|
75
|
+
found.sort((a, b) => (a.installPath < b.installPath ? -1 : a.installPath > b.installPath ? 1 : 0));
|
|
76
|
+
const roots = [];
|
|
77
|
+
for (const entry of found) {
|
|
78
|
+
const parent = roots[roots.length - 1];
|
|
79
|
+
// Sorted, so an enclosing extraneous directory is always the last one kept.
|
|
80
|
+
if (parent && entry.installPath.startsWith(`${parent.installPath}/`))
|
|
81
|
+
continue;
|
|
82
|
+
roots.push(entry);
|
|
83
|
+
}
|
|
84
|
+
return roots;
|
|
85
|
+
}
|
|
86
|
+
/** How many paths the message names before it starts counting. */
|
|
87
|
+
export const EXTRANEOUS_REPORT_LIMIT = 20;
|
|
88
|
+
/**
|
|
89
|
+
* The refusal, built to be actionable from the CI log alone: what is wrong, which
|
|
90
|
+
* paths, and the one thing that fixes it. Truncated past
|
|
91
|
+
* {@link EXTRANEOUS_REPORT_LIMIT} so a wholly foreign tree does not bury the advice
|
|
92
|
+
* under a thousand lines.
|
|
93
|
+
*
|
|
94
|
+
* It says DELETE, and says so as the only remedy, because that is what was measured:
|
|
95
|
+
* a plain `gjsify install` — and an explicit `--prune` — leave both a top-level and a
|
|
96
|
+
* nested stranger exactly where they were. The installer only adds, and `prune-prefix`
|
|
97
|
+
* judges one thing, npm's `os`/`cpu`/`libc`, so a package that is merely UNDESCRIBED
|
|
98
|
+
* matches no rule either owns. Sending a developer to `gjsify install` instead would
|
|
99
|
+
* hand them a command that changes nothing and a refusal that repeats verbatim.
|
|
100
|
+
*/
|
|
101
|
+
export function formatExtraneousError(entries, prefix) {
|
|
102
|
+
const shown = entries.slice(0, EXTRANEOUS_REPORT_LIMIT);
|
|
103
|
+
const lines = shown.map((e) => ` ${e.installPath} (${e.name}@${e.version})`);
|
|
104
|
+
if (entries.length > shown.length)
|
|
105
|
+
lines.push(` … and ${entries.length - shown.length} more`);
|
|
106
|
+
return [
|
|
107
|
+
`install: --immutable, but ${prefix}/node_modules holds ${entries.length} package(s) ` +
|
|
108
|
+
'that gjsify-lock.json does not describe.',
|
|
109
|
+
'--immutable installs the lockfile and removes nothing, so each of these would survive ' +
|
|
110
|
+
'the install and be resolved, bundled and type-checked as if it belonged ' +
|
|
111
|
+
'(gjsify#1683: a nested copy of an already-hoisted package → TS2883, one type ' +
|
|
112
|
+
'with two identities).',
|
|
113
|
+
...lines,
|
|
114
|
+
'Fix: delete those directories — or the whole node_modules — and re-run. A plain ' +
|
|
115
|
+
'`gjsify install` does NOT clear them: it only ever adds, and its prune pass judges ' +
|
|
116
|
+
'foreign-PLATFORM packages alone (prune-prefix.ts), so an undescribed package ' +
|
|
117
|
+
'survives both. Deleting is currently the only thing that removes one. ' +
|
|
118
|
+
'In CI this usually means a cache entry was restored from a DIFFERENT lockfile.',
|
|
119
|
+
].join('\n');
|
|
120
|
+
}
|
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Read a cached tarball by SRI integrity — raw bytes on a
|
|
3
|
-
* read failure (e.g. a partial write from an interrupted run) is a MISS,
|
|
4
|
-
* file is left untouched so a follow-up writer's atomic rename is not tripped.
|
|
2
|
+
* Read a cached tarball by SRI integrity — raw bytes on a VERIFIED hit, `null`
|
|
3
|
+
* otherwise. A read failure (e.g. a partial write from an interrupted run) is a MISS,
|
|
4
|
+
* and the file is left untouched so a follow-up writer's atomic rename is not tripped.
|
|
5
|
+
*
|
|
6
|
+
* The bytes are re-hashed against the integrity that named them, through the SAME
|
|
7
|
+
* {@link verifyIntegrity} the download path uses — so both paths agree by
|
|
8
|
+
* construction instead of by comment.
|
|
9
|
+
*
|
|
10
|
+
* This used to trust the path: "content-addressed, so a hash hit is byte-identical to
|
|
11
|
+
* what the registry would return". That is a claim ABOUT the bytes, and nothing read
|
|
12
|
+
* them to check it. It held while the store was a local directory this machine had
|
|
13
|
+
* written itself. It stops holding the moment the store is a restorable CI artifact —
|
|
14
|
+
* a GitHub Actions cache entry that is truncated, corrupted, or written by a workflow
|
|
15
|
+
* on another branch would have been consumed as authentic, because the only thing
|
|
16
|
+
* asserting its contents was its filename. `gjsify-setup` now restores exactly such an
|
|
17
|
+
* artifact, so the check is no longer optional. Yarn re-hashes on every install for
|
|
18
|
+
* this reason (refs/yarn-berry Cache.ts, "we check the file hashes during each install
|
|
19
|
+
* anyway"); measured here, it costs ~0.5 s over this repo's ~420 MB closure, against
|
|
20
|
+
* an extract phase of minutes.
|
|
21
|
+
*
|
|
22
|
+
* A mismatch DELETES the entry rather than merely skipping it. That is not the prune
|
|
23
|
+
* this repo refuses elsewhere — the name asserts the content and the content
|
|
24
|
+
* disproves the name, so the file is garbage by its own definition. It has to go:
|
|
25
|
+
* {@link putCachedTarball} is idempotent on existence, so a corrupt blob left in place
|
|
26
|
+
* would never be replaced and every future install would re-download it forever.
|
|
5
27
|
*/
|
|
6
|
-
export declare function getCachedTarball(integrity: string | undefined): Uint8Array | null
|
|
28
|
+
export declare function getCachedTarball(integrity: string | undefined): Promise<Uint8Array | null>;
|
|
7
29
|
/**
|
|
8
30
|
* Persist a tarball to the cache, via a `<path>.tmp.<pid>` sibling and an atomic
|
|
9
31
|
* rename so concurrent installs never observe a half-written entry. No-op when
|
|
@@ -17,9 +39,15 @@ export declare function cacheRootForLogging(): string;
|
|
|
17
39
|
export declare function isCacheHit(integrity: string | undefined): boolean;
|
|
18
40
|
/**
|
|
19
41
|
* Read a tarball from npm's cacache content store by SRI integrity — raw `.tgz`
|
|
20
|
-
* bytes on a
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
42
|
+
* bytes on a VERIFIED hit, `null` on a MISS, disabled interop or read failure.
|
|
43
|
+
*
|
|
44
|
+
* Re-hashed like {@link getCachedTarball}, and with more reason: these bytes come
|
|
45
|
+
* from ANOTHER tool's store, on a path this code does not control and cannot assume
|
|
46
|
+
* was written by a cacache that verified anything. The old note argued cacache
|
|
47
|
+
* verified on write and a corrupt tarball would fail loudly in the extractor — but
|
|
48
|
+
* "the next stage will probably notice" is not verification, and the caller
|
|
49
|
+
* write-throughs a foreign hit into OUR store, which would launder an unchecked blob
|
|
50
|
+
* into a first-class entry. A mismatch is left alone here rather than deleted: this
|
|
51
|
+
* store belongs to npm, and removing another tool's files is not this code's call.
|
|
24
52
|
*/
|
|
25
|
-
export declare function getForeignCachedTarball(integrity: string | undefined): Uint8Array | null
|
|
53
|
+
export declare function getForeignCachedTarball(integrity: string | undefined): Promise<Uint8Array | null>;
|
|
@@ -25,9 +25,10 @@
|
|
|
25
25
|
// on too. Tarballs without an integrity hash (older registries) fall through to a
|
|
26
26
|
// no-op cache and download every time.
|
|
27
27
|
import { Buffer } from 'node:buffer';
|
|
28
|
-
import { existsSync, statSync } from 'node:fs';
|
|
28
|
+
import { existsSync, statSync, unlinkSync } from 'node:fs';
|
|
29
29
|
import { homedir } from 'node:os';
|
|
30
30
|
import { join } from 'node:path';
|
|
31
|
+
import { verifyIntegrity } from '@gjsify/npm-registry';
|
|
31
32
|
import { atomicWrite, gjsifyCacheRoot, readCacheFile } from './install-cache-fs.js';
|
|
32
33
|
const CACHE_LAYOUT_VERSION = 'v1';
|
|
33
34
|
/** Root of the tarball cache: `$XDG_CACHE_HOME/gjsify/tarballs/v1`. */
|
|
@@ -68,15 +69,50 @@ function pathFor(integrity) {
|
|
|
68
69
|
return join(cacheRoot(), sri.algorithm, shard, `${sri.hex}.tgz`);
|
|
69
70
|
}
|
|
70
71
|
/**
|
|
71
|
-
* Read a cached tarball by SRI integrity — raw bytes on a
|
|
72
|
-
* read failure (e.g. a partial write from an interrupted run) is a MISS,
|
|
73
|
-
* file is left untouched so a follow-up writer's atomic rename is not tripped.
|
|
72
|
+
* Read a cached tarball by SRI integrity — raw bytes on a VERIFIED hit, `null`
|
|
73
|
+
* otherwise. A read failure (e.g. a partial write from an interrupted run) is a MISS,
|
|
74
|
+
* and the file is left untouched so a follow-up writer's atomic rename is not tripped.
|
|
75
|
+
*
|
|
76
|
+
* The bytes are re-hashed against the integrity that named them, through the SAME
|
|
77
|
+
* {@link verifyIntegrity} the download path uses — so both paths agree by
|
|
78
|
+
* construction instead of by comment.
|
|
79
|
+
*
|
|
80
|
+
* This used to trust the path: "content-addressed, so a hash hit is byte-identical to
|
|
81
|
+
* what the registry would return". That is a claim ABOUT the bytes, and nothing read
|
|
82
|
+
* them to check it. It held while the store was a local directory this machine had
|
|
83
|
+
* written itself. It stops holding the moment the store is a restorable CI artifact —
|
|
84
|
+
* a GitHub Actions cache entry that is truncated, corrupted, or written by a workflow
|
|
85
|
+
* on another branch would have been consumed as authentic, because the only thing
|
|
86
|
+
* asserting its contents was its filename. `gjsify-setup` now restores exactly such an
|
|
87
|
+
* artifact, so the check is no longer optional. Yarn re-hashes on every install for
|
|
88
|
+
* this reason (refs/yarn-berry Cache.ts, "we check the file hashes during each install
|
|
89
|
+
* anyway"); measured here, it costs ~0.5 s over this repo's ~420 MB closure, against
|
|
90
|
+
* an extract phase of minutes.
|
|
91
|
+
*
|
|
92
|
+
* A mismatch DELETES the entry rather than merely skipping it. That is not the prune
|
|
93
|
+
* this repo refuses elsewhere — the name asserts the content and the content
|
|
94
|
+
* disproves the name, so the file is garbage by its own definition. It has to go:
|
|
95
|
+
* {@link putCachedTarball} is idempotent on existence, so a corrupt blob left in place
|
|
96
|
+
* would never be replaced and every future install would re-download it forever.
|
|
74
97
|
*/
|
|
75
|
-
export function getCachedTarball(integrity) {
|
|
98
|
+
export async function getCachedTarball(integrity) {
|
|
76
99
|
const path = pathFor(integrity);
|
|
77
|
-
if (!path)
|
|
100
|
+
if (!path || !integrity)
|
|
101
|
+
return null;
|
|
102
|
+
const bytes = readCacheFile(path);
|
|
103
|
+
if (!bytes)
|
|
78
104
|
return null;
|
|
79
|
-
|
|
105
|
+
if (await verifyIntegrity(bytes, integrity))
|
|
106
|
+
return bytes;
|
|
107
|
+
// Best-effort: a read-only cache volume must not break the install, and the
|
|
108
|
+
// caller falls through to a verifying download either way.
|
|
109
|
+
try {
|
|
110
|
+
unlinkSync(path);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
/* keep going — the download path replaces the value we return as a MISS */
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
80
116
|
}
|
|
81
117
|
/**
|
|
82
118
|
* Persist a tarball to the cache, via a `<path>.tmp.<pid>` sibling and an atomic
|
|
@@ -158,14 +194,23 @@ function npmCachePathFor(integrity) {
|
|
|
158
194
|
}
|
|
159
195
|
/**
|
|
160
196
|
* Read a tarball from npm's cacache content store by SRI integrity — raw `.tgz`
|
|
161
|
-
* bytes on a
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
197
|
+
* bytes on a VERIFIED hit, `null` on a MISS, disabled interop or read failure.
|
|
198
|
+
*
|
|
199
|
+
* Re-hashed like {@link getCachedTarball}, and with more reason: these bytes come
|
|
200
|
+
* from ANOTHER tool's store, on a path this code does not control and cannot assume
|
|
201
|
+
* was written by a cacache that verified anything. The old note argued cacache
|
|
202
|
+
* verified on write and a corrupt tarball would fail loudly in the extractor — but
|
|
203
|
+
* "the next stage will probably notice" is not verification, and the caller
|
|
204
|
+
* write-throughs a foreign hit into OUR store, which would launder an unchecked blob
|
|
205
|
+
* into a first-class entry. A mismatch is left alone here rather than deleted: this
|
|
206
|
+
* store belongs to npm, and removing another tool's files is not this code's call.
|
|
165
207
|
*/
|
|
166
|
-
export function getForeignCachedTarball(integrity) {
|
|
208
|
+
export async function getForeignCachedTarball(integrity) {
|
|
167
209
|
const path = npmCachePathFor(integrity);
|
|
168
|
-
if (!path)
|
|
210
|
+
if (!path || !integrity)
|
|
211
|
+
return null;
|
|
212
|
+
const bytes = readCacheFile(path);
|
|
213
|
+
if (!bytes)
|
|
169
214
|
return null;
|
|
170
|
-
return
|
|
215
|
+
return (await verifyIntegrity(bytes, integrity)) ? bytes : null;
|
|
171
216
|
}
|
|
@@ -130,6 +130,17 @@ export function mergeBundlerOptions(base, overrides) {
|
|
|
130
130
|
if (base.transform?.define || overrides.transform?.define) {
|
|
131
131
|
out.transform.define = { ...base.transform?.define, ...overrides.transform?.define };
|
|
132
132
|
}
|
|
133
|
+
// `inject` is a map like `define` and merges the same way. Replacing it wholesale
|
|
134
|
+
// meant a user who injected ONE identifier silently lost the orchestrator's
|
|
135
|
+
// `console` → `@gjsify/rolldown-plugin-gjsify/shims/console-gjs` rewrite, which
|
|
136
|
+
// `--app gjs` cannot express any other way: GJS defines `globalThis.console`
|
|
137
|
+
// non-writable and non-configurable, so assigning to it from a register module
|
|
138
|
+
// no-ops. It also has to stay in lockstep with `mergeTransformOptions` in the
|
|
139
|
+
// plugin's auto-globals analyser — when the two merge the same options
|
|
140
|
+
// differently, the detector measures a bundle that is not the one that ships.
|
|
141
|
+
if (base.transform?.inject || overrides.transform?.inject) {
|
|
142
|
+
out.transform.inject = { ...base.transform?.inject, ...overrides.transform?.inject };
|
|
143
|
+
}
|
|
133
144
|
}
|
|
134
145
|
if (base.resolve || overrides.resolve) {
|
|
135
146
|
out.resolve = { ...base.resolve, ...overrides.resolve };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The intra-repo pins a publish is about to ship, enumerated from the manifest
|
|
3
|
+
* PAIR — the workspace's own `package.json` and the rewritten one that comes
|
|
4
|
+
* back out of the tarball.
|
|
5
|
+
*
|
|
6
|
+
* Why the pair and not just the rewritten manifest: after
|
|
7
|
+
* `rewriteWorkspaceDeps` (see `commands/pack.ts`) a sibling pin is an ordinary
|
|
8
|
+
* `^0.51.1` and indistinguishable from a third-party dependency. The `workspace:`
|
|
9
|
+
* prefix survives only in the SOURCE manifest, so which edges are this repo's
|
|
10
|
+
* own is a fact that exists in one file and the version they will ship at exists
|
|
11
|
+
* in the other.
|
|
12
|
+
*/
|
|
13
|
+
/** The blocks `rewriteWorkspaceDeps` rewrites and a consumer install resolves. */
|
|
14
|
+
export type DependencyBlock = 'dependencies' | 'optionalDependencies' | 'peerDependencies';
|
|
15
|
+
/**
|
|
16
|
+
* The one block whose unresolvable edge fails EVERY consumer install.
|
|
17
|
+
*
|
|
18
|
+
* Measured, npm 11.17.0, against the live registry, on an edge to a name that
|
|
19
|
+
* did not exist: `optionalDependencies` installs clean (exit 0, silently),
|
|
20
|
+
* `dependencies` is `npm error 404`, exit 1. Yarn refuses both (`YN0035`),
|
|
21
|
+
* which is why an optional edge is still reported — just not fatal.
|
|
22
|
+
*/
|
|
23
|
+
export declare const REQUIRED_BLOCK: DependencyBlock;
|
|
24
|
+
export interface WorkspacePin {
|
|
25
|
+
name: string;
|
|
26
|
+
/** The concrete range the tarball will carry, after the rewrite. */
|
|
27
|
+
spec: string;
|
|
28
|
+
block: DependencyBlock;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Every edge that was a `workspace:` spec before the rewrite, with the version
|
|
32
|
+
* it will ship at.
|
|
33
|
+
*
|
|
34
|
+
* A name appearing in two blocks yields two pins: `dependencies` and
|
|
35
|
+
* `optionalDependencies` do not cost the same at install time, so collapsing
|
|
36
|
+
* them would throw away the distinction the caller decides on.
|
|
37
|
+
*/
|
|
38
|
+
export declare function collectWorkspacePins(source: Record<string, unknown>, rewritten: Record<string, unknown>): WorkspacePin[];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The intra-repo pins a publish is about to ship, enumerated from the manifest
|
|
3
|
+
* PAIR — the workspace's own `package.json` and the rewritten one that comes
|
|
4
|
+
* back out of the tarball.
|
|
5
|
+
*
|
|
6
|
+
* Why the pair and not just the rewritten manifest: after
|
|
7
|
+
* `rewriteWorkspaceDeps` (see `commands/pack.ts`) a sibling pin is an ordinary
|
|
8
|
+
* `^0.51.1` and indistinguishable from a third-party dependency. The `workspace:`
|
|
9
|
+
* prefix survives only in the SOURCE manifest, so which edges are this repo's
|
|
10
|
+
* own is a fact that exists in one file and the version they will ship at exists
|
|
11
|
+
* in the other.
|
|
12
|
+
*/
|
|
13
|
+
const BLOCKS = ['dependencies', 'optionalDependencies', 'peerDependencies'];
|
|
14
|
+
/**
|
|
15
|
+
* The one block whose unresolvable edge fails EVERY consumer install.
|
|
16
|
+
*
|
|
17
|
+
* Measured, npm 11.17.0, against the live registry, on an edge to a name that
|
|
18
|
+
* did not exist: `optionalDependencies` installs clean (exit 0, silently),
|
|
19
|
+
* `dependencies` is `npm error 404`, exit 1. Yarn refuses both (`YN0035`),
|
|
20
|
+
* which is why an optional edge is still reported — just not fatal.
|
|
21
|
+
*/
|
|
22
|
+
export const REQUIRED_BLOCK = 'dependencies';
|
|
23
|
+
const depsOf = (pkg, block) => {
|
|
24
|
+
const value = pkg[block];
|
|
25
|
+
return value && typeof value === 'object' ? value : {};
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Every edge that was a `workspace:` spec before the rewrite, with the version
|
|
29
|
+
* it will ship at.
|
|
30
|
+
*
|
|
31
|
+
* A name appearing in two blocks yields two pins: `dependencies` and
|
|
32
|
+
* `optionalDependencies` do not cost the same at install time, so collapsing
|
|
33
|
+
* them would throw away the distinction the caller decides on.
|
|
34
|
+
*/
|
|
35
|
+
export function collectWorkspacePins(source, rewritten) {
|
|
36
|
+
const pins = [];
|
|
37
|
+
for (const block of BLOCKS) {
|
|
38
|
+
const before = depsOf(source, block);
|
|
39
|
+
const after = depsOf(rewritten, block);
|
|
40
|
+
for (const [name, spec] of Object.entries(before)) {
|
|
41
|
+
if (typeof spec !== 'string' || !spec.startsWith('workspace:'))
|
|
42
|
+
continue;
|
|
43
|
+
pins.push({ name, spec: after[name] ?? spec, block });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return pins;
|
|
47
|
+
}
|
|
@@ -19,9 +19,42 @@ export type ReadbackProbe = {
|
|
|
19
19
|
status?: number;
|
|
20
20
|
detail: string;
|
|
21
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* What the read-back CONCLUDED — the three outcomes a caller may act on, plus
|
|
24
|
+
* the one it may not.
|
|
25
|
+
*
|
|
26
|
+
* `absent` on its own is not a conclusion. The install document not carrying a
|
|
27
|
+
* version has two causes with opposite remedies — the write never landed
|
|
28
|
+
* (re-publish) and the write landed but that document does not serve it yet
|
|
29
|
+
* (wait; a re-publish is answered 409) — and until this type existed both left
|
|
30
|
+
* through one message asserting the first. Observed live on the ts-for-gir
|
|
31
|
+
* v5.1.0 release: `@ts-for-gir/reporter` was recorded by the registry at
|
|
32
|
+
* 21:43:00.983Z and a cache-busted abbreviated read ~40 min later still did not
|
|
33
|
+
* carry 5.1.0, while the full packument's `time` map did; two minutes after
|
|
34
|
+
* that, both agreed.
|
|
35
|
+
*/
|
|
36
|
+
export type ReadbackVerdict =
|
|
37
|
+
/** The install document serves `name@version` and advertises a tarball. */
|
|
38
|
+
'confirmed'
|
|
39
|
+
/** The registry has no record of this version at all — the v0.46.0 shape. */
|
|
40
|
+
| 'not-published'
|
|
41
|
+
/** The registry RECORDS the write; the document npm installs from does not serve it. */
|
|
42
|
+
| 'recorded-not-served'
|
|
43
|
+
/**
|
|
44
|
+
* Nothing was established. A 5xx, a timeout, DNS, an auth wall — or an
|
|
45
|
+
* install document without the version whose registry record could then
|
|
46
|
+
* not be read, which leaves BOTH other verdicts unearned. NEVER "published".
|
|
47
|
+
*/
|
|
48
|
+
| 'unknown';
|
|
22
49
|
export interface ReadbackResult {
|
|
23
50
|
/** True iff a probe saw the exact version with a tarball on it. */
|
|
24
51
|
confirmed: boolean;
|
|
52
|
+
/** What the read-back concluded. `confirmed === (verdict === 'confirmed')`. */
|
|
53
|
+
verdict: ReadbackVerdict;
|
|
54
|
+
/** One line naming the evidence the verdict rests on — it goes in the report. */
|
|
55
|
+
verdictDetail: string;
|
|
56
|
+
/** `time[version]` from the registry's own record, when the corroboration read one. */
|
|
57
|
+
recordedAt?: string;
|
|
25
58
|
/** Probes actually sent (≥ 1). */
|
|
26
59
|
attempts: number;
|
|
27
60
|
elapsedMs: number;
|
|
@@ -80,29 +113,42 @@ export declare function backoffFor(round: number): number;
|
|
|
80
113
|
*/
|
|
81
114
|
export declare function probeTimeoutFor(configuredMs: number, remainingMs: number): number;
|
|
82
115
|
/**
|
|
83
|
-
* Default polling budget:
|
|
116
|
+
* Default polling budget: 600 s.
|
|
84
117
|
*
|
|
85
|
-
* The
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
118
|
+
* The lag between a 2xx and the registry recording the version has been measured
|
|
119
|
+
* on two releases, and the second broke the first's ceiling. v0.46.0 (199
|
|
120
|
+
* packages): 19 late writes, maximum 251.7 s (@gjsify/child_process). ts-for-gir
|
|
121
|
+
* v5.1.0 (12 packages, run 34899018849, PUT lines against `time["5.1.0"]`):
|
|
122
|
+
* four recorded ~0.7 s BEFORE the 2xx, eight after it — +74, +74, +75, +75,
|
|
123
|
+
* +96, +248, +248 and +368 s (@ts-for-gir/reporter) — the last past the 300 s
|
|
124
|
+
* this constant used to be, so "above the measured maximum" had quietly
|
|
125
|
+
* stopped being true of it. A budget below the lag turns npm's normal queueing into a false
|
|
126
|
+
* red, and a false red at release time costs a manual re-run of a workflow that
|
|
127
|
+
* did its job. A budget above it costs nothing on the nine in ten that confirm
|
|
128
|
+
* on the first probe; all it delays is the report of a genuinely LOST write —
|
|
129
|
+
* one in 199, measured — by the difference. 600 s is ~1.6x the larger maximum.
|
|
91
130
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
131
|
+
* BOTH MAXIMA ARE SAMPLE MAXIMA, and neither sample looks truncated by nature.
|
|
132
|
+
* The v0.46.0 lags fall in buckets ~20-30 s apart — 56(2), 76(3), 96(3),
|
|
94
133
|
* 127(4), 157(3), 189(1), 251(3) — and the TOP bucket is as populated as the
|
|
95
134
|
* middle ones, which is the shape of a distribution cut off by the observation
|
|
96
|
-
* rather than one decaying to zero
|
|
97
|
-
*
|
|
98
|
-
* remediation is verified rather than assumed
|
|
99
|
-
* the 409 path, and that path READS BACK too (see
|
|
100
|
-
* `commands/publish.ts`), so a window that turns out
|
|
101
|
-
* re-run that confirms —
|
|
135
|
+
* rather than one decaying to zero; the next release then produced a value past
|
|
136
|
+
* it. So a lag past 600 s is not ruled out either. What makes any finite window
|
|
137
|
+
* affordable is that the remediation is verified rather than assumed: the
|
|
138
|
+
* re-run this reports lands on the 409 path, and that path READS BACK too (see
|
|
139
|
+
* the conflict branch in `commands/publish.ts`), so a window that turns out too
|
|
140
|
+
* short costs one re-run that confirms — never an unverified success.
|
|
141
|
+
*
|
|
142
|
+
* `time[version]` is when the registry RECORDED the write, a lower bound on
|
|
143
|
+
* when the document npm installs from SERVES it: the reporter above was
|
|
144
|
+
* recorded at +368 s and a cache-busted read of its install document still
|
|
145
|
+
* lacked 5.1.0 ~40 min later. No budget waits that out, and none should — that
|
|
146
|
+
* state is what the `recorded-not-served` verdict names.
|
|
102
147
|
*/
|
|
103
|
-
export declare const DEFAULT_VERIFY_BUDGET_MS =
|
|
148
|
+
export declare const DEFAULT_VERIFY_BUDGET_MS = 600000;
|
|
104
149
|
/**
|
|
105
|
-
* Poll the registry until `name@version` resolves, or the budget runs out
|
|
150
|
+
* Poll the registry until `name@version` resolves, or the budget runs out, then
|
|
151
|
+
* say WHICH of the three states it found (see {@link ReadbackVerdict}).
|
|
106
152
|
*
|
|
107
153
|
* No side effects, no exits, no printing beyond the injected `log` — the caller
|
|
108
154
|
* owns presentation, same contract as {@link import('./publish-diagnose.js')}.
|
|
@@ -130,3 +176,43 @@ export declare function formatUnconfirmedPublish(opts: {
|
|
|
130
176
|
*/
|
|
131
177
|
claim?: 'accepted' | 'already-published';
|
|
132
178
|
}): string;
|
|
179
|
+
/** Whether the registry serves a packument for a NAME at all. */
|
|
180
|
+
export type NamePresence = {
|
|
181
|
+
state: 'present';
|
|
182
|
+
status: number;
|
|
183
|
+
versions: number;
|
|
184
|
+
} | {
|
|
185
|
+
state: 'absent';
|
|
186
|
+
status: number;
|
|
187
|
+
detail: string;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* We do not KNOW — a 5xx, a timeout, an auth wall. A caller that blocks a
|
|
191
|
+
* publish on this must treat it as "not established", never as "absent":
|
|
192
|
+
* the whole point of the guard is that it cannot manufacture a red.
|
|
193
|
+
*/
|
|
194
|
+
| {
|
|
195
|
+
state: 'unknown';
|
|
196
|
+
status?: number;
|
|
197
|
+
detail: string;
|
|
198
|
+
};
|
|
199
|
+
export interface ProbePackageNameInput {
|
|
200
|
+
registry: string;
|
|
201
|
+
name: string;
|
|
202
|
+
probeTimeoutMs?: number;
|
|
203
|
+
authorization?: string;
|
|
204
|
+
fetchImpl?: typeof fetch;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Ask ONE question: does this NAME exist on the registry at all.
|
|
208
|
+
*
|
|
209
|
+
* Deliberately not a version question, and that is what makes it safe to gate a
|
|
210
|
+
* publish on. A version is eventually consistent — measured on v0.46.0, 19 of
|
|
211
|
+
* 199 packages were recorded 56-252 s AFTER their own 2xx — so "is name@version
|
|
212
|
+
* there" answers "not yet" for minutes and a gate built on it would manufacture
|
|
213
|
+
* a red on a publish that worked. Presence of the NAME does not move once the
|
|
214
|
+
* first version lands, which is the same reason
|
|
215
|
+
* `verify-published-closure.mjs --phase pre-release` asks it rather than the
|
|
216
|
+
* version: an answer that does not move when the train moves.
|
|
217
|
+
*/
|
|
218
|
+
export declare function probePackageName(input: ProbePackageNameInput): Promise<NamePresence>;
|