@gjsify/cli 0.26.1 → 0.27.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.
- package/dist/cli.gjs.mjs +188 -183
- package/lib/bundler-pick.js +12 -4
- package/lib/cli-app.js +3 -1
- package/lib/commands/affected.js +1 -1
- package/lib/commands/build.js +21 -14
- package/lib/commands/check.js +40 -4
- package/lib/commands/clear.d.ts +8 -0
- package/lib/commands/clear.js +90 -0
- package/lib/commands/copy.d.ts +8 -0
- package/lib/commands/copy.js +87 -0
- package/lib/commands/dlx.js +8 -1
- package/lib/commands/index.d.ts +2 -0
- package/lib/commands/index.js +2 -0
- package/lib/commands/install.d.ts +4 -0
- package/lib/commands/install.js +164 -15
- package/lib/commands/onboard.js +17 -14
- package/lib/commands/pack.d.ts +32 -0
- package/lib/commands/pack.js +11 -2
- package/lib/commands/publish.js +18 -4
- package/lib/commands/storybook.js +5 -1
- package/lib/commands/test.js +29 -2
- package/lib/commands/uninstall.js +44 -2
- package/lib/commands/upgrade.js +1 -4
- package/lib/templates/install.mjs.tmpl +110 -19
- package/lib/utils/bin-shim.d.ts +73 -0
- package/lib/utils/bin-shim.js +97 -2
- package/lib/utils/build-cache.d.ts +7 -27
- package/lib/utils/build-cache.js +27 -2
- package/lib/utils/check-system-deps.d.ts +15 -1
- package/lib/utils/check-system-deps.js +126 -2
- package/lib/utils/clear-targets.d.ts +30 -0
- package/lib/utils/clear-targets.js +114 -0
- package/lib/utils/copy-targets.d.ts +41 -0
- package/lib/utils/copy-targets.js +145 -0
- package/lib/utils/detect-native-packages.d.ts +160 -5
- package/lib/utils/detect-native-packages.js +326 -50
- package/lib/utils/dir-link.d.ts +34 -6
- package/lib/utils/dir-link.js +23 -9
- package/lib/utils/install-backend-native.d.ts +109 -1
- package/lib/utils/install-backend-native.js +622 -59
- package/lib/utils/install-backend.js +17 -15
- package/lib/utils/install-global.js +6 -34
- package/lib/utils/install-lock.js +1 -1
- package/lib/utils/install-packument-cache.d.ts +36 -17
- package/lib/utils/install-packument-cache.js +59 -31
- package/lib/utils/oxc-resolve.d.ts +27 -1
- package/lib/utils/oxc-resolve.js +53 -26
- package/lib/utils/pkg-json.d.ts +24 -0
- package/lib/utils/pkg-json.js +27 -1
- package/lib/utils/platform-check.d.ts +215 -0
- package/lib/utils/platform-check.js +377 -0
- package/lib/utils/resolve-npm-package.js +9 -2
- package/lib/utils/run-gjs.d.ts +1 -1
- package/lib/utils/run-gjs.js +19 -3
- package/lib/utils/spawn.js +50 -2
- package/lib/utils/win32-command.d.ts +54 -0
- package/lib/utils/win32-command.js +140 -0
- package/package.json +19 -20
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
//
|
|
13
13
|
// `--no-package-lock` keeps the cache prepare dir hermetic; the cache key
|
|
14
14
|
// already covers reproducibility. `--no-audit --no-fund` cuts ~5s off cold runs.
|
|
15
|
-
import { spawn } from 'node:child_process';
|
|
16
15
|
import { writeFileSync } from 'node:fs';
|
|
17
16
|
import { join } from 'node:path';
|
|
17
|
+
import { spawnToCompletion } from './spawn.js';
|
|
18
18
|
export { makeProgressReporter } from './install-progress.js';
|
|
19
19
|
const DEFAULT_BACKEND = process.env.GJSIFY_INSTALL_BACKEND ?? 'native';
|
|
20
20
|
export async function installPackages(opts) {
|
|
@@ -46,19 +46,21 @@ async function installViaNpm({ prefix, specs, verbose, registry }) {
|
|
|
46
46
|
...(verbose ? ['--loglevel', 'verbose'] : ['--loglevel', 'warn']),
|
|
47
47
|
...specs,
|
|
48
48
|
];
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
})
|
|
49
|
+
// Through `spawnToCompletion` so `npm` is resolved the way every other
|
|
50
|
+
// spawn in the CLI resolves it. A bare `spawn('npm', …)` is ENOENT on
|
|
51
|
+
// Windows — `CreateProcess` appends only `.exe` and npm ships a `.cmd`
|
|
52
|
+
// shim (see utils/win32-command.ts). That made this backend unusable
|
|
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.
|
|
56
|
+
//
|
|
57
|
+
// `completion: 'return'` — `installPackages` resolves a promise to its
|
|
58
|
+
// caller instead of exiting, so under GJS this must not leave a main loop
|
|
59
|
+
// armed. (Moot in practice: this backend needs npm, hence Node.)
|
|
60
|
+
const { code } = await spawnToCompletion('npm', args, {
|
|
61
|
+
completion: 'return',
|
|
62
|
+
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})`),
|
|
63
63
|
});
|
|
64
|
+
if (code !== 0)
|
|
65
|
+
throw new Error(`npm install exited with code ${code}`);
|
|
64
66
|
}
|
|
@@ -24,7 +24,7 @@ import * as fs from 'node:fs';
|
|
|
24
24
|
import * as os from 'node:os';
|
|
25
25
|
import * as path from 'node:path';
|
|
26
26
|
import { detectNativePackages } from './detect-native-packages.js';
|
|
27
|
-
import { buildLauncherShims, buildNativeEnvPreamble, parseShebang } from './bin-shim.js';
|
|
27
|
+
import { buildLauncherShims, buildNativeEnvPreamble, buildShLauncher, isGjsBundlePath, parseShebang, pickBinMap, } from './bin-shim.js';
|
|
28
28
|
/**
|
|
29
29
|
* Compute the canonical global install layout for the current user. Honours
|
|
30
30
|
* `XDG_DATA_HOME` (per the XDG Base Directory Spec) plus
|
|
@@ -168,10 +168,10 @@ export function linkGlobalBins(packageNames, layout) {
|
|
|
168
168
|
if (!fs.existsSync(pkgJsonPath))
|
|
169
169
|
continue;
|
|
170
170
|
const pkgJson = readJson(pkgJsonPath);
|
|
171
|
-
const
|
|
172
|
-
if (
|
|
171
|
+
const picked = pickBinMap(pkgName, pkgJson);
|
|
172
|
+
if (picked === null || picked.map.size === 0)
|
|
173
173
|
continue;
|
|
174
|
-
for (const [binName, binTarget] of
|
|
174
|
+
for (const [binName, binTarget] of picked.map) {
|
|
175
175
|
const targetAbs = path.join(pkgDir, binTarget);
|
|
176
176
|
if (!fs.existsSync(targetAbs))
|
|
177
177
|
continue;
|
|
@@ -197,14 +197,11 @@ export function linkGlobalBins(packageNames, layout) {
|
|
|
197
197
|
// exec'ing a shebang-less .mjs file falls back to /bin/sh which
|
|
198
198
|
// then tries to parse JavaScript as shell. Plain Node scripts
|
|
199
199
|
// with shebangs (lib/index.js) keep the direct-exec path.
|
|
200
|
-
const isGjsBundle =
|
|
200
|
+
const isGjsBundle = isGjsBundlePath(targetAbs);
|
|
201
201
|
// Only GJS bundles need the GI typelib search path. Plain Node
|
|
202
202
|
// scripts ignore GI_TYPELIB_PATH, so skipping the preamble there
|
|
203
203
|
// keeps the launcher minimal.
|
|
204
|
-
|
|
205
|
-
? `#!/bin/sh\n${envPreamble}exec gjs -m ${shQuote(targetAbs)} "$@"\n`
|
|
206
|
-
: `#!/bin/sh\nexec ${shQuote(targetAbs)} "$@"\n`;
|
|
207
|
-
fs.writeFileSync(linkPath, launcher);
|
|
204
|
+
fs.writeFileSync(linkPath, buildShLauncher(targetAbs, { envPreamble, isGjsBundle }));
|
|
208
205
|
fs.chmodSync(linkPath, 0o755);
|
|
209
206
|
// Windows runs neither an extension-less file nor a `#!` line, so
|
|
210
207
|
// the `sh` launcher above is only reachable from git-bash/MSYS/WSL.
|
|
@@ -241,9 +238,6 @@ export function linkGlobalBins(packageNames, layout) {
|
|
|
241
238
|
}
|
|
242
239
|
return created;
|
|
243
240
|
}
|
|
244
|
-
function shQuote(s) {
|
|
245
|
-
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
246
|
-
}
|
|
247
241
|
/** Parse a bin file's `#!` line; `null` when unreadable or not a shebang. */
|
|
248
242
|
function readShebangOf(file) {
|
|
249
243
|
try {
|
|
@@ -254,28 +248,6 @@ function readShebangOf(file) {
|
|
|
254
248
|
return null;
|
|
255
249
|
}
|
|
256
250
|
}
|
|
257
|
-
function pickBinMap(pkgName, pkgJson) {
|
|
258
|
-
const gjsifyEntry = pkgJson.gjsify;
|
|
259
|
-
if (gjsifyEntry?.bin !== undefined) {
|
|
260
|
-
return normalizeBin(pkgName, gjsifyEntry.bin);
|
|
261
|
-
}
|
|
262
|
-
const npmBin = pkgJson.bin;
|
|
263
|
-
if (npmBin !== undefined) {
|
|
264
|
-
return normalizeBin(pkgName, npmBin);
|
|
265
|
-
}
|
|
266
|
-
return null;
|
|
267
|
-
}
|
|
268
|
-
function normalizeBin(pkgName, bin) {
|
|
269
|
-
const out = new Map();
|
|
270
|
-
if (typeof bin === 'string') {
|
|
271
|
-
const baseName = pkgName.startsWith('@') ? pkgName.slice(pkgName.indexOf('/') + 1) : pkgName;
|
|
272
|
-
out.set(baseName, bin);
|
|
273
|
-
return out;
|
|
274
|
-
}
|
|
275
|
-
for (const [k, v] of Object.entries(bin))
|
|
276
|
-
out.set(k, v);
|
|
277
|
-
return out;
|
|
278
|
-
}
|
|
279
251
|
function readJson(file) {
|
|
280
252
|
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
281
253
|
}
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
// Escape hatch: GJSIFY_INSTALL_LOCK=0 disables locking entirely (e.g. for a
|
|
44
44
|
// network filesystem with broken rename/mkdir atomicity, or an outer tool
|
|
45
45
|
// that already serializes installs).
|
|
46
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
46
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
47
47
|
import { dirname, join, resolve } from 'node:path';
|
|
48
48
|
const LOCK_DIR_NAME = '.gjsify-install-lock';
|
|
49
49
|
const OWNER_FILE = 'owner.json';
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import type { Packument } from '@gjsify/npm-registry';
|
|
2
|
+
/**
|
|
3
|
+
* Which document shape an entry holds. `'corgi'` = the abbreviated
|
|
4
|
+
* `application/vnd.npm.install-v1+json` install document (the default for every
|
|
5
|
+
* resolve); `'full'` = `application/json`. Same names pacote uses, so the two
|
|
6
|
+
* caches are describable in one sentence.
|
|
7
|
+
*/
|
|
8
|
+
export type PackumentShape = 'corgi' | 'full';
|
|
2
9
|
export interface CachedPackument {
|
|
3
10
|
etag: string;
|
|
4
11
|
packument: Packument;
|
|
5
12
|
}
|
|
6
13
|
/**
|
|
7
|
-
* The cache key for a (registry, name)
|
|
8
|
-
* below is a PROPERTY a test can assert, not a claim in a comment.
|
|
14
|
+
* The cache key for a (shape, registry, name) triple — exported so the filename
|
|
15
|
+
* rule below is a PROPERTY a test can assert, not a claim in a comment.
|
|
9
16
|
*
|
|
10
17
|
* THE SEPARATOR IS NOT COSMETIC. It used to be `|`, producing names like
|
|
11
18
|
* `corgi|https%3A%2F%2Fregistry.npmjs.org%2F|%40gjsify%2Fnode-gi.json` — and `|`
|
|
@@ -17,25 +24,37 @@ export interface CachedPackument {
|
|
|
17
24
|
* entire reason this file exists never engaged once.
|
|
18
25
|
*
|
|
19
26
|
* `,` is legal on Win32 AND escaped by `encodeURIComponent`, which is what keeps
|
|
20
|
-
* a flat
|
|
21
|
-
* so exactly one
|
|
27
|
+
* a flat three-part name injective: an encoded part cannot contain the
|
|
28
|
+
* separator, so exactly one triple maps to any given filename. The shape needs
|
|
29
|
+
* no encoding — it is one of two literals, neither containing the separator —
|
|
30
|
+
* and it LEADS so that listing a shard groups by shape.
|
|
22
31
|
*
|
|
23
32
|
* The shard key and the filename are derived from the SAME encoded parts. They
|
|
24
|
-
* used to disagree — the key hashed the raw
|
|
25
|
-
*
|
|
26
|
-
* function whose other half looked correct.
|
|
33
|
+
* used to disagree — the key hashed the raw values while the name used the
|
|
34
|
+
* encoded ones — which is how a reserved character in the name survived review
|
|
35
|
+
* of a function whose other half looked correct.
|
|
27
36
|
*/
|
|
28
|
-
export declare function packumentCacheKey(registry: string, name: string): string;
|
|
37
|
+
export declare function packumentCacheKey(registry: string, name: string, shape: PackumentShape): string;
|
|
29
38
|
/**
|
|
30
|
-
* Read a cached packument + its ETag for `(registry, name)
|
|
31
|
-
* a miss, when the cache is disabled, or
|
|
32
|
-
* (treated as a miss — the caller just
|
|
39
|
+
* Read a cached packument + its ETag for `(registry, name)` in the given
|
|
40
|
+
* document `shape`. Returns `null` on a miss, when the cache is disabled, or
|
|
41
|
+
* when the entry is unreadable/corrupt (treated as a miss — the caller just
|
|
42
|
+
* re-fetches).
|
|
43
|
+
*
|
|
44
|
+
* `shape` defaults to `'corgi'` because that is what a resolve fetches and what
|
|
45
|
+
* every caller predating the full-document escalation meant. A caller that
|
|
46
|
+
* escalated MUST pass `'full'` — reading the corgi entry would return a body
|
|
47
|
+
* with no `libc`, i.e. the exact wrong answer the shape key exists to prevent.
|
|
33
48
|
*/
|
|
34
|
-
export declare function getCachedPackument(registry: string, name: string): CachedPackument | null;
|
|
49
|
+
export declare function getCachedPackument(registry: string, name: string, shape?: PackumentShape): CachedPackument | null;
|
|
35
50
|
/**
|
|
36
|
-
* Persist a packument + ETag for `(registry, name)
|
|
37
|
-
* sibling then atomically renames so a concurrent
|
|
38
|
-
* write. No-op when the cache is disabled, the ETag
|
|
39
|
-
* fails (read-only / out-of-disk cache volume must not
|
|
51
|
+
* Persist a packument + ETag for `(registry, name)` under its document `shape`.
|
|
52
|
+
* 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
|
+
* is empty, or the write fails (read-only / out-of-disk cache volume must not
|
|
55
|
+
* break the install).
|
|
56
|
+
*
|
|
57
|
+
* `shape` is last and defaults to `'corgi'` to keep the pre-escalation call
|
|
58
|
+
* shape valid; it must match the `accept` header the body was fetched with.
|
|
40
59
|
*/
|
|
41
|
-
export declare function putCachedPackument(registry: string, name: string, etag: string, packument: Packument): void;
|
|
60
|
+
export declare function putCachedPackument(registry: string, name: string, etag: string, packument: Packument, shape?: PackumentShape): void;
|
|
@@ -13,22 +13,37 @@
|
|
|
13
13
|
//
|
|
14
14
|
// Layout:
|
|
15
15
|
//
|
|
16
|
-
// $XDG_CACHE_HOME/gjsify/metadata/
|
|
16
|
+
// $XDG_CACHE_HOME/gjsify/metadata/v3/<shard>/<shape>,<encoded-registry>,<encoded-name>.json
|
|
17
17
|
//
|
|
18
|
-
// The cache is keyed by (registry, name) — NOT name alone — so switching
|
|
19
|
-
// registry for a scope can never serve a packument from the wrong source on
|
|
20
|
-
// coincidental ETag match. `<shard>` is a 2-hex FNV-1a digest of the key, a
|
|
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
21
|
// directory-fan-out step so the leaf dir never grows unbounded (same rationale
|
|
22
22
|
// as the tarball cache's hex sharding). `v2` is a layout version.
|
|
23
23
|
//
|
|
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.
|
|
36
|
+
//
|
|
24
37
|
// Disabled with `GJSIFY_PACKUMENT_CACHE=0` (or `false`). Honours
|
|
25
38
|
// `XDG_CACHE_HOME` like the tarball + dlx caches.
|
|
26
39
|
import { join } from 'node:path';
|
|
27
40
|
import { atomicWrite, gjsifyCacheRoot, readCacheFile } from './install-cache-fs.js';
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
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
|
|
44
|
+
// unreachable rather than merely stale, so a new root retires them instead of
|
|
45
|
+
// leaving ambiguous orphans under the old one.
|
|
46
|
+
const CACHE_LAYOUT_VERSION = 'v3';
|
|
32
47
|
/** `true` unless `GJSIFY_PACKUMENT_CACHE` is `0` / `false` / empty. */
|
|
33
48
|
function isEnabled() {
|
|
34
49
|
const flag = process.env.GJSIFY_PACKUMENT_CACHE;
|
|
@@ -37,7 +52,7 @@ function isEnabled() {
|
|
|
37
52
|
const trimmed = flag.trim();
|
|
38
53
|
return !(trimmed === '0' || trimmed === 'false' || trimmed === '');
|
|
39
54
|
}
|
|
40
|
-
/** Root of the packument cache: `$XDG_CACHE_HOME/gjsify/metadata/
|
|
55
|
+
/** Root of the packument cache: `$XDG_CACHE_HOME/gjsify/metadata/v3`. */
|
|
41
56
|
function cacheRoot() {
|
|
42
57
|
return gjsifyCacheRoot('metadata', CACHE_LAYOUT_VERSION);
|
|
43
58
|
}
|
|
@@ -62,8 +77,8 @@ function encodePart(part) {
|
|
|
62
77
|
return encodeURIComponent(part).replace(/\*/g, '%2A');
|
|
63
78
|
}
|
|
64
79
|
/**
|
|
65
|
-
* The cache key for a (registry, name)
|
|
66
|
-
* below is a PROPERTY a test can assert, not a claim in a comment.
|
|
80
|
+
* The cache key for a (shape, registry, name) triple — exported so the filename
|
|
81
|
+
* rule below is a PROPERTY a test can assert, not a claim in a comment.
|
|
67
82
|
*
|
|
68
83
|
* THE SEPARATOR IS NOT COSMETIC. It used to be `|`, producing names like
|
|
69
84
|
* `corgi|https%3A%2F%2Fregistry.npmjs.org%2F|%40gjsify%2Fnode-gi.json` — and `|`
|
|
@@ -75,30 +90,39 @@ function encodePart(part) {
|
|
|
75
90
|
* entire reason this file exists never engaged once.
|
|
76
91
|
*
|
|
77
92
|
* `,` is legal on Win32 AND escaped by `encodeURIComponent`, which is what keeps
|
|
78
|
-
* a flat
|
|
79
|
-
* so exactly one
|
|
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.
|
|
80
97
|
*
|
|
81
98
|
* The shard key and the filename are derived from the SAME encoded parts. They
|
|
82
|
-
* used to disagree — the key hashed the raw
|
|
83
|
-
*
|
|
84
|
-
* function whose other half looked correct.
|
|
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.
|
|
85
102
|
*/
|
|
86
|
-
export function packumentCacheKey(registry, name) {
|
|
87
|
-
return `${encodePart(registry)},${encodePart(name)}`;
|
|
103
|
+
export function packumentCacheKey(registry, name, shape) {
|
|
104
|
+
return `${shape},${encodePart(registry)},${encodePart(name)}`;
|
|
88
105
|
}
|
|
89
|
-
|
|
106
|
+
/** Filesystem path for a (shape, registry, name) triple, or `null` when disabled. */
|
|
107
|
+
function pathFor(registry, name, shape) {
|
|
90
108
|
if (!isEnabled())
|
|
91
109
|
return null;
|
|
92
|
-
const key = packumentCacheKey(registry, name);
|
|
110
|
+
const key = packumentCacheKey(registry, name, shape);
|
|
93
111
|
return join(cacheRoot(), shardFor(key), `${key}.json`);
|
|
94
112
|
}
|
|
95
113
|
/**
|
|
96
|
-
* Read a cached packument + its ETag for `(registry, name)
|
|
97
|
-
* a miss, when the cache is disabled, or
|
|
98
|
-
* (treated as a miss — the caller just
|
|
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).
|
|
118
|
+
*
|
|
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.
|
|
99
123
|
*/
|
|
100
|
-
export function getCachedPackument(registry, name) {
|
|
101
|
-
const path = pathFor(registry, name);
|
|
124
|
+
export function getCachedPackument(registry, name, shape = 'corgi') {
|
|
125
|
+
const path = pathFor(registry, name, shape);
|
|
102
126
|
if (!path)
|
|
103
127
|
return null;
|
|
104
128
|
const buf = readCacheFile(path);
|
|
@@ -117,15 +141,19 @@ export function getCachedPackument(registry, name) {
|
|
|
117
141
|
}
|
|
118
142
|
}
|
|
119
143
|
/**
|
|
120
|
-
* Persist a packument + ETag for `(registry, name)
|
|
121
|
-
* sibling then atomically renames so a concurrent
|
|
122
|
-
* write. No-op when the cache is disabled, the ETag
|
|
123
|
-
* fails (read-only / out-of-disk cache volume must not
|
|
144
|
+
* Persist a packument + ETag for `(registry, name)` under its document `shape`.
|
|
145
|
+
* 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).
|
|
149
|
+
*
|
|
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.
|
|
124
152
|
*/
|
|
125
|
-
export function putCachedPackument(registry, name, etag, packument) {
|
|
153
|
+
export function putCachedPackument(registry, name, etag, packument, shape = 'corgi') {
|
|
126
154
|
if (!etag)
|
|
127
155
|
return;
|
|
128
|
-
const path = pathFor(registry, name);
|
|
156
|
+
const path = pathFor(registry, name, shape);
|
|
129
157
|
if (!path)
|
|
130
158
|
return;
|
|
131
159
|
const entry = { etag, packument };
|
|
@@ -9,7 +9,33 @@ export declare function findOxcLauncher(tool: OxcTool, cwd?: string): string;
|
|
|
9
9
|
export declare class OxcNotFoundError extends Error {
|
|
10
10
|
tool: OxcTool;
|
|
11
11
|
cwd: string;
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* The interpreter that could not be executed, when the LAUNCHER resolved
|
|
14
|
+
* and the spawn still failed with `ENOENT`.
|
|
15
|
+
*
|
|
16
|
+
* Then the missing file is the interpreter, not the tool, and saying
|
|
17
|
+
* "install <tool>" sends the user in a circle: on a Node-less GJS host
|
|
18
|
+
* (postmarketOS/aarch64, gjs present, no node) `gjsify install -D oxlint`
|
|
19
|
+
* succeeds, the launcher lands on disk, and the next run repeats the very
|
|
20
|
+
* same advice. `spawnToCompletion`'s `notFound` fires for the COMMAND, and
|
|
21
|
+
* the command here is `node` — never the launcher, which
|
|
22
|
+
* {@link findOxcLauncher} already proved exists by not throwing.
|
|
23
|
+
*/
|
|
24
|
+
missingInterpreter?: string | undefined;
|
|
25
|
+
constructor(tool: OxcTool, cwd: string,
|
|
26
|
+
/**
|
|
27
|
+
* The interpreter that could not be executed, when the LAUNCHER resolved
|
|
28
|
+
* and the spawn still failed with `ENOENT`.
|
|
29
|
+
*
|
|
30
|
+
* Then the missing file is the interpreter, not the tool, and saying
|
|
31
|
+
* "install <tool>" sends the user in a circle: on a Node-less GJS host
|
|
32
|
+
* (postmarketOS/aarch64, gjs present, no node) `gjsify install -D oxlint`
|
|
33
|
+
* succeeds, the launcher lands on disk, and the next run repeats the very
|
|
34
|
+
* same advice. `spawnToCompletion`'s `notFound` fires for the COMMAND, and
|
|
35
|
+
* the command here is `node` — never the launcher, which
|
|
36
|
+
* {@link findOxcLauncher} already proved exists by not throwing.
|
|
37
|
+
*/
|
|
38
|
+
missingInterpreter?: string | undefined);
|
|
13
39
|
}
|
|
14
40
|
/**
|
|
15
41
|
* Walk up from a starting directory to find the nearest oxlint config
|
package/lib/utils/oxc-resolve.js
CHANGED
|
@@ -46,10 +46,11 @@
|
|
|
46
46
|
// lives in the Node launcher, so a native oxlint bridge could only run the
|
|
47
47
|
// Rust rule subset. Lint stays Node-spawned until that trade-off is decided
|
|
48
48
|
// (tracked in status/open-todos.md).
|
|
49
|
-
import { existsSync, readFileSync
|
|
49
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
50
50
|
import { join, resolve } from 'node:path';
|
|
51
51
|
import { createRequire } from 'node:module';
|
|
52
52
|
import { pathToFileURL } from 'node:url';
|
|
53
|
+
import { hostLibc } from './platform-check.js';
|
|
53
54
|
import { findWorkspaceRoot } from './workspace-root.js';
|
|
54
55
|
import { resolveNpmPackage } from './resolve-npm-package.js';
|
|
55
56
|
import { nodeBinary } from './run-node.js';
|
|
@@ -85,24 +86,23 @@ function oxcBindingSuffix() {
|
|
|
85
86
|
throw new Error(`[gjsify oxc] Unsupported arch on ${plat}: ${arch}`);
|
|
86
87
|
if (plat === 'linux') {
|
|
87
88
|
// musl detection on Linux — oxc ships separate musl/gnu bindings.
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
89
|
+
//
|
|
90
|
+
// ONE DEFINITION, in `platform-check.ts`. This used to be a local
|
|
91
|
+
// `readdirSync('/lib')` scan for `ld-musl-*`, which is the same question
|
|
92
|
+
// the install backend asks about a package's `libc` field — and the local
|
|
93
|
+
// copy is the one that broke: its `readdirSync` was a lazy
|
|
94
|
+
// `require('node:fs')`, a bare `require` in an ESM module, so it threw
|
|
95
|
+
// ReferenceError, the surrounding catch swallowed it, and every host was
|
|
96
|
+
// silently told to install the `-gnu` binding. `hostLibc()` is the shared
|
|
97
|
+
// probe (ldd text → process.report → loader scan), memoized and unit-
|
|
98
|
+
// tested with injected hosts, so a musl box gets the right answer here
|
|
99
|
+
// and in the resolver from the same code.
|
|
100
|
+
//
|
|
101
|
+
// Vocabulary hop: npm spells the families `glibc`/`musl`, napi-rs target
|
|
102
|
+
// triples spell them `gnu`/`musl`. An UNKNOWN libc maps to `gnu` — this
|
|
103
|
+
// string only names a package in an install HINT, and gnu is the right
|
|
104
|
+
// guess for the overwhelming majority of hosts we cannot probe.
|
|
105
|
+
const libc = hostLibc('linux') === 'musl' ? 'musl' : 'gnu';
|
|
106
106
|
return `${plat}-${a}-${libc}`;
|
|
107
107
|
}
|
|
108
108
|
if (plat === 'win32')
|
|
@@ -158,7 +158,21 @@ export function findOxcLauncher(tool, cwd = process.cwd()) {
|
|
|
158
158
|
export class OxcNotFoundError extends Error {
|
|
159
159
|
tool;
|
|
160
160
|
cwd;
|
|
161
|
-
|
|
161
|
+
missingInterpreter;
|
|
162
|
+
constructor(tool, cwd,
|
|
163
|
+
/**
|
|
164
|
+
* The interpreter that could not be executed, when the LAUNCHER resolved
|
|
165
|
+
* and the spawn still failed with `ENOENT`.
|
|
166
|
+
*
|
|
167
|
+
* Then the missing file is the interpreter, not the tool, and saying
|
|
168
|
+
* "install <tool>" sends the user in a circle: on a Node-less GJS host
|
|
169
|
+
* (postmarketOS/aarch64, gjs present, no node) `gjsify install -D oxlint`
|
|
170
|
+
* succeeds, the launcher lands on disk, and the next run repeats the very
|
|
171
|
+
* same advice. `spawnToCompletion`'s `notFound` fires for the COMMAND, and
|
|
172
|
+
* the command here is `node` — never the launcher, which
|
|
173
|
+
* {@link findOxcLauncher} already proved exists by not throwing.
|
|
174
|
+
*/
|
|
175
|
+
missingInterpreter) {
|
|
162
176
|
const binding = (() => {
|
|
163
177
|
try {
|
|
164
178
|
return bindingPackageName(tool);
|
|
@@ -167,13 +181,24 @@ export class OxcNotFoundError extends Error {
|
|
|
167
181
|
return `@${tool === 'oxlint' ? 'oxlint' : 'oxfmt'}/binding-<platform>`;
|
|
168
182
|
}
|
|
169
183
|
})();
|
|
170
|
-
super(
|
|
171
|
-
`
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
184
|
+
super(missingInterpreter === undefined
|
|
185
|
+
? `[gjsify oxc] ${tool} not found.\n` +
|
|
186
|
+
` Expected: ${tool}/bin/${tool} in node_modules of ${cwd} or any workspace root above it.\n` +
|
|
187
|
+
` Install it via: gjsify install -D ${tool}\n` +
|
|
188
|
+
` (this adds ${tool} to devDependencies; the matching ${binding} ` +
|
|
189
|
+
`napi binding lands automatically as an optionalDependency.)`
|
|
190
|
+
: `[gjsify oxc] cannot run ${tool}: no \`${missingInterpreter}\` binary on PATH.\n` +
|
|
191
|
+
` ${tool} IS installed — its launcher was found. What is missing is the\n` +
|
|
192
|
+
` interpreter that runs it: ${tool}'s launcher is a Node ESM script.\n` +
|
|
193
|
+
(tool === 'oxfmt'
|
|
194
|
+
? ` For a Node-free formatter install the GI bridge:\n` +
|
|
195
|
+
` gjsify install -D @gjsify/oxfmt-native\n` +
|
|
196
|
+
` (\`gjsify format\`/\`fix\` then run oxfmt in-process under GJS.)`
|
|
197
|
+
: ` oxlint has no native GJS bridge yet — its JS-plugin host lives in the\n` +
|
|
198
|
+
` Node launcher — so \`gjsify lint\` needs Node on the host for now.`));
|
|
175
199
|
this.tool = tool;
|
|
176
200
|
this.cwd = cwd;
|
|
201
|
+
this.missingInterpreter = missingInterpreter;
|
|
177
202
|
this.name = 'OxcNotFoundError';
|
|
178
203
|
}
|
|
179
204
|
}
|
|
@@ -322,7 +347,9 @@ function spawnOxcLauncher(tool, args, opts = {}) {
|
|
|
322
347
|
return spawnToCompletion(node, [launcher, ...args], {
|
|
323
348
|
completion: 'return',
|
|
324
349
|
cwd,
|
|
325
|
-
|
|
350
|
+
// ENOENT here is the COMMAND — `node` — not the launcher: `findOxcLauncher`
|
|
351
|
+
// above already proved that exists by not throwing.
|
|
352
|
+
notFound: () => new OxcNotFoundError(tool, cwd, node),
|
|
326
353
|
}).then(({ code, signal }) => {
|
|
327
354
|
if (signal !== null) {
|
|
328
355
|
console.error(`[gjsify oxc] ${tool} terminated by signal ${signal}`);
|
package/lib/utils/pkg-json.d.ts
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import type { PackageJson } from './pkg-json-edit.js';
|
|
2
|
+
/**
|
|
3
|
+
* Drop a leading UTF-8 BOM (`U+FEFF`, the decoded form of `EF BB BF`).
|
|
4
|
+
*
|
|
5
|
+
* `JSON.parse` rejects a BOM — it is not whitespace — so a manifest carrying
|
|
6
|
+
* one is "invalid JSON" to every reader here. That is not a hypothetical file
|
|
7
|
+
* shape on Windows: Windows PowerShell 5.1 writes a BOM for `-Encoding utf8`,
|
|
8
|
+
* which is its DEFAULT for `Out-File`/`Set-Content`, so any script that edits a
|
|
9
|
+
* manifest produces one. Notepad offers "UTF-8 with BOM" too.
|
|
10
|
+
*
|
|
11
|
+
* npm reads such a file without complaint (`npm pkg get name` on a BOM'd
|
|
12
|
+
* manifest returns the name), so the file is valid as far as the ecosystem is
|
|
13
|
+
* concerned and gjsify was the strict one. Stripping matches npm, VS Code and
|
|
14
|
+
* `parse-json`'s own guidance; nothing downstream wants the character.
|
|
15
|
+
*
|
|
16
|
+
* Only a LEADING BOM: `U+FEFF` elsewhere is a zero-width no-break space inside
|
|
17
|
+
* a string value, and removing it would corrupt the data.
|
|
18
|
+
*/
|
|
19
|
+
export declare function stripBom(text: string): string;
|
|
2
20
|
/**
|
|
3
21
|
* Read + parse a `package.json` at `pkgPath`. Permissive: any error (missing
|
|
4
22
|
* file, unreadable, invalid JSON) yields `null`.
|
|
23
|
+
*
|
|
24
|
+
* The BOM strip matters MORE here than in a throwing reader: this one reports
|
|
25
|
+
* an unparseable manifest as `null`, which every caller reads as "no
|
|
26
|
+
* package.json". On Windows a BOM therefore did not produce an error — it made
|
|
27
|
+
* a package look like it had no manifest at all, so `gjsify check` found no
|
|
28
|
+
* `check` script, discovery skipped the package, and nothing said why.
|
|
5
29
|
*/
|
|
6
30
|
export declare function readPackageJson(pkgPath: string): PackageJson | null;
|
package/lib/utils/pkg-json.js
CHANGED
|
@@ -10,13 +10,39 @@
|
|
|
10
10
|
// during `gjsify install <pkg>`. That contract is intentional and load-bearing
|
|
11
11
|
// — do not fold it into this one.
|
|
12
12
|
import { readFileSync } from 'node:fs';
|
|
13
|
+
/**
|
|
14
|
+
* Drop a leading UTF-8 BOM (`U+FEFF`, the decoded form of `EF BB BF`).
|
|
15
|
+
*
|
|
16
|
+
* `JSON.parse` rejects a BOM — it is not whitespace — so a manifest carrying
|
|
17
|
+
* one is "invalid JSON" to every reader here. That is not a hypothetical file
|
|
18
|
+
* shape on Windows: Windows PowerShell 5.1 writes a BOM for `-Encoding utf8`,
|
|
19
|
+
* which is its DEFAULT for `Out-File`/`Set-Content`, so any script that edits a
|
|
20
|
+
* manifest produces one. Notepad offers "UTF-8 with BOM" too.
|
|
21
|
+
*
|
|
22
|
+
* npm reads such a file without complaint (`npm pkg get name` on a BOM'd
|
|
23
|
+
* manifest returns the name), so the file is valid as far as the ecosystem is
|
|
24
|
+
* concerned and gjsify was the strict one. Stripping matches npm, VS Code and
|
|
25
|
+
* `parse-json`'s own guidance; nothing downstream wants the character.
|
|
26
|
+
*
|
|
27
|
+
* Only a LEADING BOM: `U+FEFF` elsewhere is a zero-width no-break space inside
|
|
28
|
+
* a string value, and removing it would corrupt the data.
|
|
29
|
+
*/
|
|
30
|
+
export function stripBom(text) {
|
|
31
|
+
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
32
|
+
}
|
|
13
33
|
/**
|
|
14
34
|
* Read + parse a `package.json` at `pkgPath`. Permissive: any error (missing
|
|
15
35
|
* file, unreadable, invalid JSON) yields `null`.
|
|
36
|
+
*
|
|
37
|
+
* The BOM strip matters MORE here than in a throwing reader: this one reports
|
|
38
|
+
* an unparseable manifest as `null`, which every caller reads as "no
|
|
39
|
+
* package.json". On Windows a BOM therefore did not produce an error — it made
|
|
40
|
+
* a package look like it had no manifest at all, so `gjsify check` found no
|
|
41
|
+
* `check` script, discovery skipped the package, and nothing said why.
|
|
16
42
|
*/
|
|
17
43
|
export function readPackageJson(pkgPath) {
|
|
18
44
|
try {
|
|
19
|
-
return JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
45
|
+
return JSON.parse(stripBom(readFileSync(pkgPath, 'utf-8')));
|
|
20
46
|
}
|
|
21
47
|
catch {
|
|
22
48
|
return null;
|