@gjsify/cli 0.13.1 → 0.13.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.
@@ -2,7 +2,7 @@ import { runBundle, runWatch, bundleToChunks } from '../bundler-pick.js';
2
2
  import { gjsifyPlugin, textLoaderPlugin, resolveShebangLine, NODE_SHEBANG } from '@gjsify/rolldown-plugin-gjsify';
3
3
  import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
4
4
  import { resolveUserPlugins } from '../utils/resolve-plugin-by-name.js';
5
- import { resolveGlobalsList, writeRegisterInjectFile, detectAutoGlobals } from '@gjsify/rolldown-plugin-gjsify/globals';
5
+ import { resolveGlobalsList, writeRegisterInjectFile, detectAutoGlobals, detectNodeGiGlobals, } from '@gjsify/rolldown-plugin-gjsify/globals';
6
6
  import { pnpPlugin } from '@gjsify/rolldown-plugin-pnp';
7
7
  import { dirname, extname, join } from 'node:path';
8
8
  import { pathToFileURL } from 'node:url';
@@ -402,6 +402,36 @@ export class BuildAction {
402
402
  { extraGlobalsList: extras, excludeGlobals, cwd: process.cwd() }, bundleToChunks);
403
403
  pluginOpts.autoGlobalsInject = injectPath;
404
404
  }
405
+ else if (app === 'node' && autoMode) {
406
+ // `--app node` reverse direction: detect GJS ambient globals
407
+ // (`print`/`imports`/…) in the bundled, tree-shaken output and
408
+ // inject `@gjsify/node-gi/globals` only when present. A single
409
+ // in-memory analysis pass (the shim is external — no convergence
410
+ // loop). Gated on `autoMode` so `--globals none` opts out. The
411
+ // detection must run BEFORE the final build so the entry can be
412
+ // wrapped; nothing is injected when no globals are referenced (the
413
+ // eager-native-load regression guard).
414
+ const gjsifyPluginFactory = async (opts) => {
415
+ const cfg = await gjsifyPlugin({
416
+ input: userBundler.input,
417
+ output: { file: outfile, dir: outdir },
418
+ userExternal,
419
+ userBanner,
420
+ userAliases: aliases,
421
+ shebang: this.configData.shebang,
422
+ }, opts);
423
+ return { options: cfg.options, plugins: cfg.plugins };
424
+ };
425
+ const needsGiGlobals = await detectNodeGiGlobals({
426
+ input: userBundler.input,
427
+ plugins: [...pnpPlugins, ...userPlugins],
428
+ external: userBundler.external,
429
+ transform: userBundler.transform,
430
+ format,
431
+ }, pluginOpts, gjsifyPluginFactory, bundleToChunks);
432
+ if (needsGiGlobals)
433
+ pluginOpts.nodeGiGlobalsInject = true;
434
+ }
405
435
  else if (extras) {
406
436
  pluginOpts.autoGlobalsInject = await this.resolveGlobalsInject(app, extras, verbose);
407
437
  }
@@ -119,8 +119,15 @@ const IGNORE = [
119
119
  /^refs\//,
120
120
  /^website\//,
121
121
  /^docs\//,
122
- /^\.github\/workflows\/(deploy-docs|commitlint|release|audit-runtimes|prebuilds)\.yml$/,
122
+ /^\.github\/workflows\/(deploy-docs|commitlint|release|audit-runtimes|prebuilds|node-gi)\.yml$/,
123
123
  /^\.githooks\//,
124
+ // @gjsify/node-gi (the Node-native GObject-Introspection engine, Axis 5) is
125
+ // NOT a gjsify workspace member — the GJS-first install/foreach tooling can't
126
+ // build its node-gyp addon, so the main workflow neither builds nor tests it.
127
+ // Its own `.github/workflows/node-gi.yml` is the source of truth (built +
128
+ // tested on Node). Without this carve-out its files map to no workspace and
129
+ // land in `unmatched`, forcing a conservative full run on every node-gi PR.
130
+ /^packages\/node-gi\//,
124
131
  // Flatpak build/distribution tooling (SDK-extension manifest + metainfo).
125
132
  // Like `.githooks/`, it has no package-test consumers; its own
126
133
  // `tests/e2e/flatpak-sdk-extension` runs on `tests/e2e/**` / global triggers.
@@ -619,6 +619,11 @@ async function workspaceInstall(cwd, args, signal) {
619
619
  overrides,
620
620
  signal,
621
621
  progress,
622
+ // Workspace-named packages are symlinked above — the native backend
623
+ // must never fetch a same-named published version over those
624
+ // symlinks (e.g. a `types-dev/<lib>` workspace that also exists on
625
+ // npm and is pinned transitively in the lockfile).
626
+ workspaceNames: new Set(byName.keys()),
622
627
  });
623
628
  }
624
629
  else if (args.verbose) {
@@ -644,6 +649,7 @@ async function workspaceInstall(cwd, args, signal) {
644
649
  lockfile: !args.immutable,
645
650
  frozen: args.immutable,
646
651
  signal,
652
+ workspaceNames: new Set(byName.keys()),
647
653
  });
648
654
  }
649
655
  // Re-wire workspace symlinks once more now that the external + scoped
@@ -21,6 +21,7 @@ import { DEFAULT_REGISTRY, fetchPackument, fetchPackumentConditional, fetchTarba
21
21
  import { extractTarball } from '@gjsify/tar';
22
22
  import { getCachedTarball, getForeignCachedTarball, putCachedTarball, } from './install-tarball-cache.js';
23
23
  import { getCachedPackument, putCachedPackument } from './install-packument-cache.js';
24
+ import { assertNativeBackendNodeVersion } from './node-version.js';
24
25
  // 16-wide download pool. Matched to the shared Soup.Session's lifted
25
26
  // `max-conns-per-host` (see @gjsify/fetch `getSharedSession`) — a higher pool
26
27
  // here only translates into real concurrency because that cap was raised from
@@ -30,6 +31,9 @@ const DEFAULT_CONCURRENCY = Number(process.env.GJSIFY_INSTALL_CONCURRENCY ?? '16
30
31
  const LOCKFILE_NAME = 'gjsify-lock.json';
31
32
  const LOCKFILE_VERSION = 2;
32
33
  export async function installPackagesNative(opts) {
34
+ // Fail clearly on an unsupported Node major BEFORE touching the ABI-locked
35
+ // native deps — otherwise they SIGSEGV mid-extract with no actionable message.
36
+ assertNativeBackendNodeVersion();
33
37
  if (opts.specs.length === 0) {
34
38
  throw new Error('installPackagesNative: empty specs list');
35
39
  }
@@ -72,12 +76,29 @@ export async function installPackagesNative(opts) {
72
76
  // (and silently bumping transitive deps) on a one-package add.
73
77
  const preferred = !opts.refreshLockfile && existingLock ? buildPreferredVersions(existingLock) : undefined;
74
78
  log('install: resolving %d top-level spec(s) → %s%s', opts.specs.length, opts.prefix, preferred ? ` (preserving ${preferred.size} locked name(s))` : '');
75
- nodes = await resolveDeps(opts.specs, npmrc, log, opts.overrides, opts.skipDeps, opts.signal, progress, preferred);
79
+ nodes = await resolveDeps(opts.specs, npmrc, log, opts.overrides, opts.skipDeps, opts.signal, progress, preferred, opts.workspaceNames);
76
80
  if (opts.lockfile) {
77
81
  writeLockfile(lockfilePath, opts.specs, nodes);
78
82
  log('install: wrote %s (%d entries)', LOCKFILE_NAME, nodes.length);
79
83
  }
80
84
  }
85
+ // A package whose name is one of the monorepo's own workspaces is provided
86
+ // by a workspace symlink (wired by `workspaceInstall`), NOT by a registry
87
+ // tarball — even when the lockfile or a transitive edge pins a same-named
88
+ // published version. Drop those nodes from the fetch/extract set so
89
+ // `extractOne` never `rm`s + overwrites the workspace source symlink (its
90
+ // data-loss guard would otherwise abort the whole install). This keeps
91
+ // `--immutable` robust against a committed lockfile that still carries such
92
+ // registry entries (it is built from the lockfile verbatim and never runs
93
+ // `resolveDeps`); a fresh resolve additionally skips them at the source.
94
+ if (opts.workspaceNames && opts.workspaceNames.size > 0) {
95
+ const before = nodes.length;
96
+ nodes = nodes.filter((n) => !opts.workspaceNames.has(n.name));
97
+ const dropped = before - nodes.length;
98
+ if (dropped > 0) {
99
+ log('install: %d workspace-provided package(s) symlinked, not fetched', dropped);
100
+ }
101
+ }
81
102
  log('install: downloading %d tarball(s)', nodes.length);
82
103
  await downloadAndExtractAll(nodes, opts.prefix, npmrc, log, opts.signal, progress);
83
104
  await linkBins(nodes, opts.prefix, log);
@@ -143,7 +164,14 @@ async function resolveDeps(specs, npmrc, log, overrides, skipDeps, signal, progr
143
164
  * Undefined ⇒ a fresh resolve that always picks the newest in-range version
144
165
  * (first install, or an explicit `--refresh-lockfile`).
145
166
  */
146
- preferredVersions) {
167
+ preferredVersions,
168
+ /**
169
+ * Names of the monorepo's own workspace packages. An edge whose name is in
170
+ * this set is satisfied by the workspace symlink, so it is skipped here —
171
+ * the published version (and its subtree) never enters the resolved tree or
172
+ * the lockfile.
173
+ */
174
+ workspaceNames) {
147
175
  progress?.beginPhase('resolve', specs.length);
148
176
  const applyOverride = (name, range) => {
149
177
  if (!overrides)
@@ -209,6 +237,11 @@ preferredVersions) {
209
237
  // cache instead of blocking on a fresh round-trip.
210
238
  for (let wi = 0; wi < wave.length; wi++) {
211
239
  const edge = wave[wi];
240
+ // A workspace member is satisfied by its workspace symlink, never by
241
+ // a registry tarball — skip the edge so the published version (and
242
+ // its subtree) never enters the resolved tree or the lockfile.
243
+ if (workspaceNames?.has(edge.name))
244
+ continue;
212
245
  // Walk the ancestor chain to see whether a satisfying placement is
213
246
  // already visible from the requester's `node_modules` lookup. npm's
214
247
  // resolver does this — each level of nesting acts as a fallback.
@@ -70,6 +70,19 @@ export interface InstallOptions {
70
70
  * to override.
71
71
  */
72
72
  progress?: ProgressReporter;
73
+ /**
74
+ * Native backend only: the names of the monorepo's own workspace packages.
75
+ * A package whose name is in this set is materialised as a symlink to its
76
+ * workspace source by `workspaceInstall`, never fetched from the registry —
77
+ * even when the lockfile or a transitive edge references a same-named
78
+ * published version (e.g. a `types-dev/<lib>` workspace that ALSO exists on
79
+ * npm). The native backend drops such names from `resolveDeps` AND from the
80
+ * fetch/extract set, so `extractOne` never tries to `rm` + overwrite a
81
+ * workspace source symlink (its data-loss guard would otherwise abort the
82
+ * whole install). The `npm` backend ignores this — npm resolves workspaces
83
+ * itself.
84
+ */
85
+ workspaceNames?: Set<string>;
73
86
  }
74
87
  export interface InstallResult {
75
88
  /** Top-level packages that were requested, with the version each
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Minimum Node.js major the native install backend supports — its prebuilt native
3
+ * modules are ABI-locked to this line (kept in sync with `.nvmrc`).
4
+ */
5
+ export declare const MIN_NODE_MAJOR = 24;
6
+ /** Parse the major from a `process.versions.node`-style string (`'24.18.0'` → 24). */
7
+ export declare function parseNodeMajor(version: string): number;
8
+ /**
9
+ * The preflight verdict for the native install backend, factored out so it can be
10
+ * unit-tested without spawning a foreign Node. Returns an error message when the
11
+ * runtime is real Node older than {@link MIN_NODE_MAJOR}, else `null` — i.e. on a
12
+ * supported Node, on GJS (which fakes `process.versions.node` and does not use
13
+ * these prebuilds), or on a version string that does not parse.
14
+ */
15
+ export declare function nativeBackendNodeError(version: string, onRealNode: boolean): string | null;
16
+ /**
17
+ * Guard the native install backend: throw a clear error on an unsupported Node
18
+ * major instead of letting the ABI-mismatched prebuilds SIGSEGV mid-install.
19
+ * No-op under GJS and on supported Node.
20
+ */
21
+ export declare function assertNativeBackendNodeVersion(): void;
@@ -0,0 +1,56 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Node-version preflight for the native install backend.
3
+ //
4
+ // The native backend composes native deps whose prebuilt binaries are ABI-locked
5
+ // to the Node line gjsify targets (see `.nvmrc`). On an older Node major they load
6
+ // a mismatched ABI and SIGSEGV mid-install instead of failing cleanly — a
7
+ // confusing crash with no actionable message (observed: Node 22 in a Fedora
8
+ // container segfaults `gjsify install --backend native` during download/extract).
9
+ // This preflight turns that into a clear, actionable error.
10
+ import { isNode } from '@gjsify/rolldown-plugin-gjsify/runtime';
11
+ /**
12
+ * Minimum Node.js major the native install backend supports — its prebuilt native
13
+ * modules are ABI-locked to this line (kept in sync with `.nvmrc`).
14
+ */
15
+ export const MIN_NODE_MAJOR = 24;
16
+ /** Parse the major from a `process.versions.node`-style string (`'24.18.0'` → 24). */
17
+ export function parseNodeMajor(version) {
18
+ return Number.parseInt(String(version).split('.', 1)[0] ?? '', 10);
19
+ }
20
+ /**
21
+ * The preflight verdict for the native install backend, factored out so it can be
22
+ * unit-tested without spawning a foreign Node. Returns an error message when the
23
+ * runtime is real Node older than {@link MIN_NODE_MAJOR}, else `null` — i.e. on a
24
+ * supported Node, on GJS (which fakes `process.versions.node` and does not use
25
+ * these prebuilds), or on a version string that does not parse.
26
+ */
27
+ export function nativeBackendNodeError(version, onRealNode) {
28
+ if (!onRealNode)
29
+ return null;
30
+ const major = parseNodeMajor(version);
31
+ if (!Number.isFinite(major) || major >= MIN_NODE_MAJOR)
32
+ return null;
33
+ return (`gjsify install (native backend) requires Node ≥ ${MIN_NODE_MAJOR}, but this is Node ${version}. ` +
34
+ `Its prebuilt native modules are ABI-locked to Node ${MIN_NODE_MAJOR} and crash on older majors. ` +
35
+ `Use Node ${MIN_NODE_MAJOR} (nvm/asdf locally, actions/setup-node in CI), or run with --backend npm.`);
36
+ }
37
+ /** True when the CLI runs on real Node — NOT GJS, which fakes `process.versions.node`. */
38
+ function onRealNode() {
39
+ // GJS exposes `imports.system.version`; @gjsify/process fakes
40
+ // `process.versions.node = '20.0.0'` for compat, so check GJS FIRST — a plain
41
+ // `process.versions.node` read would be a false Node positive under GJS.
42
+ const sysVersion = globalThis.imports?.system?.version;
43
+ if (sysVersion !== undefined)
44
+ return false;
45
+ return isNode();
46
+ }
47
+ /**
48
+ * Guard the native install backend: throw a clear error on an unsupported Node
49
+ * major instead of letting the ABI-mismatched prebuilds SIGSEGV mid-install.
50
+ * No-op under GJS and on supported Node.
51
+ */
52
+ export function assertNativeBackendNodeVersion() {
53
+ const message = nativeBackendNodeError(process.versions.node, onRealNode());
54
+ if (message)
55
+ throw new Error(message);
56
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.13.1",
3
+ "version": "0.13.2",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -108,6 +108,7 @@
108
108
  "start": "node lib/index.js",
109
109
  "build": "tsc && mkdir -p lib/templates/flatpak && cp -L src/templates/install.mjs.tmpl lib/templates/install.mjs.tmpl && cp src/templates/flatpak/*.tmpl lib/templates/flatpak/ && cp src/templates/oxlintrc.json.tmpl lib/templates/oxlintrc.json.tmpl && cp src/templates/oxfmtrc.tmpl lib/templates/oxfmtrc.tmpl && gjsify run chmod",
110
110
  "build:gjs-bundle": "node lib/index.js build src/index.ts --app gjs --outfile dist/cli.gjs.mjs --shebang",
111
+ "build:affected-bundle": "node lib/index.js build src/affected-entry.ts --app gjs --outfile dist/affected.gjs.mjs",
111
112
  "chmod": "chmod +x ./lib/index.js",
112
113
  "build:test:node": "node lib/index.js build src/test.mts --app node --outfile dist/test.node.mjs",
113
114
  "test:node": "node dist/test.node.mjs",
@@ -120,19 +121,19 @@
120
121
  "cli"
121
122
  ],
122
123
  "dependencies": {
123
- "@gjsify/buffer": "^0.13.1",
124
- "@gjsify/create-app": "^0.13.1",
125
- "@gjsify/node-globals": "^0.13.1",
126
- "@gjsify/node-polyfills": "^0.13.1",
127
- "@gjsify/npm-registry": "^0.13.1",
128
- "@gjsify/resolve-npm": "^0.13.1",
129
- "@gjsify/rolldown-plugin-gjsify": "^0.13.1",
130
- "@gjsify/rolldown-plugin-pnp": "^0.13.1",
131
- "@gjsify/semver": "^0.13.1",
132
- "@gjsify/tar": "^0.13.1",
133
- "@gjsify/tsc": "^0.13.1",
134
- "@gjsify/web-polyfills": "^0.13.1",
135
- "@gjsify/workspace": "^0.13.1",
124
+ "@gjsify/buffer": "^0.13.2",
125
+ "@gjsify/create-app": "^0.13.2",
126
+ "@gjsify/node-globals": "^0.13.2",
127
+ "@gjsify/node-polyfills": "^0.13.2",
128
+ "@gjsify/npm-registry": "^0.13.2",
129
+ "@gjsify/resolve-npm": "^0.13.2",
130
+ "@gjsify/rolldown-plugin-gjsify": "^0.13.2",
131
+ "@gjsify/rolldown-plugin-pnp": "^0.13.2",
132
+ "@gjsify/semver": "^0.13.2",
133
+ "@gjsify/tar": "^0.13.2",
134
+ "@gjsify/tsc": "^0.13.2",
135
+ "@gjsify/web-polyfills": "^0.13.2",
136
+ "@gjsify/workspace": "^0.13.2",
136
137
  "cosmiconfig": "^9.0.2",
137
138
  "get-tsconfig": "^4.14.0",
138
139
  "pkg-types": "^2.3.1",
@@ -140,13 +141,13 @@
140
141
  "yargs": "^18.0.0"
141
142
  },
142
143
  "devDependencies": {
143
- "@gjsify/unit": "^0.13.1",
144
+ "@gjsify/unit": "^0.13.2",
144
145
  "@types/yargs": "^17.0.35",
145
146
  "typescript": "^6.0.3"
146
147
  },
147
148
  "peerDependencies": {
148
- "@gjsify/oxfmt-native": "^0.13.1",
149
- "@gjsify/rolldown-native": "^0.13.1"
149
+ "@gjsify/oxfmt-native": "^0.13.2",
150
+ "@gjsify/rolldown-native": "^0.13.2"
150
151
  },
151
152
  "peerDependenciesMeta": {
152
153
  "@gjsify/oxfmt-native": {