@gjsify/rolldown-plugin-gjsify 0.3.14
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.d.ts +17 -0
- package/lib/app/browser.js +77 -0
- package/lib/app/gjs.d.ts +27 -0
- package/lib/app/gjs.js +211 -0
- package/lib/app/index.d.ts +6 -0
- package/lib/app/index.js +3 -0
- package/lib/app/node.d.ts +17 -0
- package/lib/app/node.js +102 -0
- package/lib/globals.d.ts +4 -0
- package/lib/globals.js +9 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.js +15 -0
- package/lib/library/index.d.ts +2 -0
- package/lib/library/index.js +1 -0
- package/lib/library/lib.d.ts +16 -0
- package/lib/library/lib.js +118 -0
- package/lib/plugin.d.ts +25 -0
- package/lib/plugin.js +67 -0
- package/lib/plugins/alias.d.ts +5 -0
- package/lib/plugins/alias.js +45 -0
- package/lib/plugins/css-as-string.d.ts +2 -0
- package/lib/plugins/css-as-string.js +34 -0
- package/lib/plugins/gjs-imports-empty.d.ts +2 -0
- package/lib/plugins/gjs-imports-empty.js +26 -0
- package/lib/plugins/process-stub.d.ts +28 -0
- package/lib/plugins/process-stub.js +60 -0
- package/lib/plugins/rewrite-node-modules-paths.d.ts +38 -0
- package/lib/plugins/rewrite-node-modules-paths.js +132 -0
- package/lib/plugins/shebang.d.ts +8 -0
- package/lib/plugins/shebang.js +26 -0
- package/lib/shims/console-gjs.d.ts +24 -0
- package/lib/shims/console-gjs.js +24 -0
- package/lib/types/app.d.ts +1 -0
- package/lib/types/app.js +1 -0
- package/lib/types/index.d.ts +3 -0
- package/lib/types/index.js +3 -0
- package/lib/types/plugin-options.d.ts +46 -0
- package/lib/types/plugin-options.js +1 -0
- package/lib/types/resolve-alias-options.d.ts +2 -0
- package/lib/types/resolve-alias-options.js +1 -0
- package/lib/utils/alias.d.ts +12 -0
- package/lib/utils/alias.js +29 -0
- package/lib/utils/auto-globals.d.ts +72 -0
- package/lib/utils/auto-globals.js +193 -0
- package/lib/utils/detect-free-globals.d.ts +18 -0
- package/lib/utils/detect-free-globals.js +268 -0
- package/lib/utils/entry-points.d.ts +2 -0
- package/lib/utils/entry-points.js +38 -0
- package/lib/utils/extension.d.ts +1 -0
- package/lib/utils/extension.js +7 -0
- package/lib/utils/index.d.ts +7 -0
- package/lib/utils/index.js +7 -0
- package/lib/utils/inline-static-reads.d.ts +11 -0
- package/lib/utils/inline-static-reads.js +549 -0
- package/lib/utils/merge.d.ts +2 -0
- package/lib/utils/merge.js +23 -0
- package/lib/utils/scan-globals.d.ts +32 -0
- package/lib/utils/scan-globals.js +85 -0
- package/package.json +68 -0
- package/src/app/browser.ts +102 -0
- package/src/app/gjs.ts +260 -0
- package/src/app/index.ts +6 -0
- package/src/app/node.ts +128 -0
- package/src/globals.ts +11 -0
- package/src/index.ts +32 -0
- package/src/library/index.ts +2 -0
- package/src/library/lib.ts +142 -0
- package/src/plugin.ts +91 -0
- package/src/plugins/alias.ts +53 -0
- package/src/plugins/css-as-string.ts +37 -0
- package/src/plugins/gjs-imports-empty.ts +29 -0
- package/src/plugins/process-stub.ts +91 -0
- package/src/plugins/rewrite-node-modules-paths.ts +169 -0
- package/src/plugins/shebang.ts +33 -0
- package/src/shims/console-gjs.ts +25 -0
- package/src/types/app.ts +1 -0
- package/src/types/index.ts +3 -0
- package/src/types/plugin-options.ts +48 -0
- package/src/types/resolve-alias-options.ts +1 -0
- package/src/utils/alias.ts +46 -0
- package/src/utils/auto-globals.ts +283 -0
- package/src/utils/detect-free-globals.ts +278 -0
- package/src/utils/entry-points.ts +48 -0
- package/src/utils/extension.ts +7 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/inline-static-reads.ts +541 -0
- package/src/utils/merge.ts +22 -0
- package/src/utils/scan-globals.ts +91 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Inject a `#!/usr/bin/env -S gjs -m` shebang at byte 0 of entry chunks.
|
|
2
|
+
//
|
|
3
|
+
// Rolldown's `output.banner` would also work, but a renderChunk hook with
|
|
4
|
+
// `order: 'post'` makes ordering declarative — the shebang lands AFTER all
|
|
5
|
+
// other banner / process-stub plugins have run, which is required because
|
|
6
|
+
// the `#` character is only valid as the very first byte of the file under
|
|
7
|
+
// SpiderMonkey 128+.
|
|
8
|
+
|
|
9
|
+
import type { Plugin } from 'rolldown';
|
|
10
|
+
|
|
11
|
+
export const GJS_SHEBANG = '#!/usr/bin/env -S gjs -m';
|
|
12
|
+
|
|
13
|
+
export interface ShebangPluginOptions {
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
/** Override the shebang line. Defaults to `GJS_SHEBANG`. */
|
|
16
|
+
line?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function shebangPlugin(options: ShebangPluginOptions = {}): Plugin | null {
|
|
20
|
+
if (!options.enabled) return null;
|
|
21
|
+
const line = options.line ?? GJS_SHEBANG;
|
|
22
|
+
return {
|
|
23
|
+
name: 'gjsify-shebang',
|
|
24
|
+
renderChunk: {
|
|
25
|
+
order: 'post' as const,
|
|
26
|
+
handler(code, chunk) {
|
|
27
|
+
if (!chunk.isEntry) return null;
|
|
28
|
+
if (code.startsWith('#!')) return null;
|
|
29
|
+
return { code: line + '\n' + code, map: null };
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// GJS console shim — bundled into GJS user builds via Rolldown's `inject`.
|
|
2
|
+
// Uses print()/printerr() on GJS, bypassing GLib.log_structured() — no
|
|
3
|
+
// `Gjs-Console-Message:` prefix, ANSI escapes work, output goes to
|
|
4
|
+
// stdout/stderr instead of GLib's logging stream.
|
|
5
|
+
//
|
|
6
|
+
// `@gjsify/console` is resolved by the user's `gjsify build` Rolldown
|
|
7
|
+
// run, NOT by tsc here. The bare specifier survives compilation and only
|
|
8
|
+
// gets followed at user-build time, where the CLI's `@gjsify/node-polyfills`
|
|
9
|
+
// dep tree has the package. tsc on this package would otherwise need the
|
|
10
|
+
// `@gjsify/console` lib to be pre-built (build-order coupling).
|
|
11
|
+
//
|
|
12
|
+
// We can't reassign `globalThis.console` on SpiderMonkey 128 — the
|
|
13
|
+
// property is non-configurable. Rolldown's `inject` option rewrites bare
|
|
14
|
+
// `console` references to a named import from this shim instead, leaving
|
|
15
|
+
// `globalThis.console` untouched and routing user `console.log(…)` calls
|
|
16
|
+
// through our object.
|
|
17
|
+
// @ts-ignore — resolved by Rolldown at user-build time, not by tsc here.
|
|
18
|
+
import { log, info, debug, warn, error, dir, dirxml, table, time, timeEnd, timeLog, trace, assert, clear, count, countReset, group, groupCollapsed, groupEnd, profile, profileEnd, timeStamp } from '@gjsify/console';
|
|
19
|
+
|
|
20
|
+
export const console = {
|
|
21
|
+
log, info, debug, warn, error, dir, dirxml, table,
|
|
22
|
+
time, timeEnd, timeLog, trace, assert, clear,
|
|
23
|
+
count, countReset, group, groupCollapsed, groupEnd,
|
|
24
|
+
profile, profileEnd, timeStamp,
|
|
25
|
+
};
|
package/src/types/app.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type App = 'gjs' | 'node' | 'browser';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { App } from './app.js';
|
|
2
|
+
|
|
3
|
+
/** CSS handling forwarded to Rolldown / Lightning CSS. */
|
|
4
|
+
export interface GjsifyCssOptions {
|
|
5
|
+
/** Browserslist-compatible target list. Defaults to `['firefox60']` for `--app gjs`. */
|
|
6
|
+
target?: string[];
|
|
7
|
+
/** Whether to minify the emitted CSS. Defaults to bundle-level `minify`. */
|
|
8
|
+
minify?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PluginOptions {
|
|
12
|
+
debug?: boolean;
|
|
13
|
+
app?: App;
|
|
14
|
+
aliases?: Record<string, string>;
|
|
15
|
+
/** Glob patterns to exclude when expanding entry points. */
|
|
16
|
+
exclude?: string[];
|
|
17
|
+
jsExtension?: string;
|
|
18
|
+
/** Override the bundle output format. */
|
|
19
|
+
format?: 'esm' | 'cjs';
|
|
20
|
+
/**
|
|
21
|
+
* Library mode — `'esm' | 'cjs'`. When set, the plugin emits an
|
|
22
|
+
* unbundled multi-entry library suitable for republication on npm
|
|
23
|
+
* rather than a single application bundle.
|
|
24
|
+
*/
|
|
25
|
+
library?: 'esm' | 'cjs';
|
|
26
|
+
/**
|
|
27
|
+
* Inject a console shim into GJS builds that uses print()/printerr() instead
|
|
28
|
+
* of `GLib.log_structured()`. Removes the "Gjs-Console-Message:" prefix and
|
|
29
|
+
* lets the terminal interpret ANSI escape codes correctly. Only applies to
|
|
30
|
+
* `--app gjs`. Defaults to `true`.
|
|
31
|
+
*/
|
|
32
|
+
consoleShim?: boolean;
|
|
33
|
+
/** Enable Deepkit TypeScript reflection. Defaults to `false`. */
|
|
34
|
+
reflection?: boolean;
|
|
35
|
+
/** CSS pipeline options forwarded to the Rolldown / Lightning CSS layer. */
|
|
36
|
+
css?: GjsifyCssOptions;
|
|
37
|
+
/**
|
|
38
|
+
* Path to a pre-computed globals stub file. The stub is an ESM file
|
|
39
|
+
* containing one `import '<pkg>/register';` per entry from the user's
|
|
40
|
+
* `--globals` CLI flag. When set, the plugin appends the stub path to
|
|
41
|
+
* Rolldown's inject list alongside the console shim.
|
|
42
|
+
*
|
|
43
|
+
* The plugin does no scanning or inference at this layer — the CLI is the
|
|
44
|
+
* sole source of truth for which `/register` modules get included. Only
|
|
45
|
+
* applies to `--app gjs`.
|
|
46
|
+
*/
|
|
47
|
+
autoGlobalsInject?: string;
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export interface ResolveAliasOptions { }
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EXTERNALS_NODE,
|
|
3
|
+
EXTERNALS_NPM,
|
|
4
|
+
ALIASES_GENERAL_FOR_GJS,
|
|
5
|
+
ALIASES_NODE_FOR_GJS,
|
|
6
|
+
ALIASES_WEB_FOR_GJS,
|
|
7
|
+
ALIASES_GENERAL_FOR_NODE,
|
|
8
|
+
ALIASES_GJS_FOR_NODE,
|
|
9
|
+
ALIASES_WEB_FOR_NODE
|
|
10
|
+
} from "@gjsify/resolve-npm";
|
|
11
|
+
|
|
12
|
+
import type { ResolveAliasOptions } from '../types/index.js';
|
|
13
|
+
|
|
14
|
+
export const setNodeAliasPrefix = (ALIASES: Record<string, string>) => {
|
|
15
|
+
// Also resolve alias names with `node:${ALIAS}`
|
|
16
|
+
for (const ALIAS in ALIASES) {
|
|
17
|
+
if(ALIAS.startsWith('node:')) {
|
|
18
|
+
continue
|
|
19
|
+
}
|
|
20
|
+
const key = `node:${ALIAS}`;
|
|
21
|
+
if(!ALIASES[key]) ALIASES[key] = ALIASES[ALIAS];
|
|
22
|
+
}
|
|
23
|
+
return ALIASES;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const getAliasesGeneralForGjs = (options: ResolveAliasOptions) => ALIASES_GENERAL_FOR_GJS;
|
|
27
|
+
const getAliasesNodeForGjs = (options: ResolveAliasOptions) => setNodeAliasPrefix(ALIASES_NODE_FOR_GJS);
|
|
28
|
+
const getAliasesWebForGjs = (options: ResolveAliasOptions) => ALIASES_WEB_FOR_GJS;
|
|
29
|
+
|
|
30
|
+
const getAliasesGeneralForNode = (options: ResolveAliasOptions) => ALIASES_GENERAL_FOR_NODE;
|
|
31
|
+
const getAliasesGjsForNode = (options: ResolveAliasOptions) => ALIASES_GJS_FOR_NODE;
|
|
32
|
+
const getAliasesWebForNode = (options: ResolveAliasOptions) => ALIASES_WEB_FOR_NODE;
|
|
33
|
+
|
|
34
|
+
export const getAliasesForGjs = (options: ResolveAliasOptions) => {
|
|
35
|
+
return {...getAliasesGeneralForGjs(options), ...getAliasesNodeForGjs(options), ...getAliasesWebForGjs(options) }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const getAliasesForNode = (options: ResolveAliasOptions) => {
|
|
39
|
+
return {...getAliasesGeneralForNode(options), ...getAliasesGjsForNode(options), ...getAliasesWebForNode(options) }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Array of Node.js build in module names (also with node: prefix) */
|
|
43
|
+
export const externalNode = [...EXTERNALS_NODE, ...EXTERNALS_NODE.map(E => `node:${E}`)];
|
|
44
|
+
|
|
45
|
+
/** Array of NPM module names for which we have our own implementation */
|
|
46
|
+
export const externalNPM = [...EXTERNALS_NPM];
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// Iterative multi-pass build orchestrator for `--globals auto`.
|
|
2
|
+
//
|
|
3
|
+
// Architecturally identical to the esbuild predecessor — only the inner
|
|
4
|
+
// build call swaps `esbuild.build()` for `rolldown()`. The "after
|
|
5
|
+
// tree-shaking" analysis property is bundler-agnostic and load-bearing
|
|
6
|
+
// per AGENTS.md "Tree-shakeability invariants — permanent". See the
|
|
7
|
+
// rationale block at the top of `detect-free-globals.ts`.
|
|
8
|
+
//
|
|
9
|
+
// Pass 1: rolldown() with no globals injection
|
|
10
|
+
// → in-memory bundle parsed by acorn for free globals
|
|
11
|
+
// Pass 2: rolldown() with detected globals injected
|
|
12
|
+
// → some injected register modules pull in MORE code that
|
|
13
|
+
// references additional globals (tree-shaking dependency cycle)
|
|
14
|
+
// Pass N: repeat until the detected set converges (typically 2–3 iterations,
|
|
15
|
+
// capped at MAX_ITERATIONS=5)
|
|
16
|
+
//
|
|
17
|
+
// We deliberately do NOT minify the analysis builds: minification can
|
|
18
|
+
// alias `globalThis` to a short variable and defeat MemberExpression
|
|
19
|
+
// detection in detect-free-globals.ts.
|
|
20
|
+
|
|
21
|
+
import { rolldown, type InputOptions, type OutputChunk, type RolldownPluginOption, type TransformOptions } from 'rolldown';
|
|
22
|
+
import { detectFreeGlobals } from './detect-free-globals.js';
|
|
23
|
+
import { resolveGlobalsList, writeRegisterInjectFile } from './scan-globals.js';
|
|
24
|
+
import { GJS_GLOBALS_MAP } from '@gjsify/resolve-npm/globals-map';
|
|
25
|
+
import type { PluginOptions } from '../types/plugin-options.js';
|
|
26
|
+
|
|
27
|
+
const GLOBALS_MAP: Record<string, string> = GJS_GLOBALS_MAP;
|
|
28
|
+
|
|
29
|
+
/** Maximum iterations to prevent runaway loops on pathological inputs. */
|
|
30
|
+
const MAX_ITERATIONS = 5;
|
|
31
|
+
|
|
32
|
+
export interface AutoGlobalsResult {
|
|
33
|
+
/** Global identifiers detected in the bundle */
|
|
34
|
+
detected: Set<string>;
|
|
35
|
+
/** Path to the generated inject stub, or undefined if no globals needed */
|
|
36
|
+
injectPath: string | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function setsEqual(a: Set<string>, b: Set<string>): boolean {
|
|
40
|
+
if (a.size !== b.size) return false;
|
|
41
|
+
for (const x of a) if (!b.has(x)) return false;
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function applyExcludeGlobals(
|
|
46
|
+
detected: Set<string>,
|
|
47
|
+
currentInject: string | undefined,
|
|
48
|
+
extraRegisterPaths: Set<string>,
|
|
49
|
+
excludeGlobals: string[] | undefined,
|
|
50
|
+
): Promise<AutoGlobalsResult> {
|
|
51
|
+
if (!excludeGlobals?.length) return { detected, injectPath: currentInject };
|
|
52
|
+
|
|
53
|
+
for (const id of excludeGlobals) detected.delete(id);
|
|
54
|
+
const filtered = detectedToRegisterPaths(detected);
|
|
55
|
+
for (const p of extraRegisterPaths) filtered.add(p);
|
|
56
|
+
const injectPath = filtered.size > 0 ? (await writeRegisterInjectFile(filtered)) ?? undefined : undefined;
|
|
57
|
+
return { detected, injectPath };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function detectedToRegisterPaths(detected: Set<string>): Set<string> {
|
|
61
|
+
const paths = new Set<string>();
|
|
62
|
+
for (const name of detected) {
|
|
63
|
+
const path = GLOBALS_MAP[name];
|
|
64
|
+
if (path) paths.add(path);
|
|
65
|
+
}
|
|
66
|
+
return paths;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface DetectAutoGlobalsOptions {
|
|
70
|
+
/**
|
|
71
|
+
* Extra explicit identifiers (or group aliases like `dom`/`web`/`node`)
|
|
72
|
+
* that should always be injected, in addition to whatever the iterative
|
|
73
|
+
* detection finds. Used by `--globals auto,<extras>` for cases where
|
|
74
|
+
* the detector cannot statically see a global because it's accessed via
|
|
75
|
+
* indirection (e.g. Excalibur's `BrowserComponent.nativeComponent.matchMedia`).
|
|
76
|
+
*/
|
|
77
|
+
extraGlobalsList?: string;
|
|
78
|
+
/**
|
|
79
|
+
* Identifiers to remove from the auto-detected set before writing the
|
|
80
|
+
* inject stub. Useful for globals that appear as false positives from
|
|
81
|
+
* dead browser-compat code in npm dependencies whose polyfills require
|
|
82
|
+
* unavailable native libraries.
|
|
83
|
+
*/
|
|
84
|
+
excludeGlobals?: string[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Build options accepted by the analyser. A subset of Rolldown's
|
|
89
|
+
* `InputOptions` plus the `output` shape used by `RolldownBuild.generate`.
|
|
90
|
+
*
|
|
91
|
+
* The caller passes the same input + output options it would use for the
|
|
92
|
+
* final build (input, plugins, external, define, …). We strip output-side
|
|
93
|
+
* fields that would force a write-to-disk and replace them with in-memory
|
|
94
|
+
* settings.
|
|
95
|
+
*/
|
|
96
|
+
export interface AnalysisOptions {
|
|
97
|
+
input: InputOptions['input'];
|
|
98
|
+
plugins?: RolldownPluginOption[];
|
|
99
|
+
external?: InputOptions['external'];
|
|
100
|
+
resolve?: InputOptions['resolve'];
|
|
101
|
+
/**
|
|
102
|
+
* Pass-through to Rolldown's `transform` (Oxc-driven) — `define`,
|
|
103
|
+
* `dropLabels`, `treeShake`, etc. live here in Rolldown's shape.
|
|
104
|
+
*/
|
|
105
|
+
transform?: TransformOptions;
|
|
106
|
+
/** Format for the analysis bundle output. Use 'esm' to match the final build. */
|
|
107
|
+
format?: 'esm' | 'cjs' | 'iife';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Build a `gjsifyPlugin` for the analyser to insert into the plugin array.
|
|
112
|
+
* Late-imported via dynamic `await import()` to break the cyclic dep
|
|
113
|
+
* between this file and `../plugin.ts`.
|
|
114
|
+
*/
|
|
115
|
+
type GjsifyPluginFactory = (
|
|
116
|
+
options: PluginOptions,
|
|
117
|
+
) => RolldownPluginOption | Promise<RolldownPluginOption>;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Run an iterative Rolldown build (in-memory) with acorn-based global
|
|
121
|
+
* detection. Each pass uses the globals discovered by the previous pass,
|
|
122
|
+
* stopping once the detected set is stable (fixpoint reached).
|
|
123
|
+
*
|
|
124
|
+
* Returns the inject stub path that the caller should pass to the
|
|
125
|
+
* final (real) build via `pluginOptions.autoGlobalsInject`.
|
|
126
|
+
*
|
|
127
|
+
* @param analysisOptions Rolldown input options for the in-memory build.
|
|
128
|
+
* @param pluginOptions Gjsify plugin options (without `autoGlobalsInject`,
|
|
129
|
+
* which this function computes).
|
|
130
|
+
* @param gjsifyPluginFactory Factory returning the gjsify plugin instance
|
|
131
|
+
* for a given set of plugin options. Provided by the caller to avoid a
|
|
132
|
+
* cyclic import between this module and `../plugin.ts`.
|
|
133
|
+
* @param verbose Emit per-iteration debug output to console.
|
|
134
|
+
* @param options Optional `extraGlobalsList` / `excludeGlobals`.
|
|
135
|
+
*/
|
|
136
|
+
export async function detectAutoGlobals(
|
|
137
|
+
analysisOptions: AnalysisOptions,
|
|
138
|
+
pluginOptions: Omit<PluginOptions, 'autoGlobalsInject'>,
|
|
139
|
+
gjsifyPluginFactory: GjsifyPluginFactory,
|
|
140
|
+
verbose?: boolean,
|
|
141
|
+
options: DetectAutoGlobalsOptions = {},
|
|
142
|
+
): Promise<AutoGlobalsResult> {
|
|
143
|
+
const extraRegisterPaths = options.extraGlobalsList
|
|
144
|
+
? resolveGlobalsList(options.extraGlobalsList)
|
|
145
|
+
: new Set<string>();
|
|
146
|
+
|
|
147
|
+
const excludeSet = new Set(options.excludeGlobals ?? []);
|
|
148
|
+
|
|
149
|
+
let detected = new Set<string>();
|
|
150
|
+
let currentInject: string | undefined = undefined;
|
|
151
|
+
|
|
152
|
+
if (extraRegisterPaths.size > 0) {
|
|
153
|
+
currentInject = (await writeRegisterInjectFile(extraRegisterPaths)) ?? undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Caller-provided plugins (e.g. PnP relay) survive every pass; the
|
|
157
|
+
// gjsify plugin appended last so its hooks run after any custom
|
|
158
|
+
// resolvers / loaders.
|
|
159
|
+
const callerPlugins = (analysisOptions.plugins ?? []).filter((p) => {
|
|
160
|
+
const name = p && typeof p === 'object' && 'name' in p ? p.name : undefined;
|
|
161
|
+
return name !== 'gjsify' && name !== 'gjsify-orchestrator';
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
for (let iteration = 1; iteration <= MAX_ITERATIONS; iteration++) {
|
|
165
|
+
const gjsifyInstance = await gjsifyPluginFactory({
|
|
166
|
+
...pluginOptions,
|
|
167
|
+
autoGlobalsInject: currentInject,
|
|
168
|
+
} as PluginOptions);
|
|
169
|
+
|
|
170
|
+
// The auto-globals inject stub is a side-effect-only ESM file that
|
|
171
|
+
// imports `<pkg>/register/<feature>` paths. Rolldown's `transform.inject`
|
|
172
|
+
// is the source-AST per-identifier rewrite we MUST NOT use (see
|
|
173
|
+
// AGENTS.md "Tree-shakeability invariants"). Instead, when the
|
|
174
|
+
// analyser has produced an inject path, append it as an additional
|
|
175
|
+
// entry — Rolldown bundles its side effects into the output and the
|
|
176
|
+
// detector sees the resulting identifier references.
|
|
177
|
+
const inputWithInject = currentInject
|
|
178
|
+
? appendInjectAsEntry(analysisOptions.input, currentInject)
|
|
179
|
+
: analysisOptions.input;
|
|
180
|
+
|
|
181
|
+
const build = await rolldown({
|
|
182
|
+
input: inputWithInject,
|
|
183
|
+
external: analysisOptions.external,
|
|
184
|
+
resolve: analysisOptions.resolve,
|
|
185
|
+
transform: analysisOptions.transform,
|
|
186
|
+
plugins: [...callerPlugins, gjsifyInstance],
|
|
187
|
+
logLevel: 'silent',
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const chunkCodes: string[] = [];
|
|
191
|
+
try {
|
|
192
|
+
const result = await build.generate({
|
|
193
|
+
format: analysisOptions.format ?? 'esm',
|
|
194
|
+
minify: false,
|
|
195
|
+
sourcemap: false,
|
|
196
|
+
});
|
|
197
|
+
for (const entry of result.output) {
|
|
198
|
+
if (entry.type === 'chunk') chunkCodes.push((entry as OutputChunk).code);
|
|
199
|
+
}
|
|
200
|
+
} finally {
|
|
201
|
+
await build.close();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (chunkCodes.length === 0) {
|
|
205
|
+
return { detected: new Set(), injectPath: currentInject };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Parse each chunk independently and union the detected sets.
|
|
209
|
+
// Rolldown emits one chunk per entry — concatenating them would
|
|
210
|
+
// produce a syntactically invalid combined program (duplicate
|
|
211
|
+
// top-level declarations: `File`, `Buffer`, …) that acorn can't
|
|
212
|
+
// parse. Per-chunk parsing keeps each chunk's lexical scope intact.
|
|
213
|
+
const newDetected = new Set<string>();
|
|
214
|
+
for (const code of chunkCodes) {
|
|
215
|
+
for (const id of detectFreeGlobals(code)) newDetected.add(id);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Apply excludeGlobals BEFORE writing the next iteration's inject file.
|
|
219
|
+
// Otherwise an excluded identifier would still appear in the inject
|
|
220
|
+
// import list and the analysis build itself would fail when the
|
|
221
|
+
// corresponding `@gjsify/<pkg>/register/<feature>` is not in the
|
|
222
|
+
// project's resolvable dep tree.
|
|
223
|
+
if (excludeSet.size > 0) {
|
|
224
|
+
for (const id of excludeSet) newDetected.delete(id);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Fixpoint check: detection is monotonic — once a global is needed,
|
|
228
|
+
// more code gets pulled in by the next pass, which can only ADD
|
|
229
|
+
// requirements. So a set that didn't grow is a converged set.
|
|
230
|
+
if (setsEqual(detected, newDetected)) {
|
|
231
|
+
if (verbose) {
|
|
232
|
+
const sorted = [...detected].sort();
|
|
233
|
+
const extras = extraRegisterPaths.size > 0 ? ` (+ ${extraRegisterPaths.size} extra register module(s))` : '';
|
|
234
|
+
console.debug(
|
|
235
|
+
`[gjsify] --globals auto: converged after ${iteration - 1} iteration(s), ${detected.size} global(s)${sorted.length ? ': ' + sorted.join(', ') : ''}${extras}`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return applyExcludeGlobals(detected, currentInject, extraRegisterPaths, options.excludeGlobals);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
detected = newDetected;
|
|
242
|
+
const registerPaths = detectedToRegisterPaths(detected);
|
|
243
|
+
for (const p of extraRegisterPaths) registerPaths.add(p);
|
|
244
|
+
|
|
245
|
+
if (registerPaths.size === 0) {
|
|
246
|
+
return { detected, injectPath: undefined };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
currentInject = (await writeRegisterInjectFile(registerPaths)) ?? undefined;
|
|
250
|
+
|
|
251
|
+
if (verbose) {
|
|
252
|
+
const sorted = [...detected].sort();
|
|
253
|
+
console.debug(
|
|
254
|
+
`[gjsify] --globals auto: iteration ${iteration}, ${detected.size} global(s)${sorted.length ? ': ' + sorted.join(', ') : ''}`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (verbose) {
|
|
260
|
+
console.debug(
|
|
261
|
+
`[gjsify] --globals auto: hit max iterations (${MAX_ITERATIONS}), using last detected set`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
return applyExcludeGlobals(detected, currentInject, extraRegisterPaths, options.excludeGlobals);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Append an additional entry path to a Rolldown `input` value while
|
|
269
|
+
* preserving its shape (string → array, array → array, record → record).
|
|
270
|
+
* The new entry is given the synthetic name `__gjsify_inject` when the
|
|
271
|
+
* record form is used so it doesn't collide with user-named outputs.
|
|
272
|
+
*/
|
|
273
|
+
function appendInjectAsEntry(
|
|
274
|
+
input: InputOptions['input'],
|
|
275
|
+
injectPath: string,
|
|
276
|
+
): InputOptions['input'] {
|
|
277
|
+
if (input === undefined) return [injectPath];
|
|
278
|
+
if (typeof input === 'string') return [input, injectPath];
|
|
279
|
+
if (Array.isArray(input)) {
|
|
280
|
+
return [...input, injectPath];
|
|
281
|
+
}
|
|
282
|
+
return { ...input, __gjsify_inject: injectPath };
|
|
283
|
+
}
|