@gjsify/rolldown-plugin-gjsify 0.54.0 → 0.55.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 +6 -0
- package/lib/app/gjs.js +23 -5
- package/lib/app/nativescript.js +6 -0
- package/lib/app/node.js +8 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +1 -0
- package/lib/plugins/implicit-global-assign.d.ts +26 -0
- package/lib/plugins/implicit-global-assign.js +350 -0
- package/lib/types/plugin-options.d.ts +15 -0
- package/lib/utils/auto-globals.js +1 -0
- package/lib/utils/entry-wrapper.d.ts +1 -0
- package/lib/utils/entry-wrapper.js +20 -3
- package/package.json +10 -10
package/lib/app/browser.js
CHANGED
|
@@ -30,6 +30,7 @@ import { platformResolvePlugin, browserSuffixChain, BROWSER_REFUSED_SUFFIXES } f
|
|
|
30
30
|
import { giRendererPlugin } from '../plugins/gi-renderer.js';
|
|
31
31
|
import { cssAsStringPlugin } from '../plugins/css-as-string.js';
|
|
32
32
|
import { unresolvedWorkspaceImportPlugin } from '../plugins/unresolved-workspace-import.js';
|
|
33
|
+
import { implicitGlobalAssignPlugin } from '../plugins/implicit-global-assign.js';
|
|
33
34
|
export const setupForBrowser = async (input) => {
|
|
34
35
|
const userExternal = input.userExternal ?? [];
|
|
35
36
|
const external = [...userExternal];
|
|
@@ -135,6 +136,11 @@ export const setupForBrowser = async (input) => {
|
|
|
135
136
|
// GJS app's sources must not start failing on an import that used to resolve — and the
|
|
136
137
|
// exit a browser can actually use is now beside it.
|
|
137
138
|
blueprintPlugin(),
|
|
139
|
+
// ADR 0079's addendum. The `window` define above replaces an assignment TARGET as
|
|
140
|
+
// readily as a read, so `window = {…}` would emit `globalThis = {…}` — code that
|
|
141
|
+
// replaces the whole global object. This transform runs first and leaves the define
|
|
142
|
+
// nothing to rewrite; the guarded branch stays unreachable on a page either way.
|
|
143
|
+
implicitGlobalAssignPlugin(),
|
|
138
144
|
cssAsStringPlugin(),
|
|
139
145
|
// `order: 'post'` — see app/gjs.ts. The browser target's whole job is to
|
|
140
146
|
// replace Node builtins with their `@gjsify/*` browser entries; when one
|
package/lib/app/gjs.js
CHANGED
|
@@ -21,6 +21,7 @@ import { nodeModulesPathRewritePlugin, getBundleDirFromOutput } from '../plugins
|
|
|
21
21
|
import { processStubPlugin } from '../plugins/process-stub.js';
|
|
22
22
|
import { cssAsStringPlugin } from '../plugins/css-as-string.js';
|
|
23
23
|
import { consoleAssignPlugin } from '../plugins/console-assign.js';
|
|
24
|
+
import { implicitGlobalAssignPlugin } from '../plugins/implicit-global-assign.js';
|
|
24
25
|
import { shebangPlugin, resolveShebangLine, inputShebangStripPlugin } from '../plugins/shebang.js';
|
|
25
26
|
import { wrapInputWithSideEffects } from '../utils/entry-wrapper.js';
|
|
26
27
|
const _shimDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -97,6 +98,7 @@ export const setupForGjs = async (input) => {
|
|
|
97
98
|
sideEffectImports.push(input.pluginOptions.autoGlobalsInject);
|
|
98
99
|
const virtualEntries = wrapInputWithSideEffects(entryPoints, sideEffectImports, {
|
|
99
100
|
preserveDefaultExport: input.pluginOptions.preserveDefaultExport === true,
|
|
101
|
+
exitOnReportedCode: true,
|
|
100
102
|
});
|
|
101
103
|
const finalInput = virtualEntries.input;
|
|
102
104
|
const options = {
|
|
@@ -192,6 +194,14 @@ export const setupForGjs = async (input) => {
|
|
|
192
194
|
// A module assigning the global `console` gets a local binding, or the inject
|
|
193
195
|
// below turns its assignment into `ASSIGN_TO_IMPORT` and fails the build.
|
|
194
196
|
...(consoleShimPath ? [consoleAssignPlugin()] : []),
|
|
197
|
+
// ADR 0079's addendum: a bare `window = …` reaches a module body that is strict
|
|
198
|
+
// once bundled, so it is a `ReferenceError` rather than the "this runtime has no
|
|
199
|
+
// window, make one" its `typeof` guard asked for. Here the guarded branch is dead
|
|
200
|
+
// anyway — the host has a `window` — but WITHOUT this transform the `window`
|
|
201
|
+
// define in `transform.define` replaces the assignment TARGET, emitting
|
|
202
|
+
// `globalThis = {…}`, i.e. code that would replace the whole global object if the
|
|
203
|
+
// branch ever ran. Measured; the plugin header carries the artifact diff.
|
|
204
|
+
implicitGlobalAssignPlugin(),
|
|
195
205
|
// `gi://Ns?version=X&optional` → a guarded import (ADR 0087), claimed `pre`
|
|
196
206
|
// so the externals policy never sees the flagged specifier.
|
|
197
207
|
giOptionalPlugin('gjs'),
|
|
@@ -237,11 +247,19 @@ export const setupForGjs = async (input) => {
|
|
|
237
247
|
// into the subset GTK4 understands.
|
|
238
248
|
cssAsStringPlugin({ targets: { firefox: 60 << 16 } }),
|
|
239
249
|
nodeModulesPathRewritePlugin({ bundleDir, runtimeResolve: format === 'esm' }),
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
250
|
+
// ADR 0081: the byte-1 banner ASSIGNS `globalThis.process`, so a
|
|
251
|
+
// `--globals auto` ANALYSIS bundle that carries it reads its own stub
|
|
252
|
+
// back as a live global and injects `@gjsify/process` into every build.
|
|
253
|
+
// The final build keeps the stub — `glob`/`path-scurry` need it.
|
|
254
|
+
...(input.pluginOptions.skipProcessStub
|
|
255
|
+
? []
|
|
256
|
+
: [
|
|
257
|
+
processStubPlugin({
|
|
258
|
+
userBanner: input.userBanner,
|
|
259
|
+
captureBundleUrl: format === 'esm',
|
|
260
|
+
giSystemProbes: input.giSystemProbes,
|
|
261
|
+
}),
|
|
262
|
+
]),
|
|
245
263
|
// resolveShebangLine returns null when disabled, else the resolved line
|
|
246
264
|
// with `${env:…}` expanded.
|
|
247
265
|
(() => {
|
package/lib/app/nativescript.js
CHANGED
|
@@ -46,6 +46,7 @@ import { gjsImportsEmptyPlugin } from '../plugins/gjs-imports-empty.js';
|
|
|
46
46
|
import { giRendererPlugin } from '../plugins/gi-renderer.js';
|
|
47
47
|
import { platformResolvePlugin, nativescriptSuffixChain, detectNativescriptPlatform, nativescriptPlatformDefines, } from '../plugins/platform-resolve.js';
|
|
48
48
|
import { unresolvedWorkspaceImportPlugin } from '../plugins/unresolved-workspace-import.js';
|
|
49
|
+
import { implicitGlobalAssignPlugin } from '../plugins/implicit-global-assign.js';
|
|
49
50
|
// Never bundled: an app works only with the ONE core instance the runtime boots, and what
|
|
50
51
|
// this target emits is an ENTRY the NS bundler resolves it for. An optional peer absent from
|
|
51
52
|
// the workspace install, it was not resolvable either — every UI-widget bridge failed to build.
|
|
@@ -159,6 +160,11 @@ export const setupForNativescript = async (input) => {
|
|
|
159
160
|
// consumes. A `.blp` whose projection loses anything is refused at build time
|
|
160
161
|
// rather than rendered partially; see `SHARED_TREE_QUERY` in the plugin.
|
|
161
162
|
blueprintPlugin(),
|
|
163
|
+
// ADR 0079's addendum, and the `define` comment above is why it is needed here:
|
|
164
|
+
// a NativeScript app gates on the absence of `window`, but a WRITE to it is a
|
|
165
|
+
// `ReferenceError` in a strict module body, not a gate. `globalThis.window = …`
|
|
166
|
+
// holds the author's intent; the guard and the absence are untouched.
|
|
167
|
+
implicitGlobalAssignPlugin(),
|
|
162
168
|
// NO cssAsStringPlugin — NS ships its own CSS pipeline via
|
|
163
169
|
// @nativescript/core; .css imports are handled by the consuming
|
|
164
170
|
// @nativescript/webpack or @nativescript/vite build
|
package/lib/app/node.js
CHANGED
|
@@ -11,6 +11,7 @@ import { cssAsStringPlugin } from '../plugins/css-as-string.js';
|
|
|
11
11
|
import { gjsImportsEmptyPlugin } from '../plugins/gjs-imports-empty.js';
|
|
12
12
|
import { gjsGiNodePlugin, gjsBuiltinModulesNodePlugin } from '../plugins/gjs-gi-node.js';
|
|
13
13
|
import { giOptionalPlugin } from '../plugins/gi-optional.js';
|
|
14
|
+
import { implicitGlobalAssignPlugin } from '../plugins/implicit-global-assign.js';
|
|
14
15
|
import { unresolvedWorkspaceImportPlugin } from '../plugins/unresolved-workspace-import.js';
|
|
15
16
|
import { nodeNativeExternalPlugin } from '../plugins/node-native-external.js';
|
|
16
17
|
import { platformResolvePlugin, desktopSuffixChain, desktopOsSuffix, DESKTOP_REFUSED_SUFFIXES, } from '../plugins/platform-resolve.js';
|
|
@@ -344,6 +345,13 @@ export const setupForNode = async (input) => {
|
|
|
344
345
|
// externalisation itself rides `NODE_GI_BARE_MODULE_SPECIFIERS` in
|
|
345
346
|
// `exactExternal` — see that const's doc comment.
|
|
346
347
|
gjsBuiltinModulesNodePlugin(ALIASES_GJS_FOR_NODE),
|
|
348
|
+
// ADR 0079's addendum, and the target that NEEDS it: `transform.define` above
|
|
349
|
+
// deliberately does not define `window`, so Excalibur's
|
|
350
|
+
// `if (typeof window === 'undefined') window = {…}` is a LIVE branch here — and
|
|
351
|
+
// in a strict module body that write is a `ReferenceError` (map-editor#300).
|
|
352
|
+
// `globalThis.window = {…}` is the same statement, the guard is untouched, and
|
|
353
|
+
// nothing defines `window`.
|
|
354
|
+
implicitGlobalAssignPlugin(),
|
|
347
355
|
// Decides the fate of `@girs/*` before `aliasPlugin` and the default
|
|
348
356
|
// resolver (same composition order as `app/browser.ts`). `emptyGirs` is
|
|
349
357
|
// gated on `gjsSourceBuild`:
|
package/lib/index.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type { GiOptionalTarget } from './plugins/gi-optional.js';
|
|
|
11
11
|
export { bindConsoleLocally, consoleAssignPlugin, freeConsoleAssignmentInsertion, CONSOLE_LOCAL_BINDING, } from './plugins/console-assign.js';
|
|
12
12
|
export type { GiSystemProbe } from './plugins/gi-runtime-paths.js';
|
|
13
13
|
export type { ProcessStubPluginOptions } from './plugins/process-stub.js';
|
|
14
|
+
export { findImplicitGlobalAssignments, rewriteImplicitGlobalAssignments, implicitGlobalAssignPlugin, IMPLICIT_GLOBAL_ASSIGN_PLUGIN, } from './plugins/implicit-global-assign.js';
|
|
15
|
+
export type { ImplicitGlobalAssignment } from './plugins/implicit-global-assign.js';
|
|
14
16
|
export { cssAsStringPlugin } from './plugins/css-as-string.js';
|
|
15
17
|
export { textLoaderPlugin } from './plugins/text-loader.js';
|
|
16
18
|
export type { TextLoaderPluginOptions, LoaderKind } from './plugins/text-loader.js';
|
package/lib/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export { processStubPlugin, GJS_PROCESS_STUB, composeBanner } from './plugins/pr
|
|
|
8
8
|
export { giRuntimePathsStub } from './plugins/gi-runtime-paths.js';
|
|
9
9
|
export { giOptionalPlugin, giOptionalShimSource, giOptionalNodeShimSource, giOptionalMarkerSource, parseOptionalGiSpecifier, GI_OPTIONAL_FLAG, GI_OPTIONAL_MARKER, } from './plugins/gi-optional.js';
|
|
10
10
|
export { bindConsoleLocally, consoleAssignPlugin, freeConsoleAssignmentInsertion, CONSOLE_LOCAL_BINDING, } from './plugins/console-assign.js';
|
|
11
|
+
export { findImplicitGlobalAssignments, rewriteImplicitGlobalAssignments, implicitGlobalAssignPlugin, IMPLICIT_GLOBAL_ASSIGN_PLUGIN, } from './plugins/implicit-global-assign.js';
|
|
11
12
|
export { cssAsStringPlugin } from './plugins/css-as-string.js';
|
|
12
13
|
export { textLoaderPlugin } from './plugins/text-loader.js';
|
|
13
14
|
export { shebangPlugin, GJS_SHEBANG, NODE_SHEBANG, expandEnvTemplate, resolveShebangLine } from './plugins/shebang.js';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Plugin } from 'rolldown';
|
|
2
|
+
/** The plugin's name — what the four app orchestrators are pinned on. */
|
|
3
|
+
export declare const IMPLICIT_GLOBAL_ASSIGN_PLUGIN = "gjsify-implicit-global-assign";
|
|
4
|
+
/** One free assignment: the identifier to prefix, and the name that was free. */
|
|
5
|
+
export interface ImplicitGlobalAssignment {
|
|
6
|
+
/** The undeclared name — the property the rewrite writes. */
|
|
7
|
+
readonly name: string;
|
|
8
|
+
/** Inclusive start offset. */
|
|
9
|
+
readonly start: number;
|
|
10
|
+
/** Exclusive end offset. */
|
|
11
|
+
readonly end: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The free assignments in `code` (one entry per undeclared name written to), or `null` when
|
|
15
|
+
* the module cannot be read — the whole module in the `with` case, since its bindings there
|
|
16
|
+
* are dynamic.
|
|
17
|
+
*/
|
|
18
|
+
export declare function findImplicitGlobalAssignments(code: string, id: string): ImplicitGlobalAssignment[] | null;
|
|
19
|
+
/** `code` with every free assignment rewritten, or null when it needs none. */
|
|
20
|
+
export declare function rewriteImplicitGlobalAssignments(code: string, id: string): string | null;
|
|
21
|
+
/**
|
|
22
|
+
* Composed by ALL FOUR `--app` targets (ADR 0079's addendum) — for the measured reason in
|
|
23
|
+
* the header: it is the fix on node and nativescript, and on gjs and browser it keeps the
|
|
24
|
+
* `window` define from turning the assignment target into `globalThis = …`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function implicitGlobalAssignPlugin(): Plugin;
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// A bare ASSIGNMENT to an UNDECLARED identifier is rewritten to `globalThis.X = …`.
|
|
2
|
+
//
|
|
3
|
+
// ADR 0079 dropped the `window` define on `--app node`, and code that was only green
|
|
4
|
+
// because the define supplied the binding came apart. Excalibur 0.32.0's
|
|
5
|
+
// `src/engine/polyfill.ts` opens with
|
|
6
|
+
//
|
|
7
|
+
// if (typeof window === 'undefined') { window = <any>{ audioContext() { return; } }; }
|
|
8
|
+
//
|
|
9
|
+
// An ES module body is strict, so the write is a `ReferenceError` on every runtime that
|
|
10
|
+
// has no `window` — the branch has never been able to run (PixelRPG/map-editor#300), and
|
|
11
|
+
// on GJS or in a browser the guard is false, so nothing noticed.
|
|
12
|
+
//
|
|
13
|
+
// `window` is the MOTIVATING case, not the subject. The bug is sloppy mode's IMPLICIT
|
|
14
|
+
// GLOBAL CREATION, and it is not window-shaped: `self = …`, `document = …` and `foo = 1` in
|
|
15
|
+
// a CommonJS file the bundler wrapped into a strict ES module all die the same way, and
|
|
16
|
+
// only the first of them has an ADR behind it. So the subject here is any identifier no
|
|
17
|
+
// enclosing scope binds, and `globalThis.X = …` is that same statement with the author's
|
|
18
|
+
// intent spelled out — it holds on Node, Bun, Deno, GJS and in a page alike.
|
|
19
|
+
//
|
|
20
|
+
// NOT A DEFINE AND NOT A REGISTER (ADR 0079's addendum). A define rewrites the very
|
|
21
|
+
// `typeof` guard that decides the branch — the @mtcute/web failure ADR 0079 documents —
|
|
22
|
+
// and a register that defines `window` flips every guard in the ecosystem. The guard stays a
|
|
23
|
+
// guard and nothing anywhere defines a global, so no allowlist is needed either: writing a
|
|
24
|
+
// global the runtime ALREADY has (`onerror = fn`, `self = globalThis`) is legal in strict
|
|
25
|
+
// mode, which makes `globalThis.X = …` an identity rather than a change.
|
|
26
|
+
//
|
|
27
|
+
// WHY ALL FOUR TARGETS AND NOT THE TWO THAT NEED IT, measured on the bundled artifact of
|
|
28
|
+
// the shape above. On `--app node` and `--app nativescript` the plugin IS the fix: without
|
|
29
|
+
// it the `--app node` bundle dies at load with `ReferenceError: window is not defined`,
|
|
30
|
+
// and with it the same bundle runs and the author's `audioContext` stub is there. On the
|
|
31
|
+
// two targets that DO define `window`, the `define` replaces the assignment TARGET too —
|
|
32
|
+
// `window = {…}` becomes `globalThis = {…}`, which REPLACES THE WHOLE GLOBAL OBJECT — and a
|
|
33
|
+
// plugin `transform` runs before it, so the line is already `globalThis.window = {…}` and
|
|
34
|
+
// the define finds nothing left to rewrite.
|
|
35
|
+
//
|
|
36
|
+
// WHY AN AST AND NOT A TEXT PASS: the question is "is X a BINDING in scope at this
|
|
37
|
+
// assignment", which no regex answers. `X.y = 1`, `foo.X = 1`, `{ X: 1 }`, `typeof X` and
|
|
38
|
+
// an `X` that is a parameter of the enclosing function all share the bytes of the shape,
|
|
39
|
+
// and rewriting a local binding would silently retarget a program that means something
|
|
40
|
+
// else. Same reason `utils/detect-free-globals.ts` parses.
|
|
41
|
+
//
|
|
42
|
+
// ONE WALK FOR EVERY NAME. The descent records bindings as ROOT → the names bound in it,
|
|
43
|
+
// rather than answering "is `window` bound" for one name at a time: a module has as many
|
|
44
|
+
// implicit globals as it has typos, and re-walking per name made the pass quadratic for an
|
|
45
|
+
// answer that is one set lookup once the chain is there.
|
|
46
|
+
//
|
|
47
|
+
// THE REFUSALS, each where its fact is knowable:
|
|
48
|
+
// - a member expression on the left (`X.y = 1`, `foo.X = 1`): a write ONTO a runtime
|
|
49
|
+
// object, which is exactly what the guarded branches around the Excalibur assignment do,
|
|
50
|
+
// and no error to fix;
|
|
51
|
+
// - an assignment whose name a `var`/`let`/`const`, a parameter, a `catch` binding, a
|
|
52
|
+
// function or class name, or an import binds: a local variable, not a global;
|
|
53
|
+
// - an ARITHMETIC compound assignment (`X += 1`) and `X++`/`X--`: their left side is a
|
|
54
|
+
// value USE, so sloppy mode threw there too and rewriting would trade that
|
|
55
|
+
// `ReferenceError` for a silent `NaN`. `||=`, `&&=` and `??=` read the name too, so on
|
|
56
|
+
// an undeclared one sloppy mode threw there as well and they stay untouched;
|
|
57
|
+
// - a name that can never be an implicit global (below): it is a binding, an unassignable
|
|
58
|
+
// global, or a `SyntaxError` before this pass ever sees it;
|
|
59
|
+
// - a module containing `with`: the binding is dynamic there, and no static pass can see
|
|
60
|
+
// which object supplies it;
|
|
61
|
+
// - a source the pinned parser cannot read: the module keeps the bug and the bundler
|
|
62
|
+
// reports the parse itself, loudly and with its own position. The measured list is
|
|
63
|
+
// `utils/scan-named-imports.ts`'s (`satisfies`, a `const` type parameter) plus
|
|
64
|
+
// acorn-typescript 1.4.13's blind spot on an angle-bracket assertion — `window =
|
|
65
|
+
// <any>{…}`, which is how Excalibur's SOURCE spells the line and what its shipped
|
|
66
|
+
// dist (the shape a build actually reads) has already erased.
|
|
67
|
+
//
|
|
68
|
+
// NOT HANDLED: a DESTRUCTURING target (`({ a, b } = obj)`) keeps its bare names.
|
|
69
|
+
// `extractBindingNames` yields names, not source ranges, so a pattern would need a second
|
|
70
|
+
// positional walk over every pattern shape (defaults, rest, computed keys) that nothing
|
|
71
|
+
// here already carries — an implicit global spelled `({ foo } = obj)` stays broken until
|
|
72
|
+
// that walk exists. `for (X in/of …)` IS handled, because its left is the same bare
|
|
73
|
+
// `Identifier` the assignment case already asks for, and sloppy mode created that `X` too.
|
|
74
|
+
import { extractBindingNames } from '../utils/detect-free-globals.js';
|
|
75
|
+
import { parseSource } from '../utils/inline-static-reads.js';
|
|
76
|
+
import { REWRITE_FILTER } from './rewrite-node-modules-paths.js';
|
|
77
|
+
/** The plugin's name — what the four app orchestrators are pinned on. */
|
|
78
|
+
export const IMPLICIT_GLOBAL_ASSIGN_PLUGIN = 'gjsify-implicit-global-assign';
|
|
79
|
+
/**
|
|
80
|
+
* Names that are never an implicit global, whatever the scope says.
|
|
81
|
+
*
|
|
82
|
+
* `undefined`/`NaN`/`Infinity` are non-writable own properties of the global object, so
|
|
83
|
+
* assigning to them is a `TypeError` in strict mode and a silent no-op in sloppy mode —
|
|
84
|
+
* never the `ReferenceError` this plugin exists for, and `globalThis.` would only spell the
|
|
85
|
+
* same failure out. `arguments` and `eval` are rejected by the parser in a strict body
|
|
86
|
+
* (`SyntaxError: Unexpected eval or arguments`), so they cannot reach a build; they are
|
|
87
|
+
* listed because a name this pass must not rewrite should not depend on that. `globalThis`
|
|
88
|
+
* IS a writable global property on every runtime, so `globalThis = x` already works — and
|
|
89
|
+
* rewriting it would write the property through itself.
|
|
90
|
+
*
|
|
91
|
+
* The CommonJS WRAPPER bindings are here for a measured reason, and it is the incident that
|
|
92
|
+
* put them in the set rather than a rule of thumb: `exports`, `module`, `require`,
|
|
93
|
+
* `__filename` and `__dirname` are parameters of the function the bundler wraps a
|
|
94
|
+
* CommonJS module in, so `exports = module.exports = require('./lib/_stream_readable.js')`
|
|
95
|
+
* — `readable-stream/readable.js`, the polyfill behind `node:stream` — is a LOCAL write. The
|
|
96
|
+
* descent cannot see the wrapper, because it parses with `sourceType: 'module'`, so the pass
|
|
97
|
+
* rewrote it to `globalThis.exports = …`: `module.exports` was still set, but every
|
|
98
|
+
* `exports.Writable = require('./lib/_stream_writable.js')` beside it landed on the wrapper's
|
|
99
|
+
* original object, which nothing returns. Measured on a bundle of `readable-stream` with and
|
|
100
|
+
* without the plugin: `Writable`, `Duplex`, `Transform`, `PassThrough` and `Readable` went
|
|
101
|
+
* from `function` to `undefined` with no error anywhere — and a consumer that inherits from
|
|
102
|
+
* one of them is what throws next, as `util.inherits(Child, undefined)`. The same line
|
|
103
|
+
* appears in every bundled copy of `semver`, whose default import is what a Babel-based
|
|
104
|
+
* plugin bundle then calls. Refusing them unconditionally costs one rewrite that no build
|
|
105
|
+
* can use: in an ES module these names resolve to nothing at all.
|
|
106
|
+
*/
|
|
107
|
+
const NEVER_IMPLICIT_GLOBAL = new Set([
|
|
108
|
+
'undefined',
|
|
109
|
+
'NaN',
|
|
110
|
+
'Infinity',
|
|
111
|
+
'arguments',
|
|
112
|
+
'eval',
|
|
113
|
+
'globalThis',
|
|
114
|
+
// The CommonJS wrapper parameters — a local write in the module that has them.
|
|
115
|
+
'exports',
|
|
116
|
+
'module',
|
|
117
|
+
'require',
|
|
118
|
+
'__filename',
|
|
119
|
+
'__dirname',
|
|
120
|
+
]);
|
|
121
|
+
/** Where the write lands: the global object, on every runtime there is one (ADR 0079). */
|
|
122
|
+
const GLOBAL_PREFIX = 'globalThis.';
|
|
123
|
+
/** Operators whose left side is a PLACE — see the third refusal for what is absent. */
|
|
124
|
+
const PLACE_ASSIGNMENTS = new Set(['=']);
|
|
125
|
+
/** `extractBindingNames` speaks acorn's node union, which a generic descent cannot be. */
|
|
126
|
+
const asAcornNode = (node) => node;
|
|
127
|
+
/**
|
|
128
|
+
* A container a `var`/lexical binding is scoped to: the function, or the module.
|
|
129
|
+
*
|
|
130
|
+
* A BLOCK is deliberately not one. Precision is needed between functions — a parameter
|
|
131
|
+
* named `window` in one function does not bind the name in its neighbour — and every
|
|
132
|
+
* coarsening above this line (a block, or a hoisted name marked in the scope enclosing the
|
|
133
|
+
* function it is written in) can only LOSE a rewrite, never retarget a program.
|
|
134
|
+
*/
|
|
135
|
+
function isScopeRoot(node) {
|
|
136
|
+
switch (node.type) {
|
|
137
|
+
case 'Program':
|
|
138
|
+
case 'FunctionDeclaration':
|
|
139
|
+
case 'FunctionExpression':
|
|
140
|
+
case 'ArrowFunctionExpression':
|
|
141
|
+
// Bodyless TypeScript overload signatures still bind their parameters.
|
|
142
|
+
case 'TSDeclareFunction':
|
|
143
|
+
case 'TSDeclareMethod':
|
|
144
|
+
case 'TSEmptyBodyFunctionExpression':
|
|
145
|
+
return true;
|
|
146
|
+
default:
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** Every name this node binds in the scope it stands in, empty when it binds none. */
|
|
151
|
+
function bindingNames(node) {
|
|
152
|
+
switch (node.type) {
|
|
153
|
+
case 'VariableDeclarator':
|
|
154
|
+
return node.id ? extractBindingNames(asAcornNode(node.id)) : [];
|
|
155
|
+
case 'FunctionDeclaration':
|
|
156
|
+
case 'FunctionExpression':
|
|
157
|
+
case 'ArrowFunctionExpression':
|
|
158
|
+
case 'TSDeclareFunction':
|
|
159
|
+
case 'TSDeclareMethod':
|
|
160
|
+
case 'TSEmptyBodyFunctionExpression': {
|
|
161
|
+
// A function's own name and its parameters bind in ITS scope, so the chain is
|
|
162
|
+
// already one root deeper by the time this is asked.
|
|
163
|
+
const names = node.id?.name === undefined ? [] : [node.id.name];
|
|
164
|
+
for (const param of node.params ?? [])
|
|
165
|
+
names.push(...extractBindingNames(asAcornNode(param)));
|
|
166
|
+
return names;
|
|
167
|
+
}
|
|
168
|
+
case 'ClassDeclaration':
|
|
169
|
+
case 'ClassExpression':
|
|
170
|
+
return node.id?.name === undefined ? [] : [node.id.name];
|
|
171
|
+
case 'CatchClause':
|
|
172
|
+
return node.param ? extractBindingNames(asAcornNode(node.param)) : [];
|
|
173
|
+
case 'ImportSpecifier':
|
|
174
|
+
case 'ImportDefaultSpecifier':
|
|
175
|
+
case 'ImportNamespaceSpecifier':
|
|
176
|
+
return node.local?.name === undefined ? [] : [node.local.name];
|
|
177
|
+
default:
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Does `name` bind OUTSIDE this node as well?
|
|
183
|
+
*
|
|
184
|
+
* A `function`/`class` DECLARATION's name is hoisted into the enclosing scope like a
|
|
185
|
+
* `var`, so `function window() {}` hides the name from its module; a PARAMETER binds only
|
|
186
|
+
* inside its function, which is why a neighbouring function's `window` parameter must not
|
|
187
|
+
* cost the module its rewrite.
|
|
188
|
+
*/
|
|
189
|
+
function hoistsOutward(node, name) {
|
|
190
|
+
switch (node.type) {
|
|
191
|
+
case 'FunctionDeclaration':
|
|
192
|
+
case 'ClassDeclaration':
|
|
193
|
+
case 'TSDeclareFunction':
|
|
194
|
+
return node.id?.name === name;
|
|
195
|
+
default:
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The bare identifier a statement writes to, or null.
|
|
201
|
+
*
|
|
202
|
+
* Both accepted shapes are an implicit global in sloppy mode, so both become a write to the
|
|
203
|
+
* global object; everything else here is a refusal, and the reason is at each branch.
|
|
204
|
+
*/
|
|
205
|
+
function freeTarget(node) {
|
|
206
|
+
if (node.type === 'AssignmentExpression') {
|
|
207
|
+
// A member expression on the left is a write ONTO a runtime object, not a write of
|
|
208
|
+
// one — the first refusal, and the reason `left.type` is asked at all.
|
|
209
|
+
if (node.left?.type !== 'Identifier')
|
|
210
|
+
return null;
|
|
211
|
+
// Every compound operator (`+=`, `||=`, `??=`, …) and `++` READS the left side
|
|
212
|
+
// first, so on an undeclared name sloppy mode threw there too: only plain `=` creates.
|
|
213
|
+
return PLACE_ASSIGNMENTS.has(node.operator ?? '') ? node.left : null;
|
|
214
|
+
}
|
|
215
|
+
// `for (X in/of …)` declares `X` in sloppy mode exactly as `X = …` does, and its left
|
|
216
|
+
// is the same bare Identifier — so it costs nothing to accept here. A `for (var X …)`
|
|
217
|
+
// or `for (const X …)` left is a VariableDeclaration and never reaches this branch.
|
|
218
|
+
if (node.type === 'ForInStatement' || node.type === 'ForOfStatement') {
|
|
219
|
+
return node.left?.type === 'Identifier' ? node.left : null;
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
function collectAssignments(ast) {
|
|
224
|
+
const candidates = [];
|
|
225
|
+
// Every scope root that has bound a name, and which names.
|
|
226
|
+
const boundInRoot = new Map();
|
|
227
|
+
let hasWith = false;
|
|
228
|
+
const stack = [{ node: ast, roots: [] }];
|
|
229
|
+
while (stack.length > 0) {
|
|
230
|
+
const frame = stack.pop();
|
|
231
|
+
const { node, roots } = frame;
|
|
232
|
+
// A function's own name and parameters bind in ITS scope, so a node that opens a
|
|
233
|
+
// scope joins the chain before it is asked what it binds.
|
|
234
|
+
const nodeRoots = isScopeRoot(node) ? [node, ...roots] : roots;
|
|
235
|
+
if (node.type === 'WithStatement')
|
|
236
|
+
hasWith = true;
|
|
237
|
+
for (const name of bindingNames(node)) {
|
|
238
|
+
// The NEAREST enclosing root — index 0, the chain is innermost-first — and only
|
|
239
|
+
// that one. Marking every root up the chain would degrade this into a
|
|
240
|
+
// module-wide "does any declaration anywhere mention the name" test, which loses
|
|
241
|
+
// the rewrite for a module whose unrelated function takes such a parameter.
|
|
242
|
+
const nearest = nodeRoots[0];
|
|
243
|
+
if (!nearest)
|
|
244
|
+
continue;
|
|
245
|
+
addBinding(boundInRoot, nearest, name);
|
|
246
|
+
if (hoistsOutward(node, name)) {
|
|
247
|
+
const enclosing = nodeRoots[1];
|
|
248
|
+
if (enclosing)
|
|
249
|
+
addBinding(boundInRoot, enclosing, name);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const target = freeTarget(node);
|
|
253
|
+
if (target && target.name !== undefined && !NEVER_IMPLICIT_GLOBAL.has(target.name)) {
|
|
254
|
+
candidates.push({ name: target.name, start: target.start, end: target.end, roots: nodeRoots });
|
|
255
|
+
}
|
|
256
|
+
for (const child of childrenOf(node))
|
|
257
|
+
stack.push({ node: child, roots: nodeRoots });
|
|
258
|
+
}
|
|
259
|
+
// A binding in scope for an assignment is declared in one of its ENCLOSING roots, so
|
|
260
|
+
// walking the chain is what makes the rewrite safe — an undeclared name has none.
|
|
261
|
+
// `with` is refused here rather than above: its answer is "no idea", not "rewrite".
|
|
262
|
+
if (hasWith)
|
|
263
|
+
return null;
|
|
264
|
+
return candidates
|
|
265
|
+
.filter(({ name, roots }) => !roots.some((root) => boundInRoot.get(root)?.has(name)))
|
|
266
|
+
.map(({ name, start, end }) => ({ name, start, end }));
|
|
267
|
+
}
|
|
268
|
+
function addBinding(boundInRoot, root, name) {
|
|
269
|
+
const names = boundInRoot.get(root);
|
|
270
|
+
if (names) {
|
|
271
|
+
names.add(name);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
boundInRoot.set(root, new Set([name]));
|
|
275
|
+
}
|
|
276
|
+
/** Every own-property value that is a node — `Object.values` order is irrelevant here. */
|
|
277
|
+
function childrenOf(node) {
|
|
278
|
+
const out = [];
|
|
279
|
+
for (const value of Object.values(node)) {
|
|
280
|
+
if (Array.isArray(value)) {
|
|
281
|
+
for (const item of value)
|
|
282
|
+
pushIfNode(item, out);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
pushIfNode(value, out);
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
function pushIfNode(value, out) {
|
|
290
|
+
if (value === null || typeof value !== 'object')
|
|
291
|
+
return;
|
|
292
|
+
if (typeof value.type !== 'string')
|
|
293
|
+
return;
|
|
294
|
+
out.push(value);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* The free assignments in `code` (one entry per undeclared name written to), or `null` when
|
|
298
|
+
* the module cannot be read — the whole module in the `with` case, since its bindings there
|
|
299
|
+
* are dynamic.
|
|
300
|
+
*/
|
|
301
|
+
export function findImplicitGlobalAssignments(code, id) {
|
|
302
|
+
let ast;
|
|
303
|
+
try {
|
|
304
|
+
// Extension-aware (a `.ts` source arrives with its type syntax intact), and the
|
|
305
|
+
// shared parser this package already parses source with — not a second copy.
|
|
306
|
+
ast = parseSource(code, id);
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
return collectAssignments(ast);
|
|
312
|
+
}
|
|
313
|
+
/** `code` with every free assignment rewritten, or null when it needs none. */
|
|
314
|
+
export function rewriteImplicitGlobalAssignments(code, id) {
|
|
315
|
+
const edits = findImplicitGlobalAssignments(code, id);
|
|
316
|
+
if (!edits || edits.length === 0)
|
|
317
|
+
return null;
|
|
318
|
+
// Descending, so each splice leaves the offsets of the ones after it valid. The name
|
|
319
|
+
// from the AST rather than the source slice, so an escaped `\u0061ssignments` writes
|
|
320
|
+
// the property it means.
|
|
321
|
+
let out = code;
|
|
322
|
+
for (const edit of [...edits].sort((a, b) => b.start - a.start)) {
|
|
323
|
+
out = out.slice(0, edit.start) + GLOBAL_PREFIX + edit.name + out.slice(edit.end);
|
|
324
|
+
}
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Composed by ALL FOUR `--app` targets (ADR 0079's addendum) — for the measured reason in
|
|
329
|
+
* the header: it is the fix on node and nativescript, and on gjs and browser it keeps the
|
|
330
|
+
* `window` define from turning the assignment target into `globalThis = …`.
|
|
331
|
+
*/
|
|
332
|
+
export function implicitGlobalAssignPlugin() {
|
|
333
|
+
return {
|
|
334
|
+
name: IMPLICIT_GLOBAL_ASSIGN_PLUGIN,
|
|
335
|
+
transform: {
|
|
336
|
+
filter: { id: REWRITE_FILTER },
|
|
337
|
+
handler(code, id) {
|
|
338
|
+
// Re-applied inside the handler, the way `react-native-gate.ts` does: the
|
|
339
|
+
// object form is engine plumbing (under GJS `filter.id` is lifted into a
|
|
340
|
+
// plugin-level `idFilter`), and a source rewrite must not depend on
|
|
341
|
+
// plumbing to stay off `.css`, `.blp` and a data URL, none of which acorn
|
|
342
|
+
// can read.
|
|
343
|
+
if (!REWRITE_FILTER.test(id))
|
|
344
|
+
return null;
|
|
345
|
+
const out = rewriteImplicitGlobalAssignments(code, id);
|
|
346
|
+
return out === null ? null : { code: out, map: null };
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
}
|
|
@@ -117,4 +117,19 @@ export interface PluginOptions {
|
|
|
117
117
|
* measured incident: `WorkspaceImportGuardOptions.toolchainAnchor`.
|
|
118
118
|
*/
|
|
119
119
|
toolchainAnchor?: string;
|
|
120
|
+
/**
|
|
121
|
+
* Leave the byte-1 `globalThis.process` stub OUT of the bundle (ADR 0081).
|
|
122
|
+
*
|
|
123
|
+
* The stub is not inert to `--globals auto`: `renderChunk` ASSIGNS
|
|
124
|
+
* `globalThis.process` before any module body, and the detector reads the
|
|
125
|
+
* ANALYSIS bundle's own output — so every pass saw `process` as a free
|
|
126
|
+
* global it had detected and injected `@gjsify/process` unconditionally.
|
|
127
|
+
* Measured on an empty entry: 140 KB instead of ~3 KB.
|
|
128
|
+
*
|
|
129
|
+
* Set ONLY by `detectAutoGlobals`, which builds a throwaway bundle to
|
|
130
|
+
* analyse. The FINAL build must keep the stub: `glob`, `path-scurry` and
|
|
131
|
+
* `readable-stream` read `process.platform` at top level during `__esm`
|
|
132
|
+
* lazy init, before any import side effect fires. A consumer never sets it.
|
|
133
|
+
*/
|
|
134
|
+
skipProcessStub?: boolean;
|
|
120
135
|
}
|
|
@@ -284,6 +284,7 @@ export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPl
|
|
|
284
284
|
const factoryResult = await gjsifyPluginFactory({
|
|
285
285
|
...pluginOptions,
|
|
286
286
|
autoGlobalsInject: currentInject,
|
|
287
|
+
skipProcessStub: true,
|
|
287
288
|
});
|
|
288
289
|
let gjsifyInstance;
|
|
289
290
|
let orchestratorOptions;
|
|
@@ -8,6 +8,22 @@
|
|
|
8
8
|
// convention for synthetic modules — Rolldown treats them as not-from-disk and
|
|
9
9
|
// skips the default loader.
|
|
10
10
|
import { GJSIFY_VIRTUAL_PREFIX } from './virtual-module-id.js';
|
|
11
|
+
/**
|
|
12
|
+
* The wrapper's last statement, GJS only (`opts.exitOnReportedCode`).
|
|
13
|
+
*
|
|
14
|
+
* GJS has no atexit hook: nothing reads `process.exitCode` when the entry ends
|
|
15
|
+
* naturally, so `process.exitCode = 1` exited 0 where Node exits 1. The wrapper
|
|
16
|
+
* body is the one place that runs after the entry's body AND its top-level
|
|
17
|
+
* awaits (ESM evaluates the imported entry first), so it is the end of main.
|
|
18
|
+
*
|
|
19
|
+
* Reaches `process` through `globalThis` (see the process-stub banner: a bare
|
|
20
|
+
* identifier can bind to a bundled module's own top-level). `exit(code)` is
|
|
21
|
+
* passed the number explicitly because the byte-1 stub's `exit(c)` ignores
|
|
22
|
+
* `exitCode`; the full `@gjsify/process` then also emits `'exit'`. Zero,
|
|
23
|
+
* `undefined` and non-numeric values stay a natural 0, as on Node.
|
|
24
|
+
*/
|
|
25
|
+
const END_OF_MAIN_EXIT = 'const __gjsify_p = globalThis.process, __gjsify_c = Number(__gjsify_p?.exitCode);\n' +
|
|
26
|
+
'if (Number.isInteger(__gjsify_c) && __gjsify_c !== 0) __gjsify_p.exit(__gjsify_c);';
|
|
11
27
|
/**
|
|
12
28
|
* If there are side-effect imports to land alongside the user's entry, wrap each
|
|
13
29
|
* entry in a virtual module that imports them first then re-exports the entry.
|
|
@@ -19,7 +35,7 @@ import { GJSIFY_VIRTUAL_PREFIX } from './virtual-module-id.js';
|
|
|
19
35
|
* Record-input case: values get wrapped, keys preserved.
|
|
20
36
|
*/
|
|
21
37
|
export function wrapInputWithSideEffects(input, sideEffects, opts = {}) {
|
|
22
|
-
if (sideEffects.length === 0 || input === undefined) {
|
|
38
|
+
if ((sideEffects.length === 0 && !opts.exitOnReportedCode) || input === undefined) {
|
|
23
39
|
return { input, plugin: null };
|
|
24
40
|
}
|
|
25
41
|
const userEntries = new Map(); // virtualId → realPath
|
|
@@ -49,6 +65,7 @@ export function wrapInputWithSideEffects(input, sideEffects, opts = {}) {
|
|
|
49
65
|
wrappedInput = out;
|
|
50
66
|
}
|
|
51
67
|
const sideEffectImports = sideEffects.map((p) => `import ${JSON.stringify(p)};`).join('\n');
|
|
68
|
+
const endOfMain = opts.exitOnReportedCode ? `\n${END_OF_MAIN_EXIT}\n` : '';
|
|
52
69
|
// Resolved real-path targets from `userEntries` get their moduleSideEffects
|
|
53
70
|
// forced to 'no-treeshake' so the user-entry's top-level body (`run({...})`,
|
|
54
71
|
// side-effect calls) survives tree-shake even when its package.json restricts
|
|
@@ -95,12 +112,12 @@ export function wrapInputWithSideEffects(input, sideEffects, opts = {}) {
|
|
|
95
112
|
const ns = '__gjsify_entry__';
|
|
96
113
|
return {
|
|
97
114
|
code: `${sideEffectImports}\nimport * as ${ns} from ${JSON.stringify(target)};\n` +
|
|
98
|
-
`export * from ${JSON.stringify(target)};\nexport default ${ns}.default;\n`,
|
|
115
|
+
`export * from ${JSON.stringify(target)};\nexport default ${ns}.default;\n${endOfMain}`,
|
|
99
116
|
moduleSideEffects: 'no-treeshake',
|
|
100
117
|
};
|
|
101
118
|
}
|
|
102
119
|
return {
|
|
103
|
-
code: `${sideEffectImports}\nimport ${JSON.stringify(target)};\nexport * from ${JSON.stringify(target)};\n`,
|
|
120
|
+
code: `${sideEffectImports}\nimport ${JSON.stringify(target)};\nexport * from ${JSON.stringify(target)};\n${endOfMain}`,
|
|
104
121
|
moduleSideEffects: 'no-treeshake',
|
|
105
122
|
};
|
|
106
123
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/rolldown-plugin-gjsify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.55.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",
|
|
@@ -63,12 +63,12 @@
|
|
|
63
63
|
],
|
|
64
64
|
"license": "MIT",
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"@gjsify/console": "^0.
|
|
67
|
-
"@gjsify/resolve-npm": "^0.
|
|
68
|
-
"@gjsify/rolldown-plugin-deepkit": "^0.
|
|
69
|
-
"@gjsify/rolldown-plugin-pnp": "^0.
|
|
70
|
-
"@gjsify/utils": "^0.
|
|
71
|
-
"@gjsify/vite-plugin-blueprint": "^0.
|
|
66
|
+
"@gjsify/console": "^0.55.0",
|
|
67
|
+
"@gjsify/resolve-npm": "^0.55.0",
|
|
68
|
+
"@gjsify/rolldown-plugin-deepkit": "^0.55.0",
|
|
69
|
+
"@gjsify/rolldown-plugin-pnp": "^0.55.0",
|
|
70
|
+
"@gjsify/utils": "^0.55.0",
|
|
71
|
+
"@gjsify/vite-plugin-blueprint": "^0.55.0",
|
|
72
72
|
"@rollup/pluginutils": "^5.4.0",
|
|
73
73
|
"acorn": "^8.17.0",
|
|
74
74
|
"acorn-typescript": "^1.4.13",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"sass": "^1.101.0"
|
|
79
79
|
},
|
|
80
80
|
"peerDependencies": {
|
|
81
|
-
"@gjsify/lightningcss-native": "^0.
|
|
81
|
+
"@gjsify/lightningcss-native": "^0.55.0",
|
|
82
82
|
"rolldown": "^1.1.4"
|
|
83
83
|
},
|
|
84
84
|
"peerDependenciesMeta": {
|
|
@@ -90,8 +90,8 @@
|
|
|
90
90
|
}
|
|
91
91
|
},
|
|
92
92
|
"devDependencies": {
|
|
93
|
-
"@gjsify/cli": "^0.
|
|
94
|
-
"@gjsify/unit": "^0.
|
|
93
|
+
"@gjsify/cli": "^0.55.0",
|
|
94
|
+
"@gjsify/unit": "^0.55.0",
|
|
95
95
|
"@types/node": "^25.9.2",
|
|
96
96
|
"rolldown": "^1.1.4",
|
|
97
97
|
"typescript": "^6.0.3"
|