@gjsify/cli 0.58.0 → 0.59.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,6 +21,16 @@ interface InstallOptions {
21
21
  force?: boolean;
22
22
  }
23
23
  export declare const installCommand: Command<unknown, InstallOptions>;
24
+ /**
25
+ * Version of the `@gjsify/cli` the workspace at `cwd` pins: the one DECLARED in
26
+ * its package.json (`^`/`~`/`=` prefix stripped; only a plain version counts).
27
+ * The installed copy is consulted only when package.json declares no
28
+ * `@gjsify/cli` at all — `install` replaces a stale `node_modules`, so it must
29
+ * not override a declared pin (that flagged the very state being repaired).
30
+ * A declared non-version spec (`workspace:^`, a range, a tag) falls back to the in-repo
31
+ * source so the check still fires on a fresh checkout of the gjsify monorepo.
32
+ */
33
+ export declare function readWorkspaceCliVersion(cwd: string): string | null;
24
34
  /**
25
35
  * Which dependency NAMES are optional across the given manifests.
26
36
  *
@@ -350,13 +350,31 @@ function readRunningCliVersion() {
350
350
  return null;
351
351
  }
352
352
  /**
353
- * Version of the `@gjsify/cli` the workspace at `cwd` pins. Prefers the resolved
354
- * install, falling back to the in-repo source so the check also fires on a fresh,
355
- * not-yet-installed checkout of the gjsify monorepo itself.
353
+ * Version of the `@gjsify/cli` the workspace at `cwd` pins: the one DECLARED in
354
+ * its package.json (`^`/`~`/`=` prefix stripped; only a plain version counts).
355
+ * The installed copy is consulted only when package.json declares no
356
+ * `@gjsify/cli` at all — `install` replaces a stale `node_modules`, so it must
357
+ * not override a declared pin (that flagged the very state being repaired).
358
+ * A declared non-version spec (`workspace:^`, a range, a tag) falls back to the in-repo
359
+ * source so the check still fires on a fresh checkout of the gjsify monorepo.
356
360
  */
357
- function readWorkspaceCliVersion(cwd) {
358
- return (readCliVersionFrom(join(cwd, 'node_modules', CLI_PACKAGE_NAME, 'package.json')) ??
359
- readCliVersionFrom(join(cwd, 'packages', 'infra', 'cli', 'package.json')));
361
+ export function readWorkspaceCliVersion(cwd) {
362
+ const pkg = readPackageJson(join(cwd, 'package.json'));
363
+ let declared = false;
364
+ for (const kind of ['dependencies', 'devDependencies', 'optionalDependencies']) {
365
+ const spec = pkg?.[kind]?.[CLI_PACKAGE_NAME];
366
+ if (typeof spec === 'string')
367
+ declared = true;
368
+ const m = typeof spec === 'string' ? /^[\^~=v]*(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/.exec(spec.trim()) : null;
369
+ if (m)
370
+ return m[1];
371
+ }
372
+ if (!declared) {
373
+ const installed = readCliVersionFrom(join(cwd, 'node_modules', CLI_PACKAGE_NAME, 'package.json'));
374
+ if (installed)
375
+ return installed;
376
+ }
377
+ return readCliVersionFrom(join(cwd, 'packages', 'infra', 'cli', 'package.json'));
360
378
  }
361
379
  /**
362
380
  * Warn (never block) when the running gjsify CLI differs from the `@gjsify/cli`
@@ -41,6 +41,24 @@ export declare function needsSelfShim(opts: {
41
41
  * reuse it instead of creating their own.
42
42
  */
43
43
  export declare function ensureGjsifyShimOnPath(): void;
44
+ /**
45
+ * Write `files` (relative path → content) into a directory NAMED BY THEIR HASH and
46
+ * return it. Same content, same directory: a run finds what the last one wrote and
47
+ * writes nothing, so shim dirs no longer pile up one per invocation (5930 in a
48
+ * `/tmp`, none ever removed).
49
+ *
50
+ * A reused dir, rather than one removed on exit, is what lets a child outlive its
51
+ * parent: a detached grandchild keeps resolving `gjsify` through the inherited PATH
52
+ * after the CLI that made the shim is gone, and a dir that is deleted at exit
53
+ * breaks it. Content-addressed means a file in it never changes, so a running
54
+ * process can never see a different shim than the one it was started with; each
55
+ * member lands by tmp+`rename`, so a concurrent run sees it whole or not at all.
56
+ *
57
+ * Lives under `$XDG_CACHE_HOME/gjsify/shim` — a `/tmp` path is world-writable, and
58
+ * a PATH entry there lets another local user plant a `gjsify`. A cache dir that
59
+ * cannot be written (read-only HOME) falls back to a private `mkdtemp` dir.
60
+ */
61
+ export declare function materializeShimDir(files: Record<string, string>, root?: string): string;
44
62
  /**
45
63
  * The tree whose `node_modules` holds the running CLI and its prebuilds: the
46
64
  * install prefix above the first `node_modules` segment, or the workspace root
@@ -39,13 +39,15 @@
39
39
  // came through the workspace's own `node_modules/.bin/gjsify`, that shim IS the CLI
40
40
  // the tree wants. Widening this to every Node invocation would silently move in-repo
41
41
  // nested builds off the GJS bundle.
42
- import { mkdtempSync, mkdirSync, writeFileSync, chmodSync, existsSync } from 'node:fs';
42
+ import { createHash } from 'node:crypto';
43
+ import { mkdtempSync, mkdirSync, writeFileSync, chmodSync, existsSync, readFileSync, renameSync, rmSync, } from 'node:fs';
43
44
  import { tmpdir } from 'node:os';
44
45
  import { join, delimiter, relative, isAbsolute, dirname } from 'node:path';
45
46
  import { fileURLToPath } from 'node:url';
46
47
  import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
47
48
  import { buildLauncherShims, buildNativeEnvPreamble, buildShLauncher, shQuoteArg } from './bin-shim.js';
48
49
  import { detectNativePackages } from './detect-native-packages.js';
50
+ import { gjsifyCacheRoot } from './install-cache-fs.js';
49
51
  import { resolveBinOnPath } from './install-global.js';
50
52
  import { findWorkspaceRoot } from './workspace-root.js';
51
53
  let _ensured = false;
@@ -128,8 +130,6 @@ export function ensureGjsifyShimOnPath() {
128
130
  const workspaceRoot = findWorkspaceRoot(process.cwd()) ?? process.cwd();
129
131
  if (!needsSelfShim({ gjs, selfEntry, workspaceRoot }))
130
132
  return;
131
- const dir = mkdtempSync(join(tmpdir(), 'gjsify-shim-'));
132
- const shim = join(dir, 'gjsify');
133
133
  // The interpreter is NAMED, not pathed, matching `buildShLauncher` and
134
134
  // `buildLauncherShims`, so a missing one fails the same way on every OS. Not
135
135
  // `process.execPath`: the batch member cannot quote an interpreter argument
@@ -143,23 +143,78 @@ export function ensureGjsifyShimOnPath() {
143
143
  // beifahrer consumer on macOS: `gjsify workspace <pkg> build` died in
144
144
  // `get columns` with "Unsupported type void".
145
145
  const envPreamble = gjs ? selfShimPreamble(selfEntry) : '';
146
- writeFileSync(shim, gjs
147
- ? buildShLauncher(selfEntry, { isGjsBundle: true, envPreamble, gjs: interpreter })
148
- : `#!/bin/sh\nexec "${interpreter}" "${selfEntry}" "$@"\n`, { mode: 0o755 });
149
- chmodSync(shim, 0o755);
146
+ const files = {
147
+ gjsify: gjs
148
+ ? buildShLauncher(selfEntry, { isGjsBundle: true, envPreamble, gjs: interpreter })
149
+ : `#!/bin/sh\nexec "${interpreter}" "${selfEntry}" "$@"\n`,
150
+ };
150
151
  // cmd.exe and pwsh cannot run the extension-less member: not on PATHEXT, and
151
152
  // Windows has no shebang handling. `buildLauncherShims` ports npm's
152
153
  // three-sibling answer, including the IF/ELSE ERRORLEVEL handling that
153
154
  // `&&`/`||` chaining gets wrong.
154
155
  if (!gjs && process.platform === 'win32') {
155
156
  const { cmd, ps1 } = buildLauncherShims({ interpreter, interpreterArgs: [], target: selfEntry });
156
- writeFileSync(`${shim}.cmd`, cmd);
157
- writeFileSync(`${shim}.ps1`, ps1);
157
+ files['gjsify.cmd'] = cmd;
158
+ files['gjsify.ps1'] = ps1;
158
159
  }
160
+ const nodeShim = buildNodeShim(gjs, selfEntry, envPreamble);
161
+ if (nodeShim)
162
+ files[`${NODE_SHIM_SUBDIR}/node`] = nodeShim;
163
+ const dir = materializeShimDir(files);
159
164
  _selfShimActive = true;
160
165
  process.env.GJSIFY_SHIM_DIR = dir;
161
166
  process.env.PATH = dir + delimiter + (process.env.PATH ?? '');
162
- writeNodeShim(dir, gjs, selfEntry, envPreamble);
167
+ }
168
+ /**
169
+ * Write `files` (relative path → content) into a directory NAMED BY THEIR HASH and
170
+ * return it. Same content, same directory: a run finds what the last one wrote and
171
+ * writes nothing, so shim dirs no longer pile up one per invocation (5930 in a
172
+ * `/tmp`, none ever removed).
173
+ *
174
+ * A reused dir, rather than one removed on exit, is what lets a child outlive its
175
+ * parent: a detached grandchild keeps resolving `gjsify` through the inherited PATH
176
+ * after the CLI that made the shim is gone, and a dir that is deleted at exit
177
+ * breaks it. Content-addressed means a file in it never changes, so a running
178
+ * process can never see a different shim than the one it was started with; each
179
+ * member lands by tmp+`rename`, so a concurrent run sees it whole or not at all.
180
+ *
181
+ * Lives under `$XDG_CACHE_HOME/gjsify/shim` — a `/tmp` path is world-writable, and
182
+ * a PATH entry there lets another local user plant a `gjsify`. A cache dir that
183
+ * cannot be written (read-only HOME) falls back to a private `mkdtemp` dir.
184
+ */
185
+ export function materializeShimDir(files, root = gjsifyCacheRoot('shim', 'v1')) {
186
+ const names = Object.keys(files).sort();
187
+ const hash = createHash('sha256');
188
+ for (const name of names)
189
+ hash.update(`${name}\0${files[name]}\0`);
190
+ const key = hash.digest('hex').slice(0, 16);
191
+ try {
192
+ return writeShimDir(join(root, key), files, names);
193
+ }
194
+ catch {
195
+ // Unwritable cache (read-only HOME, full disk): the shim is still needed
196
+ // for this run, so take the per-run dir this function exists to avoid.
197
+ return writeShimDir(mkdtempSync(join(tmpdir(), 'gjsify-shim-')), files, names);
198
+ }
199
+ }
200
+ function writeShimDir(dir, files, names) {
201
+ for (const name of names) {
202
+ const path = join(dir, name);
203
+ if (existsSync(path) && readFileSync(path, 'utf8') === files[name])
204
+ continue;
205
+ mkdirSync(dirname(path), { recursive: true });
206
+ const tmp = `${path}.tmp.${process.pid}`;
207
+ try {
208
+ writeFileSync(tmp, files[name], { mode: 0o755 });
209
+ chmodSync(tmp, 0o755);
210
+ renameSync(tmp, path);
211
+ }
212
+ catch (err) {
213
+ rmSync(tmp, { force: true });
214
+ throw err;
215
+ }
216
+ }
217
+ return dir;
163
218
  }
164
219
  /**
165
220
  * The tree whose `node_modules` holds the running CLI and its prebuilds: the
@@ -268,22 +323,17 @@ export function nodeShimDir() {
268
323
  * resolves NOWHERE, and it is reachable only from a package script's PATH (see
269
324
  * {@link nodeShimDir}).
270
325
  */
271
- function writeNodeShim(dir, gjs, selfEntry, envPreamble) {
326
+ function buildNodeShim(gjs, selfEntry, envPreamble) {
272
327
  if (!gjs)
273
- return;
274
- const sub = join(dir, NODE_SHIM_SUBDIR);
275
- if (existsSync(join(sub, 'node')))
276
- return; // inherited from a parent gjsify
328
+ return null;
277
329
  if (resolveBinOnPath('node'))
278
- return; // a real Node is present — leave it alone
330
+ return null; // a real Node is present — leave it alone
279
331
  const interpreter = process.env.GJS_CONSOLE || 'gjs';
280
- mkdirSync(sub, { recursive: true });
281
- const shim = join(sub, 'node');
282
332
  // A leading FLAG is refused rather than forwarded: `node --test x.mjs` wants
283
333
  // Node's own test runner and `node -e '…'` an eval, neither of which
284
334
  // `--node-script` can honour, and yargs would take `--test` FOR the script
285
335
  // path (`unknown-options-as-args` turns an unknown flag into a positional).
286
- writeFileSync(shim, '#!/bin/sh\n' +
336
+ return ('#!/bin/sh\n' +
287
337
  // Same SIP hop as the self-shim — see `buildSelfShimScript`.
288
338
  dyldCarryPreamble(process.platform, process.env) +
289
339
  'case "$1" in\n' +
@@ -293,6 +343,5 @@ function writeNodeShim(dir, gjs, selfEntry, envPreamble) {
293
343
  'esac\n' +
294
344
  // The same hop as the `gjsify` shim, so the same preamble.
295
345
  envPreamble +
296
- `exec "${interpreter}" -m "${selfEntry}" run --node-script "$@"\n`, { mode: 0o755 });
297
- chmodSync(shim, 0o755);
346
+ `exec "${interpreter}" -m "${selfEntry}" run --node-script "$@"\n`);
298
347
  }
@@ -59,7 +59,10 @@ export interface ExtraneousPackage {
59
59
  * - a dot-entry like `.bin` or `.cache`, already dropped by the scan;
60
60
  * - a package the lockfile describes but the PLATFORM filter skipped on this host.
61
61
  * It is described, so it is not a stranger; removing the foreign-platform residue
62
- * is `prune-prefix.ts`'s separate job under a separate rule.
62
+ * is `prune-prefix.ts`'s separate job under a separate rule;
63
+ * - anything inside the `node_modules` of a package that declares
64
+ * `bundleDependencies`: its tarball put it there, so a second `--immutable` run
65
+ * over the first run's own output must not refuse it.
63
66
  *
64
67
  * Descendants of an extraneous directory are collapsed away: reporting the parent
65
68
  * says everything, and the nested `@girs` tree of the incident would otherwise list
@@ -52,7 +52,10 @@ function normalizePath(p) {
52
52
  * - a dot-entry like `.bin` or `.cache`, already dropped by the scan;
53
53
  * - a package the lockfile describes but the PLATFORM filter skipped on this host.
54
54
  * It is described, so it is not a stranger; removing the foreign-platform residue
55
- * is `prune-prefix.ts`'s separate job under a separate rule.
55
+ * is `prune-prefix.ts`'s separate job under a separate rule;
56
+ * - anything inside the `node_modules` of a package that declares
57
+ * `bundleDependencies`: its tarball put it there, so a second `--immutable` run
58
+ * over the first run's own output must not refuse it.
56
59
  *
57
60
  * Descendants of an extraneous directory are collapsed away: reporting the parent
58
61
  * says everything, and the nested `@girs` tree of the incident would otherwise list
@@ -60,6 +63,12 @@ function normalizePath(p) {
60
63
  */
61
64
  export function findExtraneous(opts) {
62
65
  const described = new Set(opts.expected.map((n) => normalizePath(n.installPath)));
66
+ // A package with `bundleDependencies` ships its own `node_modules` in the tarball
67
+ // (nativescript → universal-analytics + its debug/ms/uuid). Extracting it creates
68
+ // those directories, so the install's own output must not be refused on the next run.
69
+ const bundlerRoots = opts.installed
70
+ .filter((p) => p.bundles && !p.linked)
71
+ .map((p) => `${normalizePath(relative(opts.prefix, p.dir))}/node_modules/`);
63
72
  const found = [];
64
73
  for (const pkg of opts.installed) {
65
74
  if (pkg.linked)
@@ -70,6 +79,8 @@ export function findExtraneous(opts) {
70
79
  continue;
71
80
  if (described.has(installPath))
72
81
  continue;
82
+ if (bundlerRoots.some((root) => installPath.startsWith(root)))
83
+ continue;
73
84
  found.push({ installPath, name: pkg.name, version: pkg.version });
74
85
  }
75
86
  found.sort((a, b) => (a.installPath < b.installPath ? -1 : a.installPath > b.installPath ? 1 : 0));
@@ -15,6 +15,11 @@ export interface InstalledPackage {
15
15
  linked: boolean;
16
16
  /** The `os`/`cpu`/`libc` slice, absent when the manifest declares none. */
17
17
  platform?: PlatformDeclaration;
18
+ /**
19
+ * The manifest names `bundleDependencies`: its tarball ships a `node_modules` of
20
+ * its own, so whatever sits there came from extracting THIS package.
21
+ */
22
+ bundles?: boolean;
18
23
  }
19
24
  export interface PruneEntry {
20
25
  pkg: InstalledPackage;
@@ -47,6 +47,7 @@ function readManifest(dir) {
47
47
  name: typeof raw.name === 'string' ? raw.name : undefined,
48
48
  version: typeof raw.version === 'string' ? raw.version : undefined,
49
49
  platform: declaresPlatform(platform) ? platform : undefined,
50
+ bundles: [raw.bundleDependencies, raw.bundledDependencies].some((b) => b === true || (Array.isArray(b) && b.length > 0)),
50
51
  };
51
52
  }
52
53
  catch {
@@ -102,6 +103,7 @@ function addPackage(dir, out) {
102
103
  dir,
103
104
  linked,
104
105
  platform: manifest?.platform,
106
+ bundles: manifest?.bundles,
105
107
  });
106
108
  // A linked package is the user's own source tree — its `node_modules` is not
107
109
  // this prefix's to prune.
@@ -106,8 +106,9 @@ export interface ResolvedNodeGiPackage {
106
106
  * `Layout.dirs().native`, the directory a `.app` keeps loadable code in.
107
107
  *
108
108
  * So the rule is what `require('@gjsify/node-gi/<subpath>')` can REACH:
109
- * `package.json` (the `exports` map is what resolves every subpath), every `.js`
110
- * at the package root (every `exports` target is one), and `overrides/`, which
109
+ * `package.json` (the `exports` map is what resolves every subpath, and itself an
110
+ * `exports` target for bundler hook discovery), every `.js` at the package root
111
+ * (every other `exports` target is one), and `overrides/`, which
111
112
  * `gi.js` loads at runtime. `tests/e2e/ship-macos` holds that rule against the
112
113
  * REAL package's `exports` map, so a node-gi release that adds an entry outside
113
114
  * this set fails there rather than in a `.app` a stranger downloaded.
@@ -217,8 +217,9 @@ function resolveStagedPackage(specifier, options) {
217
217
  * `Layout.dirs().native`, the directory a `.app` keeps loadable code in.
218
218
  *
219
219
  * So the rule is what `require('@gjsify/node-gi/<subpath>')` can REACH:
220
- * `package.json` (the `exports` map is what resolves every subpath), every `.js`
221
- * at the package root (every `exports` target is one), and `overrides/`, which
220
+ * `package.json` (the `exports` map is what resolves every subpath, and itself an
221
+ * `exports` target for bundler hook discovery), every `.js` at the package root
222
+ * (every other `exports` target is one), and `overrides/`, which
222
223
  * `gi.js` loads at runtime. `tests/e2e/ship-macos` holds that rule against the
223
224
  * REAL package's `exports` map, so a node-gi release that adds an entry outside
224
225
  * this set fails there rather than in a `.app` a stranger downloaded.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.58.0",
3
+ "version": "0.59.1",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -142,22 +142,22 @@
142
142
  "cli"
143
143
  ],
144
144
  "dependencies": {
145
- "@gjsify/blueprint": "^0.58.0",
146
- "@gjsify/buffer": "^0.58.0",
147
- "@gjsify/create-app": "^0.58.0",
148
- "@gjsify/node-globals": "^0.58.0",
149
- "@gjsify/node-polyfills": "^0.58.0",
150
- "@gjsify/npm-registry": "^0.58.0",
151
- "@gjsify/resolve-npm": "^0.58.0",
152
- "@gjsify/rolldown-plugin-gjsify": "^0.58.0",
153
- "@gjsify/vite-plugin-blueprint": "^0.58.0",
154
- "@gjsify/rolldown-plugin-pnp": "^0.58.0",
155
- "@gjsify/semver": "^0.58.0",
156
- "@gjsify/tar": "^0.58.0",
157
- "@gjsify/tsc": "^0.58.0",
158
- "@gjsify/utils": "^0.58.0",
159
- "@gjsify/web-polyfills": "^0.58.0",
160
- "@gjsify/workspace": "^0.58.0",
145
+ "@gjsify/blueprint": "^0.59.1",
146
+ "@gjsify/buffer": "^0.59.1",
147
+ "@gjsify/create-app": "^0.59.1",
148
+ "@gjsify/node-globals": "^0.59.1",
149
+ "@gjsify/node-polyfills": "^0.59.1",
150
+ "@gjsify/npm-registry": "^0.59.1",
151
+ "@gjsify/resolve-npm": "^0.59.1",
152
+ "@gjsify/rolldown-plugin-gjsify": "^0.59.1",
153
+ "@gjsify/vite-plugin-blueprint": "^0.59.1",
154
+ "@gjsify/rolldown-plugin-pnp": "^0.59.1",
155
+ "@gjsify/semver": "^0.59.1",
156
+ "@gjsify/tar": "^0.59.1",
157
+ "@gjsify/tsc": "^0.59.1",
158
+ "@gjsify/utils": "^0.59.1",
159
+ "@gjsify/web-polyfills": "^0.59.1",
160
+ "@gjsify/workspace": "^0.59.1",
161
161
  "acorn": "^8.17.0",
162
162
  "cosmiconfig": "^9.0.2",
163
163
  "get-tsconfig": "^4.14.0",
@@ -166,13 +166,13 @@
166
166
  "yargs": "^18.0.0"
167
167
  },
168
168
  "devDependencies": {
169
- "@gjsify/unit": "^0.58.0",
169
+ "@gjsify/unit": "^0.59.1",
170
170
  "@types/yargs": "^17.0.35",
171
171
  "typescript": "^6.0.3"
172
172
  },
173
173
  "peerDependencies": {
174
- "@gjsify/oxfmt-native": "^0.58.0",
175
- "@gjsify/rolldown-native": "^0.58.0"
174
+ "@gjsify/oxfmt-native": "^0.59.1",
175
+ "@gjsify/rolldown-native": "^0.59.1"
176
176
  },
177
177
  "peerDependenciesMeta": {
178
178
  "@gjsify/oxfmt-native": {