@gjsify/cli 0.35.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.
Files changed (124) hide show
  1. package/dist/cli.gjs.mjs +136 -126
  2. package/lib/actions/build.d.ts +19 -0
  3. package/lib/actions/build.js +2 -2
  4. package/lib/bundler-pick.js +72 -98
  5. package/lib/cli-fail.d.ts +4 -7
  6. package/lib/cli-fail.js +29 -55
  7. package/lib/commands/affected.js +161 -286
  8. package/lib/commands/install.d.ts +63 -66
  9. package/lib/commands/install.js +372 -536
  10. package/lib/commands/run.d.ts +1 -0
  11. package/lib/commands/run.js +37 -0
  12. package/lib/templates/install.mjs.tmpl +38 -68
  13. package/lib/types/cli-build-options.d.ts +33 -67
  14. package/lib/types/command.d.ts +9 -12
  15. package/lib/types/config-data.d.ts +125 -221
  16. package/lib/utils/auth-npmrc.d.ts +5 -10
  17. package/lib/utils/auth-npmrc.js +11 -21
  18. package/lib/utils/check-system-deps.d.ts +16 -0
  19. package/lib/utils/check-system-deps.js +49 -0
  20. package/lib/utils/clear-targets.d.ts +8 -11
  21. package/lib/utils/clear-targets.js +22 -29
  22. package/lib/utils/copy-targets.d.ts +13 -19
  23. package/lib/utils/copy-targets.js +31 -44
  24. package/lib/utils/dep-aggregation.d.ts +7 -15
  25. package/lib/utils/dep-aggregation.js +9 -22
  26. package/lib/utils/detect-native-packages.d.ts +83 -116
  27. package/lib/utils/detect-native-packages.js +111 -149
  28. package/lib/utils/dir-link.d.ts +29 -48
  29. package/lib/utils/dir-link.js +33 -56
  30. package/lib/utils/discover-showcases.d.ts +6 -7
  31. package/lib/utils/discover-showcases.js +11 -15
  32. package/lib/utils/dlx-cache.d.ts +29 -38
  33. package/lib/utils/dlx-cache.js +53 -63
  34. package/lib/utils/force-exit.d.ts +0 -3
  35. package/lib/utils/force-exit.js +13 -24
  36. package/lib/utils/gi-search-path.d.ts +16 -24
  37. package/lib/utils/gi-search-path.js +54 -82
  38. package/lib/utils/gjs-bundle-guard.d.ts +5 -13
  39. package/lib/utils/gjs-bundle-guard.js +35 -57
  40. package/lib/utils/gjs-source-escape.d.ts +2 -2
  41. package/lib/utils/gjs-source-escape.js +21 -39
  42. package/lib/utils/gjsify-shim.d.ts +26 -8
  43. package/lib/utils/gjsify-shim.js +128 -80
  44. package/lib/utils/html-entry.d.ts +8 -12
  45. package/lib/utils/html-entry.js +19 -28
  46. package/lib/utils/install-backend-native.d.ts +123 -161
  47. package/lib/utils/install-backend-native.js +536 -737
  48. package/lib/utils/install-backend.d.ts +42 -57
  49. package/lib/utils/install-backend.js +16 -21
  50. package/lib/utils/install-cache-fs.d.ts +10 -15
  51. package/lib/utils/install-cache-fs.js +18 -26
  52. package/lib/utils/install-lock.d.ts +5 -6
  53. package/lib/utils/install-lock.js +46 -65
  54. package/lib/utils/install-packument-cache.d.ts +27 -33
  55. package/lib/utils/install-packument-cache.js +44 -57
  56. package/lib/utils/install-progress.d.ts +2 -9
  57. package/lib/utils/install-progress.js +14 -39
  58. package/lib/utils/install-tarball-cache.d.ts +14 -20
  59. package/lib/utils/install-tarball-cache.js +41 -59
  60. package/lib/utils/library-output.d.ts +9 -22
  61. package/lib/utils/library-output.js +19 -37
  62. package/lib/utils/load-npmrc.d.ts +5 -6
  63. package/lib/utils/load-npmrc.js +18 -27
  64. package/lib/utils/node-bundle-guard.d.ts +4 -8
  65. package/lib/utils/node-bundle-guard.js +21 -35
  66. package/lib/utils/node-script.d.ts +12 -0
  67. package/lib/utils/node-script.js +95 -0
  68. package/lib/utils/node-version.d.ts +5 -14
  69. package/lib/utils/node-version.js +15 -24
  70. package/lib/utils/normalize-bundler-options.d.ts +7 -11
  71. package/lib/utils/normalize-bundler-options.js +24 -42
  72. package/lib/utils/npm-oidc.d.ts +23 -37
  73. package/lib/utils/npm-oidc.js +28 -53
  74. package/lib/utils/npm-otp-cache.d.ts +4 -4
  75. package/lib/utils/npm-otp-cache.js +20 -25
  76. package/lib/utils/npm-otp.d.ts +20 -30
  77. package/lib/utils/npm-otp.js +32 -51
  78. package/lib/utils/onboard-probe.d.ts +7 -13
  79. package/lib/utils/onboard-probe.js +16 -27
  80. package/lib/utils/parse-spec.d.ts +3 -3
  81. package/lib/utils/parse-spec.js +4 -5
  82. package/lib/utils/pkg-json-edit.d.ts +7 -12
  83. package/lib/utils/pkg-json-edit.js +13 -26
  84. package/lib/utils/pkg-json.d.ts +11 -18
  85. package/lib/utils/pkg-json.js +19 -28
  86. package/lib/utils/prefixed-output.d.ts +3 -4
  87. package/lib/utils/prefixed-output.js +13 -19
  88. package/lib/utils/prompt.d.ts +3 -4
  89. package/lib/utils/prompt.js +14 -18
  90. package/lib/utils/publish-diagnose.d.ts +11 -16
  91. package/lib/utils/publish-diagnose.js +15 -28
  92. package/lib/utils/publish-headers.d.ts +7 -11
  93. package/lib/utils/publish-headers.js +36 -65
  94. package/lib/utils/publishable-packages.d.ts +4 -6
  95. package/lib/utils/publishable-packages.js +14 -18
  96. package/lib/utils/resolve-gjs-entry.d.ts +5 -8
  97. package/lib/utils/resolve-gjs-entry.js +10 -27
  98. package/lib/utils/resolve-npm-package.d.ts +6 -11
  99. package/lib/utils/resolve-npm-package.js +29 -63
  100. package/lib/utils/resolve-plugin-by-name.d.ts +11 -19
  101. package/lib/utils/resolve-plugin-by-name.js +9 -14
  102. package/lib/utils/run-gjs.d.ts +19 -17
  103. package/lib/utils/run-gjs.js +37 -51
  104. package/lib/utils/run-lifecycle-script.d.ts +5 -9
  105. package/lib/utils/run-lifecycle-script.js +23 -42
  106. package/lib/utils/run-node.d.ts +23 -33
  107. package/lib/utils/run-node.js +56 -60
  108. package/lib/utils/runtimes.d.ts +25 -35
  109. package/lib/utils/runtimes.js +34 -46
  110. package/lib/utils/spawn.d.ts +32 -48
  111. package/lib/utils/spawn.js +51 -84
  112. package/lib/utils/stdin.d.ts +2 -2
  113. package/lib/utils/stdin.js +10 -25
  114. package/lib/utils/suggest.d.ts +6 -7
  115. package/lib/utils/suggest.js +8 -10
  116. package/lib/utils/system-gi.d.ts +18 -27
  117. package/lib/utils/system-gi.js +60 -114
  118. package/lib/utils/trust-registry.d.ts +4 -8
  119. package/lib/utils/trust-registry.js +7 -15
  120. package/lib/utils/win32-command.d.ts +8 -14
  121. package/lib/utils/win32-command.js +29 -43
  122. package/lib/utils/workspace-root.js +7 -12
  123. package/package.json +18 -17
  124. package/showcases.json +6 -0
@@ -1,13 +1,11 @@
1
1
  // On-disk packument metadata cache for `gjsify install`.
2
2
  //
3
3
  // Sibling to the content-addressable tarball cache (install-tarball-cache.ts).
4
- // Stores each package's abbreviated packument plus the registry `ETag` it came
5
- // with, so a re-resolve (lockfile miss — e.g. a dependency range changed) can
6
- // send a conditional `If-None-Match` and turn the unchanged majority into empty
7
- // `304 Not Modified` responses — a real bandwidth + parse saving on repeated /
8
- // dep-churn installs, on top of the ~4× gzip transfer for the changed minority
9
- // (packuments are fetched gzip-compressed and decoded by the fetch layer; see
10
- // `@gjsify/npm-registry` `fetchPackumentConditional`).
4
+ // Stores each package's packument plus the registry `ETag` it came with, so a
5
+ // re-resolve (lockfile miss — e.g. a dependency range changed) can send a
6
+ // conditional `If-None-Match` and turn the unchanged majority into empty
7
+ // `304 Not Modified` responses, on top of the ~4× gzip transfer for the changed
8
+ // minority (see `@gjsify/npm-registry` `fetchPackumentConditional`).
11
9
  //
12
10
  // Mirrors pnpm's metadata cache + npm's make-fetch-happen HTTP-cache layer.
13
11
  //
@@ -15,32 +13,26 @@
15
13
  //
16
14
  // $XDG_CACHE_HOME/gjsify/metadata/v3/<shard>/<shape>,<encoded-registry>,<encoded-name>.json
17
15
  //
18
- // The cache is keyed by (SHAPE, registry, name) — NOT name alone — so switching
19
- // the registry for a scope can never serve a packument from the wrong source on
20
- // a coincidental ETag match. `<shard>` is a 2-hex FNV-1a digest of the key, a
21
- // directory-fan-out step so the leaf dir never grows unbounded (same rationale
22
- // as the tarball cache's hex sharding). `v2` is a layout version.
16
+ // Keyed by (SHAPE, registry, name) — NOT name alone — so switching the registry
17
+ // for a scope can never serve a packument from the wrong source on a coincidental
18
+ // ETag match. `<shard>` is a 2-hex FNV-1a digest of the key, directory fan-out so
19
+ // the leaf dir never grows unbounded (as with the tarball cache's hex sharding).
23
20
  //
24
- // WHY THE SHAPE IS PART OF THE KEY (the v1 → v2 bump): the same packument URL
25
- // serves TWO different documents — the abbreviated "corgi" install document and
26
- // the full one — and only the full one carries a version's `libc` field (see
27
- // `@gjsify/npm-registry` `FetchOptions.fullMetadata`). With one key per (registry,
28
- // name), an escalated read would be answered by whichever shape happened to be
29
- // cached first: a musl-only package would come back `libc`-less, be judged
30
- // compatible on a glibc host, and get installed — a WRONG ANSWER that looks
31
- // exactly like a cache hit, with no failure until something dlopens the binary.
32
- // pacote prefixes its own cache key with `full:`/`corgi:` for precisely this
33
- // reason. The layout bump (rather than just a longer filename) makes the v1
34
- // entries — written before the distinction existed and therefore of unknown
35
- // shape — unreachable instead of ambiguous.
21
+ // WHY THE SHAPE IS PART OF THE KEY: one packument URL serves TWO documents — the
22
+ // abbreviated "corgi" install document and the full one — and only the full one
23
+ // carries a version's `libc` (`@gjsify/npm-registry` `FetchOptions.fullMetadata`).
24
+ // Keyed per (registry, name) alone, a read is answered by whichever shape was cached
25
+ // first, so a musl-only package comes back `libc`-less, is judged compatible on a
26
+ // glibc host, and installs — a wrong answer indistinguishable from a cache hit, with
27
+ // no failure until something dlopens the binary. pacote prefixes its own key with
28
+ // `full:`/`corgi:` for the same reason.
36
29
  //
37
30
  // Disabled with `GJSIFY_PACKUMENT_CACHE=0` (or `false`). Honours
38
31
  // `XDG_CACHE_HOME` like the tarball + dlx caches.
39
32
  import { join } from 'node:path';
40
33
  import { atomicWrite, gjsifyCacheRoot, readCacheFile } from './install-cache-fs.js';
41
- // v3, and BOTH halves of the bump matter. v2 moved the separator off the
42
- // Win32-reserved `|` (see `packumentCacheKey`); v3 additionally makes the
43
- // document SHAPE part of the key. Either change alone makes older entries
34
+ // v2 moved the separator off the Win32-reserved `|` (see `packumentCacheKey`); v3
35
+ // made the document SHAPE part of the key. Either change alone makes older entries
44
36
  // unreachable rather than merely stale, so a new root retires them instead of
45
37
  // leaving ambiguous orphans under the old one.
46
38
  const CACHE_LAYOUT_VERSION = 'v3';
@@ -80,25 +72,21 @@ function encodePart(part) {
80
72
  * The cache key for a (shape, registry, name) triple — exported so the filename
81
73
  * rule below is a PROPERTY a test can assert, not a claim in a comment.
82
74
  *
83
- * THE SEPARATOR IS NOT COSMETIC. It used to be `|`, producing names like
84
- * `corgi|https%3A%2F%2Fregistry.npmjs.org%2F|%40gjsify%2Fnode-gi.json` — and `|`
85
- * is a Win32 RESERVED filename character, so every write failed with `EINVAL`.
86
- * Nothing reported it: `atomicWrite` is best-effort on purpose (a read-only
87
- * cache volume must not break an install) and `readCacheFile` reads an
88
- * unreadable path as a MISS. So on Windows this cache was never broken loudly —
89
- * it was silently DEAD, and the `If-None-Match` → `304` revalidation that is the
90
- * entire reason this file exists never engaged once.
75
+ * THE SEPARATOR IS NOT COSMETIC. `|` is a Win32 RESERVED filename character, so
76
+ * while it was the separator every write there failed with `EINVAL` and nothing
77
+ * reported it: `atomicWrite` is best-effort on purpose (a read-only cache volume
78
+ * must not break an install) and `readCacheFile` treats an unreadable path as a
79
+ * MISS. The cache was not broken loudly on Windows, it was silently DEAD, and the
80
+ * `If-None-Match` → `304` revalidation this file exists for never engaged once.
91
81
  *
92
- * `,` is legal on Win32 AND escaped by `encodeURIComponent`, which is what keeps
93
- * a flat three-part name injective: an encoded part cannot contain the
94
- * separator, so exactly one triple maps to any given filename. The shape needs
95
- * no encoding — it is one of two literals, neither containing the separator —
96
- * and it LEADS so that listing a shard groups by shape.
82
+ * `,` is legal on Win32 AND escaped by `encodeURIComponent`, which keeps a flat
83
+ * three-part name injective: an encoded part cannot contain the separator, so
84
+ * exactly one triple maps to any filename. The shape needs no encoding — one of two
85
+ * literals — and LEADS so listing a shard groups by shape.
97
86
  *
98
- * The shard key and the filename are derived from the SAME encoded parts. They
99
- * used to disagree — the key hashed the raw values while the name used the
100
- * encoded ones — which is how a reserved character in the name survived review
101
- * of a function whose other half looked correct.
87
+ * The shard key and the filename must derive from the SAME encoded parts; hashing
88
+ * the raw values while naming with the encoded ones is how a reserved character
89
+ * survived review of a function whose other half looked correct.
102
90
  */
103
91
  export function packumentCacheKey(registry, name, shape) {
104
92
  return `${shape},${encodePart(registry)},${encodePart(name)}`;
@@ -111,15 +99,15 @@ function pathFor(registry, name, shape) {
111
99
  return join(cacheRoot(), shardFor(key), `${key}.json`);
112
100
  }
113
101
  /**
114
- * Read a cached packument + its ETag for `(registry, name)` in the given
115
- * document `shape`. Returns `null` on a miss, when the cache is disabled, or
116
- * when the entry is unreadable/corrupt (treated as a miss — the caller just
117
- * re-fetches).
102
+ * Read a cached packument + its ETag for `(registry, name)` in the given document
103
+ * `shape`. Returns `null` on a miss, when the cache is disabled, or when the entry
104
+ * is unreadable/corrupt (treated as a miss — the caller re-fetches).
118
105
  *
119
- * `shape` defaults to `'corgi'` because that is what a resolve fetches and what
120
- * every caller predating the full-document escalation meant. A caller that
121
- * escalated MUST pass `'full'` — reading the corgi entry would return a body
122
- * with no `libc`, i.e. the exact wrong answer the shape key exists to prevent.
106
+ * The `shape` must be the one the body was fetched under: the two documents are
107
+ * different bodies with independently-versioned ETags behind ONE URL, so reading
108
+ * across shapes returns a `libc`-less body that looks exactly like a hit. The
109
+ * install backend resolves with `'full'`; the `'corgi'` default serves callers that
110
+ * do not escalate.
123
111
  */
124
112
  export function getCachedPackument(registry, name, shape = 'corgi') {
125
113
  const path = pathFor(registry, name, shape);
@@ -143,12 +131,11 @@ export function getCachedPackument(registry, name, shape = 'corgi') {
143
131
  /**
144
132
  * Persist a packument + ETag for `(registry, name)` under its document `shape`.
145
133
  * Writes to a `<path>.tmp.<pid>` sibling then atomically renames so a concurrent
146
- * reader never sees a partial write. No-op when the cache is disabled, the ETag
147
- * is empty, or the write fails (read-only / out-of-disk cache volume must not
148
- * break the install).
134
+ * reader never sees a partial write. No-op when the cache is disabled, the ETag is
135
+ * empty, or the write fails (a read-only / out-of-disk cache volume must not break
136
+ * the install).
149
137
  *
150
- * `shape` is last and defaults to `'corgi'` to keep the pre-escalation call
151
- * shape valid; it must match the `accept` header the body was fetched with.
138
+ * `shape` must match the `accept` header the body was fetched with.
152
139
  */
153
140
  export function putCachedPackument(registry, name, etag, packument, shape = 'corgi') {
154
141
  if (!etag)
@@ -7,19 +7,12 @@ export interface ProgressEvent {
7
7
  name?: string;
8
8
  }
9
9
  export interface ProgressReporter {
10
- /** Called for each step within a phase. */
11
10
  update(event: ProgressEvent): void;
12
- /** Called once when a phase begins; lets the reporter print a header line. */
13
11
  beginPhase(phase: ProgressPhase, total: number): void;
14
- /** Called once when a phase completes; clears the inline progress bar. */
12
+ /** Clears the inline bar and replaces it with a completion line. */
15
13
  endPhase(phase: ProgressPhase): void;
16
14
  }
17
- /**
18
- * Make a progress reporter that auto-targets `process.stderr`.
19
- * - `enabled=false` → silent.
20
- * - stderr is not a TTY → fall back to one line per phase (begin + end).
21
- * - stderr is a TTY → live single-line progress bar (\r-updated, 30fps).
22
- */
15
+ /** Pick the reporter for `process.stderr` (or `opts.stream`): silent, plain, or TTY bar. */
23
16
  export declare function makeProgressReporter(opts?: {
24
17
  enabled?: boolean;
25
18
  stream?: NodeJS.WriteStream;
@@ -1,16 +1,7 @@
1
- // Progress reporter for `gjsify install` + `gjsify dlx`.
2
- //
3
- // Auto-enables on TTY stderr. Renders a single-line progress bar that gets
4
- // overwritten via `\r`, mirroring what `yarn install` / `pnpm install` show:
5
- //
6
- // resolving [████████░░░░░░░░] 234/500 typescript@^6.0.3
7
- //
8
- // Falls back to a chatty mode (one line per completed step) when stderr is
9
- // not a TTY, e.g. when piped into a log file or running on CI.
10
- //
11
- // Caller plumbing — the install backend calls `report({phase, current, total,
12
- // name})` after every resolve/download/extract step; the renderer rate-limits
13
- // to ~30fps so a tight inner loop doesn't drown the terminal.
1
+ // Progress reporter for `gjsify install` + `gjsify dlx`. The backend reports every
2
+ // resolve/download/extract/link step; on TTY stderr this renders one `\r`-overwritten
3
+ // bar, rate-limited to ~30fps so a tight inner loop cannot drown the terminal, and
4
+ // on a non-TTY it degrades to the heartbeat mode described below.
14
5
  const PHASE_LABEL = {
15
6
  resolve: 'resolving',
16
7
  download: 'downloading',
@@ -22,12 +13,7 @@ const NOOP_REPORTER = {
22
13
  beginPhase() { },
23
14
  endPhase() { },
24
15
  };
25
- /**
26
- * Make a progress reporter that auto-targets `process.stderr`.
27
- * - `enabled=false` → silent.
28
- * - stderr is not a TTY → fall back to one line per phase (begin + end).
29
- * - stderr is a TTY → live single-line progress bar (\r-updated, 30fps).
30
- */
16
+ /** Pick the reporter for `process.stderr` (or `opts.stream`): silent, plain, or TTY bar. */
31
17
  export function makeProgressReporter(opts = {}) {
32
18
  if (opts.enabled === false)
33
19
  return NOOP_REPORTER;
@@ -38,7 +24,6 @@ export function makeProgressReporter(opts = {}) {
38
24
  return NOOP_REPORTER;
39
25
  return isTty ? makeTtyReporter(stream) : makePlainReporter(stream);
40
26
  }
41
- // ──── TTY reporter — single-line, \r-updated, 30fps rate-limit ────────────
42
27
  function makeTtyReporter(stream) {
43
28
  let lastRender = 0;
44
29
  let lastLine = '';
@@ -49,7 +34,7 @@ function makeTtyReporter(stream) {
49
34
  return;
50
35
  lastRender = now;
51
36
  lastLine = line;
52
- // Clear current line, write new content. \x1b[2K = erase entire line.
37
+ // `\x1b[2K` = erase entire line.
53
38
  stream.write('\r\x1b[2K' + line);
54
39
  }
55
40
  function clearLine() {
@@ -58,33 +43,26 @@ function makeTtyReporter(stream) {
58
43
  }
59
44
  return {
60
45
  beginPhase(phase, total) {
61
- // First frame so the user sees immediate feedback.
46
+ // Forced, so the first frame is not eaten by the rate limit.
62
47
  render(formatBar(phase, 0, total, undefined, stream.columns ?? 80), true);
63
48
  },
64
49
  update(ev) {
65
50
  render(formatBar(ev.phase, ev.current, ev.total, ev.name, stream.columns ?? 80));
66
51
  },
67
52
  endPhase(phase) {
68
- // Replace the live bar with a completion line so the user sees
69
- // a definitive "done" after the spinner stops.
53
+ // A definitive "done" line, or the stopped bar reads as a stall.
70
54
  const label = PHASE_LABEL[phase];
71
55
  clearLine();
72
56
  stream.write(`gjsify install: ${label} done\n`);
73
57
  },
74
58
  };
75
59
  }
76
- // ──── Plain reporter — phase begin/end + periodic heartbeat (non-TTY) ──────
77
- //
78
- // On a non-TTY (piped to a log file, CI, or captured output) the bar can't
79
- // `\r`-overwrite, so per-package lines are suppressed to avoid flooding the
80
- // log. But emitting NOTHING between `beginPhase` and `endPhase` makes a long
81
- // download/extract phase look frozen — the exact "no further output for 25+
82
- // minutes" symptom of a slow install. So we emit a low-frequency heartbeat:
83
- // at most one line per HEARTBEAT_MS, AND only after at least a few packages of
84
- // real progress, so a fast warm install (most nodes skipped) stays quiet while
85
- // a genuinely long phase reports `… N/M (X%)` every few seconds. This mirrors
86
- // what npm (`--loglevel http`) and pnpm log on non-TTY: progress you can watch
87
- // without it being one line per file.
60
+ // Non-TTY heartbeat. Per-package lines would flood a log file, but emitting
61
+ // NOTHING between `beginPhase` and `endPhase` makes a long download/extract look
62
+ // frozen — the exact "no further output for 25+ minutes" symptom of a slow install.
63
+ // So: at most one line per HEARTBEAT_MS, and only once `current` has actually
64
+ // advanced, which keeps a fast warm install (most nodes skipped) silent while a
65
+ // genuinely long phase reports `… N/M (X%)` every few seconds.
88
66
  const HEARTBEAT_MS = 5_000;
89
67
  function makePlainReporter(stream) {
90
68
  let lastHeartbeat = 0;
@@ -111,12 +89,10 @@ function makePlainReporter(stream) {
111
89
  },
112
90
  };
113
91
  }
114
- // ──── Bar formatter ────────────────────────────────────────────────────────
115
92
  function formatBar(phase, current, total, name, columns) {
116
93
  const label = PHASE_LABEL[phase];
117
94
  const ratio = total > 0 ? Math.min(1, current / total) : 0;
118
95
  const pct = `${current}/${total}`;
119
- // Reserve: "gjsify install: " (16) + label (max ~12) + " [" (2) + "] " (2) + pct (~8) + " " + name
120
96
  const prefix = `gjsify install: ${label} `;
121
97
  const suffix = ` ${pct}` + (name ? ` ${name}` : '');
122
98
  const barWidth = Math.max(8, Math.min(40, columns - prefix.length - suffix.length - 4));
@@ -124,7 +100,6 @@ function formatBar(phase, current, total, name, columns) {
124
100
  const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
125
101
  let line = `${prefix}[${bar}]${suffix}`;
126
102
  if (line.length > columns) {
127
- // Truncate name if the terminal is narrow.
128
103
  const overflow = line.length - columns + 1;
129
104
  const truncatedName = name && name.length > overflow ? name.slice(0, name.length - overflow - 1) + '…' : '';
130
105
  line = `${prefix}[${bar}] ${pct}${truncatedName ? ' ' + truncatedName : ''}`;
@@ -1,31 +1,25 @@
1
1
  /**
2
- * Read a cached tarball by SRI integrity. Returns the raw tarball bytes if
3
- * the cache has a HIT, `null` otherwise. A read failure (e.g. partial
4
- * write from an interrupted previous run) is treated as a MISS — the file
5
- * is left untouched so we don't trip a follow-up writer's atomic rename.
2
+ * Read a cached tarball by SRI integrity — raw bytes on a HIT, `null` otherwise. A
3
+ * read failure (e.g. a partial write from an interrupted run) is a MISS, and the
4
+ * file is left untouched so a follow-up writer's atomic rename is not tripped.
6
5
  */
7
6
  export declare function getCachedTarball(integrity: string | undefined): Uint8Array | null;
8
7
  /**
9
- * Persist a tarball to the cache. Writes to a `<path>.tmp.<pid>` sibling
10
- * then atomically renames into place so concurrent installs can never
11
- * observe a half-written entry. No-op when:
12
- * - `integrity` is missing / malformed (no cache key)
13
- * - the destination already exists (idempotent — content-addressed)
14
- * - the write fails (e.g. cache root is read-only) — silently degrade
15
- * so a cache-volume issue doesn't break the install itself.
8
+ * Persist a tarball to the cache, via a `<path>.tmp.<pid>` sibling and an atomic
9
+ * rename so concurrent installs never observe a half-written entry. No-op when
10
+ * `integrity` is missing/malformed, when the destination already exists
11
+ * (content-addressed, so immutable), or when the write fails — a read-only cache
12
+ * volume must not break the install itself.
16
13
  */
17
14
  export declare function putCachedTarball(integrity: string | undefined, bytes: Uint8Array): void;
18
- /**
19
- * Best-effort cache stats for diagnostics. Returns `null` when the cache
20
- * root doesn't exist yet (first run).
21
- */
15
+ /** The cache root, for diagnostics. Not probed — the directory may not exist yet. */
22
16
  export declare function cacheRootForLogging(): string;
23
17
  export declare function isCacheHit(integrity: string | undefined): boolean;
24
18
  /**
25
- * Read a tarball from npm's cacache content store by SRI integrity. Returns
26
- * the raw `.tgz` bytes on a HIT, `null` on a MISS / disabled interop / read
27
- * failure. Like {@link getCachedTarball}, this trusts the content-addressed
28
- * path rather than re-hashing — the extractor surfaces any genuinely corrupt
29
- * tarball loudly, and cacache verified the bytes on write.
19
+ * Read a tarball from npm's cacache content store by SRI integrity — raw `.tgz`
20
+ * bytes on a HIT, `null` on a MISS, disabled interop or read failure. Like
21
+ * {@link getCachedTarball} it trusts the content-addressed path rather than
22
+ * re-hashing: cacache verified the bytes on write, and the extractor surfaces a
23
+ * genuinely corrupt tarball loudly.
30
24
  */
31
25
  export declare function getForeignCachedTarball(integrity: string | undefined): Uint8Array | null;
@@ -1,35 +1,29 @@
1
1
  // Content-addressable tarball cache for `gjsify install`.
2
2
  //
3
- // Mirrors pnpm / yarn-berry's store layout: each tarball is stored once on
4
- // disk keyed by its SRI integrity hash (`sha512-…`). When the resolver hits
5
- // a node whose integrity is already cached, `fetchTarball` is skipped and
6
- // we read the bytes off the local filesystem instead.
3
+ // Mirrors pnpm / yarn-berry's store layout: each tarball is stored once on disk
4
+ // keyed by its SRI integrity hash (`sha512-…`), so a node whose integrity is
5
+ // already cached skips `fetchTarball` and reads the bytes off local disk.
7
6
  //
8
- // Why this matters: a cold install on this monorepo (200+ workspaces,
9
- // 600+ transitive deps) spends ~20 minutes at 80% CPU. Most of that time is
10
- // re-downloading + re-extracting the SAME tarballs that just came down in
11
- // the previous run because there is no cache between runs. With this cache,
12
- // the second `gjsify install` on the same repo skips every tarball fetch
13
- // and just goes straight to extract → drops well below 1 minute in practice.
7
+ // Measured on this monorepo (200+ workspaces, 600+ transitive deps): a cold
8
+ // install spends ~20 minutes at 80% CPU, most of it re-downloading tarballs the
9
+ // previous run had already fetched. With the cache the second `gjsify install`
10
+ // goes straight to extract and drops well under a minute.
14
11
  //
15
- // Layout (matches the pnpm pattern so we stay forward-compatible with
16
- // `~/.cache/gjsify/store` if we eventually share a store across projects):
12
+ // Layout, following the pnpm pattern:
17
13
  //
18
14
  // $XDG_CACHE_HOME/gjsify/tarballs/v1/<hex-prefix-2>/<full-hex>.tgz
19
15
  //
20
- // The 2-byte prefix is a directory-sharding step so the leaf directory
21
- // never gets pathologically large — same pattern git's loose objects use.
22
- // `v1` is a layout version so we can change the file shape (e.g. add a
23
- // manifest sidecar) without invalidating the world.
16
+ // The 2-byte prefix shards the directory so the leaf never gets pathologically
17
+ // large (git's loose-object pattern). `v1` is a layout version, so the file shape
18
+ // can change without invalidating the world.
24
19
  //
25
- // SRI integrity input → cache key:
20
+ // SRI integrity → cache key, by hex-encoding the base64 digest:
26
21
  //
27
22
  // "sha512-AbCd…==" → ("sha512", "ab/cdefg….tgz")
28
23
  //
29
- // We hex-encode the base64 SRI digest. Two integrities that produce the
30
- // same hex bytes share the same cache entry; that is the invariant pnpm
31
- // relies on too. Tarballs without an integrity hash (older registries)
32
- // fall through to a no-op cache and download every time.
24
+ // Two integrities with the same hex bytes share an entry, the invariant pnpm relies
25
+ // on too. Tarballs without an integrity hash (older registries) fall through to a
26
+ // no-op cache and download every time.
33
27
  import { Buffer } from 'node:buffer';
34
28
  import { existsSync, statSync } from 'node:fs';
35
29
  import { homedir } from 'node:os';
@@ -41,10 +35,9 @@ function cacheRoot() {
41
35
  return gjsifyCacheRoot('tarballs', CACHE_LAYOUT_VERSION);
42
36
  }
43
37
  /**
44
- * Parse an SRI integrity string (`sha512-AbCd…=`) into its algorithm + hex
45
- * digest, or `null` for a missing / malformed value (caller falls back to a
46
- * fresh download). Shared by both the gjsify-store and npm-cacache path
47
- * derivations below — the base64→hex decode used to be inlined in each.
38
+ * Parse an SRI integrity string (`sha512-AbCd…=`) into its algorithm + hex digest,
39
+ * or `null` for a missing / malformed value (the caller falls back to a fresh
40
+ * download). Shared by the gjsify-store and npm-cacache path derivations below.
48
41
  */
49
42
  function parseSri(integrity) {
50
43
  if (!integrity)
@@ -75,10 +68,9 @@ function pathFor(integrity) {
75
68
  return join(cacheRoot(), sri.algorithm, shard, `${sri.hex}.tgz`);
76
69
  }
77
70
  /**
78
- * Read a cached tarball by SRI integrity. Returns the raw tarball bytes if
79
- * the cache has a HIT, `null` otherwise. A read failure (e.g. partial
80
- * write from an interrupted previous run) is treated as a MISS — the file
81
- * is left untouched so we don't trip a follow-up writer's atomic rename.
71
+ * Read a cached tarball by SRI integrity — raw bytes on a HIT, `null` otherwise. A
72
+ * read failure (e.g. a partial write from an interrupted run) is a MISS, and the
73
+ * file is left untouched so a follow-up writer's atomic rename is not tripped.
82
74
  */
83
75
  export function getCachedTarball(integrity) {
84
76
  const path = pathFor(integrity);
@@ -87,13 +79,11 @@ export function getCachedTarball(integrity) {
87
79
  return readCacheFile(path);
88
80
  }
89
81
  /**
90
- * Persist a tarball to the cache. Writes to a `<path>.tmp.<pid>` sibling
91
- * then atomically renames into place so concurrent installs can never
92
- * observe a half-written entry. No-op when:
93
- * - `integrity` is missing / malformed (no cache key)
94
- * - the destination already exists (idempotent — content-addressed)
95
- * - the write fails (e.g. cache root is read-only) — silently degrade
96
- * so a cache-volume issue doesn't break the install itself.
82
+ * Persist a tarball to the cache, via a `<path>.tmp.<pid>` sibling and an atomic
83
+ * rename so concurrent installs never observe a half-written entry. No-op when
84
+ * `integrity` is missing/malformed, when the destination already exists
85
+ * (content-addressed, so immutable), or when the write fails — a read-only cache
86
+ * volume must not break the install itself.
97
87
  */
98
88
  export function putCachedTarball(integrity, bytes) {
99
89
  const path = pathFor(integrity);
@@ -104,10 +94,7 @@ export function putCachedTarball(integrity, bytes) {
104
94
  return;
105
95
  atomicWrite(path, bytes);
106
96
  }
107
- /**
108
- * Best-effort cache stats for diagnostics. Returns `null` when the cache
109
- * root doesn't exist yet (first run).
110
- */
97
+ /** The cache root, for diagnostics. Not probed — the directory may not exist yet. */
111
98
  export function cacheRootForLogging() {
112
99
  return cacheRoot();
113
100
  }
@@ -123,26 +110,21 @@ export function isCacheHit(integrity) {
123
110
  return false;
124
111
  }
125
112
  }
126
- // ---------------------------------------------------------------------------
127
113
  // Foreign cache interop — read npm's cacache content store.
128
114
  //
129
- // npm stores downloaded tarballs in a content-addressable store
130
- // (`cacache`) keyed by the SAME SRI integrity we key our own store on, and
131
- // holds the raw `.tgz` bytes verbatim. Layout (cacache `content-v2`):
115
+ // npm keys its content-addressable store on the SAME SRI integrity as ours and
116
+ // holds the raw `.tgz` bytes verbatim, so anyone who has run `npm install` already
117
+ // has these tarballs on disk and reading them turns a cold `gjsify install` into a
118
+ // near-warm one with no network round-trip. Layout (cacache `content-v2`):
132
119
  //
133
120
  // <npm-cache>/_cacache/content-v2/<algo>/<hex[0:2]>/<hex[2:4]>/<hex[4:]>
134
121
  //
135
- // `<algo>` is the SRI algorithm (`sha512`), `<hex>` the hex-encoded digest —
136
- // identical derivation to our own `pathFor`, just a different root and no
137
- // `.tgz` extension. So anyone who has run `npm install` before already has
138
- // these tarballs on disk; reading them turns a cold `gjsify install` into a
139
- // near-warm one without a single network round-trip.
122
+ // Identical derivation to our own `pathFor`, with a different root and no `.tgz`
123
+ // extension.
140
124
  //
141
- // pnpm/yarn/bun stores are deliberately NOT read here: pnpm/bun store
142
- // *unpacked* per-file content (no tarball to hand to the extractor) and yarn
143
- // berry stores zip archives under its own (non-SRI) cache key — none map to a
144
- // tarball-by-integrity lookup the way npm's cacache does.
145
- // ---------------------------------------------------------------------------
125
+ // pnpm/yarn/bun stores are deliberately NOT read: pnpm and bun store *unpacked*
126
+ // per-file content (no tarball to hand the extractor) and yarn berry stores zips
127
+ // under its own non-SRI key, so none map to a tarball-by-integrity lookup.
146
128
  /**
147
129
  * Resolve npm's `content-v2` directory, honouring `GJSIFY_NPM_CACHE`
148
130
  * (full path to a `_cacache` dir; `0`/`false`/empty disables the interop),
@@ -175,11 +157,11 @@ function npmCachePathFor(integrity) {
175
157
  return join(contentDir, sri.algorithm, sri.hex.slice(0, 2), sri.hex.slice(2, 4), sri.hex.slice(4));
176
158
  }
177
159
  /**
178
- * Read a tarball from npm's cacache content store by SRI integrity. Returns
179
- * the raw `.tgz` bytes on a HIT, `null` on a MISS / disabled interop / read
180
- * failure. Like {@link getCachedTarball}, this trusts the content-addressed
181
- * path rather than re-hashing — the extractor surfaces any genuinely corrupt
182
- * tarball loudly, and cacache verified the bytes on write.
160
+ * Read a tarball from npm's cacache content store by SRI integrity — raw `.tgz`
161
+ * bytes on a HIT, `null` on a MISS, disabled interop or read failure. Like
162
+ * {@link getCachedTarball} it trusts the content-addressed path rather than
163
+ * re-hashing: cacache verified the bytes on write, and the extractor surfaces a
164
+ * genuinely corrupt tarball loudly.
183
165
  */
184
166
  export function getForeignCachedTarball(integrity) {
185
167
  const path = npmCachePathFor(integrity);
@@ -1,30 +1,17 @@
1
1
  /**
2
- * The literal (non-glob) base directories the build's input source files live
3
- * in, derived from the bundler `input` patterns. A file entry maps to its
4
- * containing dir, a glob is reduced to the literal prefix before the first
5
- * wildcard:
6
- * - `src/index.ts` → `<cwd>/src`
7
- * - `src/**` + `/*.ts` → `<cwd>/src`
8
- * - `src` → `<cwd>/src`
2
+ * The literal (non-glob) base directories the input source files live in, derived from the
3
+ * bundler `input` patterns: a file entry maps to its containing dir, a glob is reduced to the
4
+ * literal prefix before the first wildcard (`src/index.ts`, `src` and `src/**` + `/*.ts` all
5
+ * give `<cwd>/src`).
9
6
  *
10
- * The project root itself is intentionally excluded — a flat library that
11
- * keeps its source at the project root and emits there is an explicit layout,
12
- * not the `src/`-subdir footgun this guard targets.
7
+ * The project root is intentionally excluded — a flat library keeping its source at the root and
8
+ * emitting there is an explicit layout, not the `src/`-subdir footgun this guard targets.
13
9
  */
14
10
  export declare function inputSourceDirs(input: unknown, cwd: string): string[];
15
11
  /**
16
- * `true` when writing library output to `outdir` would land build artifacts
17
- * inside (or directly on top of) the input source tree. Unsafe iff `outdir`
18
- * equals, or is nested under, any input source directory.
19
- *
20
- * isOutdirInsideSource('src', ['<cwd>/src']) → true (leak)
21
- * isOutdirInsideSource('src/nest', ['<cwd>/src']) → true (leak)
22
- * isOutdirInsideSource('dist/esm', ['<cwd>/src']) → false (safe)
23
- * isOutdirInsideSource('lib', ['<cwd>/src']) → false (safe)
12
+ * `true` when library output written to `outdir` would land inside (or on top of) the input
13
+ * source tree — i.e. `outdir` equals, or is nested under, any input source directory.
24
14
  */
25
15
  export declare function isOutdirInsideSource(outdir: string, sourceDirs: readonly string[], cwd: string): boolean;
26
- /**
27
- * The error thrown when a `--library` build would emit into its own source
28
- * tree. Names both remedies so the fix is actionable at the point of failure.
29
- */
16
+ /** The error thrown when a `--library` build would emit into its own source tree. */
30
17
  export declare function libraryOutputLeakError(outdir: string): Error;