@gjsify/rolldown-plugin-gjsify 0.52.0 → 0.54.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 +1 -0
- package/lib/app/gjs.js +18 -2
- package/lib/app/nativescript.js +1 -1
- package/lib/app/node.d.ts +1 -1
- package/lib/app/node.js +34 -10
- package/lib/index.d.ts +5 -1
- package/lib/index.js +4 -1
- package/lib/plugins/console-assign.d.ts +13 -0
- package/lib/plugins/console-assign.js +125 -0
- package/lib/plugins/css-as-string.js +128 -44
- package/lib/plugins/gi-optional.d.ts +60 -0
- package/lib/plugins/gi-optional.js +152 -0
- 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/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 +13 -10
- 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/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
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// `@import`s are resolved + inlined in JS first (`flattenCssImports`, so bare
|
|
18
18
|
// node_modules specifiers work — the native `bundle()` FileProvider can't walk
|
|
19
19
|
// node_modules), then the flattened CSS goes through the native `transform()`
|
|
20
|
-
// for lowering. Both use the same
|
|
20
|
+
// for lowering. Both use the same resolver. The `--app gjs`
|
|
21
21
|
// orchestrator passes `targets: { firefox: 60 << 16 }` so nesting + modern
|
|
22
22
|
// selectors get flattened to GTK4-CSS-engine-compatible output. Targeting is
|
|
23
23
|
// opt-in — a missing `targets` keeps the source pristine.
|
|
@@ -43,7 +43,10 @@ import { createRequire } from 'node:module';
|
|
|
43
43
|
import { dirname, isAbsolute, resolve as resolvePath } from 'node:path';
|
|
44
44
|
import { pathToFileURL } from 'node:url';
|
|
45
45
|
import { isGjs } from '../utils/runtime.js';
|
|
46
|
+
import { declareBuildInput } from '../utils/declare-build-input.js';
|
|
46
47
|
let _bundlerPromise = null;
|
|
48
|
+
/** Why the native bridge's library would not open, when `tryLoadNativeBundler()` measured it. */
|
|
49
|
+
let _nativeLoadError = null;
|
|
47
50
|
async function pickBundler() {
|
|
48
51
|
const forced = globalThis.process?.env
|
|
49
52
|
?.GJSIFY_CSS_BACKEND;
|
|
@@ -52,11 +55,22 @@ async function pickBundler() {
|
|
|
52
55
|
if (forced === 'native') {
|
|
53
56
|
const native = await tryLoadNativeBundler();
|
|
54
57
|
if (!native)
|
|
55
|
-
throw new Error('GJSIFY_CSS_BACKEND=native but @gjsify/lightningcss-native is not loadable'
|
|
58
|
+
throw new Error('GJSIFY_CSS_BACKEND=native but @gjsify/lightningcss-native is not loadable' +
|
|
59
|
+
(_nativeLoadError ? `\n${_nativeLoadError.message}` : ''));
|
|
56
60
|
return native;
|
|
57
61
|
}
|
|
58
62
|
const native = await tryLoadNativeBundler();
|
|
59
|
-
|
|
63
|
+
if (native)
|
|
64
|
+
return native;
|
|
65
|
+
// The npm fallback is the right answer, and it is silent on purpose — a
|
|
66
|
+
// missing optional backend is not an error. But if we MEASURED why the
|
|
67
|
+
// native one would not load, that measurement is the only place the user
|
|
68
|
+
// ever hears it, so it goes to the same `console.debug` channel
|
|
69
|
+
// `loadOptionalNativeModule` uses rather than into nothing. A backend that
|
|
70
|
+
// cannot load is the reason someone reaches for this flag.
|
|
71
|
+
if (_nativeLoadError)
|
|
72
|
+
console.debug(_nativeLoadError.message);
|
|
73
|
+
return loadNpmBundler();
|
|
60
74
|
}
|
|
61
75
|
async function tryLoadNativeBundler() {
|
|
62
76
|
// The native bridge only exists under GJS — `imports.gi` marker. Skip
|
|
@@ -79,14 +93,44 @@ async function tryLoadNativeBundler() {
|
|
|
79
93
|
const mod = (await import(/* @vite-ignore */ pathToFileURL(resolved).href));
|
|
80
94
|
if (!mod.hasNativeLightningcss())
|
|
81
95
|
return null;
|
|
82
|
-
|
|
96
|
+
// The typelib resolved; its library opens at the first class access. Open
|
|
97
|
+
// it now, beside that typelib: this module cannot leave it to the wrapper,
|
|
98
|
+
// whose `lib/` is imported by file URL where GJS resolves no bare
|
|
99
|
+
// specifier. A library that will not load then names its missing
|
|
100
|
+
// dependency and the npm fallback runs, instead of the nameless
|
|
101
|
+
// "Unsupported type void" inside `transform()`.
|
|
102
|
+
// The same resolve-then-import dance as above, for the probe itself:
|
|
103
|
+
// off disk, so the lazy edge costs nothing and the static one would have
|
|
104
|
+
// cost a bootable CLI. `./native-library` rather than `./core`: `core`
|
|
105
|
+
// re-exports `main-loop`, whose module-level singleton would then exist
|
|
106
|
+
// twice in a process that already has it inlined in the GJS bundle. The
|
|
107
|
+
// walk follows the WORKSPACE, so `@gjsify/utils build:esm` must precede
|
|
108
|
+
// it — rule 5 of `scripts/check-build-infra-order.mjs` orders that edge,
|
|
109
|
+
// the one `bundler-pick.ts` is on the success path of.
|
|
110
|
+
//
|
|
111
|
+
// Its own `try` because the outer one cannot tell this apart from "there
|
|
112
|
+
// is no native backend" — and reporting nothing is the one outcome this
|
|
113
|
+
// file must not produce: a missing measurement reads as a passing one.
|
|
114
|
+
let openNativeLibrary;
|
|
115
|
+
try {
|
|
116
|
+
const utilsHref = pathToFileURL(createRequire(import.meta.url).resolve('@gjsify/utils/native-library')).href;
|
|
117
|
+
({ openNativeLibrary } = (await import(/* @vite-ignore */ utilsHref)));
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
_nativeLoadError = err instanceof Error ? err : new Error(String(err));
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
_nativeLoadError = openNativeLibrary('GjsifyLightningcss');
|
|
124
|
+
if (_nativeLoadError)
|
|
125
|
+
return null;
|
|
126
|
+
return async (filename, targets, declare) => {
|
|
83
127
|
// The native `@gjsify/lightningcss-native` `bundle()` resolves
|
|
84
128
|
// `@import` chains through lightningcss's filesystem-backed
|
|
85
129
|
// FileProvider, which walks relative + absolute paths but NOT
|
|
86
130
|
// bare node_modules specifiers (`@import "@scope/pkg/x.css"`) —
|
|
87
131
|
// a JS resolver callback can't cross the GI/Rust boundary. So we
|
|
88
132
|
// do `@import` resolution in JS first, using the SAME
|
|
89
|
-
//
|
|
133
|
+
// resolver the npm `bundleAsync` path uses (npm-package
|
|
90
134
|
// + `exports`-map aware), and hand the fully-flattened CSS to the
|
|
91
135
|
// native `transform()` for the GTK4 nesting/modern-syntax lowering
|
|
92
136
|
// (`targets`). The native shim accepts a browserslist string; the
|
|
@@ -94,7 +138,7 @@ async function tryLoadNativeBundler() {
|
|
|
94
138
|
// (`firefox: 60 << 16` etc), so convert per browser key.
|
|
95
139
|
let flattened;
|
|
96
140
|
try {
|
|
97
|
-
flattened = await flattenCssImports(filename);
|
|
141
|
+
flattened = await flattenCssImports(filename, declare);
|
|
98
142
|
}
|
|
99
143
|
catch (err) {
|
|
100
144
|
// The native rolldown engine flattens a thrown plugin error to
|
|
@@ -147,13 +191,16 @@ async function loadNpmBundler() {
|
|
|
147
191
|
// `dependency` of this package, so the runtime resolve finds it.
|
|
148
192
|
const specifier = 'lightningcss';
|
|
149
193
|
const { bundleAsync } = (await import(/* @vite-ignore */ specifier));
|
|
150
|
-
return async (filename, targets) => {
|
|
194
|
+
return async (filename, targets, declare) => {
|
|
151
195
|
const result = await bundleAsync({
|
|
152
196
|
filename,
|
|
153
197
|
targets,
|
|
154
198
|
minify: false,
|
|
155
199
|
errorRecovery: true,
|
|
156
|
-
|
|
200
|
+
// Per call, not a module singleton: the resolver is where every
|
|
201
|
+
// `@import` target is resolved, so it is where the declaration has
|
|
202
|
+
// to happen — and it must carry THIS load's sink.
|
|
203
|
+
resolver: createCssBundleResolver(declare),
|
|
157
204
|
});
|
|
158
205
|
return { code: result.code };
|
|
159
206
|
};
|
|
@@ -185,35 +232,50 @@ const ASSET_REF_RE = /\.(woff2?|ttf|otf|eot|svg|png|jpe?g|gif|webp|avif|ico)(\?|
|
|
|
185
232
|
function isAssetReference(specifier) {
|
|
186
233
|
return /^(data|https?|file):/i.test(specifier) || ASSET_REF_RE.test(specifier);
|
|
187
234
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
// Bare specifier — walk node_modules and honor package.json exports.
|
|
196
|
-
// `createRequire` takes a file URL or path; passing the importer
|
|
197
|
-
// lets it scope its node_modules walk to the right starting point.
|
|
198
|
-
const req = createRequire(pathToFileURL(from).href);
|
|
199
|
-
try {
|
|
200
|
-
return req.resolve(specifier);
|
|
201
|
-
}
|
|
202
|
-
catch (err) {
|
|
203
|
-
// Not an installed module. If it's a `url()`/asset reference,
|
|
204
|
-
// leave it verbatim so lightningcss keeps the `@font-face` /
|
|
205
|
-
// `url()` rule intact (the consumer serves the asset) instead of
|
|
206
|
-
// crashing the build. A genuine unresolvable CSS `@import`
|
|
207
|
-
// re-throws with actionable context so the missing dependency
|
|
208
|
-
// surfaces clearly on both backends (npm and native).
|
|
209
|
-
if (isAssetReference(specifier))
|
|
235
|
+
function createCssBundleResolver(declare) {
|
|
236
|
+
return {
|
|
237
|
+
resolve(specifier, from) {
|
|
238
|
+
// A relative `@import` is resolved here, but lightningcss reads the
|
|
239
|
+
// target itself — so the resolver is the only place its name is known.
|
|
240
|
+
if (isAbsolute(specifier)) {
|
|
241
|
+
declare(stripQuery(specifier));
|
|
210
242
|
return specifier;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
243
|
+
}
|
|
244
|
+
if (specifier.startsWith('./') || specifier.startsWith('../')) {
|
|
245
|
+
const resolved = resolvePath(dirname(from), specifier);
|
|
246
|
+
declare(resolved);
|
|
247
|
+
return resolved;
|
|
248
|
+
}
|
|
249
|
+
// Bare specifier — walk node_modules and honor package.json exports.
|
|
250
|
+
// `createRequire` takes a file URL or path; passing the importer
|
|
251
|
+
// lets it scope its node_modules walk to the right starting point.
|
|
252
|
+
const req = createRequire(pathToFileURL(from).href);
|
|
253
|
+
try {
|
|
254
|
+
const resolved = req.resolve(specifier);
|
|
255
|
+
declare(resolved);
|
|
256
|
+
return resolved;
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
// Not an installed module. If it's a `url()`/asset reference,
|
|
260
|
+
// leave it verbatim so lightningcss keeps the `@font-face` /
|
|
261
|
+
// `url()` rule intact (the consumer serves the asset) instead of
|
|
262
|
+
// crashing the build. A genuine unresolvable CSS `@import`
|
|
263
|
+
// re-throws with actionable context so the missing dependency
|
|
264
|
+
// surfaces clearly on both backends (npm and native).
|
|
265
|
+
if (isAssetReference(specifier))
|
|
266
|
+
return specifier;
|
|
267
|
+
throw new Error(`cannot resolve @import "${specifier}" from ${from} (${err.message}). ` +
|
|
268
|
+
'If it is a workspace/npm package, ensure it is installed and exposes the CSS ' +
|
|
269
|
+
'file via its package.json "exports".');
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/** `@scope/pkg/x.css?v=2` is a cache-busting convention; the file is the part before it. */
|
|
275
|
+
function stripQuery(specifier) {
|
|
276
|
+
const cut = specifier.search(/[?#]/);
|
|
277
|
+
return cut === -1 ? specifier : specifier.slice(0, cut);
|
|
278
|
+
}
|
|
217
279
|
// Matches a CSS `@import` at-rule and captures the specifier + any trailing
|
|
218
280
|
// condition tokens (media query / `layer()` / `supports()`) before the `;`.
|
|
219
281
|
// Covers `@import "x";`, `@import 'x';`, and `@import url("x")` / `url(x)`.
|
|
@@ -236,7 +298,7 @@ async function replaceAllAsync(source, re, fn) {
|
|
|
236
298
|
}
|
|
237
299
|
/**
|
|
238
300
|
* Recursively resolve + inline every bundleable `@import` in `entry`, using the
|
|
239
|
-
* same
|
|
301
|
+
* same resolver the npm `bundleAsync` path uses. Returns a
|
|
240
302
|
* single flattened CSS string with no bundleable `@import` statements left — the
|
|
241
303
|
* native `transform()` then applies GTK4 nesting/target lowering.
|
|
242
304
|
*
|
|
@@ -249,20 +311,26 @@ async function replaceAllAsync(source, re, fn) {
|
|
|
249
311
|
* URL) are kept verbatim (the resolver leaves them alone), matching the npm
|
|
250
312
|
* path. A cycle inlines each file at most once.
|
|
251
313
|
*/
|
|
252
|
-
async function flattenCssImports(entry) {
|
|
314
|
+
async function flattenCssImports(entry, declare) {
|
|
253
315
|
const seen = new Set();
|
|
316
|
+
const resolver = createCssBundleResolver(declare);
|
|
254
317
|
const inline = async (file, isRoot) => {
|
|
255
|
-
const abs = isAbsolute(file) ? file : resolvePath(file);
|
|
318
|
+
const abs = isAbsolute(file) ? stripQuery(file) : resolvePath(file);
|
|
256
319
|
if (seen.has(abs))
|
|
257
320
|
return ''; // @import cycle — inline once
|
|
258
321
|
seen.add(abs);
|
|
322
|
+
// Declared at the READ, not at the resolution: the root stylesheet, and
|
|
323
|
+
// any import a form the regex does not match, are still inputs of the
|
|
324
|
+
// output — and a resolution declares nothing if lightningcss then
|
|
325
|
+
// decides not to read it.
|
|
326
|
+
declare(abs);
|
|
259
327
|
let source = await readFile(abs, 'utf8');
|
|
260
328
|
// `@charset` is only valid as the very first token of a stylesheet;
|
|
261
329
|
// an inlined sub-file's `@charset` mid-stream would be invalid CSS.
|
|
262
330
|
if (!isRoot)
|
|
263
331
|
source = source.replace(/@charset[^;]*;/gi, '');
|
|
264
332
|
return replaceAllAsync(source, IMPORT_RE, async (match, spec, condition) => {
|
|
265
|
-
const resolved =
|
|
333
|
+
const resolved = resolver.resolve(spec, abs);
|
|
266
334
|
// Asset-reference `@import` (rare): resolver returns it verbatim —
|
|
267
335
|
// keep the at-rule intact.
|
|
268
336
|
if (resolved === spec)
|
|
@@ -310,13 +378,29 @@ export function cssAsStringPlugin(options = {}) {
|
|
|
310
378
|
// flattens nesting, so no further lightningcss lowering is needed).
|
|
311
379
|
filter: { id: /\.(css|s[ac]ss)$/ },
|
|
312
380
|
async handler(id) {
|
|
381
|
+
// Every file the backends read on our behalf, declared through
|
|
382
|
+
// the standard contract — `gjsify test`'s freshness check reads
|
|
383
|
+
// this list, and it is the only account of a stylesheet's
|
|
384
|
+
// `@import`/`@use` chain, which no module graph names. Why the
|
|
385
|
+
// call is feature-detected: `utils/declare-build-input.ts`.
|
|
386
|
+
const declare = (abs) => declareBuildInput(this, abs);
|
|
387
|
+
// The entry itself is read here in every branch, and a Sass
|
|
388
|
+
// file's PARTIALS are not: dart-sass resolves a relative
|
|
389
|
+
// `@use "./x"` through its own filesystem importer before any
|
|
390
|
+
// custom importer is consulted (measured 2026-09-29 with
|
|
391
|
+
// dart-sass 1.101: `importers[].findFileUrl` and
|
|
392
|
+
// `canonicalize` are called zero times for a relative load, and
|
|
393
|
+
// for a bare one only on the way to being declined, so the path
|
|
394
|
+
// is never ours to declare). Tracking that:
|
|
395
|
+
// status/open-todos/bundler.md.
|
|
396
|
+
declare(stripQuery(id));
|
|
313
397
|
let code;
|
|
314
398
|
if (/\.s[ac]ss$/.test(id)) {
|
|
315
399
|
code = await compileSass(id);
|
|
316
400
|
}
|
|
317
401
|
else {
|
|
318
402
|
code = bundle
|
|
319
|
-
? new TextDecoder('utf-8').decode(await loadAndBundleCss(id, targets))
|
|
403
|
+
? new TextDecoder('utf-8').decode(await loadAndBundleCss(id, targets, declare))
|
|
320
404
|
: await readFile(id, 'utf8');
|
|
321
405
|
}
|
|
322
406
|
return {
|
|
@@ -339,7 +423,7 @@ async function compileSass(filename) {
|
|
|
339
423
|
// `import './x.scss'` fails with UNLOADABLE_DEPENDENCY. Resolving it to a file
|
|
340
424
|
// first (`tryLoadNativeBundler`'s trick) does not help: `sass.default.js` then
|
|
341
425
|
// imports a bare `immutable`. Only INLINING dart-sass works, and 3.6 MB minified
|
|
342
|
-
// onto a 6.6 MB CLI bundle is a carrier-package decision — status/open-todos.md
|
|
426
|
+
// onto a 6.6 MB CLI bundle is a carrier-package decision — status/open-todos/README.md
|
|
343
427
|
// (#1053). dart-sass DOES run under GJS: `adwaita-web`'s `build:scss` does.
|
|
344
428
|
_sassPromise = import(/* @vite-ignore */ 'sass');
|
|
345
429
|
}
|
|
@@ -352,10 +436,10 @@ async function compileSass(filename) {
|
|
|
352
436
|
`Is the \`sass\` package installed? (${err.message})`);
|
|
353
437
|
}
|
|
354
438
|
}
|
|
355
|
-
async function loadAndBundleCss(filename, targets) {
|
|
439
|
+
async function loadAndBundleCss(filename, targets, declare) {
|
|
356
440
|
if (!_bundlerPromise)
|
|
357
441
|
_bundlerPromise = pickBundler();
|
|
358
442
|
const bundler = await _bundlerPromise;
|
|
359
|
-
const { code } = await bundler(filename, targets);
|
|
443
|
+
const { code } = await bundler(filename, targets, declare);
|
|
360
444
|
return code;
|
|
361
445
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Plugin } from 'rolldown';
|
|
2
|
+
declare const GI_OPTIONAL_VIRTUAL_PREFIX: {
|
|
3
|
+
readonly gjs: "\0gjsify-gi-optional:";
|
|
4
|
+
readonly node: "\0gjsify-gi-optional-node:";
|
|
5
|
+
};
|
|
6
|
+
/** Which build target's shim a flagged specifier gets. */
|
|
7
|
+
export type GiOptionalTarget = keyof typeof GI_OPTIONAL_VIRTUAL_PREFIX;
|
|
8
|
+
/** The query flag that declares a `gi://` import optional. */
|
|
9
|
+
export declare const GI_OPTIONAL_FLAG = "optional";
|
|
10
|
+
/**
|
|
11
|
+
* `Symbol.for` key of the marker statement every optional shim emits, naming its own
|
|
12
|
+
* namespace so a reader of the ARTIFACT can tell an optional namespace from a hard one.
|
|
13
|
+
*
|
|
14
|
+
* WHY IT IS IN THE BUNDLE AND NOT ALONGSIDE IT, which is the design decision this
|
|
15
|
+
* constant exists to pin: `gjsify ship` reads the emitted file, not the build tree,
|
|
16
|
+
* and reads the STAGE MANIFEST when it packs on another host — so a sidecar next to
|
|
17
|
+
* the bundle is a second source that can be stale, absent, or a different file. The
|
|
18
|
+
* marker is therefore one statement in the module body the plugin already emits, and
|
|
19
|
+
* it is a `globalThis[Symbol.for(…)]?.(…)` call for two reasons, both measured on
|
|
20
|
+
* rolldown's own minifier: an unknown global call is not statically pure, so it
|
|
21
|
+
* survives minification where a bare string constant is dropped by tree-shaking, and
|
|
22
|
+
* the optional call means nothing on a host that never registers a handler.
|
|
23
|
+
*/
|
|
24
|
+
export declare const GI_OPTIONAL_MARKER = "gjsify.optionalGi";
|
|
25
|
+
/**
|
|
26
|
+
* The statement that records `namespace` as optional in the artifact.
|
|
27
|
+
*
|
|
28
|
+
* Two string arguments in the spelling the node arm's `requireGi("Ns", "X")` already
|
|
29
|
+
* uses, so one reader answers both targets. The version argument is OMITTED when
|
|
30
|
+
* there is none, rather than sent empty: a `requireGi` call cannot tell `''` from a
|
|
31
|
+
* version, and neither can this.
|
|
32
|
+
*/
|
|
33
|
+
export declare function giOptionalMarkerSource(namespace: string, version?: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* Split `gi://Ns?version=X&optional` into the clean specifier the loader sees and the
|
|
36
|
+
* flag; `null` when the specifier is not a `gi://` one or does not carry the flag.
|
|
37
|
+
*/
|
|
38
|
+
export declare function parseOptionalGiSpecifier(source: string): {
|
|
39
|
+
specifier: string;
|
|
40
|
+
namespace: string;
|
|
41
|
+
version?: string;
|
|
42
|
+
} | null;
|
|
43
|
+
/**
|
|
44
|
+
* The module body for one optional namespace. Self-contained: it runs at module
|
|
45
|
+
* evaluation, where no bundled helper may be assumed, and reaches ambient globals
|
|
46
|
+
* through `globalThis.` only.
|
|
47
|
+
*/
|
|
48
|
+
export declare function giOptionalShimSource(specifier: string, namespace: string, version?: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* The `--app node` body: the same contract, reached through `@gjsify/node-gi`.
|
|
51
|
+
*
|
|
52
|
+
* The load is EAGER, which is the one place this arm differs from the hard node shim
|
|
53
|
+
* and the reason is the flag: `requireGi` only fails when it is called, so a lazy Proxy
|
|
54
|
+
* cannot answer "is it there" without loading it — and the answer is the whole point.
|
|
55
|
+
* A missing `@gjsify/node-gi` itself lands in the same catch and yields `undefined`,
|
|
56
|
+
* which is honest: without node-gi no GI namespace loads, optional or not.
|
|
57
|
+
*/
|
|
58
|
+
export declare function giOptionalNodeShimSource(namespace: string, version?: string): string;
|
|
59
|
+
export declare function giOptionalPlugin(target: GiOptionalTarget): Plugin;
|
|
60
|
+
export {};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// `import Goa from 'gi://Goa?version=1.0&optional'` — a GI namespace the app can run
|
|
3
|
+
// without (ADR 0087). Composed by `--app gjs` and `--app node`.
|
|
4
|
+
//
|
|
5
|
+
// A plain `gi://` import is a hard edge: GJS loads the typelib when the specifier is
|
|
6
|
+
// EVALUATED and a missing one aborts the module graph before the app decides anything.
|
|
7
|
+
// The `optional` query flag is the declaration that the app has that decision to make.
|
|
8
|
+
// The import resolves to a virtual module that loads the SAME specifier without the
|
|
9
|
+
// flag inside a try/catch, so the emitted bundle still carries the verbatim
|
|
10
|
+
// `gi://Goa?version=1.0` that `ship/gi-namespaces.ts` and `depends.ts` read, and the
|
|
11
|
+
// namespace is `undefined` when it is absent.
|
|
12
|
+
//
|
|
13
|
+
// BOTH app targets, and the second one was the trap. `--app node` already loads a
|
|
14
|
+
// `gi://` namespace LAZILY (`gjsGiNodePlugin` default-exports a Proxy that calls
|
|
15
|
+
// `requireGi` on first member access), so with the flag ignored the same source kept
|
|
16
|
+
// returning a truthy Proxy where the app checks `Goa === undefined`, and the first
|
|
17
|
+
// real member access threw instead of degrading — a degrade path that works on one
|
|
18
|
+
// build target and not the other is the defect, not the laziness. Hence a NODE
|
|
19
|
+
// shim with the SAME contract: the namespace or `undefined`, plus one warn. It is
|
|
20
|
+
// synchronous rather than a top-level `await import()` (the shape the node arm's
|
|
21
|
+
// synchronous `require()` already has), because a flag whose whole point is that
|
|
22
|
+
// the app can branch on the result must not force every importer to be async.
|
|
23
|
+
import { GJSIFY_VIRTUAL_PREFIX } from '../utils/virtual-module-id.js';
|
|
24
|
+
import { parseGiSpecifier } from './gjs-gi-node.js';
|
|
25
|
+
const GI_OPTIONAL_VIRTUAL_PREFIX = {
|
|
26
|
+
gjs: `${GJSIFY_VIRTUAL_PREFIX}gi-optional:`,
|
|
27
|
+
node: `${GJSIFY_VIRTUAL_PREFIX}gi-optional-node:`,
|
|
28
|
+
};
|
|
29
|
+
/** The query flag that declares a `gi://` import optional. */
|
|
30
|
+
export const GI_OPTIONAL_FLAG = 'optional';
|
|
31
|
+
/**
|
|
32
|
+
* `Symbol.for` key of the marker statement every optional shim emits, naming its own
|
|
33
|
+
* namespace so a reader of the ARTIFACT can tell an optional namespace from a hard one.
|
|
34
|
+
*
|
|
35
|
+
* WHY IT IS IN THE BUNDLE AND NOT ALONGSIDE IT, which is the design decision this
|
|
36
|
+
* constant exists to pin: `gjsify ship` reads the emitted file, not the build tree,
|
|
37
|
+
* and reads the STAGE MANIFEST when it packs on another host — so a sidecar next to
|
|
38
|
+
* the bundle is a second source that can be stale, absent, or a different file. The
|
|
39
|
+
* marker is therefore one statement in the module body the plugin already emits, and
|
|
40
|
+
* it is a `globalThis[Symbol.for(…)]?.(…)` call for two reasons, both measured on
|
|
41
|
+
* rolldown's own minifier: an unknown global call is not statically pure, so it
|
|
42
|
+
* survives minification where a bare string constant is dropped by tree-shaking, and
|
|
43
|
+
* the optional call means nothing on a host that never registers a handler.
|
|
44
|
+
*/
|
|
45
|
+
export const GI_OPTIONAL_MARKER = 'gjsify.optionalGi';
|
|
46
|
+
/**
|
|
47
|
+
* The statement that records `namespace` as optional in the artifact.
|
|
48
|
+
*
|
|
49
|
+
* Two string arguments in the spelling the node arm's `requireGi("Ns", "X")` already
|
|
50
|
+
* uses, so one reader answers both targets. The version argument is OMITTED when
|
|
51
|
+
* there is none, rather than sent empty: a `requireGi` call cannot tell `''` from a
|
|
52
|
+
* version, and neither can this.
|
|
53
|
+
*/
|
|
54
|
+
export function giOptionalMarkerSource(namespace, version) {
|
|
55
|
+
const args = version === undefined ? JSON.stringify(namespace) : `${JSON.stringify(namespace)}, ${JSON.stringify(version)}`;
|
|
56
|
+
return `globalThis[Symbol.for(${JSON.stringify(GI_OPTIONAL_MARKER)})]?.(${args});`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Split `gi://Ns?version=X&optional` into the clean specifier the loader sees and the
|
|
60
|
+
* flag; `null` when the specifier is not a `gi://` one or does not carry the flag.
|
|
61
|
+
*/
|
|
62
|
+
export function parseOptionalGiSpecifier(source) {
|
|
63
|
+
if (!source.startsWith('gi://'))
|
|
64
|
+
return null;
|
|
65
|
+
const queryIndex = source.indexOf('?');
|
|
66
|
+
if (queryIndex === -1)
|
|
67
|
+
return null;
|
|
68
|
+
const params = new URLSearchParams(source.slice(queryIndex + 1));
|
|
69
|
+
if (!params.has(GI_OPTIONAL_FLAG))
|
|
70
|
+
return null;
|
|
71
|
+
params.delete(GI_OPTIONAL_FLAG);
|
|
72
|
+
const rest = params.toString();
|
|
73
|
+
const specifier = source.slice(0, queryIndex) + (rest ? `?${rest}` : '');
|
|
74
|
+
const parsed = parseGiSpecifier(specifier);
|
|
75
|
+
if (parsed === null)
|
|
76
|
+
return null;
|
|
77
|
+
return { specifier, ...parsed };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The module body for one optional namespace. Self-contained: it runs at module
|
|
81
|
+
* evaluation, where no bundled helper may be assumed, and reaches ambient globals
|
|
82
|
+
* through `globalThis.` only.
|
|
83
|
+
*/
|
|
84
|
+
export function giOptionalShimSource(specifier, namespace, version) {
|
|
85
|
+
const label = version ? `${namespace} ${version}` : namespace;
|
|
86
|
+
return (`let ns;\n` +
|
|
87
|
+
`try {\n` +
|
|
88
|
+
` const m = await import(${JSON.stringify(specifier)});\n` +
|
|
89
|
+
` ns = m.default ?? m;\n` +
|
|
90
|
+
`} catch (error) {\n` +
|
|
91
|
+
` let searched = '';\n` +
|
|
92
|
+
` try {\n` +
|
|
93
|
+
` const r = globalThis.imports.gi.GIRepository.Repository.dup_default();\n` +
|
|
94
|
+
` searched = ' (typelib search path: ' + r.get_search_path().join(':') + ')';\n` +
|
|
95
|
+
` } catch {}\n` +
|
|
96
|
+
` console.warn(${JSON.stringify(`optional GI namespace ${label} is not available`)} + searched + ': ' + (error && error.message ? error.message : String(error)));\n` +
|
|
97
|
+
`}\n` +
|
|
98
|
+
`${giOptionalMarkerSource(namespace, version)}\n` +
|
|
99
|
+
`export default ns;\n`);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The `--app node` body: the same contract, reached through `@gjsify/node-gi`.
|
|
103
|
+
*
|
|
104
|
+
* The load is EAGER, which is the one place this arm differs from the hard node shim
|
|
105
|
+
* and the reason is the flag: `requireGi` only fails when it is called, so a lazy Proxy
|
|
106
|
+
* cannot answer "is it there" without loading it — and the answer is the whole point.
|
|
107
|
+
* A missing `@gjsify/node-gi` itself lands in the same catch and yields `undefined`,
|
|
108
|
+
* which is honest: without node-gi no GI namespace loads, optional or not.
|
|
109
|
+
*/
|
|
110
|
+
export function giOptionalNodeShimSource(namespace, version) {
|
|
111
|
+
const label = version ? `${namespace} ${version}` : namespace;
|
|
112
|
+
const versionArg = version === undefined ? '' : `, ${JSON.stringify(version)}`;
|
|
113
|
+
return (`import { createRequire } from 'node:module';\n` +
|
|
114
|
+
`const require = createRequire(import.meta.url);\n` +
|
|
115
|
+
`let ns;\n` +
|
|
116
|
+
`try {\n` +
|
|
117
|
+
` ns = require('@gjsify/node-gi/gi').requireGi(${JSON.stringify(namespace)}${versionArg});\n` +
|
|
118
|
+
`} catch (error) {\n` +
|
|
119
|
+
` console.warn(${JSON.stringify(`optional GI namespace ${label} is not available`)} + ': ' + (error && error.message ? error.message : String(error)));\n` +
|
|
120
|
+
`}\n` +
|
|
121
|
+
`${giOptionalMarkerSource(namespace, version)}\n` +
|
|
122
|
+
`export default ns;\n`);
|
|
123
|
+
}
|
|
124
|
+
export function giOptionalPlugin(target) {
|
|
125
|
+
const prefix = GI_OPTIONAL_VIRTUAL_PREFIX[target];
|
|
126
|
+
return {
|
|
127
|
+
name: `gjsify-gi-optional-${target}`,
|
|
128
|
+
resolveId: {
|
|
129
|
+
order: 'pre',
|
|
130
|
+
filter: { id: /^gi:\/\/.*[?&]optional(?:&|=|$)/ },
|
|
131
|
+
handler(source) {
|
|
132
|
+
const parsed = parseOptionalGiSpecifier(source);
|
|
133
|
+
if (parsed === null)
|
|
134
|
+
return null;
|
|
135
|
+
const version = parsed.version ? `@${parsed.version}` : '';
|
|
136
|
+
return { id: `${prefix}${parsed.namespace}${version}` };
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
load(id) {
|
|
140
|
+
if (!id.startsWith(prefix))
|
|
141
|
+
return null;
|
|
142
|
+
const spec = id.slice(prefix.length);
|
|
143
|
+
const at = spec.lastIndexOf('@');
|
|
144
|
+
const namespace = at === -1 ? spec : spec.slice(0, at);
|
|
145
|
+
const version = at === -1 ? undefined : spec.slice(at + 1);
|
|
146
|
+
const code = target === 'gjs'
|
|
147
|
+
? giOptionalShimSource(`gi://${namespace}${version ? `?version=${version}` : ''}`, namespace, version)
|
|
148
|
+
: giOptionalNodeShimSource(namespace, version);
|
|
149
|
+
return { code, moduleSideEffects: false };
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -29,7 +29,7 @@ export interface GiRendererOptions {
|
|
|
29
29
|
* passes `--gi-renderer`, so the arm never composes there and this text cannot reach them
|
|
30
30
|
* — measured, nothing under `tests/` names the flag but `gi-renderer-arms` itself. Making
|
|
31
31
|
* that suite's two assertions import-shaped would retire this rule outright; the argument,
|
|
32
|
-
* the other twelve sites and the retirement condition are in `status/open-todos.md`.
|
|
32
|
+
* the other twelve sites and the retirement condition are in `status/open-todos/README.md`.
|
|
33
33
|
*/
|
|
34
34
|
export declare function giRendererShimSource(options: GiRendererOptions, namespace: string, version: string): string;
|
|
35
35
|
export declare function giRendererPlugin(options: GiRendererOptions): Plugin;
|
|
@@ -90,7 +90,7 @@ const GI_RENDERER_VIRTUAL_PREFIX = `${GJSIFY_VIRTUAL_PREFIX}gi-renderer:`;
|
|
|
90
90
|
* passes `--gi-renderer`, so the arm never composes there and this text cannot reach them
|
|
91
91
|
* — measured, nothing under `tests/` names the flag but `gi-renderer-arms` itself. Making
|
|
92
92
|
* that suite's two assertions import-shaped would retire this rule outright; the argument,
|
|
93
|
-
* the other twelve sites and the retirement condition are in `status/open-todos.md`.
|
|
93
|
+
* the other twelve sites and the retirement condition are in `status/open-todos/README.md`.
|
|
94
94
|
*/
|
|
95
95
|
export function giRendererShimSource(options, namespace, version) {
|
|
96
96
|
return (`import { ${namespace} as namespace } from ${JSON.stringify(options.renderer)};\n` +
|
|
@@ -9,8 +9,11 @@
|
|
|
9
9
|
// e2e): a banner is the entry chunk's BODY and ESM evaluates imports first, so a
|
|
10
10
|
// STATIC `import … from 'gi://Ns'` has already loaded its typelib before this runs.
|
|
11
11
|
// What is left is what loads LATER — `await import('gi://Soup')` and the other
|
|
12
|
-
// optional namespaces.
|
|
13
|
-
//
|
|
12
|
+
// optional namespaces. Confirmed on darwin-arm64 in the same shape (macOS 27 / M4 /
|
|
13
|
+
// Homebrew, the bundle loading Gtk through `await import` and failing through a
|
|
14
|
+
// static one). Reaching the static ones changes how a bundle acquires GI namespaces
|
|
15
|
+
// at all: ADR 0085 decides to lower them here, in `renderChunk`, to awaited dynamic
|
|
16
|
+
// imports placed after this prologue — proposed, not implemented.
|
|
14
17
|
//
|
|
15
18
|
// TWO KINDS OF DIRECTORY, split by WHOSE FACT each is. `dirs` describe the SHIPPED
|
|
16
19
|
// TREE, so they are baked, relative to the program. `systemProbes` describe the host
|
|
@@ -12,11 +12,49 @@ export interface NapiNodeAddonPluginOptions {
|
|
|
12
12
|
* `@gjsify/napi` is not resolvable in the consumer graph. Default `true`.
|
|
13
13
|
*/
|
|
14
14
|
warnOnMissingNapi?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Is the output an ESM single-file build, i.e. does it carry the
|
|
17
|
+
* bundle-URL banner the run-time resolver anchors on? Default `true`;
|
|
18
|
+
* `app/gjs.ts` passes the same `format === 'esm'` the node-modules path
|
|
19
|
+
* rewriter gates its own runtime resolution on.
|
|
20
|
+
*
|
|
21
|
+
* `false` DECLINES every rewrite, with one warning naming the reason. It is
|
|
22
|
+
* a decline and not the pre-ADR baked path because there is no correct baked
|
|
23
|
+
* path left: the whole point of ADR 0084 is that a path chosen at build time
|
|
24
|
+
* is what made the artifact unusable off the build machine, so emitting one
|
|
25
|
+
* again would reintroduce the defect for a mode that cannot anchor at run
|
|
26
|
+
* time anyway. A `--library cjs` build is a library for a consumer's own
|
|
27
|
+
* toolchain, which resolves its addons itself.
|
|
28
|
+
*/
|
|
29
|
+
runtimeResolve?: boolean;
|
|
15
30
|
}
|
|
16
31
|
/** A native addon package root has no resolvable compiled `.node`. */
|
|
17
32
|
export declare class AddonNotBuiltError extends Error {
|
|
18
33
|
constructor(pkgRoot: string);
|
|
19
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Enumerate every `.node` an addon package ships, keyed by platform, as a
|
|
37
|
+
* `<package>/<subpath>` spec per entry. ADR 0084: the build ENUMERATES, it
|
|
38
|
+
* does not select — the bundle picks the right entry at RUN time from the
|
|
39
|
+
* host it finds itself on.
|
|
40
|
+
*
|
|
41
|
+
* The table has:
|
|
42
|
+
* - One entry per `prebuilds/<tuple>/` directory, keyed by the tuple's
|
|
43
|
+
* platform and EVERY architecture it declares, and per libc variant. Each
|
|
44
|
+
* entry is chosen by {@link selectPrebuildFile} — node-gyp-build's own
|
|
45
|
+
* algorithm, run for a synthetic host of that tuple — so the entry for the
|
|
46
|
+
* build host is exactly the binary Node would load, and a foreign platform's
|
|
47
|
+
* entry is the best one that host could load rather than the best-looking
|
|
48
|
+
* file in the directory.
|
|
49
|
+
* - One entry per `build/Release` and `build/Debug`, keyed by the BUILD host
|
|
50
|
+
* ({@link buildHostTarget}, deliberately not the `npm_config_*` override
|
|
51
|
+
* those files are not built for), overriding the prebuilds entry for the
|
|
52
|
+
* same key: node-gyp-build's order has build/Release winning.
|
|
53
|
+
*
|
|
54
|
+
* Returns an empty record when no `.node` exists anywhere — the caller then
|
|
55
|
+
* throws {@link AddonNotBuiltError} as a build-time gate.
|
|
56
|
+
*/
|
|
57
|
+
export declare function enumerateAddonTargets(pkgRoot: string, pkg: AddonPackageJson): Record<string, string>;
|
|
20
58
|
/**
|
|
21
59
|
* Locate the compiled `.node` for an addon package root, matching node-gyp-build's
|
|
22
60
|
* probe order: `build/Release` → `build/Debug` →
|
|
@@ -28,14 +66,19 @@ export declare function resolveAddonPath(pkgRoot: string, opts?: {
|
|
|
28
66
|
}): string;
|
|
29
67
|
/** Nearest ancestor directory of `importerFile` holding a `package.json`. */
|
|
30
68
|
export declare function nearestPackageRoot(importerFile: string): string | null;
|
|
69
|
+
/**
|
|
70
|
+
* The addon table as a JSON string — the platform-key → `<pkg>/<subpath>`
|
|
71
|
+
* map the runtime resolver (`__gjsifyAddonResolve`) picks from. ADR 0084.
|
|
72
|
+
*/
|
|
73
|
+
type AddonTable = string;
|
|
31
74
|
/** Direct `.node` import → the addon's exports (ESM default). */
|
|
32
|
-
export declare function directNodeShim(
|
|
75
|
+
export declare function directNodeShim(addonTable: AddonTable): string;
|
|
33
76
|
/** `node-gyp-build` replacement — a callable `load(dir)` carrying `.path()`. */
|
|
34
|
-
export declare function nodeGypBuildShim(
|
|
77
|
+
export declare function nodeGypBuildShim(addonTable: AddonTable): string;
|
|
35
78
|
/** `bindings` replacement — a callable `bindings(name)` returning the addon. */
|
|
36
|
-
export declare function bindingsShim(
|
|
79
|
+
export declare function bindingsShim(addonTable: AddonTable): string;
|
|
37
80
|
/** napi-rs sibling → the raw native exports as the module value. */
|
|
38
|
-
export declare function napiRsShim(
|
|
81
|
+
export declare function napiRsShim(addonTable: AddonTable): string;
|
|
39
82
|
/** Classify a specifier for interception — pure decision logic, no filesystem. */
|
|
40
83
|
export declare function classifySpecifier(source: string): {
|
|
41
84
|
kind: 'node-gyp-build' | 'bindings';
|
|
@@ -122,3 +165,4 @@ export declare function hostNapiRsTriple(): string | null;
|
|
|
122
165
|
* else. Register ONLY for `--app gjs` — the C-ABI runs under `@gjsify/napi`.
|
|
123
166
|
*/
|
|
124
167
|
export declare function napiNodeAddonPlugin(options?: NapiNodeAddonPluginOptions): Plugin;
|
|
168
|
+
export {};
|