@gjsify/cli 0.36.0 → 0.37.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 +3 -3
- package/lib/bundler-pick.js +61 -87
- package/lib/cli-fail.d.ts +4 -7
- package/lib/cli-fail.js +29 -55
- package/lib/commands/affected.js +161 -286
- package/lib/commands/install.d.ts +63 -66
- package/lib/commands/install.js +372 -536
- package/lib/templates/install.mjs.tmpl +38 -68
- package/lib/types/cli-build-options.d.ts +33 -67
- package/lib/types/command.d.ts +9 -12
- package/lib/types/config-data.d.ts +125 -221
- package/lib/utils/auth-npmrc.d.ts +5 -10
- package/lib/utils/auth-npmrc.js +11 -21
- package/lib/utils/clear-targets.d.ts +8 -11
- package/lib/utils/clear-targets.js +22 -29
- package/lib/utils/copy-targets.d.ts +13 -19
- package/lib/utils/copy-targets.js +31 -44
- package/lib/utils/dep-aggregation.d.ts +7 -15
- package/lib/utils/dep-aggregation.js +9 -22
- package/lib/utils/detect-native-packages.d.ts +83 -116
- package/lib/utils/detect-native-packages.js +111 -149
- package/lib/utils/dir-link.d.ts +29 -48
- package/lib/utils/dir-link.js +33 -56
- package/lib/utils/discover-showcases.d.ts +6 -7
- package/lib/utils/discover-showcases.js +11 -15
- package/lib/utils/dlx-cache.d.ts +29 -38
- package/lib/utils/dlx-cache.js +53 -63
- package/lib/utils/force-exit.d.ts +0 -3
- package/lib/utils/force-exit.js +13 -24
- package/lib/utils/gi-search-path.d.ts +16 -24
- package/lib/utils/gi-search-path.js +54 -82
- package/lib/utils/gjs-bundle-guard.d.ts +5 -13
- package/lib/utils/gjs-bundle-guard.js +35 -57
- package/lib/utils/gjs-source-escape.d.ts +2 -2
- package/lib/utils/gjs-source-escape.js +21 -39
- package/lib/utils/gjsify-shim.d.ts +17 -20
- package/lib/utils/gjsify-shim.js +80 -120
- package/lib/utils/html-entry.d.ts +8 -12
- package/lib/utils/html-entry.js +19 -28
- package/lib/utils/install-backend-native.d.ts +123 -161
- package/lib/utils/install-backend-native.js +536 -737
- package/lib/utils/install-backend.d.ts +42 -57
- package/lib/utils/install-backend.js +16 -21
- package/lib/utils/install-cache-fs.d.ts +10 -15
- package/lib/utils/install-cache-fs.js +18 -26
- package/lib/utils/install-lock.d.ts +5 -6
- package/lib/utils/install-lock.js +46 -65
- package/lib/utils/install-packument-cache.d.ts +27 -33
- package/lib/utils/install-packument-cache.js +44 -57
- package/lib/utils/install-progress.d.ts +2 -9
- package/lib/utils/install-progress.js +14 -39
- package/lib/utils/install-tarball-cache.d.ts +14 -20
- package/lib/utils/install-tarball-cache.js +41 -59
- package/lib/utils/library-output.d.ts +9 -22
- package/lib/utils/library-output.js +19 -37
- package/lib/utils/load-npmrc.d.ts +5 -6
- package/lib/utils/load-npmrc.js +18 -27
- package/lib/utils/node-bundle-guard.d.ts +4 -8
- package/lib/utils/node-bundle-guard.js +21 -35
- package/lib/utils/node-script.js +33 -54
- package/lib/utils/node-version.d.ts +5 -14
- package/lib/utils/node-version.js +15 -24
- package/lib/utils/normalize-bundler-options.d.ts +7 -11
- package/lib/utils/normalize-bundler-options.js +24 -42
- package/lib/utils/npm-oidc.d.ts +23 -37
- package/lib/utils/npm-oidc.js +28 -53
- package/lib/utils/npm-otp-cache.d.ts +4 -4
- package/lib/utils/npm-otp-cache.js +20 -25
- package/lib/utils/npm-otp.d.ts +20 -30
- package/lib/utils/npm-otp.js +32 -51
- package/lib/utils/onboard-probe.d.ts +7 -13
- package/lib/utils/onboard-probe.js +16 -27
- package/lib/utils/parse-spec.d.ts +3 -3
- package/lib/utils/parse-spec.js +4 -5
- package/lib/utils/pkg-json-edit.d.ts +7 -12
- package/lib/utils/pkg-json-edit.js +13 -26
- package/lib/utils/pkg-json.d.ts +11 -18
- package/lib/utils/pkg-json.js +19 -28
- package/lib/utils/prefixed-output.d.ts +3 -4
- package/lib/utils/prefixed-output.js +13 -19
- package/lib/utils/prompt.d.ts +3 -4
- package/lib/utils/prompt.js +14 -18
- package/lib/utils/publish-diagnose.d.ts +11 -16
- package/lib/utils/publish-diagnose.js +15 -28
- package/lib/utils/publish-headers.d.ts +7 -11
- package/lib/utils/publish-headers.js +36 -65
- package/lib/utils/publishable-packages.d.ts +4 -6
- package/lib/utils/publishable-packages.js +14 -18
- package/lib/utils/resolve-gjs-entry.d.ts +5 -8
- package/lib/utils/resolve-gjs-entry.js +10 -27
- package/lib/utils/resolve-npm-package.d.ts +6 -11
- package/lib/utils/resolve-npm-package.js +29 -63
- package/lib/utils/resolve-plugin-by-name.d.ts +11 -19
- package/lib/utils/resolve-plugin-by-name.js +9 -14
- package/lib/utils/run-gjs.d.ts +16 -21
- package/lib/utils/run-gjs.js +31 -56
- package/lib/utils/run-lifecycle-script.d.ts +5 -9
- package/lib/utils/run-lifecycle-script.js +23 -42
- package/lib/utils/run-node.d.ts +21 -33
- package/lib/utils/run-node.js +43 -64
- package/lib/utils/runtimes.d.ts +25 -35
- package/lib/utils/runtimes.js +34 -46
- package/lib/utils/spawn.d.ts +32 -48
- package/lib/utils/spawn.js +51 -84
- package/lib/utils/stdin.d.ts +2 -2
- package/lib/utils/stdin.js +10 -25
- package/lib/utils/suggest.d.ts +6 -7
- package/lib/utils/suggest.js +8 -10
- package/lib/utils/system-gi.d.ts +18 -27
- package/lib/utils/system-gi.js +60 -114
- package/lib/utils/trust-registry.d.ts +4 -8
- package/lib/utils/trust-registry.js +7 -15
- package/lib/utils/win32-command.d.ts +8 -14
- package/lib/utils/win32-command.js +29 -43
- package/lib/utils/workspace-root.js +7 -12
- package/package.json +18 -18
|
@@ -19,86 +19,71 @@ export interface InstallOptions {
|
|
|
19
19
|
/** Use `<prefix>/gjsify-lock.json` as the source of truth — fail if missing. */
|
|
20
20
|
frozen?: boolean;
|
|
21
21
|
/**
|
|
22
|
-
* Native backend only: force a fresh re-resolution that bumps every spec to
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* way `npm install` / `yarn install` / `pnpm install` do. Set this to
|
|
28
|
-
* intentionally pick up in-range updates (≈ `yarn install
|
|
29
|
-
* --mode=update-lockfile` / `npm update` / `pnpm update`).
|
|
22
|
+
* Native backend only: force a fresh re-resolution that bumps every spec to the
|
|
23
|
+
* newest version satisfying its range, rewriting the lockfile. Off, a resolve
|
|
24
|
+
* that has to run PRESERVES the versions already pinned and only resolves the
|
|
25
|
+
* genuinely new/changed deps, as `npm`/`yarn`/`pnpm install` do. Set it to
|
|
26
|
+
* intentionally pick up in-range updates (≈ `npm update`).
|
|
30
27
|
*/
|
|
31
28
|
refreshLockfile?: boolean;
|
|
32
29
|
/**
|
|
33
|
-
* Per-package version overrides — `<name> → <range
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* Lets a workspace root pin a transitive dep version when the
|
|
40
|
-
* deduplicated tree would otherwise pick a different one — e.g. for
|
|
41
|
-
* forcing `typescript@~5.9` across every `typescript@*` devDep.
|
|
30
|
+
* Per-package version overrides — `<name> → <range>`, applied to every edge
|
|
31
|
+
* during resolution irrespective of the requester. Mirrors npm's top-level
|
|
32
|
+
* `overrides` and yarn's `resolutions` (name-only flavour; pattern keys like
|
|
33
|
+
* `typescript@*` are normalised to bare `typescript` by the caller). Lets a
|
|
34
|
+
* workspace root pin a transitive dep the deduplicated tree would otherwise
|
|
35
|
+
* resolve differently.
|
|
42
36
|
*/
|
|
43
37
|
overrides?: Record<string, string>;
|
|
44
38
|
/**
|
|
45
|
-
* Native backend only:
|
|
46
|
-
*
|
|
47
|
-
* are
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* avoids spurious packument fetches for workspace-internal packages.
|
|
39
|
+
* Native backend only: install only the top-level requested packages, skipping
|
|
40
|
+
* transitive resolution. For self-contained bundles whose declared
|
|
41
|
+
* `dependencies` are either bundled into the artifact (e.g. `@gjsify/cli`'s GJS
|
|
42
|
+
* bundle) or workspace-only and never published, where resolution would only
|
|
43
|
+
* produce spurious packument fetches.
|
|
51
44
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* (npm does its own resolution and does not consult this flag).
|
|
45
|
+
* No effect under `frozen: true` (the lockfile holds the full resolved tree and
|
|
46
|
+
* is used verbatim) or `GJSIFY_INSTALL_BACKEND=npm` (npm resolves for itself).
|
|
55
47
|
*/
|
|
56
48
|
skipDeps?: boolean;
|
|
57
49
|
/**
|
|
58
|
-
* Native backend only: overall wall-clock budget
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* timeouts in @gjsify/npm-registry still apply.
|
|
50
|
+
* Native backend only: overall wall-clock budget. On firing, in-flight packument
|
|
51
|
+
* + tarball fetches abort via their AbortSignals (surfaced as a normal
|
|
52
|
+
* AbortError), so a stall fails cleanly instead of hanging. Zero/undefined
|
|
53
|
+
* disables the overall budget; per-request timeouts in @gjsify/npm-registry
|
|
54
|
+
* still apply.
|
|
64
55
|
*/
|
|
65
56
|
signal?: AbortSignal;
|
|
66
57
|
/**
|
|
67
|
-
* Native backend only: progress reporter for resolve / download / extract /
|
|
68
|
-
* link phases.
|
|
69
|
-
*
|
|
70
|
-
* to override.
|
|
58
|
+
* Native backend only: progress reporter for the resolve / download / extract /
|
|
59
|
+
* link phases. Defaults to a TTY-aware reporter; pass a custom one (or the
|
|
60
|
+
* `NOOP` from `makeProgressReporter({enabled:false})`) to override.
|
|
71
61
|
*/
|
|
72
62
|
progress?: ProgressReporter;
|
|
73
63
|
/**
|
|
74
|
-
* Native backend only: the
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* whole install). The `npm` backend ignores this — npm resolves workspaces
|
|
83
|
-
* itself.
|
|
64
|
+
* Native backend only: the monorepo's own workspace package names. A name in
|
|
65
|
+
* this set is materialised as a symlink to its workspace source by
|
|
66
|
+
* `workspaceInstall` and never fetched, even when the lockfile or a transitive
|
|
67
|
+
* edge references a same-named PUBLISHED version (e.g. a `types-dev/<lib>`
|
|
68
|
+
* workspace that also exists on npm). Such names are dropped from `resolveDeps`
|
|
69
|
+
* AND from the fetch/extract set, so `extractOne` never tries to `rm` +
|
|
70
|
+
* overwrite a workspace source symlink — its data-loss guard would abort the
|
|
71
|
+
* whole install. The `npm` backend ignores this and resolves workspaces itself.
|
|
84
72
|
*/
|
|
85
73
|
workspaceNames?: Set<string>;
|
|
86
74
|
/**
|
|
87
|
-
* Native backend only: requester labels for top-level specs, keyed by the
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* per-requester attribution (single-project installs, dlx) omit it and the
|
|
93
|
-
* warning falls back to the bare ranges.
|
|
75
|
+
* Native backend only: requester labels for top-level specs, keyed by the exact
|
|
76
|
+
* `"<name>@<range>"` string, so the resolver's version-conflict warning can name
|
|
77
|
+
* BOTH sides ("^1 requested by @scope/a, ^2 requested by @scope/b"). Optional —
|
|
78
|
+
* flows without per-requester attribution (single-project installs, dlx) omit it
|
|
79
|
+
* and the warning falls back to bare ranges.
|
|
94
80
|
*/
|
|
95
81
|
specOrigins?: Map<string, string[]>;
|
|
96
82
|
}
|
|
97
83
|
export interface InstallResult {
|
|
98
|
-
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* GJSIFY_INSTALL_BACKEND=native). */
|
|
84
|
+
/** Requested top-level packages with the version each resolved to. Empty for
|
|
85
|
+
* the npm backend — parsing npm's stdout would be unreliable, so a caller that
|
|
86
|
+
* needs this must use the native backend. */
|
|
102
87
|
installed: Array<{
|
|
103
88
|
name: string;
|
|
104
89
|
version: string;
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
// Install backend abstraction.
|
|
2
2
|
//
|
|
3
|
-
// Default: native backend (resolves packuments via @gjsify/npm-registry,
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// for parity with the legacy code path. Switched via
|
|
7
|
-
// `GJSIFY_INSTALL_BACKEND=native|npm`.
|
|
3
|
+
// Default: native backend (resolves packuments via @gjsify/npm-registry, extracts
|
|
4
|
+
// tarballs via @gjsify/tar — no Node, no npm required at runtime). Fallback:
|
|
5
|
+
// `npm install`. Switched via `GJSIFY_INSTALL_BACKEND=native|npm`.
|
|
8
6
|
//
|
|
9
7
|
// `gjsify dlx` uses this seam — installing under a cache prefix, with no
|
|
10
|
-
// package.json update to the user's project.
|
|
11
|
-
// workflow without ever shelling out to Node.
|
|
8
|
+
// package.json update to the user's project.
|
|
12
9
|
//
|
|
13
|
-
// `--no-package-lock` keeps the cache prepare dir hermetic
|
|
14
|
-
// already covers reproducibility
|
|
10
|
+
// On the npm path, `--no-package-lock` keeps the cache prepare dir hermetic (the
|
|
11
|
+
// cache key already covers reproducibility) and `--no-audit --no-fund` cuts ~5s
|
|
12
|
+
// off cold runs.
|
|
15
13
|
import { writeFileSync } from 'node:fs';
|
|
16
14
|
import { join } from 'node:path';
|
|
17
15
|
import { spawnToCompletion } from './spawn.js';
|
|
@@ -30,8 +28,8 @@ async function installViaNpm({ prefix, specs, verbose, registry }) {
|
|
|
30
28
|
if (specs.length === 0) {
|
|
31
29
|
throw new Error('installPackages: empty specs list');
|
|
32
30
|
}
|
|
33
|
-
// Seed an empty package.json so npm doesn't walk up from prefix and pick
|
|
34
|
-
//
|
|
31
|
+
// Seed an empty package.json so npm doesn't walk up from prefix and pick up the
|
|
32
|
+
// user's project metadata. Cosmetic name/version only.
|
|
35
33
|
writeFileSync(join(prefix, 'package.json'), JSON.stringify({ name: 'gjsify-dlx-cache', version: '0.0.0', private: true }, null, 2));
|
|
36
34
|
if (registry) {
|
|
37
35
|
writeFileSync(join(prefix, '.npmrc'), `registry=${registry}\n`);
|
|
@@ -46,17 +44,14 @@ async function installViaNpm({ prefix, specs, verbose, registry }) {
|
|
|
46
44
|
...(verbose ? ['--loglevel', 'verbose'] : ['--loglevel', 'warn']),
|
|
47
45
|
...specs,
|
|
48
46
|
];
|
|
49
|
-
// Through `spawnToCompletion`
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
// there, and the ENOENT hint below actively misdiagnosed it: npm IS on
|
|
54
|
-
// PATH on such a host, and it told the user to select the backend that
|
|
55
|
-
// was already the default while calling it unsupported.
|
|
47
|
+
// Through `spawnToCompletion` because a bare `spawn('npm', …)` is ENOENT on
|
|
48
|
+
// Windows — `CreateProcess` appends only `.exe` and npm ships a `.cmd` shim
|
|
49
|
+
// (see utils/win32-command.ts). That made this backend unusable there while the
|
|
50
|
+
// ENOENT hint below misdiagnosed it as npm being absent from PATH.
|
|
56
51
|
//
|
|
57
|
-
// `completion: 'return'` —
|
|
58
|
-
//
|
|
59
|
-
//
|
|
52
|
+
// `completion: 'return'` — this resolves a promise to its caller instead of
|
|
53
|
+
// exiting, so under GJS it must not leave a main loop armed. (Moot in practice:
|
|
54
|
+
// this backend needs npm, hence Node.)
|
|
60
55
|
const { code } = await spawnToCompletion('npm', args, {
|
|
61
56
|
completion: 'return',
|
|
62
57
|
notFound: (err) => new Error(`npm not found on PATH — install Node.js, or unset GJSIFY_INSTALL_BACKEND to use the default native backend (${err.message})`),
|
|
@@ -6,26 +6,21 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export declare function gjsifyCacheRoot(kind: string, layoutVersion: string): string;
|
|
8
8
|
/**
|
|
9
|
-
* Write `bytes` to `path` atomically
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* out-of-disk cache volume) is swallowed so a cache hiccup never breaks the
|
|
13
|
-
* install; the caller proceeds with its in-memory copy.
|
|
9
|
+
* Write `bytes` to `path` atomically via a `<path>.tmp.<pid>` sibling + `rename`. Best-effort:
|
|
10
|
+
* any failure (read-only / out-of-disk cache volume) is swallowed so a cache hiccup never
|
|
11
|
+
* breaks the install — the caller proceeds with its in-memory copy.
|
|
14
12
|
*/
|
|
15
13
|
export declare function atomicWrite(path: string, bytes: Uint8Array | string): void;
|
|
16
14
|
/**
|
|
17
|
-
* Strict sibling of {@link atomicWrite}:
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* POSIX, so a concurrent reader (or a crash mid-write) sees either the old
|
|
22
|
-
* complete file or the new complete file — never a torn one.
|
|
15
|
+
* Strict sibling of {@link atomicWrite}: same tmp+`rename` pattern, but failures PROPAGATE.
|
|
16
|
+
* Use for files whose loss must abort the operation (the `gjsify-lock.json` writer) rather
|
|
17
|
+
* than degrade silently like a cache entry. `rename` over the destination is atomic on POSIX,
|
|
18
|
+
* so a concurrent reader — or a crash mid-write — sees the old or the new file, never a torn one.
|
|
23
19
|
*/
|
|
24
20
|
export declare function atomicWriteStrict(path: string, bytes: Uint8Array | string): void;
|
|
25
21
|
/**
|
|
26
|
-
* Read a cache file
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* writer's atomic rename isn't disturbed.
|
|
22
|
+
* Read a cache file: bytes on a HIT, `null` on a MISS. Missing, zero-byte (an interrupted
|
|
23
|
+
* previous write) and unreadable all count as a MISS, and the file is left untouched so a
|
|
24
|
+
* follow-up writer's atomic rename isn't disturbed.
|
|
30
25
|
*/
|
|
31
26
|
export declare function readCacheFile(path: string): Buffer | null;
|
|
@@ -2,18 +2,15 @@
|
|
|
2
2
|
// Shared filesystem primitives for `gjsify install`'s on-disk caches and its
|
|
3
3
|
// lockfile writer.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// (`gjsify-lock.json`) shares the same tmp+rename pattern via the strict
|
|
12
|
-
// variant so an interrupted install can never leave a torn lockfile behind.
|
|
5
|
+
// The tarball cache (install-tarball-cache.ts) and the packument metadata cache
|
|
6
|
+
// (install-packument-cache.ts) need the same three things: an `XDG_CACHE_HOME`-honouring
|
|
7
|
+
// root, an atomic write (so a concurrent install never observes a half-written entry), and a
|
|
8
|
+
// read treating missing / zero-byte / unreadable as a MISS. The `gjsify-lock.json` writer
|
|
9
|
+
// shares the tmp+rename pattern via the strict variant, so an interrupted install cannot
|
|
10
|
+
// leave a torn lockfile.
|
|
13
11
|
//
|
|
14
|
-
// Out of scope: the dlx cache (`dlx-cache.ts`) has a different layout
|
|
15
|
-
//
|
|
16
|
-
// it is a precedent for the XDG helper, not a reuse target.
|
|
12
|
+
// Out of scope: the dlx cache (`dlx-cache.ts`) has a different layout (`gjsify/dlx`, sha256 +
|
|
13
|
+
// symlink-swap) and its own TTL concerns — a precedent for the XDG helper, not a reuse target.
|
|
17
14
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
18
15
|
import { homedir } from 'node:os';
|
|
19
16
|
import { join } from 'node:path';
|
|
@@ -29,11 +26,9 @@ export function gjsifyCacheRoot(kind, layoutVersion) {
|
|
|
29
26
|
return join(base, 'gjsify', kind, layoutVersion);
|
|
30
27
|
}
|
|
31
28
|
/**
|
|
32
|
-
* Write `bytes` to `path` atomically
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* out-of-disk cache volume) is swallowed so a cache hiccup never breaks the
|
|
36
|
-
* install; the caller proceeds with its in-memory copy.
|
|
29
|
+
* Write `bytes` to `path` atomically via a `<path>.tmp.<pid>` sibling + `rename`. Best-effort:
|
|
30
|
+
* any failure (read-only / out-of-disk cache volume) is swallowed so a cache hiccup never
|
|
31
|
+
* breaks the install — the caller proceeds with its in-memory copy.
|
|
37
32
|
*/
|
|
38
33
|
export function atomicWrite(path, bytes) {
|
|
39
34
|
try {
|
|
@@ -44,12 +39,10 @@ export function atomicWrite(path, bytes) {
|
|
|
44
39
|
}
|
|
45
40
|
}
|
|
46
41
|
/**
|
|
47
|
-
* Strict sibling of {@link atomicWrite}:
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* POSIX, so a concurrent reader (or a crash mid-write) sees either the old
|
|
52
|
-
* complete file or the new complete file — never a torn one.
|
|
42
|
+
* Strict sibling of {@link atomicWrite}: same tmp+`rename` pattern, but failures PROPAGATE.
|
|
43
|
+
* Use for files whose loss must abort the operation (the `gjsify-lock.json` writer) rather
|
|
44
|
+
* than degrade silently like a cache entry. `rename` over the destination is atomic on POSIX,
|
|
45
|
+
* so a concurrent reader — or a crash mid-write — sees the old or the new file, never a torn one.
|
|
53
46
|
*/
|
|
54
47
|
export function atomicWriteStrict(path, bytes) {
|
|
55
48
|
mkdirSync(join(path, '..'), { recursive: true });
|
|
@@ -58,10 +51,9 @@ export function atomicWriteStrict(path, bytes) {
|
|
|
58
51
|
renameSync(tmp, path);
|
|
59
52
|
}
|
|
60
53
|
/**
|
|
61
|
-
* Read a cache file
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* writer's atomic rename isn't disturbed.
|
|
54
|
+
* Read a cache file: bytes on a HIT, `null` on a MISS. Missing, zero-byte (an interrupted
|
|
55
|
+
* previous write) and unreadable all count as a MISS, and the file is left untouched so a
|
|
56
|
+
* follow-up writer's atomic rename isn't disturbed.
|
|
65
57
|
*/
|
|
66
58
|
export function readCacheFile(path) {
|
|
67
59
|
if (!existsSync(path))
|
|
@@ -3,12 +3,11 @@ export interface InstallLockHandle {
|
|
|
3
3
|
release(): void;
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
|
-
* Acquire the cross-process install lock for `prefix
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* to the overall install budget so a waiter never outlives its own timeout.
|
|
6
|
+
* Acquire the cross-process install lock for `prefix`, resolving once this process may mutate
|
|
7
|
+
* `<prefix>/node_modules` + `<prefix>/gjsify-lock.json`. The caller MUST `release()` in a
|
|
8
|
+
* `finally`. Waits with backoff while a live install holds the lock, and steals provably-stale
|
|
9
|
+
* ones. `signal` aborts the wait — wired to the install budget so a waiter never outlives its
|
|
10
|
+
* own timeout.
|
|
12
11
|
*/
|
|
13
12
|
export declare function acquireInstallLock(prefix: string, opts?: {
|
|
14
13
|
signal?: AbortSignal;
|
|
@@ -1,48 +1,36 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
// Cross-process install lock for `gjsify install` (ADR 0001, step 2).
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// prefix
|
|
7
|
-
//
|
|
8
|
-
// destination directories (corrupt trees, spurious ENOENT mid-extract) and
|
|
9
|
-
// tear the lockfile. The lock serializes them. Installs into DIFFERENT
|
|
10
|
-
// prefixes stay fully concurrent.
|
|
4
|
+
// LOCKED: mutation of ONE install prefix — its `node_modules/` tree, its `gjsify-lock.json`, and
|
|
5
|
+
// the user-global prefix `gjsify install -g` extracts into. Two processes installing into the
|
|
6
|
+
// SAME prefix interleaved `rmSync` + extract on the same destinations (corrupt trees, spurious
|
|
7
|
+
// ENOENT mid-extract) and tore the lockfile. Different prefixes stay fully concurrent.
|
|
11
8
|
//
|
|
12
|
-
//
|
|
13
|
-
// (
|
|
14
|
-
//
|
|
15
|
-
// writers are already safe lock-free — identical-content last-rename-wins.
|
|
9
|
+
// NOT LOCKED: the shared XDG caches (`$XDG_CACHE_HOME/gjsify/{tarballs,metadata}`) — they are
|
|
10
|
+
// content-addressed and written via atomic tmp+`rename` (`install-cache-fs.ts`), so concurrent
|
|
11
|
+
// writers are already safe lock-free.
|
|
16
12
|
//
|
|
17
|
-
// Mechanism: `mkdir` of `<prefix>/node_modules/.gjsify-install-lock` is the
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// happily overwrites.) An `owner.json` inside the dir records pid + start
|
|
23
|
-
// time for staleness checks and diagnostics.
|
|
13
|
+
// Mechanism: `mkdir` of `<prefix>/node_modules/.gjsify-install-lock` is the atomic acquire, with
|
|
14
|
+
// exclusive-create semantics on every runtime targeted (Node's `mkdirSync` throws EEXIST; the
|
|
15
|
+
// GJS fs polyfill maps Gio's G_IO_ERROR_EXISTS to the same code). A lock FILE with `flag: 'wx'`
|
|
16
|
+
// would NOT work — the GJS `writeFileSync` polyfill ignores open flags and overwrites. An
|
|
17
|
+
// `owner.json` inside records pid + start time for staleness checks.
|
|
24
18
|
//
|
|
25
|
-
// Staleness
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
// grace period (the owner died between the two steps).
|
|
35
|
-
// The steal itself is race-free: the stale dir is claimed via an atomic
|
|
36
|
-
// `rename` to a unique sibling name — exactly one contender's rename
|
|
37
|
-
// succeeds; losers see ENOENT and simply retry the acquire loop.
|
|
19
|
+
// Staleness — a crashed install cannot release its lock, so contenders steal when
|
|
20
|
+
// (a) the owner pid is provably dead (`/proc/<pid>` on Linux, readable under Node and GJS
|
|
21
|
+
// alike; a kill(0) probe covers non-/proc Nodes), or
|
|
22
|
+
// (b) the lock outlives GJSIFY_INSTALL_LOCK_STALE_MS (default 35 min — just above the 30-min
|
|
23
|
+
// install budget, so a budget-bound install always exits before its lock can be stolen out
|
|
24
|
+
// from under it), or
|
|
25
|
+
// (c) `mkdir` succeeded but `owner.json` never appeared within a short grace period.
|
|
26
|
+
// The steal is race-free: the stale dir is claimed by an atomic `rename` to a unique sibling, so
|
|
27
|
+
// exactly one contender wins and losers see ENOENT and retry the acquire loop.
|
|
38
28
|
//
|
|
39
|
-
// Re-entrant per process: `workspaceInstall` holds the root-prefix lock for
|
|
40
|
-
//
|
|
41
|
-
// re-acquire the same prefix — those just bump a refcount.
|
|
29
|
+
// Re-entrant per process: `workspaceInstall` holds the root-prefix lock for the whole workspace
|
|
30
|
+
// flow while inner `installPackagesNative` calls re-acquire the same prefix and just refcount.
|
|
42
31
|
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// that already serializes installs).
|
|
32
|
+
// GJSIFY_INSTALL_LOCK=0 disables locking entirely — for a network filesystem with broken
|
|
33
|
+
// rename/mkdir atomicity, or an outer tool that already serializes installs.
|
|
46
34
|
import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
47
35
|
import { dirname, join, resolve } from 'node:path';
|
|
48
36
|
const LOCK_DIR_NAME = '.gjsify-install-lock';
|
|
@@ -75,18 +63,15 @@ function lockingDisabled() {
|
|
|
75
63
|
/**
|
|
76
64
|
* Is `pid` a live process on this machine?
|
|
77
65
|
*
|
|
78
|
-
* `/proc/<pid>` is the reliable cross-runtime probe on Linux
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* Unknown outcomes are treated as ALIVE (conservative — never steal a lock
|
|
84
|
-
* we cannot prove abandoned; the mtime staleness budget still applies).
|
|
66
|
+
* `/proc/<pid>` is the reliable cross-runtime probe on Linux, identical under Node and GJS;
|
|
67
|
+
* elsewhere a signal-0 probe (ESRCH dead, EPERM alive-but-foreign). The GJS `process.kill`
|
|
68
|
+
* polyfill shells out and cannot report ESRCH, but GJS only runs on /proc platforms in practice,
|
|
69
|
+
* so the fallback branch is Node's. Unknown outcomes count as ALIVE — never steal a lock that
|
|
70
|
+
* cannot be proven abandoned; the mtime staleness budget still applies.
|
|
85
71
|
*/
|
|
86
72
|
function isPidAlive(pid) {
|
|
87
|
-
// `existsSync` never throws by contract: Node returns false on any error,
|
|
88
|
-
//
|
|
89
|
-
// throw path in the GIR). Off Linux the `if` is simply false.
|
|
73
|
+
// `existsSync` never throws by contract: Node returns false on any error, and the GJS shim is
|
|
74
|
+
// a typeof guard + Gio `query_exists`, neither with a throw path in the GIR.
|
|
90
75
|
if (existsSync('/proc/self'))
|
|
91
76
|
return existsSync(`/proc/${pid}`);
|
|
92
77
|
try {
|
|
@@ -122,9 +107,8 @@ function assessLock(lockDir) {
|
|
|
122
107
|
}
|
|
123
108
|
const started = typeof owner.startedAt === 'number' ? owner.startedAt : mtimeMs(ownerPath);
|
|
124
109
|
if (started !== null && now - started > staleMs()) {
|
|
125
|
-
// Alive but far beyond any plausible install duration — the
|
|
126
|
-
//
|
|
127
|
-
// process can't block every future install of this prefix.
|
|
110
|
+
// Alive but far beyond any plausible install duration — the observed 0%-CPU
|
|
111
|
+
// hung-install shape. Steal, so one wedged process cannot block this prefix forever.
|
|
128
112
|
return {
|
|
129
113
|
stale: true,
|
|
130
114
|
reason: `owner pid ${owner.pid} exceeded the ${staleMs()}ms staleness budget`,
|
|
@@ -133,8 +117,8 @@ function assessLock(lockDir) {
|
|
|
133
117
|
}
|
|
134
118
|
return { stale: false, ownerPid: owner.pid };
|
|
135
119
|
}
|
|
136
|
-
// No readable owner.json: the winner may be between mkdir and the owner
|
|
137
|
-
//
|
|
120
|
+
// No readable owner.json: the winner may be between mkdir and the owner write, so allow a
|
|
121
|
+
// grace period before treating the dir as abandoned.
|
|
138
122
|
const dirMtime = mtimeMs(lockDir);
|
|
139
123
|
if (dirMtime === null)
|
|
140
124
|
return { stale: false }; // vanished — retry mkdir
|
|
@@ -144,9 +128,8 @@ function assessLock(lockDir) {
|
|
|
144
128
|
return { stale: false };
|
|
145
129
|
}
|
|
146
130
|
/**
|
|
147
|
-
* Atomically claim + remove a stale lock dir.
|
|
148
|
-
*
|
|
149
|
-
* contender got there first — the caller just retries its acquire loop.
|
|
131
|
+
* Atomically claim + remove a stale lock dir. `true` when THIS process won the claim, `false`
|
|
132
|
+
* when another contender got there first — the caller just retries its acquire loop.
|
|
150
133
|
*/
|
|
151
134
|
function stealStaleLock(lockDir) {
|
|
152
135
|
const claimed = `${lockDir}.stale-${process.pid}-${++stealCounter}`;
|
|
@@ -206,8 +189,8 @@ function makeHandle(lockDir) {
|
|
|
206
189
|
if (entry.count > 0)
|
|
207
190
|
return;
|
|
208
191
|
held.delete(lockDir);
|
|
209
|
-
// Only remove the on-disk dir when it is still OURS — a staleness
|
|
210
|
-
//
|
|
192
|
+
// Only remove the on-disk dir when it is still OURS — a staleness steal could have
|
|
193
|
+
// handed the path to another process meanwhile.
|
|
211
194
|
try {
|
|
212
195
|
const owner = JSON.parse(readFileSync(join(lockDir, OWNER_FILE), 'utf-8'));
|
|
213
196
|
if (owner.nonce !== undefined && owner.nonce !== entry.nonce)
|
|
@@ -226,12 +209,11 @@ function makeHandle(lockDir) {
|
|
|
226
209
|
};
|
|
227
210
|
}
|
|
228
211
|
/**
|
|
229
|
-
* Acquire the cross-process install lock for `prefix
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
* to the overall install budget so a waiter never outlives its own timeout.
|
|
212
|
+
* Acquire the cross-process install lock for `prefix`, resolving once this process may mutate
|
|
213
|
+
* `<prefix>/node_modules` + `<prefix>/gjsify-lock.json`. The caller MUST `release()` in a
|
|
214
|
+
* `finally`. Waits with backoff while a live install holds the lock, and steals provably-stale
|
|
215
|
+
* ones. `signal` aborts the wait — wired to the install budget so a waiter never outlives its
|
|
216
|
+
* own timeout.
|
|
235
217
|
*/
|
|
236
218
|
export async function acquireInstallLock(prefix, opts = {}) {
|
|
237
219
|
if (lockingDisabled()) {
|
|
@@ -241,8 +223,7 @@ export async function acquireInstallLock(prefix, opts = {}) {
|
|
|
241
223
|
},
|
|
242
224
|
};
|
|
243
225
|
}
|
|
244
|
-
// Canonicalize so two spellings of
|
|
245
|
-
// path) share one re-entrancy slot.
|
|
226
|
+
// Canonicalize so two spellings of one prefix (symlink, relative path) share a re-entrancy slot.
|
|
246
227
|
let canonicalPrefix;
|
|
247
228
|
try {
|
|
248
229
|
canonicalPrefix = realpathSync(prefix);
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { Packument } from '@gjsify/npm-registry';
|
|
2
2
|
/**
|
|
3
3
|
* Which document shape an entry holds. `'corgi'` = the abbreviated
|
|
4
|
-
* `application/vnd.npm.install-v1+json` install document
|
|
5
|
-
*
|
|
6
|
-
* caches are describable in one sentence.
|
|
4
|
+
* `application/vnd.npm.install-v1+json` install document; `'full'` =
|
|
5
|
+
* `application/json`, the only one carrying `libc`. Same names pacote uses.
|
|
7
6
|
*/
|
|
8
7
|
export type PackumentShape = 'corgi' | 'full';
|
|
9
8
|
export interface CachedPackument {
|
|
@@ -14,47 +13,42 @@ export interface CachedPackument {
|
|
|
14
13
|
* The cache key for a (shape, registry, name) triple — exported so the filename
|
|
15
14
|
* rule below is a PROPERTY a test can assert, not a claim in a comment.
|
|
16
15
|
*
|
|
17
|
-
* THE SEPARATOR IS NOT COSMETIC.
|
|
18
|
-
* `
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* cache
|
|
22
|
-
*
|
|
23
|
-
* it was silently DEAD, and the `If-None-Match` → `304` revalidation that is the
|
|
24
|
-
* entire reason this file exists never engaged once.
|
|
16
|
+
* THE SEPARATOR IS NOT COSMETIC. `|` is a Win32 RESERVED filename character, so
|
|
17
|
+
* while it was the separator every write there failed with `EINVAL` and nothing
|
|
18
|
+
* reported it: `atomicWrite` is best-effort on purpose (a read-only cache volume
|
|
19
|
+
* must not break an install) and `readCacheFile` treats an unreadable path as a
|
|
20
|
+
* MISS. The cache was not broken loudly on Windows, it was silently DEAD, and the
|
|
21
|
+
* `If-None-Match` → `304` revalidation this file exists for never engaged once.
|
|
25
22
|
*
|
|
26
|
-
* `,` is legal on Win32 AND escaped by `encodeURIComponent`, which
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* and it LEADS so that listing a shard groups by shape.
|
|
23
|
+
* `,` is legal on Win32 AND escaped by `encodeURIComponent`, which keeps a flat
|
|
24
|
+
* three-part name injective: an encoded part cannot contain the separator, so
|
|
25
|
+
* exactly one triple maps to any filename. The shape needs no encoding — one of two
|
|
26
|
+
* literals — and LEADS so listing a shard groups by shape.
|
|
31
27
|
*
|
|
32
|
-
* The shard key and the filename
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* of a function whose other half looked correct.
|
|
28
|
+
* The shard key and the filename must derive from the SAME encoded parts; hashing
|
|
29
|
+
* the raw values while naming with the encoded ones is how a reserved character
|
|
30
|
+
* survived review of a function whose other half looked correct.
|
|
36
31
|
*/
|
|
37
32
|
export declare function packumentCacheKey(registry: string, name: string, shape: PackumentShape): string;
|
|
38
33
|
/**
|
|
39
|
-
* Read a cached packument + its ETag for `(registry, name)` in the given
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* re-fetches).
|
|
34
|
+
* Read a cached packument + its ETag for `(registry, name)` in the given document
|
|
35
|
+
* `shape`. Returns `null` on a miss, when the cache is disabled, or when the entry
|
|
36
|
+
* is unreadable/corrupt (treated as a miss — the caller re-fetches).
|
|
43
37
|
*
|
|
44
|
-
* `shape`
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
38
|
+
* The `shape` must be the one the body was fetched under: the two documents are
|
|
39
|
+
* different bodies with independently-versioned ETags behind ONE URL, so reading
|
|
40
|
+
* across shapes returns a `libc`-less body that looks exactly like a hit. The
|
|
41
|
+
* install backend resolves with `'full'`; the `'corgi'` default serves callers that
|
|
42
|
+
* do not escalate.
|
|
48
43
|
*/
|
|
49
44
|
export declare function getCachedPackument(registry: string, name: string, shape?: PackumentShape): CachedPackument | null;
|
|
50
45
|
/**
|
|
51
46
|
* Persist a packument + ETag for `(registry, name)` under its document `shape`.
|
|
52
47
|
* Writes to a `<path>.tmp.<pid>` sibling then atomically renames so a concurrent
|
|
53
|
-
* reader never sees a partial write. No-op when the cache is disabled, the ETag
|
|
54
|
-
*
|
|
55
|
-
*
|
|
48
|
+
* reader never sees a partial write. No-op when the cache is disabled, the ETag is
|
|
49
|
+
* empty, or the write fails (a read-only / out-of-disk cache volume must not break
|
|
50
|
+
* the install).
|
|
56
51
|
*
|
|
57
|
-
* `shape`
|
|
58
|
-
* shape valid; it must match the `accept` header the body was fetched with.
|
|
52
|
+
* `shape` must match the `accept` header the body was fetched with.
|
|
59
53
|
*/
|
|
60
54
|
export declare function putCachedPackument(registry: string, name: string, etag: string, packument: Packument, shape?: PackumentShape): void;
|