@gjsify/rolldown-plugin-gjsify 0.48.0 → 0.50.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/browser.js +17 -3
- package/lib/app/nativescript.js +15 -3
- package/lib/plugins/gi-renderer.d.ts +35 -0
- package/lib/plugins/gi-renderer.js +162 -0
- package/lib/types/plugin-options.d.ts +18 -0
- package/lib/utils/index.d.ts +1 -1
- package/lib/utils/index.js +1 -1
- package/lib/utils/inline-static-reads.d.ts +49 -0
- package/lib/utils/inline-static-reads.js +138 -9
- package/package.json +9 -9
package/lib/app/browser.js
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Browser builds redirect `@girs/*` and `gi://*` to an empty virtual module
|
|
4
4
|
// (they appear transitively via `@gjsify/unit` and similar packages with
|
|
5
|
-
// GJS-specific code paths)
|
|
5
|
+
// GJS-specific code paths) — unless `--gi-renderer` composes the ADR 0034
|
|
6
|
+
// stage 9 arm, which answers `gi://Adw` / `gi://Gtk` out of `@gjsify/adwaita-web`
|
|
7
|
+
// and carves `@girs/*` out of the empty redirect so they reach it.
|
|
8
|
+
//
|
|
9
|
+
// Bare Node specifiers + their `node:*` prefix
|
|
6
10
|
// variants are routed to `@gjsify/<X>` via the curated
|
|
7
11
|
// `ALIASES_NODE_FOR_BROWSER` table, whose VALUES already carry the
|
|
8
12
|
// per-runtimes-triplet slot routing (`@gjsify/<X>` → `@gjsify/<X>/browser` /
|
|
@@ -15,9 +19,10 @@
|
|
|
15
19
|
import { aliasPlugin } from '../plugins/alias.js';
|
|
16
20
|
import { deepkitPlugin } from '@gjsify/rolldown-plugin-deepkit';
|
|
17
21
|
import blueprintPlugin from '@gjsify/vite-plugin-blueprint';
|
|
18
|
-
import { ALIASES_NODE_FOR_BROWSER, getDerivedAliasesSync } from '@gjsify/resolve-npm';
|
|
22
|
+
import { ALIASES_NODE_FOR_BROWSER, GI_RENDERERS, getDerivedAliasesSync } from '@gjsify/resolve-npm';
|
|
19
23
|
import { globToEntryPoints } from '../utils/entry-points.js';
|
|
20
24
|
import { gjsImportsEmptyPlugin } from '../plugins/gjs-imports-empty.js';
|
|
25
|
+
import { giRendererPlugin } from '../plugins/gi-renderer.js';
|
|
21
26
|
import { cssAsStringPlugin } from '../plugins/css-as-string.js';
|
|
22
27
|
import { unresolvedWorkspaceImportPlugin } from '../plugins/unresolved-workspace-import.js';
|
|
23
28
|
export const setupForBrowser = async (input) => {
|
|
@@ -25,6 +30,9 @@ export const setupForBrowser = async (input) => {
|
|
|
25
30
|
const external = [...userExternal];
|
|
26
31
|
const exclude = input.pluginOptions.exclude ?? [];
|
|
27
32
|
const entryPoints = await globToEntryPoints(input.input, exclude);
|
|
33
|
+
// The arm is composed only when asked for. `config.ts` already refuses the flag
|
|
34
|
+
// on a target with no row, so an undefined row here means the flag was not passed.
|
|
35
|
+
const giRenderer = input.pluginOptions.giRenderer ? GI_RENDERERS['browser'] : undefined;
|
|
28
36
|
// Bare Node-builtin + `node:*` prefix aliases — both forms route to the
|
|
29
37
|
// same `@gjsify/<X>` target. Generated deterministically from
|
|
30
38
|
// `ALIASES_NODE_FOR_BROWSER` so a single source-of-truth in
|
|
@@ -90,7 +98,13 @@ export const setupForBrowser = async (input) => {
|
|
|
90
98
|
// transform can strip the annotations out from under it (see `GjsifyConfig`).
|
|
91
99
|
const prePlugins = [deepkitPlugin({ reflection: input.pluginOptions.reflection })];
|
|
92
100
|
const plugins = [
|
|
93
|
-
|
|
101
|
+
// ADR 0034 stage 9 — the `gi://` arm, ahead of the empty redirect so it
|
|
102
|
+
// claims the specifier first, exactly as `gjsGiNodePlugin` does on the node
|
|
103
|
+
// target. `emptyGirs` follows it: with the arm on, `@girs/<ns>-<ver>` must
|
|
104
|
+
// reach its real body so its inner `gi://` lands here instead of being
|
|
105
|
+
// stranded as `{}`.
|
|
106
|
+
...(giRenderer ? [giRendererPlugin({ app: 'browser', ...giRenderer })] : []),
|
|
107
|
+
gjsImportsEmptyPlugin({ emptyGirs: !giRenderer }),
|
|
94
108
|
aliasPlugin({ entries: aliasEntries }),
|
|
95
109
|
blueprintPlugin(),
|
|
96
110
|
cssAsStringPlugin(),
|
package/lib/app/nativescript.js
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
//
|
|
14
14
|
// `@girs/*` and `gi://*` imports are silenced via `gjsImportsEmptyPlugin` —
|
|
15
15
|
// they appear transitively through `@gjsify/unit` and similar packages with
|
|
16
|
-
// GJS-specific code paths that never execute on NS.
|
|
16
|
+
// GJS-specific code paths that never execute on NS. `--gi-renderer` (ADR 0034
|
|
17
|
+
// stage 9) opts out of that for `gi://Adw` / `gi://Gtk`, answering them from
|
|
18
|
+
// `@gjsify/adwaita-nativescript` and carving `@girs/*` out of the empty redirect.
|
|
17
19
|
//
|
|
18
20
|
// Bare Node specifiers + their `node:*` prefix variants are routed to
|
|
19
21
|
// `@gjsify/<X>` via the curated `ALIASES_NODE_FOR_NATIVESCRIPT` table. Unlike
|
|
@@ -34,9 +36,10 @@
|
|
|
34
36
|
// of `@nativescript/core`) and no `blueprintPlugin` (Blueprint is GTK-only).
|
|
35
37
|
import { aliasPlugin } from '../plugins/alias.js';
|
|
36
38
|
import { deepkitPlugin } from '@gjsify/rolldown-plugin-deepkit';
|
|
37
|
-
import { ALIASES_NODE_FOR_NATIVESCRIPT, getDerivedAliasesSync } from '@gjsify/resolve-npm';
|
|
39
|
+
import { ALIASES_NODE_FOR_NATIVESCRIPT, GI_RENDERERS, getDerivedAliasesSync } from '@gjsify/resolve-npm';
|
|
38
40
|
import { globToEntryPoints } from '../utils/entry-points.js';
|
|
39
41
|
import { gjsImportsEmptyPlugin } from '../plugins/gjs-imports-empty.js';
|
|
42
|
+
import { giRendererPlugin } from '../plugins/gi-renderer.js';
|
|
40
43
|
import { platformResolvePlugin, nativescriptSuffixChain, detectNativescriptPlatform, nativescriptPlatformDefines, } from '../plugins/platform-resolve.js';
|
|
41
44
|
import { unresolvedWorkspaceImportPlugin } from '../plugins/unresolved-workspace-import.js';
|
|
42
45
|
// Never bundled: an app works only with the ONE core instance the runtime boots, and what
|
|
@@ -49,6 +52,9 @@ export const setupForNativescript = async (input) => {
|
|
|
49
52
|
const isExternal = (id) => external.some((entry) => (typeof entry === 'string' ? entry === id : entry.test(id)));
|
|
50
53
|
const exclude = input.pluginOptions.exclude ?? [];
|
|
51
54
|
const entryPoints = await globToEntryPoints(input.input, exclude);
|
|
55
|
+
// The arm is composed only when asked for. `config.ts` already refuses the flag
|
|
56
|
+
// on a target with no row, so an undefined row here means the flag was not passed.
|
|
57
|
+
const giRenderer = input.pluginOptions.giRenderer ? GI_RENDERERS['nativescript'] : undefined;
|
|
52
58
|
// Target platform — discovered from the env NS' CLI sets when it spawns a
|
|
53
59
|
// bundler (else `undefined` → `.native`-only resolution + neutral defines).
|
|
54
60
|
// `gjsify build` is a production bundler, so `__DEV__` is `false` here.
|
|
@@ -128,7 +134,13 @@ export const setupForNativescript = async (input) => {
|
|
|
128
134
|
// transform can strip the annotations out from under it (see `GjsifyConfig`).
|
|
129
135
|
const prePlugins = [deepkitPlugin({ reflection: input.pluginOptions.reflection })];
|
|
130
136
|
const plugins = [
|
|
131
|
-
|
|
137
|
+
// ADR 0034 stage 9 — the `gi://` arm, ahead of the empty redirect so it
|
|
138
|
+
// claims the specifier first, exactly as `gjsGiNodePlugin` does on the node
|
|
139
|
+
// target. `emptyGirs` follows it: with the arm on, `@girs/<ns>-<ver>` must
|
|
140
|
+
// reach its real body so its inner `gi://` lands here instead of being
|
|
141
|
+
// stranded as `{}`.
|
|
142
|
+
...(giRenderer ? [giRendererPlugin({ app: 'nativescript', ...giRenderer })] : []),
|
|
143
|
+
gjsImportsEmptyPlugin({ emptyGirs: !giRenderer }),
|
|
132
144
|
// Platform-specific source variants (`*.android` / `*.ios` /
|
|
133
145
|
// `*.native`) win over the base file — resolved BEFORE the Node-builtin
|
|
134
146
|
// alias routing so a platform fork of a portable module is honored.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Plugin } from 'rolldown';
|
|
2
|
+
export interface GiRendererOptions {
|
|
3
|
+
/** The `--app` target, named in every refusal so the message says WHERE it applies. */
|
|
4
|
+
app: string;
|
|
5
|
+
/** The package whose `Adw` / `Gtk` namespace exports answer `gi://` on this target. */
|
|
6
|
+
renderer: string;
|
|
7
|
+
/** GI namespace → the version this arm answers for, from `@gjsify/resolve-npm`. */
|
|
8
|
+
namespaces: Readonly<Record<string, string>>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The runtime shim for one answered namespace.
|
|
12
|
+
*
|
|
13
|
+
* Exports `default` and nothing else, which is what GJS's own `gi://` module exports:
|
|
14
|
+
* measured on gjs 1.88.1, `import * as Adw from 'gi://Adw?version=1'` yields a namespace
|
|
15
|
+
* whose only key is `default`, and `import { ActionRow } from 'gi://Adw?version=1'` is a
|
|
16
|
+
* SyntaxError (`doesn't provide an export named: 'ActionRow'`). Adding named member
|
|
17
|
+
* exports here would compile on these two targets and fail on `--app gjs` — a second
|
|
18
|
+
* spelling, which is the thing ADR 0034 exists to remove.
|
|
19
|
+
*
|
|
20
|
+
* ITS REFUSAL NAMES THE NAMESPACE AND VERSION SEPARATELY RATHER THAN QUOTING THE
|
|
21
|
+
* SPECIFIER. `tests/e2e/gi-renderer-arms` asserts `!bundle.includes('gi://')` on the
|
|
22
|
+
* bundles it builds — a SUBSTRING, because on these two targets an unresolved GI import
|
|
23
|
+
* is a missing alias (`tests/AGENTS.md`) — and this string is reachable, so rolldown keeps
|
|
24
|
+
* it. Measured: with the specifier quoted, both green probe bundles carried one `gi://`
|
|
25
|
+
* occurrence and neither of them was an import.
|
|
26
|
+
*
|
|
27
|
+
* THE CONSTRAINT IS SELF-IMPOSED, and only that one suite imposes it. `app-browser` and
|
|
28
|
+
* `ns-bridge-bundles` hold the same substring rule over their own bundles, but neither
|
|
29
|
+
* passes `--gi-renderer`, so the arm never composes there and this text cannot reach them
|
|
30
|
+
* — measured, nothing under `tests/` names the flag but `gi-renderer-arms` itself. Making
|
|
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`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function giRendererShimSource(options: GiRendererOptions, namespace: string, version: string): string;
|
|
35
|
+
export declare function giRendererPlugin(options: GiRendererOptions): Plugin;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// For `--app browser` / `--app nativescript`: resolve `gi://Ns?version=X` to the
|
|
2
|
+
// TARGET'S WIDGET RENDERER instead of an empty module — ADR 0034 stage 9, the
|
|
3
|
+
// `gi://` arms.
|
|
4
|
+
//
|
|
5
|
+
// WHAT IT BUYS. `import Adw from 'gi://Adw?version=1'` is the spelling every GJS
|
|
6
|
+
// source already uses, and `@girs/adw-1/adw-1.js` is literally that line plus
|
|
7
|
+
// `export default Adw`. Without an arm the specifier lands in
|
|
8
|
+
// `gjsImportsEmptyPlugin` and becomes `{}`, so `Adw.ActionRow` is `undefined` and
|
|
9
|
+
// `class Row extends Adw.ActionRow {}` throws `Class extends value undefined is not
|
|
10
|
+
// a constructor or null` — measured on both targets before this plugin existed, and
|
|
11
|
+
// the exact failure the sibling header records for `@girs/*`. With the arm the same
|
|
12
|
+
// import evaluates to `@gjsify/adwaita-web`'s (or `@gjsify/adwaita-nativescript`'s)
|
|
13
|
+
// `Adw` namespace, which ADR 0034 clause 2 makes a namespace OBJECT precisely so it
|
|
14
|
+
// can stand in for the GI one.
|
|
15
|
+
//
|
|
16
|
+
// IT IS OPT-IN (`--gi-renderer`), and NOT because turning it on breaks anything here.
|
|
17
|
+
// That was the hypothesis and it is measured wrong: forcing the arm on across the 53
|
|
18
|
+
// `--app browser` test entries and the 5 `packages/nativescript-bridge/*` packages in
|
|
19
|
+
// this repository changes ZERO outcomes, because no `gi://` reaches those builds at
|
|
20
|
+
// all — instrumented, the only specifier that arrives at the empty-import plugin is
|
|
21
|
+
// `@girs/gjs` (7 entries), whose body is `globalThis.imports || {}`. That measurement
|
|
22
|
+
// bounds the blast radius HERE and says nothing about a consumer tree, where a
|
|
23
|
+
// transitively-imported `gi://GLib` would go from a silent stub to a build failure.
|
|
24
|
+
//
|
|
25
|
+
// What opt-in buys is that the arm makes a tier-2 widget toolkit a BUILD-TIME
|
|
26
|
+
// DEPENDENCY of any bundle naming `gi://Adw` (measured on the probe fixture:
|
|
27
|
+
// 133 bytes to 500 202 on browser and 214 041 on NativeScript, the latter with a
|
|
28
|
+
// `@nativescript/core` import clause the flag-less bundle did not have), and nothing
|
|
29
|
+
// in a tree can infer that its `gi://Adw` was meant to be `@gjsify/adwaita-web`
|
|
30
|
+
// rather than nothing. `--dialect` is opt-in for the same shape of reason; see
|
|
31
|
+
// `PluginOptions.dialect`.
|
|
32
|
+
//
|
|
33
|
+
// THREE REFUSALS, EACH AT THE EARLIEST POINT ITS FACT IS KNOWN.
|
|
34
|
+
//
|
|
35
|
+
// 1. A namespace with no renderer — `gi://Gio` — is refused at BUILD time, from
|
|
36
|
+
// `resolveId`. The specifier carries the namespace, so nothing has to run for
|
|
37
|
+
// the answer to be knowable, and the alternative is the empty module this arm
|
|
38
|
+
// exists to replace.
|
|
39
|
+
// 2. A `?version=` the renderer's vocabulary was not generated against is refused
|
|
40
|
+
// at BUILD time too. GJS itself throws here (`Requiring Adw, version 9: Typelib
|
|
41
|
+
// file for namespace 'Adw', version '9' not found`, measured on gjs 1.88.1), so
|
|
42
|
+
// accepting it would make these two targets the only place a wrong version
|
|
43
|
+
// passes.
|
|
44
|
+
// 3. An ABSENT MEMBER — `Adw.Window` on a renderer that ships no window — is
|
|
45
|
+
// refused at RUNTIME, by a Proxy, because a property access is not knowable
|
|
46
|
+
// from a specifier. These namespaces are SPARSE by construction (a renderer
|
|
47
|
+
// implements a fraction of the GIR's widgets), so this is ADR 0034 § 1 clause 3
|
|
48
|
+
// at member granularity: reading an absent member would hand back `undefined`
|
|
49
|
+
// and re-create the very `Class extends value undefined` this arm removes,
|
|
50
|
+
// thrown far from the import that caused it.
|
|
51
|
+
//
|
|
52
|
+
// WHY A PROXY AND NOT A COPIED OBJECT. The member set has to be the renderer's own,
|
|
53
|
+
// read at runtime from the module it re-exports — a second list here would be the
|
|
54
|
+
// copy that drifts, and `Object.keys` of the real namespace is also what makes the
|
|
55
|
+
// refusal able to PRINT what is available.
|
|
56
|
+
//
|
|
57
|
+
// THE EMITTED MODULE IS NOT ALIASED, by construction: `aliasPlugin` skips importers
|
|
58
|
+
// whose id carries `GJSIFY_VIRTUAL_PREFIX`, so the renderer import below names the
|
|
59
|
+
// package the arm decided on and no slot-routing or user `--alias` re-points it. On
|
|
60
|
+
// `--app nativescript` that layer would today be a no-op anyway — the renderer
|
|
61
|
+
// declares `nativescript: "native"` and ships no `globals.mjs`, so the derived map
|
|
62
|
+
// leaves it alone with a warn-once (visible in every NS build's output) — but the
|
|
63
|
+
// exemption is what keeps that true after the slot vocabulary is settled.
|
|
64
|
+
//
|
|
65
|
+
// Portability note (as in `gjs-imports-empty.ts`): the `filter` is a Rolldown
|
|
66
|
+
// fast-path; the guard inside the handler is the load-bearing check, so the plugin
|
|
67
|
+
// stays correct under an engine that does not pre-filter.
|
|
68
|
+
import { GJSIFY_VIRTUAL_PREFIX } from '../utils/virtual-module-id.js';
|
|
69
|
+
import { parseGiSpecifier } from './gjs-gi-node.js';
|
|
70
|
+
const GI_RENDERER_VIRTUAL_PREFIX = `${GJSIFY_VIRTUAL_PREFIX}gi-renderer:`;
|
|
71
|
+
/**
|
|
72
|
+
* The runtime shim for one answered namespace.
|
|
73
|
+
*
|
|
74
|
+
* Exports `default` and nothing else, which is what GJS's own `gi://` module exports:
|
|
75
|
+
* measured on gjs 1.88.1, `import * as Adw from 'gi://Adw?version=1'` yields a namespace
|
|
76
|
+
* whose only key is `default`, and `import { ActionRow } from 'gi://Adw?version=1'` is a
|
|
77
|
+
* SyntaxError (`doesn't provide an export named: 'ActionRow'`). Adding named member
|
|
78
|
+
* exports here would compile on these two targets and fail on `--app gjs` — a second
|
|
79
|
+
* spelling, which is the thing ADR 0034 exists to remove.
|
|
80
|
+
*
|
|
81
|
+
* ITS REFUSAL NAMES THE NAMESPACE AND VERSION SEPARATELY RATHER THAN QUOTING THE
|
|
82
|
+
* SPECIFIER. `tests/e2e/gi-renderer-arms` asserts `!bundle.includes('gi://')` on the
|
|
83
|
+
* bundles it builds — a SUBSTRING, because on these two targets an unresolved GI import
|
|
84
|
+
* is a missing alias (`tests/AGENTS.md`) — and this string is reachable, so rolldown keeps
|
|
85
|
+
* it. Measured: with the specifier quoted, both green probe bundles carried one `gi://`
|
|
86
|
+
* occurrence and neither of them was an import.
|
|
87
|
+
*
|
|
88
|
+
* THE CONSTRAINT IS SELF-IMPOSED, and only that one suite imposes it. `app-browser` and
|
|
89
|
+
* `ns-bridge-bundles` hold the same substring rule over their own bundles, but neither
|
|
90
|
+
* passes `--gi-renderer`, so the arm never composes there and this text cannot reach them
|
|
91
|
+
* — measured, nothing under `tests/` names the flag but `gi-renderer-arms` itself. Making
|
|
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`.
|
|
94
|
+
*/
|
|
95
|
+
export function giRendererShimSource(options, namespace, version) {
|
|
96
|
+
return (`import { ${namespace} as namespace } from ${JSON.stringify(options.renderer)};\n` +
|
|
97
|
+
`const RENDERER = ${JSON.stringify(options.renderer)};\n` +
|
|
98
|
+
`const APP = ${JSON.stringify(options.app)};\n` +
|
|
99
|
+
`const NS = ${JSON.stringify(namespace)};\n` +
|
|
100
|
+
`const VERSION = ${JSON.stringify(version)};\n` +
|
|
101
|
+
`export default new Proxy(namespace, {\n` +
|
|
102
|
+
` get(target, property, receiver) {\n` +
|
|
103
|
+
// Symbols are protocol probes (`Symbol.toStringTag`, `Symbol.iterator`,
|
|
104
|
+
// `Symbol.toPrimitive`) and `then` is the thenable probe every `await` and
|
|
105
|
+
// dynamic `import()` performs; refusing those would break the module rather
|
|
106
|
+
// than report a missing widget.
|
|
107
|
+
` if (typeof property === 'symbol' || property === 'then' || property in target) {\n` +
|
|
108
|
+
` return Reflect.get(target, property, receiver);\n` +
|
|
109
|
+
` }\n` +
|
|
110
|
+
` throw new Error(\n` +
|
|
111
|
+
` 'the ' + NS + ' arm (version ' + VERSION + ') on --app ' + APP + ': ' + RENDERER +\n` +
|
|
112
|
+
` ' has no ' + String(property) + '. A renderer implements a fraction of the GIR ' +\n` +
|
|
113
|
+
` 'vocabulary, and every absent member is a declared remainder (ADR 0034 clause 3) ' +\n` +
|
|
114
|
+
` 'rather than an oversight — refused by name here because reading it would be ' +\n` +
|
|
115
|
+
` 'undefined and \`class X extends undefined\` throws far from the import that asked ' +\n` +
|
|
116
|
+
` 'for it. It has: ' + Object.keys(target).sort().join(', ') + '.'\n` +
|
|
117
|
+
` );\n` +
|
|
118
|
+
` },\n` +
|
|
119
|
+
`});\n`);
|
|
120
|
+
}
|
|
121
|
+
export function giRendererPlugin(options) {
|
|
122
|
+
const known = Object.keys(options.namespaces).sort();
|
|
123
|
+
const answered = known.map((name) => `${name} (version ${options.namespaces[name]})`).join(', ');
|
|
124
|
+
return {
|
|
125
|
+
name: 'gjsify-gi-renderer',
|
|
126
|
+
resolveId: {
|
|
127
|
+
order: 'pre',
|
|
128
|
+
filter: { id: /^gi:\/\// },
|
|
129
|
+
handler(source) {
|
|
130
|
+
const parsed = parseGiSpecifier(source);
|
|
131
|
+
if (parsed === null)
|
|
132
|
+
return null;
|
|
133
|
+
const expected = options.namespaces[parsed.namespace];
|
|
134
|
+
if (expected === undefined) {
|
|
135
|
+
throw new Error(`gjsify build --app ${options.app}: \`${source}\` has no widget renderer. ` +
|
|
136
|
+
`--gi-renderer answers ${answered} out of ${options.renderer} and nothing else — ` +
|
|
137
|
+
'there is no GObject introspection on this target, so a namespace with no renderer ' +
|
|
138
|
+
'counterpart cannot be substituted for. Drop --gi-renderer to keep the previous ' +
|
|
139
|
+
'behaviour (every `gi://` import becomes an empty module), or gate the import to ' +
|
|
140
|
+
'a target that has GI (`--app gjs`, `--app node`).');
|
|
141
|
+
}
|
|
142
|
+
if (parsed.version !== undefined && parsed.version !== expected) {
|
|
143
|
+
throw new Error(`gjsify build --app ${options.app}: \`${source}\` asks for ${parsed.namespace} version ` +
|
|
144
|
+
`${parsed.version}; this arm answers version ${expected} — the version ` +
|
|
145
|
+
`${options.renderer}'s vocabulary is held against. GJS throws on a version it has no ` +
|
|
146
|
+
'typelib for, so accepting it here would make this target the only place a wrong ' +
|
|
147
|
+
'version passes.');
|
|
148
|
+
}
|
|
149
|
+
return { id: `${GI_RENDERER_VIRTUAL_PREFIX}${parsed.namespace}` };
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
load(id) {
|
|
153
|
+
if (!id.startsWith(GI_RENDERER_VIRTUAL_PREFIX))
|
|
154
|
+
return null;
|
|
155
|
+
const namespace = id.slice(GI_RENDERER_VIRTUAL_PREFIX.length);
|
|
156
|
+
const version = options.namespaces[namespace];
|
|
157
|
+
if (version === undefined)
|
|
158
|
+
return null;
|
|
159
|
+
return { code: giRendererShimSource(options, namespace, version), moduleSideEffects: false };
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -87,6 +87,24 @@ export interface PluginOptions {
|
|
|
87
87
|
* opposite questions and once shared a name. See `SourceDialect`.
|
|
88
88
|
*/
|
|
89
89
|
dialect?: SourceDialect;
|
|
90
|
+
/**
|
|
91
|
+
* Resolve `gi://Ns?version=X` to the target's widget renderer rather than to an
|
|
92
|
+
* empty module — ADR 0034 stage 9. `--app browser` answers out of
|
|
93
|
+
* `@gjsify/adwaita-web`, `--app nativescript` out of `@gjsify/adwaita-nativescript`;
|
|
94
|
+
* the table is `GI_RENDERERS` in `@gjsify/resolve-npm`, and a target with no row
|
|
95
|
+
* there has no arm.
|
|
96
|
+
*
|
|
97
|
+
* Composing it also sets `emptyGirs: false` on the same build, so `@girs/adw-1`
|
|
98
|
+
* reaches its real body (`import Adw from 'gi://Adw?version=1'; export default Adw`)
|
|
99
|
+
* and its inner `gi://` is claimed here — identical to the node-gi path.
|
|
100
|
+
*
|
|
101
|
+
* OPT-IN and never inferred, for the reason `plugins/gi-renderer.ts` states at
|
|
102
|
+
* length: a `gi://` import reaching a browser or NativeScript build today is
|
|
103
|
+
* usually a GJS-only code path pulled in transitively, and answering it by default
|
|
104
|
+
* would both drag a widget toolkit into bundles that never asked for one and turn
|
|
105
|
+
* every unanswerable namespace from a silent stub into a hard build failure.
|
|
106
|
+
*/
|
|
107
|
+
giRenderer?: boolean;
|
|
90
108
|
/**
|
|
91
109
|
* Where to look for a `@gjsify/*` the PROJECT cannot resolve, when the input is
|
|
92
110
|
* TOOLCHAIN rather than user code: a file path inside the running CLI's own
|
package/lib/utils/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export * from './runtime.js';
|
|
|
5
5
|
export * from './merge.js';
|
|
6
6
|
export { detectFreeGlobals } from './detect-free-globals.js';
|
|
7
7
|
export { resolveGlobalsList, writeRegisterInjectFile } from './scan-globals.js';
|
|
8
|
-
export { inlineStaticReads, isAbsoluteFsPath } from './inline-static-reads.js';
|
|
8
|
+
export { inlineStaticReads, isAbsoluteFsPath, isWithin, resourceRootFor } from './inline-static-reads.js';
|
|
9
9
|
export { GJSIFY_VIRTUAL_PREFIX, isGjsifyVirtualModuleId } from './virtual-module-id.js';
|
|
10
10
|
export { locateSurvivingJsx, classifyJsxParseFailure, formatSurvivingJsx } from './jsx-survival.js';
|
|
11
11
|
export type { SurvivingJsx } from './jsx-survival.js';
|
package/lib/utils/index.js
CHANGED
|
@@ -5,6 +5,6 @@ export * from './runtime.js';
|
|
|
5
5
|
export * from './merge.js';
|
|
6
6
|
export { detectFreeGlobals } from './detect-free-globals.js';
|
|
7
7
|
export { resolveGlobalsList, writeRegisterInjectFile } from './scan-globals.js';
|
|
8
|
-
export { inlineStaticReads, isAbsoluteFsPath } from './inline-static-reads.js';
|
|
8
|
+
export { inlineStaticReads, isAbsoluteFsPath, isWithin, resourceRootFor } from './inline-static-reads.js';
|
|
9
9
|
export { GJSIFY_VIRTUAL_PREFIX, isGjsifyVirtualModuleId } from './virtual-module-id.js';
|
|
10
10
|
export { locateSurvivingJsx, classifyJsxParseFailure, formatSurvivingJsx } from './jsx-survival.js';
|
|
@@ -2,6 +2,55 @@ export declare function inlineStaticReads(src: string, sourceFilePath: string):
|
|
|
2
2
|
contents: string;
|
|
3
3
|
inlined: number;
|
|
4
4
|
};
|
|
5
|
+
/**
|
|
6
|
+
* The tree whose contents are a property of the BUILD rather than of the host —
|
|
7
|
+
* the reading module's own package.
|
|
8
|
+
*
|
|
9
|
+
* A package's own resources are what this module exists to bundle. Anything else
|
|
10
|
+
* — `/lib`, `/etc`, `/proc`, a sibling package's data — is either the machine's
|
|
11
|
+
* layout or another package's business, and its answer at build time is not its
|
|
12
|
+
* answer at run time.
|
|
13
|
+
*
|
|
14
|
+
* TWO RULES, because "the package" is answered two different ways and picking one
|
|
15
|
+
* of them gets the other wrong.
|
|
16
|
+
*
|
|
17
|
+
* For an INSTALLED file the root is STRUCTURAL: the directory whose parent is
|
|
18
|
+
* `node_modules` (or `node_modules/@scope`). Probing for the nearest
|
|
19
|
+
* `package.json` instead lands on the wrong directory for any package that
|
|
20
|
+
* dual-publishes, because the `{"type":"commonjs"}` marker beside the emitted
|
|
21
|
+
* output IS a `package.json`, and some of those markers carry the package's own
|
|
22
|
+
* `name` too — so neither presence nor a `name` tells a marker from a root.
|
|
23
|
+
* Measured in this repo's installed tree: `engine.io-client/build/{cjs,esm}` and
|
|
24
|
+
* `@socket.io/component-emitter/lib/cjs` are that shape. A module there reading
|
|
25
|
+
* its package's real manifest — the `new URL('../package.json',
|
|
26
|
+
* import.meta.url)` this file's header opens with, one level deeper — would be
|
|
27
|
+
* reading OUTSIDE its root, get declined, and ship the live read this module
|
|
28
|
+
* exists to remove.
|
|
29
|
+
*
|
|
30
|
+
* For a FIRST-PARTY file the root is the nearest ancestor carrying a
|
|
31
|
+
* `package.json`, and it has to be the NEAREST: in a monorepo the outermost one
|
|
32
|
+
* is the workspace root, which would let one package inline its siblings' files.
|
|
33
|
+
* The fallback when nothing above declares one is the module's own directory,
|
|
34
|
+
* which is the narrowest answer available rather than a licence to read upward.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resourceRootFor(sourceFilePath: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Is `target` inside `root` (or `root` itself)?
|
|
39
|
+
*
|
|
40
|
+
* `relative()` rather than `startsWith`: the string test calls `/lib64` a child
|
|
41
|
+
* of `/lib`. `resolve()` first, so a `..` composition is normalised before the
|
|
42
|
+
* comparison rather than after it. `platform` is injected and the branches use
|
|
43
|
+
* `path.win32`/`path.posix` explicitly, the same shape as
|
|
44
|
+
* {@link isAbsoluteFsPath}, so both run from either host.
|
|
45
|
+
*
|
|
46
|
+
* TEXTUAL, deliberately: no `realpathSync`. Both directions of that choice are
|
|
47
|
+
* the safe one here. A read that reaches into the package through a symlink
|
|
48
|
+
* resolves to a path outside the root and is DECLINED, which leaves a working
|
|
49
|
+
* runtime read; and a path textually inside the root cannot be widened by a
|
|
50
|
+
* symlink npm never creates. Resolving would also cost a syscall per candidate
|
|
51
|
+
* and break the zip-resident PnP case, whose paths are not files.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isWithin(target: string, root: string, platform?: string): boolean;
|
|
5
54
|
/**
|
|
6
55
|
* Is `v` an absolute filesystem path on `platform`?
|
|
7
56
|
*
|
|
@@ -40,6 +40,27 @@
|
|
|
40
40
|
//
|
|
41
41
|
// Anything not statically resolvable is left untouched — the legacy
|
|
42
42
|
// `import.meta.url` rewriter still applies as a fallback.
|
|
43
|
+
//
|
|
44
|
+
// WHAT IS DELIBERATELY NOT INLINED, and it is not about resolvability. A read
|
|
45
|
+
// only belongs in the bundle when its answer is a property of the BUILD; a read
|
|
46
|
+
// of the machine's own layout is a property of the HOST, and freezing it ships
|
|
47
|
+
// the build machine's answer to every user. `evalPathExpr` therefore refuses a
|
|
48
|
+
// path that resolves outside the reading module's own package (see
|
|
49
|
+
// `resourceRootFor`), however statically resolvable it is.
|
|
50
|
+
//
|
|
51
|
+
// Measured on a OnePlus 6T (postmarketOS, musl 1.2.6, aarch64) against the
|
|
52
|
+
// bundled CLI of 2026-09-08, before that gate existed:
|
|
53
|
+
//
|
|
54
|
+
// readdirSync('/lib') 77 entries, no ld-musl-*
|
|
55
|
+
// existsSync('/lib/ld-musl-aarch64.so.1') false
|
|
56
|
+
//
|
|
57
|
+
// Both answers were the Fedora build host's, baked in at build time — the real
|
|
58
|
+
// directory holds 2076 entries including `ld-musl-aarch64.so.1`. That is the
|
|
59
|
+
// probe `detectHostLibc` reads, so a bundled `gjsify` answered `glibc` on a musl
|
|
60
|
+
// machine and every musl-aware decision downstream — `prebuildDirCandidates`'
|
|
61
|
+
// `-musl` preference, the launcher shims, the fallback report — was inert for
|
|
62
|
+
// the life of the bundle. Ten runs, ten times `glibc`. Nothing failed loudly:
|
|
63
|
+
// the glibc prebuild loads under musl's loader until it wants a glibc symbol.
|
|
43
64
|
import * as acorn from 'acorn';
|
|
44
65
|
import { tsPlugin } from 'acorn-typescript';
|
|
45
66
|
import { dirname, join, resolve, basename, relative, extname, posix, win32 } from 'node:path';
|
|
@@ -138,6 +159,7 @@ export function inlineStaticReads(src, sourceFilePath) {
|
|
|
138
159
|
}
|
|
139
160
|
const ctx = {
|
|
140
161
|
sourceUrl: pathToFileURL(sourceFilePath).href,
|
|
162
|
+
resourceRoot: resourceRootFor(sourceFilePath),
|
|
141
163
|
};
|
|
142
164
|
const edits = [];
|
|
143
165
|
forEachCallExpression(ast, (node) => {
|
|
@@ -341,18 +363,125 @@ function tryInlineReadFile(node, ctx, forceTextEncoding) {
|
|
|
341
363
|
*/
|
|
342
364
|
function evalPathExpr(node, ctx) {
|
|
343
365
|
const v = evalExpr(node, ctx);
|
|
366
|
+
// ONE exit for both shapes, and that is the whole point of the arrangement. The
|
|
367
|
+
// URL branch used to `return` before any gate ran, so `new URL('/proc',
|
|
368
|
+
// import.meta.url)` and `new URL('file:///lib')` were read on the builder while
|
|
369
|
+
// the same read spelled `fileURLToPath(new URL(…))` was declined. Two spellings
|
|
370
|
+
// of one expression cannot answer differently, and `new URL(<lit>,
|
|
371
|
+
// import.meta.url)` is the spelling this module was written for.
|
|
372
|
+
let asPath;
|
|
344
373
|
if (v instanceof URL) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
374
|
+
asPath = v.protocol === 'file:' ? fileURLToPath(v) : undefined;
|
|
375
|
+
}
|
|
376
|
+
else if (typeof v === 'string') {
|
|
377
|
+
asPath = v.startsWith('file://') ? fileURLToPath(v) : isAbsoluteFsPath(v) ? v : undefined;
|
|
348
378
|
}
|
|
349
|
-
if (
|
|
379
|
+
if (asPath === undefined)
|
|
350
380
|
return undefined;
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
381
|
+
// Resolvable is not the same as bundlable. Declining is the safe direction:
|
|
382
|
+
// the call is simply left in place and reads at runtime, which is what a host
|
|
383
|
+
// probe wants. Silent on purpose — a probe of `/proc`, `/sys` or `/lib` is
|
|
384
|
+
// ordinary in a runtime shim, and warning about each one would bury the
|
|
385
|
+
// decline that matters under the ones that do not. What stops this silence
|
|
386
|
+
// costing what `isAbsoluteFsPath`'s did is that the declines have ROWS in
|
|
387
|
+
// `inline-static-reads.spec.ts` — a warning nobody reads would not have caught
|
|
388
|
+
// that one either.
|
|
389
|
+
if (!isWithin(asPath, ctx.resourceRoot))
|
|
390
|
+
return undefined;
|
|
391
|
+
return asPath;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* The tree whose contents are a property of the BUILD rather than of the host —
|
|
395
|
+
* the reading module's own package.
|
|
396
|
+
*
|
|
397
|
+
* A package's own resources are what this module exists to bundle. Anything else
|
|
398
|
+
* — `/lib`, `/etc`, `/proc`, a sibling package's data — is either the machine's
|
|
399
|
+
* layout or another package's business, and its answer at build time is not its
|
|
400
|
+
* answer at run time.
|
|
401
|
+
*
|
|
402
|
+
* TWO RULES, because "the package" is answered two different ways and picking one
|
|
403
|
+
* of them gets the other wrong.
|
|
404
|
+
*
|
|
405
|
+
* For an INSTALLED file the root is STRUCTURAL: the directory whose parent is
|
|
406
|
+
* `node_modules` (or `node_modules/@scope`). Probing for the nearest
|
|
407
|
+
* `package.json` instead lands on the wrong directory for any package that
|
|
408
|
+
* dual-publishes, because the `{"type":"commonjs"}` marker beside the emitted
|
|
409
|
+
* output IS a `package.json`, and some of those markers carry the package's own
|
|
410
|
+
* `name` too — so neither presence nor a `name` tells a marker from a root.
|
|
411
|
+
* Measured in this repo's installed tree: `engine.io-client/build/{cjs,esm}` and
|
|
412
|
+
* `@socket.io/component-emitter/lib/cjs` are that shape. A module there reading
|
|
413
|
+
* its package's real manifest — the `new URL('../package.json',
|
|
414
|
+
* import.meta.url)` this file's header opens with, one level deeper — would be
|
|
415
|
+
* reading OUTSIDE its root, get declined, and ship the live read this module
|
|
416
|
+
* exists to remove.
|
|
417
|
+
*
|
|
418
|
+
* For a FIRST-PARTY file the root is the nearest ancestor carrying a
|
|
419
|
+
* `package.json`, and it has to be the NEAREST: in a monorepo the outermost one
|
|
420
|
+
* is the workspace root, which would let one package inline its siblings' files.
|
|
421
|
+
* The fallback when nothing above declares one is the module's own directory,
|
|
422
|
+
* which is the narrowest answer available rather than a licence to read upward.
|
|
423
|
+
*/
|
|
424
|
+
export function resourceRootFor(sourceFilePath) {
|
|
425
|
+
const own = dirname(resolve(sourceFilePath));
|
|
426
|
+
const installed = installedPackageRootFor(own);
|
|
427
|
+
if (installed !== undefined)
|
|
428
|
+
return installed;
|
|
429
|
+
let dir = own;
|
|
430
|
+
for (;;) {
|
|
431
|
+
if (existsSync(join(dir, 'package.json')))
|
|
432
|
+
return dir;
|
|
433
|
+
const parent = dirname(dir);
|
|
434
|
+
if (parent === dir)
|
|
435
|
+
return own;
|
|
436
|
+
dir = parent;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* The `node_modules/<name>` — or `node_modules/@scope/<name>` — directory `dir`
|
|
441
|
+
* lives in, or `undefined` when it is not inside an installed package.
|
|
442
|
+
*
|
|
443
|
+
* The LAST `node_modules` segment on the way up, so a nested dependency
|
|
444
|
+
* (`node_modules/a/node_modules/b`) and a Yarn-PnP zip
|
|
445
|
+
* (`…/cache/x.zip/node_modules/@scope/b`) both resolve to themselves rather than
|
|
446
|
+
* to whatever contains them. No filesystem probe: the layout is the answer, and a
|
|
447
|
+
* zip-resident path has no `package.json` a plain `existsSync` can see.
|
|
448
|
+
*/
|
|
449
|
+
function installedPackageRootFor(dir) {
|
|
450
|
+
let cur = dir;
|
|
451
|
+
for (;;) {
|
|
452
|
+
const parent = dirname(cur);
|
|
453
|
+
if (parent === cur)
|
|
454
|
+
return undefined;
|
|
455
|
+
const parentName = basename(parent);
|
|
456
|
+
if (parentName === 'node_modules')
|
|
457
|
+
return cur;
|
|
458
|
+
if (parentName.startsWith('@') && basename(dirname(parent)) === 'node_modules')
|
|
459
|
+
return cur;
|
|
460
|
+
cur = parent;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Is `target` inside `root` (or `root` itself)?
|
|
465
|
+
*
|
|
466
|
+
* `relative()` rather than `startsWith`: the string test calls `/lib64` a child
|
|
467
|
+
* of `/lib`. `resolve()` first, so a `..` composition is normalised before the
|
|
468
|
+
* comparison rather than after it. `platform` is injected and the branches use
|
|
469
|
+
* `path.win32`/`path.posix` explicitly, the same shape as
|
|
470
|
+
* {@link isAbsoluteFsPath}, so both run from either host.
|
|
471
|
+
*
|
|
472
|
+
* TEXTUAL, deliberately: no `realpathSync`. Both directions of that choice are
|
|
473
|
+
* the safe one here. A read that reaches into the package through a symlink
|
|
474
|
+
* resolves to a path outside the root and is DECLINED, which leaves a working
|
|
475
|
+
* runtime read; and a path textually inside the root cannot be widened by a
|
|
476
|
+
* symlink npm never creates. Resolving would also cost a syscall per candidate
|
|
477
|
+
* and break the zip-resident PnP case, whose paths are not files.
|
|
478
|
+
*/
|
|
479
|
+
export function isWithin(target, root, platform = process.platform) {
|
|
480
|
+
const p = platform === 'win32' ? win32 : posix;
|
|
481
|
+
const rel = p.relative(p.resolve(root), p.resolve(target));
|
|
482
|
+
if (rel === '')
|
|
483
|
+
return true;
|
|
484
|
+
return !rel.startsWith('..') && !p.isAbsolute(rel);
|
|
356
485
|
}
|
|
357
486
|
/**
|
|
358
487
|
* Is `v` an absolute filesystem path on `platform`?
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/rolldown-plugin-gjsify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.0",
|
|
4
4
|
"description": "Rolldown / Rollup / Vite plugin orchestrator for GJS, Node, and Browser targets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -60,11 +60,11 @@
|
|
|
60
60
|
],
|
|
61
61
|
"license": "MIT",
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@gjsify/console": "^0.
|
|
64
|
-
"@gjsify/resolve-npm": "^0.
|
|
65
|
-
"@gjsify/rolldown-plugin-deepkit": "^0.
|
|
66
|
-
"@gjsify/rolldown-plugin-pnp": "^0.
|
|
67
|
-
"@gjsify/vite-plugin-blueprint": "^0.
|
|
63
|
+
"@gjsify/console": "^0.50.0",
|
|
64
|
+
"@gjsify/resolve-npm": "^0.50.0",
|
|
65
|
+
"@gjsify/rolldown-plugin-deepkit": "^0.50.0",
|
|
66
|
+
"@gjsify/rolldown-plugin-pnp": "^0.50.0",
|
|
67
|
+
"@gjsify/vite-plugin-blueprint": "^0.50.0",
|
|
68
68
|
"@rollup/pluginutils": "^5.4.0",
|
|
69
69
|
"acorn": "^8.17.0",
|
|
70
70
|
"acorn-typescript": "^1.4.13",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"sass": "^1.101.0"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
|
-
"@gjsify/lightningcss-native": "^0.
|
|
77
|
+
"@gjsify/lightningcss-native": "^0.50.0",
|
|
78
78
|
"rolldown": "^1.1.4"
|
|
79
79
|
},
|
|
80
80
|
"peerDependenciesMeta": {
|
|
@@ -86,8 +86,8 @@
|
|
|
86
86
|
}
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
|
-
"@gjsify/cli": "^0.
|
|
90
|
-
"@gjsify/unit": "^0.
|
|
89
|
+
"@gjsify/cli": "^0.50.0",
|
|
90
|
+
"@gjsify/unit": "^0.50.0",
|
|
91
91
|
"@types/node": "^25.9.2",
|
|
92
92
|
"rolldown": "^1.1.4",
|
|
93
93
|
"typescript": "^6.0.3"
|