@gjsify/cli 0.51.1 → 0.53.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 (126) hide show
  1. package/dist/cli.gjs.mjs +539 -493
  2. package/lib/actions/build.d.ts +22 -0
  3. package/lib/actions/build.js +37 -9
  4. package/lib/bundler-pick.d.ts +37 -1
  5. package/lib/bundler-pick.js +108 -17
  6. package/lib/cli-app.js +7 -2
  7. package/lib/commands/affected-classify.d.ts +7 -1
  8. package/lib/commands/affected-classify.js +66 -9
  9. package/lib/commands/affected-script-refs.d.ts +20 -0
  10. package/lib/commands/affected-script-refs.js +218 -0
  11. package/lib/commands/affected.js +24 -2
  12. package/lib/commands/env.d.ts +31 -0
  13. package/lib/commands/env.js +114 -0
  14. package/lib/commands/exec.d.ts +10 -0
  15. package/lib/commands/exec.js +208 -0
  16. package/lib/commands/flatpak/diff.js +8 -2
  17. package/lib/commands/flatpak/init.js +4 -1
  18. package/lib/commands/flatpak/sources.js +4 -3
  19. package/lib/commands/flatpak/sync-flathub.d.ts +2 -0
  20. package/lib/commands/flatpak/sync-flathub.js +149 -7
  21. package/lib/commands/flatpak/utils.d.ts +10 -0
  22. package/lib/commands/flatpak/utils.js +10 -0
  23. package/lib/commands/foreach.js +5 -97
  24. package/lib/commands/format.js +26 -6
  25. package/lib/commands/index.d.ts +4 -0
  26. package/lib/commands/index.js +4 -0
  27. package/lib/commands/install.d.ts +2 -2
  28. package/lib/commands/install.js +81 -5
  29. package/lib/commands/link.d.ts +13 -0
  30. package/lib/commands/link.js +243 -0
  31. package/lib/commands/onboard.js +18 -2
  32. package/lib/commands/publish.d.ts +34 -0
  33. package/lib/commands/publish.js +153 -8
  34. package/lib/commands/self-update.js +2 -1
  35. package/lib/commands/ship.js +19 -1
  36. package/lib/commands/test.d.ts +18 -9
  37. package/lib/commands/test.js +56 -11
  38. package/lib/commands/tsc.js +4 -4
  39. package/lib/commands/upgrade.d.ts +15 -0
  40. package/lib/commands/upgrade.js +76 -2
  41. package/lib/commands/webext/index.d.ts +2 -0
  42. package/lib/commands/webext/index.js +15 -0
  43. package/lib/commands/webext/subcommands.d.ts +16 -0
  44. package/lib/commands/webext/subcommands.js +204 -0
  45. package/lib/types/config-data.d.ts +2 -2
  46. package/lib/utils/bin-shim.d.ts +8 -2
  47. package/lib/utils/bin-shim.js +6 -5
  48. package/lib/utils/bundle-inputs.d.ts +84 -0
  49. package/lib/utils/bundle-inputs.js +268 -0
  50. package/lib/utils/check-system-deps.d.ts +5 -19
  51. package/lib/utils/check-system-deps.js +31 -94
  52. package/lib/utils/dev-link.d.ts +225 -0
  53. package/lib/utils/dev-link.js +684 -0
  54. package/lib/utils/exec-bin.d.ts +137 -0
  55. package/lib/utils/exec-bin.js +321 -0
  56. package/lib/utils/gi-runtime-paths.js +3 -2
  57. package/lib/utils/gitignore.d.ts +15 -0
  58. package/lib/utils/gitignore.js +178 -0
  59. package/lib/utils/gjsify-shim.d.ts +47 -0
  60. package/lib/utils/gjsify-shim.js +97 -8
  61. package/lib/utils/hang-watchdog.d.ts +95 -0
  62. package/lib/utils/hang-watchdog.js +194 -0
  63. package/lib/utils/install-backend-native.d.ts +33 -1
  64. package/lib/utils/install-backend-native.js +184 -20
  65. package/lib/utils/install-extraneous.d.ts +90 -0
  66. package/lib/utils/install-extraneous.js +120 -0
  67. package/lib/utils/install-lock.js +4 -25
  68. package/lib/utils/install-tarball-cache.d.ts +37 -9
  69. package/lib/utils/install-tarball-cache.js +59 -14
  70. package/lib/utils/native-skip-scan.d.ts +20 -0
  71. package/lib/utils/native-skip-scan.js +343 -0
  72. package/lib/utils/node-script.js +1 -1
  73. package/lib/utils/normalize-bundler-options.js +11 -0
  74. package/lib/utils/oxc-resolve.js +42 -3
  75. package/lib/utils/process-table.d.ts +22 -0
  76. package/lib/utils/process-table.js +188 -0
  77. package/lib/utils/prune-prefix.js +1 -1
  78. package/lib/utils/publish-pins.d.ts +38 -0
  79. package/lib/utils/publish-pins.js +47 -0
  80. package/lib/utils/publish-readback.d.ts +103 -17
  81. package/lib/utils/publish-readback.js +326 -47
  82. package/lib/utils/run-gjs.d.ts +15 -1
  83. package/lib/utils/run-gjs.js +98 -13
  84. package/lib/utils/run-node.d.ts +2 -0
  85. package/lib/utils/run-node.js +2 -1
  86. package/lib/utils/runtimes.js +14 -13
  87. package/lib/utils/ship/gzip.js +1 -1
  88. package/lib/utils/ship/icns.d.ts +14 -0
  89. package/lib/utils/ship/icns.js +83 -0
  90. package/lib/utils/ship/ico.d.ts +25 -0
  91. package/lib/utils/ship/ico.js +120 -0
  92. package/lib/utils/ship/icons.d.ts +197 -0
  93. package/lib/utils/ship/icons.js +399 -0
  94. package/lib/utils/ship/launcher.js +16 -1
  95. package/lib/utils/ship/layout.d.ts +32 -0
  96. package/lib/utils/ship/layout.js +112 -38
  97. package/lib/utils/ship/msi.d.ts +1 -1
  98. package/lib/utils/ship/msi.js +30 -5
  99. package/lib/utils/ship/pe-launcher.d.ts +18 -0
  100. package/lib/utils/ship/pe-launcher.js +125 -4
  101. package/lib/utils/ship/plan.d.ts +1 -34
  102. package/lib/utils/ship/plan.js +7 -56
  103. package/lib/utils/ship/plist.d.ts +20 -10
  104. package/lib/utils/ship/plist.js +29 -12
  105. package/lib/utils/ship/settings.js +3 -2
  106. package/lib/utils/spawn.d.ts +13 -0
  107. package/lib/utils/spawn.js +10 -5
  108. package/lib/utils/watch-loop.d.ts +15 -1
  109. package/lib/utils/watch-loop.js +9 -5
  110. package/lib/utils/webext/build.d.ts +42 -0
  111. package/lib/utils/webext/build.js +120 -0
  112. package/lib/utils/webext/config.d.ts +72 -0
  113. package/lib/utils/webext/config.js +234 -0
  114. package/lib/utils/webext/icons.d.ts +24 -0
  115. package/lib/utils/webext/icons.js +83 -0
  116. package/lib/utils/webext/launch.d.ts +32 -0
  117. package/lib/utils/webext/launch.js +77 -0
  118. package/lib/utils/webext/manifest.d.ts +53 -0
  119. package/lib/utils/webext/manifest.js +219 -0
  120. package/lib/utils/webext/pages.d.ts +26 -0
  121. package/lib/utils/webext/pages.js +61 -0
  122. package/lib/utils/webext/targets.d.ts +13 -0
  123. package/lib/utils/webext/targets.js +36 -0
  124. package/lib/utils/win32-command.d.ts +17 -0
  125. package/lib/utils/win32-command.js +21 -6
  126. package/package.json +20 -20
@@ -0,0 +1,137 @@
1
+ import type { ExampleRuntime } from './runtimes.js';
2
+ /** `gjsify exec`'s own options and the bin invocation that follows them. */
3
+ export interface ExecInvocation {
4
+ runtime: ExampleRuntime | null;
5
+ rebuild: boolean;
6
+ verbose: boolean;
7
+ bin: string;
8
+ args: string[];
9
+ }
10
+ /**
11
+ * Split `gjsify exec [options] <bin> [args…]`. Options are gjsify's only BEFORE the
12
+ * bin name; everything after it belongs to the bin, flags included, as with `npx`
13
+ * — `gjsify exec wxt --runtime x` hands `--runtime x` to wxt. yargs cannot draw
14
+ * that line (a declared option is taken wherever it appears), so the command
15
+ * declares none and this function reads the leading ones.
16
+ *
17
+ * `tail` is what yargs split off at the first `--`: one AFTER the bin belongs to
18
+ * the bin, one BEFORE it only separates (`gjsify exec -- --weird-bin-name`).
19
+ */
20
+ export declare function splitExecArgv(entries: readonly string[], tail?: readonly string[]): ExecInvocation;
21
+ /** A bin name resolved to the files that implement it. */
22
+ export interface ResolvedBin {
23
+ binName: string;
24
+ pkgName: string;
25
+ pkgVersion: string;
26
+ /** Real path of the owning package's directory. */
27
+ pkgDir: string;
28
+ /** npm `bin` target (real path) — the file node/bun/deno run. Null when only `gjsify.bin` names it. */
29
+ entry: string | null;
30
+ /** `gjsify.bin` target (real path) — a bundle the package already ships for GJS. */
31
+ gjsEntry: string | null;
32
+ /**
33
+ * The `node_modules` directory the package was found through, or null for the
34
+ * project's OWN bin. The rebuilt bundle is written below it — see
35
+ * {@link execCacheRoot} for why it cannot live in the XDG cache.
36
+ */
37
+ nodeModules: string | null;
38
+ /**
39
+ * True when the package's real location is inside a `node_modules` tree, i.e. an
40
+ * INSTALLED copy whose version string identifies its content. A workspace link or
41
+ * the project's own bin is edited in place without a version bump, so its rebuild
42
+ * is never reused.
43
+ */
44
+ installed: boolean;
45
+ }
46
+ export declare class ExecBinNotFoundError extends Error {
47
+ readonly binName: string;
48
+ readonly searched: readonly string[];
49
+ constructor(binName: string, searched: readonly string[]);
50
+ }
51
+ export declare class ExecPlanError extends Error {
52
+ constructor(message: string);
53
+ }
54
+ /**
55
+ * Resolve `binName` the way `npx` does for an installed bin: the project's OWN
56
+ * `package.json#bin` first, then every `node_modules` from `cwd` up to the root.
57
+ *
58
+ * `.bin/<name>` is only a HINT. Its shape depends on who wrote it — a symlink (npm,
59
+ * pnpm), an `sh` launcher (`gjsify install` for a `gjsify.bin`), a `.cmd`/`.ps1` pair
60
+ * (Windows) — and only the symlink names its target, so the other shapes fall back to
61
+ * reading the package manifests of that `node_modules`. A package whose NAME is the
62
+ * bin name wins that scan, as `npx <pkg>` would pick it.
63
+ */
64
+ export declare function resolveExecBin(binName: string, cwd: string): ResolvedBin;
65
+ /** What `gjsify exec` does with a resolved bin on a given runtime. */
66
+ export type ExecPlan =
67
+ /** node/bun/deno: run the npm entry unchanged — nothing is built. */
68
+ {
69
+ kind: 'direct';
70
+ runtime: Exclude<ExampleRuntime, 'gjs'>;
71
+ file: string;
72
+ }
73
+ /** gjs, and the package ships its own GJS bundle (`gjsify.bin`): run that. */
74
+ | {
75
+ kind: 'gjs-bundle';
76
+ file: string;
77
+ }
78
+ /** gjs, Node-only package: rebuild the npm entry `--app gjs`, then run the bundle. */
79
+ | {
80
+ kind: 'rebuild';
81
+ entry: string;
82
+ };
83
+ /**
84
+ * Pick the plan. `runtime` is the HOST runtime unless the caller passed an explicit
85
+ * `--runtime`; there is no implicit fallback from gjs to node — a failed rebuild is
86
+ * reported, never papered over by a runtime the user did not ask for.
87
+ */
88
+ export declare function planExec(bin: ResolvedBin, runtime: ExampleRuntime): ExecPlan;
89
+ /** Inputs that decide whether a cached rebuild still describes the bin. */
90
+ export interface ExecCacheKeyInput {
91
+ pkgName: string;
92
+ pkgVersion: string;
93
+ /** The entry, relative to the package directory. */
94
+ entry: string;
95
+ /** The running CLI's version: a new bundler or polyfill set is a new artifact. */
96
+ cliVersion: string;
97
+ /** sha256 of the project's lockfile, or null when there is none. */
98
+ lockfileHash: string | null;
99
+ }
100
+ /**
101
+ * Content key of a rebuilt bin. The package version identifies the entry's own code;
102
+ * the LOCKFILE identifies its dependencies, which a reinstall can move without the
103
+ * version changing; the CLI version identifies the bundler and every polyfill that
104
+ * went into the artifact. With no lockfile, dependency drift is invisible to the key,
105
+ * which is why `--rebuild` exists.
106
+ */
107
+ export declare function execCacheKey(input: ExecCacheKeyInput): string;
108
+ /** The nearest lockfile walking up from `start`, or null. */
109
+ export declare function findProjectLockfile(start: string): string | null;
110
+ export declare function hashFile(path: string): string;
111
+ /**
112
+ * Where rebuilt bins are written: `<node_modules>/.cache/gjsify/exec/`.
113
+ *
114
+ * NOT `$XDG_CACHE_HOME`, although the key is content-addressed and would allow it:
115
+ * a bundled dependency that reads its own files (`package.json` for `--version`, a
116
+ * template, a locale) has its `import.meta.url`/`__dirname` rewritten to resolve the
117
+ * package AT RUNTIME from the bundle's location (`shims/module-resolve.ts`). From a
118
+ * directory outside the project's `node_modules` that walk finds nothing and falls
119
+ * back to the bundle's own directory, so the read misses.
120
+ */
121
+ export declare function execCacheRoot(nodeModules: string): string;
122
+ /**
123
+ * Directory + file name of a rebuilt bin. The hash goes in the DIRECTORY and the
124
+ * file keeps the entry's basename: a bin that tests `process.argv[1]` against its
125
+ * own name (the standard is-main guard) must still see it — the same lesson as
126
+ * `bundleFileForGjsCached`'s layout.
127
+ */
128
+ export declare function execArtifactPath(cacheRoot: string, bin: Pick<ResolvedBin, 'pkgName' | 'pkgVersion'>, entry: string, key: string): string;
129
+ /**
130
+ * The entry's basename, except that `.cjs` becomes `.mjs`: the artifact is ESM, and
131
+ * the bundler reads the output FORMAT off a `.cjs` file name — measured on prettier's
132
+ * `bin/prettier.cjs`, whose rebuild came out as CommonJS with `require("gi://GLib")`
133
+ * and died under gjs with `ReferenceError: require is not defined`.
134
+ */
135
+ export declare function artifactFileName(entry: string): string;
136
+ /** The entry path relative to its package, for the key. */
137
+ export declare function entryWithinPackage(bin: Pick<ResolvedBin, 'pkgDir'>, entry: string): string;
@@ -0,0 +1,321 @@
1
+ // `gjsify exec <bin>` — the pure half: which file a bin name means, which runtime
2
+ // runs it, and the key its rebuilt artifact is cached under (ADR 0076).
3
+ //
4
+ // Kept free of spawning and bundling so every decision is unit-testable on any host;
5
+ // `commands/exec.ts` is the thin shell that acts on the answers.
6
+ import { createHash } from 'node:crypto';
7
+ import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
8
+ import { basename, dirname, join, relative, resolve, sep } from 'node:path';
9
+ import { normalizeBinMap } from './bin-shim.js';
10
+ import { DEP_CHANGE_LOCKFILES } from './package-inputs.js';
11
+ import { readPackageJson } from './pkg-json.js';
12
+ const EXEC_RUNTIMES = ['gjs', 'node', 'bun', 'deno'];
13
+ /**
14
+ * Split `gjsify exec [options] <bin> [args…]`. Options are gjsify's only BEFORE the
15
+ * bin name; everything after it belongs to the bin, flags included, as with `npx`
16
+ * — `gjsify exec wxt --runtime x` hands `--runtime x` to wxt. yargs cannot draw
17
+ * that line (a declared option is taken wherever it appears), so the command
18
+ * declares none and this function reads the leading ones.
19
+ *
20
+ * `tail` is what yargs split off at the first `--`: one AFTER the bin belongs to
21
+ * the bin, one BEFORE it only separates (`gjsify exec -- --weird-bin-name`).
22
+ */
23
+ export function splitExecArgv(entries, tail = []) {
24
+ const out = { runtime: null, rebuild: false, verbose: false };
25
+ const setRuntime = (value) => {
26
+ if (value === undefined || !EXEC_RUNTIMES.includes(value)) {
27
+ throw new ExecPlanError(`gjsify exec: --runtime expects one of ${EXEC_RUNTIMES.join(', ')}, got ${value === undefined ? 'nothing' : `"${value}"`}.`);
28
+ }
29
+ out.runtime = value;
30
+ };
31
+ let i = 0;
32
+ for (; i < entries.length; i++) {
33
+ const word = entries[i];
34
+ if (!word.startsWith('-'))
35
+ break;
36
+ if (word === '--rebuild')
37
+ out.rebuild = true;
38
+ else if (word === '--verbose')
39
+ out.verbose = true;
40
+ else if (word === '--runtime')
41
+ setRuntime(entries[++i]);
42
+ else if (word.startsWith('--runtime='))
43
+ setRuntime(word.slice('--runtime='.length));
44
+ else {
45
+ throw new ExecPlanError(`gjsify exec: unknown option "${word}" before the bin name. Options for the bin go AFTER it ` +
46
+ '(`gjsify exec <bin> --flag`); gjsify exec itself takes --runtime <gjs|node|bun|deno>, --rebuild, --verbose.');
47
+ }
48
+ }
49
+ if (i < entries.length) {
50
+ return { ...out, bin: entries[i], args: [...entries.slice(i + 1), ...(tail.length ? ['--', ...tail] : [])] };
51
+ }
52
+ if (tail.length > 0)
53
+ return { ...out, bin: tail[0], args: tail.slice(1) };
54
+ throw new ExecPlanError('gjsify exec: no bin given. Usage: gjsify exec [--runtime <r>] [--rebuild] <bin> [args…]');
55
+ }
56
+ export class ExecBinNotFoundError extends Error {
57
+ binName;
58
+ searched;
59
+ constructor(binName, searched) {
60
+ super(`gjsify exec: no bin named "${binName}" in this project.\n` +
61
+ `Looked in the nearest package.json and in:\n${searched.map((d) => ` ${d}`).join('\n') || ' (no node_modules found)'}\n` +
62
+ `Install the package that provides it (\`gjsify install <pkg>\`), or use \`gjsify dlx\` for a one-off run.`);
63
+ this.binName = binName;
64
+ this.searched = searched;
65
+ this.name = 'ExecBinNotFoundError';
66
+ }
67
+ }
68
+ export class ExecPlanError extends Error {
69
+ constructor(message) {
70
+ super(message);
71
+ this.name = 'ExecPlanError';
72
+ }
73
+ }
74
+ function readManifest(pkgDir) {
75
+ return readPackageJson(join(pkgDir, 'package.json'));
76
+ }
77
+ function realOrNull(path) {
78
+ try {
79
+ return realpathSync(path);
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ }
85
+ function isInsideNodeModules(path) {
86
+ return path.split(sep).includes('node_modules');
87
+ }
88
+ /** The bin's targets in `pkgDir`, or null when the package does not declare `binName`. */
89
+ function binFromPackage(pkgDir, binName, nodeModules) {
90
+ const manifest = readManifest(pkgDir);
91
+ if (!manifest?.name)
92
+ return null;
93
+ const npmTarget = manifest.bin === undefined ? undefined : normalizeBinMap(manifest.name, manifest.bin).get(binName);
94
+ const gjsTarget = manifest.gjsify?.bin === undefined
95
+ ? undefined
96
+ : normalizeBinMap(manifest.name, manifest.gjsify.bin).get(binName);
97
+ if (npmTarget === undefined && gjsTarget === undefined)
98
+ return null;
99
+ const realDir = realOrNull(pkgDir) ?? resolve(pkgDir);
100
+ // A declared target that is not on disk is a broken install, not a different bin:
101
+ // report it as unresolvable rather than silently picking another package.
102
+ const entry = npmTarget === undefined ? null : realOrNull(join(pkgDir, npmTarget));
103
+ const gjsEntry = gjsTarget === undefined ? null : realOrNull(join(pkgDir, gjsTarget));
104
+ if (entry === null && gjsEntry === null)
105
+ return null;
106
+ return {
107
+ binName,
108
+ pkgName: manifest.name,
109
+ pkgVersion: manifest.version ?? '0.0.0',
110
+ pkgDir: realDir,
111
+ entry,
112
+ gjsEntry,
113
+ nodeModules,
114
+ installed: isInsideNodeModules(realDir),
115
+ };
116
+ }
117
+ /** Top-level package directories of a `node_modules` (scoped ones expanded). */
118
+ function listPackages(nodeModules) {
119
+ const out = [];
120
+ let names;
121
+ try {
122
+ names = readdirSync(nodeModules);
123
+ }
124
+ catch {
125
+ return out;
126
+ }
127
+ for (const name of names) {
128
+ if (name.startsWith('.'))
129
+ continue;
130
+ if (name.startsWith('@')) {
131
+ let scoped;
132
+ try {
133
+ scoped = readdirSync(join(nodeModules, name));
134
+ }
135
+ catch {
136
+ continue;
137
+ }
138
+ for (const s of scoped)
139
+ out.push(join(nodeModules, name, s));
140
+ }
141
+ else {
142
+ out.push(join(nodeModules, name));
143
+ }
144
+ }
145
+ return out;
146
+ }
147
+ /**
148
+ * The package that owns the file `.bin/<name>` links to: the nearest ancestor with a
149
+ * `package.json` that declares the bin. npm, pnpm and yarn all write a SYMLINK there
150
+ * on POSIX, which makes this the cheap path.
151
+ */
152
+ function ownerOfLinkedBin(linkTarget, binName, nodeModules) {
153
+ let dir = dirname(linkTarget);
154
+ for (;;) {
155
+ if (existsSync(join(dir, 'package.json'))) {
156
+ const hit = binFromPackage(dir, binName, nodeModules);
157
+ if (hit && (hit.entry === linkTarget || hit.gjsEntry === linkTarget))
158
+ return hit;
159
+ }
160
+ const parent = dirname(dir);
161
+ if (parent === dir)
162
+ return null;
163
+ dir = parent;
164
+ }
165
+ }
166
+ /**
167
+ * Resolve `binName` the way `npx` does for an installed bin: the project's OWN
168
+ * `package.json#bin` first, then every `node_modules` from `cwd` up to the root.
169
+ *
170
+ * `.bin/<name>` is only a HINT. Its shape depends on who wrote it — a symlink (npm,
171
+ * pnpm), an `sh` launcher (`gjsify install` for a `gjsify.bin`), a `.cmd`/`.ps1` pair
172
+ * (Windows) — and only the symlink names its target, so the other shapes fall back to
173
+ * reading the package manifests of that `node_modules`. A package whose NAME is the
174
+ * bin name wins that scan, as `npx <pkg>` would pick it.
175
+ */
176
+ export function resolveExecBin(binName, cwd) {
177
+ const searched = [];
178
+ let dir = resolve(cwd);
179
+ for (;;) {
180
+ if (existsSync(join(dir, 'package.json'))) {
181
+ const own = binFromPackage(dir, binName, null);
182
+ if (own)
183
+ return own;
184
+ break;
185
+ }
186
+ const parent = dirname(dir);
187
+ if (parent === dir)
188
+ break;
189
+ dir = parent;
190
+ }
191
+ dir = resolve(cwd);
192
+ for (;;) {
193
+ const nodeModules = join(dir, 'node_modules');
194
+ if (existsSync(nodeModules)) {
195
+ searched.push(nodeModules);
196
+ const binDir = join(nodeModules, '.bin');
197
+ const hint = [binName, `${binName}.cmd`].some((n) => existsSync(join(binDir, n)));
198
+ if (hint) {
199
+ const link = join(binDir, binName);
200
+ const isLink = (() => {
201
+ try {
202
+ return lstatSync(link).isSymbolicLink();
203
+ }
204
+ catch {
205
+ return false;
206
+ }
207
+ })();
208
+ const linkTarget = isLink ? realOrNull(link) : null;
209
+ const linked = linkTarget ? ownerOfLinkedBin(linkTarget, binName, nodeModules) : null;
210
+ if (linked)
211
+ return linked;
212
+ const candidates = listPackages(nodeModules).sort((a, b) => Number(basename(b) === binName) - Number(basename(a) === binName));
213
+ for (const pkgDir of candidates) {
214
+ const hit = binFromPackage(pkgDir, binName, nodeModules);
215
+ if (hit)
216
+ return hit;
217
+ }
218
+ }
219
+ }
220
+ const parent = dirname(dir);
221
+ if (parent === dir)
222
+ break;
223
+ dir = parent;
224
+ }
225
+ throw new ExecBinNotFoundError(binName, searched);
226
+ }
227
+ /**
228
+ * Pick the plan. `runtime` is the HOST runtime unless the caller passed an explicit
229
+ * `--runtime`; there is no implicit fallback from gjs to node — a failed rebuild is
230
+ * reported, never papered over by a runtime the user did not ask for.
231
+ */
232
+ export function planExec(bin, runtime) {
233
+ if (runtime === 'gjs') {
234
+ if (bin.gjsEntry)
235
+ return { kind: 'gjs-bundle', file: bin.gjsEntry };
236
+ if (bin.entry)
237
+ return { kind: 'rebuild', entry: bin.entry };
238
+ }
239
+ else if (bin.entry) {
240
+ return { kind: 'direct', runtime, file: bin.entry };
241
+ }
242
+ else {
243
+ throw new ExecPlanError(`gjsify exec: "${bin.binName}" from ${bin.pkgName} is declared only as a GJS bundle (\`gjsify.bin\`), ` +
244
+ `which ${runtime} cannot run. Run it on GJS: \`gjsify exec --runtime gjs ${bin.binName}\`.`);
245
+ }
246
+ throw new ExecPlanError(`gjsify exec: "${bin.binName}" from ${bin.pkgName} names no file that exists.`);
247
+ }
248
+ /**
249
+ * Content key of a rebuilt bin. The package version identifies the entry's own code;
250
+ * the LOCKFILE identifies its dependencies, which a reinstall can move without the
251
+ * version changing; the CLI version identifies the bundler and every polyfill that
252
+ * went into the artifact. With no lockfile, dependency drift is invisible to the key,
253
+ * which is why `--rebuild` exists.
254
+ */
255
+ export function execCacheKey(input) {
256
+ const payload = JSON.stringify([
257
+ 'gjsify-exec/v1',
258
+ input.pkgName,
259
+ input.pkgVersion,
260
+ input.entry.split(sep).join('/'),
261
+ input.cliVersion,
262
+ input.lockfileHash,
263
+ ]);
264
+ return createHash('sha256').update(payload).digest('hex');
265
+ }
266
+ /** Lockfiles that pin a dependency tree, `DEP_CHANGE_LOCKFILES` plus bun's. */
267
+ const EXEC_LOCKFILES = [...DEP_CHANGE_LOCKFILES, 'bun.lock', 'bun.lockb'];
268
+ /** The nearest lockfile walking up from `start`, or null. */
269
+ export function findProjectLockfile(start) {
270
+ let dir = resolve(start);
271
+ for (;;) {
272
+ for (const name of EXEC_LOCKFILES) {
273
+ const candidate = join(dir, name);
274
+ if (existsSync(candidate))
275
+ return candidate;
276
+ }
277
+ const parent = dirname(dir);
278
+ if (parent === dir)
279
+ return null;
280
+ dir = parent;
281
+ }
282
+ }
283
+ export function hashFile(path) {
284
+ return createHash('sha256').update(readFileSync(path)).digest('hex');
285
+ }
286
+ /**
287
+ * Where rebuilt bins are written: `<node_modules>/.cache/gjsify/exec/`.
288
+ *
289
+ * NOT `$XDG_CACHE_HOME`, although the key is content-addressed and would allow it:
290
+ * a bundled dependency that reads its own files (`package.json` for `--version`, a
291
+ * template, a locale) has its `import.meta.url`/`__dirname` rewritten to resolve the
292
+ * package AT RUNTIME from the bundle's location (`shims/module-resolve.ts`). From a
293
+ * directory outside the project's `node_modules` that walk finds nothing and falls
294
+ * back to the bundle's own directory, so the read misses.
295
+ */
296
+ export function execCacheRoot(nodeModules) {
297
+ return join(nodeModules, '.cache', 'gjsify', 'exec');
298
+ }
299
+ /**
300
+ * Directory + file name of a rebuilt bin. The hash goes in the DIRECTORY and the
301
+ * file keeps the entry's basename: a bin that tests `process.argv[1]` against its
302
+ * own name (the standard is-main guard) must still see it — the same lesson as
303
+ * `bundleFileForGjsCached`'s layout.
304
+ */
305
+ export function execArtifactPath(cacheRoot, bin, entry, key) {
306
+ const safe = `${bin.pkgName}@${bin.pkgVersion}`.replace(/[^a-zA-Z0-9._@-]/g, '_');
307
+ return join(cacheRoot, `${safe}-${key.slice(0, 16)}`, artifactFileName(entry));
308
+ }
309
+ /**
310
+ * The entry's basename, except that `.cjs` becomes `.mjs`: the artifact is ESM, and
311
+ * the bundler reads the output FORMAT off a `.cjs` file name — measured on prettier's
312
+ * `bin/prettier.cjs`, whose rebuild came out as CommonJS with `require("gi://GLib")`
313
+ * and died under gjs with `ReferenceError: require is not defined`.
314
+ */
315
+ export function artifactFileName(entry) {
316
+ return basename(entry).replace(/\.cjs$/i, '.mjs');
317
+ }
318
+ /** The entry path relative to its package, for the key. */
319
+ export function entryWithinPackage(bin, entry) {
320
+ return relative(bin.pkgDir, entry);
321
+ }
@@ -9,8 +9,9 @@
9
9
  // launcher goes through, so the only repair that survives is one the process makes to
10
10
  // girepository's own search paths from the inside. Measurements, and what this reaches
11
11
  // (loads AFTER module evaluation starts, not a static `gi://` import):
12
- // `status/open-todos.md` § "A globally installed GJS launcher still cannot load a
13
- // system GTK on macOS".
12
+ // `status/open-todos/macos.md` § "A globally installed GJS launcher still cannot load a
13
+ // system GTK on macOS", and the darwin-arm64 end-to-end run under it. The static half is
14
+ // ADR 0085 (`docs/poc/gi-prologue-import-order.md` is its placement study).
14
15
  //
15
16
  // A SHIPPED BUNDLE RUNS ON A MACHINE THE BUILD NEVER SAW, which rules out both
16
17
  // tempting sources. `systemGiLibraryDirs()` MEASURES the build host and answers `[]`
@@ -0,0 +1,15 @@
1
+ export type IgnoreMatch = 'ignore' | 'whitelist' | null;
2
+ export interface GitignoreMatcher {
3
+ /** Directory the patterns are relative to. */
4
+ readonly base: string;
5
+ /** Decide `path` alone; `null` when no line matches or `path` is outside `base`. */
6
+ matched(path: string, isDir: boolean): IgnoreMatch;
7
+ /** Decide `path`, falling back to its parent directories up to `base`. */
8
+ matchedPathOrAnyParents(path: string, isDir: boolean): IgnoreMatch;
9
+ }
10
+ /** Translate one glob to a regex source; `*`, `?` and classes stay inside a segment. */
11
+ export declare function globToRegexSource(glob: string): string;
12
+ /** Build a matcher from pattern lines, relative to `base`. */
13
+ export declare function compileGitignore(lines: readonly string[], base: string): GitignoreMatcher;
14
+ /** Read an ignore file; a missing or unreadable file yields `null`. */
15
+ export declare function readGitignoreFile(file: string, base?: string): GitignoreMatcher | null;
@@ -0,0 +1,178 @@
1
+ // Gitignore-syntax matcher, the semantics of the Rust `ignore` crate's
2
+ // `Gitignore` that oxfmt (and oxlint) build every ignore layer from:
3
+ // `.gitignore`, `.git/info/exclude`, `.prettierignore` and a config's
4
+ // `ignorePatterns`. One reader for all of them, so a layer cannot be read
5
+ // with different rules than its neighbour.
6
+ //
7
+ // Rules held (gitignore(5)): `#` comments, `\#`/`\!` escapes, unescaped
8
+ // trailing spaces dropped, `!` re-includes, a trailing `/` matches
9
+ // directories only, a pattern with a `/` before its end is anchored to the
10
+ // file's directory while one without matches a basename at any depth,
11
+ // `**/`, `/**/` and `/**`, `*`/`?` never cross `/`, `[...]` classes. The
12
+ // LAST matching line of one matcher decides.
13
+ import { readFileSync } from 'node:fs';
14
+ import { dirname, isAbsolute, relative, sep } from 'node:path';
15
+ /** Translate one glob to a regex source; `*`, `?` and classes stay inside a segment. */
16
+ export function globToRegexSource(glob) {
17
+ let out = '';
18
+ let i = 0;
19
+ while (i < glob.length) {
20
+ const c = glob[i];
21
+ if (c === '*' && glob[i + 1] === '*' && (i === 0 || glob[i - 1] === '/')) {
22
+ if (i + 2 === glob.length) {
23
+ out += '.*';
24
+ i += 2;
25
+ continue;
26
+ }
27
+ if (glob[i + 2] === '/') {
28
+ out += '(?:.*/)?';
29
+ i += 3;
30
+ continue;
31
+ }
32
+ }
33
+ if (c === '*') {
34
+ out += '[^/]*';
35
+ while (glob[i + 1] === '*')
36
+ i++;
37
+ }
38
+ else if (c === '?') {
39
+ out += '[^/]';
40
+ }
41
+ else if (c === '[') {
42
+ const close = findClassEnd(glob, i);
43
+ if (close === -1) {
44
+ out += '\\[';
45
+ }
46
+ else {
47
+ let body = glob.slice(i + 1, close);
48
+ let negate = false;
49
+ if (body.startsWith('!') || body.startsWith('^')) {
50
+ negate = true;
51
+ body = body.slice(1);
52
+ }
53
+ // Gitignore class escapes (`\-`, `\\`, `\]`) are already valid
54
+ // regex class escapes — doubling the backslashes would turn
55
+ // `[a\-z]` into a range.
56
+ body = body.replace(/\]/g, '\\]');
57
+ out += `[${negate ? '^/' : ''}${body}]`;
58
+ i = close;
59
+ }
60
+ }
61
+ else if (c === '\\' && i + 1 < glob.length) {
62
+ i++;
63
+ out += escapeRegex(glob[i]);
64
+ }
65
+ else {
66
+ out += escapeRegex(c);
67
+ }
68
+ i++;
69
+ }
70
+ return out;
71
+ }
72
+ function findClassEnd(glob, open) {
73
+ let j = open + 1;
74
+ if (glob[j] === '!' || glob[j] === '^')
75
+ j++;
76
+ if (glob[j] === ']')
77
+ j++;
78
+ for (; j < glob.length; j++)
79
+ if (glob[j] === ']')
80
+ return j;
81
+ return -1;
82
+ }
83
+ function escapeRegex(c) {
84
+ return /[.+^${}()|[\]\\*?]/.test(c) ? `\\${c}` : c;
85
+ }
86
+ function parseLine(raw) {
87
+ let line = raw.replace(/\r$/, '');
88
+ // Trailing spaces are dropped unless the last one is backslash-escaped.
89
+ line = line.replace(/(?<!\\)\s+$/, '');
90
+ if (line === '' || line.startsWith('#'))
91
+ return null;
92
+ let negate = false;
93
+ if (line.startsWith('!')) {
94
+ negate = true;
95
+ line = line.slice(1);
96
+ }
97
+ else if (line.startsWith('\\!') || line.startsWith('\\#')) {
98
+ line = line.slice(1);
99
+ }
100
+ let dirOnly = false;
101
+ if (line.endsWith('/') && !line.endsWith('\\/')) {
102
+ dirOnly = true;
103
+ line = line.slice(0, -1);
104
+ }
105
+ if (line === '')
106
+ return null;
107
+ const anchored = line.includes('/');
108
+ if (line.startsWith('/'))
109
+ line = line.slice(1);
110
+ const src = globToRegexSource(line);
111
+ const prefix = anchored ? '' : '(?:.*/)?';
112
+ return { re: new RegExp(`^${prefix}${src}$`), negate, dirOnly };
113
+ }
114
+ /** Build a matcher from pattern lines, relative to `base`. */
115
+ export function compileGitignore(lines, base) {
116
+ const rules = [];
117
+ for (const line of lines) {
118
+ const rule = parseLine(line);
119
+ if (rule)
120
+ rules.push(rule);
121
+ }
122
+ const relOf = (path) => {
123
+ const rel = relative(base, path);
124
+ if (rel === '' || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel))
125
+ return null;
126
+ return sep === '/' ? rel : rel.split(sep).join('/');
127
+ };
128
+ const decide = (rel, isDir) => {
129
+ for (let k = rules.length - 1; k >= 0; k--) {
130
+ const rule = rules[k];
131
+ if (rule.dirOnly && !isDir)
132
+ continue;
133
+ if (rule.re.test(rel))
134
+ return rule.negate ? 'whitelist' : 'ignore';
135
+ }
136
+ return null;
137
+ };
138
+ return {
139
+ base,
140
+ matched(path, isDir) {
141
+ if (rules.length === 0)
142
+ return null;
143
+ const rel = relOf(path);
144
+ return rel === null ? null : decide(rel, isDir);
145
+ },
146
+ matchedPathOrAnyParents(path, isDir) {
147
+ if (rules.length === 0)
148
+ return null;
149
+ let rel = relOf(path);
150
+ if (rel === null)
151
+ return null;
152
+ // The nearest path component any line decides wins — an ignored
153
+ // ancestor directory hides everything under it.
154
+ let dir = isDir;
155
+ for (;;) {
156
+ const m = decide(rel, dir);
157
+ if (m !== null)
158
+ return m;
159
+ const slash = rel.lastIndexOf('/');
160
+ if (slash === -1)
161
+ return null;
162
+ rel = rel.slice(0, slash);
163
+ dir = true;
164
+ }
165
+ },
166
+ };
167
+ }
168
+ /** Read an ignore file; a missing or unreadable file yields `null`. */
169
+ export function readGitignoreFile(file, base = dirname(file)) {
170
+ let text;
171
+ try {
172
+ text = readFileSync(file, 'utf-8');
173
+ }
174
+ catch {
175
+ return null;
176
+ }
177
+ return compileGitignore(text.split('\n'), base);
178
+ }