@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.
- package/README.md +11 -0
- package/lib/app/browser.js +28 -0
- package/lib/app/gjs.js +14 -2
- package/lib/app/nativescript.js +15 -3
- package/lib/app/node.d.ts +1 -1
- package/lib/app/node.js +26 -10
- package/lib/index.d.ts +4 -2
- package/lib/index.js +4 -2
- package/lib/plugins/console-assign.d.ts +13 -0
- package/lib/plugins/console-assign.js +125 -0
- package/lib/plugins/css-as-string.js +126 -44
- package/lib/plugins/gi-renderer.d.ts +1 -1
- package/lib/plugins/gi-renderer.js +1 -1
- package/lib/plugins/gi-runtime-paths.js +5 -2
- package/lib/plugins/napi-node-addon.d.ts +48 -4
- package/lib/plugins/napi-node-addon.js +261 -85
- package/lib/plugins/node-native-external.d.ts +20 -0
- package/lib/plugins/node-native-external.js +145 -0
- package/lib/plugins/platform-resolve.d.ts +95 -5
- package/lib/plugins/platform-resolve.js +132 -11
- package/lib/plugins/rewrite-node-modules-paths.d.ts +18 -1
- package/lib/plugins/rewrite-node-modules-paths.js +181 -25
- package/lib/plugins/unresolved-workspace-import.js +89 -68
- package/lib/shims/addon-resolve.d.ts +19 -0
- package/lib/shims/addon-resolve.js +193 -0
- package/lib/utils/addon-platform.d.ts +23 -0
- package/lib/utils/addon-platform.js +50 -0
- package/lib/utils/auto-globals.d.ts +6 -0
- package/lib/utils/auto-globals.js +92 -15
- package/lib/utils/declare-build-input.d.ts +13 -0
- package/lib/utils/declare-build-input.js +37 -0
- package/lib/utils/detect-free-globals.d.ts +3 -0
- package/lib/utils/detect-free-globals.js +1 -1
- package/lib/utils/entry-wrapper.js +6 -1
- package/lib/utils/index.d.ts +1 -0
- package/lib/utils/index.js +1 -0
- package/lib/utils/inline-static-reads.d.ts +15 -1
- package/lib/utils/inline-static-reads.js +16 -8
- package/lib/utils/runtime.js +1 -1
- package/lib/utils/scan-globals.d.ts +2 -2
- package/lib/utils/scan-globals.js +20 -14
- package/lib/utils/zip-path.d.ts +9 -0
- package/lib/utils/zip-path.js +12 -0
- package/package.json +13 -9
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// For `--app node`: keep every Node-API addon PACKAGE external, so Node loads it from
|
|
2
|
+
// `node_modules` exactly as it would without a bundler.
|
|
3
|
+
//
|
|
4
|
+
// An addon package finds its `.node` binary relative to ITS OWN files —
|
|
5
|
+
// `require('node-gyp-build')(__dirname)`, `bindings('x')`, `require('../build/Release/x.node')`,
|
|
6
|
+
// napi-rs' `require(`./x.${triple}.node`)`. Bundled, `__dirname` / `import.meta.dirname`
|
|
7
|
+
// is the bundle's directory, and the lookup misses. Measured on
|
|
8
|
+
// `@signalapp/libsignal-client`: `node-gyp-build(import.meta.dirname + '/..')` threw
|
|
9
|
+
// "No native build was found" from an `--app node` bundle that ran fine as source.
|
|
10
|
+
// `bufferutil` fails SILENTLY instead — its `try { node-gyp-build } catch { fallback }`
|
|
11
|
+
// quietly swaps the native build for the JS one.
|
|
12
|
+
//
|
|
13
|
+
// External, not copied: `--app gjs` has to rewrite an addon (`napiNodeAddonPlugin`
|
|
14
|
+
// routes it through `@gjsify/napi`) because GJS cannot load one itself, but Node can,
|
|
15
|
+
// and every loader convention resolves against a package-shaped directory, not a flat
|
|
16
|
+
// prebuild next to the bundle. So an `--app node` bundle of a native dependency needs
|
|
17
|
+
// `node_modules` at runtime, like any `npm install`-ed program.
|
|
18
|
+
//
|
|
19
|
+
// Detection is by PACKAGE, from its manifest and layout, never by source sniffing, and
|
|
20
|
+
// shares the napi-rs + gjsify-bridge rules with `napiNodeAddonPlugin`.
|
|
21
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
22
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
23
|
+
import { isGjsifyNativeBridge, isNapiRsPackageJson } from './napi-node-addon.js';
|
|
24
|
+
// oxlint-disable-next-line no-control-regex -- NUL marks a bundler virtual id, never a package.
|
|
25
|
+
const BARE_SPECIFIER_RE = /^[^.\\/:\s\x00][^:\s\x00]*$/;
|
|
26
|
+
/** Runtime helpers whose only job is locating an addon binary next to the package. */
|
|
27
|
+
const ADDON_LOADER_DEPENDENCIES = [
|
|
28
|
+
'node-gyp-build',
|
|
29
|
+
'node-gyp-build-optional-packages',
|
|
30
|
+
'bindings',
|
|
31
|
+
'prebuild-install',
|
|
32
|
+
'node-pre-gyp',
|
|
33
|
+
'@mapbox/node-pre-gyp',
|
|
34
|
+
'cmake-js',
|
|
35
|
+
];
|
|
36
|
+
function readdirSafe(dir) {
|
|
37
|
+
try {
|
|
38
|
+
return readdirSync(dir);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** A `.node` file in `dir` or one directory below it (`prebuilds/<target>/`, `build/Release/`). */
|
|
45
|
+
function hasNodeBinary(dir) {
|
|
46
|
+
for (const name of readdirSafe(dir)) {
|
|
47
|
+
if (name.endsWith('.node'))
|
|
48
|
+
return true;
|
|
49
|
+
if (readdirSafe(join(dir, name)).some((n) => n.endsWith('.node')))
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Is the package at `pkgRoot` a Node-API addon — one whose code loads a `.node`
|
|
56
|
+
* relative to its own directory? True on any of: `gypfile`, a `binding.gyp`, a
|
|
57
|
+
* node-pre-gyp `binary` block, a dependency on an addon loader, the napi-rs manifest
|
|
58
|
+
* signals, or a `.node` under `prebuilds/` or `build/`. A gjsify native bridge is
|
|
59
|
+
* never one: it ships a GI typelib loaded through `gi://`.
|
|
60
|
+
*/
|
|
61
|
+
export function isNodeAddonPackage(pkgRoot, pkg) {
|
|
62
|
+
if (isGjsifyNativeBridge(pkg))
|
|
63
|
+
return false;
|
|
64
|
+
if (pkg.gypfile === true)
|
|
65
|
+
return true;
|
|
66
|
+
if (pkg.binary !== null && typeof pkg.binary === 'object')
|
|
67
|
+
return true;
|
|
68
|
+
if (existsSync(join(pkgRoot, 'binding.gyp')))
|
|
69
|
+
return true;
|
|
70
|
+
const deps = { ...pkg.dependencies, ...pkg.optionalDependencies };
|
|
71
|
+
if (ADDON_LOADER_DEPENDENCIES.some((name) => name in deps))
|
|
72
|
+
return true;
|
|
73
|
+
if (isNapiRsPackageJson(pkg))
|
|
74
|
+
return true;
|
|
75
|
+
return hasNodeBinary(join(pkgRoot, 'prebuilds')) || hasNodeBinary(join(pkgRoot, 'build'));
|
|
76
|
+
}
|
|
77
|
+
/** `@scope/name/sub` → `@scope/name`, `name/sub` → `name`; null for anything not bare. */
|
|
78
|
+
export function packageNameOf(specifier) {
|
|
79
|
+
if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('\0'))
|
|
80
|
+
return null;
|
|
81
|
+
if (specifier.includes(':'))
|
|
82
|
+
return null; // node:fs, gi://Gtk, data:, C:\…
|
|
83
|
+
const parts = specifier.split('/');
|
|
84
|
+
if (specifier.startsWith('@'))
|
|
85
|
+
return parts.length >= 2 && parts[1] ? `${parts[0]}/${parts[1]}` : null;
|
|
86
|
+
return parts[0] || null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The root of package `name` that `file` belongs to: the nearest ancestor whose
|
|
90
|
+
* `package.json` is NAMED `name`. Matching on the name skips the nested
|
|
91
|
+
* `{ "type": "module" }` manifests dual packages put in `dist/esm/`.
|
|
92
|
+
*/
|
|
93
|
+
function packageRootFor(file, name) {
|
|
94
|
+
let dir = dirname(file);
|
|
95
|
+
for (let i = 0; i < 64; i++) {
|
|
96
|
+
const manifest = join(dir, 'package.json');
|
|
97
|
+
if (existsSync(manifest)) {
|
|
98
|
+
try {
|
|
99
|
+
const pkg = JSON.parse(readFileSync(manifest, 'utf8'));
|
|
100
|
+
if (pkg.name === name)
|
|
101
|
+
return { root: dir, pkg };
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// An unreadable manifest is not this package's — keep walking up.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const parent = dirname(dir);
|
|
108
|
+
if (parent === dir)
|
|
109
|
+
break;
|
|
110
|
+
dir = parent;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
/** Keeps every import of a Node-API addon package external on `--app node`. */
|
|
115
|
+
export function nodeNativeExternalPlugin() {
|
|
116
|
+
// Keyed by package name + importer directory: node resolution depends on both.
|
|
117
|
+
const verdicts = new Map();
|
|
118
|
+
return {
|
|
119
|
+
name: 'gjsify-node-native-external',
|
|
120
|
+
resolveId: {
|
|
121
|
+
order: 'pre',
|
|
122
|
+
// Rust-regex compatible (no lookaround, NUL as \x00): `@gjsify/rolldown-native`
|
|
123
|
+
// hands it to the Rust core as a string. Bare specifiers only.
|
|
124
|
+
filter: { id: BARE_SPECIFIER_RE },
|
|
125
|
+
async handler(source, rawImporter) {
|
|
126
|
+
const name = packageNameOf(source);
|
|
127
|
+
if (name === null)
|
|
128
|
+
return null;
|
|
129
|
+
const importer = typeof rawImporter === 'string' ? rawImporter : undefined;
|
|
130
|
+
const key = `${name}\0${importer === undefined ? '' : dirname(importer)}`;
|
|
131
|
+
let native = verdicts.get(key);
|
|
132
|
+
if (native === undefined) {
|
|
133
|
+
native = false;
|
|
134
|
+
const resolved = await this.resolve(source, importer, { skipSelf: true });
|
|
135
|
+
if (resolved && !resolved.external && isAbsolute(resolved.id) && existsSync(resolved.id)) {
|
|
136
|
+
const owner = packageRootFor(resolved.id, name);
|
|
137
|
+
native = owner !== null && isNodeAddonPackage(owner.root, owner.pkg);
|
|
138
|
+
}
|
|
139
|
+
verdicts.set(key, native);
|
|
140
|
+
}
|
|
141
|
+
return native ? { id: source, external: true } : null;
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -39,6 +39,48 @@ export declare const DESKTOP_OS_SUFFIXES: Readonly<Record<string, string>>;
|
|
|
39
39
|
* Without that, an author's fork is dead code with nothing anywhere saying so.
|
|
40
40
|
*/
|
|
41
41
|
export declare const DESKTOP_REFUSED_SUFFIXES: readonly string[];
|
|
42
|
+
/**
|
|
43
|
+
* Suffixes that must NEVER be a rung of the BROWSER chain — the mirror of
|
|
44
|
+
* {@link DESKTOP_REFUSED_SUFFIXES}, built by the same rule read from the other
|
|
45
|
+
* side: **refuse the other families' UMBRELLA rungs, not their per-target
|
|
46
|
+
* spellings.** The desktop list names `.web` (the whole browser chain) and
|
|
47
|
+
* `.native` (the phone family's umbrella) and deliberately leaves
|
|
48
|
+
* `.android`/`.ios`/`.visionos` out; from here the same rule names the GTK
|
|
49
|
+
* desktop family's two non-OS rungs plus that same phone umbrella.
|
|
50
|
+
*
|
|
51
|
+
* `.gtk` is the one a reader reaches for while chasing a missing widget, and it
|
|
52
|
+
* is the worst possible answer: `--app browser` redirects `gi://*` and `@girs/*`
|
|
53
|
+
* to an EMPTY module, so a `.gtk.tsx` reaching `Adw.ActionRow` does not fail to
|
|
54
|
+
* import — it gets `{}`, and `class X extends Adw.ActionRow` throws
|
|
55
|
+
* `Class extends value undefined` at load. That is measured, and the ADR 0034
|
|
56
|
+
* stage 9 e2e keeps exactly that row as its control.
|
|
57
|
+
*
|
|
58
|
+
* `.desktop` is named as well as `.gtk`, though only one of the two appears in
|
|
59
|
+
* this repository today: § 9 writes `.desktop` as the umbrella over `.gtk` and
|
|
60
|
+
* the three OS spellings, so refusing one and not the other is a gap, and a gap
|
|
61
|
+
* in a refusal list reads as a judgement that the omitted one is fine here.
|
|
62
|
+
*
|
|
63
|
+
* `.native` is the phone bridge — `NativeModules` exists in neither a browser
|
|
64
|
+
* nor a GTK host, which is why both lists carry it.
|
|
65
|
+
*
|
|
66
|
+
* NOT here, and each is a DECISION rather than an omission:
|
|
67
|
+
* - **the three OS spellings** (`linux`/`macos`/`windows`). They only ever
|
|
68
|
+
* appear as rungs of the desktop chain, whose umbrella is already named — the
|
|
69
|
+
* same reason `.android` is absent from the desktop list. Naming them would
|
|
70
|
+
* put a warning line on every OS-forked module of a normal dual-target tree,
|
|
71
|
+
* which is how a warning gets switched off.
|
|
72
|
+
* - **a refusal list for NativeScript at all.** That chain runs with
|
|
73
|
+
* `siblingIndex` OFF under a byte-identical mandate, and with no listing to
|
|
74
|
+
* filter them a refusal list costs two real `this.resolve` calls on every
|
|
75
|
+
* relative import that has no variant — the majority, inside
|
|
76
|
+
* `@nativescript/core` included. The desktop chain's own measurement (+14% on
|
|
77
|
+
* a ~1400-module bundle from six failed resolves) is what says failed resolves
|
|
78
|
+
* are not free. It is a cost with a measured-zero benefit on this tree: the
|
|
79
|
+
* 30 `.gtk.*`/`.native.*` pairs in `@gjsify/adwaita-react-native` all HIT the
|
|
80
|
+
* NS chain at `.native`, and the refusal probe only runs when the chain found
|
|
81
|
+
* nothing. Reopen it from a measurement of that tree with the index on.
|
|
82
|
+
*/
|
|
83
|
+
export declare const BROWSER_REFUSED_SUFFIXES: readonly string[];
|
|
42
84
|
/**
|
|
43
85
|
* The NativeScript chain: platform-specific first, then the platform-agnostic
|
|
44
86
|
* `native` suffix. Without a known platform only `native` applies.
|
|
@@ -57,6 +99,37 @@ export declare function nativescriptSuffixChain(platform?: NativescriptPlatform)
|
|
|
57
99
|
* guessing one.
|
|
58
100
|
*/
|
|
59
101
|
export declare function desktopSuffixChain(os?: string): readonly string[];
|
|
102
|
+
/**
|
|
103
|
+
* The BROWSER chain: `.web` → base. Exactly ONE rung, and the count is the
|
|
104
|
+
* decision.
|
|
105
|
+
*
|
|
106
|
+
* A desktop build knows three different things about itself and so gets three
|
|
107
|
+
* rungs: the toolkit (`.gtk`), the kernel (`.<os>`) and the target family
|
|
108
|
+
* (`.desktop`). A `--app browser` build knows ONE — it targets the DOM — and
|
|
109
|
+
* every candidate second rung was rejected for a stated reason rather than left
|
|
110
|
+
* out for tidiness:
|
|
111
|
+
*
|
|
112
|
+
* - **No engine rung** (`.firefox` / `.chromium`). The target compiles one
|
|
113
|
+
* `esnext` bundle with no per-engine branch anywhere in it; a rung nobody can
|
|
114
|
+
* fill is a resolution rule that can only ever surprise someone.
|
|
115
|
+
* - **No OS rung.** One browser bundle is served to every operating system, so
|
|
116
|
+
* an OS rung would resolve against the BUILD HOST and bake it into a
|
|
117
|
+
* platform-neutral artifact. That is the mistake `plugins/gi-runtime-paths.ts`
|
|
118
|
+
* already documents for the GI prologue, committed here at resolution time
|
|
119
|
+
* where the evidence is a file name and not a code path.
|
|
120
|
+
* - **No `.dom` synonym.** Two spellings for one concept is a priority order
|
|
121
|
+
* someone has to memorise, and `.web` is what § 9 writes and what every
|
|
122
|
+
* `.web.*` file in this repository already uses.
|
|
123
|
+
* - **No `.browser` umbrella.** `.desktop` earns its rung by sitting above four
|
|
124
|
+
* spellings; above `.web` there is nothing for an umbrella to cover.
|
|
125
|
+
*
|
|
126
|
+
* Takes no parameter, unlike {@link desktopSuffixChain}: nothing about this
|
|
127
|
+
* chain depends on the host, which is the previous point restated from the API
|
|
128
|
+
* side. It is a function rather than a constant so that
|
|
129
|
+
* {@link PlatformResolvePluginOptions.suffixes} keeps its rule — a hand-written
|
|
130
|
+
* array is a second resolution order the tree does not state.
|
|
131
|
+
*/
|
|
132
|
+
export declare function browserSuffixChain(): readonly string[];
|
|
60
133
|
/**
|
|
61
134
|
* The desktop suffix for the BUILD HOST, or `undefined` for a host outside
|
|
62
135
|
* ADR 0018's target set.
|
|
@@ -81,16 +154,19 @@ export declare function desktopOsSuffix(platform?: string): string | undefined;
|
|
|
81
154
|
export interface PlatformResolvePluginOptions {
|
|
82
155
|
/**
|
|
83
156
|
* The suffix chain, most specific first. Build it with
|
|
84
|
-
* {@link nativescriptSuffixChain}
|
|
85
|
-
* hand-written array is a second resolution
|
|
157
|
+
* {@link nativescriptSuffixChain}, {@link desktopSuffixChain} or
|
|
158
|
+
* {@link browserSuffixChain} — a hand-written array is a second resolution
|
|
159
|
+
* order the tree does not state.
|
|
86
160
|
*/
|
|
87
161
|
suffixes: readonly string[];
|
|
88
162
|
/**
|
|
89
163
|
* Suffixes deliberately NOT resolved, but whose presence on disk is worth
|
|
90
164
|
* saying out loud: {@link DESKTOP_REFUSED_SUFFIXES} for the desktop chain,
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
165
|
+
* {@link BROWSER_REFUSED_SUFFIXES} for the browser chain, empty for
|
|
166
|
+
* NativeScript — where `.native` is a real rung and the rest is a measured
|
|
167
|
+
* decision written out on `BROWSER_REFUSED_SUFFIXES`. Probed only after the
|
|
168
|
+
* chain found nothing, so an empty list costs zero extra resolves and the NS
|
|
169
|
+
* path is byte-unchanged.
|
|
94
170
|
*/
|
|
95
171
|
refusedSuffixes?: readonly string[];
|
|
96
172
|
/**
|
|
@@ -121,6 +197,20 @@ export interface PlatformResolvePluginOptions {
|
|
|
121
197
|
* that chain is byte-identical behaviour — a filter that another plugin's
|
|
122
198
|
* virtual `resolveId` could see past is not something to switch on there
|
|
123
199
|
* without a measurement of that tree.
|
|
200
|
+
*
|
|
201
|
+
* ON WHEREVER THE CHAIN CARRIES A REFUSAL LIST, and that is not a preference.
|
|
202
|
+
* A refused suffix is probed on every miss, so a chain with refusals pays its
|
|
203
|
+
* probes on the MAJORITY of imports rather than the few that have a variant.
|
|
204
|
+
* Measured on `tests/e2e/vite-plugin-gjsify`'s fixture with the browser chain
|
|
205
|
+
* (one rung, three refusals) under `--max-old-space-size=4096`: index off is
|
|
206
|
+
* 1172 probes and `FATAL ERROR: Reached heap limit`; index on is 0 probes,
|
|
207
|
+
* 0.25 s and 191 MB. Under Vite each probe re-enters the whole plugin
|
|
208
|
+
* container, which is where the memory goes. Leaving the index off is
|
|
209
|
+
* therefore a choice only a chain with NO refusal list can afford.
|
|
210
|
+
*
|
|
211
|
+
* Kept fresh by {@link platformResolvePlugin}'s `watchChange` hook, so a
|
|
212
|
+
* long-lived host (a Vite dev server, `gjsify build --watch`) does not answer
|
|
213
|
+
* from a directory listing taken before the author added the variant.
|
|
124
214
|
*/
|
|
125
215
|
siblingIndex?: boolean;
|
|
126
216
|
}
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
// Platform file resolution — ONE plugin, ONE resolution order,
|
|
1
|
+
// Platform file resolution — ONE plugin, ONE resolution order, three chains.
|
|
2
2
|
//
|
|
3
3
|
// Lets a shared codebase fork a single module per target by file name. An
|
|
4
4
|
// `import './foo'` (or `import './foo.js'`) resolves to the most specific
|
|
5
5
|
// variant on disk, in the priority order the caller's chain declares:
|
|
6
6
|
//
|
|
7
|
-
// NativeScript (`--app nativescript`)
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
7
|
+
// NativeScript (`--app nativescript`) GTK / desktop (`--app gjs|node`)
|
|
8
|
+
// ./foo.<android|ios|visionos>.<ext> ./foo.gtk.<ext>
|
|
9
|
+
// ./foo.native.<ext> ./foo.<linux|macos|windows>.<ext>
|
|
10
|
+
// ./foo.<ext> ./foo.desktop.<ext>
|
|
11
|
+
// ./foo.<ext>
|
|
12
12
|
//
|
|
13
|
-
//
|
|
13
|
+
// Browser (`--app browser`)
|
|
14
|
+
// ./foo.web.<ext>
|
|
15
|
+
// ./foo.<ext>
|
|
16
|
+
//
|
|
17
|
+
// THREE CHAINS, NOT THREE PLUGINS (ADR 0032 § 9). A second plugin would give the
|
|
14
18
|
// tree two resolution orders that no single file states, and the order IS the
|
|
15
19
|
// contract — `.gtk` before `.<os>` before `.desktop` is a decision, not an
|
|
16
20
|
// implementation detail. So the chain is a PARAMETER and the builders below are
|
|
@@ -82,6 +86,48 @@ export const DESKTOP_OS_SUFFIXES = {
|
|
|
82
86
|
* Without that, an author's fork is dead code with nothing anywhere saying so.
|
|
83
87
|
*/
|
|
84
88
|
export const DESKTOP_REFUSED_SUFFIXES = ['native', 'web'];
|
|
89
|
+
/**
|
|
90
|
+
* Suffixes that must NEVER be a rung of the BROWSER chain — the mirror of
|
|
91
|
+
* {@link DESKTOP_REFUSED_SUFFIXES}, built by the same rule read from the other
|
|
92
|
+
* side: **refuse the other families' UMBRELLA rungs, not their per-target
|
|
93
|
+
* spellings.** The desktop list names `.web` (the whole browser chain) and
|
|
94
|
+
* `.native` (the phone family's umbrella) and deliberately leaves
|
|
95
|
+
* `.android`/`.ios`/`.visionos` out; from here the same rule names the GTK
|
|
96
|
+
* desktop family's two non-OS rungs plus that same phone umbrella.
|
|
97
|
+
*
|
|
98
|
+
* `.gtk` is the one a reader reaches for while chasing a missing widget, and it
|
|
99
|
+
* is the worst possible answer: `--app browser` redirects `gi://*` and `@girs/*`
|
|
100
|
+
* to an EMPTY module, so a `.gtk.tsx` reaching `Adw.ActionRow` does not fail to
|
|
101
|
+
* import — it gets `{}`, and `class X extends Adw.ActionRow` throws
|
|
102
|
+
* `Class extends value undefined` at load. That is measured, and the ADR 0034
|
|
103
|
+
* stage 9 e2e keeps exactly that row as its control.
|
|
104
|
+
*
|
|
105
|
+
* `.desktop` is named as well as `.gtk`, though only one of the two appears in
|
|
106
|
+
* this repository today: § 9 writes `.desktop` as the umbrella over `.gtk` and
|
|
107
|
+
* the three OS spellings, so refusing one and not the other is a gap, and a gap
|
|
108
|
+
* in a refusal list reads as a judgement that the omitted one is fine here.
|
|
109
|
+
*
|
|
110
|
+
* `.native` is the phone bridge — `NativeModules` exists in neither a browser
|
|
111
|
+
* nor a GTK host, which is why both lists carry it.
|
|
112
|
+
*
|
|
113
|
+
* NOT here, and each is a DECISION rather than an omission:
|
|
114
|
+
* - **the three OS spellings** (`linux`/`macos`/`windows`). They only ever
|
|
115
|
+
* appear as rungs of the desktop chain, whose umbrella is already named — the
|
|
116
|
+
* same reason `.android` is absent from the desktop list. Naming them would
|
|
117
|
+
* put a warning line on every OS-forked module of a normal dual-target tree,
|
|
118
|
+
* which is how a warning gets switched off.
|
|
119
|
+
* - **a refusal list for NativeScript at all.** That chain runs with
|
|
120
|
+
* `siblingIndex` OFF under a byte-identical mandate, and with no listing to
|
|
121
|
+
* filter them a refusal list costs two real `this.resolve` calls on every
|
|
122
|
+
* relative import that has no variant — the majority, inside
|
|
123
|
+
* `@nativescript/core` included. The desktop chain's own measurement (+14% on
|
|
124
|
+
* a ~1400-module bundle from six failed resolves) is what says failed resolves
|
|
125
|
+
* are not free. It is a cost with a measured-zero benefit on this tree: the
|
|
126
|
+
* 30 `.gtk.*`/`.native.*` pairs in `@gjsify/adwaita-react-native` all HIT the
|
|
127
|
+
* NS chain at `.native`, and the refusal probe only runs when the chain found
|
|
128
|
+
* nothing. Reopen it from a measurement of that tree with the index on.
|
|
129
|
+
*/
|
|
130
|
+
export const BROWSER_REFUSED_SUFFIXES = ['gtk', 'desktop', 'native'];
|
|
85
131
|
/**
|
|
86
132
|
* The NativeScript chain: platform-specific first, then the platform-agnostic
|
|
87
133
|
* `native` suffix. Without a known platform only `native` applies.
|
|
@@ -104,6 +150,39 @@ export function nativescriptSuffixChain(platform) {
|
|
|
104
150
|
export function desktopSuffixChain(os) {
|
|
105
151
|
return os ? ['gtk', os, 'desktop'] : ['gtk', 'desktop'];
|
|
106
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* The BROWSER chain: `.web` → base. Exactly ONE rung, and the count is the
|
|
155
|
+
* decision.
|
|
156
|
+
*
|
|
157
|
+
* A desktop build knows three different things about itself and so gets three
|
|
158
|
+
* rungs: the toolkit (`.gtk`), the kernel (`.<os>`) and the target family
|
|
159
|
+
* (`.desktop`). A `--app browser` build knows ONE — it targets the DOM — and
|
|
160
|
+
* every candidate second rung was rejected for a stated reason rather than left
|
|
161
|
+
* out for tidiness:
|
|
162
|
+
*
|
|
163
|
+
* - **No engine rung** (`.firefox` / `.chromium`). The target compiles one
|
|
164
|
+
* `esnext` bundle with no per-engine branch anywhere in it; a rung nobody can
|
|
165
|
+
* fill is a resolution rule that can only ever surprise someone.
|
|
166
|
+
* - **No OS rung.** One browser bundle is served to every operating system, so
|
|
167
|
+
* an OS rung would resolve against the BUILD HOST and bake it into a
|
|
168
|
+
* platform-neutral artifact. That is the mistake `plugins/gi-runtime-paths.ts`
|
|
169
|
+
* already documents for the GI prologue, committed here at resolution time
|
|
170
|
+
* where the evidence is a file name and not a code path.
|
|
171
|
+
* - **No `.dom` synonym.** Two spellings for one concept is a priority order
|
|
172
|
+
* someone has to memorise, and `.web` is what § 9 writes and what every
|
|
173
|
+
* `.web.*` file in this repository already uses.
|
|
174
|
+
* - **No `.browser` umbrella.** `.desktop` earns its rung by sitting above four
|
|
175
|
+
* spellings; above `.web` there is nothing for an umbrella to cover.
|
|
176
|
+
*
|
|
177
|
+
* Takes no parameter, unlike {@link desktopSuffixChain}: nothing about this
|
|
178
|
+
* chain depends on the host, which is the previous point restated from the API
|
|
179
|
+
* side. It is a function rather than a constant so that
|
|
180
|
+
* {@link PlatformResolvePluginOptions.suffixes} keeps its rule — a hand-written
|
|
181
|
+
* array is a second resolution order the tree does not state.
|
|
182
|
+
*/
|
|
183
|
+
export function browserSuffixChain() {
|
|
184
|
+
return ['web'];
|
|
185
|
+
}
|
|
107
186
|
/**
|
|
108
187
|
* The desktop suffix for the BUILD HOST, or `undefined` for a host outside
|
|
109
188
|
* ADR 0018's target set.
|
|
@@ -155,13 +234,21 @@ export function platformResolvePlugin(options) {
|
|
|
155
234
|
// EMPTY chain is one that was computed and came out empty — the shape a
|
|
156
235
|
// mis-wired orchestrator has — and it would send every import to base
|
|
157
236
|
// while looking installed.
|
|
158
|
-
throw new Error('gjsify platform resolve: the suffix chain is empty. Pass nativescriptSuffixChain(…)
|
|
159
|
-
'desktopSuffixChain(…), or omit the plugin.');
|
|
237
|
+
throw new Error('gjsify platform resolve: the suffix chain is empty. Pass nativescriptSuffixChain(…), ' +
|
|
238
|
+
'desktopSuffixChain(…) or browserSuffixChain(), or omit the plugin.');
|
|
160
239
|
}
|
|
161
240
|
const refused = options.refusedSuffixes ?? [];
|
|
162
241
|
const useIndex = options.siblingIndex ?? false;
|
|
163
242
|
// Per-directory listing cache, lowercased. `null` marks a directory that
|
|
164
243
|
// could not be read, which means "no opinion" — the resolver decides.
|
|
244
|
+
//
|
|
245
|
+
// CACHED FOR THE PLUGIN INSTANCE'S LIFE, so it needs the `watchChange` hook
|
|
246
|
+
// below: in a one-shot build the process ends before a listing can go stale,
|
|
247
|
+
// but `gjsify build --watch` and a Vite dev server both keep one plugin
|
|
248
|
+
// instance across rebuilds. Without invalidation an author who ADDS
|
|
249
|
+
// `foo.web.ts` beside `foo.ts` while the watcher runs keeps being served
|
|
250
|
+
// `foo.ts` until restart — the filter would be answering from a directory
|
|
251
|
+
// that no longer exists in that shape.
|
|
165
252
|
const listings = new Map();
|
|
166
253
|
const listingFor = (dir) => {
|
|
167
254
|
const cached = listings.get(dir);
|
|
@@ -196,6 +283,19 @@ export function platformResolvePlugin(options) {
|
|
|
196
283
|
const warned = new Set();
|
|
197
284
|
return {
|
|
198
285
|
name: 'gjsify-platform-resolve',
|
|
286
|
+
// The sibling index's invalidation. Both hosts call this for every path
|
|
287
|
+
// the watcher reports (`create` / `update` / `delete`), which is exactly
|
|
288
|
+
// the event that can make a cached listing wrong — a NEW `foo.web.ts` is
|
|
289
|
+
// a `create` in the directory the filter already answered for.
|
|
290
|
+
//
|
|
291
|
+
// Only the CHANGED path's directory is dropped, not the whole map: the
|
|
292
|
+
// map is refilled lazily, so a blanket clear would re-`readdirSync` every
|
|
293
|
+
// directory the next rebuild touches for one unrelated save. `dirname`
|
|
294
|
+
// of a virtual or `\0`-prefixed id is a directory that was never cached,
|
|
295
|
+
// and deleting an absent key is a no-op — no guard needed for that.
|
|
296
|
+
watchChange(id) {
|
|
297
|
+
listings.delete(dirname(id));
|
|
298
|
+
},
|
|
199
299
|
resolveId: {
|
|
200
300
|
order: 'pre',
|
|
201
301
|
async handler(source, importer, extraOptions) {
|
|
@@ -204,6 +304,23 @@ export function platformResolvePlugin(options) {
|
|
|
204
304
|
return null;
|
|
205
305
|
if (!source.startsWith('./') && !source.startsWith('../'))
|
|
206
306
|
return null;
|
|
307
|
+
// A QUERY NAMES A TRANSFORM OF ONE FILE, NOT A FILE TO FORK.
|
|
308
|
+
//
|
|
309
|
+
// `./x.blp?shared-tree`, `?raw`, `?url`: the plugin serving the query owns the
|
|
310
|
+
// resolution, and there is no second FILE for a platform chain to prefer —
|
|
311
|
+
// whatever the suffix is appended to, `./x.blp?shared-tree.native` is a
|
|
312
|
+
// specifier no convention gives a meaning to.
|
|
313
|
+
//
|
|
314
|
+
// Standing down is not merely tidier here, it is required. A probe that misses
|
|
315
|
+
// is normally free: `this.resolve('./x.native')` on a path that is not there
|
|
316
|
+
// returns null and the loop walks on. With a query in the specifier it is NOT
|
|
317
|
+
// — measured on `--app nativescript`, the missed probe surfaced as
|
|
318
|
+
// `UNLOADABLE_DEPENDENCY … No such file or directory (os error 2)` against the
|
|
319
|
+
// ORIGINAL import, and the chain never reached the plugin that would have
|
|
320
|
+
// resolved it. So the blind probe does not waste a lookup, it fails the build,
|
|
321
|
+
// and it does so pointing at a line that is correct.
|
|
322
|
+
if (source.includes('?'))
|
|
323
|
+
return null;
|
|
207
324
|
const extMatch = KNOWN_EXT_RE.exec(source);
|
|
208
325
|
const origExt = extMatch ? extMatch[0] : '';
|
|
209
326
|
const base = origExt ? source.slice(0, -origExt.length) : source;
|
|
@@ -253,12 +370,16 @@ export function platformResolvePlugin(options) {
|
|
|
253
370
|
if (!probe || warned.has(probe.id))
|
|
254
371
|
continue;
|
|
255
372
|
warned.add(probe.id);
|
|
373
|
+
// The remedy names THIS chain's own rungs. It used to end in
|
|
374
|
+
// a hardcoded `.desktop`, which was true of the only chain
|
|
375
|
+
// that had a refusal list and became advice to write a file
|
|
376
|
+
// the browser chain refuses the moment a second one did.
|
|
377
|
+
const ownVariants = suffixes.map((own) => `.${own}`).join(' / ');
|
|
256
378
|
this.warn(`gjsify platform resolve: "${base}.${suffix}" exists but is NOT a rung of this ` +
|
|
257
379
|
`target's chain (${suffixes.join(' → ')} → base), so ${importer} gets the base ` +
|
|
258
380
|
`file. ADR 0032 § 9: a .${suffix} variant is written for a runtime this build ` +
|
|
259
381
|
`is not, and reaching for it would hand this target code whose failure only ` +
|
|
260
|
-
`shows up on screen. Move what applies here into
|
|
261
|
-
`.desktop variant.`);
|
|
382
|
+
`shows up on screen. Move what applies here into ${ownVariants} or the base file.`);
|
|
262
383
|
}
|
|
263
384
|
return null;
|
|
264
385
|
},
|
|
@@ -58,13 +58,29 @@ export interface RewriteResult {
|
|
|
58
58
|
moduleType?: 'ts' | 'js';
|
|
59
59
|
map?: null;
|
|
60
60
|
}
|
|
61
|
+
/** The `import.meta` members the rewriter answers for: Node's three location properties. */
|
|
62
|
+
type ImportMetaProp = 'url' | 'dirname' | 'filename';
|
|
63
|
+
/**
|
|
64
|
+
* Replace every `import.meta.url` / `.dirname` / `.filename` expression in `src`.
|
|
65
|
+
*
|
|
66
|
+
* On the AST, not the text: the token also occurs inside STRINGS, and a text rewrite
|
|
67
|
+
* put a quoted replacement inside a quoted key — vite's `define: { "import.meta.url":
|
|
68
|
+
* … }` became `"__gjsifyModuleUrl("vite/…")"`, a PARSE_ERROR ("Expected `:` but found
|
|
69
|
+
* `Identifier`") that failed every `--app gjs` build reaching vite or wxt. A source
|
|
70
|
+
* acorn cannot parse keeps the token rewrite it always had.
|
|
71
|
+
*
|
|
72
|
+
* `dirname` and `filename` (Node ≥ 20.11) are answered too: GJS defines neither, so
|
|
73
|
+
* unplugin's `path.resolve(import.meta.dirname, …)` threw `The "path" argument must be
|
|
74
|
+
* of type string. Received type undefined` at load — where wxt's rebuild stopped next.
|
|
75
|
+
*/
|
|
76
|
+
export declare function replaceImportMeta(src: string, path: string, replacements: Record<ImportMetaProp, string>): string;
|
|
61
77
|
/**
|
|
62
78
|
* Pure rewriter: the rewritten code plus the module type to re-parse it with, or `null`
|
|
63
79
|
* when the file references none of the tokens.
|
|
64
80
|
*/
|
|
65
81
|
export declare function rewriteContents(args: {
|
|
66
82
|
path: string;
|
|
67
|
-
}, srcInput: string, bundleDir: string, runtimeResolve: boolean): RewriteResult | null;
|
|
83
|
+
}, srcInput: string, bundleDir: string, runtimeResolve: boolean, declare?: (abs: string) => void): RewriteResult | null;
|
|
68
84
|
export interface NodeModulesPathRewriteOptions {
|
|
69
85
|
/** Bundle output directory, derived from `output.file` / `output.dir`. */
|
|
70
86
|
bundleDir: string;
|
|
@@ -85,3 +101,4 @@ export interface NodeModulesPathRewriteOptions {
|
|
|
85
101
|
* wider scope nearly free. See {@link shouldInline} for why the scopes differ.
|
|
86
102
|
*/
|
|
87
103
|
export declare function nodeModulesPathRewritePlugin(options: NodeModulesPathRewriteOptions): Plugin;
|
|
104
|
+
export {};
|