@gjsify/cli 0.26.0 → 0.26.1

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.
@@ -13,19 +13,22 @@
13
13
  //
14
14
  // Layout:
15
15
  //
16
- // $XDG_CACHE_HOME/gjsify/metadata/v1/<shard>/<encoded-registry>|<encoded-name>.json
16
+ // $XDG_CACHE_HOME/gjsify/metadata/v2/<shard>/<encoded-registry>,<encoded-name>.json
17
17
  //
18
18
  // The cache is keyed by (registry, name) — NOT name alone — so switching the
19
19
  // registry for a scope can never serve a packument from the wrong source on a
20
20
  // coincidental ETag match. `<shard>` is a 2-hex FNV-1a digest of the key, a
21
21
  // directory-fan-out step so the leaf dir never grows unbounded (same rationale
22
- // as the tarball cache's hex sharding). `v1` is a layout version.
22
+ // as the tarball cache's hex sharding). `v2` is a layout version.
23
23
  //
24
24
  // Disabled with `GJSIFY_PACKUMENT_CACHE=0` (or `false`). Honours
25
25
  // `XDG_CACHE_HOME` like the tarball + dlx caches.
26
26
  import { join } from 'node:path';
27
27
  import { atomicWrite, gjsifyCacheRoot, readCacheFile } from './install-cache-fs.js';
28
- const CACHE_LAYOUT_VERSION = 'v1';
28
+ // v2: the separator changed from the Win32-reserved `|` to `,` (see `pathFor`),
29
+ // so every v1 entry is unreachable rather than merely stale — a new root retires
30
+ // them instead of leaving orphans under the old one.
31
+ const CACHE_LAYOUT_VERSION = 'v2';
29
32
  /** `true` unless `GJSIFY_PACKUMENT_CACHE` is `0` / `false` / empty. */
30
33
  function isEnabled() {
31
34
  const flag = process.env.GJSIFY_PACKUMENT_CACHE;
@@ -34,7 +37,7 @@ function isEnabled() {
34
37
  const trimmed = flag.trim();
35
38
  return !(trimmed === '0' || trimmed === 'false' || trimmed === '');
36
39
  }
37
- /** Root of the packument cache: `$XDG_CACHE_HOME/gjsify/metadata/v1`. */
40
+ /** Root of the packument cache: `$XDG_CACHE_HOME/gjsify/metadata/v2`. */
38
41
  function cacheRoot() {
39
42
  return gjsifyCacheRoot('metadata', CACHE_LAYOUT_VERSION);
40
43
  }
@@ -47,15 +50,47 @@ function shardFor(key) {
47
50
  }
48
51
  return ((h >>> 0) & 0xff).toString(16).padStart(2, '0');
49
52
  }
50
- /** Filesystem path for a (registry, name) pair, or `null` when disabled. */
53
+ /**
54
+ * One path part, made safe for EVERY filesystem we run on.
55
+ *
56
+ * `encodeURIComponent` alone is not enough: it deliberately leaves `*` (and
57
+ * `!'()`) unescaped, and `*` is one of Win32's reserved filename characters.
58
+ * A scoped package never contains one today, but a registry URL can, and the
59
+ * failure mode is the silent one described on {@link pathFor}.
60
+ */
61
+ function encodePart(part) {
62
+ return encodeURIComponent(part).replace(/\*/g, '%2A');
63
+ }
64
+ /**
65
+ * The cache key for a (registry, name) pair — exported so the filename rule
66
+ * below is a PROPERTY a test can assert, not a claim in a comment.
67
+ *
68
+ * THE SEPARATOR IS NOT COSMETIC. It used to be `|`, producing names like
69
+ * `corgi|https%3A%2F%2Fregistry.npmjs.org%2F|%40gjsify%2Fnode-gi.json` — and `|`
70
+ * is a Win32 RESERVED filename character, so every write failed with `EINVAL`.
71
+ * Nothing reported it: `atomicWrite` is best-effort on purpose (a read-only
72
+ * cache volume must not break an install) and `readCacheFile` reads an
73
+ * unreadable path as a MISS. So on Windows this cache was never broken loudly —
74
+ * it was silently DEAD, and the `If-None-Match` → `304` revalidation that is the
75
+ * entire reason this file exists never engaged once.
76
+ *
77
+ * `,` is legal on Win32 AND escaped by `encodeURIComponent`, which is what keeps
78
+ * a flat two-part name injective: an encoded part cannot contain the separator,
79
+ * so exactly one `(registry, name)` pair maps to any given filename.
80
+ *
81
+ * The shard key and the filename are derived from the SAME encoded parts. They
82
+ * used to disagree — the key hashed the raw pair while the name used the encoded
83
+ * one — which is how a reserved character in the name survived review of a
84
+ * function whose other half looked correct.
85
+ */
86
+ export function packumentCacheKey(registry, name) {
87
+ return `${encodePart(registry)},${encodePart(name)}`;
88
+ }
51
89
  function pathFor(registry, name) {
52
90
  if (!isEnabled())
53
91
  return null;
54
- const key = `${registry}|${name}`;
55
- // encodeURIComponent makes both halves filesystem-safe (`/` → `%2F`, etc.)
56
- // while staying a stable, reversible, single path segment.
57
- const file = `${encodeURIComponent(registry)}|${encodeURIComponent(name)}.json`;
58
- return join(cacheRoot(), shardFor(key), file);
92
+ const key = packumentCacheKey(registry, name);
93
+ return join(cacheRoot(), shardFor(key), `${key}.json`);
59
94
  }
60
95
  /**
61
96
  * Read a cached packument + its ETag for `(registry, name)`. Returns `null` on
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.26.0",
3
+ "version": "0.26.1",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -130,19 +130,19 @@
130
130
  "cli"
131
131
  ],
132
132
  "dependencies": {
133
- "@gjsify/buffer": "^0.26.0",
134
- "@gjsify/create-app": "^0.26.0",
135
- "@gjsify/node-globals": "^0.26.0",
136
- "@gjsify/node-polyfills": "^0.26.0",
137
- "@gjsify/npm-registry": "^0.26.0",
138
- "@gjsify/resolve-npm": "^0.26.0",
139
- "@gjsify/rolldown-plugin-gjsify": "^0.26.0",
140
- "@gjsify/rolldown-plugin-pnp": "^0.26.0",
141
- "@gjsify/semver": "^0.26.0",
142
- "@gjsify/tar": "^0.26.0",
143
- "@gjsify/tsc": "^0.26.0",
144
- "@gjsify/web-polyfills": "^0.26.0",
145
- "@gjsify/workspace": "^0.26.0",
133
+ "@gjsify/buffer": "^0.26.1",
134
+ "@gjsify/create-app": "^0.26.1",
135
+ "@gjsify/node-globals": "^0.26.1",
136
+ "@gjsify/node-polyfills": "^0.26.1",
137
+ "@gjsify/npm-registry": "^0.26.1",
138
+ "@gjsify/resolve-npm": "^0.26.1",
139
+ "@gjsify/rolldown-plugin-gjsify": "^0.26.1",
140
+ "@gjsify/rolldown-plugin-pnp": "^0.26.1",
141
+ "@gjsify/semver": "^0.26.1",
142
+ "@gjsify/tar": "^0.26.1",
143
+ "@gjsify/tsc": "^0.26.1",
144
+ "@gjsify/web-polyfills": "^0.26.1",
145
+ "@gjsify/workspace": "^0.26.1",
146
146
  "cosmiconfig": "^9.0.2",
147
147
  "get-tsconfig": "^4.14.0",
148
148
  "pkg-types": "^2.3.1",
@@ -150,13 +150,13 @@
150
150
  "yargs": "^18.0.0"
151
151
  },
152
152
  "devDependencies": {
153
- "@gjsify/unit": "^0.26.0",
153
+ "@gjsify/unit": "^0.26.1",
154
154
  "@types/yargs": "^17.0.35",
155
155
  "typescript": "^6.0.3"
156
156
  },
157
157
  "peerDependencies": {
158
- "@gjsify/oxfmt-native": "^0.26.0",
159
- "@gjsify/rolldown-native": "^0.26.0"
158
+ "@gjsify/oxfmt-native": "^0.26.1",
159
+ "@gjsify/rolldown-native": "^0.26.1"
160
160
  },
161
161
  "peerDependenciesMeta": {
162
162
  "@gjsify/oxfmt-native": {