@gjsify/cli 0.26.0 → 0.27.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 +184 -182
- package/lib/cli-app.js +3 -1
- package/lib/commands/affected.js +146 -15
- 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/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 +34 -4
- 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 +86 -0
- package/lib/utils/dir-link.js +140 -0
- package/lib/utils/dlx-cache.d.ts +30 -3
- package/lib/utils/dlx-cache.js +61 -7
- 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 +51 -9
- package/lib/utils/install-packument-cache.js +88 -25
- 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
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Creating a link to a DIRECTORY, portably.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS FILE EXISTS. Windows cannot create a directory *symlink* without
|
|
4
|
+
// elevation or Developer Mode — the call fails with `EPERM` — but any user can
|
|
5
|
+
// create an NTFS **junction**, which is why npm, yarn and pnpm all use junctions
|
|
6
|
+
// for directory links. `commands/install.ts` learned that and encoded it, twice,
|
|
7
|
+
// as a local constant for its workspace links. Nothing shared it, so the next
|
|
8
|
+
// site to link a directory got it wrong: `dlx-cache.ts` linked with `'dir'` and
|
|
9
|
+
// every non-elevated Windows user hit
|
|
10
|
+
//
|
|
11
|
+
// Could not resolve showcase "canvas2d-fireworks": EPERM: operation not
|
|
12
|
+
// permitted, symlink '…\<prepare-dir>' -> '…\pkg.tmp-…'
|
|
13
|
+
//
|
|
14
|
+
// on a plain `npx @gjsify/cli@latest showcase …`. The knowledge existed in the
|
|
15
|
+
// repo and the bug shipped anyway, which is the argument for a single callable
|
|
16
|
+
// definition rather than a convention.
|
|
17
|
+
//
|
|
18
|
+
// THE TWO GOTCHAS, both load-bearing:
|
|
19
|
+
//
|
|
20
|
+
// 1. **A junction target must be ABSOLUTE.** Node normalises a `'junction'`
|
|
21
|
+
// target with `path.resolve()` — against the process CWD, NOT against the
|
|
22
|
+
// link's own directory. A relative target that looks right produces a
|
|
23
|
+
// junction pointing somewhere else entirely, and only on Windows.
|
|
24
|
+
// 2. **POSIX targets should stay RELATIVE** where the caller wants a movable
|
|
25
|
+
// tree: a relative symlink survives the whole tree being moved or mounted
|
|
26
|
+
// elsewhere. So the target spelling is per-platform, not one string.
|
|
27
|
+
//
|
|
28
|
+
// Autodetection is NOT a substitute. Node picks `'file'` or `'dir'` when the
|
|
29
|
+
// type argument is omitted — never `'junction'` — so an omitted type is the
|
|
30
|
+
// failing case, not the safe default.
|
|
31
|
+
import { readlinkSync, statSync, symlinkSync } from 'node:fs';
|
|
32
|
+
import { symlink } from 'node:fs/promises';
|
|
33
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
34
|
+
/** The link kind directory links use on this platform. */
|
|
35
|
+
export const DIR_LINK_KIND = process.platform === 'win32' ? 'junction' : 'symlink';
|
|
36
|
+
/**
|
|
37
|
+
* The link type a directory link must pass to `fs.symlink`.
|
|
38
|
+
*
|
|
39
|
+
* `undefined` on POSIX is deliberate: Node's default is correct there, and
|
|
40
|
+
* passing a type would be noise. Derived from {@link DIR_LINK_KIND} so the
|
|
41
|
+
* platform decision has exactly one source.
|
|
42
|
+
*/
|
|
43
|
+
export const DIR_LINK_TYPE = DIR_LINK_KIND === 'junction' ? 'junction' : undefined;
|
|
44
|
+
/** True when directory links on this platform are NTFS junctions. */
|
|
45
|
+
export const dirLinksAreJunctions = () => DIR_LINK_KIND === 'junction';
|
|
46
|
+
/**
|
|
47
|
+
* Spell the `target` argument for a link at `linkPath` pointing at `absTarget`.
|
|
48
|
+
*
|
|
49
|
+
* Absolute on Windows because a junction demands it (gotcha 1 above); relative
|
|
50
|
+
* on POSIX so the tree stays movable (gotcha 2).
|
|
51
|
+
*
|
|
52
|
+
* `linkKind` is a PARAMETER, defaulting to the host's, for the same reason
|
|
53
|
+
* `detect-native-packages.ts` takes `platform`/`arch`: the Windows branch is
|
|
54
|
+
* then unit-testable from a Linux host, which is the only place anyone runs the
|
|
55
|
+
* tests. A branch nobody can exercise is how the `'dir'` bug survived to a
|
|
56
|
+
* release — and see {@link DirLinkKind} for why the POSIX side had to be named
|
|
57
|
+
* rather than spelled `undefined` before that promise actually held.
|
|
58
|
+
*
|
|
59
|
+
* BOTH paths must be in the SAME canonical space — either both canonical or
|
|
60
|
+
* both spelled through the same symlinks. The POSIX target is relative to the
|
|
61
|
+
* link's own directory, but the kernel resolves it from the link's REAL
|
|
62
|
+
* directory, so mixing the two spaces yields a link that points nowhere. macOS
|
|
63
|
+
* is where this actually happens: `os.tmpdir()` is `/var/folders/…` and `/var`
|
|
64
|
+
* is a symlink to `/private/var`, so a `linkPath` built from `tmpdir()` and an
|
|
65
|
+
* `absTarget` that has been through `realpathSync` disagree, and the relative
|
|
66
|
+
* path between them walks out of the real tree. `realpathSync` whichever side
|
|
67
|
+
* is not already canonical before calling this.
|
|
68
|
+
*
|
|
69
|
+
* @param linkPath where the link itself will live
|
|
70
|
+
* @param absTarget the directory it should point at, as an ABSOLUTE path
|
|
71
|
+
* @param linkKind override the host's link kind (tests only)
|
|
72
|
+
*/
|
|
73
|
+
export function dirLinkTarget(linkPath, absTarget, linkKind = DIR_LINK_KIND) {
|
|
74
|
+
return linkKind === 'junction' ? resolve(absTarget) : relative(dirname(linkPath), absTarget);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Link `linkPath` → `absTarget`, choosing symlink-vs-junction and the target
|
|
78
|
+
* spelling for the host.
|
|
79
|
+
*
|
|
80
|
+
* `absTarget` must be absolute; passing a relative path is the mistake gotcha 1
|
|
81
|
+
* describes, and it would only misbehave on Windows, so it is rejected here on
|
|
82
|
+
* every platform rather than left to surface on the one host nobody tests on.
|
|
83
|
+
*/
|
|
84
|
+
export function linkDirSync(linkPath, absTarget) {
|
|
85
|
+
assertAbsolute(absTarget);
|
|
86
|
+
symlinkSync(dirLinkTarget(linkPath, absTarget), linkPath, DIR_LINK_TYPE);
|
|
87
|
+
}
|
|
88
|
+
/** Promise-returning twin of {@link linkDirSync}. */
|
|
89
|
+
export async function linkDir(linkPath, absTarget) {
|
|
90
|
+
assertAbsolute(absTarget);
|
|
91
|
+
await symlink(dirLinkTarget(linkPath, absTarget), linkPath, DIR_LINK_TYPE);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Recreate an EXISTING link at a new path, preserving what it points at.
|
|
95
|
+
*
|
|
96
|
+
* A different job from {@link linkDirSync}, and it cannot reuse it: the source
|
|
97
|
+
* link's target is whatever it already is — frequently relative, deliberately so
|
|
98
|
+
* — while a junction demands an absolute one. Copying the target verbatim is
|
|
99
|
+
* therefore right on POSIX and wrong on Windows the moment the target is a
|
|
100
|
+
* directory.
|
|
101
|
+
*
|
|
102
|
+
* So the target's KIND decides: a link to a directory becomes a junction with
|
|
103
|
+
* the target resolved against the source link's own directory (the resolution
|
|
104
|
+
* Node will not do for you — see gotcha 1 above); anything else stays a plain
|
|
105
|
+
* file symlink, which Windows allows unprivileged.
|
|
106
|
+
*
|
|
107
|
+
* A dangling source link keeps the verbatim target: it points nowhere either
|
|
108
|
+
* way, and guessing a kind for it would turn a faithful copy into a fabrication.
|
|
109
|
+
*
|
|
110
|
+
* Primitive fs ops only — this runs under the GJS facade as well as Node, which
|
|
111
|
+
* is why there is no `fs.cp`/`opendir` here.
|
|
112
|
+
*/
|
|
113
|
+
export function replicateLinkSync(srcLink, dstLink) {
|
|
114
|
+
const rawTarget = readlinkSync(srcLink);
|
|
115
|
+
if (!dirLinksAreJunctions()) {
|
|
116
|
+
symlinkSync(rawTarget, dstLink);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const resolvedTarget = resolve(dirname(srcLink), rawTarget);
|
|
120
|
+
let targetIsDir;
|
|
121
|
+
try {
|
|
122
|
+
targetIsDir = statSync(resolvedTarget).isDirectory();
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Dangling — copy it as-is rather than inventing a kind for it.
|
|
126
|
+
symlinkSync(rawTarget, dstLink);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (targetIsDir)
|
|
130
|
+
symlinkSync(resolvedTarget, dstLink, 'junction');
|
|
131
|
+
else
|
|
132
|
+
symlinkSync(rawTarget, dstLink, 'file');
|
|
133
|
+
}
|
|
134
|
+
function assertAbsolute(absTarget) {
|
|
135
|
+
if (resolve(absTarget) !== absTarget) {
|
|
136
|
+
throw new TypeError(`linkDir: target must be an absolute path, got ${JSON.stringify(absTarget)}. ` +
|
|
137
|
+
'Node resolves a Windows junction target against the process CWD rather than the ' +
|
|
138
|
+
"link's own directory, so a relative target silently points somewhere else there.");
|
|
139
|
+
}
|
|
140
|
+
}
|
package/lib/utils/dlx-cache.d.ts
CHANGED
|
@@ -20,13 +20,40 @@ export declare function getValidCachedPkg(cacheDir: string, maxAgeMinutes?: numb
|
|
|
20
20
|
* Atomically swap `<cacheDir>/pkg` to point at `prepareDir`.
|
|
21
21
|
*
|
|
22
22
|
* Strategy:
|
|
23
|
-
* 1. Create new
|
|
24
|
-
* 2. `rename(pkg
|
|
23
|
+
* 1. Create new link `<cacheDir>/pkg.tmp-<ts>-<pid>` → prepareDir.
|
|
24
|
+
* 2. `rename(pkg.tmp-…, pkg)` — POSIX guarantees rename-over-existing is atomic.
|
|
25
25
|
*
|
|
26
26
|
* Returns the realpath of the new live target. EBUSY/EEXIST indicates a race
|
|
27
27
|
* — a parallel process won, return its realpath.
|
|
28
|
+
*
|
|
29
|
+
* WINDOWS, both halves of which shipped broken:
|
|
30
|
+
*
|
|
31
|
+
* • Step 1 used `symlinkSync(…, 'dir')`, and a directory SYMLINK needs
|
|
32
|
+
* elevation or Developer Mode there, so a plain
|
|
33
|
+
* `npx @gjsify/cli@latest showcase …` died with `EPERM: operation not
|
|
34
|
+
* permitted, symlink`. It now goes through {@link linkDirSync}, which picks
|
|
35
|
+
* an NTFS junction — the same choice npm and yarn make, and the one
|
|
36
|
+
* `commands/install.ts` already made for workspace links without sharing it.
|
|
37
|
+
* • Step 2's rename-over-existing is a POSIX guarantee, not a Windows one:
|
|
38
|
+
* `MoveFileEx` will not replace an existing DIRECTORY, and a junction is a
|
|
39
|
+
* directory reparse point. So on the second run — when `pkg` already exists
|
|
40
|
+
* — the rename failed with `EPERM`, the catch below read that as "race
|
|
41
|
+
* lost", and the function returned the OLD target. That is not a crash but
|
|
42
|
+
* it is worse than one: the cache silently never refreshed. Windows
|
|
43
|
+
* therefore unlinks first and accepts the non-atomic window, which the
|
|
44
|
+
* existing race handling already covers.
|
|
45
|
+
*
|
|
46
|
+
* @param opts.junctions force the Windows link strategy (TESTS ONLY). Defaults
|
|
47
|
+
* to the host's. It is a parameter for the same reason `dirLinkTarget` takes
|
|
48
|
+
* `linkType`: nothing in CI runs on Windows, so an injected `true` is the ONLY
|
|
49
|
+
* way the unlink-then-rename ordering above is ever EXECUTED, and an
|
|
50
|
+
* unexecutable Windows branch is exactly how the `'dir'` bug shipped. What the
|
|
51
|
+
* injection proves off-host is the ORDERING and that the second swap really
|
|
52
|
+
* refreshes; what it cannot prove is `MoveFileEx`/junction syscall behaviour.
|
|
28
53
|
*/
|
|
29
|
-
export declare function symlinkSwap(cacheDir: string, prepareDir: string
|
|
54
|
+
export declare function symlinkSwap(cacheDir: string, prepareDir: string, opts?: {
|
|
55
|
+
junctions?: boolean;
|
|
56
|
+
}): string;
|
|
30
57
|
/** Clean up `<cacheDir>/<oldPrepareDir>` siblings older than `maxAgeMinutes`. */
|
|
31
58
|
export declare function cleanupStalePrepareDirs(cacheDir: string, _maxAgeMinutes?: number): void;
|
|
32
59
|
/** Resolve absolute path to the installed package's directory inside cache. */
|
package/lib/utils/dlx-cache.js
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
// - prepare into a fresh temp dir, then atomically swap a `pkg` symlink
|
|
7
7
|
// - TTL via lstat mtime + maxAgeMinutes (default 7 days)
|
|
8
8
|
import { createHash } from 'node:crypto';
|
|
9
|
-
import { lstatSync, mkdirSync, realpathSync, renameSync, rmSync
|
|
9
|
+
import { lstatSync, mkdirSync, realpathSync, renameSync, rmSync } from 'node:fs';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
11
|
import { join, resolve } from 'node:path';
|
|
12
|
+
import { dirLinksAreJunctions, linkDirSync } from './dir-link.js';
|
|
12
13
|
const ONE_MINUTE_MS = 60_000;
|
|
13
14
|
const DEFAULT_TTL_MIN = 60 * 24 * 7; // 7 days
|
|
14
15
|
function lexCompare(a, b) {
|
|
@@ -74,29 +75,82 @@ export function getValidCachedPkg(cacheDir, maxAgeMinutes = DEFAULT_TTL_MIN) {
|
|
|
74
75
|
* Atomically swap `<cacheDir>/pkg` to point at `prepareDir`.
|
|
75
76
|
*
|
|
76
77
|
* Strategy:
|
|
77
|
-
* 1. Create new
|
|
78
|
-
* 2. `rename(pkg
|
|
78
|
+
* 1. Create new link `<cacheDir>/pkg.tmp-<ts>-<pid>` → prepareDir.
|
|
79
|
+
* 2. `rename(pkg.tmp-…, pkg)` — POSIX guarantees rename-over-existing is atomic.
|
|
79
80
|
*
|
|
80
81
|
* Returns the realpath of the new live target. EBUSY/EEXIST indicates a race
|
|
81
82
|
* — a parallel process won, return its realpath.
|
|
83
|
+
*
|
|
84
|
+
* WINDOWS, both halves of which shipped broken:
|
|
85
|
+
*
|
|
86
|
+
* • Step 1 used `symlinkSync(…, 'dir')`, and a directory SYMLINK needs
|
|
87
|
+
* elevation or Developer Mode there, so a plain
|
|
88
|
+
* `npx @gjsify/cli@latest showcase …` died with `EPERM: operation not
|
|
89
|
+
* permitted, symlink`. It now goes through {@link linkDirSync}, which picks
|
|
90
|
+
* an NTFS junction — the same choice npm and yarn make, and the one
|
|
91
|
+
* `commands/install.ts` already made for workspace links without sharing it.
|
|
92
|
+
* • Step 2's rename-over-existing is a POSIX guarantee, not a Windows one:
|
|
93
|
+
* `MoveFileEx` will not replace an existing DIRECTORY, and a junction is a
|
|
94
|
+
* directory reparse point. So on the second run — when `pkg` already exists
|
|
95
|
+
* — the rename failed with `EPERM`, the catch below read that as "race
|
|
96
|
+
* lost", and the function returned the OLD target. That is not a crash but
|
|
97
|
+
* it is worse than one: the cache silently never refreshed. Windows
|
|
98
|
+
* therefore unlinks first and accepts the non-atomic window, which the
|
|
99
|
+
* existing race handling already covers.
|
|
100
|
+
*
|
|
101
|
+
* @param opts.junctions force the Windows link strategy (TESTS ONLY). Defaults
|
|
102
|
+
* to the host's. It is a parameter for the same reason `dirLinkTarget` takes
|
|
103
|
+
* `linkType`: nothing in CI runs on Windows, so an injected `true` is the ONLY
|
|
104
|
+
* way the unlink-then-rename ordering above is ever EXECUTED, and an
|
|
105
|
+
* unexecutable Windows branch is exactly how the `'dir'` bug shipped. What the
|
|
106
|
+
* injection proves off-host is the ORDERING and that the second swap really
|
|
107
|
+
* refreshes; what it cannot prove is `MoveFileEx`/junction syscall behaviour.
|
|
82
108
|
*/
|
|
83
|
-
export function symlinkSwap(cacheDir, prepareDir) {
|
|
109
|
+
export function symlinkSwap(cacheDir, prepareDir, opts = {}) {
|
|
110
|
+
const junctions = opts.junctions ?? dirLinksAreJunctions();
|
|
84
111
|
const linkPath = join(cacheDir, 'pkg');
|
|
85
112
|
const tmpName = `pkg.tmp-${Date.now().toString(16)}-${process.pid.toString(16)}`;
|
|
86
113
|
const tmpLink = join(cacheDir, tmpName);
|
|
87
114
|
// If we cannot even create the tmp link, give up (the error propagates).
|
|
88
|
-
|
|
115
|
+
linkDirSync(tmpLink, prepareDir);
|
|
89
116
|
try {
|
|
117
|
+
if (junctions) {
|
|
118
|
+
// Windows: no rename-over-a-directory. Removing first opens a window
|
|
119
|
+
// in which `pkg` is absent; a concurrent reader treats that as a cache
|
|
120
|
+
// miss and prepares its own, which is correct, just wasteful. force:true
|
|
121
|
+
// keeps a first-ever swap (nothing to remove) from throwing.
|
|
122
|
+
//
|
|
123
|
+
// maxRetries/retryDelay for the same reason `install-backend-native.ts`
|
|
124
|
+
// wraps every delete in `rmWithRetry`: on Windows a delete fails with
|
|
125
|
+
// EBUSY/EPERM while ANY process holds a handle (an antivirus scan, a
|
|
126
|
+
// concurrent reader that just realpath'd this very link), and the
|
|
127
|
+
// deletion then stays PENDING — so without the retry a transient
|
|
128
|
+
// handle sends us down the "race lost" branch and the cache silently
|
|
129
|
+
// does not refresh. Free on POSIX, where the first attempt succeeds.
|
|
130
|
+
rmSync(linkPath, { force: true, recursive: false, maxRetries: 10, retryDelay: 100 });
|
|
131
|
+
}
|
|
90
132
|
renameSync(tmpLink, linkPath);
|
|
91
133
|
}
|
|
92
134
|
catch (err) {
|
|
93
135
|
const code = err.code;
|
|
94
|
-
if (code === 'EBUSY' || code === 'EPERM' || code === 'EEXIST') {
|
|
136
|
+
if (code === 'EBUSY' || code === 'EPERM' || code === 'EEXIST' || code === 'EACCES') {
|
|
95
137
|
// Race lost — clean up our tmp and use whoever won. force:true is
|
|
96
138
|
// the non-throwing spelling of "remove if still there"; any other
|
|
97
139
|
// failure on our own pid-unique link is real and should surface.
|
|
98
140
|
rmSync(tmpLink, { force: true });
|
|
99
|
-
|
|
141
|
+
try {
|
|
142
|
+
return realpathSync(linkPath);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Windows only, and only because the junction path unlinks before
|
|
146
|
+
// it renames: "race lost" can also mean the winner is INSIDE that
|
|
147
|
+
// non-atomic window, so `pkg` momentarily does not exist and this
|
|
148
|
+
// would throw ENOENT out of a function whose caller reports it to
|
|
149
|
+
// the user as `Could not resolve showcase "<name>"`. Our own
|
|
150
|
+
// prepareDir is a complete, correctly installed tree — a truthful
|
|
151
|
+
// answer for THIS run. The next run reads whichever link won.
|
|
152
|
+
return realpathSync(prepareDir);
|
|
153
|
+
}
|
|
100
154
|
}
|
|
101
155
|
throw err;
|
|
102
156
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Packument } from '@gjsify/npm-registry';
|
|
2
2
|
import type { InstallOptions } from './install-backend.ts';
|
|
3
|
+
import { type PlatformDeclaration, type PlatformTarget } from './platform-check.js';
|
|
3
4
|
interface ParsedSpec {
|
|
4
5
|
name: string;
|
|
5
6
|
range: string;
|
|
@@ -19,12 +20,105 @@ interface ResolvedNode {
|
|
|
19
20
|
/** `optionalDependencies` field from the packument. */
|
|
20
21
|
optionalDependencies: Record<string, string>;
|
|
21
22
|
bin?: string | Record<string, string>;
|
|
23
|
+
/**
|
|
24
|
+
* The version's `os`/`cpu`/`libc` declaration, as published. Recorded on the
|
|
25
|
+
* node (and in the lockfile) so the compatibility VERDICT can be recomputed
|
|
26
|
+
* for whichever host runs the install — see the header note on portability.
|
|
27
|
+
*
|
|
28
|
+
* Read from the FULL packument, which is the only document that carries
|
|
29
|
+
* `libc`. That is not an optimisation detail leaking into a comment: the
|
|
30
|
+
* abbreviated document can never PROVE the absence of a `libc` restriction,
|
|
31
|
+
* so a declaration built from it would be silently incomplete for exactly
|
|
32
|
+
* the packages the field exists for (this repo publishes nine of them —
|
|
33
|
+
* `@gjsify/{rolldown,lightningcss,oxfmt}-native`, `webgl`, `napi`,
|
|
34
|
+
* `sab-native`, `terminal-native`, `http2-native`, `http-soup-bridge` all
|
|
35
|
+
* declare `libc: ["glibc"]` and NO `os`/`cpu`).
|
|
36
|
+
*/
|
|
37
|
+
platform?: PlatformDeclaration;
|
|
38
|
+
/**
|
|
39
|
+
* Is this node reachable ONLY through optionalDependency edges? Decides what
|
|
40
|
+
* an incompatible platform means: skip silently (optional) vs. fail the
|
|
41
|
+
* install (required). npm calls the same set `optionalSet`.
|
|
42
|
+
*
|
|
43
|
+
* DERIVED, not an input — {@link computeOptionalFlags} recomputes it as a
|
|
44
|
+
* fixpoint over the placed graph before anything reads it, on the fresh-
|
|
45
|
+
* resolve AND the lockfile path. The value the BFS assigns at placement time
|
|
46
|
+
* is a forward guess used only to decide whether a resolve FAILURE under this
|
|
47
|
+
* node is tolerated; it is edge-order-dependent and must never be the final
|
|
48
|
+
* answer (header note, invariant 2).
|
|
49
|
+
*
|
|
50
|
+
* OPTIONAL FIELD, and absent must read as `false` (= required): nodes are
|
|
51
|
+
* also built structurally by callers that only care about name/installPath,
|
|
52
|
+
* and the safe default for an unknown edge kind is the loud one — a required
|
|
53
|
+
* incompatible dependency fails the install instead of being silently
|
|
54
|
+
* dropped from the tree.
|
|
55
|
+
*/
|
|
56
|
+
optional?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Set by {@link applyPlatformFilter}: this node cannot run on the install
|
|
59
|
+
* target, so it is not downloaded or extracted. Never persisted — it is a
|
|
60
|
+
* per-host verdict, not a property of the resolution.
|
|
61
|
+
*/
|
|
62
|
+
inert?: boolean;
|
|
22
63
|
}
|
|
23
64
|
export interface InstalledTopLevel {
|
|
24
65
|
name: string;
|
|
25
66
|
version: string;
|
|
26
67
|
}
|
|
27
|
-
|
|
68
|
+
/**
|
|
69
|
+
* {@link InstallOptions} plus what only the NATIVE backend can use.
|
|
70
|
+
*
|
|
71
|
+
* `optionalSpecs` exists because a spec is a flat `"<name>@<range>"` string by
|
|
72
|
+
* the time it reaches here — `projectSpecsFromPackageJson` flattens
|
|
73
|
+
* `dependencies`, `devDependencies` and `optionalDependencies` into one list —
|
|
74
|
+
* yet the KIND decides what an incompatible `os`/`cpu`/`libc` means: an optional
|
|
75
|
+
* dependency is skipped, a required one fails the install. The shape that makes
|
|
76
|
+
* this concrete is `optionalDependencies: { fsevents }` in a project's own
|
|
77
|
+
* manifest: on Linux npm leaves fsevents out and installs fine, so treating
|
|
78
|
+
* every top-level spec as required would turn that into a hard EBADPLATFORM for
|
|
79
|
+
* a package nothing on the host would ever load.
|
|
80
|
+
*
|
|
81
|
+
* It rides the SAME options object `install-backend.ts` forwards wholesale to
|
|
82
|
+
* this function. If that forwarding is ever changed to destructure, this field
|
|
83
|
+
* must be forwarded with it — otherwise top-level optional deps quietly become
|
|
84
|
+
* required again (and the `install-platform-filter` e2e goes red, which is where
|
|
85
|
+
* that assumption is pinned).
|
|
86
|
+
*/
|
|
87
|
+
export interface NativeInstallOptions extends InstallOptions {
|
|
88
|
+
/**
|
|
89
|
+
* Names (not spec strings — the range differs per requester) of top-level
|
|
90
|
+
* specs declared as `optionalDependencies`. Absent ⇒ every top-level spec is
|
|
91
|
+
* required, the pre-platform-filter behaviour.
|
|
92
|
+
*/
|
|
93
|
+
optionalSpecs?: Set<string>;
|
|
94
|
+
}
|
|
95
|
+
export declare function installPackagesNative(opts: NativeInstallOptions): Promise<InstalledTopLevel[]>;
|
|
96
|
+
/**
|
|
97
|
+
* Split a resolved tree into what THIS target can install, marking the rest
|
|
98
|
+
* inert. Mirrors arborist's post-resolve platform pass
|
|
99
|
+
* (`refs/npm-cli/workspaces/arborist/lib/arborist/build-ideal-tree.js`, the
|
|
100
|
+
* `checkPlatform` loop around line 210) rather than filtering during the walk,
|
|
101
|
+
* for two reasons: the verdict must be identical whether the tree came from a
|
|
102
|
+
* fresh resolve or from `gjsify-lock.json`, and a node's optionality is only
|
|
103
|
+
* final once every edge that could reach it has been visited.
|
|
104
|
+
*
|
|
105
|
+
* The two outcomes are npm's, including the asymmetry around `force`:
|
|
106
|
+
* - REQUIRED + incompatible → EBADPLATFORM. A required dependency the host
|
|
107
|
+
* cannot run is a broken install, not a smaller one; failing loudly with
|
|
108
|
+
* `pkgid`/`current`/`required` is the only honest answer. `--force`
|
|
109
|
+
* suppresses it (the user is asserting something about their own machine).
|
|
110
|
+
* - OPTIONAL + incompatible → inert: not downloaded, not extracted, still in
|
|
111
|
+
* the lockfile. `--force` does NOT lift this — npm says so in as many words
|
|
112
|
+
* ("We ignore the --force and --engine-strict flags") and it is right:
|
|
113
|
+
* forcing an optional binary that cannot load buys a download and nothing
|
|
114
|
+
* else.
|
|
115
|
+
*
|
|
116
|
+
* The skip line goes through the debug logger with npm's `current`/`required`
|
|
117
|
+
* payload, so `--verbose` recovers exactly WHY a package is absent. Silence
|
|
118
|
+
* here is what made the original defect invisible in the other direction: 3.67
|
|
119
|
+
* GB arrived with no line saying it should not have.
|
|
120
|
+
*/
|
|
121
|
+
export declare function applyPlatformFilter(nodes: ResolvedNode[], target: PlatformTarget, force: boolean, log: Logger): ResolvedNode[];
|
|
28
122
|
/**
|
|
29
123
|
* Warn about a native package that installed WITHOUT a usable binary.
|
|
30
124
|
*
|
|
@@ -64,6 +158,20 @@ export declare function writeBinEntry(opts: {
|
|
|
64
158
|
binName: string;
|
|
65
159
|
targetAbs: string;
|
|
66
160
|
platform?: string;
|
|
161
|
+
/**
|
|
162
|
+
* Set when this entry is the package's GJS-runnable artifact (declared via
|
|
163
|
+
* `gjsify.bin`). Then the entry is a GENERATED launcher rather than a link
|
|
164
|
+
* to a file with a shebang — see {@link buildShLauncher} for why, and
|
|
165
|
+
* {@link pickBinMap} for why `gjsify.bin` has to win here at all.
|
|
166
|
+
*
|
|
167
|
+
* The preamble is what exports `GI_TYPELIB_PATH` / the loader path before
|
|
168
|
+
* `gjs` starts. It cannot be done from inside the bundle: GI resolves the
|
|
169
|
+
* typelib while the runtime boots, before the first line runs.
|
|
170
|
+
*/
|
|
171
|
+
gjs?: {
|
|
172
|
+
envPreamble: string;
|
|
173
|
+
prebuildDirs: readonly string[];
|
|
174
|
+
};
|
|
67
175
|
}): void;
|
|
68
176
|
type Logger = (fmt: string, ...args: unknown[]) => void;
|
|
69
177
|
export {};
|