@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.
Files changed (57) hide show
  1. package/dist/cli.gjs.mjs +184 -182
  2. package/lib/cli-app.js +3 -1
  3. package/lib/commands/affected.js +146 -15
  4. package/lib/commands/build.js +21 -14
  5. package/lib/commands/check.js +40 -4
  6. package/lib/commands/clear.d.ts +8 -0
  7. package/lib/commands/clear.js +90 -0
  8. package/lib/commands/copy.d.ts +8 -0
  9. package/lib/commands/copy.js +87 -0
  10. package/lib/commands/dlx.js +8 -1
  11. package/lib/commands/index.d.ts +2 -0
  12. package/lib/commands/index.js +2 -0
  13. package/lib/commands/install.d.ts +4 -0
  14. package/lib/commands/install.js +164 -15
  15. package/lib/commands/onboard.js +17 -14
  16. package/lib/commands/publish.js +18 -4
  17. package/lib/commands/storybook.js +5 -1
  18. package/lib/commands/test.js +29 -2
  19. package/lib/commands/uninstall.js +44 -2
  20. package/lib/commands/upgrade.js +1 -4
  21. package/lib/templates/install.mjs.tmpl +110 -19
  22. package/lib/utils/bin-shim.d.ts +73 -0
  23. package/lib/utils/bin-shim.js +97 -2
  24. package/lib/utils/build-cache.d.ts +7 -27
  25. package/lib/utils/build-cache.js +34 -4
  26. package/lib/utils/check-system-deps.d.ts +15 -1
  27. package/lib/utils/check-system-deps.js +126 -2
  28. package/lib/utils/clear-targets.d.ts +30 -0
  29. package/lib/utils/clear-targets.js +114 -0
  30. package/lib/utils/copy-targets.d.ts +41 -0
  31. package/lib/utils/copy-targets.js +145 -0
  32. package/lib/utils/detect-native-packages.d.ts +160 -5
  33. package/lib/utils/detect-native-packages.js +326 -50
  34. package/lib/utils/dir-link.d.ts +86 -0
  35. package/lib/utils/dir-link.js +140 -0
  36. package/lib/utils/dlx-cache.d.ts +30 -3
  37. package/lib/utils/dlx-cache.js +61 -7
  38. package/lib/utils/install-backend-native.d.ts +109 -1
  39. package/lib/utils/install-backend-native.js +622 -59
  40. package/lib/utils/install-backend.js +17 -15
  41. package/lib/utils/install-global.js +6 -34
  42. package/lib/utils/install-lock.js +1 -1
  43. package/lib/utils/install-packument-cache.d.ts +51 -9
  44. package/lib/utils/install-packument-cache.js +88 -25
  45. package/lib/utils/oxc-resolve.d.ts +27 -1
  46. package/lib/utils/oxc-resolve.js +53 -26
  47. package/lib/utils/pkg-json.d.ts +24 -0
  48. package/lib/utils/pkg-json.js +27 -1
  49. package/lib/utils/platform-check.d.ts +215 -0
  50. package/lib/utils/platform-check.js +377 -0
  51. package/lib/utils/resolve-npm-package.js +9 -2
  52. package/lib/utils/run-gjs.d.ts +1 -1
  53. package/lib/utils/run-gjs.js +19 -3
  54. package/lib/utils/spawn.js +50 -2
  55. package/lib/utils/win32-command.d.ts +54 -0
  56. package/lib/utils/win32-command.js +140 -0
  57. 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
+ }
@@ -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 symlink `<cacheDir>/pkg-<rand>` → prepareDir.
24
- * 2. `rename(pkg-<rand>, pkg)` — POSIX guarantees rename-over-existing is atomic.
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): 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. */
@@ -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, symlinkSync } from 'node:fs';
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 symlink `<cacheDir>/pkg-<rand>` → prepareDir.
78
- * 2. `rename(pkg-<rand>, pkg)` — POSIX guarantees rename-over-existing is atomic.
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
- symlinkSync(prepareDir, tmpLink, 'dir');
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
- return realpathSync(linkPath);
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
- export declare function installPackagesNative(opts: InstallOptions): Promise<InstalledTopLevel[]>;
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 {};