@gjsify/cli 0.24.1 → 0.24.2

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.
@@ -16,6 +16,21 @@ export interface EnsureOpts {
16
16
  registry?: string;
17
17
  cacheMaxAge: number;
18
18
  frozen: boolean;
19
+ /**
20
+ * Extra specs to install into the SAME cache tree as the requested package.
21
+ *
22
+ * A dlx tree is the package plus its own `dependencies`, which is right for
23
+ * the GJS path — the showcase's `--app gjs` bundle needs nothing else. The
24
+ * `--app node` bundle does: its `gi://` imports resolve through
25
+ * `@gjsify/node-gi`, which is a build-time concern for the showcase (a
26
+ * devDependency) but a RUNTIME one for whoever launches it off GJS. Nobody
27
+ * can act on the resulting "add @gjsify/node-gi as a dependency" error —
28
+ * the tree belongs to the cache, not to the user — so the launcher that
29
+ * knows the runtime supplies it.
30
+ *
31
+ * Part of the cache key, so the gjs and node trees never share an entry.
32
+ */
33
+ extraSpecs?: readonly string[];
19
34
  }
20
35
  /**
21
36
  * Resolve (installing + caching when remote) the on-disk directory of a package
@@ -106,7 +106,8 @@ export async function ensurePkgDir(parsed, opts) {
106
106
  if (parsed.kind === 'local') {
107
107
  return { pkgDir: parsed.path, cachedPkgName: null };
108
108
  }
109
- const cacheKey = createCacheKey({ packages: [parsed.spec] });
109
+ const specs = [parsed.spec, ...(opts.extraSpecs ?? [])];
110
+ const cacheKey = createCacheKey({ packages: specs });
110
111
  const cacheDir = cacheDirFor(cacheKey);
111
112
  const cached = opts.cacheMaxAge > 0 ? getValidCachedPkg(cacheDir, opts.cacheMaxAge) : undefined;
112
113
  if (cached) {
@@ -124,7 +125,7 @@ export async function ensurePkgDir(parsed, opts) {
124
125
  const progress = makeProgressReporter({ enabled: !opts.verbose });
125
126
  await installPackages({
126
127
  prefix: prepareDir,
127
- specs: [parsed.spec],
128
+ specs,
128
129
  verbose: opts.verbose,
129
130
  registry: opts.registry,
130
131
  // Cache-prepare dirs are scoped per cache key, so writing a lockfile
@@ -14,7 +14,7 @@ interface InstallOptions {
14
14
  timeout: number;
15
15
  }
16
16
  export declare const installCommand: Command<unknown, InstallOptions>;
17
- export declare function buildBinShim(wsLocation: string, nodeTarget?: string, gjsTarget?: string, nativePrebuildDirs?: string[], platform?: string): string;
17
+ export declare function buildBinShim(wsLocation: string, nodeTarget?: string, gjsTarget?: string, nativePrebuildDirs?: string[], platform?: string, scanRoot?: string): string;
18
18
  /**
19
19
  * `.cmd` / `.ps1` companions for {@link buildBinShim}.
20
20
  *
@@ -34,8 +34,8 @@ import { spawnSync } from 'node:child_process';
34
34
  import { gjsExit } from '@gjsify/rolldown-plugin-gjsify/runtime';
35
35
  import { discoverWorkspaces } from '@gjsify/workspace';
36
36
  import { buildInstallCommand, detectPackageManager, runMinimalChecks } from '../utils/check-system-deps.js';
37
- import { detectNativePackages, libraryPathVar } from '../utils/detect-native-packages.js';
38
- import { buildLauncherShims } from '../utils/bin-shim.js';
37
+ import { detectNativePackages } from '../utils/detect-native-packages.js';
38
+ import { buildLauncherShims, buildNativeEnvPreamble } from '../utils/bin-shim.js';
39
39
  import { installPackages, makeProgressReporter } from '../utils/install-backend.js';
40
40
  import { atomicWriteStrict } from '../utils/install-cache-fs.js';
41
41
  import { acquireInstallLock } from '../utils/install-lock.js';
@@ -957,9 +957,7 @@ function writeWorkspaceBinShims(cwd, workspaces) {
957
957
  /* fine */
958
958
  }
959
959
  }
960
- writeFileSync(linkPath, buildBinShim(ws.location, nodeTarget, gjsTarget, nativePrebuildDirs), {
961
- mode: 0o755,
962
- });
960
+ writeFileSync(linkPath, buildBinShim(ws.location, nodeTarget, gjsTarget, nativePrebuildDirs, process.platform, cwd), { mode: 0o755 });
963
961
  chmodSync(linkPath, 0o755);
964
962
  // Windows executes neither an extension-less file nor a `#!` line —
965
963
  // the sh shim above is only reachable from git-bash/MSYS/WSL. Write
@@ -1045,26 +1043,19 @@ function scopedOverrideFor(ws, depName, extracted) {
1045
1043
  }
1046
1044
  return undefined;
1047
1045
  }
1048
- export function buildBinShim(wsLocation, nodeTarget, gjsTarget, nativePrebuildDirs = [], platform = process.platform) {
1046
+ export function buildBinShim(wsLocation, nodeTarget, gjsTarget, nativePrebuildDirs = [], platform = process.platform, scanRoot = wsLocation) {
1049
1047
  const nodeAbs = nodeTarget ? join(wsLocation, nodeTarget) : null;
1050
1048
  const gjsAbs = gjsTarget ? join(wsLocation, gjsTarget) : null;
1051
1049
  // GJS-only env preamble — Node ignores GI_TYPELIB_PATH so we scope the
1052
1050
  // export to the gjs branch, keeping the shim minimal when no native pkgs
1053
1051
  // exist or only the Node bin is in play.
1054
1052
  //
1055
- // The library-path variable is host-dependent: `dyld` on macOS never reads
1056
- // LD_LIBRARY_PATH, so exporting it there silently produced a launcher that
1057
- // could not load a single `darwin-arm64` prebuild. `libraryPathVar()` is the
1058
- // one place that mapping lives.
1059
- const { name: libVar } = libraryPathVar(platform);
1060
- const gjsPreamble = nativePrebuildDirs.length === 0
1061
- ? ''
1062
- : (() => {
1063
- const joined = `'${nativePrebuildDirs.join(':').replace(/'/g, `'\\''`)}'`;
1064
- return (`GI_TYPELIB_PATH=${joined}\${GI_TYPELIB_PATH:+":$GI_TYPELIB_PATH"}\n` +
1065
- `${libVar}=${joined}\${${libVar}:+":$${libVar}"}\n` +
1066
- `export GI_TYPELIB_PATH ${libVar}\n`);
1067
- })();
1053
+ // `scanRoot` is the INSTALL prefix (the tree holding `node_modules/.bin`),
1054
+ // not the workspace package dir: the preamble globs its `node_modules` at
1055
+ // launch time instead of embedding what was installed the day the shim was
1056
+ // written, so `gjsify install <some>-native` afterwards is picked up without
1057
+ // a re-link. See `buildNativeEnvPreamble`.
1058
+ const gjsPreamble = buildNativeEnvPreamble(scanRoot, nativePrebuildDirs, { platform });
1068
1059
  if (nodeAbs && gjsAbs) {
1069
1060
  // GJS-FIRST: prefer the committed Node-free GJS bundle when `gjs` is on
1070
1061
  // PATH AND the bundle exists; fall back to the Node entry otherwise (a
@@ -18,6 +18,27 @@ function readCliVersion() {
18
18
  return undefined;
19
19
  }
20
20
  }
21
+ /**
22
+ * `[gjsify <version>] ` for the banner — the CLI naming ITSELF.
23
+ *
24
+ * `showcase` pins the showcase package to the CLI's own version, so a stale CLI
25
+ * silently runs a stale showcase, and the only version in the output was the
26
+ * showcase's. That is not a hypothetical: every runner that caches an unpinned
27
+ * bin reuses whatever it resolved once (`npx @gjsify/cli` → a 0.4.x `_npx`
28
+ * entry; `deno run npm:@gjsify/cli` → whatever sits in `DENO_DIR`), so the same
29
+ * command produced 0.24.1 under bunx, 0.4.25 under npx and 0.23.0 under deno on
30
+ * ONE machine on ONE day — and the two old ones failed in ways that read as
31
+ * gjsify bugs. gjsify cannot flush somebody else's cache, but it can stop the
32
+ * output from hiding which gjsify is talking.
33
+ */
34
+ function runningAs(cliVersion) {
35
+ return cliVersion ? `[gjsify ${cliVersion}] ` : '';
36
+ }
37
+ /** Pinned invocations for the three runners that cache an unpinned bin. */
38
+ const PIN_HINT = 'A package runner reuses a CACHED copy of an unpinned bin. Force the current one:\n' +
39
+ ' npx @gjsify/cli@latest showcase <name>\n' +
40
+ ' bunx @gjsify/cli@latest showcase <name>\n' +
41
+ ' deno run -A --reload npm:@gjsify/cli@latest showcase <name>';
21
42
  export const showcaseCommand = {
22
43
  command: 'showcase [name]',
23
44
  description: 'List or run curated gjsify showcase applications.',
@@ -146,7 +167,7 @@ export const showcaseCommand = {
146
167
  // newer CLI assumes (the `@gjsify/http-soup-bridge` regression
147
168
  // reported against `@gjsify/cli@0.3.17`).
148
169
  const dlxSpec = cliVersion ? `${showcase.packageName}@${cliVersion}` : showcase.packageName;
149
- console.log(`Running showcase: ${showcase.name} (via gjsify dlx ${dlxSpec})\n`);
170
+ console.log(`Running showcase: ${showcase.name} ${runningAs(cliVersion)}(via gjsify dlx ${dlxSpec})\n`);
150
171
  // Dispatch `dlx` IN-PROCESS (not `spawn(process.execPath, [cliBin, …])`):
151
172
  // under the committed GJS bundle `process.execPath` is `gjs` and
152
173
  // `../index.js` is the Node entry, so the spawn ran `gjs cli.gjs.mjs
@@ -169,7 +190,7 @@ export const showcaseCommand = {
169
190
  * in-tree build), falling back to `gjsify dlx`'s install-into-cache for a
170
191
  * published showcase. Returns the directory containing the package.json.
171
192
  */
172
- async function resolveShowcaseDir(showcase, cliVersion) {
193
+ async function resolveShowcaseDir(showcase, cliVersion, extraSpecs = []) {
173
194
  // Local-first: a workspace showcase resolves via its own package name.
174
195
  try {
175
196
  const req = createRequire(import.meta.url);
@@ -185,6 +206,7 @@ async function resolveShowcaseDir(showcase, cliVersion) {
185
206
  verbose: false,
186
207
  cacheMaxAge: 60 * 24 * 7,
187
208
  frozen: false,
209
+ extraSpecs,
188
210
  });
189
211
  return pkgDir;
190
212
  }
@@ -197,7 +219,16 @@ async function resolveShowcaseDir(showcase, cliVersion) {
197
219
  async function runShowcaseOnRuntime(showcase, runtime, cliVersion) {
198
220
  let pkgDir;
199
221
  try {
200
- pkgDir = await resolveShowcaseDir(showcase, cliVersion);
222
+ // `@gjsify/node-gi` is the `gi://` bridge the `--app node` bundle
223
+ // resolves at RUNTIME. Showcases carry it as a devDependency (it is a
224
+ // build input for them), so a dlx tree — the package plus its own
225
+ // `dependencies` — does not contain it and the launch died with "add
226
+ // @gjsify/node-gi as a dependency": advice the user cannot act on,
227
+ // since the tree is the CLI's cache. The launcher knows the runtime, so
228
+ // it supplies the bridge. Pinned to the CLI's version like the showcase
229
+ // itself, so the bridge and the bundle move together.
230
+ const nodeGiSpec = cliVersion ? `@gjsify/node-gi@${cliVersion}` : '@gjsify/node-gi';
231
+ pkgDir = await resolveShowcaseDir(showcase, cliVersion, [nodeGiSpec]);
201
232
  }
202
233
  catch (err) {
203
234
  console.error(`Could not resolve showcase "${showcase.name}": ${err.message}`);
@@ -222,10 +253,19 @@ async function runShowcaseOnRuntime(showcase, runtime, cliVersion) {
222
253
  entry = resolveNodeEntry(pkgDir);
223
254
  }
224
255
  catch (err) {
225
- console.error(`Cannot run showcase "${showcase.name}" on ${runtime}: ${err.message}`);
256
+ // The showcase promised this runtime and did not ship the bundle. Two
257
+ // causes, and the user can only act on one of them, so name both: the
258
+ // package version is genuinely broken (fixed by a newer one), or THIS
259
+ // CLI is a cached old copy that pinned a genuinely broken old showcase.
260
+ const version = typeof pkg.name === 'string' ? `${pkg.name}@${cliVersion ?? '?'}` : showcase.packageName;
261
+ console.error(`Cannot run showcase "${showcase.name}" on ${runtime}: ${err.message}\n\n` +
262
+ ` ${version} declares --runtime ${runtime} but ships no \`--app node\` bundle, so that\n` +
263
+ ` version of the showcase cannot run there. You are running gjsify ${cliVersion ?? '(unknown)'},\n` +
264
+ ` and \`showcase\` pins the showcase to the CLI's own version.\n\n ${PIN_HINT}\n\n` +
265
+ ` Or run it on GJS, which every showcase ships: gjsify showcase ${showcase.name} --runtime gjs`);
226
266
  return process.exit(1);
227
267
  }
228
- console.log(`Running showcase: ${showcase.name} on ${runtime} (${entry})\n`);
268
+ console.log(`Running showcase: ${showcase.name} ${runningAs(cliVersion)}on ${runtime} (${entry})\n`);
229
269
  // Terminal call — exit on success.
230
270
  await runRuntimeBundle(runtime, entry, [], { exitOnSuccess: true });
231
271
  }
@@ -64,3 +64,52 @@ export declare function buildLauncherShims(opts: {
64
64
  cmd: string;
65
65
  ps1: string;
66
66
  };
67
+ /**
68
+ * The `sh` preamble a GJS launcher needs so `imports.gi.X` resolves against the
69
+ * `@gjsify/*` native prebuilds installed next to it.
70
+ *
71
+ * WHY IT SCANS AT RUN TIME INSTEAD OF BAKING A LIST
72
+ *
73
+ * The typelib/library lookup happens inside the GJS runtime, before a single
74
+ * line of the CLI bundle runs, so the env MUST be set by the launcher — the CLI
75
+ * cannot repair it from the inside. The launcher used to bake the directories
76
+ * `detectNativePackages()` found at INSTALL time, which makes the env a SNAPSHOT
77
+ * of one moment:
78
+ *
79
+ * - a native package installed into the same prefix LATER (`gjsify install -g
80
+ * @gjsify/<x>-native`, a project `gjsify install` that adds one) is invisible
81
+ * until something happens to rewrite the launcher;
82
+ * - and if the scan comes back empty for any reason, the launcher is written
83
+ * with NO preamble at all and nothing says so. The failure then surfaces
84
+ * much later and somewhere else entirely — `gjsify build` dying with "no
85
+ * usable bundler engine under GJS" in an unrelated project, which reads as a
86
+ * broken install rather than a stale launcher. That is what shipped in
87
+ * v0.24.1: a `~/.local/bin/gjsify` with a bare `exec gjs -m …` line while
88
+ * `@gjsify/rolldown-native` sat correctly installed in the prefix.
89
+ *
90
+ * Deriving the env from disk on every launch removes both: the launcher states
91
+ * WHERE to look rather than WHAT was there, so it cannot go stale, and an empty
92
+ * result is a fact about the current prefix instead of a silently frozen one.
93
+ * The cost is a handful of shell globs per launch (they are builtins, and a
94
+ * pattern that matches nothing stays literal and is rejected by `[ -d ]`).
95
+ *
96
+ * `bakedDirs` covers what a run-time scan of `scanRoot` structurally cannot see:
97
+ * `detectNativePackages()` walks UP the directory chain, so in a hoisted layout
98
+ * a prebuild may live in an ancestor's `node_modules`. Those are still embedded
99
+ * verbatim; only the primary root is dynamic. Duplicates are harmless (both
100
+ * variables are search paths).
101
+ *
102
+ * Windows is deliberately excluded: there the `sh` file is only reachable from
103
+ * git-bash/MSYS, the real launchers are the `.cmd`/`.ps1` companions built by
104
+ * {@link buildLauncherShims} from the same baked list, and `PATH` doubles as the
105
+ * DLL search path with `;` separators. It keeps the snapshot behaviour.
106
+ *
107
+ * @param scanRoot Directory whose `node_modules` is globbed at launch time.
108
+ * @param bakedDirs Prebuild dirs found at write time; entries outside `scanRoot`
109
+ * are embedded (entries inside it are dropped — the scan finds them).
110
+ * @returns the preamble, or `''` when there is nothing to export.
111
+ */
112
+ export declare function buildNativeEnvPreamble(scanRoot: string, bakedDirs?: readonly string[], target?: {
113
+ platform?: string;
114
+ arch?: string;
115
+ }): string;
@@ -24,6 +24,7 @@
24
24
  // All three re-derive the interpreter from the target's shebang, so a bin
25
25
  // declaring `#!/usr/bin/env node` and one declaring `#!/usr/bin/env -S gjs -m`
26
26
  // both work without the caller knowing which it is.
27
+ import { libraryPathVar, prebuildDirCandidates } from './detect-native-packages.js';
27
28
  // Matches `#!<prog> <args>` and `#!/usr/bin/env [-S] [K=V ...] <prog> <args>`.
28
29
  // Verbatim from cmd-shim's `shebangExpr`.
29
30
  const SHEBANG_RE = /^#!\s*(?:\/usr\/bin\/env\s+(?:-S\s+)?((?:[^ \t=]+=[^ \t=]+\s+)*))?([^ \t]+)(.*)$/;
@@ -244,3 +245,107 @@ export function buildLauncherShims(opts) {
244
245
  'exit $LASTEXITCODE\n';
245
246
  return { cmd, ps1 };
246
247
  }
248
+ /** POSIX `sh` single-quoting — the one place the escape lives. */
249
+ function shQuote(s) {
250
+ return `'${s.replace(/'/g, `'\\''`)}'`;
251
+ }
252
+ /**
253
+ * The `sh` preamble a GJS launcher needs so `imports.gi.X` resolves against the
254
+ * `@gjsify/*` native prebuilds installed next to it.
255
+ *
256
+ * WHY IT SCANS AT RUN TIME INSTEAD OF BAKING A LIST
257
+ *
258
+ * The typelib/library lookup happens inside the GJS runtime, before a single
259
+ * line of the CLI bundle runs, so the env MUST be set by the launcher — the CLI
260
+ * cannot repair it from the inside. The launcher used to bake the directories
261
+ * `detectNativePackages()` found at INSTALL time, which makes the env a SNAPSHOT
262
+ * of one moment:
263
+ *
264
+ * - a native package installed into the same prefix LATER (`gjsify install -g
265
+ * @gjsify/<x>-native`, a project `gjsify install` that adds one) is invisible
266
+ * until something happens to rewrite the launcher;
267
+ * - and if the scan comes back empty for any reason, the launcher is written
268
+ * with NO preamble at all and nothing says so. The failure then surfaces
269
+ * much later and somewhere else entirely — `gjsify build` dying with "no
270
+ * usable bundler engine under GJS" in an unrelated project, which reads as a
271
+ * broken install rather than a stale launcher. That is what shipped in
272
+ * v0.24.1: a `~/.local/bin/gjsify` with a bare `exec gjs -m …` line while
273
+ * `@gjsify/rolldown-native` sat correctly installed in the prefix.
274
+ *
275
+ * Deriving the env from disk on every launch removes both: the launcher states
276
+ * WHERE to look rather than WHAT was there, so it cannot go stale, and an empty
277
+ * result is a fact about the current prefix instead of a silently frozen one.
278
+ * The cost is a handful of shell globs per launch (they are builtins, and a
279
+ * pattern that matches nothing stays literal and is rejected by `[ -d ]`).
280
+ *
281
+ * `bakedDirs` covers what a run-time scan of `scanRoot` structurally cannot see:
282
+ * `detectNativePackages()` walks UP the directory chain, so in a hoisted layout
283
+ * a prebuild may live in an ancestor's `node_modules`. Those are still embedded
284
+ * verbatim; only the primary root is dynamic. Duplicates are harmless (both
285
+ * variables are search paths).
286
+ *
287
+ * Windows is deliberately excluded: there the `sh` file is only reachable from
288
+ * git-bash/MSYS, the real launchers are the `.cmd`/`.ps1` companions built by
289
+ * {@link buildLauncherShims} from the same baked list, and `PATH` doubles as the
290
+ * DLL search path with `;` separators. It keeps the snapshot behaviour.
291
+ *
292
+ * @param scanRoot Directory whose `node_modules` is globbed at launch time.
293
+ * @param bakedDirs Prebuild dirs found at write time; entries outside `scanRoot`
294
+ * are embedded (entries inside it are dropped — the scan finds them).
295
+ * @returns the preamble, or `''` when there is nothing to export.
296
+ */
297
+ export function buildNativeEnvPreamble(scanRoot, bakedDirs = [], target = {}) {
298
+ const platform = target.platform ?? process.platform;
299
+ const arch = target.arch ?? process.arch;
300
+ const { name: libVar, separator } = libraryPathVar(platform);
301
+ if (platform === 'win32') {
302
+ if (bakedDirs.length === 0)
303
+ return '';
304
+ const joined = shQuote(bakedDirs.join(separator));
305
+ return (`GI_TYPELIB_PATH=${joined}\${GI_TYPELIB_PATH:+"${separator}$GI_TYPELIB_PATH"}\n` +
306
+ `${libVar}=${joined}\${${libVar}:+"${separator}$${libVar}"}\n` +
307
+ `export GI_TYPELIB_PATH ${libVar}\n`);
308
+ }
309
+ // Only the ancestors' hits need embedding — anything under `scanRoot` is
310
+ // what the loop below finds, and re-embedding it would reintroduce the
311
+ // snapshot for exactly the packages the scan exists to keep current.
312
+ const nmRoot = `${scanRoot.replace(/\/+$/, '')}/node_modules/`;
313
+ const external = bakedDirs.filter((d) => !d.startsWith(nmRoot));
314
+ // The same `<os>-<arch>` spellings `resolvePrebuildDirName()` probes, in the
315
+ // same order: the canonical `${platform}-${arch}` plus the legacy uname one
316
+ // a tarball published before the rename still ships. Passing no declared
317
+ // platforms is correct here — the shell cannot read a package.json, and a
318
+ // package declaring ONLY a non-canonical spelling of this host has not
319
+ // existed since the audit made that state impossible.
320
+ const candidates = prebuildDirCandidates(platform, arch);
321
+ const patterns = [];
322
+ for (const candidate of candidates) {
323
+ // Scoped (`@gjsify/x`) and unscoped packages, matching `scanNodeModules`.
324
+ patterns.push(`${shQuote(scanRoot)}/node_modules/@*/*/prebuilds/${candidate}`);
325
+ patterns.push(`${shQuote(scanRoot)}/node_modules/*/prebuilds/${candidate}`);
326
+ }
327
+ const seed = external.length > 0 ? shQuote(external.join(separator)) : `''`;
328
+ // A directory match is not enough: `prebuilds/<os>-<arch>/` is ALSO the
329
+ // prebuildify convention, so a plain glob sweeps in `bare-fs`, `bare-os`
330
+ // and friends — packages with no typelib at all, which `detectNativePackages`
331
+ // never returned because it keyed on `gjsify.prebuilds`. Onto
332
+ // GI_TYPELIB_PATH that is merely noise; onto the LOADER path it is a
333
+ // directory of foreign shared objects ahead of the system ones. The
334
+ // `.typelib` probe restores the old precision from the filesystem alone —
335
+ // and it is exactly what the artifact invariant already guarantees every
336
+ // gjsify GI bridge ships (a library AND the typelib without which
337
+ // GI_TYPELIB_PATH resolves nothing).
338
+ return (`gjsify_np=${seed}\n` +
339
+ `for gjsify_d in ${patterns.join(' ')}; do\n` +
340
+ ` [ -d "$gjsify_d" ] || continue\n` +
341
+ ` for gjsify_t in "$gjsify_d"/*.typelib; do\n` +
342
+ ` if [ -f "$gjsify_t" ]; then gjsify_np="\${gjsify_np:+$gjsify_np${separator}}$gjsify_d"; break; fi\n` +
343
+ ` done\n` +
344
+ `done\n` +
345
+ `if [ -n "$gjsify_np" ]; then\n` +
346
+ ` GI_TYPELIB_PATH="$gjsify_np\${GI_TYPELIB_PATH:+${separator}$GI_TYPELIB_PATH}"\n` +
347
+ ` ${libVar}="$gjsify_np\${${libVar}:+${separator}$${libVar}}"\n` +
348
+ ` export GI_TYPELIB_PATH ${libVar}\n` +
349
+ `fi\n` +
350
+ `unset gjsify_np gjsify_d gjsify_t\n`);
351
+ }
@@ -571,7 +571,6 @@ function findVisible(requesterPath, name, byPath) {
571
571
  candidates.push(`${requesterPath}/node_modules/${name}`);
572
572
  // Walk up: strip the last `/node_modules/<pkg>` segment and try again.
573
573
  let p = requesterPath;
574
- // eslint-disable-next-line no-constant-condition
575
574
  while (true) {
576
575
  // Find the deepest `/node_modules/<pkg>` in `p`, strip it.
577
576
  const idx = p.lastIndexOf('/node_modules/');
@@ -86,22 +86,6 @@ export interface LinkedBin {
86
86
  * simplicity and so we don't need to discriminate by runtime here.
87
87
  */
88
88
  export declare function linkGlobalBins(packageNames: string[], layout: GlobalLayout): LinkedBin[];
89
- /**
90
- * Build the POSIX-sh `export` lines that prepend the given prebuild
91
- * directories to GI_TYPELIB_PATH and the host's shared-library search path.
92
- * Any pre-existing value inherited from the user's environment is preserved as
93
- * a suffix so user-installed typelibs/libraries still resolve.
94
- *
95
- * The library variable is host-dependent — `LD_LIBRARY_PATH` on Linux,
96
- * `DYLD_LIBRARY_PATH` on macOS (dyld never reads the ELF one), so a launcher
97
- * written on a Mac used to export a variable nothing consults, leaving every
98
- * `darwin-arm64` prebuild unloadable. `platform` is a parameter so both
99
- * branches are unit-testable from either host.
100
- *
101
- * Returns the empty string when no prebuilds were found — avoids emitting an
102
- * inert assignment in the launcher.
103
- */
104
- export declare function buildLauncherEnvPreamble(prebuildsDirs: string[], platform?: string): string;
105
89
  /** Returns `true` if `binDir` is on the user's PATH. */
106
90
  export declare function binDirOnPath(binDir: string): boolean;
107
91
  /**
@@ -23,8 +23,8 @@
23
23
  import * as fs from 'node:fs';
24
24
  import * as os from 'node:os';
25
25
  import * as path from 'node:path';
26
- import { detectNativePackages, libraryPathVar } from './detect-native-packages.js';
27
- import { buildLauncherShims, parseShebang } from './bin-shim.js';
26
+ import { detectNativePackages } from './detect-native-packages.js';
27
+ import { buildLauncherShims, buildNativeEnvPreamble, parseShebang } 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
@@ -143,18 +143,25 @@ export async function installGjsEnginePackages(prefix, version, opts = {}) {
143
143
  export function linkGlobalBins(packageNames, layout) {
144
144
  fs.mkdirSync(layout.binDir, { recursive: true });
145
145
  const created = [];
146
- // Discover @gjsify/* packages with native prebuilds (Vala/GObject typelibs
147
- // + shared libs) under the global prefix. The launcher bakes their
148
- // directories into GI_TYPELIB_PATH / LD_LIBRARY_PATH so `imports.gi.X`
149
- // resolves at CLI startup — required for e.g. @gjsify/terminal-native,
150
- // without which process.stdout.isTTY / columns / colors all fall back to
151
- // the conservative env-only defaults (no colors, 80-col wrap).
146
+ // The launcher exports GI_TYPELIB_PATH / <loader path> for the @gjsify/*
147
+ // native prebuilds (Vala/GObject typelibs + shared libs) installed in this
148
+ // prefix, so `imports.gi.X` resolves at CLI startup — required for the
149
+ // bundler engine `@gjsify/rolldown-native` (without which `gjsify build`
150
+ // hard-fails under GJS) and for e.g. `@gjsify/terminal-native`, without
151
+ // which isTTY / columns / colors fall back to conservative defaults.
152
152
  //
153
- // `runGjsBundle()` does the same dance for `gjsify run <bundle>` at
154
- // runtime; here we do it at install time because the global launcher
155
- // invokes the CLI bundle directly, with no opportunity to set env first.
153
+ // `runGjsBundle()` does the same dance for `gjsify run <bundle>` at runtime;
154
+ // here it has to be the launcher, because the typelib lookup happens inside
155
+ // the GJS runtime before the bundle's first line runs.
156
+ //
157
+ // The preamble SCANS the prefix at launch time rather than embedding what
158
+ // was there at install time — see `buildNativeEnvPreamble` for why a
159
+ // snapshot is the wrong shape. `nativePrebuildDirs` is still computed: the
160
+ // Windows `.cmd`/`.ps1` shims take the baked list, and a hit in an
161
+ // ANCESTOR's node_modules (which the walk finds and a single-root scan
162
+ // cannot) is embedded verbatim.
156
163
  const nativePrebuildDirs = detectNativePackages(layout.prefix).map((p) => p.prebuildsDir);
157
- const envPreamble = buildLauncherEnvPreamble(nativePrebuildDirs);
164
+ const envPreamble = buildNativeEnvPreamble(layout.prefix, nativePrebuildDirs);
158
165
  for (const pkgName of packageNames) {
159
166
  const pkgDir = path.join(layout.prefix, 'node_modules', pkgName);
160
167
  const pkgJsonPath = path.join(pkgDir, 'package.json');
@@ -237,30 +244,6 @@ export function linkGlobalBins(packageNames, layout) {
237
244
  function shQuote(s) {
238
245
  return `'${s.replace(/'/g, `'\\''`)}'`;
239
246
  }
240
- /**
241
- * Build the POSIX-sh `export` lines that prepend the given prebuild
242
- * directories to GI_TYPELIB_PATH and the host's shared-library search path.
243
- * Any pre-existing value inherited from the user's environment is preserved as
244
- * a suffix so user-installed typelibs/libraries still resolve.
245
- *
246
- * The library variable is host-dependent — `LD_LIBRARY_PATH` on Linux,
247
- * `DYLD_LIBRARY_PATH` on macOS (dyld never reads the ELF one), so a launcher
248
- * written on a Mac used to export a variable nothing consults, leaving every
249
- * `darwin-arm64` prebuild unloadable. `platform` is a parameter so both
250
- * branches are unit-testable from either host.
251
- *
252
- * Returns the empty string when no prebuilds were found — avoids emitting an
253
- * inert assignment in the launcher.
254
- */
255
- export function buildLauncherEnvPreamble(prebuildsDirs, platform = process.platform) {
256
- if (prebuildsDirs.length === 0)
257
- return '';
258
- const { name: libVar } = libraryPathVar(platform);
259
- const joined = shQuote(prebuildsDirs.join(':'));
260
- return (`GI_TYPELIB_PATH=${joined}\${GI_TYPELIB_PATH:+":$GI_TYPELIB_PATH"}\n` +
261
- `${libVar}=${joined}\${${libVar}:+":$${libVar}"}\n` +
262
- `export GI_TYPELIB_PATH ${libVar}\n`);
263
- }
264
247
  /** Parse a bin file's `#!` line; `null` when unreadable or not a shebang. */
265
248
  function readShebangOf(file) {
266
249
  try {
@@ -24,7 +24,6 @@ export function normalizeBundlerOptions(configData) {
24
24
  raw['define'] !== null) {
25
25
  if (!warnedDefineOnce) {
26
26
  warnedDefineOnce = true;
27
- // eslint-disable-next-line no-console
28
27
  console.warn("[gjsify] WARNING: 'bundler.define' is not a valid Rolldown option and would be " +
29
28
  "silently ignored — it has been auto-mapped to 'bundler.transform.define'. " +
30
29
  "Move 'define' under 'bundler.transform.define' in your config to suppress " +
@@ -46,7 +45,6 @@ export function normalizeBundlerOptions(configData) {
46
45
  return fromBundler;
47
46
  if (!warnedOnce) {
48
47
  warnedOnce = true;
49
- // eslint-disable-next-line no-console
50
48
  console.warn("[gjsify] DEPRECATION: the 'esbuild' config key is deprecated and will be removed in 0.5.0. " +
51
49
  "Rename it to 'bundler' (typed as RolldownOptions). See the migration notes in the gjsify CHANGELOG.");
52
50
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.24.1",
3
+ "version": "0.24.2",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -130,19 +130,19 @@
130
130
  "cli"
131
131
  ],
132
132
  "dependencies": {
133
- "@gjsify/buffer": "^0.24.1",
134
- "@gjsify/create-app": "^0.24.1",
135
- "@gjsify/node-globals": "^0.24.1",
136
- "@gjsify/node-polyfills": "^0.24.1",
137
- "@gjsify/npm-registry": "^0.24.1",
138
- "@gjsify/resolve-npm": "^0.24.1",
139
- "@gjsify/rolldown-plugin-gjsify": "^0.24.1",
140
- "@gjsify/rolldown-plugin-pnp": "^0.24.1",
141
- "@gjsify/semver": "^0.24.1",
142
- "@gjsify/tar": "^0.24.1",
143
- "@gjsify/tsc": "^0.24.1",
144
- "@gjsify/web-polyfills": "^0.24.1",
145
- "@gjsify/workspace": "^0.24.1",
133
+ "@gjsify/buffer": "^0.24.2",
134
+ "@gjsify/create-app": "^0.24.2",
135
+ "@gjsify/node-globals": "^0.24.2",
136
+ "@gjsify/node-polyfills": "^0.24.2",
137
+ "@gjsify/npm-registry": "^0.24.2",
138
+ "@gjsify/resolve-npm": "^0.24.2",
139
+ "@gjsify/rolldown-plugin-gjsify": "^0.24.2",
140
+ "@gjsify/rolldown-plugin-pnp": "^0.24.2",
141
+ "@gjsify/semver": "^0.24.2",
142
+ "@gjsify/tar": "^0.24.2",
143
+ "@gjsify/tsc": "^0.24.2",
144
+ "@gjsify/web-polyfills": "^0.24.2",
145
+ "@gjsify/workspace": "^0.24.2",
146
146
  "cosmiconfig": "^9.0.2",
147
147
  "get-tsconfig": "^4.14.0",
148
148
  "pkg-types": "^2.3.1",
@@ -150,13 +150,13 @@
150
150
  "yargs": "^18.0.0"
151
151
  },
152
152
  "devDependencies": {
153
- "@gjsify/unit": "^0.24.1",
153
+ "@gjsify/unit": "^0.24.2",
154
154
  "@types/yargs": "^17.0.35",
155
155
  "typescript": "^6.0.3"
156
156
  },
157
157
  "peerDependencies": {
158
- "@gjsify/oxfmt-native": "^0.24.1",
159
- "@gjsify/rolldown-native": "^0.24.1"
158
+ "@gjsify/oxfmt-native": "^0.24.2",
159
+ "@gjsify/rolldown-native": "^0.24.2"
160
160
  },
161
161
  "peerDependenciesMeta": {
162
162
  "@gjsify/oxfmt-native": {