@gjsify/rolldown-plugin-gjsify 0.36.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/app/gjs.d.ts +28 -54
- package/lib/app/gjs.js +124 -211
- package/lib/app/node.d.ts +31 -41
- package/lib/app/node.js +118 -221
- package/lib/plugins/css-as-string.js +10 -2
- package/lib/plugins/napi-node-addon.d.ts +46 -83
- package/lib/plugins/napi-node-addon.js +188 -308
- package/lib/plugins/process-stub.js +27 -1
- package/lib/plugins/rewrite-node-modules-paths.d.ts +12 -36
- package/lib/plugins/rewrite-node-modules-paths.js +60 -144
- package/lib/plugins/unresolved-workspace-import.d.ts +20 -38
- package/lib/plugins/unresolved-workspace-import.js +65 -157
- package/lib/shims/console-gjs.js +11 -19
- package/lib/utils/auto-globals.d.ts +56 -102
- package/lib/utils/auto-globals.js +100 -171
- package/lib/utils/detect-free-globals.d.ts +27 -46
- package/lib/utils/detect-free-globals.js +106 -185
- package/package.json +7 -7
|
@@ -1,85 +1,24 @@
|
|
|
1
|
-
// Resolver-layer guard: an unresolvable `@gjsify/*` edge is a build ERROR,
|
|
2
|
-
//
|
|
1
|
+
// Resolver-layer guard: an unresolvable `@gjsify/*` edge is a build ERROR, never a
|
|
2
|
+
// silent external. Full rationale, the measured incident and the legitimate-vs-broken
|
|
3
|
+
// line: ../../AGENTS.md § "Unresolvable `@gjsify/*` edge".
|
|
3
4
|
//
|
|
4
|
-
//
|
|
5
|
+
// Why this sits UPSTREAM of `utils/gjs-bundle-guard.ts` (which refuses to EMIT a
|
|
6
|
+
// `--app gjs` bundle still importing a bare `node:` specifier, and stays as the last
|
|
7
|
+
// line of defence). That guard is:
|
|
8
|
+
// - gjs-only — the same silent externalisation happens on `--app node`/`browser`,
|
|
9
|
+
// where a surviving bare `@gjsify/foo` is equally unloadable and no
|
|
10
|
+
// `node:`-shaped symptom exists to detect;
|
|
11
|
+
// - symptom-shaped — it names `node:fs`, not the `@gjsify/fs` edge that failed,
|
|
12
|
+
// and cannot name the importer;
|
|
13
|
+
// - post-tree-shake — an unresolved edge that gets shaken out leaves no trace, so
|
|
14
|
+
// the artefact passes while the build is still wrong.
|
|
5
15
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// import { readFileSync } from 'node:fs'
|
|
14
|
-
// → aliasPlugin rewrites to `@gjsify/fs`
|
|
15
|
-
// → `@gjsify/fs` cannot be resolved (no `node_modules`, or the workspace
|
|
16
|
-
// package has no built `lib/` so its `exports["."]` points at a file
|
|
17
|
-
// that does not exist)
|
|
18
|
-
// → aliasPlugin falls through, nothing else claims it
|
|
19
|
-
// → `node:fs` is externalised, the build "succeeds"
|
|
20
|
-
//
|
|
21
|
-
// Measured on `packages/infra/cli/dist/cli.gjs.mjs` (repo task #63): 1 bare
|
|
22
|
-
// `node:` import on `origin/main` vs 11 from a worktree with no `node_modules`
|
|
23
|
-
// — a bundle stock GJS aborts on before one line runs
|
|
24
|
-
// (`ImportError: Unsupported URI scheme for importing: node`). The pre-commit
|
|
25
|
-
// hook then rebuilt and AUTO-STAGED it, and nothing local objected: tsc, lint,
|
|
26
|
-
// format, unit and e2e all run the Node entry, never the emitted GJS bundle.
|
|
27
|
-
//
|
|
28
|
-
// ## Why this is upstream of the `--app gjs` `node:` guard (PR #828)
|
|
29
|
-
//
|
|
30
|
-
// `utils/gjs-bundle-guard.ts` reads the finished module graph and refuses to
|
|
31
|
-
// EMIT a `--app gjs` bundle that still statically imports a bare `node:`
|
|
32
|
-
// specifier. That is the right last line of defence, and it stays. But it is:
|
|
33
|
-
//
|
|
34
|
-
// - **gjs-only** — the same silent externalisation happens on `--app node`
|
|
35
|
-
// and `--app browser`, where a surviving bare `@gjsify/foo` is equally
|
|
36
|
-
// unloadable and no `node:`-shaped symptom exists to detect;
|
|
37
|
-
// - **symptom-shaped** — it names `node:fs`, not the `@gjsify/fs` edge that
|
|
38
|
-
// actually failed, and cannot name the importer;
|
|
39
|
-
// - **post-tree-shake** — an unresolved edge that gets shaken out leaves no
|
|
40
|
-
// trace at all, so the artefact passes while the build is still wrong.
|
|
41
|
-
//
|
|
42
|
-
// This guard fires at RESOLUTION time instead: it names the specifier, the
|
|
43
|
-
// substitution target, the importer, and the likely cause.
|
|
44
|
-
//
|
|
45
|
-
// ## The legitimate-vs-broken line
|
|
46
|
-
//
|
|
47
|
-
// A blanket "fail on any external" would break every real build — externals are
|
|
48
|
-
// load-bearing on every target (`gi://*`, `cairo`/`system`/`gettext` on gjs; the
|
|
49
|
-
// whole `EXTERNALS_NODE` set plus `@gjsify/node-gi*` and `node-datachannel` on
|
|
50
|
-
// node; whatever the user passed to `--external`). Those never reach the
|
|
51
|
-
// unresolved fallback by accident — a `resolveId` hook or the `external` option
|
|
52
|
-
// CLAIMS them, deliberately. So the distinction drawn here is:
|
|
53
|
-
//
|
|
54
|
-
// DECLARED external → legitimate, ignored. Asked via the target's own
|
|
55
|
-
// externals predicate (`isExternal`), i.e. exactly the
|
|
56
|
-
// policy `externalsPlugin` enforces, so the guard and the
|
|
57
|
-
// policy cannot drift.
|
|
58
|
-
// PROMISED, missing → broken. Either (a) the specifier has an entry in the
|
|
59
|
-
// target's substitution table and that target does not
|
|
60
|
-
// resolve, or (b) the specifier IS a bare `@gjsify/*`
|
|
61
|
-
// workspace package that does not resolve.
|
|
62
|
-
// anything else → not our business. A missing `lodash` is a plain
|
|
63
|
-
// dependency bug that `UNRESOLVED_IMPORT` already
|
|
64
|
-
// reports; widening to every bare specifier would turn a
|
|
65
|
-
// genuinely optional peer into a hard build failure, and
|
|
66
|
-
// this guard's whole claim is that the `@gjsify/*` edge
|
|
67
|
-
// is NOT optional — the target cannot run without it.
|
|
68
|
-
//
|
|
69
|
-
// On (a): the check is keyed on the ALIAS TARGET being a `@gjsify/*` package,
|
|
70
|
-
// not merely on the entry existing. Every value in the curated + derived tables
|
|
71
|
-
// is a `@gjsify/*` specifier today, so this is a distinction without a
|
|
72
|
-
// difference for them — but it keeps a user `--alias foo=./some/local.js` (or a
|
|
73
|
-
// shim path like `random-access-file/index.js`) on the old fall-through
|
|
74
|
-
// behaviour instead of promoting a user's own mapping mistake to a fatal error
|
|
75
|
-
// in a plugin they did not ask for.
|
|
76
|
-
//
|
|
77
|
-
// ## Not registered for `--library`
|
|
78
|
-
//
|
|
79
|
-
// Library mode PRESERVES imports on purpose (`preserveModules`, bare specifiers
|
|
80
|
-
// kept so the consumer resolves them). An unresolved `@gjsify/*` there is the
|
|
81
|
-
// contract, not a bug, so `library/lib.ts` deliberately does not compose this
|
|
82
|
-
// plugin.
|
|
16
|
+
// Case (a) of the fatal line keys on the ALIAS TARGET being a `@gjsify/*` package,
|
|
17
|
+
// not merely on the entry existing. Every value in the curated + derived tables is
|
|
18
|
+
// `@gjsify/*` today, so it makes no difference for them — but it keeps a user
|
|
19
|
+
// `--alias foo=./some/local.js` (or a shim path like `random-access-file/index.js`)
|
|
20
|
+
// on the old fall-through instead of promoting a user's own mapping mistake to a
|
|
21
|
+
// fatal error in a plugin they did not ask for.
|
|
83
22
|
import { dirname } from 'node:path';
|
|
84
23
|
/** Bare `@gjsify/*` package specifier — the workspace edge this guard protects. */
|
|
85
24
|
const WORKSPACE_SCOPE = '@gjsify/';
|
|
@@ -90,41 +29,33 @@ const WORKSPACE_SCOPE = '@gjsify/';
|
|
|
90
29
|
*/
|
|
91
30
|
const MAX_REPORTED_ALIAS_SOURCES = 4;
|
|
92
31
|
/**
|
|
93
|
-
* Is `id` a bare `@gjsify/*` package specifier (root or subpath)?
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* `/…/node_modules/@gjsify/fs/lib/esm/index.js` is already resolved, and a
|
|
97
|
-
* `\0gjsify-*` virtual id belongs to the plugin that emitted it.
|
|
32
|
+
* Is `id` a bare `@gjsify/*` package specifier (root or subpath)? Excludes disk
|
|
33
|
+
* paths and synthetic ids: a resolved `…/node_modules/@gjsify/fs/…` is already
|
|
34
|
+
* resolved, and a `\0gjsify-*` virtual id belongs to the plugin that emitted it.
|
|
98
35
|
*/
|
|
99
36
|
export function isWorkspaceSpecifier(id) {
|
|
100
37
|
if (!id.startsWith(WORKSPACE_SCOPE))
|
|
101
38
|
return false;
|
|
102
39
|
if (id.includes('\0'))
|
|
103
40
|
return false;
|
|
104
|
-
// `@gjsify/fs
|
|
105
|
-
// `@gjsify//x` do not (no package name). A resolved disk path can never
|
|
106
|
-
// reach here — it is absolute or `./`-relative, so it never starts with the
|
|
107
|
-
// scope in the first place.
|
|
41
|
+
// `@gjsify/fs/register/x` qualifies; `@gjsify/` and `@gjsify//x` do not.
|
|
108
42
|
const rest = id.slice(WORKSPACE_SCOPE.length);
|
|
109
43
|
return rest.length > 0 && !rest.startsWith('/');
|
|
110
44
|
}
|
|
111
45
|
/**
|
|
112
|
-
* Pure decision: does this import edge have to resolve, and to what?
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* without a bundler (`packages/infra/cli/src/unresolved-workspace-import.spec.ts`).
|
|
46
|
+
* Pure decision: does this import edge have to resolve, and to what? Split out from
|
|
47
|
+
* the plugin so the legitimate-vs-broken line is unit-testable without a bundler
|
|
48
|
+
* (`packages/infra/cli/src/unresolved-workspace-import.spec.ts`).
|
|
116
49
|
*/
|
|
117
50
|
export function classifyImport(input) {
|
|
118
51
|
const { source, importer, aliases, isExternal } = input;
|
|
119
|
-
// Entry modules are files the caller named
|
|
120
|
-
//
|
|
52
|
+
// Entry modules are files the caller named — resolved by the orchestrator, never
|
|
53
|
+
// externalised.
|
|
121
54
|
if (importer === undefined)
|
|
122
55
|
return { verdict: 'ignore', reason: 'entry' };
|
|
123
|
-
// Relative / absolute / synthetic ids are not workspace edges.
|
|
124
56
|
if (source.startsWith('.') || source.startsWith('/') || source.startsWith('\0')) {
|
|
125
57
|
return { verdict: 'ignore', reason: 'not-bare' };
|
|
126
58
|
}
|
|
127
|
-
// A specifier the target DECLARES external is a deliberate external.
|
|
128
59
|
if (isExternal?.(source))
|
|
129
60
|
return { verdict: 'ignore', reason: 'declared-external' };
|
|
130
61
|
const aliasTarget = aliases?.[source];
|
|
@@ -148,22 +79,17 @@ export class UnresolvedWorkspaceImportError extends Error {
|
|
|
148
79
|
}
|
|
149
80
|
}
|
|
150
81
|
/**
|
|
151
|
-
* The actionable message
|
|
152
|
-
*
|
|
153
|
-
* cannot tell you.
|
|
82
|
+
* The actionable message: names the specifier, the importer and the likely cause —
|
|
83
|
+
* the three things `UNRESOLVED_IMPORT` and the post-hoc `node:` guard cannot.
|
|
154
84
|
*/
|
|
155
85
|
export function formatUnresolvedWorkspaceImport(details) {
|
|
156
86
|
const { target, source, candidate, aliasTarget, aliasedFrom, importer } = details;
|
|
157
|
-
// The specifiers `candidate`
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
// which one this importer wrote, while `@gjsify/fs` inverts to exactly
|
|
164
|
-
// `['fs','node:fs']` (the two spellings of one builtin), which is the
|
|
165
|
-
// information the reader needs. Above the threshold, fall back to naming
|
|
166
|
-
// the package alone rather than guessing.
|
|
87
|
+
// The specifiers `candidate` stood in for: `source` when IT carried the table
|
|
88
|
+
// entry, else the reverse lookup — but only when its fan-in is SMALL. A shared
|
|
89
|
+
// sink inverts to a wall of text (`@gjsify/empty` is the browser target for 49
|
|
90
|
+
// specifiers) that says nothing about which one this importer wrote, while
|
|
91
|
+
// `@gjsify/fs` inverts to exactly the two spellings of one builtin. Above the
|
|
92
|
+
// threshold, name the package alone rather than guess.
|
|
167
93
|
const reversed = aliasedFrom ?? [];
|
|
168
94
|
const substitutedFor = aliasTarget === undefined ? (reversed.length <= MAX_REPORTED_ALIAS_SOURCES ? reversed : []) : [source];
|
|
169
95
|
const quoted = substitutedFor.map((s) => `\`${s}\``).join(', ');
|
|
@@ -172,10 +98,9 @@ export function formatUnresolvedWorkspaceImport(details) {
|
|
|
172
98
|
: `gjsify build --app ${target}: cannot resolve \`${candidate}\`, the \`--app ${target}\` ` +
|
|
173
99
|
`substitution for ${quoted}.`;
|
|
174
100
|
// The symptom is a property of the SPECIFIER that would survive, not of the
|
|
175
|
-
// target: only a `node:` specifier reaching stock GJS
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
// `exports` maps). Naming the wrong one sends the reader hunting the wrong bug.
|
|
101
|
+
// target: only a `node:` specifier reaching stock GJS gives the URI-scheme abort,
|
|
102
|
+
// while a bare `@gjsify/*` gives `Module not found`. Naming the wrong one sends
|
|
103
|
+
// the reader hunting the wrong bug.
|
|
179
104
|
const surviving = substitutedFor.length === 0 ? [source] : substitutedFor;
|
|
180
105
|
const symptom = target !== 'gjs'
|
|
181
106
|
? ''
|
|
@@ -202,12 +127,7 @@ export function formatUnresolvedWorkspaceImport(details) {
|
|
|
202
127
|
` - \`${candidate}\` is genuinely not a dependency of this project — add it.`,
|
|
203
128
|
].join('\n');
|
|
204
129
|
}
|
|
205
|
-
/**
|
|
206
|
-
* Reverse the substitution table: alias TARGET → the specifiers that route to it.
|
|
207
|
-
*
|
|
208
|
-
* Built once per plugin instance (the tables run to ~150 entries per target, and
|
|
209
|
-
* the guard only consults this on the failure path).
|
|
210
|
-
*/
|
|
130
|
+
/** Reverse the substitution table: alias TARGET → the specifiers that route to it. */
|
|
211
131
|
export function buildReverseAliasIndex(aliases) {
|
|
212
132
|
const index = new Map();
|
|
213
133
|
if (!aliases)
|
|
@@ -226,54 +146,42 @@ export function buildReverseAliasIndex(aliases) {
|
|
|
226
146
|
/**
|
|
227
147
|
* Refuse a build whose `@gjsify/*` substitution could not be resolved.
|
|
228
148
|
*
|
|
229
|
-
* `order: 'post'` so every deliberate claim
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
* The hook re-runs the full resolution chain via `this.resolve(…, { skipSelf })`
|
|
236
|
-
* — which is what Rolldown's builtin resolver would have done next — and returns
|
|
237
|
-
* its result when it succeeds, so a healthy build resolves the specifier exactly
|
|
238
|
-
* once and behaves identically. Only a genuine `null` throws.
|
|
149
|
+
* `order: 'post'` so every deliberate claim (alias/napi/gi-node at `pre`,
|
|
150
|
+
* imports-empty/externals at normal) gets first refusal — only ids NOTHING claimed
|
|
151
|
+
* reach this hook. It then re-runs the resolution Rolldown would have run next
|
|
152
|
+
* (`this.resolve(…, { skipSelf })`) and returns that, so a healthy build resolves
|
|
153
|
+
* each specifier exactly once and behaves identically. Only a genuine `null` throws.
|
|
239
154
|
*/
|
|
240
155
|
export function unresolvedWorkspaceImportPlugin(options) {
|
|
241
156
|
const { target, aliases, isExternal } = options;
|
|
242
|
-
// Re-entrancy belt: `skipSelf: true`
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
// explicitly. Keyed by `candidate\0importer` — the same package legitimately
|
|
246
|
-
// resolves from many importers.
|
|
157
|
+
// Re-entrancy belt: `skipSelf: true` should keep our own `this.resolve` out of
|
|
158
|
+
// this hook, but a hang is a far worse failure than a missed check. Keyed by
|
|
159
|
+
// `candidate\0importer` — the same package legitimately resolves from many.
|
|
247
160
|
const inFlight = new Set();
|
|
248
|
-
//
|
|
249
|
-
//
|
|
161
|
+
// Read only on the failure path, to recover the `node:fs` behind a failed
|
|
162
|
+
// `@gjsify/fs`.
|
|
250
163
|
const reverseAliases = buildReverseAliasIndex(aliases);
|
|
251
|
-
// Successful resolutions
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
// Only POSITIVE results are cached: a failure throws, so it is never hot,
|
|
259
|
-
// and caching it could outlive a package appearing mid-watch.
|
|
164
|
+
// Successful resolutions memoized per (candidate, importer DIRECTORY) — the pair
|
|
165
|
+
// node resolution actually depends on, so a hit is the answer the chain would have
|
|
166
|
+
// recomputed. Without it this hook was the most expensive plugin in a CLI-bundle
|
|
167
|
+
// build (32% of plugin time): `@gjsify/*` edges repeat across hundreds of modules
|
|
168
|
+
// and each miss re-runs the whole `pre`-order chain. POSITIVE results only — a
|
|
169
|
+
// failure throws, so it is never hot, and caching it could outlive a package
|
|
170
|
+
// appearing mid-watch.
|
|
260
171
|
const resolvedCache = new Map();
|
|
261
172
|
return {
|
|
262
173
|
name: 'gjsify-unresolved-workspace-import',
|
|
263
174
|
resolveId: {
|
|
264
175
|
order: 'post',
|
|
265
176
|
async handler(source, rawImporter, extraOptions) {
|
|
266
|
-
// The two engines disagree on "no importer": npm `rolldown`
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
// `
|
|
270
|
-
// "
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
// explicitly. Normalise once, at the boundary — same fix as
|
|
275
|
-
// `napiNodeAddonPlugin` (#840), whose `dirname(null)` threw
|
|
276
|
-
// and took the whole GJS build down as an UNHANDLEABLE_ERROR.
|
|
177
|
+
// The two engines disagree on "no importer": npm `rolldown` passes
|
|
178
|
+
// `undefined`, `@gjsify/rolldown-native` passes `null` (its hook
|
|
179
|
+
// payload round-trips through JSON, which has no `undefined`).
|
|
180
|
+
// `classifyImport` reads an absent importer as "ENTRY, never
|
|
181
|
+
// externalised" via `=== undefined`, which `null` fails — so under
|
|
182
|
+
// the GJS bundler an entry module got treated as a normal edge and
|
|
183
|
+
// could throw this plugin's error for a file the caller named.
|
|
184
|
+
// Normalise once, at the boundary (same fix as `napiNodeAddonPlugin`).
|
|
277
185
|
const importer = typeof rawImporter === 'string' ? rawImporter : undefined;
|
|
278
186
|
if (extraOptions?.isEntry)
|
|
279
187
|
return null;
|
package/lib/shims/console-gjs.js
CHANGED
|
@@ -1,27 +1,19 @@
|
|
|
1
1
|
// GJS console shim — bundled into GJS user builds via Rolldown's `inject`.
|
|
2
|
-
//
|
|
3
|
-
// `Gjs-Console-Message:` prefix, ANSI escapes
|
|
4
|
-
// stdout/stderr instead of GLib's logging stream.
|
|
2
|
+
// Routes through print()/printerr() rather than GLib.log_structured(): no
|
|
3
|
+
// `Gjs-Console-Message:` prefix, ANSI escapes survive, output on stdout/stderr.
|
|
5
4
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// `@gjsify/console` lib to be pre-built (build-order coupling).
|
|
5
|
+
// `globalThis.console` is non-writable AND non-configurable in GJS — re-measured
|
|
6
|
+
// on 1.88/SM 140, so it is a property of the runtime and not of one version — which
|
|
7
|
+
// is why nothing here assigns to it. Rolldown's `inject` rewrites bare `console`
|
|
8
|
+
// references to a named import from this shim instead.
|
|
11
9
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// `
|
|
16
|
-
//
|
|
10
|
+
// The `@gjsify/console` specifier is followed at USER-build time by Rolldown, not by
|
|
11
|
+
// tsc here; resolving it here would make this package depend on that one being built
|
|
12
|
+
// first. Hence the namespace import: a single statement no formatter can wrap, so the
|
|
13
|
+
// `@ts-ignore` stays on the line directly above it. A multi-line named import would
|
|
14
|
+
// let oxfmt detach the suppression from the `from '@gjsify/console'` line.
|
|
17
15
|
// @ts-ignore — resolved by Rolldown at user-build time, not by tsc here.
|
|
18
16
|
import * as gjsConsole from '@gjsify/console';
|
|
19
|
-
// NOTE: a namespace import is used deliberately. It is a single statement that
|
|
20
|
-
// no formatter (oxfmt) can wrap across lines, so the `@ts-ignore` above always
|
|
21
|
-
// sits on the line immediately preceding the import and reliably suppresses the
|
|
22
|
-
// TS2307 (`@gjsify/console` is resolved by Rolldown at user-build time, not by
|
|
23
|
-
// tsc here). A multi-line named import would let the reformatter detach the
|
|
24
|
-
// suppression from the offending `from '@gjsify/console'` line.
|
|
25
17
|
export const console = {
|
|
26
18
|
log: gjsConsole.log,
|
|
27
19
|
info: gjsConsole.info,
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import type { InputOptions, RolldownPluginOption, TransformOptions } from 'rolldown';
|
|
2
2
|
/**
|
|
3
|
-
* In-memory bundle function
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* The default impl below dynamically imports npm rolldown; the CLI
|
|
10
|
-
* overrides this from `actions/build.ts` to route via the same engine the
|
|
11
|
-
* final build uses.
|
|
3
|
+
* In-memory bundle function returning the per-entry chunk code strings, injected so
|
|
4
|
+
* auto-globals runs under either engine: npm rolldown cannot load under GJS (its Rust
|
|
5
|
+
* prebuild's init code calls `require('node:fs')` synchronously). The default impl
|
|
6
|
+
* dynamically imports npm rolldown; the CLI overrides it from `actions/build.ts` to
|
|
7
|
+
* route via the same engine the final build uses.
|
|
12
8
|
*/
|
|
13
9
|
export type AnalysisBundler = (input: {
|
|
14
10
|
rolldownInput: InputOptions;
|
|
@@ -16,101 +12,73 @@ export type AnalysisBundler = (input: {
|
|
|
16
12
|
}) => Promise<string[]>;
|
|
17
13
|
import type { PluginOptions } from '../types/plugin-options.js';
|
|
18
14
|
export interface AutoGlobalsResult {
|
|
19
|
-
/** Global identifiers detected in the bundle */
|
|
20
15
|
detected: Set<string>;
|
|
21
|
-
/** Path to the generated inject stub
|
|
16
|
+
/** Path to the generated inject stub; undefined when no globals are needed. */
|
|
22
17
|
injectPath: string | undefined;
|
|
23
18
|
}
|
|
24
19
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* `--globals auto` injecting a GI-backed register makes the bundle
|
|
30
|
-
* hard-require a GTK/GNOME runtime at load — under a GTK-less host (headless
|
|
31
|
-
* type-check container, SSR generator, plain CI) it throws `Typelib … not
|
|
32
|
-
* found`. The note names which globals triggered the injection so that
|
|
33
|
-
* otherwise-silent runtime crash is traceable at build time.
|
|
20
|
+
* The build-time note for any GI-backed register modules in a final inject set, or
|
|
21
|
+
* null when none are. Pure so it is unit-testable; `emitGiBackedDiagnostic` is the
|
|
22
|
+
* side-effecting wrapper.
|
|
34
23
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
24
|
+
* Injecting a GI-backed register makes the bundle hard-require a GTK/GNOME runtime at
|
|
25
|
+
* load, so under a GTK-less host (headless type-check container, SSR generator, plain
|
|
26
|
+
* CI) it throws `Typelib … not found`. Naming the triggering globals makes that
|
|
27
|
+
* otherwise-silent runtime crash traceable at build time.
|
|
37
28
|
*/
|
|
38
29
|
export declare function describeGiBackedInjection(registerPaths: Set<string>, detected: Set<string>): string | null;
|
|
39
30
|
export interface DetectAutoGlobalsOptions {
|
|
40
31
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* indirection (e.g. Excalibur's `BrowserComponent.nativeComponent.matchMedia`).
|
|
32
|
+
* Identifiers or group aliases (`dom`/`web`/`node`) always injected on top of what
|
|
33
|
+
* detection finds — `--globals auto,<extras>`, for globals reached through
|
|
34
|
+
* indirection the detector cannot follow statically (Excalibur's
|
|
35
|
+
* `BrowserComponent.nativeComponent.matchMedia`).
|
|
46
36
|
*/
|
|
47
37
|
extraGlobalsList?: string;
|
|
48
38
|
/**
|
|
49
|
-
* Identifiers to remove
|
|
50
|
-
*
|
|
51
|
-
* dead browser-compat code in npm dependencies whose polyfills require
|
|
52
|
-
* unavailable native libraries.
|
|
39
|
+
* Identifiers to remove before writing the inject stub — for false positives from
|
|
40
|
+
* dead browser-compat code in deps whose polyfills need unavailable native libs.
|
|
53
41
|
*/
|
|
54
42
|
excludeGlobals?: string[];
|
|
55
43
|
/**
|
|
56
|
-
* Skip the
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* every identifier the OLD entry contained — removed globals could never
|
|
61
|
-
* leave the map (a ratchet, with transitive pollution into sibling
|
|
44
|
+
* Skip the closure-map expansion and run the pure iterative loop. Only the
|
|
45
|
+
* closure-map GENERATOR sets this: computing a register's closure THROUGH the
|
|
46
|
+
* committed map would re-add every identifier the OLD entry contained, so removed
|
|
47
|
+
* globals could never leave it (a ratchet, with transitive pollution into sibling
|
|
62
48
|
* entries) and `--check` would stay green on semantically stale data.
|
|
63
|
-
* Builds never set this; the expansion is exactly what makes them
|
|
64
|
-
* converge after one detection pass.
|
|
65
49
|
*/
|
|
66
50
|
disableClosureExpansion?: boolean;
|
|
67
51
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* When omitted the resolvability gate is BYPASSED (every detected
|
|
73
|
-
* register path is written as-is) to preserve the original behaviour
|
|
74
|
-
* for callers that don't know the project root. The real CLI build
|
|
75
|
-
* (`actions/build.ts`) passes `process.cwd()` so the gate is active in
|
|
76
|
-
* a normal `gjsify build`.
|
|
52
|
+
* Project root, used to locate `node_modules` when checking whether a detected
|
|
53
|
+
* global's polyfill package is installed. Omitting it BYPASSES that resolvability
|
|
54
|
+
* gate (every detected register path is written as-is); the real CLI build passes
|
|
55
|
+
* `process.cwd()`.
|
|
77
56
|
*/
|
|
78
57
|
cwd?: string;
|
|
79
58
|
}
|
|
80
59
|
/**
|
|
81
|
-
* Build options
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* The caller passes the same input + output options it would use for the
|
|
85
|
-
* final build (input, plugins, external, define, …). We strip output-side
|
|
86
|
-
* fields that would force a write-to-disk and replace them with in-memory
|
|
87
|
-
* settings.
|
|
60
|
+
* Build options for the analyser: the same input options the caller would use for the
|
|
61
|
+
* final build, with the output-side fields that force a write-to-disk stripped and
|
|
62
|
+
* replaced by in-memory settings.
|
|
88
63
|
*/
|
|
89
64
|
export interface AnalysisOptions {
|
|
90
65
|
input: InputOptions['input'];
|
|
91
66
|
plugins?: RolldownPluginOption[];
|
|
92
67
|
external?: InputOptions['external'];
|
|
93
68
|
resolve?: InputOptions['resolve'];
|
|
94
|
-
/**
|
|
95
|
-
* Pass-through to Rolldown's `transform` (Oxc-driven) — `define`,
|
|
96
|
-
* `dropLabels`, `treeShake`, etc. live here in Rolldown's shape.
|
|
97
|
-
*/
|
|
69
|
+
/** Pass-through to Rolldown's Oxc-driven `transform` (`define`, `treeShake`, …). */
|
|
98
70
|
transform?: TransformOptions;
|
|
99
|
-
/**
|
|
71
|
+
/** Use 'esm' to match the final build. */
|
|
100
72
|
format?: 'esm' | 'cjs' | 'iife';
|
|
101
73
|
}
|
|
102
74
|
/**
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* `mainFields` / `external` / `treeshake` settings for the in-memory
|
|
111
|
-
* analysis bundle — without that, native-rolldown and npm-rolldown default
|
|
112
|
-
* to different module-resolution conditions and the bundled output (and
|
|
113
|
-
* therefore the detected free-global set) diverges between engines.
|
|
75
|
+
* Supplies the gjsify plugin to the analyser; passed in by the caller to break the
|
|
76
|
+
* cyclic dep between this file and `../plugin.ts`. Returns either the plugin array
|
|
77
|
+
* alone or the full orchestrator config `{ options, plugins }` — the latter lets the
|
|
78
|
+
* analysis bundle reuse the orchestrator's `resolve.conditionNames`/`mainFields`/
|
|
79
|
+
* `external`/`treeshake`, without which native-rolldown and npm-rolldown default to
|
|
80
|
+
* different module-resolution conditions and the detected free-global set diverges
|
|
81
|
+
* between engines.
|
|
114
82
|
*/
|
|
115
83
|
type GjsifyFactoryReturn = RolldownPluginOption | {
|
|
116
84
|
options: InputOptions;
|
|
@@ -118,21 +86,10 @@ type GjsifyFactoryReturn = RolldownPluginOption | {
|
|
|
118
86
|
};
|
|
119
87
|
type GjsifyPluginFactory = (options: PluginOptions) => GjsifyFactoryReturn | Promise<GjsifyFactoryReturn>;
|
|
120
88
|
/**
|
|
121
|
-
* Run
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* Returns the inject stub path that the caller should pass to the
|
|
126
|
-
* final (real) build via `pluginOptions.autoGlobalsInject`.
|
|
127
|
-
*
|
|
128
|
-
* @param analysisOptions Rolldown input options for the in-memory build.
|
|
129
|
-
* @param pluginOptions Gjsify plugin options (without `autoGlobalsInject`,
|
|
130
|
-
* which this function computes).
|
|
131
|
-
* @param gjsifyPluginFactory Factory returning the gjsify plugin instance
|
|
132
|
-
* for a given set of plugin options. Provided by the caller to avoid a
|
|
133
|
-
* cyclic import between this module and `../plugin.ts`.
|
|
134
|
-
* @param verbose Emit per-iteration debug output to console.
|
|
135
|
-
* @param options Optional `extraGlobalsList` / `excludeGlobals`.
|
|
89
|
+
* Run the iterative in-memory build with acorn-based global detection, each pass
|
|
90
|
+
* seeded by the previous pass's globals, until the detected set is stable. Returns the
|
|
91
|
+
* inject stub path the caller passes to the final build via
|
|
92
|
+
* `pluginOptions.autoGlobalsInject`.
|
|
136
93
|
*/
|
|
137
94
|
export declare function detectAutoGlobals(analysisOptions: AnalysisOptions, pluginOptions: Omit<PluginOptions, 'autoGlobalsInject'>, gjsifyPluginFactory: GjsifyPluginFactory, verbose?: boolean, options?: DetectAutoGlobalsOptions, bundler?: AnalysisBundler): Promise<AutoGlobalsResult>;
|
|
138
95
|
/**
|
|
@@ -145,24 +102,21 @@ export declare function detectAutoGlobals(analysisOptions: AnalysisOptions, plug
|
|
|
145
102
|
* 1. a GJS ambient global (`print`/`printerr`/`log`/`logError`/`ARGV`/
|
|
146
103
|
* `imports`) referenced as a BARE identifier — the legacy GJS-source shape
|
|
147
104
|
* `@gjsify/node-gi/globals` seeds (`detectGjsAmbientGlobals`);
|
|
148
|
-
* 2. a STATIC import of `@gjsify/node-gi` (root or subpath) — the shape the
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
* shim dropped (the `gjsify storybook --runtime node` no-`requireGi`
|
|
156
|
-
* regression).
|
|
105
|
+
* 2. a STATIC import of `@gjsify/node-gi` (root or subpath) — the shape the bare GJS
|
|
106
|
+
* built-ins are externalised to, i.e. the PORTABLE GJS-source spelling
|
|
107
|
+
* (`detectNodeGiModuleImports`). A bundle retaining one is already bound to the
|
|
108
|
+
* bridge at load, so flipping injection cannot cost plain-Node loadability, while
|
|
109
|
+
* a portable GJS app with no ambient global at all (`system.programArgs` instead
|
|
110
|
+
* of `ARGV`) would otherwise get its `@girs/*` bodies emptied and its globals shim
|
|
111
|
+
* dropped (the `gjsify storybook --runtime node` no-`requireGi` regression).
|
|
157
112
|
*
|
|
158
|
-
* A SINGLE pass suffices
|
|
159
|
-
*
|
|
160
|
-
*
|
|
113
|
+
* A SINGLE pass suffices, unlike the GJS register-injection loop: the shim is EXTERNAL,
|
|
114
|
+
* so injecting it adds no bundled code that could reference more globals — no
|
|
115
|
+
* tree-shaking dependency cycle to converge.
|
|
161
116
|
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* crashes on plain Node without node-gi. See `detectGjsAmbientGlobals`.
|
|
117
|
+
* Detection is on the bundled output AFTER tree-shaking, so a `print`/`imports`
|
|
118
|
+
* reference behind statically-dead code does not trigger injection — importing the shim
|
|
119
|
+
* eagerly loads the native node-gi addon, which crashes on plain Node without node-gi.
|
|
166
120
|
*/
|
|
167
121
|
export declare function detectNodeGiGlobals(analysisOptions: AnalysisOptions, pluginOptions: Omit<PluginOptions, 'nodeGiGlobalsInject'>, gjsifyPluginFactory: GjsifyPluginFactory, bundler?: AnalysisBundler): Promise<boolean>;
|
|
168
122
|
export {};
|