@gjsify/cli 0.35.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 (124) hide show
  1. package/dist/cli.gjs.mjs +136 -126
  2. package/lib/actions/build.d.ts +19 -0
  3. package/lib/actions/build.js +2 -2
  4. package/lib/bundler-pick.js +72 -98
  5. package/lib/cli-fail.d.ts +4 -7
  6. package/lib/cli-fail.js +29 -55
  7. package/lib/commands/affected.js +161 -286
  8. package/lib/commands/install.d.ts +63 -66
  9. package/lib/commands/install.js +372 -536
  10. package/lib/commands/run.d.ts +1 -0
  11. package/lib/commands/run.js +37 -0
  12. package/lib/templates/install.mjs.tmpl +38 -68
  13. package/lib/types/cli-build-options.d.ts +33 -67
  14. package/lib/types/command.d.ts +9 -12
  15. package/lib/types/config-data.d.ts +125 -221
  16. package/lib/utils/auth-npmrc.d.ts +5 -10
  17. package/lib/utils/auth-npmrc.js +11 -21
  18. package/lib/utils/check-system-deps.d.ts +16 -0
  19. package/lib/utils/check-system-deps.js +49 -0
  20. package/lib/utils/clear-targets.d.ts +8 -11
  21. package/lib/utils/clear-targets.js +22 -29
  22. package/lib/utils/copy-targets.d.ts +13 -19
  23. package/lib/utils/copy-targets.js +31 -44
  24. package/lib/utils/dep-aggregation.d.ts +7 -15
  25. package/lib/utils/dep-aggregation.js +9 -22
  26. package/lib/utils/detect-native-packages.d.ts +83 -116
  27. package/lib/utils/detect-native-packages.js +111 -149
  28. package/lib/utils/dir-link.d.ts +29 -48
  29. package/lib/utils/dir-link.js +33 -56
  30. package/lib/utils/discover-showcases.d.ts +6 -7
  31. package/lib/utils/discover-showcases.js +11 -15
  32. package/lib/utils/dlx-cache.d.ts +29 -38
  33. package/lib/utils/dlx-cache.js +53 -63
  34. package/lib/utils/force-exit.d.ts +0 -3
  35. package/lib/utils/force-exit.js +13 -24
  36. package/lib/utils/gi-search-path.d.ts +16 -24
  37. package/lib/utils/gi-search-path.js +54 -82
  38. package/lib/utils/gjs-bundle-guard.d.ts +5 -13
  39. package/lib/utils/gjs-bundle-guard.js +35 -57
  40. package/lib/utils/gjs-source-escape.d.ts +2 -2
  41. package/lib/utils/gjs-source-escape.js +21 -39
  42. package/lib/utils/gjsify-shim.d.ts +26 -8
  43. package/lib/utils/gjsify-shim.js +128 -80
  44. package/lib/utils/html-entry.d.ts +8 -12
  45. package/lib/utils/html-entry.js +19 -28
  46. package/lib/utils/install-backend-native.d.ts +123 -161
  47. package/lib/utils/install-backend-native.js +536 -737
  48. package/lib/utils/install-backend.d.ts +42 -57
  49. package/lib/utils/install-backend.js +16 -21
  50. package/lib/utils/install-cache-fs.d.ts +10 -15
  51. package/lib/utils/install-cache-fs.js +18 -26
  52. package/lib/utils/install-lock.d.ts +5 -6
  53. package/lib/utils/install-lock.js +46 -65
  54. package/lib/utils/install-packument-cache.d.ts +27 -33
  55. package/lib/utils/install-packument-cache.js +44 -57
  56. package/lib/utils/install-progress.d.ts +2 -9
  57. package/lib/utils/install-progress.js +14 -39
  58. package/lib/utils/install-tarball-cache.d.ts +14 -20
  59. package/lib/utils/install-tarball-cache.js +41 -59
  60. package/lib/utils/library-output.d.ts +9 -22
  61. package/lib/utils/library-output.js +19 -37
  62. package/lib/utils/load-npmrc.d.ts +5 -6
  63. package/lib/utils/load-npmrc.js +18 -27
  64. package/lib/utils/node-bundle-guard.d.ts +4 -8
  65. package/lib/utils/node-bundle-guard.js +21 -35
  66. package/lib/utils/node-script.d.ts +12 -0
  67. package/lib/utils/node-script.js +95 -0
  68. package/lib/utils/node-version.d.ts +5 -14
  69. package/lib/utils/node-version.js +15 -24
  70. package/lib/utils/normalize-bundler-options.d.ts +7 -11
  71. package/lib/utils/normalize-bundler-options.js +24 -42
  72. package/lib/utils/npm-oidc.d.ts +23 -37
  73. package/lib/utils/npm-oidc.js +28 -53
  74. package/lib/utils/npm-otp-cache.d.ts +4 -4
  75. package/lib/utils/npm-otp-cache.js +20 -25
  76. package/lib/utils/npm-otp.d.ts +20 -30
  77. package/lib/utils/npm-otp.js +32 -51
  78. package/lib/utils/onboard-probe.d.ts +7 -13
  79. package/lib/utils/onboard-probe.js +16 -27
  80. package/lib/utils/parse-spec.d.ts +3 -3
  81. package/lib/utils/parse-spec.js +4 -5
  82. package/lib/utils/pkg-json-edit.d.ts +7 -12
  83. package/lib/utils/pkg-json-edit.js +13 -26
  84. package/lib/utils/pkg-json.d.ts +11 -18
  85. package/lib/utils/pkg-json.js +19 -28
  86. package/lib/utils/prefixed-output.d.ts +3 -4
  87. package/lib/utils/prefixed-output.js +13 -19
  88. package/lib/utils/prompt.d.ts +3 -4
  89. package/lib/utils/prompt.js +14 -18
  90. package/lib/utils/publish-diagnose.d.ts +11 -16
  91. package/lib/utils/publish-diagnose.js +15 -28
  92. package/lib/utils/publish-headers.d.ts +7 -11
  93. package/lib/utils/publish-headers.js +36 -65
  94. package/lib/utils/publishable-packages.d.ts +4 -6
  95. package/lib/utils/publishable-packages.js +14 -18
  96. package/lib/utils/resolve-gjs-entry.d.ts +5 -8
  97. package/lib/utils/resolve-gjs-entry.js +10 -27
  98. package/lib/utils/resolve-npm-package.d.ts +6 -11
  99. package/lib/utils/resolve-npm-package.js +29 -63
  100. package/lib/utils/resolve-plugin-by-name.d.ts +11 -19
  101. package/lib/utils/resolve-plugin-by-name.js +9 -14
  102. package/lib/utils/run-gjs.d.ts +19 -17
  103. package/lib/utils/run-gjs.js +37 -51
  104. package/lib/utils/run-lifecycle-script.d.ts +5 -9
  105. package/lib/utils/run-lifecycle-script.js +23 -42
  106. package/lib/utils/run-node.d.ts +23 -33
  107. package/lib/utils/run-node.js +56 -60
  108. package/lib/utils/runtimes.d.ts +25 -35
  109. package/lib/utils/runtimes.js +34 -46
  110. package/lib/utils/spawn.d.ts +32 -48
  111. package/lib/utils/spawn.js +51 -84
  112. package/lib/utils/stdin.d.ts +2 -2
  113. package/lib/utils/stdin.js +10 -25
  114. package/lib/utils/suggest.d.ts +6 -7
  115. package/lib/utils/suggest.js +8 -10
  116. package/lib/utils/system-gi.d.ts +18 -27
  117. package/lib/utils/system-gi.js +60 -114
  118. package/lib/utils/trust-registry.d.ts +4 -8
  119. package/lib/utils/trust-registry.js +7 -15
  120. package/lib/utils/win32-command.d.ts +8 -14
  121. package/lib/utils/win32-command.js +29 -43
  122. package/lib/utils/workspace-root.js +7 -12
  123. package/package.json +18 -17
  124. package/showcases.json +6 -0
@@ -1,39 +1,28 @@
1
1
  // The one way to end the CLI with a chosen exit code on BOTH runtimes.
2
2
  //
3
- // Node exits at natural shutdown once `process.exitCode` is set. GJS does not:
4
- // it has no atexit hook, and `process.exit()` there is not immediate — its
5
- // `exitProcess` IDLE-SCHEDULES `quitMainLoop()` + `imports.system.exit()` (see
6
- // `utils/spawn.ts` for the main-loop machinery this rides on). From inside an
7
- // asynchronous continuation — a `child.on('close'|'error')` handler, a
8
- // `setTimeout`, a promise callback — that idle can lose the race with the
9
- // module's own shutdown, and gjs then exits **0** no matter what code was
10
- // requested. `imports.system.exit` has no such gap: SpiderMonkey raises its
11
- // uncatchable exit exception from any context.
3
+ // Node exits at natural shutdown once `process.exitCode` is set. GJS has no
4
+ // atexit hook, and its `process.exit()` is not immediate — `exitProcess`
5
+ // idle-schedules `quitMainLoop()` + `imports.system.exit()`. From an async
6
+ // continuation (a `child.on('close'|'error')` handler, a `setTimeout`, a promise
7
+ // callback) that idle can lose the race with module shutdown, and gjs then exits
8
+ // **0** no matter what code was requested. `imports.system.exit` has no such
9
+ // gap: SpiderMonkey raises its uncatchable exit exception from any context.
12
10
  //
13
- // This is not theoretical. `gjsify tsc` spawned the compiler and ended both its
14
- // `close` and `error` handlers with a bare `process.exit(…)`. Under the GJS
15
- // bundle every one of those exits was dropped, so the command reported SUCCESS
11
+ // Incident: `gjsify tsc` ended its `close` and `error` handlers with a bare
12
+ // `process.exit(…)`, so under the GJS bundle the command reported SUCCESS
16
13
  // whatever the compiler did — a type error exited 0, and so did "no compiler
17
- // could be spawned at all". A release-cut on a cold tree then ran
18
- // `node scripts/process-template.mjs && gjsify tsc && node scripts/set-bin-mode.mjs`
19
- // to completion and died in `set-bin-mode.mjs` on a `lib/index.js` that `tsc`
20
- // had never written — pointing the blame two steps past the actual failure.
21
- //
22
- // Two copies of this logic already existed (`commands/install.ts`'s local
23
- // `forceExit`, `utils/oxc-resolve.ts`'s `setOxcExitCode`) and a third call site
24
- // simply did without. One exported helper is the point: a new async exit path
25
- // gets it right by importing it rather than by remembering the rule.
14
+ // could be spawned at all". A release-cut then died two steps later, in
15
+ // `set-bin-mode.mjs`, on a `lib/index.js` that `tsc` had never written.
26
16
  import { gjsExit } from '@gjsify/rolldown-plugin-gjsify/runtime';
27
17
  /**
28
18
  * Exit the process with `code`, honoured on Node and GJS alike.
29
19
  *
30
20
  * Safe to call from any context, including an async callback where a bare
31
21
  * `process.exit()` would be dropped under GJS. Does not return.
32
- *
33
- * `process.exitCode` is set FIRST so that a host which somehow reaches natural
34
- * shutdown anyway still reports the right code.
35
22
  */
36
23
  export function forceExit(code) {
24
+ // Set first, so a host that somehow reaches natural shutdown anyway still
25
+ // reports the right code.
37
26
  process.exitCode = code;
38
27
  // Under GJS this does not return — `imports.system.exit` unwinds via
39
28
  // SpiderMonkey's uncatchable exit exception.
@@ -3,32 +3,24 @@ import { type NativePackage } from './detect-native-packages.js';
3
3
  * Put every detected prebuild directory on girepository's typelib AND library
4
4
  * search paths. Idempotent — the first call decides, later ones return it.
5
5
  *
6
- * This is THE LAUNCHER'S ENVIRONMENT, APPLIED TO THE PROCESS THAT IS ALREADY
7
- * RUNNING: it feeds on the same `detectNativePackages()` whose output the
8
- * `gjsify` bin turns into `GI_TYPELIB_PATH` + the host library-path variable
9
- * (`buildNativeEnv`), so the two mechanisms cannot resolve different sets. That
10
- * symmetry is also why this is not per-bridge: there is no list of covered
11
- * engines to maintain, and a bridge added later is covered without touching
12
- * this file. Today it covers all three the CLI loads in-process —
13
- * `rolldown-native`, `lightningcss-native` (via the `css-as-string` plugin) and
14
- * `oxfmt-native`.
6
+ * The launcher's environment applied to the already-running process: it feeds on
7
+ * the same `detectNativePackages()` whose output the `gjsify` bin turns into
8
+ * `GI_TYPELIB_PATH` + the host library-path variable (`buildNativeEnv`), so the two
9
+ * mechanisms cannot resolve different sets and a bridge added later needs no change
10
+ * here.
15
11
  *
16
- * TWO ANCHORS, merged first-wins exactly as `computeNativeEnvForBundle()` does
17
- * it: the cwd, and the directory this module itself lives in. The second is not
18
- * redundant — with a GLOBAL CLI the engine sits next to the bundle while the cwd
19
- * is an unrelated project, which is the same case `tryLoadNative()` documents
20
- * needing its `bundleUrl` anchor for. After bundling, every CLI module collapses
21
- * into `dist/cli.gjs.mjs`, so `import.meta.url` is the bundle's own URL, which
22
- * is precisely the "where does the CLI live" question being asked.
12
+ * TWO ANCHORS, merged first-wins as `computeNativeEnvForBundle()` does: the cwd, and
13
+ * this module's own directory. The second is not redundant — with a GLOBAL CLI the
14
+ * engine sits next to the bundle while the cwd is an unrelated project (the case
15
+ * `tryLoadNative()` needs its `bundleUrl` anchor for), and after bundling
16
+ * `import.meta.url` is the bundle's own URL.
23
17
  *
24
- * Called from the native-load paths, which are themselves memoized, so the
25
- * node_modules sweep (~65 ms per anchor on this 960-workspace tree, far less in
26
- * a consumer) is paid at most once per process and only by a command that
27
- * actually wants an engine. Deliberately NOT skipped when the launcher already
28
- * exported the same directories: that check can be wrong, prepending a
29
- * directory that already resolves changes nothing, and a guard whose job is
30
- * watching another mechanism is the shape `docs/governance.md` § simplicity
31
- * warns about.
18
+ * Callers are memoized, so the node_modules sweep (~65 ms per anchor on this
19
+ * 960-workspace tree) is paid at most once per process, by a command that wants an
20
+ * engine. Deliberately NOT skipped when the launcher already exported the same
21
+ * directories: that check can be wrong, prepending an already-resolving directory
22
+ * changes nothing, and a guard watching another mechanism is the shape
23
+ * `docs/governance.md` § simplicity warns about.
32
24
  *
33
25
  * @returns the packages activated — empty when there is no repository to write
34
26
  * to (Node, or GLib < 2.86), which every caller reads as "carry on".
@@ -1,9 +1,8 @@
1
1
  // Make the native prebuilds this process can see resolvable to the RUNNING GJS
2
2
  // process, with nothing exported into the environment beforehand. ADR 0021.
3
3
  //
4
- // THE MEASUREMENT (gjs 1.88.1 / GLib 2.88.3, importing `gi://GjsifyRolldown`
5
- // from a bare `gjs -m`) — there are TWO lookups, not one, and they fail
6
- // differently:
4
+ // There are TWO lookups, not one, and they fail differently (measured on gjs
5
+ // 1.88.1 / GLib 2.88.3, importing `gi://GjsifyRolldown` from a bare `gjs -m`):
7
6
  //
8
7
  // neither var "Typelib file for namespace 'GjsifyRolldown' (any
9
8
  // version) not found"
@@ -12,30 +11,22 @@
12
11
  // "Unsupported type void, deriving from fundamental void"
13
12
  // both loads
14
13
  //
15
- // The received answer was "the typelib lookup happens inside the GJS runtime,
16
- // so those must be set BEFORE the process starts — the CLI cannot repair it
17
- // from the inside". Half of that is permanently true: `LD_LIBRARY_PATH` cannot
18
- // be changed in-process because ld.so freezes its search path at startup, which
19
- // is an OS property and is why the `gjsify` launcher still exists for user
20
- // bundles.
21
- //
22
- // It is the wrong frame, because girepository delegates NEITHER lookup to the
23
- // loader's environment. It keeps a typelib search path AND a library search
24
- // path of its own, consults both before falling back to the system loader, and
25
- // both are writable at runtime. `prepend_library_path()` is the half nobody
26
- // went looking for: `LD_LIBRARY_PATH` was assumed to BE the mechanism when it
27
- // is only girepository's fallback — which is exactly what the middle row above
14
+ // girepository delegates NEITHER lookup to the loader's environment: it keeps a
15
+ // typelib search path AND a library search path of its own, consults both before
16
+ // falling back to the system loader, and both are writable at runtime.
17
+ // `LD_LIBRARY_PATH` is only girepository's fallback — which the middle row above
28
18
  // proves, since girepository's own library path starts empty and no environment
29
- // variable seeds it.
19
+ // variable seeds it. That variable genuinely cannot be changed in-process (ld.so
20
+ // freezes its search path at startup), which is why the `gjsify` launcher still
21
+ // exists for user bundles — but it is not this lookup's mechanism.
30
22
  //
31
- // NOT an rpath problem, and the rpath is not the fix. `libgjsifyrolldown.so`
32
- // already carries `RUNPATH=$ORIGIN` (every bridge's `meson.build` sets it so
33
- // the Vala library finds the Rust cdylib shipped beside it — on macOS nothing
34
- // sets `DYLD_LIBRARY_PATH`, so it is the only thing that does). It structurally
35
- // cannot help gjs FIND `libgjsifyrolldown.so` itself: the typelib names it by
36
- // bare SONAME, and `dlopen()` of a bare name searches the RPATH/RUNPATH of the
37
- // CALLING object — libgirepository, whose `$ORIGIN` is /usr/lib64. An rpath on
38
- // the callee is never consulted to find the callee, so no artifact changes.
23
+ // NOT an rpath problem, and the rpath is not the fix. `libgjsifyrolldown.so` already
24
+ // carries `RUNPATH=$ORIGIN` (every bridge's `meson.build` sets it so the Vala library
25
+ // finds the Rust cdylib beside it). That structurally cannot help gjs FIND
26
+ // `libgjsifyrolldown.so` itself: the typelib names it by bare SONAME, and `dlopen()`
27
+ // of a bare name searches the RPATH/RUNPATH of the CALLING object — libgirepository,
28
+ // whose `$ORIGIN` is /usr/lib64. An rpath on the callee is never consulted to find
29
+ // the callee, so no artifact changes.
39
30
  import { dirname } from 'node:path';
40
31
  import { fileURLToPath } from 'node:url';
41
32
  import { detectNativePackages } from './detect-native-packages.js';
@@ -43,18 +34,15 @@ import { detectNativePackages } from './detect-native-packages.js';
43
34
  * girepository's process-global default repository — the one GJS's own `gi://`
44
35
  * importer resolves against — or `null` when there is none to write to.
45
36
  *
46
- * `globalThis.imports?.gi` is both the GJS probe (`.oxlintrc.json` sanctions
47
- * exactly this spelling for a runtime probe) and the correct CONDITION, which
48
- * is why there is no `isGjs()` beside it: the question is "is there a
49
- * GIRepository to prepend to", and a Node host answers no all by itself. That
50
- * is the required no-op — on Node the CLI uses the npm `rolldown` crate and
51
- * never wants a prebuild.
37
+ * `globalThis.imports?.gi` is both the sanctioned GJS probe spelling
38
+ * (`.oxlintrc.json`) and the correct CONDITION, so no `isGjs()` beside it: the
39
+ * question is "is there a GIRepository to prepend to", and a Node host answers
40
+ * no by itself — the required no-op, since on Node the CLI uses the npm
41
+ * `rolldown` crate and never wants a prebuild.
52
42
  *
53
- * `dup_default()` is GLib >= 2.86. A capability probe rather than a try/catch
54
- * because this is a KNOWN host property with a defined answer, not an
55
- * exceptional one: an older GLib simply keeps today's behaviour (the launcher's
56
- * environment, or an unchanged diagnosis), so the change cannot make any host
57
- * worse than it is.
43
+ * `dup_default()` is GLib >= 2.86, probed as a capability rather than caught:
44
+ * an older GLib simply keeps today's behaviour (the launcher's environment, or
45
+ * an unchanged diagnosis), so no host can be made worse.
58
46
  */
59
47
  function defaultRepository() {
60
48
  const gi = globalThis.imports?.gi;
@@ -68,32 +56,24 @@ let activated = null;
68
56
  * Put every detected prebuild directory on girepository's typelib AND library
69
57
  * search paths. Idempotent — the first call decides, later ones return it.
70
58
  *
71
- * This is THE LAUNCHER'S ENVIRONMENT, APPLIED TO THE PROCESS THAT IS ALREADY
72
- * RUNNING: it feeds on the same `detectNativePackages()` whose output the
73
- * `gjsify` bin turns into `GI_TYPELIB_PATH` + the host library-path variable
74
- * (`buildNativeEnv`), so the two mechanisms cannot resolve different sets. That
75
- * symmetry is also why this is not per-bridge: there is no list of covered
76
- * engines to maintain, and a bridge added later is covered without touching
77
- * this file. Today it covers all three the CLI loads in-process —
78
- * `rolldown-native`, `lightningcss-native` (via the `css-as-string` plugin) and
79
- * `oxfmt-native`.
59
+ * The launcher's environment applied to the already-running process: it feeds on
60
+ * the same `detectNativePackages()` whose output the `gjsify` bin turns into
61
+ * `GI_TYPELIB_PATH` + the host library-path variable (`buildNativeEnv`), so the two
62
+ * mechanisms cannot resolve different sets and a bridge added later needs no change
63
+ * here.
80
64
  *
81
- * TWO ANCHORS, merged first-wins exactly as `computeNativeEnvForBundle()` does
82
- * it: the cwd, and the directory this module itself lives in. The second is not
83
- * redundant — with a GLOBAL CLI the engine sits next to the bundle while the cwd
84
- * is an unrelated project, which is the same case `tryLoadNative()` documents
85
- * needing its `bundleUrl` anchor for. After bundling, every CLI module collapses
86
- * into `dist/cli.gjs.mjs`, so `import.meta.url` is the bundle's own URL, which
87
- * is precisely the "where does the CLI live" question being asked.
65
+ * TWO ANCHORS, merged first-wins as `computeNativeEnvForBundle()` does: the cwd, and
66
+ * this module's own directory. The second is not redundant — with a GLOBAL CLI the
67
+ * engine sits next to the bundle while the cwd is an unrelated project (the case
68
+ * `tryLoadNative()` needs its `bundleUrl` anchor for), and after bundling
69
+ * `import.meta.url` is the bundle's own URL.
88
70
  *
89
- * Called from the native-load paths, which are themselves memoized, so the
90
- * node_modules sweep (~65 ms per anchor on this 960-workspace tree, far less in
91
- * a consumer) is paid at most once per process and only by a command that
92
- * actually wants an engine. Deliberately NOT skipped when the launcher already
93
- * exported the same directories: that check can be wrong, prepending a
94
- * directory that already resolves changes nothing, and a guard whose job is
95
- * watching another mechanism is the shape `docs/governance.md` § simplicity
96
- * warns about.
71
+ * Callers are memoized, so the node_modules sweep (~65 ms per anchor on this
72
+ * 960-workspace tree) is paid at most once per process, by a command that wants an
73
+ * engine. Deliberately NOT skipped when the launcher already exported the same
74
+ * directories: that check can be wrong, prepending an already-resolving directory
75
+ * changes nothing, and a guard watching another mechanism is the shape
76
+ * `docs/governance.md` § simplicity warns about.
97
77
  *
98
78
  * @returns the packages activated — empty when there is no repository to write
99
79
  * to (Node, or GLib < 2.86), which every caller reads as "carry on".
@@ -101,10 +81,9 @@ let activated = null;
101
81
  export function activateNativePrebuilds() {
102
82
  if (activated)
103
83
  return activated;
104
- // Memoized BEFORE the work, so every outcome — including a throw — is
105
- // decided once and every later caller gets an answer instead of a repeat
106
- // attempt. `diagnoseNativeEngine()` depends on exactly that: it documents
107
- // that nothing it calls may throw while explaining a failure.
84
+ // Memoized BEFORE the work, so every outcome — including a throw — is decided
85
+ // once. `diagnoseNativeEngine()` depends on that: nothing it calls may throw
86
+ // while explaining a failure.
108
87
  activated = [];
109
88
  try {
110
89
  const repository = defaultRepository();
@@ -121,26 +100,19 @@ export function activateNativePrebuilds() {
121
100
  activated = packages;
122
101
  }
123
102
  catch {
124
- // TWO REAL THROW PATHS, named because a catch without one is the
125
- // anti-pattern this repo measures:
126
- //
127
- // 1. `imports.gi.GIRepository` LOADS the namespace. GJS raises
128
- // "Requiring GIRepository, version none: Typelib file for namespace
129
- // 'GIRepository' (any version) not found" when that typelib is
130
- // absent — verified by probe. It is a SEPARATE FILE from the
131
- // girepository library GJS links against, and distributions split
132
- // them (Debian ships `gir1.2-girepository-3.0` apart from
103
+ // Two real throw paths:
104
+ // 1. `imports.gi.GIRepository` LOADS the namespace, and that typelib is
105
+ // a SEPARATE FILE from the girepository library GJS links against —
106
+ // distributions split them (Debian's `gir1.2-girepository-3.0` vs
133
107
  // `libgirepository-2.0-0`), so a lean host can have the second
134
- // without the first.
135
- // 2. The two `detectNativePackages()` walks are filesystem I/O and can
136
- // raise (EACCES on an unreadable node_modules, a dir removed
137
- // mid-walk).
108
+ // without the first and GJS raises "Typelib file for namespace
109
+ // 'GIRepository' (any version) not found".
110
+ // 2. The two `detectNativePackages()` walks are filesystem I/O (EACCES
111
+ // on an unreadable node_modules, a dir removed mid-walk).
138
112
  //
139
- // Both mean the same thing — no activation — and swallowing is the
140
- // CORRECT handling rather than a hidden failure: the caller ends up
141
- // exactly where it was before this function existed (the launcher's
142
- // environment, and the unchanged diagnosis if that is absent too), so
143
- // no host can be made worse by it.
113
+ // Both mean no activation, and swallowing leaves the caller exactly where
114
+ // it was before this function existed (the launcher's environment, or the
115
+ // unchanged diagnosis), so no host can be made worse by it.
144
116
  }
145
117
  return activated;
146
118
  }
@@ -8,14 +8,11 @@ export interface GuardedChunk {
8
8
  /**
9
9
  * Bare `node:` specifiers a `--app gjs` bundle may keep as static imports.
10
10
  *
11
- * Deliberately EMPTY. Every Node builtin has an `@gjsify/*` polyfill that
12
- * `--app gjs` aliases to, so there is no specifier that both survives into the
13
- * module graph and loads under GJS. Reading the graph instead of the text is
14
- * what keeps this list empty — the entries an earlier text-scanning version
15
- * needed (`node:module`, `node:process`) were quoted lookalikes, never imports.
11
+ * Deliberately EMPTY: every Node builtin has an `@gjsify/*` polyfill that `--app gjs`
12
+ * aliases to, so no specifier both survives into the module graph and loads under GJS.
16
13
  *
17
- * Add an entry ONLY with a comment naming why GJS can resolve it. Each one is a
18
- * specifier this guard can no longer catch for real.
14
+ * Add an entry ONLY with a comment naming why GJS can resolve it. Each one is a specifier
15
+ * this guard can no longer catch for real.
19
16
  */
20
17
  export declare const GJS_ALLOWED_NODE_IMPORTS: readonly string[];
21
18
  /**
@@ -23,10 +20,5 @@ export declare const GJS_ALLOWED_NODE_IMPORTS: readonly string[];
23
20
  * allowlist. Sorted, so the error message and any test are stable.
24
21
  */
25
22
  export declare function findDisallowedNodeImports(chunks: readonly GuardedChunk[], allowed?: readonly string[]): string[];
26
- /**
27
- * Throw when a built `--app gjs` bundle statically imports Node builtins.
28
- *
29
- * @param chunks the emitted chunks, as reported by the bundler
30
- * @param outfile path of the bundle, for the error message
31
- */
23
+ /** Throw when a built `--app gjs` bundle statically imports Node builtins. */
32
24
  export declare function assertGjsBundleLoadable(chunks: readonly GuardedChunk[], outfile: string): void;
@@ -1,67 +1,50 @@
1
1
  // Build-time guard: a `--app gjs` bundle must not keep bare `node:` imports.
2
2
  //
3
- // Stock GJS has NO `node:` module loader. A single surviving
4
- // `import … from "node:fs"` makes it abort the ENTIRE module graph before one
5
- // line executes:
3
+ // Stock GJS has NO `node:` module loader. A single surviving `import … from "node:fs"`
4
+ // makes it abort the ENTIRE module graph before one line executes, with
5
+ // "ImportError: Unsupported URI scheme for importing: node".
6
6
  //
7
- // Gjs-WARNING **: JS ERROR: ImportError: Unsupported URI scheme for importing: node
8
- // Gjs-CRITICAL **: Failed to resolve imports for module: '…/dist/cli.gjs.mjs'
7
+ // `--app gjs` maps every Node builtin to its `@gjsify/*` polyfill, so a healthy bundle has
8
+ // ZERO of them. But when an alias TARGET cannot be resolved — classically a workspace
9
+ // checkout whose `@gjsify/*` packages have no built `lib/`, so `exports["."]` points at a
10
+ // file that does not exist — the bundler falls back to external and re-emits the ORIGINAL
11
+ // `node:*` specifier. That is a WARNING, not an error: the build "succeeds" and writes a
12
+ // bundle that cannot load anywhere. It cost three red CI jobs to notice, because nothing
13
+ // between `gjsify build` and CI ever executes the emitted GJS bundle (tsc, lint, format and
14
+ // the unit/e2e suites all run the Node entry). The pre-commit hook makes it worse rather
15
+ // than better: it rebuilds and AUTO-STAGES `dist/affected.gjs.mjs`, built through this same
16
+ // `--app gjs` path, so a developer whose checkout cannot resolve the polyfills commits a
17
+ // broken artifact without ever seeing it.
9
18
  //
10
- // `--app gjs` maps every Node builtin to its `@gjsify/*` polyfill, so a healthy
11
- // bundle has ZERO of them. But when an alias TARGET cannot be resolved — the
12
- // classic case being a workspace checkout whose `@gjsify/*` packages have no
13
- // built `lib/`, so `exports["."]` points at a file that does not exist — the
14
- // bundler falls back to treating the import as external and re-emits the
15
- // ORIGINAL `node:*` specifier. That is a WARNING, not an error: the build
16
- // "succeeds" and writes a bundle that cannot load anywhere. It cost three red
17
- // CI jobs to notice, because nothing between `gjsify build` and CI ever
18
- // executes the emitted GJS bundle (tsc, lint, format and the unit/e2e suites
19
- // all run the Node entry).
20
- //
21
- // The pre-commit hook makes it worse rather than better: it rebuilds and
22
- // AUTO-STAGES `dist/cli.gjs.mjs`, so a developer whose checkout cannot resolve
23
- // the polyfills commits a broken artifact without ever seeing it.
24
- //
25
- // So the refusal belongs at build time, in `gjsify build --app gjs` itself —
26
- // not in a git hook and not in one package's `build:gjs-bundle` script. The
27
- // invariant is a property of the ARTIFACT, and every showcase, every downstream
28
- // app (buchhaltung, bauplaner, Learn6502) and CI all produce it through this
29
- // one code path. A hook would protect only commits in this repo; a per-package
30
- // script would protect one package.
19
+ // So the refusal belongs at build time, in `gjsify build --app gjs` itself — not in a git
20
+ // hook and not in one package's `build:gjs-bundle` script. The invariant is a property of
21
+ // the ARTIFACT, and every showcase, every downstream app (buchhaltung, bauplaner,
22
+ // Learn6502) and CI all produce it through this one code path.
31
23
  //
32
24
  // ## The oracle is the module graph, NOT the emitted text
33
25
  //
34
- // The first cut of this guard grepped the output for `from "node:…"`. That is
35
- // wrong, and CI caught it: `@gjsify/process`'s own test bundle names a case
36
- //
37
- // await it(`named-import hrtime (from "node:process") preserves .bigint`, …)
38
- //
39
- // so the phrase appears verbatim inside a STRING, in a bundle that loads and
40
- // runs under GJS perfectly well. The same trap sits in `@gjsify/cli`'s own
26
+ // The first cut grepped the output for `from "node:…"`, and CI caught it:
27
+ // `@gjsify/process`'s own test bundle names a case `named-import hrtime (from
28
+ // "node:process") preserves .bigint`, so the phrase appears verbatim inside a STRING in a
29
+ // bundle that runs under GJS perfectly well. The same trap sits in `@gjsify/cli`'s own
41
30
  // bundle, which embeds `giNodeShimSource()`'s `--app node` codegen template
42
- // (`import { createRequire } from 'node:module';`). Telling a statement from a
43
- // quoted lookalike needs a JS lexer — but the bundler already did that work:
44
- // every chunk carries the list of external specifiers it actually imports. That
45
- // list is exact, engine-independent (npm `rolldown` returns it directly,
46
- // `@gjsify/rolldown-native` forwards it through `synthRolldownOutput`), and
47
- // free.
31
+ // (`import { createRequire } from 'node:module';`). Telling a statement from a quoted
32
+ // lookalike needs a JS lexer — but the bundler already did that work: every chunk carries
33
+ // the external specifiers it actually imports, exact and engine-independent (npm `rolldown`
34
+ // returns it directly, `@gjsify/rolldown-native` forwards it via `synthRolldownOutput`).
48
35
  //
49
- // Scope is STATIC imports, which is precisely the failure mode above: they are
50
- // resolved when GJS links the module, so one bad specifier kills the whole
51
- // bundle. A dynamic `import('node:x')` throws lazily at its call site instead —
52
- // a real bug, but not "the artifact cannot load", and not what this gate is
53
- // for.
36
+ // Scope is STATIC imports, which is the failure mode above: they are resolved when GJS
37
+ // links the module, so one bad specifier kills the whole bundle. A dynamic
38
+ // `import('node:x')` throws lazily at its call site — a real bug, but not "the artifact
39
+ // cannot load", and not what this gate is for.
54
40
  /**
55
41
  * Bare `node:` specifiers a `--app gjs` bundle may keep as static imports.
56
42
  *
57
- * Deliberately EMPTY. Every Node builtin has an `@gjsify/*` polyfill that
58
- * `--app gjs` aliases to, so there is no specifier that both survives into the
59
- * module graph and loads under GJS. Reading the graph instead of the text is
60
- * what keeps this list empty — the entries an earlier text-scanning version
61
- * needed (`node:module`, `node:process`) were quoted lookalikes, never imports.
43
+ * Deliberately EMPTY: every Node builtin has an `@gjsify/*` polyfill that `--app gjs`
44
+ * aliases to, so no specifier both survives into the module graph and loads under GJS.
62
45
  *
63
- * Add an entry ONLY with a comment naming why GJS can resolve it. Each one is a
64
- * specifier this guard can no longer catch for real.
46
+ * Add an entry ONLY with a comment naming why GJS can resolve it. Each one is a specifier
47
+ * this guard can no longer catch for real.
65
48
  */
66
49
  export const GJS_ALLOWED_NODE_IMPORTS = [];
67
50
  /**
@@ -81,12 +64,7 @@ export function findDisallowedNodeImports(chunks, allowed = GJS_ALLOWED_NODE_IMP
81
64
  }
82
65
  return [...found].sort();
83
66
  }
84
- /**
85
- * Throw when a built `--app gjs` bundle statically imports Node builtins.
86
- *
87
- * @param chunks the emitted chunks, as reported by the bundler
88
- * @param outfile path of the bundle, for the error message
89
- */
67
+ /** Throw when a built `--app gjs` bundle statically imports Node builtins. */
90
68
  export function assertGjsBundleLoadable(chunks, outfile) {
91
69
  const offenders = findDisallowedNodeImports(chunks);
92
70
  if (offenders.length === 0)
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Replace every raw U+0000 in `code` with the `\x00` escape.
3
3
  *
4
- * Returns the rewritten code and how many were replaced, so the caller can skip
5
- * writing the file (and reporting) in the overwhelmingly common zero case.
4
+ * Reports how many were replaced so the caller can skip writing the file (and reporting)
5
+ * in the overwhelmingly common zero case.
6
6
  */
7
7
  export declare function escapeRawNulForGjs(code: string): {
8
8
  code: string;
@@ -1,49 +1,31 @@
1
1
  // Build-time rewrite: a `--app gjs` bundle must not contain a raw U+0000 byte.
2
2
  //
3
- // GJS hands module source to SpiderMonkey as a NUL-TERMINATED C string, so a raw
4
- // U+0000 in the emitted file truncates it at that point. Everything after is
5
- // silently discarded and the error names whatever construct happened to be open
6
- // when the text ran out:
7
- //
8
- // SyntaxError: `` literal not terminated before end of script
9
- //
10
- // which points at the wrong place entirely and never mentions a NUL.
11
- //
12
- // This is easy to hit and hard to diagnose. `'\u0000'` in TypeScript source is an
13
- // ESCAPE — six harmless ASCII characters — but the minifier is free to inline the
14
- // constant and emit the CHARACTER:
15
- //
16
- // const P = '\u0000'; … return `${P}${i}${P}` // source: escaped, loads fine
17
- // … return`<NUL>${e}<NUL>` // minified: raw byte, will not load
18
- //
19
- // so the same code builds and runs unminified and breaks under `--minify`, which is
20
- // the default. A real consumer hit exactly this with an IMAP literal marker — NUL
21
- // being the one byte that cannot occur in IMAP response text is precisely why it
22
- // was chosen as the marker.
23
- //
24
- // Node and browsers load a raw NUL in source without complaint (U+0000 is a legal
25
- // source character per the spec), so no other target notices and nothing upstream
26
- // is going to change. Escaping it on the way out is gjsify's job.
27
- //
28
- // The rewrite is safe as a plain text substitution: in valid JS a raw NUL can only
29
- // appear inside a string, a template literal, a regex or a comment, and `\x00`
30
- // denotes exactly U+0000 in all four — so the meaning is preserved wherever it sits.
31
- //
32
- // `\x00` and NOT `\0`: `\0` followed by a digit is a legacy octal escape, which is a
33
- // SyntaxError in a template literal and under strict mode. A digit right after the
34
- // marker is exactly the shape that triggered this (`\0${index}\0`), so `\0` would
35
- // trade one unloadable bundle for another.
3
+ // GJS hands module source to SpiderMonkey as a NUL-TERMINATED C string, so a raw U+0000
4
+ // truncates the file there. Everything after is silently discarded and the error names
5
+ // whatever construct happened to be open when the text ran out — "SyntaxError: `` literal
6
+ // not terminated before end of script" — never the NUL.
7
+ //
8
+ // Easy to hit by accident: `'\u0000'` in TypeScript source is a six-character ESCAPE, but
9
+ // the minifier may inline the constant and emit the CHARACTER, so the same code runs
10
+ // unminified and breaks under `--minify` (the default). A consumer hit this with an IMAP
11
+ // literal marker — NUL cannot occur in IMAP response text, which is why it was the marker.
12
+ // Node and browsers load a raw NUL in source per spec, so no other target notices and
13
+ // nothing upstream will change; escaping it on the way out is gjsify's job.
14
+ //
15
+ // Safe as a plain text substitution: in valid JS a raw NUL can only sit inside a string, a
16
+ // template literal, a regex or a comment, and `\x00` denotes U+0000 in all four. NOT `\0`,
17
+ // which is a legacy octal escape when followed by a digit — a SyntaxError in a template
18
+ // literal and under strict mode, and `\0${index}\0` is exactly the shape that triggered it.
36
19
  /**
37
20
  * Replace every raw U+0000 in `code` with the `\x00` escape.
38
21
  *
39
- * Returns the rewritten code and how many were replaced, so the caller can skip
40
- * writing the file (and reporting) in the overwhelmingly common zero case.
22
+ * Reports how many were replaced so the caller can skip writing the file (and reporting)
23
+ * in the overwhelmingly common zero case.
41
24
  */
42
25
  export function escapeRawNulForGjs(code) {
43
- // split/join rather than a regex: a NUL inside a regex is exactly what oxlint's
44
- // `no-control-regex` exists to flag, and the rule is right in general even though this use
45
- // is deliberate. Building the needle with fromCharCode also keeps THIS file free of the raw
46
- // byte it exists to remove.
26
+ // split/join rather than a regex: a NUL inside a regex is what oxlint's `no-control-regex`
27
+ // exists to flag, and the rule is right in general even though this use is deliberate.
28
+ // fromCharCode also keeps THIS file free of the raw byte it exists to remove.
47
29
  const parts = code.split(String.fromCharCode(0));
48
30
  return { code: parts.join('\\x00'), replaced: parts.length - 1 };
49
31
  }
@@ -3,18 +3,18 @@
3
3
  * a self-shim rather than through the workspace's `node_modules/.bin/gjsify`.
4
4
  *
5
5
  * `detectPackageManager()` reads this: a bootstrap CLI must run package scripts
6
- * ITSELF, because npm re-prepends `node_modules/.bin` for every lifecycle
7
- * script it starts — which puts the tree's dead shim back ahead of the shim dir
8
- * this module placed on PATH, and undoes the whole repair one level down.
6
+ * ITSELF, because npm re-prepends `node_modules/.bin` for every lifecycle script it
7
+ * starts — putting the tree's dead shim back ahead of this module's shim dir and
8
+ * undoing the repair one level down.
9
9
  */
10
10
  export declare function usingSelfShim(): boolean;
11
11
  /**
12
- * Does this invocation need its own `gjsify` on PATH, rather than the
13
- * workspace's npm bin?
12
+ * Does this invocation need its own `gjsify` on PATH, rather than the workspace's
13
+ * npm bin?
14
14
  *
15
- * Pure, and takes the host facts as PARAMETERS rather than reading them
16
- * ambiently — the same shape `platform-check.ts` documents, and what lets the
17
- * Windows-relevant branch be tested from a Linux host.
15
+ * Host facts are PARAMETERS rather than read ambiently — the shape
16
+ * `platform-check.ts` documents, and what lets the Windows branch be tested from
17
+ * a Linux host.
18
18
  *
19
19
  * @param gjs running under GJS (the npm bin is the Node entry there)
20
20
  * @param selfEntry absolute path of the CLI entry currently executing
@@ -31,3 +31,21 @@ export declare function needsSelfShim(opts: {
31
31
  * reuse it instead of creating their own.
32
32
  */
33
33
  export declare function ensureGjsifyShimOnPath(): void;
34
+ /**
35
+ * Directory carrying a `node` that runs the file through
36
+ * `gjsify run --node-script`, or null when this host needs none.
37
+ *
38
+ * **It is NOT on `process.env.PATH`, and that is the whole point.** Only
39
+ * `runScript` puts it on the PATH of a PACKAGE SCRIPT's child (`commands/run.ts`).
40
+ * Prepending it globally breaks the CLI's ability to SEE a missing Node: `gjsify
41
+ * tsc`'s Node fallback spawns a real `node` for typescript's CJS entry, and
42
+ * `tests/e2e/tsc-node-fallback` asserts that on a PATH without one it fails loudly
43
+ * naming the missing interpreter — an assertion that exists because a silent exit
44
+ * 0 there once let `set-bin-mode.mjs` run against a `lib/index.js` tsc never wrote.
45
+ *
46
+ * The line the subdirectory draws: a PACKAGE SCRIPT saying `node x.mjs` means "run
47
+ * this script", which gjsify can serve; the CLI's own internals asking for `node`
48
+ * mean "I need a real Node", and lying to them turns an honest diagnosis into a
49
+ * confusing one two layers down.
50
+ */
51
+ export declare function nodeShimDir(): string | null;