@gjsify/rolldown-plugin-gjsify 0.4.0 → 0.4.3
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/package.json +71 -67
- package/src/app/browser.ts +0 -101
- package/src/app/gjs.ts +0 -334
- package/src/app/index.ts +0 -6
- package/src/app/node.ts +0 -127
- package/src/globals.ts +0 -11
- package/src/index.ts +0 -34
- package/src/library/index.ts +0 -2
- package/src/library/lib.ts +0 -141
- package/src/plugin.ts +0 -96
- package/src/plugins/alias.ts +0 -61
- package/src/plugins/css-as-string.ts +0 -189
- package/src/plugins/gjs-imports-empty.ts +0 -29
- package/src/plugins/process-stub.ts +0 -91
- package/src/plugins/rewrite-node-modules-paths.ts +0 -169
- package/src/plugins/shebang.ts +0 -93
- package/src/plugins/text-loader.ts +0 -54
- package/src/shims/console-gjs.ts +0 -25
- package/src/shims/unicorn-magic.ts +0 -75
- package/src/types/app.ts +0 -1
- package/src/types/index.ts +0 -3
- package/src/types/plugin-options.ts +0 -48
- package/src/types/resolve-alias-options.ts +0 -1
- package/src/utils/alias.ts +0 -46
- package/src/utils/auto-globals.ts +0 -321
- package/src/utils/detect-free-globals.ts +0 -284
- package/src/utils/entry-points.ts +0 -48
- package/src/utils/extension.ts +0 -7
- package/src/utils/index.ts +0 -7
- package/src/utils/inline-static-reads.ts +0 -541
- package/src/utils/merge.ts +0 -22
- package/src/utils/scan-globals.ts +0 -91
- package/tsconfig.json +0 -16
|
@@ -1,321 +0,0 @@
|
|
|
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 type { InputOptions, RolldownPluginOption, TransformOptions } from 'rolldown';
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* In-memory bundle function — returns the per-entry chunk code strings.
|
|
25
|
-
* Implementations: npm rolldown (Node default), `@gjsify/rolldown-native`
|
|
26
|
-
* (GJS). Pulled out so auto-globals can run under either engine without
|
|
27
|
-
* hardcoding npm rolldown (which can't load under GJS — the Rust prebuild's
|
|
28
|
-
* init code uses `require('node:fs')` synchronously).
|
|
29
|
-
*
|
|
30
|
-
* The default impl below dynamically imports npm rolldown; the CLI
|
|
31
|
-
* overrides this from `actions/build.ts` to route via the same engine the
|
|
32
|
-
* final build uses.
|
|
33
|
-
*/
|
|
34
|
-
export type AnalysisBundler = (input: {
|
|
35
|
-
rolldownInput: InputOptions;
|
|
36
|
-
format: 'esm' | 'cjs' | 'iife';
|
|
37
|
-
}) => Promise<string[]>;
|
|
38
|
-
|
|
39
|
-
const defaultBundler: AnalysisBundler = async ({ rolldownInput, format }) => {
|
|
40
|
-
// Indirect specifier so the GJS bundle doesn't pull npm rolldown in
|
|
41
|
-
// statically. Only reached when the caller doesn't override (Node).
|
|
42
|
-
const specifier = 'rolldown';
|
|
43
|
-
const mod = (await import(/* @vite-ignore */ specifier)) as typeof import('rolldown');
|
|
44
|
-
const build = await mod.rolldown(rolldownInput);
|
|
45
|
-
try {
|
|
46
|
-
const result = await build.generate({ format, minify: false, sourcemap: false });
|
|
47
|
-
const codes: string[] = [];
|
|
48
|
-
for (const entry of result.output) {
|
|
49
|
-
if (entry.type === 'chunk') codes.push(entry.code);
|
|
50
|
-
}
|
|
51
|
-
return codes;
|
|
52
|
-
} finally {
|
|
53
|
-
await build.close();
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
import { detectFreeGlobals } from './detect-free-globals.js';
|
|
57
|
-
import { resolveGlobalsList, writeRegisterInjectFile } from './scan-globals.js';
|
|
58
|
-
import { GJS_GLOBALS_MAP } from '@gjsify/resolve-npm/globals-map';
|
|
59
|
-
import type { PluginOptions } from '../types/plugin-options.js';
|
|
60
|
-
|
|
61
|
-
const GLOBALS_MAP: Record<string, string> = GJS_GLOBALS_MAP;
|
|
62
|
-
|
|
63
|
-
/** Maximum iterations to prevent runaway loops on pathological inputs. */
|
|
64
|
-
const MAX_ITERATIONS = 5;
|
|
65
|
-
|
|
66
|
-
export interface AutoGlobalsResult {
|
|
67
|
-
/** Global identifiers detected in the bundle */
|
|
68
|
-
detected: Set<string>;
|
|
69
|
-
/** Path to the generated inject stub, or undefined if no globals needed */
|
|
70
|
-
injectPath: string | undefined;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function setsEqual(a: Set<string>, b: Set<string>): boolean {
|
|
74
|
-
if (a.size !== b.size) return false;
|
|
75
|
-
for (const x of a) if (!b.has(x)) return false;
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async function applyExcludeGlobals(
|
|
80
|
-
detected: Set<string>,
|
|
81
|
-
currentInject: string | undefined,
|
|
82
|
-
extraRegisterPaths: Set<string>,
|
|
83
|
-
excludeGlobals: string[] | undefined,
|
|
84
|
-
): Promise<AutoGlobalsResult> {
|
|
85
|
-
if (!excludeGlobals?.length) return { detected, injectPath: currentInject };
|
|
86
|
-
|
|
87
|
-
for (const id of excludeGlobals) detected.delete(id);
|
|
88
|
-
const filtered = detectedToRegisterPaths(detected);
|
|
89
|
-
for (const p of extraRegisterPaths) filtered.add(p);
|
|
90
|
-
const injectPath = filtered.size > 0 ? (await writeRegisterInjectFile(filtered)) ?? undefined : undefined;
|
|
91
|
-
return { detected, injectPath };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function detectedToRegisterPaths(detected: Set<string>): Set<string> {
|
|
95
|
-
const paths = new Set<string>();
|
|
96
|
-
for (const name of detected) {
|
|
97
|
-
const path = GLOBALS_MAP[name];
|
|
98
|
-
if (path) paths.add(path);
|
|
99
|
-
}
|
|
100
|
-
return paths;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export interface DetectAutoGlobalsOptions {
|
|
104
|
-
/**
|
|
105
|
-
* Extra explicit identifiers (or group aliases like `dom`/`web`/`node`)
|
|
106
|
-
* that should always be injected, in addition to whatever the iterative
|
|
107
|
-
* detection finds. Used by `--globals auto,<extras>` for cases where
|
|
108
|
-
* the detector cannot statically see a global because it's accessed via
|
|
109
|
-
* indirection (e.g. Excalibur's `BrowserComponent.nativeComponent.matchMedia`).
|
|
110
|
-
*/
|
|
111
|
-
extraGlobalsList?: string;
|
|
112
|
-
/**
|
|
113
|
-
* Identifiers to remove from the auto-detected set before writing the
|
|
114
|
-
* inject stub. Useful for globals that appear as false positives from
|
|
115
|
-
* dead browser-compat code in npm dependencies whose polyfills require
|
|
116
|
-
* unavailable native libraries.
|
|
117
|
-
*/
|
|
118
|
-
excludeGlobals?: string[];
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Build options accepted by the analyser. A subset of Rolldown's
|
|
123
|
-
* `InputOptions` plus the `output` shape used by `RolldownBuild.generate`.
|
|
124
|
-
*
|
|
125
|
-
* The caller passes the same input + output options it would use for the
|
|
126
|
-
* final build (input, plugins, external, define, …). We strip output-side
|
|
127
|
-
* fields that would force a write-to-disk and replace them with in-memory
|
|
128
|
-
* settings.
|
|
129
|
-
*/
|
|
130
|
-
export interface AnalysisOptions {
|
|
131
|
-
input: InputOptions['input'];
|
|
132
|
-
plugins?: RolldownPluginOption[];
|
|
133
|
-
external?: InputOptions['external'];
|
|
134
|
-
resolve?: InputOptions['resolve'];
|
|
135
|
-
/**
|
|
136
|
-
* Pass-through to Rolldown's `transform` (Oxc-driven) — `define`,
|
|
137
|
-
* `dropLabels`, `treeShake`, etc. live here in Rolldown's shape.
|
|
138
|
-
*/
|
|
139
|
-
transform?: TransformOptions;
|
|
140
|
-
/** Format for the analysis bundle output. Use 'esm' to match the final build. */
|
|
141
|
-
format?: 'esm' | 'cjs' | 'iife';
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Build a `gjsifyPlugin` for the analyser to insert into the plugin array.
|
|
146
|
-
* Late-imported via dynamic `await import()` to break the cyclic dep
|
|
147
|
-
* between this file and `../plugin.ts`.
|
|
148
|
-
*/
|
|
149
|
-
type GjsifyPluginFactory = (
|
|
150
|
-
options: PluginOptions,
|
|
151
|
-
) => RolldownPluginOption | Promise<RolldownPluginOption>;
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Run an iterative Rolldown build (in-memory) with acorn-based global
|
|
155
|
-
* detection. Each pass uses the globals discovered by the previous pass,
|
|
156
|
-
* stopping once the detected set is stable (fixpoint reached).
|
|
157
|
-
*
|
|
158
|
-
* Returns the inject stub path that the caller should pass to the
|
|
159
|
-
* final (real) build via `pluginOptions.autoGlobalsInject`.
|
|
160
|
-
*
|
|
161
|
-
* @param analysisOptions Rolldown input options for the in-memory build.
|
|
162
|
-
* @param pluginOptions Gjsify plugin options (without `autoGlobalsInject`,
|
|
163
|
-
* which this function computes).
|
|
164
|
-
* @param gjsifyPluginFactory Factory returning the gjsify plugin instance
|
|
165
|
-
* for a given set of plugin options. Provided by the caller to avoid a
|
|
166
|
-
* cyclic import between this module and `../plugin.ts`.
|
|
167
|
-
* @param verbose Emit per-iteration debug output to console.
|
|
168
|
-
* @param options Optional `extraGlobalsList` / `excludeGlobals`.
|
|
169
|
-
*/
|
|
170
|
-
export async function detectAutoGlobals(
|
|
171
|
-
analysisOptions: AnalysisOptions,
|
|
172
|
-
pluginOptions: Omit<PluginOptions, 'autoGlobalsInject'>,
|
|
173
|
-
gjsifyPluginFactory: GjsifyPluginFactory,
|
|
174
|
-
verbose?: boolean,
|
|
175
|
-
options: DetectAutoGlobalsOptions = {},
|
|
176
|
-
bundler: AnalysisBundler = defaultBundler,
|
|
177
|
-
): Promise<AutoGlobalsResult> {
|
|
178
|
-
const extraRegisterPaths = options.extraGlobalsList
|
|
179
|
-
? resolveGlobalsList(options.extraGlobalsList)
|
|
180
|
-
: new Set<string>();
|
|
181
|
-
|
|
182
|
-
const excludeSet = new Set(options.excludeGlobals ?? []);
|
|
183
|
-
|
|
184
|
-
let detected = new Set<string>();
|
|
185
|
-
let currentInject: string | undefined = undefined;
|
|
186
|
-
|
|
187
|
-
if (extraRegisterPaths.size > 0) {
|
|
188
|
-
currentInject = (await writeRegisterInjectFile(extraRegisterPaths)) ?? undefined;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Caller-provided plugins (e.g. PnP relay) survive every pass; the
|
|
192
|
-
// gjsify plugin appended last so its hooks run after any custom
|
|
193
|
-
// resolvers / loaders.
|
|
194
|
-
const callerPlugins = (analysisOptions.plugins ?? []).filter((p) => {
|
|
195
|
-
const name = p && typeof p === 'object' && 'name' in p ? p.name : undefined;
|
|
196
|
-
return name !== 'gjsify' && name !== 'gjsify-orchestrator';
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
for (let iteration = 1; iteration <= MAX_ITERATIONS; iteration++) {
|
|
200
|
-
const gjsifyInstance = await gjsifyPluginFactory({
|
|
201
|
-
...pluginOptions,
|
|
202
|
-
autoGlobalsInject: currentInject,
|
|
203
|
-
} as PluginOptions);
|
|
204
|
-
|
|
205
|
-
// The auto-globals inject stub is a side-effect-only ESM file that
|
|
206
|
-
// imports `<pkg>/register/<feature>` paths. Rolldown's `transform.inject`
|
|
207
|
-
// is the source-AST per-identifier rewrite we MUST NOT use (see
|
|
208
|
-
// AGENTS.md "Tree-shakeability invariants"). Instead, when the
|
|
209
|
-
// analyser has produced an inject path, append it as an additional
|
|
210
|
-
// entry — Rolldown bundles its side effects into the output and the
|
|
211
|
-
// detector sees the resulting identifier references.
|
|
212
|
-
const inputWithInject = currentInject
|
|
213
|
-
? appendInjectAsEntry(analysisOptions.input, currentInject)
|
|
214
|
-
: analysisOptions.input;
|
|
215
|
-
|
|
216
|
-
const chunkCodes = await bundler({
|
|
217
|
-
rolldownInput: {
|
|
218
|
-
input: inputWithInject,
|
|
219
|
-
external: analysisOptions.external,
|
|
220
|
-
resolve: analysisOptions.resolve,
|
|
221
|
-
transform: analysisOptions.transform,
|
|
222
|
-
plugins: [...callerPlugins, gjsifyInstance],
|
|
223
|
-
logLevel: 'silent',
|
|
224
|
-
},
|
|
225
|
-
format: analysisOptions.format ?? 'esm',
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
if (chunkCodes.length === 0) {
|
|
229
|
-
return { detected: new Set(), injectPath: currentInject };
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Parse each chunk independently and union the detected sets.
|
|
233
|
-
// Rolldown emits one chunk per entry — concatenating them would
|
|
234
|
-
// produce a syntactically invalid combined program (duplicate
|
|
235
|
-
// top-level declarations: `File`, `Buffer`, …) that acorn can't
|
|
236
|
-
// parse. Per-chunk parsing keeps each chunk's lexical scope intact.
|
|
237
|
-
const newDetected = new Set<string>();
|
|
238
|
-
for (let i = 0; i < chunkCodes.length; i++) {
|
|
239
|
-
const code = chunkCodes[i] ?? '';
|
|
240
|
-
try {
|
|
241
|
-
for (const id of detectFreeGlobals(code)) newDetected.add(id);
|
|
242
|
-
} catch (e) {
|
|
243
|
-
if ((globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.GJSIFY_DEBUG_AUTO_GLOBALS) {
|
|
244
|
-
const path = `/tmp/gjsify-auto-globals-failed-chunk-${i}.mjs`;
|
|
245
|
-
try {
|
|
246
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
247
|
-
const fs = await import('node:fs');
|
|
248
|
-
fs.writeFileSync(path, code);
|
|
249
|
-
console.error(`[gjsify-auto-globals] parse failed on chunk #${i} — wrote ${path} for inspection`);
|
|
250
|
-
} catch { /* ignore */ }
|
|
251
|
-
}
|
|
252
|
-
throw e;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Apply excludeGlobals BEFORE writing the next iteration's inject file.
|
|
257
|
-
// Otherwise an excluded identifier would still appear in the inject
|
|
258
|
-
// import list and the analysis build itself would fail when the
|
|
259
|
-
// corresponding `@gjsify/<pkg>/register/<feature>` is not in the
|
|
260
|
-
// project's resolvable dep tree.
|
|
261
|
-
if (excludeSet.size > 0) {
|
|
262
|
-
for (const id of excludeSet) newDetected.delete(id);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Fixpoint check: detection is monotonic — once a global is needed,
|
|
266
|
-
// more code gets pulled in by the next pass, which can only ADD
|
|
267
|
-
// requirements. So a set that didn't grow is a converged set.
|
|
268
|
-
if (setsEqual(detected, newDetected)) {
|
|
269
|
-
if (verbose) {
|
|
270
|
-
const sorted = [...detected].sort();
|
|
271
|
-
const extras = extraRegisterPaths.size > 0 ? ` (+ ${extraRegisterPaths.size} extra register module(s))` : '';
|
|
272
|
-
console.debug(
|
|
273
|
-
`[gjsify] --globals auto: converged after ${iteration - 1} iteration(s), ${detected.size} global(s)${sorted.length ? ': ' + sorted.join(', ') : ''}${extras}`,
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
return applyExcludeGlobals(detected, currentInject, extraRegisterPaths, options.excludeGlobals);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
detected = newDetected;
|
|
280
|
-
const registerPaths = detectedToRegisterPaths(detected);
|
|
281
|
-
for (const p of extraRegisterPaths) registerPaths.add(p);
|
|
282
|
-
|
|
283
|
-
if (registerPaths.size === 0) {
|
|
284
|
-
return { detected, injectPath: undefined };
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
currentInject = (await writeRegisterInjectFile(registerPaths)) ?? undefined;
|
|
288
|
-
|
|
289
|
-
if (verbose) {
|
|
290
|
-
const sorted = [...detected].sort();
|
|
291
|
-
console.debug(
|
|
292
|
-
`[gjsify] --globals auto: iteration ${iteration}, ${detected.size} global(s)${sorted.length ? ': ' + sorted.join(', ') : ''}`,
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (verbose) {
|
|
298
|
-
console.debug(
|
|
299
|
-
`[gjsify] --globals auto: hit max iterations (${MAX_ITERATIONS}), using last detected set`,
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
return applyExcludeGlobals(detected, currentInject, extraRegisterPaths, options.excludeGlobals);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Append an additional entry path to a Rolldown `input` value while
|
|
307
|
-
* preserving its shape (string → array, array → array, record → record).
|
|
308
|
-
* The new entry is given the synthetic name `__gjsify_inject` when the
|
|
309
|
-
* record form is used so it doesn't collide with user-named outputs.
|
|
310
|
-
*/
|
|
311
|
-
function appendInjectAsEntry(
|
|
312
|
-
input: InputOptions['input'],
|
|
313
|
-
injectPath: string,
|
|
314
|
-
): InputOptions['input'] {
|
|
315
|
-
if (input === undefined) return [injectPath];
|
|
316
|
-
if (typeof input === 'string') return [input, injectPath];
|
|
317
|
-
if (Array.isArray(input)) {
|
|
318
|
-
return [...input, injectPath];
|
|
319
|
-
}
|
|
320
|
-
return { ...input, __gjsify_inject: injectPath };
|
|
321
|
-
}
|
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
// Detect free (unbound) global identifiers in bundled JS output.
|
|
2
|
-
//
|
|
3
|
-
// Used by the `--globals auto` two-pass build: the first esbuild pass
|
|
4
|
-
// produces a minified bundle without globals injection, this module
|
|
5
|
-
// parses it with acorn and finds references to known GJS globals that
|
|
6
|
-
// are not locally declared. The result feeds the second pass's inject
|
|
7
|
-
// stub so only actually-needed globals are registered.
|
|
8
|
-
|
|
9
|
-
import * as acorn from 'acorn';
|
|
10
|
-
import * as walk from 'acorn-walk';
|
|
11
|
-
import { GJS_GLOBALS_MAP } from '@gjsify/resolve-npm/globals-map';
|
|
12
|
-
|
|
13
|
-
const KNOWN_GLOBALS = new Set(Object.keys(GJS_GLOBALS_MAP as Record<string, string>));
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Method markers — `<host>.<method>(…)` patterns that imply a global
|
|
17
|
-
* identifier should be injected even though the identifier itself never
|
|
18
|
-
* appears in the bundle.
|
|
19
|
-
*
|
|
20
|
-
* Example: a project that calls `navigator.getGamepads()` doesn't reference
|
|
21
|
-
* any of the gamepad-related identifiers in the globals map, but it still
|
|
22
|
-
* needs `@gjsify/gamepad/register` to patch `navigator` with the method.
|
|
23
|
-
* This marker maps `navigator.getGamepads` → inject the `GamepadEvent`
|
|
24
|
-
* register path (which is the gamepad package's register entry).
|
|
25
|
-
*
|
|
26
|
-
* Keyed by `host.method` (lowercase host, exact method name). Values are
|
|
27
|
-
* KNOWN_GLOBALS identifiers — the detector adds them as free globals if
|
|
28
|
-
* the corresponding member expression is found in the bundle.
|
|
29
|
-
*/
|
|
30
|
-
const METHOD_MARKERS: Record<string, string> = {
|
|
31
|
-
// Gamepad API — navigator.getGamepads is patched on by @gjsify/gamepad/register
|
|
32
|
-
'navigator.getGamepads': 'GamepadEvent',
|
|
33
|
-
// WebRTC — navigator.mediaDevices is patched on by @gjsify/webrtc/register/media-devices
|
|
34
|
-
'navigator.mediaDevices': 'MediaDevices',
|
|
35
|
-
// WebAssembly Promise APIs — the runtime stubs throw at first call, so
|
|
36
|
-
// any reference to these methods needs the `@gjsify/webassembly` polyfill.
|
|
37
|
-
// The register entry replaces the stubs with wrappers around the working
|
|
38
|
-
// synchronous `new WebAssembly.{Module,Instance}` constructors.
|
|
39
|
-
'WebAssembly.compile': 'WebAssembly',
|
|
40
|
-
'WebAssembly.compileStreaming': 'WebAssembly',
|
|
41
|
-
'WebAssembly.instantiate': 'WebAssembly',
|
|
42
|
-
'WebAssembly.instantiateStreaming': 'WebAssembly',
|
|
43
|
-
'WebAssembly.validate': 'WebAssembly',
|
|
44
|
-
// Note: URL.createObjectURL / URL.revokeObjectURL don't need markers —
|
|
45
|
-
// they are first-class static methods on @gjsify/url's URL class, so the
|
|
46
|
-
// free `URL` identifier (detected directly, maps to
|
|
47
|
-
// @gjsify/node-globals/register/url in GJS_GLOBALS_MAP) already pulls in
|
|
48
|
-
// the correct register module.
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Extract all bound names from a binding pattern
|
|
53
|
-
* (Identifier, ObjectPattern, ArrayPattern, AssignmentPattern, RestElement).
|
|
54
|
-
*/
|
|
55
|
-
function extractBindingNames(node: acorn.AnyNode): string[] {
|
|
56
|
-
if (!node) return [];
|
|
57
|
-
switch (node.type) {
|
|
58
|
-
case 'Identifier':
|
|
59
|
-
return [(node as acorn.Identifier).name];
|
|
60
|
-
case 'ObjectPattern':
|
|
61
|
-
return (node as acorn.ObjectPattern).properties.flatMap((p) =>
|
|
62
|
-
p.type === 'RestElement'
|
|
63
|
-
? extractBindingNames(p.argument)
|
|
64
|
-
: extractBindingNames((p as acorn.Property).value),
|
|
65
|
-
);
|
|
66
|
-
case 'ArrayPattern':
|
|
67
|
-
return (node as acorn.ArrayPattern).elements.flatMap((e) =>
|
|
68
|
-
e
|
|
69
|
-
? e.type === 'RestElement'
|
|
70
|
-
? extractBindingNames(e.argument)
|
|
71
|
-
: extractBindingNames(e)
|
|
72
|
-
: [],
|
|
73
|
-
);
|
|
74
|
-
case 'AssignmentPattern':
|
|
75
|
-
return extractBindingNames((node as acorn.AssignmentPattern).left);
|
|
76
|
-
case 'RestElement':
|
|
77
|
-
return extractBindingNames((node as acorn.RestElement).argument);
|
|
78
|
-
default:
|
|
79
|
-
return [];
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Parse bundled JS code and return the set of free (unbound) identifiers
|
|
85
|
-
* that match known GJS globals from `GJS_GLOBALS_MAP`.
|
|
86
|
-
*
|
|
87
|
-
* "Free" means the identifier is referenced but never declared in the
|
|
88
|
-
* module (var/let/const/function/class/import/param/catch).
|
|
89
|
-
*
|
|
90
|
-
* After esbuild bundling + minification, local variables that shadow
|
|
91
|
-
* globals are renamed to short names, so any surviving known-global name
|
|
92
|
-
* in the output is almost certainly a true global reference. The
|
|
93
|
-
* declared-names check is a safety net for edge cases where esbuild
|
|
94
|
-
* keeps the original name.
|
|
95
|
-
*
|
|
96
|
-
* `typeof X` references ARE included — if code guards with
|
|
97
|
-
* `typeof fetch !== 'undefined'`, it intends to use fetch when available
|
|
98
|
-
* and we can provide it.
|
|
99
|
-
*/
|
|
100
|
-
export function detectFreeGlobals(code: string): Set<string> {
|
|
101
|
-
const ast = acorn.parse(code, {
|
|
102
|
-
ecmaVersion: 'latest',
|
|
103
|
-
sourceType: 'module',
|
|
104
|
-
// Some bundled chunks carry an embedded `#!shebang` line —
|
|
105
|
-
// notably any project bundling its own CLI gets the
|
|
106
|
-
// `#!/usr/bin/env -S gjs -m` shebang hoisted to byte 0.
|
|
107
|
-
// Acorn rejects shebangs by default; allow them so the
|
|
108
|
-
// free-globals analyzer doesn't choke on its own input.
|
|
109
|
-
allowHashBang: true,
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// --- Pass 1: collect all declared names across the entire module ---
|
|
113
|
-
const declaredNames = new Set<string>();
|
|
114
|
-
|
|
115
|
-
walk.simple(ast, {
|
|
116
|
-
VariableDeclarator(node: acorn.VariableDeclarator) {
|
|
117
|
-
for (const name of extractBindingNames(node.id)) {
|
|
118
|
-
declaredNames.add(name);
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
FunctionDeclaration(node: acorn.FunctionDeclaration) {
|
|
122
|
-
if (node.id) declaredNames.add(node.id.name);
|
|
123
|
-
for (const param of node.params) {
|
|
124
|
-
for (const name of extractBindingNames(param)) {
|
|
125
|
-
declaredNames.add(name);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
},
|
|
129
|
-
FunctionExpression(node: acorn.FunctionExpression) {
|
|
130
|
-
if (node.id) declaredNames.add(node.id.name);
|
|
131
|
-
for (const param of node.params) {
|
|
132
|
-
for (const name of extractBindingNames(param)) {
|
|
133
|
-
declaredNames.add(name);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
ArrowFunctionExpression(node: acorn.ArrowFunctionExpression) {
|
|
138
|
-
for (const param of node.params) {
|
|
139
|
-
for (const name of extractBindingNames(param)) {
|
|
140
|
-
declaredNames.add(name);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
},
|
|
144
|
-
ClassDeclaration(node: acorn.ClassDeclaration) {
|
|
145
|
-
if (node.id) declaredNames.add(node.id.name);
|
|
146
|
-
},
|
|
147
|
-
ImportSpecifier(node: acorn.ImportSpecifier) {
|
|
148
|
-
declaredNames.add(node.local.name);
|
|
149
|
-
},
|
|
150
|
-
ImportDefaultSpecifier(node: acorn.ImportDefaultSpecifier) {
|
|
151
|
-
declaredNames.add(node.local.name);
|
|
152
|
-
},
|
|
153
|
-
ImportNamespaceSpecifier(node: acorn.ImportNamespaceSpecifier) {
|
|
154
|
-
declaredNames.add(node.local.name);
|
|
155
|
-
},
|
|
156
|
-
CatchClause(node: acorn.CatchClause) {
|
|
157
|
-
if (node.param) {
|
|
158
|
-
for (const name of extractBindingNames(node.param)) {
|
|
159
|
-
declaredNames.add(name);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// --- Pass 2: find Identifier nodes in reference position ---
|
|
166
|
-
// Also detects MemberExpressions like `globalThis.X` / `global.X` /
|
|
167
|
-
// `window.X` / `self.X` where X is a known global. esbuild's `define`
|
|
168
|
-
// config replaces `global`/`window` with `globalThis`, but we accept
|
|
169
|
-
// all four host-object names for safety (esbuild also never renames
|
|
170
|
-
// these because they are language keywords / pre-defined globals).
|
|
171
|
-
const freeGlobals = new Set<string>();
|
|
172
|
-
const HOST_OBJECTS = new Set(['globalThis', 'global', 'window', 'self', 'globalObject']);
|
|
173
|
-
|
|
174
|
-
walk.ancestor(ast, {
|
|
175
|
-
MemberExpression(node: acorn.MemberExpression) {
|
|
176
|
-
// Only dot-access — skip computed (bracket) access since the
|
|
177
|
-
// property is then a dynamic Expression, not a known name.
|
|
178
|
-
if (node.computed) return;
|
|
179
|
-
if (node.object.type !== 'Identifier') return;
|
|
180
|
-
if (node.property.type !== 'Identifier') return;
|
|
181
|
-
|
|
182
|
-
const objName = (node.object as acorn.Identifier).name;
|
|
183
|
-
const propName = (node.property as acorn.Identifier).name;
|
|
184
|
-
|
|
185
|
-
// Pattern A: globalThis.X / global.X / window.X / self.X
|
|
186
|
-
// The property is a known global identifier itself.
|
|
187
|
-
if (HOST_OBJECTS.has(objName)) {
|
|
188
|
-
if (KNOWN_GLOBALS.has(propName)) {
|
|
189
|
-
freeGlobals.add(propName);
|
|
190
|
-
}
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Pattern B: known-instance method markers like
|
|
195
|
-
// `navigator.getGamepads` → marker map forwards to a global
|
|
196
|
-
// identifier that triggers the right register path even though
|
|
197
|
-
// the identifier itself never appears in the bundle.
|
|
198
|
-
const markerKey = `${objName}.${propName}`;
|
|
199
|
-
const markerTarget = METHOD_MARKERS[markerKey];
|
|
200
|
-
if (markerTarget && KNOWN_GLOBALS.has(markerTarget)) {
|
|
201
|
-
freeGlobals.add(markerTarget);
|
|
202
|
-
}
|
|
203
|
-
},
|
|
204
|
-
Identifier(node: acorn.Identifier, ancestors: acorn.AnyNode[]) {
|
|
205
|
-
const name = node.name;
|
|
206
|
-
|
|
207
|
-
// Quick filter: only check known globals
|
|
208
|
-
if (!KNOWN_GLOBALS.has(name)) return;
|
|
209
|
-
|
|
210
|
-
// Skip if locally declared
|
|
211
|
-
if (declaredNames.has(name)) return;
|
|
212
|
-
|
|
213
|
-
// Determine if this Identifier is in a reference position
|
|
214
|
-
// by checking the parent node.
|
|
215
|
-
const parent = ancestors[ancestors.length - 2];
|
|
216
|
-
if (!parent) {
|
|
217
|
-
freeGlobals.add(name);
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
switch (parent.type) {
|
|
222
|
-
// obj.prop — skip if this is the non-computed property
|
|
223
|
-
case 'MemberExpression': {
|
|
224
|
-
const mem = parent as acorn.MemberExpression;
|
|
225
|
-
if (mem.property === (node as acorn.AnyNode) && !mem.computed) return;
|
|
226
|
-
break;
|
|
227
|
-
}
|
|
228
|
-
// { key: value } — skip if this is the non-computed key
|
|
229
|
-
case 'Property': {
|
|
230
|
-
const prop = parent as acorn.Property;
|
|
231
|
-
if (prop.key === (node as acorn.AnyNode) && !prop.computed) return;
|
|
232
|
-
break;
|
|
233
|
-
}
|
|
234
|
-
// Method/property definitions in classes
|
|
235
|
-
case 'MethodDefinition':
|
|
236
|
-
case 'PropertyDefinition': {
|
|
237
|
-
const def = parent as acorn.MethodDefinition | acorn.PropertyDefinition;
|
|
238
|
-
if (def.key === (node as acorn.AnyNode) && !def.computed) return;
|
|
239
|
-
break;
|
|
240
|
-
}
|
|
241
|
-
// label: — skip
|
|
242
|
-
case 'LabeledStatement': {
|
|
243
|
-
const labeled = parent as acorn.LabeledStatement;
|
|
244
|
-
if (labeled.label === (node as acorn.AnyNode)) return;
|
|
245
|
-
break;
|
|
246
|
-
}
|
|
247
|
-
// export { X as Y } — skip the exported name
|
|
248
|
-
case 'ExportSpecifier': {
|
|
249
|
-
const spec = parent as acorn.ExportSpecifier;
|
|
250
|
-
if (spec.exported === (node as acorn.AnyNode)) return;
|
|
251
|
-
break;
|
|
252
|
-
}
|
|
253
|
-
// Declaration ids (function name, class name, variable id)
|
|
254
|
-
// are already in declaredNames, but guard anyway
|
|
255
|
-
case 'FunctionDeclaration':
|
|
256
|
-
case 'FunctionExpression':
|
|
257
|
-
case 'ClassDeclaration':
|
|
258
|
-
case 'ClassExpression': {
|
|
259
|
-
const decl = parent as
|
|
260
|
-
| acorn.FunctionDeclaration
|
|
261
|
-
| acorn.FunctionExpression
|
|
262
|
-
| acorn.ClassDeclaration
|
|
263
|
-
| acorn.ClassExpression;
|
|
264
|
-
if (decl.id === (node as acorn.AnyNode)) return;
|
|
265
|
-
break;
|
|
266
|
-
}
|
|
267
|
-
case 'VariableDeclarator': {
|
|
268
|
-
const vd = parent as acorn.VariableDeclarator;
|
|
269
|
-
if (vd.id === (node as acorn.AnyNode)) return;
|
|
270
|
-
break;
|
|
271
|
-
}
|
|
272
|
-
// import { X } / import X — already in declaredNames
|
|
273
|
-
case 'ImportSpecifier':
|
|
274
|
-
case 'ImportDefaultSpecifier':
|
|
275
|
-
case 'ImportNamespaceSpecifier':
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
freeGlobals.add(name);
|
|
280
|
-
},
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
return freeGlobals;
|
|
284
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// Glob expansion for Rolldown entry-point input.
|
|
2
|
-
//
|
|
3
|
-
// Rolldown's `input` accepts:
|
|
4
|
-
// - a single path string
|
|
5
|
-
// - an array of strings
|
|
6
|
-
// - a record mapping output names to input paths
|
|
7
|
-
//
|
|
8
|
-
// `globToEntryPoints` accepts the same shapes and expands any glob patterns
|
|
9
|
-
// against the filesystem via `fast-glob`. Pure-string entries return as-is
|
|
10
|
-
// when they don't contain wildcards (fast-glob handles that gracefully).
|
|
11
|
-
//
|
|
12
|
-
// `.d.ts` files are always excluded — they are type-only declarations,
|
|
13
|
-
// not parseable as runtime modules. esbuild handled this implicitly via
|
|
14
|
-
// its loader table; Rolldown's Oxc parser errors on declaration-only
|
|
15
|
-
// shapes (`get foo(): T;`).
|
|
16
|
-
|
|
17
|
-
import fastGlob from 'fast-glob';
|
|
18
|
-
|
|
19
|
-
export type EntryPoints = string | string[] | Record<string, string>;
|
|
20
|
-
|
|
21
|
-
const DEFAULT_IGNORE = ['**/*.d.ts'];
|
|
22
|
-
|
|
23
|
-
export const globToEntryPoints = async (
|
|
24
|
-
_entryPoints: EntryPoints | undefined,
|
|
25
|
-
ignore: string[] = [],
|
|
26
|
-
): Promise<EntryPoints | undefined> => {
|
|
27
|
-
if (_entryPoints === undefined) return undefined;
|
|
28
|
-
const fullIgnore = [...DEFAULT_IGNORE, ...ignore];
|
|
29
|
-
|
|
30
|
-
if (typeof _entryPoints === 'string') {
|
|
31
|
-
const expanded = await fastGlob([_entryPoints], { ignore: fullIgnore });
|
|
32
|
-
return expanded;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (Array.isArray(_entryPoints)) {
|
|
36
|
-
return await fastGlob(_entryPoints, { ignore: fullIgnore });
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const entryPoints: Record<string, string> = {};
|
|
40
|
-
for (const input in _entryPoints) {
|
|
41
|
-
const output = _entryPoints[input];
|
|
42
|
-
const inputs = await fastGlob(input, { ignore: fullIgnore });
|
|
43
|
-
for (const matched of inputs) {
|
|
44
|
-
entryPoints[matched] = output;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return entryPoints;
|
|
48
|
-
};
|
package/src/utils/extension.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export const getJsExtensions = (allowExt?: string) => {
|
|
2
|
-
const extensions: Record<string, string> = {'.js': '.js', '.ts': '.js', '.mts': '.js', '.cts': '.js', '.cjs': '.js', '.mjs': '.js'};
|
|
3
|
-
if(allowExt && extensions[allowExt]) {
|
|
4
|
-
delete extensions[allowExt]
|
|
5
|
-
}
|
|
6
|
-
return extensions;
|
|
7
|
-
}
|
package/src/utils/index.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export * from './alias.js';
|
|
2
|
-
export * from './entry-points.js';
|
|
3
|
-
export * from './extension.js';
|
|
4
|
-
export * from './merge.js';
|
|
5
|
-
export { detectFreeGlobals } from './detect-free-globals.js';
|
|
6
|
-
export { resolveGlobalsList, writeRegisterInjectFile } from './scan-globals.js';
|
|
7
|
-
export { inlineStaticReads } from './inline-static-reads.js';
|