@gjsify/rolldown-plugin-gjsify 0.51.1 → 0.53.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 (44) hide show
  1. package/README.md +11 -0
  2. package/lib/app/browser.js +28 -0
  3. package/lib/app/gjs.js +14 -2
  4. package/lib/app/nativescript.js +15 -3
  5. package/lib/app/node.d.ts +1 -1
  6. package/lib/app/node.js +26 -10
  7. package/lib/index.d.ts +4 -2
  8. package/lib/index.js +4 -2
  9. package/lib/plugins/console-assign.d.ts +13 -0
  10. package/lib/plugins/console-assign.js +125 -0
  11. package/lib/plugins/css-as-string.js +126 -44
  12. package/lib/plugins/gi-renderer.d.ts +1 -1
  13. package/lib/plugins/gi-renderer.js +1 -1
  14. package/lib/plugins/gi-runtime-paths.js +5 -2
  15. package/lib/plugins/napi-node-addon.d.ts +48 -4
  16. package/lib/plugins/napi-node-addon.js +261 -85
  17. package/lib/plugins/node-native-external.d.ts +20 -0
  18. package/lib/plugins/node-native-external.js +145 -0
  19. package/lib/plugins/platform-resolve.d.ts +95 -5
  20. package/lib/plugins/platform-resolve.js +132 -11
  21. package/lib/plugins/rewrite-node-modules-paths.d.ts +18 -1
  22. package/lib/plugins/rewrite-node-modules-paths.js +181 -25
  23. package/lib/plugins/unresolved-workspace-import.js +89 -68
  24. package/lib/shims/addon-resolve.d.ts +19 -0
  25. package/lib/shims/addon-resolve.js +193 -0
  26. package/lib/utils/addon-platform.d.ts +23 -0
  27. package/lib/utils/addon-platform.js +50 -0
  28. package/lib/utils/auto-globals.d.ts +6 -0
  29. package/lib/utils/auto-globals.js +92 -15
  30. package/lib/utils/declare-build-input.d.ts +13 -0
  31. package/lib/utils/declare-build-input.js +37 -0
  32. package/lib/utils/detect-free-globals.d.ts +3 -0
  33. package/lib/utils/detect-free-globals.js +1 -1
  34. package/lib/utils/entry-wrapper.js +6 -1
  35. package/lib/utils/index.d.ts +1 -0
  36. package/lib/utils/index.js +1 -0
  37. package/lib/utils/inline-static-reads.d.ts +15 -1
  38. package/lib/utils/inline-static-reads.js +16 -8
  39. package/lib/utils/runtime.js +1 -1
  40. package/lib/utils/scan-globals.d.ts +2 -2
  41. package/lib/utils/scan-globals.js +20 -14
  42. package/lib/utils/zip-path.d.ts +9 -0
  43. package/lib/utils/zip-path.js +12 -0
  44. package/package.json +13 -9
package/README.md CHANGED
@@ -40,6 +40,17 @@ import {
40
40
  } from '@gjsify/rolldown-plugin-gjsify';
41
41
  ```
42
42
 
43
+ ## Native addons (`--app gjs`)
44
+
45
+ A Node-API addon package (`bufferutil`, `better-sqlite3`, `lightningcss`, `@signalapp/libsignal-client`, …) is rewritten to `loadAddon()` from `@gjsify/napi`, which GJS cannot load by itself. On `--app node` the same packages stay external and keep resolving from `node_modules`.
46
+
47
+ For a GJS build the bundle records the addon's **package identity**, not a path — see [ADR 0084](../../docs/adr/0084-an-addon-is-found-by-package-identity.md). Every `.node` the package ships is enumerated at build time, keyed by platform (`linux-x64`, `linux-x64-musl`, `darwin-arm64`, …), and the bundle picks the entry for the host it finds itself on when it loads. Consequences worth knowing:
48
+
49
+ - The bundle is **relocatable** — move the output tree and it still loads its addon, because nothing names the machine that built it.
50
+ - The addon **package** must be installed somewhere the bundle can reach (its own `node_modules`, or an `addons/<package>/` next to the bundle that no gjsify step fills yet). A single copied `.node` file is not enough; a `.node` that `dlopen`s a sibling library or reads a data file beside itself needs its directory.
51
+ - A cross-build works when the target's binary is installed on the builder: a multi-platform npm package, or `gjsify install`, which materialises every platform package.
52
+ - `runtimeResolve` (set from `format === 'esm'` by `gjsify build`) gates the whole mechanism: without the bundle-URL banner there is nothing to anchor on, so the rewrite is declined with a warning rather than emitting a path that would only be right on the build machine.
53
+
43
54
  ## License
44
55
 
45
56
  MIT
@@ -1,5 +1,9 @@
1
1
  // `--app browser` Rolldown configuration factory.
2
2
  //
3
+ // Platform-file forks resolve through `plugins/platform-resolve.ts` on the
4
+ // BROWSER chain (ADR 0032 § 9): `./foo.web.<ext>` ahead of `./foo.<ext>`, one
5
+ // rung, with `.gtk` / `.desktop` / `.native` refused and warned about.
6
+ //
3
7
  // Browser builds redirect `@girs/*` and `gi://*` to an empty virtual module
4
8
  // (they appear transitively via `@gjsify/unit` and similar packages with
5
9
  // GJS-specific code paths) — unless `--gi-renderer` composes the ADR 0034
@@ -22,6 +26,7 @@ import blueprintPlugin from '@gjsify/vite-plugin-blueprint';
22
26
  import { ALIASES_NODE_FOR_BROWSER, GI_RENDERERS, getDerivedAliasesSync } from '@gjsify/resolve-npm';
23
27
  import { globToEntryPoints } from '../utils/entry-points.js';
24
28
  import { gjsImportsEmptyPlugin } from '../plugins/gjs-imports-empty.js';
29
+ import { platformResolvePlugin, browserSuffixChain, BROWSER_REFUSED_SUFFIXES } from '../plugins/platform-resolve.js';
25
30
  import { giRendererPlugin } from '../plugins/gi-renderer.js';
26
31
  import { cssAsStringPlugin } from '../plugins/css-as-string.js';
27
32
  import { unresolvedWorkspaceImportPlugin } from '../plugins/unresolved-workspace-import.js';
@@ -78,6 +83,7 @@ export const setupForBrowser = async (input) => {
78
83
  define: {
79
84
  global: 'globalThis',
80
85
  window: 'globalThis',
86
+ 'process.env.NODE_ENV': '"production"',
81
87
  },
82
88
  },
83
89
  output: {
@@ -98,6 +104,22 @@ export const setupForBrowser = async (input) => {
98
104
  // transform can strip the annotations out from under it (see `GjsifyConfig`).
99
105
  const prePlugins = [deepkitPlugin({ reflection: input.pluginOptions.reflection })];
100
106
  const plugins = [
107
+ // Platform-file forks for the browser, ADR 0032 § 9: `.web` → base. One
108
+ // rung, and `browserSuffixChain`'s doc comment carries why each candidate
109
+ // second rung is absent. FIRST in the array for the reason `app/gjs.ts`
110
+ // states: the plugin claims RELATIVE imports only, and a platform fork of
111
+ // a module that also has a Node-builtin substitution must win over the
112
+ // substitution, because the fork is the more specific statement. `.gtk`,
113
+ // `.desktop` and `.native` are deliberately not rungs here and are warned
114
+ // about when present; `BROWSER_REFUSED_SUFFIXES` carries the reason.
115
+ // `siblingIndex: true` as on the desktop chain — same population (first-
116
+ // party relative imports), same directory-listing filter, and without it
117
+ // a one-rung chain plus three refusals is four failed resolves per import.
118
+ platformResolvePlugin({
119
+ suffixes: browserSuffixChain(),
120
+ refusedSuffixes: BROWSER_REFUSED_SUFFIXES,
121
+ siblingIndex: true,
122
+ }),
101
123
  // ADR 0034 stage 9 — the `gi://` arm, ahead of the empty redirect so it
102
124
  // claims the specifier first, exactly as `gjsGiNodePlugin` does on the node
103
125
  // target. `emptyGirs` follows it: with the arm on, `@girs/<ns>-<ver>` must
@@ -106,6 +128,12 @@ export const setupForBrowser = async (input) => {
106
128
  ...(giRenderer ? [giRendererPlugin({ app: 'browser', ...giRenderer })] : []),
107
129
  gjsImportsEmptyPlugin({ emptyGirs: !giRenderer }),
108
130
  aliasPlugin({ entries: aliasEntries }),
131
+ // Blueprint has been registered here since it was registered anywhere, and until the
132
+ // `?shared-tree` exit existed the XML string it emitted had no reader on this target:
133
+ // a browser has no `Gtk.Builder`, so `import Template from './x.blp'` compiled, shipped
134
+ // its bytes and was never parsed by anything. It stays — a `--app browser` build of a
135
+ // GJS app's sources must not start failing on an import that used to resolve — and the
136
+ // exit a browser can actually use is now beside it.
109
137
  blueprintPlugin(),
110
138
  cssAsStringPlugin(),
111
139
  // `order: 'post'` — see app/gjs.ts. The browser target's whole job is to
package/lib/app/gjs.js CHANGED
@@ -19,6 +19,7 @@ import { globToEntryPoints } from '../utils/entry-points.js';
19
19
  import { nodeModulesPathRewritePlugin, getBundleDirFromOutput } from '../plugins/rewrite-node-modules-paths.js';
20
20
  import { processStubPlugin } from '../plugins/process-stub.js';
21
21
  import { cssAsStringPlugin } from '../plugins/css-as-string.js';
22
+ import { consoleAssignPlugin } from '../plugins/console-assign.js';
22
23
  import { shebangPlugin, resolveShebangLine, inputShebangStripPlugin } from '../plugins/shebang.js';
23
24
  import { wrapInputWithSideEffects } from '../utils/entry-wrapper.js';
24
25
  const _shimDir = dirname(fileURLToPath(import.meta.url));
@@ -132,8 +133,13 @@ export const setupForGjs = async (input) => {
132
133
  target: 'firefox140',
133
134
  define: {
134
135
  global: 'globalThis',
136
+ // An identity, unlike on `--app node` (ADR 0079): GJS itself defines `window`
137
+ // as a non-configurable alias of the global, so `typeof window` is 'object'
138
+ // with or without it. What a browser branch then calls on it comes from
139
+ // `dom-events/register/global-event-target` (`addEventListener` & co.).
135
140
  window: 'globalThis',
136
141
  'process.env.READABLE_STREAM': '"disable"',
142
+ 'process.env.NODE_ENV': '"production"',
137
143
  },
138
144
  // Rewrite bare `console` to a named import from our shim. Rolldown's
139
145
  // `inject` (not a register-style global write) because GJS defines
@@ -182,6 +188,9 @@ export const setupForGjs = async (input) => {
182
188
  // banner, and acorn (the auto-globals detector) rejects the `#` byte.
183
189
  // The final-output shebang is composed by shebangPlugin's renderChunk.
184
190
  inputShebangStripPlugin(),
191
+ // A module assigning the global `console` gets a local binding, or the inject
192
+ // below turns its assignment into `ASSIGN_TO_IMPORT` and fails the build.
193
+ ...(consoleShimPath ? [consoleAssignPlugin()] : []),
185
194
  // Platform-file forks for the desktop, ADR 0032 § 9: `.gtk` → `.<os>` →
186
195
  // `.desktop` → base. BEFORE the alias layer, so a platform fork of a
187
196
  // module that also has a Node-builtin substitution wins over the
@@ -208,8 +217,11 @@ export const setupForGjs = async (input) => {
208
217
  // from `@gjsify/napi`, which is BUNDLED, not external. Order matters:
209
218
  // after aliasPlugin so a user alias pinning an addon's native entry wins,
210
219
  // before externalsPlugin so the acquisition is claimed first. Inert
211
- // otherwise; `--app gjs` only.
212
- napiNodeAddonPlugin(),
220
+ // otherwise; `--app gjs` only. `runtimeResolve` is the SAME `format ===
221
+ // 'esm'` gate the path rewriter below carries: the shim resolves the
222
+ // addon from the bundle's own URL, which only the ESM output's
223
+ // bundle-URL banner records (ADR 0084).
224
+ napiNodeAddonPlugin({ runtimeResolve: format === 'esm' }),
213
225
  // Externals policy as a resolveId hook — the only form BOTH engines
214
226
  // honour. Runs after the alias plugin's `pre` resolveId so aliases apply
215
227
  // first.
@@ -26,15 +26,19 @@
26
26
  // "this package IS the native implementation" sense and therefore promises a
27
27
  // `globals.mjs` it does not ship. The slot vocabulary's `native` means "the
28
28
  // RUNTIME provides this, use its value", so the bridge packages are
29
- // mis-declared; that is tracked in status/open-todos.md and must be settled
29
+ // mis-declared; that is tracked in status/open-todos/README.md and must be settled
30
30
  // before this table is composed.
31
31
  //
32
32
  // Derived `@gjsify/<X>` entries are merged into the map below, so an import by
33
33
  // PACKAGE NAME still routes per its declared slot in a single hop.
34
34
  //
35
35
  // No `cssAsStringPlugin` (NativeScript ships its own CSS pipeline as part
36
- // of `@nativescript/core`) and no `blueprintPlugin` (Blueprint is GTK-only).
36
+ // of `@nativescript/core`). `blueprintPlugin` IS here: a bare `.blp` import
37
+ // still compiles to GtkBuilder XML that nothing on this target reads, but
38
+ // `./x.blp?shared-tree` projects the same file into the node shape
39
+ // `@gjsify/adwaita-nativescript`'s `build` consumes.
37
40
  import { aliasPlugin } from '../plugins/alias.js';
41
+ import blueprintPlugin from '@gjsify/vite-plugin-blueprint';
38
42
  import { deepkitPlugin } from '@gjsify/rolldown-plugin-deepkit';
39
43
  import { ALIASES_NODE_FOR_NATIVESCRIPT, GI_RENDERERS, getDerivedAliasesSync } from '@gjsify/resolve-npm';
40
44
  import { globToEntryPoints } from '../utils/entry-points.js';
@@ -146,7 +150,15 @@ export const setupForNativescript = async (input) => {
146
150
  // alias routing so a platform fork of a portable module is honored.
147
151
  platformResolvePlugin({ suffixes: nativescriptSuffixChain(platform) }),
148
152
  aliasPlugin({ entries: aliasEntries }),
149
- // NO blueprintPlugin — Blueprint is a GTK-specific UI DSL
153
+ // Blueprint: `./x.blp?shared-tree` is how ONE authored template reaches this
154
+ // target. The comment this replaced read "NO blueprintPlugin — Blueprint is a
155
+ // GTK-specific UI DSL", and that was true of the only exit the plugin had: a
156
+ // GtkBuilder-XML string, which no NativeScript runtime can load. It is the
157
+ // NOTATION that was never GTK-specific — ADR 0053 clause 1 made `.blp` a second
158
+ // READER of the shared node shape, and the projection is what this target
159
+ // consumes. A `.blp` whose projection loses anything is refused at build time
160
+ // rather than rendered partially; see `SHARED_TREE_QUERY` in the plugin.
161
+ blueprintPlugin(),
150
162
  // NO cssAsStringPlugin — NS ships its own CSS pipeline via
151
163
  // @nativescript/core; .css imports are handled by the consuming
152
164
  // @nativescript/webpack or @nativescript/vite build
package/lib/app/node.d.ts CHANGED
@@ -43,7 +43,7 @@ export declare function enableGjsRegistersForNode(baseAliases: Record<string, st
43
43
  * emptying both stay, so a cross-platform package's node bundle keeps loading on
44
44
  * plain Node without node-gi installed.
45
45
  *
46
- * KNOWN NARROWNESS (tracked in status/open-todos.md): a genuine GJS source whose
46
+ * KNOWN NARROWNESS (tracked in status/open-todos/README.md): a genuine GJS source whose
47
47
  * ONLY platform reach is `gi://` (no ambient global, no bare built-in) is not
48
48
  * recognised — its `@girs/*` and its registers are both emptied. A surviving
49
49
  * `gi://` import cannot simply become a third signal: its shim loads node-gi
package/lib/app/node.js CHANGED
@@ -11,6 +11,7 @@ import { cssAsStringPlugin } from '../plugins/css-as-string.js';
11
11
  import { gjsImportsEmptyPlugin } from '../plugins/gjs-imports-empty.js';
12
12
  import { gjsGiNodePlugin, gjsBuiltinModulesNodePlugin } from '../plugins/gjs-gi-node.js';
13
13
  import { unresolvedWorkspaceImportPlugin } from '../plugins/unresolved-workspace-import.js';
14
+ import { nodeNativeExternalPlugin } from '../plugins/node-native-external.js';
14
15
  import { platformResolvePlugin, desktopSuffixChain, desktopOsSuffix, DESKTOP_REFUSED_SUFFIXES, } from '../plugins/platform-resolve.js';
15
16
  import { reactNativeAliasPlugin } from '../plugins/react-native-alias.js';
16
17
  import { reactNativeSupportGatePlugin } from '../plugins/react-native-gate.js';
@@ -178,7 +179,7 @@ export function enableGjsRegistersForNode(baseAliases) {
178
179
  * emptying both stay, so a cross-platform package's node bundle keeps loading on
179
180
  * plain Node without node-gi installed.
180
181
  *
181
- * KNOWN NARROWNESS (tracked in status/open-todos.md): a genuine GJS source whose
182
+ * KNOWN NARROWNESS (tracked in status/open-todos/README.md): a genuine GJS source whose
182
183
  * ONLY platform reach is `gi://` (no ambient global, no bare built-in) is not
183
184
  * recognised — its `@girs/*` and its registers are both emptied. A surviving
184
185
  * `gi://` import cannot simply become a third signal: its shim loads node-gi
@@ -192,9 +193,9 @@ export function isGjsSourceBuild(options) {
192
193
  }
193
194
  export const setupForNode = async (input) => {
194
195
  const userExternal = input.userExternal ?? [];
195
- // node-datachannel and `@gjsify/node-gi` are native addons that cannot be
196
- // bundled — their loaders resolve a `.node` binary relative to their own
197
- // installed location, so both must stay external.
196
+ // `@gjsify/node-gi` stays external by name: the injected globals shim and the
197
+ // bare-module specifiers below name it before any package is resolved. Every
198
+ // other native addon package is found by `nodeNativeExternalPlugin`.
198
199
  //
199
200
  // GJS-specific specifiers (`gi://*`, `@girs/*`) are deliberately NOT
200
201
  // externalised: `gjsGiNodePlugin` rewrites `gi://` to `requireGi`, and
@@ -204,7 +205,6 @@ export const setupForNode = async (input) => {
204
205
  // `ERR_UNSUPPORTED_ESM_URL_SCHEME`.
205
206
  const exactExternal = [
206
207
  ...EXTERNALS_NODE,
207
- 'node-datachannel',
208
208
  '@gjsify/node-gi',
209
209
  '@gjsify/node-gi/gi',
210
210
  NODE_GI_GLOBALS_SPECIFIER,
@@ -275,16 +275,28 @@ export const setupForNode = async (input) => {
275
275
  external: exactExternal,
276
276
  resolve: {
277
277
  mainFields: format === 'esm' ? ['module', 'main', 'browser'] : ['main', 'module', 'browser'],
278
- // CJS-priority conditions. Rolldown takes the package's first matching
279
- // key, so adding 'import' would route ws v8 (which lists 'import'
280
- // before 'require') through its incomplete ESM wrapper.
281
- conditionNames: format === 'esm' ? ['require', 'node', 'module'] : ['require'],
278
+ // Neither 'import' nor 'require' on the esm build: rolldown adds the one
279
+ // matching each call site's kind, as Node's resolver does. Listing 'require'
280
+ // made every `import` take the CJS build wherever a package declares
281
+ // `require` first — `import basex from 'base-x'` then bound
282
+ // `module.exports` as the default and threw "(0, x.default) is not a
283
+ // function" at load, while the same source ran on Node. `--app gjs` lost
284
+ // the pinned 'import' for the mirror bug (#1130). 'module' is not a
285
+ // condition Node knows, and a require() would take it wherever a package
286
+ // lists it before 'require'.
287
+ conditionNames: format === 'esm' ? ['node'] : ['require'],
282
288
  },
283
289
  transform: {
284
290
  target: 'node24',
285
291
  define: {
286
292
  global: 'globalThis',
287
- window: 'globalThis',
293
+ // NO `window` define (ADR 0079): Node has none, and a bundle must see the same
294
+ // `typeof window` as its source run. A reverse-bridge DOM app gets `window`
295
+ // from @gjsify/dom-elements/register/document, which defines it at runtime.
296
+ // NO `process.env.NODE_ENV` define either, unlike gjs/browser (#1355): Node
297
+ // has a real `process.env`, so a Node bundle reads NODE_ENV at run time like
298
+ // its source does. Baking "production" in selected React's production build,
299
+ // which has no `act`, in every `--app node` test bundle.
288
300
  },
289
301
  },
290
302
  output: {
@@ -352,6 +364,10 @@ export const setupForNode = async (input) => {
352
364
  // externals policy — a redirect after `externalsPlugin` would find the
353
365
  // specifier already externalised.
354
366
  ...(input.pluginOptions.dialect === 'react-native' ? [reactNativeAliasPlugin()] : []),
367
+ // Node-API addon packages stay external so Node loads their `.node` from
368
+ // `node_modules` (see the plugin header). Resolves THROUGH the alias layer
369
+ // (`this.resolve`), so an aliased specifier is judged by where it lands.
370
+ nodeNativeExternalPlugin(),
355
371
  aliasPlugin({ entries: aliasEntries }),
356
372
  // Blueprint (.blp → XML string): the reverse bridge runs REAL GTK on Node,
357
373
  // so a GJS app entry with composite-template windows must build for
package/lib/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export { REWRITE_FILTER, extractPackageSpec, getBundleDirFromOutput, rewriteCont
6
6
  export type { NodeModulesPathRewriteOptions, RewriteResult } from './plugins/rewrite-node-modules-paths.js';
7
7
  export { processStubPlugin, GJS_PROCESS_STUB, composeBanner } from './plugins/process-stub.js';
8
8
  export { giRuntimePathsStub } from './plugins/gi-runtime-paths.js';
9
+ export { bindConsoleLocally, consoleAssignPlugin, freeConsoleAssignmentInsertion, CONSOLE_LOCAL_BINDING, } from './plugins/console-assign.js';
9
10
  export type { GiSystemProbe } from './plugins/gi-runtime-paths.js';
10
11
  export type { ProcessStubPluginOptions } from './plugins/process-stub.js';
11
12
  export { cssAsStringPlugin } from './plugins/css-as-string.js';
@@ -20,9 +21,10 @@ export { externalsPlugin } from './plugins/externals.js';
20
21
  export type { ExternalsPredicate, ExternalsPluginOptions } from './plugins/externals.js';
21
22
  export { unresolvedWorkspaceImportPlugin, classifyImport, isWorkspaceSpecifier, formatUnresolvedWorkspaceImport, buildReverseAliasIndex, UnresolvedWorkspaceImportError, } from './plugins/unresolved-workspace-import.js';
22
23
  export type { WorkspaceImportGuardOptions, WorkspaceImportGuardTarget, ImportVerdict, ClassifyImportInput, UnresolvedWorkspaceImportDetails, } from './plugins/unresolved-workspace-import.js';
23
- export { napiNodeAddonPlugin, resolveAddonPath, nearestPackageRoot, classifySpecifier, directNodeShim, nodeGypBuildShim, bindingsShim, napiRsShim, ADDON_FILTER_RE, isNapiRsPackageJson, isNapiRsSibling, isGjsifyNativeBridge, detectNapiRsEntry, hostNapiRsTriple, AddonNotBuiltError, } from './plugins/napi-node-addon.js';
24
+ export { napiNodeAddonPlugin, resolveAddonPath, enumerateAddonTargets, nearestPackageRoot, classifySpecifier, directNodeShim, nodeGypBuildShim, bindingsShim, napiRsShim, ADDON_FILTER_RE, isNapiRsPackageJson, isNapiRsSibling, isGjsifyNativeBridge, detectNapiRsEntry, hostNapiRsTriple, AddonNotBuiltError, } from './plugins/napi-node-addon.js';
24
25
  export type { NapiNodeAddonPluginOptions, AddonPackageJson } from './plugins/napi-node-addon.js';
25
- export { platformResolvePlugin, nativescriptSuffixChain, desktopSuffixChain, desktopOsSuffix, DESKTOP_OS_SUFFIXES, DESKTOP_REFUSED_SUFFIXES, PlatformVariantExternalError, detectNativescriptPlatform, nativescriptPlatformDefines, } from './plugins/platform-resolve.js';
26
+ export { nodeNativeExternalPlugin, isNodeAddonPackage, packageNameOf } from './plugins/node-native-external.js';
27
+ export { platformResolvePlugin, nativescriptSuffixChain, desktopSuffixChain, browserSuffixChain, desktopOsSuffix, DESKTOP_OS_SUFFIXES, DESKTOP_REFUSED_SUFFIXES, BROWSER_REFUSED_SUFFIXES, PlatformVariantExternalError, detectNativescriptPlatform, nativescriptPlatformDefines, } from './plugins/platform-resolve.js';
26
28
  export type { PlatformResolvePluginOptions, NativescriptPlatform } from './plugins/platform-resolve.js';
27
29
  export { rnRouteManifestPlugin, renderRouteManifest, walkRoutes, RouteManifestError, RN_ROUTES_MODULE_ID, MAX_ROUTE_DEPTH, } from './plugins/rn-route-manifest.js';
28
30
  export type { RnRouteManifestOptions, FoundRoute } from './plugins/rn-route-manifest.js';
package/lib/index.js CHANGED
@@ -6,6 +6,7 @@ export * from './library/index.js';
6
6
  export { REWRITE_FILTER, extractPackageSpec, getBundleDirFromOutput, rewriteContents, shouldRewrite, shouldInline, nodeModulesPathRewritePlugin, } from './plugins/rewrite-node-modules-paths.js';
7
7
  export { processStubPlugin, GJS_PROCESS_STUB, composeBanner } from './plugins/process-stub.js';
8
8
  export { giRuntimePathsStub } from './plugins/gi-runtime-paths.js';
9
+ export { bindConsoleLocally, consoleAssignPlugin, freeConsoleAssignmentInsertion, CONSOLE_LOCAL_BINDING, } from './plugins/console-assign.js';
9
10
  export { cssAsStringPlugin } from './plugins/css-as-string.js';
10
11
  export { textLoaderPlugin } from './plugins/text-loader.js';
11
12
  export { shebangPlugin, GJS_SHEBANG, NODE_SHEBANG, expandEnvTemplate, resolveShebangLine } from './plugins/shebang.js';
@@ -13,8 +14,9 @@ export { aliasPlugin } from './plugins/alias.js';
13
14
  export { gjsImportsEmptyPlugin } from './plugins/gjs-imports-empty.js';
14
15
  export { externalsPlugin } from './plugins/externals.js';
15
16
  export { unresolvedWorkspaceImportPlugin, classifyImport, isWorkspaceSpecifier, formatUnresolvedWorkspaceImport, buildReverseAliasIndex, UnresolvedWorkspaceImportError, } from './plugins/unresolved-workspace-import.js';
16
- export { napiNodeAddonPlugin, resolveAddonPath, nearestPackageRoot, classifySpecifier, directNodeShim, nodeGypBuildShim, bindingsShim, napiRsShim, ADDON_FILTER_RE, isNapiRsPackageJson, isNapiRsSibling, isGjsifyNativeBridge, detectNapiRsEntry, hostNapiRsTriple, AddonNotBuiltError, } from './plugins/napi-node-addon.js';
17
- export { platformResolvePlugin, nativescriptSuffixChain, desktopSuffixChain, desktopOsSuffix, DESKTOP_OS_SUFFIXES, DESKTOP_REFUSED_SUFFIXES, PlatformVariantExternalError, detectNativescriptPlatform, nativescriptPlatformDefines, } from './plugins/platform-resolve.js';
17
+ export { napiNodeAddonPlugin, resolveAddonPath, enumerateAddonTargets, nearestPackageRoot, classifySpecifier, directNodeShim, nodeGypBuildShim, bindingsShim, napiRsShim, ADDON_FILTER_RE, isNapiRsPackageJson, isNapiRsSibling, isGjsifyNativeBridge, detectNapiRsEntry, hostNapiRsTriple, AddonNotBuiltError, } from './plugins/napi-node-addon.js';
18
+ export { nodeNativeExternalPlugin, isNodeAddonPackage, packageNameOf } from './plugins/node-native-external.js';
19
+ export { platformResolvePlugin, nativescriptSuffixChain, desktopSuffixChain, browserSuffixChain, desktopOsSuffix, DESKTOP_OS_SUFFIXES, DESKTOP_REFUSED_SUFFIXES, BROWSER_REFUSED_SUFFIXES, PlatformVariantExternalError, detectNativescriptPlatform, nativescriptPlatformDefines, } from './plugins/platform-resolve.js';
18
20
  export { rnRouteManifestPlugin, renderRouteManifest, walkRoutes, RouteManifestError, RN_ROUTES_MODULE_ID, MAX_ROUTE_DEPTH, } from './plugins/rn-route-manifest.js';
19
21
  export { reactNativeAliasPlugin, classifyReactNativeSpecifier, couldBeSurfaceSpecifier, loadLayer, FALLBACK_SURFACES, REACT_NATIVE_ALIAS_TARGET, REACT_NATIVE_SPECIFIER, ReactNativeDeepImportError, ReactNativeAliasTargetMissingError, SURFACE_MENTION, SURFACE_NAME_PREFIXES, } from './plugins/react-native-alias.js';
20
22
  export { reactNativeSupportGatePlugin, loadSupportTable, findSupportViolations, formatSupportViolations, formatOpaqueReference, formatUnreadableModule, SUPPORT_TABLE_SUBPATH, WATCHED_SPECIFIERS, watchedSpecifiers, ReactNativeUnsupportedImportError, SupportTableUnreadableError, } from './plugins/react-native-gate.js';
@@ -0,0 +1,13 @@
1
+ import type { Plugin } from 'rolldown';
2
+ export declare const CONSOLE_LOCAL_BINDING = "var console = globalThis.console;";
3
+ /**
4
+ * Where to insert the local binding in `code`, or null when the module does not assign
5
+ * the GLOBAL `console`. Null as well when the module declares a `console` anywhere: its
6
+ * assignments may target that binding, and declaring a second one would not parse.
7
+ * The offset is after the directive prologue, so a `'use strict'` stays a directive.
8
+ */
9
+ export declare function freeConsoleAssignmentInsertion(code: string): number | null;
10
+ /** `code` with the local `console` binding inserted, or null when it needs none. */
11
+ export declare function bindConsoleLocally(code: string): string | null;
12
+ /** Composed by `--app gjs` whenever the console shim is injected. */
13
+ export declare function consoleAssignPlugin(): Plugin;
@@ -0,0 +1,125 @@
1
+ // Give a module that ASSIGNS the global `console` a local binding of its own, so the
2
+ // `--app gjs` console `inject` leaves it alone.
3
+ //
4
+ // `--app gjs` rewrites every free `console` into an import from `shims/console-gjs`
5
+ // (rolldown `transform.inject`), because GJS defines `globalThis.console` non-writable
6
+ // and non-configurable. An import binding cannot be assigned, so ONE sloppy-mode
7
+ // `console = { log() {} }` anywhere in the dependency graph failed the whole build with
8
+ // `ASSIGN_TO_IMPORT: Cannot assign to import 'console'`. Measured: `node-forge`'s
9
+ // `lib/log.js` does exactly that on its browser-without-console branch, and it reaches
10
+ // `wxt` through `web-ext`, so `gjsify exec wxt` could not be rebuilt at all.
11
+ //
12
+ // The binding is `var console = globalThis.console`: the module keeps working with the
13
+ // runtime's console, and its assignment lands on its own variable — which is all a
14
+ // sloppy-mode write to a non-writable global ever did under GJS. Only such a module
15
+ // gives up the shim's print()-based output; every other module is untouched.
16
+ //
17
+ // Detection is an AST question (a parameter or a `const console` is a binding, not an
18
+ // assignment to the global), asked only of sources the cheap pattern admits.
19
+ import * as acorn from 'acorn';
20
+ import * as walk from 'acorn-walk';
21
+ import { extractBindingNames } from '../utils/detect-free-globals.js';
22
+ const CANDIDATE_RE = /(?:^|[^.\w$])console\s*=(?![=>])/;
23
+ const SOURCE_RE = /\.(?:m|c)?jsx?$/;
24
+ export const CONSOLE_LOCAL_BINDING = 'var console = globalThis.console;';
25
+ function parse(code) {
26
+ const base = { ecmaVersion: 'latest', allowHashBang: true, allowAwaitOutsideFunction: true };
27
+ try {
28
+ return acorn.parse(code, { ...base, sourceType: 'module' });
29
+ }
30
+ catch {
31
+ // Sloppy CJS (the node-forge case: `with`, legacy octals, top-level `return`)
32
+ // is not a valid module; the script grammar is the one it was written in.
33
+ }
34
+ try {
35
+ return acorn.parse(code, { ...base, sourceType: 'script', allowReturnOutsideFunction: true });
36
+ }
37
+ catch {
38
+ // Not JavaScript acorn can read (a syntax newer than acorn): leave it to the
39
+ // bundler, which reports an assignment it cannot bundle loudly on its own.
40
+ return null;
41
+ }
42
+ }
43
+ /**
44
+ * Where to insert the local binding in `code`, or null when the module does not assign
45
+ * the GLOBAL `console`. Null as well when the module declares a `console` anywhere: its
46
+ * assignments may target that binding, and declaring a second one would not parse.
47
+ * The offset is after the directive prologue, so a `'use strict'` stays a directive.
48
+ */
49
+ export function freeConsoleAssignmentInsertion(code) {
50
+ if (!CANDIDATE_RE.test(code))
51
+ return null;
52
+ const ast = parse(code);
53
+ if (!ast)
54
+ return null;
55
+ let declares = false;
56
+ let assigns = false;
57
+ const bindsConsole = (node) => !!node && extractBindingNames(node).includes('console');
58
+ walk.full(ast, (node) => {
59
+ switch (node.type) {
60
+ case 'VariableDeclarator':
61
+ if (bindsConsole(node.id))
62
+ declares = true;
63
+ break;
64
+ case 'FunctionDeclaration':
65
+ case 'FunctionExpression':
66
+ case 'ArrowFunctionExpression':
67
+ if (bindsConsole(node.id) || node.params.some((p) => bindsConsole(p)))
68
+ declares = true;
69
+ break;
70
+ case 'ClassDeclaration':
71
+ case 'ClassExpression':
72
+ if (bindsConsole(node.id))
73
+ declares = true;
74
+ break;
75
+ case 'CatchClause':
76
+ if (bindsConsole(node.param))
77
+ declares = true;
78
+ break;
79
+ case 'ImportSpecifier':
80
+ case 'ImportDefaultSpecifier':
81
+ case 'ImportNamespaceSpecifier':
82
+ if (node.local.name === 'console')
83
+ declares = true;
84
+ break;
85
+ case 'AssignmentExpression':
86
+ if (bindsConsole(node.left))
87
+ assigns = true;
88
+ break;
89
+ }
90
+ });
91
+ if (!assigns || declares)
92
+ return null;
93
+ let offset = 0;
94
+ for (const statement of ast.body) {
95
+ if (statement.type !== 'ExpressionStatement' || !('directive' in statement))
96
+ break;
97
+ offset = statement.end;
98
+ }
99
+ return offset;
100
+ }
101
+ /** `code` with the local `console` binding inserted, or null when it needs none. */
102
+ export function bindConsoleLocally(code) {
103
+ const at = freeConsoleAssignmentInsertion(code);
104
+ if (at === null)
105
+ return null;
106
+ // No newline, so every later line keeps its number; after a directive the `;` ends
107
+ // it even when the source relied on ASI (`'use strict'\n`).
108
+ const sep = at === 0 ? '' : ';';
109
+ return `${code.slice(0, at)}${sep}${CONSOLE_LOCAL_BINDING}${code.slice(at)}`;
110
+ }
111
+ /** Composed by `--app gjs` whenever the console shim is injected. */
112
+ export function consoleAssignPlugin() {
113
+ return {
114
+ name: 'gjsify-console-assign',
115
+ transform: {
116
+ filter: { id: SOURCE_RE },
117
+ handler(code, id) {
118
+ if (!SOURCE_RE.test(id))
119
+ return null;
120
+ const out = bindConsoleLocally(code);
121
+ return out === null ? null : { code: out, map: null };
122
+ },
123
+ },
124
+ };
125
+ }