@gjsify/rolldown-plugin-gjsify 0.43.0 → 0.45.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.
package/lib/app/gjs.js CHANGED
@@ -9,6 +9,9 @@ import { aliasPlugin } from '../plugins/alias.js';
9
9
  import { externalsPlugin } from '../plugins/externals.js';
10
10
  import { napiNodeAddonPlugin } from '../plugins/napi-node-addon.js';
11
11
  import { unresolvedWorkspaceImportPlugin } from '../plugins/unresolved-workspace-import.js';
12
+ import { platformResolvePlugin, desktopSuffixChain, desktopOsSuffix, DESKTOP_REFUSED_SUFFIXES, } from '../plugins/platform-resolve.js';
13
+ import { reactNativeAliasPlugin } from '../plugins/react-native-alias.js';
14
+ import { reactNativeSupportGatePlugin } from '../plugins/react-native-gate.js';
12
15
  import { deepkitPlugin } from '@gjsify/rolldown-plugin-deepkit';
13
16
  import blueprintPlugin from '@gjsify/vite-plugin-blueprint';
14
17
  import { getAliasesForGjs } from '../utils/alias.js';
@@ -161,6 +164,15 @@ export const setupForGjs = async (input) => {
161
164
  // Reflection reads the ORIGINAL TypeScript, so it runs before any user
162
165
  // transform can strip the annotations out from under it (see `GjsifyConfig`).
163
166
  const prePlugins = [deepkitPlugin({ reflection: input.pluginOptions.reflection })];
167
+ // ADR 0032 § 8's build-time gate reads the ORIGINAL source for the same reason
168
+ // reflection does: `import type { ViewProps }` is what tells it a name costs
169
+ // nothing, and a normal-order transform may already have stripped it.
170
+ // This target composes a dialect, so it is one of `DIALECT_APPS` — the set
171
+ // the CLI refuses `--dialect` against for every OTHER target. Adding a
172
+ // dialect here without adding the target there means a flag accepted and
173
+ // silently ignored.
174
+ if (input.pluginOptions.dialect === 'react-native')
175
+ prePlugins.push(reactNativeSupportGatePlugin());
164
176
  const plugins = [
165
177
  // Virtual-entry plugin runs FIRST so its resolveId/load match the
166
178
  // synthetic input ids that `wrapInputWithSideEffects` produces.
@@ -170,6 +182,22 @@ export const setupForGjs = async (input) => {
170
182
  // banner, and acorn (the auto-globals detector) rejects the `#` byte.
171
183
  // The final-output shebang is composed by shebangPlugin's renderChunk.
172
184
  inputShebangStripPlugin(),
185
+ // Platform-file forks for the desktop, ADR 0032 § 9: `.gtk` → `.<os>` →
186
+ // `.desktop` → base. BEFORE the alias layer, so a platform fork of a
187
+ // module that also has a Node-builtin substitution wins over the
188
+ // substitution — the fork is the more specific statement. `.native` and
189
+ // `.web` are deliberately absent from the chain and warned about when
190
+ // present; `DESKTOP_REFUSED_SUFFIXES` carries the reason.
191
+ platformResolvePlugin({
192
+ suffixes: desktopSuffixChain(desktopOsSuffix()),
193
+ refusedSuffixes: DESKTOP_REFUSED_SUFFIXES,
194
+ siblingIndex: true,
195
+ }),
196
+ // ADR 0032 § 2's alias line, only when the consumer asked for it
197
+ // (`--dialect react-native`). `pre`, ahead of the substitution table and the
198
+ // externals policy — a redirect after `externalsPlugin` would find the
199
+ // specifier already externalised.
200
+ ...(input.pluginOptions.dialect === 'react-native' ? [reactNativeAliasPlugin()] : []),
173
201
  // random-access-file's 'browser' field maps to a throwing stub; the alias
174
202
  // table forces the fs-backed Node entry.
175
203
  aliasPlugin({ entries: aliasEntries }),
@@ -37,7 +37,7 @@ import { deepkitPlugin } from '@gjsify/rolldown-plugin-deepkit';
37
37
  import { ALIASES_NODE_FOR_NATIVESCRIPT, getDerivedAliasesSync } from '@gjsify/resolve-npm';
38
38
  import { globToEntryPoints } from '../utils/entry-points.js';
39
39
  import { gjsImportsEmptyPlugin } from '../plugins/gjs-imports-empty.js';
40
- import { platformResolvePlugin, detectNativescriptPlatform, nativescriptPlatformDefines, } from '../plugins/platform-resolve.js';
40
+ import { platformResolvePlugin, nativescriptSuffixChain, detectNativescriptPlatform, nativescriptPlatformDefines, } from '../plugins/platform-resolve.js';
41
41
  import { unresolvedWorkspaceImportPlugin } from '../plugins/unresolved-workspace-import.js';
42
42
  // Never bundled: an app works only with the ONE core instance the runtime boots, and what
43
43
  // this target emits is an ENTRY the NS bundler resolves it for. An optional peer absent from
@@ -132,7 +132,7 @@ export const setupForNativescript = async (input) => {
132
132
  // Platform-specific source variants (`*.android` / `*.ios` /
133
133
  // `*.native`) win over the base file — resolved BEFORE the Node-builtin
134
134
  // alias routing so a platform fork of a portable module is honored.
135
- platformResolvePlugin({ platform }),
135
+ platformResolvePlugin({ suffixes: nativescriptSuffixChain(platform) }),
136
136
  aliasPlugin({ entries: aliasEntries }),
137
137
  // NO blueprintPlugin — Blueprint is a GTK-specific UI DSL
138
138
  // NO cssAsStringPlugin — NS ships its own CSS pipeline via
package/lib/app/node.d.ts CHANGED
@@ -6,12 +6,15 @@ import type { PluginOptions } from '../types/plugin-options.js';
6
6
  * factory, matchMedia, …) over `@gjsify/node-gi`, not the default `@gjsify/empty`
7
7
  * stubs that keep CROSS-PLATFORM node bundles loadable on plain Node.
8
8
  *
9
- * Two adjustments over the standard node alias map:
9
+ * Three adjustments over the standard node alias map:
10
10
  * 1. every `<pkg>/register…` → `@gjsify/empty` entry is DROPPED so the register
11
11
  * import resolves to its real body;
12
12
  * 2. the gjs target's bare→scoped register routes (`xmlhttprequest/register` →
13
13
  * `@gjsify/fetch/register/xhr`, …) are merged in so the inject stub's bare
14
- * specifiers resolve on node exactly as they do on gjs.
14
+ * specifiers resolve on node exactly as they do on gjs;
15
+ * 3. `REVERSE_BRIDGE_ENTRY_ROUTES` is merged in, so a package whose `node`
16
+ * export condition is a different program (solid-js's SSR build) is routed
17
+ * to the entry the `--app gjs` build of the same source gets.
15
18
  *
16
19
  * Applied to the BASE map only, when `isGjsSourceBuild` — `pluginOptions.aliases` /
17
20
  * user aliases still override, and a non-reverse-bridge build is untouched.
package/lib/app/node.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // `--app node` Rolldown configuration factory.
2
+ import { createRequire } from 'node:module';
2
3
  import { aliasPlugin } from '../plugins/alias.js';
3
4
  import blueprintPlugin from '@gjsify/vite-plugin-blueprint';
4
5
  import { deepkitPlugin } from '@gjsify/rolldown-plugin-deepkit';
@@ -10,6 +11,9 @@ import { cssAsStringPlugin } from '../plugins/css-as-string.js';
10
11
  import { gjsImportsEmptyPlugin } from '../plugins/gjs-imports-empty.js';
11
12
  import { gjsGiNodePlugin, gjsBuiltinModulesNodePlugin } from '../plugins/gjs-gi-node.js';
12
13
  import { unresolvedWorkspaceImportPlugin } from '../plugins/unresolved-workspace-import.js';
14
+ import { platformResolvePlugin, desktopSuffixChain, desktopOsSuffix, DESKTOP_REFUSED_SUFFIXES, } from '../plugins/platform-resolve.js';
15
+ import { reactNativeAliasPlugin } from '../plugins/react-native-alias.js';
16
+ import { reactNativeSupportGatePlugin } from '../plugins/react-native-gate.js';
13
17
  import { wrapInputWithSideEffects } from '../utils/entry-wrapper.js';
14
18
  /**
15
19
  * Side-effect specifier the node target injects when GJS ambient globals
@@ -38,18 +42,100 @@ const NODE_GI_GLOBALS_SPECIFIER = '@gjsify/node-gi/globals';
38
42
  const NODE_GI_BARE_MODULE_SPECIFIERS = Object.values(ALIASES_GJS_FOR_NODE);
39
43
  /** Matches a register subpath in an alias key: `<pkg>/register` or `<pkg>/register/<feature>`. */
40
44
  const REGISTER_SUBPATH_RE = /\/register(\/|$)/;
45
+ /**
46
+ * npm packages a REVERSE-BRIDGE build must be routed away from, because their
47
+ * `node` export condition is not a node build of the same program — it is a
48
+ * DIFFERENT program, chosen for an assumption a GJS-on-node bundle does not
49
+ * hold. The value is the entry the `--app gjs` build of the same source gets,
50
+ * so the two ADR 0030 legs run one program.
51
+ *
52
+ * `solid-js` is the measured case and so far the only one. Its export map
53
+ * routes `node` (and `deno`, and `worker`) to `dist/server.js` — the SSR
54
+ * build, whose `createEffect` is `function createEffect(fn, value) {}`, an
55
+ * empty body. The initial render is therefore PERFECT and every reactive
56
+ * update silently never reaches GTK: sixteen of @gjsify/gtk-host's node-leg
57
+ * suites failed as `Expected ["second"], Actual ["first"]` while the identical
58
+ * source passed on gjs, and under ADR 0030's "same suite green on gjs ⇒
59
+ * node-gi defect" that read as a binding defect for as long as it stood. The
60
+ * map's own top-level `import`/`require` keys already point at the client
61
+ * build — `node` merely shadows them, being declared first.
62
+ *
63
+ * A ROUTE and not a condition change, deliberately. The obvious fix is to stop
64
+ * applying `node`, and it does not work: `platform: 'node'` implies that
65
+ * condition whatever `conditionNames` says (measured — dropping it from the
66
+ * list left solid-js on the SSR build). The reachable lever is `browser`,
67
+ * which outranks `node` in solid-js's map, and taking it wholesale is the
68
+ * SYMMETRIC defect: the gjs target can afford `browser` only because
69
+ * `getAliasesForGjs` has already replaced the node-facing npm packages, while
70
+ * the reverse bridge lifts the `/register` routes and keeps the rest real. Then
71
+ * `ws` — whose map declares `browser` FIRST, pointing at a one-line
72
+ * `throw new Error('ws does not work in the browser…')` — took the node-gi
73
+ * consumer harness from `pass 19/19` to `0/5 passed, 10 failed`, every one
74
+ * `W.WebSocket is not a constructor`. One named package resolves differently;
75
+ * every other package, and every cross-platform node bundle, is byte-unchanged.
76
+ */
77
+ const REVERSE_BRIDGE_ENTRY_ROUTES = {
78
+ // The client build both legs share. Subpaths need no route: `solid-js/universal`
79
+ // (the renderer @gjsify/gtk-host binds to) declares no `node` condition at all,
80
+ // and reaches the root through this same specifier.
81
+ 'solid-js': 'solid-js/dist/solid.js',
82
+ };
83
+ /**
84
+ * A dangling route above is SILENT, and silence here restores exactly the bug
85
+ * the route exists to prevent. MEASURED: point the solid-js route at a file
86
+ * that does not exist and the build exits 0 with an EMPTY log — `aliasPlugin`
87
+ * answers an unresolvable target with `null`, which a `pre`-order `resolveId`
88
+ * means as "let the default chain continue", and the default chain is the
89
+ * `node` condition, i.e. the SSR build with the empty `createEffect`. So a
90
+ * route that stops resolving fails the BUILD instead, naming the package.
91
+ *
92
+ * The CONTROL is the unrouted resolution of the same specifier — what the
93
+ * route replaces. If that does not resolve, the package is not in this
94
+ * project's graph at all and there is nothing to route (the routes are a
95
+ * table, not a dependency list). If it DOES resolve, the routed entry must
96
+ * too: a package that is installed and whose client entry has moved is the
97
+ * one case that must not pass quietly.
98
+ *
99
+ * `createRequire` is used only as an `exports`-map-aware RESOLVER, as in
100
+ * `app/gjs.ts`'s `resolveShim`; nothing is loaded through it.
101
+ */
102
+ function assertReverseBridgeRoutes() {
103
+ const require_ = createRequire(import.meta.url);
104
+ const resolves = (specifier) => {
105
+ try {
106
+ require_.resolve(specifier);
107
+ return true;
108
+ }
109
+ catch {
110
+ return false;
111
+ }
112
+ };
113
+ for (const [pkg, target] of Object.entries(REVERSE_BRIDGE_ENTRY_ROUTES)) {
114
+ if (!resolves(pkg))
115
+ continue; // not in this graph — see the CONTROL note
116
+ if (resolves(target))
117
+ continue;
118
+ throw new Error(`@gjsify/rolldown-plugin-gjsify: the reverse-bridge entry route for "${pkg}" is dangling — ` +
119
+ `"${target}" does not resolve, while "${pkg}" does. Without the route this build silently ` +
120
+ `takes ${pkg}'s "node" export condition, which is why the route exists; update ` +
121
+ `REVERSE_BRIDGE_ENTRY_ROUTES in app/node.ts to the entry the --app gjs build resolves.`);
122
+ }
123
+ }
41
124
  /**
42
125
  * Reverse-bridge register routing: a node build of a GENUINE GJS SOURCE needs the
43
126
  * REAL `@gjsify/*` register bodies (document, HTMLCanvasElement, the `'2d'` context
44
127
  * factory, matchMedia, …) over `@gjsify/node-gi`, not the default `@gjsify/empty`
45
128
  * stubs that keep CROSS-PLATFORM node bundles loadable on plain Node.
46
129
  *
47
- * Two adjustments over the standard node alias map:
130
+ * Three adjustments over the standard node alias map:
48
131
  * 1. every `<pkg>/register…` → `@gjsify/empty` entry is DROPPED so the register
49
132
  * import resolves to its real body;
50
133
  * 2. the gjs target's bare→scoped register routes (`xmlhttprequest/register` →
51
134
  * `@gjsify/fetch/register/xhr`, …) are merged in so the inject stub's bare
52
- * specifiers resolve on node exactly as they do on gjs.
135
+ * specifiers resolve on node exactly as they do on gjs;
136
+ * 3. `REVERSE_BRIDGE_ENTRY_ROUTES` is merged in, so a package whose `node`
137
+ * export condition is a different program (solid-js's SSR build) is routed
138
+ * to the entry the `--app gjs` build of the same source gets.
53
139
  *
54
140
  * Applied to the BASE map only, when `isGjsSourceBuild` — `pluginOptions.aliases` /
55
141
  * user aliases still override, and a non-reverse-bridge build is untouched.
@@ -65,6 +151,8 @@ export function enableGjsRegistersForNode(baseAliases) {
65
151
  if (REGISTER_SUBPATH_RE.test(key))
66
152
  out[key] = value;
67
153
  }
154
+ // 3. the packages whose `node` export is a different PROGRAM — see the table.
155
+ Object.assign(out, REVERSE_BRIDGE_ENTRY_ROUTES);
68
156
  return out;
69
157
  }
70
158
  /**
@@ -153,6 +241,9 @@ export const setupForNode = async (input) => {
153
241
  // predicate's doc comment. The node-gi golden `test/canvas2d-bridge.test.mjs`
154
242
  // pins the asymmetry it fixed.
155
243
  const gjsSourceBuild = isGjsSourceBuild({ ...input.pluginOptions, registerInject });
244
+ // Only a reverse-bridge build carries the routes, so only it is held to them.
245
+ if (gjsSourceBuild)
246
+ assertReverseBridgeRoutes();
156
247
  const baseAliases = getAliasesForNode({ external });
157
248
  const aliasMap = {
158
249
  ...(gjsSourceBuild ? enableGjsRegistersForNode(baseAliases) : baseAliases),
@@ -209,6 +300,15 @@ export const setupForNode = async (input) => {
209
300
  // Reflection reads the ORIGINAL TypeScript, so it runs before any user
210
301
  // transform can strip the annotations out from under it (see `GjsifyConfig`).
211
302
  const prePlugins = [deepkitPlugin({ reflection: input.pluginOptions.reflection })];
303
+ // ADR 0032 § 8's build-time gate reads the ORIGINAL source for the same reason
304
+ // reflection does: `import type { ViewProps }` is what tells it a name costs
305
+ // nothing, and a normal-order transform may already have stripped it.
306
+ // This target composes a dialect, so it is one of `DIALECT_APPS` — the set
307
+ // the CLI refuses `--dialect` against for every OTHER target. Adding a
308
+ // dialect here without adding the target there means a flag accepted and
309
+ // silently ignored.
310
+ if (input.pluginOptions.dialect === 'react-native')
311
+ prePlugins.push(reactNativeSupportGatePlugin());
212
312
  const plugins = [
213
313
  // Virtual-entry plugin runs FIRST so its resolveId/load match the synthetic
214
314
  // `\0gjsify-entry:` ids `wrapInputWithSideEffects` produces (no-op when
@@ -236,6 +336,22 @@ export const setupForNode = async (input) => {
236
336
  // `requireGi('Adw','1')` with no lowercased-pkg→namespace map needed.
237
337
  // This plugin's own `gi://` branch is dead on the node target.
238
338
  gjsImportsEmptyPlugin({ emptyGirs: !gjsSourceBuild }),
339
+ // Platform-file forks for the desktop, ADR 0032 § 9: `.gtk` → `.<os>` →
340
+ // `.desktop` → base. BEFORE the alias layer, so a platform fork of a
341
+ // module that also has a Node-builtin substitution wins over the
342
+ // substitution — the fork is the more specific statement. `.native` and
343
+ // `.web` are deliberately absent from the chain and warned about when
344
+ // present; `DESKTOP_REFUSED_SUFFIXES` carries the reason.
345
+ platformResolvePlugin({
346
+ suffixes: desktopSuffixChain(desktopOsSuffix()),
347
+ refusedSuffixes: DESKTOP_REFUSED_SUFFIXES,
348
+ siblingIndex: true,
349
+ }),
350
+ // ADR 0032 § 2's alias line, only when the consumer asked for it
351
+ // (`--dialect react-native`). `pre`, ahead of the substitution table and the
352
+ // externals policy — a redirect after `externalsPlugin` would find the
353
+ // specifier already externalised.
354
+ ...(input.pluginOptions.dialect === 'react-native' ? [reactNativeAliasPlugin()] : []),
239
355
  aliasPlugin({ entries: aliasEntries }),
240
356
  // Blueprint (.blp → XML string): the reverse bridge runs REAL GTK on Node,
241
357
  // so a GJS app entry with composite-template windows must build for
package/lib/index.d.ts CHANGED
@@ -22,8 +22,16 @@ export { unresolvedWorkspaceImportPlugin, classifyImport, isWorkspaceSpecifier,
22
22
  export type { WorkspaceImportGuardOptions, WorkspaceImportGuardTarget, ImportVerdict, ClassifyImportInput, UnresolvedWorkspaceImportDetails, } from './plugins/unresolved-workspace-import.js';
23
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
24
  export type { NapiNodeAddonPluginOptions, AddonPackageJson } from './plugins/napi-node-addon.js';
25
- export { platformResolvePlugin, detectNativescriptPlatform, nativescriptPlatformDefines, } from './plugins/platform-resolve.js';
25
+ export { platformResolvePlugin, nativescriptSuffixChain, desktopSuffixChain, desktopOsSuffix, DESKTOP_OS_SUFFIXES, DESKTOP_REFUSED_SUFFIXES, PlatformVariantExternalError, detectNativescriptPlatform, nativescriptPlatformDefines, } from './plugins/platform-resolve.js';
26
26
  export type { PlatformResolvePluginOptions, NativescriptPlatform } from './plugins/platform-resolve.js';
27
+ export { rnRouteManifestPlugin, renderRouteManifest, walkRoutes, RouteManifestError, RN_ROUTES_MODULE_ID, MAX_ROUTE_DEPTH, } from './plugins/rn-route-manifest.js';
28
+ export type { RnRouteManifestOptions, FoundRoute } from './plugins/rn-route-manifest.js';
29
+ export { reactNativeAliasPlugin, classifyReactNativeSpecifier, REACT_NATIVE_ALIAS_TARGET, REACT_NATIVE_SPECIFIER, ReactNativeDeepImportError, ReactNativeAliasTargetMissingError, } from './plugins/react-native-alias.js';
30
+ export type { ReactNativeSpecifierKind } from './plugins/react-native-alias.js';
31
+ export { reactNativeSupportGatePlugin, loadSupportTable, findSupportViolations, formatSupportViolations, formatOpaqueReference, formatUnreadableModule, SUPPORT_TABLE_SUBPATH, WATCHED_SPECIFIERS, ReactNativeUnsupportedImportError, SupportTableUnreadableError, } from './plugins/react-native-gate.js';
32
+ export type { ReactNativeSupportGateOptions, SupportTableReader, SupportTableResolver, SupportViolation, } from './plugins/react-native-gate.js';
33
+ export { scanNamedImports, ImportScanParseError } from './utils/scan-named-imports.js';
34
+ export type { NamedImport, OpaqueReference, ScanResult } from './utils/scan-named-imports.js';
27
35
  export * from './plugin.js';
28
36
  import { gjsifyPlugin } from './plugin.js';
29
37
  export { gjsifyPlugin };
package/lib/index.js CHANGED
@@ -14,7 +14,11 @@ export { gjsImportsEmptyPlugin } from './plugins/gjs-imports-empty.js';
14
14
  export { externalsPlugin } from './plugins/externals.js';
15
15
  export { unresolvedWorkspaceImportPlugin, classifyImport, isWorkspaceSpecifier, formatUnresolvedWorkspaceImport, buildReverseAliasIndex, UnresolvedWorkspaceImportError, } from './plugins/unresolved-workspace-import.js';
16
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, detectNativescriptPlatform, nativescriptPlatformDefines, } from './plugins/platform-resolve.js';
17
+ export { platformResolvePlugin, nativescriptSuffixChain, desktopSuffixChain, desktopOsSuffix, DESKTOP_OS_SUFFIXES, DESKTOP_REFUSED_SUFFIXES, PlatformVariantExternalError, detectNativescriptPlatform, nativescriptPlatformDefines, } from './plugins/platform-resolve.js';
18
+ export { rnRouteManifestPlugin, renderRouteManifest, walkRoutes, RouteManifestError, RN_ROUTES_MODULE_ID, MAX_ROUTE_DEPTH, } from './plugins/rn-route-manifest.js';
19
+ export { reactNativeAliasPlugin, classifyReactNativeSpecifier, REACT_NATIVE_ALIAS_TARGET, REACT_NATIVE_SPECIFIER, ReactNativeDeepImportError, ReactNativeAliasTargetMissingError, } from './plugins/react-native-alias.js';
20
+ export { reactNativeSupportGatePlugin, loadSupportTable, findSupportViolations, formatSupportViolations, formatOpaqueReference, formatUnreadableModule, SUPPORT_TABLE_SUBPATH, WATCHED_SPECIFIERS, ReactNativeUnsupportedImportError, SupportTableUnreadableError, } from './plugins/react-native-gate.js';
21
+ export { scanNamedImports, ImportScanParseError } from './utils/scan-named-imports.js';
18
22
  export * from './plugin.js';
19
23
  import { gjsifyPlugin } from './plugin.js';
20
24
  export { gjsifyPlugin };
@@ -1,19 +1,144 @@
1
1
  import type { Plugin } from 'rolldown';
2
2
  export type NativescriptPlatform = 'android' | 'ios' | 'visionos';
3
+ /**
4
+ * The desktop suffix for one host OS, keyed by `process.platform`.
5
+ *
6
+ * TWO VOCABULARIES MEET HERE and neither is negotiable. The KEYS are
7
+ * `process.platform`'s (`linux`/`darwin`/`win32`) — ADR 0018's target set, the
8
+ * spelling `gjsify.os` and `gjsify.platforms` use, and what a running process
9
+ * computes about itself. The VALUES are React Native's `Platform.OS` spelling
10
+ * (`linux`/`macos`/`windows`), which is what ADR 0032 § 9 writes and what
11
+ * `@gjsify/react-native`'s own `Platform.OS` reports — so a `foo.macos.tsx`
12
+ * agrees with the `Platform.OS === 'macos'` branch beside it. Spelling the file
13
+ * `foo.darwin.tsx` would make those two disagree in the one place a reader looks
14
+ * to check them against each other.
15
+ *
16
+ * Exported so `platform-resolve.spec.ts` can iterate the real values instead of
17
+ * a copy: nothing in the type system stops someone adding a `web` row here, and
18
+ * that row is exactly what § 9 forbids.
19
+ */
20
+ export declare const DESKTOP_OS_SUFFIXES: Readonly<Record<string, string>>;
21
+ /**
22
+ * Suffixes that must NEVER be a rung of the DESKTOP chain, with § 9's reason.
23
+ *
24
+ * `.native` looks like an oversight and is the opposite: a `.native.tsx` is by
25
+ * definition written for a React Native runtime, so reaching for it would
26
+ * silently feed the GTK build code that expects `NativeModules` — a bridge that
27
+ * exists only in the phone host. No import error, no type error, no failing
28
+ * test; the divergence surfaces as a window that does not work. Falling through
29
+ * to the BASE file is the honest outcome, because the base file at least has to
30
+ * compile against what this target really has.
31
+ *
32
+ * `.web` is worse. It reads as the obvious pick for a desktop target — a desktop
33
+ * is not a phone, so surely the web variant? — and it carries exactly the DOM
34
+ * assumptions ADR 0032 rules out, the same two-lossy-mappings-stacked shape that
35
+ * rules out react-native-web over a DOM (§ 3, Rejected).
36
+ *
37
+ * Not silence, though: when the desktop chain falls through to base and one of
38
+ * these siblings exists, {@link platformResolvePlugin} warns naming the file.
39
+ * Without that, an author's fork is dead code with nothing anywhere saying so.
40
+ */
41
+ export declare const DESKTOP_REFUSED_SUFFIXES: readonly string[];
42
+ /**
43
+ * The NativeScript chain: platform-specific first, then the platform-agnostic
44
+ * `native` suffix. Without a known platform only `native` applies.
45
+ */
46
+ export declare function nativescriptSuffixChain(platform?: NativescriptPlatform): readonly string[];
47
+ /**
48
+ * The GTK/desktop chain (ADR 0032 § 9): `.gtk` → `.<os>` → `.desktop` → base.
49
+ *
50
+ * `.gtk` outranks the OS because a GTK fork is a statement about the TOOLKIT,
51
+ * which is the more specific claim: `foo.gtk.tsx` says "this is how the widget
52
+ * tree does it", `foo.linux.tsx` says "this is how this kernel does it", and a
53
+ * project holding both means the first on a GTK build.
54
+ *
55
+ * `os` is a suffix from {@link DESKTOP_OS_SUFFIXES}, never a `process.platform`
56
+ * token — see {@link desktopOsSuffix}. Omitting it drops the OS rung rather than
57
+ * guessing one.
58
+ */
59
+ export declare function desktopSuffixChain(os?: string): readonly string[];
60
+ /**
61
+ * The desktop suffix for the BUILD HOST, or `undefined` for a host outside
62
+ * ADR 0018's target set.
63
+ *
64
+ * The host, not a cross-target: there is no cross-OS desktop build in this chain
65
+ * (ADR 0032's Consequences put macOS and Windows on Node + `@gjsify/node-gi`,
66
+ * whose installable artifact is ADR 0024 stages 4 and 5 and does not exist yet).
67
+ * A future cross-build states its own suffix by passing one to
68
+ * {@link desktopSuffixChain}, which is why the decision function takes the value
69
+ * and only this one function reads the ambient.
70
+ *
71
+ * The ambient read is `process.platform` behind a defaulted parameter — the
72
+ * shape `extractPackageSpec` and `isAbsoluteFsPath` already use in this package.
73
+ * The canonical repo-wide spelling is `@gjsify/utils/core`'s `hostOs()`, and
74
+ * `os-axis.mjs` sanctions infra keeping its own raw read for the purity reason
75
+ * `cli/src/utils/platform-check.ts` documents. Here there is a second, harder
76
+ * reason: `@gjsify/utils` is BUILT BY the CLI this package is part of, so
77
+ * depending on it would make `build:infra` need its own output.
78
+ * `package.json#gjsify.os` on this package already carries the ADR 0018 claim.
79
+ */
80
+ export declare function desktopOsSuffix(platform?: string): string | undefined;
3
81
  export interface PlatformResolvePluginOptions {
4
82
  /**
5
- * Target platform. When omitted, only `.native.*` variants are tried
6
- * (the platform-specific `.android.*` / `.ios.*` lookups are skipped).
83
+ * The suffix chain, most specific first. Build it with
84
+ * {@link nativescriptSuffixChain} or {@link desktopSuffixChain} — a
85
+ * hand-written array is a second resolution order the tree does not state.
86
+ */
87
+ suffixes: readonly string[];
88
+ /**
89
+ * Suffixes deliberately NOT resolved, but whose presence on disk is worth
90
+ * saying out loud: {@link DESKTOP_REFUSED_SUFFIXES} for the desktop chain,
91
+ * empty for NativeScript (where `.native` is a real rung). Probed only after
92
+ * the chain found nothing, so an empty list costs zero extra resolves and
93
+ * the NS path is byte-unchanged.
7
94
  */
8
- platform?: NativescriptPlatform;
95
+ refusedSuffixes?: readonly string[];
96
+ /**
97
+ * Consult the importer's directory listing before entering the resolver.
98
+ *
99
+ * MEASURED, and the reason this option exists at all. The NativeScript chain
100
+ * runs on one build target; the desktop chain runs on every `--app gjs` and
101
+ * `--app node` build in the repository, and a three-rung chain costs up to
102
+ * six failed `this.resolve` calls per relative import. On this repository's
103
+ * largest bundle (`dist/cli.gjs.mjs`, ~1400 modules) that was 21.9–22.2 s
104
+ * without the plugin against 25.0–26.3 s with it — about +14%, and Rolldown
105
+ * reported the plugin as 49% of all plugin time. Skipping `node_modules`
106
+ * importers recovered under a second of it: the cost is FIRST-PARTY imports,
107
+ * which is exactly the population the feature is for.
108
+ *
109
+ * So the filter is a directory listing, cached per directory: one
110
+ * `readdirSync` answers every suffix for every import in that directory, and
111
+ * the resolver is entered only for a candidate that has a matching sibling.
112
+ * Same bundle, 22.0–22.8 s and 25% of plugin time — the surcharge is gone and
113
+ * the plugin now sits alongside its four siblings instead of ahead of them.
114
+ * The listing is a FILTER and never an answer — a directory it cannot read
115
+ * (a virtual importer id, a `\0`-prefixed module) falls straight through to
116
+ * the resolver, and names are compared lowercased so a case-insensitive
117
+ * filesystem can only make the filter more permissive, never less.
118
+ *
119
+ * OFF for NativeScript, deliberately: `@nativescript/core` ships
120
+ * `.android`/`.ios` siblings and imports them relatively, and the mandate on
121
+ * that chain is byte-identical behaviour — a filter that another plugin's
122
+ * virtual `resolveId` could see past is not something to switch on there
123
+ * without a measurement of that tree.
124
+ */
125
+ siblingIndex?: boolean;
126
+ }
127
+ /** Thrown when a variant is on disk but the resolver hands it back as external. */
128
+ export declare class PlatformVariantExternalError extends Error {
129
+ readonly candidate: string;
130
+ readonly importer: string;
131
+ readonly name = "PlatformVariantExternalError";
132
+ constructor(candidate: string, importer: string);
9
133
  }
10
134
  /**
11
- * Resolve platform-specific source-file variants (`*.android` / `*.ios` /
12
- * `*.native`) ahead of the base file. Relative imports only — bare/package
13
- * specifiers are left to the normal resolver chain (package platform-`main`
14
- * fields are a separate, rarer concern handled by the alias layer).
135
+ * Resolve platform-specific source-file variants ahead of the base file.
136
+ *
137
+ * Relative imports only — bare/package specifiers are left to the normal
138
+ * resolver chain (package platform-`main` fields are a separate, rarer concern
139
+ * handled by the alias layer).
15
140
  */
16
- export declare function platformResolvePlugin(options?: PlatformResolvePluginOptions): Plugin;
141
+ export declare function platformResolvePlugin(options: PlatformResolvePluginOptions): Plugin;
17
142
  /**
18
143
  * Best-effort detection of the NativeScript target platform from the
19
144
  * environment NS' CLI sets when it spawns a bundler (`NATIVESCRIPT_BUNDLER_ENV`