@gjsify/cli 0.36.0 → 0.37.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 (116) hide show
  1. package/dist/cli.gjs.mjs +3 -3
  2. package/lib/bundler-pick.js +61 -87
  3. package/lib/cli-fail.d.ts +4 -7
  4. package/lib/cli-fail.js +29 -55
  5. package/lib/commands/affected.js +161 -286
  6. package/lib/commands/install.d.ts +63 -66
  7. package/lib/commands/install.js +372 -536
  8. package/lib/templates/install.mjs.tmpl +38 -68
  9. package/lib/types/cli-build-options.d.ts +33 -67
  10. package/lib/types/command.d.ts +9 -12
  11. package/lib/types/config-data.d.ts +125 -221
  12. package/lib/utils/auth-npmrc.d.ts +5 -10
  13. package/lib/utils/auth-npmrc.js +11 -21
  14. package/lib/utils/clear-targets.d.ts +8 -11
  15. package/lib/utils/clear-targets.js +22 -29
  16. package/lib/utils/copy-targets.d.ts +13 -19
  17. package/lib/utils/copy-targets.js +31 -44
  18. package/lib/utils/dep-aggregation.d.ts +7 -15
  19. package/lib/utils/dep-aggregation.js +9 -22
  20. package/lib/utils/detect-native-packages.d.ts +83 -116
  21. package/lib/utils/detect-native-packages.js +111 -149
  22. package/lib/utils/dir-link.d.ts +29 -48
  23. package/lib/utils/dir-link.js +33 -56
  24. package/lib/utils/discover-showcases.d.ts +6 -7
  25. package/lib/utils/discover-showcases.js +11 -15
  26. package/lib/utils/dlx-cache.d.ts +29 -38
  27. package/lib/utils/dlx-cache.js +53 -63
  28. package/lib/utils/force-exit.d.ts +0 -3
  29. package/lib/utils/force-exit.js +13 -24
  30. package/lib/utils/gi-search-path.d.ts +16 -24
  31. package/lib/utils/gi-search-path.js +54 -82
  32. package/lib/utils/gjs-bundle-guard.d.ts +5 -13
  33. package/lib/utils/gjs-bundle-guard.js +35 -57
  34. package/lib/utils/gjs-source-escape.d.ts +2 -2
  35. package/lib/utils/gjs-source-escape.js +21 -39
  36. package/lib/utils/gjsify-shim.d.ts +17 -20
  37. package/lib/utils/gjsify-shim.js +80 -120
  38. package/lib/utils/html-entry.d.ts +8 -12
  39. package/lib/utils/html-entry.js +19 -28
  40. package/lib/utils/install-backend-native.d.ts +123 -161
  41. package/lib/utils/install-backend-native.js +536 -737
  42. package/lib/utils/install-backend.d.ts +42 -57
  43. package/lib/utils/install-backend.js +16 -21
  44. package/lib/utils/install-cache-fs.d.ts +10 -15
  45. package/lib/utils/install-cache-fs.js +18 -26
  46. package/lib/utils/install-lock.d.ts +5 -6
  47. package/lib/utils/install-lock.js +46 -65
  48. package/lib/utils/install-packument-cache.d.ts +27 -33
  49. package/lib/utils/install-packument-cache.js +44 -57
  50. package/lib/utils/install-progress.d.ts +2 -9
  51. package/lib/utils/install-progress.js +14 -39
  52. package/lib/utils/install-tarball-cache.d.ts +14 -20
  53. package/lib/utils/install-tarball-cache.js +41 -59
  54. package/lib/utils/library-output.d.ts +9 -22
  55. package/lib/utils/library-output.js +19 -37
  56. package/lib/utils/load-npmrc.d.ts +5 -6
  57. package/lib/utils/load-npmrc.js +18 -27
  58. package/lib/utils/node-bundle-guard.d.ts +4 -8
  59. package/lib/utils/node-bundle-guard.js +21 -35
  60. package/lib/utils/node-script.js +33 -54
  61. package/lib/utils/node-version.d.ts +5 -14
  62. package/lib/utils/node-version.js +15 -24
  63. package/lib/utils/normalize-bundler-options.d.ts +7 -11
  64. package/lib/utils/normalize-bundler-options.js +24 -42
  65. package/lib/utils/npm-oidc.d.ts +23 -37
  66. package/lib/utils/npm-oidc.js +28 -53
  67. package/lib/utils/npm-otp-cache.d.ts +4 -4
  68. package/lib/utils/npm-otp-cache.js +20 -25
  69. package/lib/utils/npm-otp.d.ts +20 -30
  70. package/lib/utils/npm-otp.js +32 -51
  71. package/lib/utils/onboard-probe.d.ts +7 -13
  72. package/lib/utils/onboard-probe.js +16 -27
  73. package/lib/utils/parse-spec.d.ts +3 -3
  74. package/lib/utils/parse-spec.js +4 -5
  75. package/lib/utils/pkg-json-edit.d.ts +7 -12
  76. package/lib/utils/pkg-json-edit.js +13 -26
  77. package/lib/utils/pkg-json.d.ts +11 -18
  78. package/lib/utils/pkg-json.js +19 -28
  79. package/lib/utils/prefixed-output.d.ts +3 -4
  80. package/lib/utils/prefixed-output.js +13 -19
  81. package/lib/utils/prompt.d.ts +3 -4
  82. package/lib/utils/prompt.js +14 -18
  83. package/lib/utils/publish-diagnose.d.ts +11 -16
  84. package/lib/utils/publish-diagnose.js +15 -28
  85. package/lib/utils/publish-headers.d.ts +7 -11
  86. package/lib/utils/publish-headers.js +36 -65
  87. package/lib/utils/publishable-packages.d.ts +4 -6
  88. package/lib/utils/publishable-packages.js +14 -18
  89. package/lib/utils/resolve-gjs-entry.d.ts +5 -8
  90. package/lib/utils/resolve-gjs-entry.js +10 -27
  91. package/lib/utils/resolve-npm-package.d.ts +6 -11
  92. package/lib/utils/resolve-npm-package.js +29 -63
  93. package/lib/utils/resolve-plugin-by-name.d.ts +11 -19
  94. package/lib/utils/resolve-plugin-by-name.js +9 -14
  95. package/lib/utils/run-gjs.d.ts +16 -21
  96. package/lib/utils/run-gjs.js +31 -56
  97. package/lib/utils/run-lifecycle-script.d.ts +5 -9
  98. package/lib/utils/run-lifecycle-script.js +23 -42
  99. package/lib/utils/run-node.d.ts +21 -33
  100. package/lib/utils/run-node.js +43 -64
  101. package/lib/utils/runtimes.d.ts +25 -35
  102. package/lib/utils/runtimes.js +34 -46
  103. package/lib/utils/spawn.d.ts +32 -48
  104. package/lib/utils/spawn.js +51 -84
  105. package/lib/utils/stdin.d.ts +2 -2
  106. package/lib/utils/stdin.js +10 -25
  107. package/lib/utils/suggest.d.ts +6 -7
  108. package/lib/utils/suggest.js +8 -10
  109. package/lib/utils/system-gi.d.ts +18 -27
  110. package/lib/utils/system-gi.js +60 -114
  111. package/lib/utils/trust-registry.d.ts +4 -8
  112. package/lib/utils/trust-registry.js +7 -15
  113. package/lib/utils/win32-command.d.ts +8 -14
  114. package/lib/utils/win32-command.js +29 -43
  115. package/lib/utils/workspace-root.js +7 -12
  116. package/package.json +18 -18
@@ -1,88 +1,59 @@
1
1
  // Multi-anchor npm-package resolver for the GJS-bundled CLI.
2
2
  //
3
- // GJS's native ESM loader has no node_modules walker — a bare
4
- // `await import('rolldown')` from inside the bundle throws
5
- // `ImportError: Module not found: rolldown` even when the package is
6
- // physically present in a node_modules above the caller's cwd.
3
+ // GJS's native ESM loader has no node_modules walker — a bare `await import('rolldown')`
4
+ // from inside the bundle throws `ImportError: Module not found: rolldown` even when the
5
+ // package is physically present in a node_modules above the caller's cwd.
7
6
  //
8
- // `createRequire(import.meta.url)` works when invoked under Node, and
9
- // when invoked under GJS *with* `@gjsify/module`'s polyfill it walks the
10
- // node_modules chain from the URL it was anchored at. But it walks
11
- // only ONE chain — the one rooted at the anchor's parent. When the
12
- // bundle lives at `<install>/dist/cli.gjs.mjs` and the user runs
13
- // `gjs -m <install>/dist/cli.gjs.mjs build …` from a completely
14
- // unrelated cwd (`/tmp/sandbox/`) whose `node_modules` is the only
15
- // place rolldown is present, anchoring on the bundle URL misses it.
7
+ // `createRequire(import.meta.url)` works under Node, and under GJS with `@gjsify/module`'s
8
+ // polyfill it walks the node_modules chain from the URL it was anchored at — but only ONE
9
+ // chain, the one rooted at the anchor's parent. When the bundle lives at
10
+ // `<install>/dist/cli.gjs.mjs` and the user runs it from an unrelated cwd whose
11
+ // `node_modules` is the only place rolldown is present, anchoring on the bundle URL misses
12
+ // it. So a bare specifier is tried against several anchors, first hit wins:
16
13
  //
17
- // This helper resolves a bare specifier against multiple anchors in
18
- // order — the first one that resolves wins:
19
- //
20
- // 1. `GJSIFY_NODE_PATH` env override (colon-separated dir list,
21
- // matches Node's NODE_PATH semantics — each entry is treated as a
22
- // synthetic `node_modules` parent).
14
+ // 1. `GJSIFY_NODE_PATH` env override (NODE_PATH semantics — each entry is a synthetic
15
+ // `node_modules` parent).
23
16
  // 2. Caller-supplied anchor dir (typically process.cwd()).
24
- // 3. Workspace root from `findWorkspaceRoot(anchorDir)` — finds the
25
- // monorepo top when invoked from a sub-package directory.
17
+ // 3. Workspace root from `findWorkspaceRoot(anchorDir)` — the monorepo top when invoked
18
+ // from a sub-package directory.
26
19
  // 4. Bundle path's parent chain (`import.meta.url` of THIS module).
27
- // 5. Parent dirs of the anchor (fallback for nested-without-workspace
28
- // setups — matches `oxc-resolve.ts`'s walker).
29
- //
30
- // Returns the absolute path on success (the caller usually wraps it in
31
- // `pathToFileURL().href` before passing to dynamic `import(...)`).
32
- // Returns null when every anchor failed.
20
+ // 5. Parent dirs of the anchor (nested-without-workspace layouts).
33
21
  //
34
- // Reference: `packages/infra/cli/src/commands/tsc.ts` (workspace-root +
35
- // cwd anchoring precedent) and `packages/infra/cli/src/utils/oxc-resolve.ts`
36
- // (parent-dir walker precedent).
22
+ // Precedents: `commands/tsc.ts` (workspace-root + cwd anchoring), `utils/oxc-resolve.ts`
23
+ // (parent-dir walker).
37
24
  import { createRequire } from 'node:module';
38
25
  import { pathToFileURL } from 'node:url';
39
26
  import { delimiter, join, resolve } from 'node:path';
40
27
  import { findWorkspaceRoot } from './workspace-root.js';
41
28
  /**
42
- * Resolve a bare npm specifier through multiple createRequire anchors.
43
- * Returns the absolute filesystem path of the resolved module, or null
44
- * if no anchor succeeded.
45
- *
46
- * Each anchor is tried in priority order — `GJSIFY_NODE_PATH` (highest)
47
- * → caller cwd → workspace root → bundle URL → parent-dir walk from
48
- * cwd. The first anchor whose `require.resolve()` succeeds wins.
29
+ * Resolve a bare npm specifier through the anchors listed above, returning the resolved
30
+ * module's absolute path or null when every anchor failed. Callers usually wrap the result in
31
+ * `pathToFileURL().href` before a dynamic `import(...)`.
49
32
  */
50
33
  export function resolveNpmPackage(specifier, opts = {}) {
51
34
  const cwd = opts.cwd ?? process.cwd();
52
- // 1. GJSIFY_NODE_PATH env override (NODE_PATH-like) — each entry is
53
- // treated as a directory whose own `node_modules` participates
54
- // in the lookup. Mirrors Node's NODE_PATH semantics.
55
35
  const envPath = process.env['GJSIFY_NODE_PATH'];
56
36
  if (envPath) {
57
- // `path.delimiter`, not `':'` — Node's own NODE_PATH uses `;` on
58
- // Windows, and this override claims to mirror NODE_PATH semantics. Split
59
- // on a colon there and even a SINGLE entry is destroyed: `C:\tools`
60
- // becomes `['C', '\tools']`, neither of which resolves, so the documented
61
- // override silently did nothing on Windows and fell through to anchor 2.
62
- // `commands/run.ts` and `utils/run-lifecycle-script.ts` already get this
63
- // right; this was the site that was missed.
37
+ // `path.delimiter`, not `':'` — NODE_PATH uses `;` on Windows, where splitting on a
38
+ // colon destroys even a SINGLE entry (`C:\tools` → `['C', '\tools']`), so the
39
+ // documented override silently did nothing there and fell through to anchor 2.
64
40
  for (const dir of envPath.split(delimiter).filter(Boolean)) {
65
41
  const hit = tryResolveFromDir(specifier, resolve(dir));
66
42
  if (hit)
67
43
  return hit;
68
44
  }
69
45
  }
70
- // 2. Caller cwd → walk up for nearest node_modules/<pkg>.
71
46
  const fromCwd = tryResolveFromDir(specifier, cwd);
72
47
  if (fromCwd)
73
48
  return fromCwd;
74
- // 3. Workspace root (monorepo top, if any).
75
49
  const wsRoot = findWorkspaceRoot(cwd);
76
50
  if (wsRoot && wsRoot !== cwd) {
77
51
  const fromWsRoot = tryResolveFromDir(specifier, wsRoot);
78
52
  if (fromWsRoot)
79
53
  return fromWsRoot;
80
54
  }
81
- // 4. Bundle path's own parent chain — anchored at the bundle URL
82
- // so the install-time node_modules layout (e.g.
83
- // `<install>/node_modules/rolldown` next to the CLI bundle)
84
- // is reachable even when the user runs the bundle directly from
85
- // an unrelated cwd.
55
+ // Anchored at the bundle URL so the install-time layout (`<install>/node_modules/rolldown`
56
+ // next to the CLI bundle) stays reachable when the bundle is run from an unrelated cwd.
86
57
  if (opts.bundleUrl) {
87
58
  try {
88
59
  const req = createRequire(opts.bundleUrl);
@@ -92,11 +63,8 @@ export function resolveNpmPackage(specifier, opts = {}) {
92
63
  // Fall through.
93
64
  }
94
65
  }
95
- // 5. Parent-dir walk from cwd as a last resort (nested
96
- // package layouts without a workspace marker). Capped at 8
97
- // levels to avoid runaway filesystem walks on pathological
98
- // layouts. Matches the depth `oxc-resolve.ts` uses for its
99
- // equivalent walker.
66
+ // Last resort for nested layouts without a workspace marker. Depth-capped so a
67
+ // pathological layout cannot turn a failed resolve into a filesystem walk to `/`.
100
68
  let dir = resolve(cwd, '..');
101
69
  for (let i = 0; i < 8; i++) {
102
70
  const hit = tryResolveFromDir(specifier, dir);
@@ -110,11 +78,9 @@ export function resolveNpmPackage(specifier, opts = {}) {
110
78
  return null;
111
79
  }
112
80
  /**
113
- * Attempt to resolve `specifier` as if a fictional source file at
114
- * `<dir>/__gjsify_resolve__.js` were doing the lookup. createRequire's
115
- * algorithm walks up from the anchor's parent for `node_modules/<pkg>`
116
- * so seeding it inside `<dir>` makes `<dir>/node_modules/<pkg>` the
117
- * first hit.
81
+ * Resolve `specifier` as if a fictional source file at `<dir>/__gjsify_resolve__.js` were
82
+ * doing the lookup: createRequire walks up from the anchor's PARENT for `node_modules/<pkg>`,
83
+ * so seeding it inside `<dir>` makes `<dir>/node_modules/<pkg>` the first hit.
118
84
  */
119
85
  function tryResolveFromDir(specifier, dir) {
120
86
  try {
@@ -10,32 +10,24 @@ export interface PluginByName {
10
10
  /**
11
11
  * Strategy for turning a resolved plugin file path into its module namespace.
12
12
  *
13
- * On Node this is a plain dynamic `import()`. Under GJS it cannot be — GJS's
14
- * native ESM loader does not follow npm `package.json#exports` subpath maps
15
- * for bare specifiers, so importing a plugin whose source does
16
- * `import … from '@scope/pkg/internal-subpath'` throws
17
- * `Module not found: @scope/pkg/internal-subpath` even when the file is on
18
- * disk. The CLI injects a GJS strategy that first bundles the plugin to a
19
- * single self-contained ESM file (Rolldown resolves the exports map at
20
- * bundle time), then imports that. See `BuildAction.loadPluginViaGjsBundle`.
13
+ * A plain dynamic `import()` on Node, but under GJS it cannot be: GJS's native ESM loader does not
14
+ * follow npm `package.json#exports` subpath maps for bare specifiers, so a plugin whose source
15
+ * does `import … from '@scope/pkg/internal-subpath'` throws `Module not found` even with the file
16
+ * on disk. The CLI injects a GJS strategy that first bundles the plugin to one self-contained ESM
17
+ * file (Rolldown resolves the exports map at bundle time), then imports that — see
18
+ * `BuildAction.loadPluginViaGjsBundle`.
21
19
  */
22
20
  export type LoadPluginModule = (resolvedPath: string, pluginName: string) => Promise<Record<string, unknown>>;
23
21
  export interface ResolveUserPluginsOptions {
24
- /**
25
- * Override how a resolved plugin path is turned into its module
26
- * namespace. Defaults to a direct dynamic `import()` (correct on Node).
27
- */
22
+ /** Defaults to a direct dynamic `import()`, which is correct on Node. */
28
23
  loadModule?: LoadPluginModule;
29
24
  }
30
25
  /** Type-guard: a `PluginByName` shape rather than a Rolldown plugin object. */
31
26
  export declare function isPluginByName(value: unknown): value is PluginByName;
32
27
  /**
33
- * Resolve a list of mixed user plugins. Entries that are already plugin
34
- * objects pass through unchanged; entries shaped like `PluginByName` get
35
- * dynamically imported, instantiated with their `options`, and returned in
36
- * the same position. Resolution is anchored at `projectDir`.
37
- *
38
- * Throws when a name fails to resolve, when the chosen export is not a
39
- * function, or when the factory returns nothing.
28
+ * Resolve a list of mixed user plugins, anchored at `projectDir`. Plugin objects pass through
29
+ * unchanged; `PluginByName` entries are imported, instantiated with their `options`, and returned
30
+ * in the same position. Throws when a name fails to resolve, the chosen export is not a function,
31
+ * or the factory returns nothing.
40
32
  */
41
33
  export declare function resolveUserPlugins(plugins: ReadonlyArray<RolldownPluginOption | PluginByName>, projectDir: string, options?: ResolveUserPluginsOptions): Promise<RolldownPluginOption[]>;
@@ -8,10 +8,9 @@
8
8
  // ]
9
9
  // }
10
10
  //
11
- // Lets `package.json#gjsify` describe the full plugin chain without dropping
12
- // to a JS-form config file (`gjsify.config.mjs`). Resolution is anchored at
13
- // the project root (where the config lives) so the project's own
14
- // `node_modules` wins over the CLI's own dependencies.
11
+ // Lets `package.json#gjsify` describe the full plugin chain without dropping to a JS-form config
12
+ // file (`gjsify.config.mjs`). Resolution is anchored at the project root, so the project's own
13
+ // `node_modules` wins over the CLI's dependencies.
15
14
  import { createRequire } from 'node:module';
16
15
  import { join } from 'node:path';
17
16
  import { pathToFileURL } from 'node:url';
@@ -22,9 +21,8 @@ export function isPluginByName(value) {
22
21
  return (typeof value === 'object' &&
23
22
  value !== null &&
24
23
  typeof value.name === 'string' &&
25
- // RolldownPluginOption can be `false | null | undefined | Plugin | Promise<Plugin>`.
26
- // A real plugin always has a function-shape behavior; `name` alone is shared
27
- // with our shape, so we additionally require absence of plugin-shape fields.
24
+ // `name` alone does not discriminate — a real Rolldown plugin has it too — so the
25
+ // absence of every plugin-shape hook is what separates the two.
28
26
  !('apply' in value) &&
29
27
  !('resolveId' in value) &&
30
28
  !('load' in value) &&
@@ -33,13 +31,10 @@ export function isPluginByName(value) {
33
31
  !('generateBundle' in value));
34
32
  }
35
33
  /**
36
- * Resolve a list of mixed user plugins. Entries that are already plugin
37
- * objects pass through unchanged; entries shaped like `PluginByName` get
38
- * dynamically imported, instantiated with their `options`, and returned in
39
- * the same position. Resolution is anchored at `projectDir`.
40
- *
41
- * Throws when a name fails to resolve, when the chosen export is not a
42
- * function, or when the factory returns nothing.
34
+ * Resolve a list of mixed user plugins, anchored at `projectDir`. Plugin objects pass through
35
+ * unchanged; `PluginByName` entries are imported, instantiated with their `options`, and returned
36
+ * in the same position. Throws when a name fails to resolve, the chosen export is not a function,
37
+ * or the factory returns nothing.
43
38
  */
44
39
  export async function resolveUserPlugins(plugins, projectDir, options = {}) {
45
40
  const loadModule = options.loadModule ?? defaultLoadModule;
@@ -1,38 +1,33 @@
1
1
  import { type NativeEnv } from './detect-native-packages.js';
2
2
  /**
3
- * Pure env computation for a given bundle. Returns the typelib +
4
- * shared-library search paths that {@link runGjsBundle} would inject into the
5
- * spawned `gjs` process, plus the formatted env-prefix string used for the
6
- * `$ …` echo. Which library variable is set is host-dependent
7
- * (`LD_LIBRARY_PATH` / `DYLD_LIBRARY_PATH` / `PATH`) — see `buildNativeEnv`.
3
+ * Pure env computation: the typelib + shared-library search paths {@link runGjsBundle} would
4
+ * inject into the spawned `gjs`, plus the formatted prefix for the `$ …` echo. Which library
5
+ * variable is set is host-dependent (`LD_LIBRARY_PATH` / `DYLD_LIBRARY_PATH` / `PATH`) — see
6
+ * `buildNativeEnv`.
8
7
  */
9
8
  export declare function computeNativeEnvForBundle(bundlePath: string, cwd?: string, inherited?: Record<string, string | undefined>): {
10
9
  env: NativeEnv;
11
10
  envPrefix: string;
12
11
  };
13
12
  /**
14
- * Run a GJS bundle, automatically setting LD_LIBRARY_PATH and GI_TYPELIB_PATH
15
- * for any installed native gjsify packages discoverable from either the CWD
16
- * or the bundle's own node_modules tree.
13
+ * Options for {@link runGjsBundle}, which runs a GJS bundle with `GI_TYPELIB_PATH` and the
14
+ * host's library variable set for any native gjsify packages discoverable from the CWD or the
15
+ * bundle's own node_modules tree.
17
16
  */
18
17
  export interface RunGjsBundleOptions {
19
18
  /**
20
- * Exit this process with code 0 once the child succeeds. Under GJS,
21
- * `ensureMainLoop()` (armed by spawn) keeps the parent's GLib loop alive
22
- * after the child exits — a TERMINAL caller (`gjsify run <file>`, `dlx`)
23
- * must opt in here or it parks forever (this exact gap hung CI's
24
- * "Test WebGL conformance" for 83 min: the suite finished in 1.35 s, the
25
- * parent `gjsify run conformance.gjs.js` never exited). Callers that
26
- * continue after the bundle (e.g. `gjsify test`'s multi-runtime
27
- * aggregation loop) MUST leave this false — an unconditional exit here
28
- * truncated `gjsify test` after the first gjs bundle.
19
+ * Exit this process with code 0 once the child succeeds. Under GJS, `ensureMainLoop()`
20
+ * (armed by spawn) keeps the parent's GLib loop alive after the child exits, so a TERMINAL
21
+ * caller (`gjsify run <file>`, `dlx`) must opt in or it parks forever — this gap hung CI's
22
+ * WebGL conformance job for 83 min on a suite that finished in 1.35 s. Callers that
23
+ * continue after the bundle (`gjsify test`'s multi-runtime loop) MUST leave this false; an
24
+ * unconditional exit truncated `gjsify test` after the first gjs bundle.
29
25
  */
30
26
  exitOnSuccess?: boolean;
31
27
  /**
32
- * Suppress the `$ <env> gjs -m …` echo. The echo exists so a user can
33
- * copy-paste the exact command, which is worth ~2 kB of env prefix for a
34
- * one-shot `gjsify run <bundle>` — and is pure noise for a build script the
35
- * chain invokes a dozen times (`utils/node-script.ts`).
28
+ * Suppress the `$ <env> gjs -m …` echo. Worth its ~2 kB of env prefix for a one-shot
29
+ * `gjsify run <bundle>`; pure noise for a build script invoked a dozen times per chain
30
+ * (`utils/node-script.ts`).
36
31
  */
37
32
  quiet?: boolean;
38
33
  }
@@ -1,30 +1,22 @@
1
1
  // Shared utility for running a GJS bundle with native package env vars.
2
2
  // Used by `gjsify run`, `gjsify dlx`, and the showcase command (via dlx).
3
3
  //
4
- // Detection runs the same exhaustive node_modules walker (`detectNativePackages`)
5
- // from two starting points and merges by package name (CWD shadows bundle):
4
+ // Detection runs `detectNativePackages` from two starting points and merges by package name
5
+ // (CWD shadows bundle): `process.cwd()` for native deps in the user's project, and
6
+ // `dirname(bundlePath)` for those in whatever node_modules the bundle lives in — critical for
7
+ // `gjsify dlx`, where the bundle sits under `~/.cache/gjsify/dlx/<sha>/…` and the user's CWD
8
+ // is unrelated. The bundle-side walk also catches transitive deps' typelibs.
6
9
  //
7
- // 1. process.cwd() — picks up native deps in the user's project
8
- // (yarn / pnpm / npm node_modules walking up).
9
- // 2. dirname(bundlePath) — picks up native deps in whatever node_modules
10
- // the bundle lives in. Critical for `gjsify dlx`
11
- // where the bundle resides in
12
- // `~/.cache/gjsify/dlx/<sha>/.../node_modules/<pkg>/dist/`
13
- // and the user's CWD is unrelated. The bundle-side
14
- // walk also catches transitive deps' typelibs.
15
- //
16
- // Env composition is split out as `computeNativeEnvForBundle()` — a pure
17
- // function that takes a bundle path + cwd and returns the env it would inject.
18
- // This lets the e2e tests assert the env without spawning gjs.
10
+ // Env composition is split out as the pure `computeNativeEnvForBundle()` so the e2e tests can
11
+ // assert the env without spawning gjs.
19
12
  import { spawn } from 'node:child_process';
20
13
  import { dirname, resolve } from 'node:path';
21
14
  import { detectNativePackages, buildNativeEnv } from './detect-native-packages.js';
22
15
  /**
23
- * Pure env computation for a given bundle. Returns the typelib +
24
- * shared-library search paths that {@link runGjsBundle} would inject into the
25
- * spawned `gjs` process, plus the formatted env-prefix string used for the
26
- * `$ …` echo. Which library variable is set is host-dependent
27
- * (`LD_LIBRARY_PATH` / `DYLD_LIBRARY_PATH` / `PATH`) — see `buildNativeEnv`.
16
+ * Pure env computation: the typelib + shared-library search paths {@link runGjsBundle} would
17
+ * inject into the spawned `gjs`, plus the formatted prefix for the `$ …` echo. Which library
18
+ * variable is set is host-dependent (`LD_LIBRARY_PATH` / `DYLD_LIBRARY_PATH` / `PATH`) — see
19
+ * `buildNativeEnv`.
28
20
  */
29
21
  export function computeNativeEnvForBundle(bundlePath, cwd = process.cwd(), inherited = process.env) {
30
22
  const resolvedBundle = resolve(bundlePath);
@@ -32,23 +24,16 @@ export function computeNativeEnvForBundle(bundlePath, cwd = process.cwd(), inher
32
24
  const bundlePackages = detectNativePackages(dirname(resolvedBundle));
33
25
  const seen = new Set(cwdPackages.map((p) => p.name));
34
26
  const nativePackages = [...cwdPackages, ...bundlePackages.filter((p) => !seen.has(p.name))];
35
- // `inherited` feeds BOTH halves — the composition and the comparison. Split
36
- // them and the win32 branch disagrees with itself: `buildNativeEnv` writes
37
- // the library variable back under the host's own spelling (`Path` in a
38
- // stock env block), so a comparison keyed on the canonical `PATH` would
39
- // look up a key that is not there and call every run a change.
27
+ // `inherited` feeds BOTH halves — composition and comparison. Split them and the win32
28
+ // branch disagrees with itself: `buildNativeEnv` writes the library variable back under
29
+ // the host's own spelling (`Path` in a stock env block), so a comparison keyed on the
30
+ // canonical `PATH` would look up a key that is not there and call every run a change.
40
31
  const env = buildNativeEnv(nativePackages, { env: inherited });
41
- // Show only what this CLI actually CHANGED. `buildNativeEnv` prepends the
42
- // detected directories to the inherited value and returns the result
43
- // unconditionally, so with no native packages it hands back the host's own
44
- // variable verbatim — and the echo then claimed the CLI had set it.
45
- //
46
- // Harmless on Linux, where `LD_LIBRARY_PATH` is usually unset and the empty
47
- // filter caught it. On Windows the library variable IS `PATH`, which is
48
- // never empty, so every run printed a ~2 kB dump of the host PATH in front
49
- // of a command the user is invited to copy — burying the command, and
50
- // claiming a change that never happened. Comparing against the inherited
51
- // value is what makes the echo honest on both.
32
+ // Show only what this CLI actually CHANGED: `buildNativeEnv` prepends to the inherited
33
+ // value unconditionally, so with no native packages it hands back the host's own variable
34
+ // verbatim. Harmless on Linux, where `LD_LIBRARY_PATH` is usually unset; on Windows the
35
+ // library variable IS `PATH`, never empty, so every run printed a ~2 kB dump of the host
36
+ // PATH in front of a command the user is invited to copy.
52
37
  const envPrefix = Object.entries(env)
53
38
  .filter(([key, value]) => value !== undefined && value !== '' && value !== inherited[key])
54
39
  .map(([key, value]) => `${key}=${value}`)
@@ -62,24 +47,16 @@ export async function runGjsBundle(bundlePath, extraArgs = [], options = {}) {
62
47
  ...nativeEnv,
63
48
  };
64
49
  const gjsArgs = ['-m', bundlePath, ...extraArgs];
65
- // Print the exact command being executed so users can copy-paste it to
66
- // run gjs directly without the wrapper. Env vars are only shown if we
67
- // actually set any (i.e. native gjsify packages were detected).
68
- // Use stderr so that children speaking a protocol on stdout (e.g. an
69
- // MCP stdio server) receive an uncontaminated stdout stream.
50
+ // Echo the exact command so users can copy-paste it and run gjs without the wrapper.
51
+ // stderr, so children speaking a protocol on stdout (an MCP stdio server) keep a clean one.
70
52
  const gjsCommand = ['gjs', ...gjsArgs.map((a) => (a.includes(' ') ? `"${a}"` : a))].join(' ');
71
53
  if (!options.quiet)
72
54
  console.error(`$ ${envPrefix ? `${envPrefix} ` : ''}${gjsCommand}`);
73
55
  const child = spawn('gjs', gjsArgs, { env, stdio: 'inherit' });
74
56
  let failed = false;
75
- // The CHILD's own exit code, so it can be re-raised verbatim. A spawn error
76
- // (no `gjs` on PATH) has no code of its own and falls back to 1.
77
- //
78
- // Collapsing every failure to 1 lost information a caller acts on: a script
79
- // run through `gjsify run --node-script` reports ITS status (the bootstrap
80
- // scripts all end `process.exit(r.status ?? 1)`, forwarding a child's code),
81
- // and a chain that reads `$?` to distinguish "tool failed" from "tool says
82
- // no" saw the same 1 for both.
57
+ // The CHILD's own exit code, re-raised verbatim; a spawn error (no `gjs` on PATH) has none
58
+ // of its own and falls back to 1. Collapsing every failure to 1 lost information callers
59
+ // act on — a chain reading `$?` to tell "tool failed" from "tool says no" saw 1 for both.
83
60
  let childCode = 1;
84
61
  await new Promise((resolvePromise, reject) => {
85
62
  child.on('close', (code) => {
@@ -97,14 +74,12 @@ export async function runGjsBundle(bundlePath, extraArgs = [], options = {}) {
97
74
  failed = true;
98
75
  });
99
76
  if (failed) {
100
- // Set `process.exitCode` synchronously BEFORE exiting: under GJS
101
- // `process.exit()` is deferred (the GLib main loop is armed), so when
102
- // this runs via run.ts's in-process script dispatch, execution returns
103
- // to the caller, which propagates by reading `process.exitCode`. Without
104
- // this, `gjsify run <script>` whose body is `gjsify run <bundle>`
105
- // swallowed a non-zero gjs exit and returned 0 — masking a failing
106
- // `test:gjs` in the `test` script chain (and, via `gjsify foreach test`,
107
- // in CI). The `return` guards against a fall-through second `exit`.
77
+ // Set `process.exitCode` synchronously BEFORE exiting: under GJS `process.exit()` is
78
+ // deferred (the GLib main loop is armed), so under run.ts's in-process script dispatch
79
+ // execution returns to the caller, which propagates by reading `process.exitCode`.
80
+ // Without it, `gjsify run <script>` wrapping `gjsify run <bundle>` swallowed a non-zero
81
+ // gjs exit and returned 0 — masking a failing `test:gjs` in the `test` chain, and via
82
+ // `gjsify foreach test` in CI. The `return` guards a fall-through second `exit`.
108
83
  process.exitCode = childCode;
109
84
  return process.exit(childCode);
110
85
  }
@@ -2,20 +2,16 @@ export interface RunLifecycleScriptOptions {
2
2
  /** When true, do not throw on missing scripts — return `false` instead. */
3
3
  optional?: boolean;
4
4
  /**
5
- * Stdio inheritance for the spawned script. Default `'inherit'` so output
6
- * appears in the parent's terminal. Pass `'inherit-stderr'` to mirror
7
- * inheritance but redirect the child's stdout → parent's stderr — used by
8
- * `gjsify pack --json` and `gjsify publish` so the parent's stdout stays
9
- * a clean JSON stream (script log lines would otherwise corrupt the
10
- * machine-readable output that callers `JSON.parse`).
5
+ * Stdio for the spawned script; default `'inherit'`. `'inherit-stderr'` redirects the
6
+ * child's stdout → parent's stderr, used by `gjsify pack --json` and `gjsify publish` so
7
+ * script log lines cannot corrupt the JSON stream callers `JSON.parse`.
11
8
  */
12
9
  stdio?: 'inherit' | 'inherit-stderr' | 'pipe' | 'ignore';
13
10
  /** Extra environment variables layered on top of the defaults. */
14
11
  env?: Record<string, string>;
15
12
  }
16
13
  /**
17
- * Run a lifecycle script defined in `pkg.scripts[name]` from `wsDir`.
18
- * Returns `true` if the script existed and exited 0. Returns `false` if
19
- * `optional: true` and the script is missing. Throws on non-zero exit.
14
+ * Run the lifecycle script `pkg.scripts[name]` from `wsDir`. `true` when it existed and exited
15
+ * 0, `false` when it is missing and `optional` is set; throws on a non-zero exit.
20
16
  */
21
17
  export declare function runLifecycleScript(wsDir: string, pkg: Record<string, unknown>, name: string, opts?: RunLifecycleScriptOptions): Promise<boolean>;
@@ -2,47 +2,31 @@
2
2
  // `postpack`, `postpublish`, …) from inside a `gjsify pack` / `gjsify
3
3
  // publish` flow.
4
4
  //
5
- // Why this exists separately from `gjsify run`: `gjsify run <script>` ends
6
- // with `process.exit(<code>)` so it can be used as a CLI entrypoint that
7
- // behaves like `yarn run` / `npm run`. That's wrong for the embedded use
8
- // case where pack/publish needs the lifecycle script to finish, then keep
9
- // running its own logic. This helper resolves to a Promise that settles
10
- // when the child exits.
5
+ // Separate from `gjsify run` because that ends with `process.exit(<code>)` so it can act as a
6
+ // CLI entrypoint like `yarn run`. Pack/publish instead needs the lifecycle script to finish and
7
+ // then keep running its own logic, so this settles a Promise when the child exits.
11
8
  //
12
- // ## Not calling `process.exit()` is what ARMED the hang, not what avoided it
13
- //
14
- // This header used to end "no process.exit, no GLib-mainloop intermingling
15
- // from `ensureMainLoop`". The second half was exactly backwards, and it is why
16
- // nobody looked here for #1010.
17
- //
18
- // `@gjsify/child_process`'s async `spawn()` calls `ensureMainLoop()`
19
- // unconditionally — the caller has no say. That arms a GJS main-loop hook whose
20
- // blocking `g_main_loop_run()` only `process.exit()` tears down. So a helper
21
- // that spawns asynchronously and deliberately does NOT exit leaves the loop
22
- // armed forever: `gjsify pack` on a package with a `prepack` wrote its tarball,
23
- // printed its complete JSON, and then parked at 0% CPU until killed at 5m30s.
24
- // A package with no lifecycle script exited in under a second, and the same
25
- // pack under Node exited in 0.68s.
26
- //
27
- // `utils/spawn.ts` already owns that contract and makes every call site DECLARE
28
- // which side of it they are on. This helper was the one spawn in the CLI that
29
- // bypassed it — the seventh of the "six near-identical wrappers" that file was
30
- // written to consolidate. It now goes through it with `completion: 'return'`,
31
- // which is what "settle, then keep running our own logic" means.
9
+ // NOT calling `process.exit()` is what ARMS a hang, not what avoids it (#1010):
10
+ // `@gjsify/child_process`'s async `spawn()` calls `ensureMainLoop()` unconditionally — the
11
+ // caller has no say — and that main-loop hook's blocking `g_main_loop_run()` is torn down only
12
+ // by `process.exit()`. A helper that spawns asynchronously and deliberately does not exit
13
+ // therefore leaves the loop armed forever: `gjsify pack` on a package with a `prepack` wrote
14
+ // its tarball, printed its complete JSON, then parked at 0% CPU until killed at 5m30s (no
15
+ // lifecycle script: under a second; the same pack under Node: 0.68s). `utils/spawn.ts` owns
16
+ // that contract and makes every call site DECLARE which side of it it is on; this goes through
17
+ // it with `completion: 'return'`.
32
18
  //
33
19
  // Matches yarn / npm script semantics:
34
20
  // - `shell: true` so `&&` / `|` / env-var refs work
35
21
  // - PATH prepended with `<wsDir>/node_modules/.bin` + monorepo-root bin
36
- // - `npm_lifecycle_event` / `npm_package_name` / `npm_package_version`
37
- // env vars set
22
+ // - `npm_lifecycle_event` / `npm_package_name` / `npm_package_version` set
38
23
  // - FORCE_COLOR=1 default unless caller overrides
39
24
  import { delimiter, join } from 'node:path';
40
25
  import { describeExit, spawnToCompletion } from './spawn.js';
41
26
  import { findWorkspaceRoot } from './workspace-root.js';
42
27
  /**
43
- * Run a lifecycle script defined in `pkg.scripts[name]` from `wsDir`.
44
- * Returns `true` if the script existed and exited 0. Returns `false` if
45
- * `optional: true` and the script is missing. Throws on non-zero exit.
28
+ * Run the lifecycle script `pkg.scripts[name]` from `wsDir`. `true` when it existed and exited
29
+ * 0, `false` when it is missing and `optional` is set; throws on a non-zero exit.
46
30
  */
47
31
  export async function runLifecycleScript(wsDir, pkg, name, opts = {}) {
48
32
  const scripts = pkg.scripts ?? {};
@@ -57,11 +41,9 @@ export async function runLifecycleScript(wsDir, pkg, name, opts = {}) {
57
41
  if (monorepoRoot && monorepoRoot !== wsDir) {
58
42
  binDirs.push(join(monorepoRoot, 'node_modules', '.bin'));
59
43
  }
60
- // Match yarn / npm color-forcing default — see runScript() in run.ts
61
- // for the full reasoning. Without this, lifecycle scripts that call
62
- // tools like biome / esbuild / tsc lose ANSI color in piped contexts
63
- // (CI logs, redirected output) because `process.stdout.isTTY` is
64
- // false for the spawned child.
44
+ // Match yarn / npm color-forcing — see `runScript()` in run.ts. Without it, scripts calling
45
+ // biome / esbuild / tsc lose ANSI color in piped contexts (CI logs, redirected output)
46
+ // because `process.stdout.isTTY` is false for the spawned child.
65
47
  const colorEnv = process.env.FORCE_COLOR !== undefined || process.env.NO_COLOR !== undefined ? {} : { FORCE_COLOR: '1' };
66
48
  const env = {
67
49
  ...process.env,
@@ -72,16 +54,15 @@ export async function runLifecycleScript(wsDir, pkg, name, opts = {}) {
72
54
  npm_package_version: pkg.version ?? '',
73
55
  ...opts.env,
74
56
  };
75
- // `'ignore'` has no `spawnToCompletion` equivalent and no call site in the
76
- // tree; folding it onto `'inherit'` would silently start printing where a
77
- // caller asked for silence, so it is rejected rather than approximated.
57
+ // `'ignore'` has no `spawnToCompletion` equivalent and no call site; folding it onto
58
+ // `'inherit'` would start printing where a caller asked for silence, so it is rejected
59
+ // rather than approximated.
78
60
  if (opts.stdio === 'ignore') {
79
61
  throw new Error('gjsify lifecycle-script: stdio "ignore" is not supported — see utils/spawn.ts');
80
62
  }
81
63
  const result = await spawnToCompletion(literal, [], {
82
- // The whole point of this helper: pack/publish waits for the script and
83
- // then keeps running. Under GJS that is the blocking path, which is what
84
- // keeps the armed main loop from outliving us — see the header.
64
+ // pack/publish waits for the script, then keeps running. Under GJS that is the blocking
65
+ // path, which is what keeps the armed main loop from outliving us — see the header.
85
66
  completion: 'return',
86
67
  cwd: wsDir,
87
68
  env: env,