@gjsify/cli 0.3.12 → 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.
@@ -1,16 +1,17 @@
1
- import type { ConfigData } from "../types/index.js";
2
- import type { App } from "@gjsify/esbuild-plugin-gjsify";
3
- import { build, BuildOptions, BuildResult, Plugin } from "esbuild";
4
- import { gjsifyPlugin } from "@gjsify/esbuild-plugin-gjsify";
1
+ import type { ConfigData, BundlerOptions } from "../types/index.js";
2
+ import type { App, PluginOptions } from "@gjsify/rolldown-plugin-gjsify";
3
+ import type { RolldownOutput, RolldownPluginOption } from "rolldown";
4
+ import { rolldown } from "rolldown";
5
+ import { gjsifyPlugin } from "@gjsify/rolldown-plugin-gjsify";
5
6
  import {
6
- resolveGlobalsList,
7
- writeRegisterInjectFile,
8
- detectAutoGlobals,
9
- } from "@gjsify/esbuild-plugin-gjsify/globals";
10
- import { getBundleDir, rewriteContents } from "@gjsify/esbuild-plugin-gjsify";
11
- import { getPnpPlugin } from "@gjsify/resolve-npm/pnp-relay";
7
+ resolveGlobalsList,
8
+ writeRegisterInjectFile,
9
+ detectAutoGlobals,
10
+ } from "@gjsify/rolldown-plugin-gjsify/globals";
11
+ import { pnpPlugin } from "@gjsify/rolldown-plugin-pnp";
12
12
  import { dirname, extname } from "node:path";
13
13
  import { chmod, readFile, writeFile } from "node:fs/promises";
14
+ import { normalizeBundlerOptions, mergeBundlerOptions } from "../utils/normalize-bundler-options.js";
14
15
 
15
16
  const GJS_SHEBANG = "#!/usr/bin/env -S gjs -m\n";
16
17
 
@@ -21,10 +22,10 @@ const GJS_SHEBANG = "#!/usr/bin/env -S gjs -m\n";
21
22
  * - paths that live under a `src/` segment (relative or absolute)
22
23
  */
23
24
  function isUnsafeDefaultOutput(path: string): boolean {
24
- if (/\.[cm]?tsx?$/i.test(path)) return true;
25
- const norm = path.replace(/\\/g, "/");
26
- if (/(?:^|\/)src\//.test(norm)) return true;
27
- return false;
25
+ if (/\.[cm]?tsx?$/i.test(path)) return true;
26
+ const norm = path.replace(/\\/g, "/");
27
+ if (/(?:^|\/)src\//.test(norm)) return true;
28
+ return false;
28
29
  }
29
30
 
30
31
  /**
@@ -33,345 +34,365 @@ function isUnsafeDefaultOutput(path: string): boolean {
33
34
  * @gjsify/{node,web}-polyfills) are resolvable for external consumers without
34
35
  * each one having to be a direct devDep.
35
36
  *
36
- * Wires the @gjsify/esbuild-plugin-gjsify rewriter (`__filename`/`__dirname`
37
- * injection for CJS code in node_modules) into the pnp plugin's onLoad
38
- * esbuild stops at the first matching onLoad, so the rewriter MUST run from
39
- * inside the pnp plugin's onLoad rather than as a separate registration.
37
+ * The path rewriter (`__filename`/`__dirname` + `import.meta.url` injection
38
+ * for node_modules code) is registered separately by the orchestrator
39
+ * Rolldown's transform hooks all run sequentially, no shared `onLoad` race.
40
40
  */
41
- async function buildPnpPlugin(): Promise<Plugin | null> {
42
- return getPnpPlugin({
43
- issuerUrl: import.meta.url,
44
- transformContentsFactory: (build) => {
45
- const bundleDir = getBundleDir(build);
46
- return (args, contents) => rewriteContents(args, contents, bundleDir);
47
- },
48
- });
41
+ async function buildPnpPlugin(): Promise<RolldownPluginOption | null> {
42
+ return pnpPlugin({ issuerUrl: import.meta.url });
49
43
  }
50
44
 
51
45
  export class BuildAction {
52
- constructor(readonly configData: ConfigData = {}) {}
53
-
54
- getEsBuildDefaults() {
55
- const defaults: BuildOptions = {
56
- allowOverwrite: true,
57
- };
58
- return defaults;
59
- }
60
-
61
- /** Library mode */
62
- async buildLibrary() {
63
- let { verbose, library, esbuild, typescript, exclude } = this.configData;
64
- library ||= {};
65
- esbuild ||= {};
66
- typescript ||= {};
67
-
68
- const moduleOutdir = library?.module ? dirname(library.module) : undefined;
69
- const mainOutdir = library?.main ? dirname(library.main) : undefined;
70
-
71
- const moduleOutExt = library.module ? extname(library.module) : ".js";
72
- const mainOutExt = library.main ? extname(library.main) : ".js";
73
-
74
- const multipleBuilds =
75
- moduleOutdir && mainOutdir && moduleOutdir !== mainOutdir;
76
-
77
- const pnpPlugin = await buildPnpPlugin();
78
- const pnpPlugins: Plugin[] = pnpPlugin ? [pnpPlugin] : [];
79
-
80
- const results: BuildResult[] = [];
81
-
82
- if (multipleBuilds) {
83
- const moduleFormat =
84
- moduleOutdir.includes("/cjs") || moduleOutExt === ".cjs"
85
- ? "cjs"
86
- : "esm";
87
- results.push(
88
- await build({
89
- ...this.getEsBuildDefaults(),
90
- ...esbuild,
91
- format: moduleFormat,
92
- outdir: moduleOutdir,
93
- plugins: [
94
- ...pnpPlugins,
95
- gjsifyPlugin({
96
- debug: verbose,
97
- library: moduleFormat,
98
- exclude,
99
- reflection: typescript?.reflection,
100
- jsExtension: moduleOutExt,
101
- }),
102
- ],
103
- }),
104
- );
105
-
106
- const mainFormat =
107
- mainOutdir.includes("/cjs") || mainOutExt === ".cjs" ? "cjs" : "esm";
108
- results.push(
109
- await build({
110
- ...this.getEsBuildDefaults(),
111
- ...esbuild,
112
- format: mainFormat,
113
- outdir: mainOutdir,
114
- plugins: [
115
- ...pnpPlugins,
116
- gjsifyPlugin({
117
- debug: verbose,
118
- library: mainFormat,
119
- exclude,
120
- reflection: typescript?.reflection,
121
- jsExtension: mainOutExt,
122
- }),
123
- ],
124
- }),
125
- );
126
- } else {
127
- const outfilePath = esbuild?.outfile || library?.module || library?.main;
128
- const outExt = outfilePath ? extname(outfilePath) : ".js";
129
- const outdir =
130
- esbuild?.outdir || (outfilePath ? dirname(outfilePath) : undefined);
131
- const format: "esm" | "cjs" =
132
- (esbuild?.format as "esm" | "cjs") ??
133
- (outdir?.includes("/cjs") || outExt === ".cjs" ? "cjs" : "esm");
134
- results.push(
135
- await build({
136
- ...this.getEsBuildDefaults(),
137
- ...esbuild,
138
- format,
139
- outdir,
140
- plugins: [
141
- ...pnpPlugins,
142
- gjsifyPlugin({
143
- debug: verbose,
144
- library: format,
145
- exclude,
146
- reflection: typescript?.reflection,
147
- jsExtension: outExt,
148
- }),
149
- ],
150
- }),
151
- );
152
- }
153
- return results;
154
- }
155
-
156
- /**
157
- * Parse the `--globals` value into { autoMode, extras }.
158
- * - `auto` → { autoMode: true, extras: '' }
159
- * - `auto,dom` → { autoMode: true, extras: 'dom' }
160
- * - `auto,dom,fetch` → { autoMode: true, extras: 'dom,fetch' }
161
- * - `dom,fetch` { autoMode: false, extras: 'dom,fetch' }
162
- * - `none` / `` → { autoMode: false, extras: '' }
163
- * - `undefined` → { autoMode: true, extras: '' } (default)
164
- */
165
- private parseGlobalsValue(value: string | undefined): {
166
- autoMode: boolean;
167
- extras: string;
168
- } {
169
- if (value === undefined) return { autoMode: true, extras: "" };
170
- if (value === "none" || value === "")
171
- return { autoMode: false, extras: "" };
172
-
173
- const tokens = value
174
- .split(",")
175
- .map((t) => t.trim())
176
- .filter(Boolean);
177
- const hasAuto = tokens.includes("auto");
178
- const extras = tokens.filter((t) => t !== "auto").join(",");
179
-
180
- return { autoMode: hasAuto, extras };
181
- }
182
-
183
- /**
184
- * Resolve the `--globals` CLI list into a pre-computed inject stub path
185
- * that the esbuild plugin will append to its `inject` list. Only runs
186
- * for `--app gjs` — Node and browser builds rely on native globals.
187
- *
188
- * Used only for the explicit-only path (no `auto` token in the value).
189
- * The auto path is handled in `buildApp` via the two-pass build.
190
- */
191
- private async resolveGlobalsInject(
192
- app: App,
193
- globals: string,
194
- verbose: boolean | undefined,
195
- ): Promise<string | undefined> {
196
- if (app !== "gjs") return undefined;
197
- if (!globals) return undefined;
198
-
199
- const registerPaths = resolveGlobalsList(globals);
200
- if (registerPaths.size === 0) return undefined;
201
-
202
- const injectPath = await writeRegisterInjectFile(
203
- registerPaths,
204
- process.cwd(),
205
- );
206
- if (verbose && injectPath) {
207
- console.debug(
208
- `[gjsify] globals: injected ${registerPaths.size} register module(s) from --globals ${globals}`,
209
- );
210
- }
211
- return injectPath ?? undefined;
212
- }
213
-
214
- /**
215
- * Post-processing: prepend GJS shebang and mark the output file executable.
216
- * Only runs for GJS app builds with a resolvable single outfile.
217
- */
218
- private async applyShebang(
219
- outfile: string | undefined,
220
- verbose: boolean | undefined,
221
- ): Promise<void> {
222
- if (!outfile) {
223
- if (verbose)
224
- console.warn(
225
- "[gjsify] --shebang skipped: no single outfile (use --outfile for GJS executables)",
226
- );
227
- return;
228
- }
229
-
230
- const content = await readFile(outfile, "utf-8");
231
- if (content.startsWith("#!")) {
232
- if (verbose)
233
- console.debug(
234
- `[gjsify] --shebang skipped: ${outfile} already starts with a shebang`,
235
- );
236
- } else {
237
- await writeFile(outfile, GJS_SHEBANG + content);
238
- }
239
- await chmod(outfile, 0o755);
240
- if (verbose)
241
- console.debug(
242
- `[gjsify] --shebang: wrote shebang + chmod 0o755 to ${outfile}`,
243
- );
244
- }
245
-
246
- /** Application mode */
247
- async buildApp(app: App = "gjs") {
248
- const {
249
- verbose,
250
- esbuild,
251
- typescript,
252
- exclude,
253
- library: pgk,
254
- aliases,
255
- excludeGlobals,
256
- } = this.configData;
257
-
258
- const format: "esm" | "cjs" =
259
- (esbuild?.format as "esm" | "cjs") ??
260
- (esbuild?.outfile?.endsWith(".cjs") ? "cjs" : "esm");
261
-
262
- // Set default outfile if no outdir is set
263
- if (
264
- esbuild &&
265
- !esbuild?.outfile &&
266
- !esbuild?.outdir &&
267
- (pgk?.main || pgk?.module)
268
- ) {
269
- const candidate =
270
- esbuild?.format === "cjs"
271
- ? pgk.main || pgk.module
272
- : pgk.module || pgk.main;
273
- if (candidate && isUnsafeDefaultOutput(candidate)) {
274
- // `package.json#main`/`module` commonly points at a TypeScript
275
- // source (e.g. `src/index.ts` for TS-direct workflows). Falling
276
- // back to that value would have esbuild OVERWRITE the source.
277
- // Surface a clear error and require an explicit outfile/outdir
278
- // instead of silently destroying the user's code.
279
- throw new Error(
280
- `gjsify build: refusing to default --outfile to ${candidate} ` +
281
- `(would overwrite a TypeScript source file). Pass --outfile/--outdir ` +
282
- `explicitly, or set "gjsify.esbuild.outfile" in package.json.`,
283
- );
284
- }
285
- esbuild.outfile = candidate;
286
- }
287
-
288
- const { consoleShim, globals } = this.configData;
289
-
290
- const pluginOpts = {
291
- debug: verbose,
292
- app,
293
- format,
294
- exclude,
295
- reflection: typescript?.reflection,
296
- consoleShim,
297
- ...(aliases ? { aliases } : {}),
298
- };
299
-
300
- const { autoMode, extras } = this.parseGlobalsValue(globals);
301
-
302
- const pnpPlugin = await buildPnpPlugin();
303
- const pnpPlugins: Plugin[] = pnpPlugin ? [pnpPlugin] : [];
304
-
305
- // --- Auto mode (with optional extras): iterative multi-pass build ---
306
- // The extras token is used for cases where the detector cannot
307
- // statically see a global (e.g. Excalibur indirects globalThis via
308
- // BrowserComponent.nativeComponent). Common pattern: --globals auto,dom
309
- if (app === "gjs" && autoMode) {
310
- const { injectPath } = await detectAutoGlobals(
311
- {
312
- ...this.getEsBuildDefaults(),
313
- ...esbuild,
314
- format,
315
- plugins: pnpPlugins,
316
- },
317
- pluginOpts,
318
- verbose,
319
- { extraGlobalsList: extras, excludeGlobals },
320
- );
321
-
322
- const result = await build({
323
- ...this.getEsBuildDefaults(),
324
- ...esbuild,
325
- format,
326
- plugins: [
327
- ...pnpPlugins,
328
- gjsifyPlugin({
329
- ...pluginOpts,
330
- autoGlobalsInject: injectPath,
331
- }),
332
- ],
333
- });
334
-
335
- if (app === "gjs" && this.configData.shebang) {
336
- await this.applyShebang(esbuild?.outfile, verbose);
337
- }
338
-
339
- return [result];
340
- }
341
-
342
- // --- Explicit list (no `auto` token) or none mode ---
343
- const autoGlobalsInject = extras
344
- ? await this.resolveGlobalsInject(app, extras, verbose)
345
- : undefined;
346
-
347
- const result = await build({
348
- ...this.getEsBuildDefaults(),
349
- ...esbuild,
350
- format,
351
- plugins: [
352
- ...pnpPlugins,
353
- gjsifyPlugin({
354
- ...pluginOpts,
355
- autoGlobalsInject,
356
- }),
357
- ],
358
- });
359
-
360
- if (app === "gjs" && this.configData.shebang) {
361
- await this.applyShebang(esbuild?.outfile, verbose);
362
- }
363
-
364
- return [result];
365
- }
366
-
367
- async start(buildType: { library?: boolean; app?: App } = { app: "gjs" }) {
368
- const results: BuildResult[] = [];
369
- if (buildType.library) {
370
- results.push(...(await this.buildLibrary()));
371
- } else {
372
- results.push(...(await this.buildApp(buildType.app)));
373
- }
374
-
375
- return results;
376
- }
46
+ constructor(readonly configData: ConfigData = {}) {}
47
+
48
+ /** Library mode */
49
+ async buildLibrary(): Promise<RolldownOutput[]> {
50
+ const { verbose, library, typescript, exclude, aliases } = this.configData;
51
+ const lib = library ?? {};
52
+ const userBundler = normalizeBundlerOptions(this.configData);
53
+
54
+ const moduleOutdir = lib.module ? dirname(lib.module) : undefined;
55
+ const mainOutdir = lib.main ? dirname(lib.main) : undefined;
56
+
57
+ const moduleOutExt = lib.module ? extname(lib.module) : ".js";
58
+ const mainOutExt = lib.main ? extname(lib.main) : ".js";
59
+
60
+ const multipleBuilds =
61
+ moduleOutdir && mainOutdir && moduleOutdir !== mainOutdir;
62
+
63
+ const pnp = await buildPnpPlugin();
64
+ const pnpPlugins: RolldownPluginOption[] = pnp ? [pnp] : [];
65
+
66
+ const results: RolldownOutput[] = [];
67
+
68
+ if (multipleBuilds) {
69
+ const moduleFormat: "esm" | "cjs" =
70
+ moduleOutdir.includes("/cjs") || moduleOutExt === ".cjs"
71
+ ? "cjs"
72
+ : "esm";
73
+ results.push(
74
+ await runOneLibraryBuild({
75
+ pluginOpts: {
76
+ debug: verbose,
77
+ library: moduleFormat,
78
+ exclude,
79
+ reflection: typescript?.reflection,
80
+ jsExtension: moduleOutExt,
81
+ },
82
+ userBundler,
83
+ output: { dir: moduleOutdir },
84
+ userAliases: aliases,
85
+ pnpPlugins,
86
+ }),
87
+ );
88
+
89
+ const mainFormat: "esm" | "cjs" =
90
+ mainOutdir.includes("/cjs") || mainOutExt === ".cjs" ? "cjs" : "esm";
91
+ results.push(
92
+ await runOneLibraryBuild({
93
+ pluginOpts: {
94
+ debug: verbose,
95
+ library: mainFormat,
96
+ exclude,
97
+ reflection: typescript?.reflection,
98
+ jsExtension: mainOutExt,
99
+ },
100
+ userBundler,
101
+ output: { dir: mainOutdir },
102
+ userAliases: aliases,
103
+ pnpPlugins,
104
+ }),
105
+ );
106
+ } else {
107
+ const outfilePath =
108
+ userBundler.output?.file ?? lib.module ?? lib.main;
109
+ const outExt = outfilePath ? extname(outfilePath) : ".js";
110
+ const outdir =
111
+ userBundler.output?.dir ?? (outfilePath ? dirname(outfilePath) : undefined);
112
+ const format: "esm" | "cjs" =
113
+ (userBundler.output?.format as "esm" | "cjs" | undefined) ??
114
+ (outdir?.includes("/cjs") || outExt === ".cjs" ? "cjs" : "esm");
115
+ results.push(
116
+ await runOneLibraryBuild({
117
+ pluginOpts: {
118
+ debug: verbose,
119
+ library: format,
120
+ exclude,
121
+ reflection: typescript?.reflection,
122
+ jsExtension: outExt,
123
+ },
124
+ userBundler,
125
+ output: { dir: outdir },
126
+ userAliases: aliases,
127
+ pnpPlugins,
128
+ }),
129
+ );
130
+ }
131
+ return results;
132
+ }
133
+
134
+ /**
135
+ * Parse the `--globals` value into { autoMode, extras }.
136
+ * - `auto` → { autoMode: true, extras: '' }
137
+ * - `auto,dom` → { autoMode: true, extras: 'dom' }
138
+ * - `auto,dom,fetch` → { autoMode: true, extras: 'dom,fetch' }
139
+ * - `dom,fetch` → { autoMode: false, extras: 'dom,fetch' }
140
+ * - `none` / `` → { autoMode: false, extras: '' }
141
+ * - `undefined` → { autoMode: true, extras: '' } (default)
142
+ */
143
+ private parseGlobalsValue(value: string | undefined): {
144
+ autoMode: boolean;
145
+ extras: string;
146
+ } {
147
+ if (value === undefined) return { autoMode: true, extras: "" };
148
+ if (value === "none" || value === "")
149
+ return { autoMode: false, extras: "" };
150
+
151
+ const tokens = value
152
+ .split(",")
153
+ .map((t) => t.trim())
154
+ .filter(Boolean);
155
+ const hasAuto = tokens.includes("auto");
156
+ const extras = tokens.filter((t) => t !== "auto").join(",");
157
+
158
+ return { autoMode: hasAuto, extras };
159
+ }
160
+
161
+ /**
162
+ * Resolve the `--globals` CLI list into a pre-computed inject stub path
163
+ * that the orchestrator appends to its input list. Only runs for
164
+ * `--app gjs` Node and browser builds rely on native globals.
165
+ *
166
+ * Used only for the explicit-only path (no `auto` token in the value).
167
+ * The auto path is handled in `buildApp` via the iterative multi-pass build.
168
+ */
169
+ private async resolveGlobalsInject(
170
+ app: App,
171
+ globals: string,
172
+ verbose: boolean | undefined,
173
+ ): Promise<string | undefined> {
174
+ if (app !== "gjs") return undefined;
175
+ if (!globals) return undefined;
176
+
177
+ const registerPaths = resolveGlobalsList(globals);
178
+ if (registerPaths.size === 0) return undefined;
179
+
180
+ const injectPath = await writeRegisterInjectFile(
181
+ registerPaths,
182
+ process.cwd(),
183
+ );
184
+ if (verbose && injectPath) {
185
+ console.debug(
186
+ `[gjsify] globals: injected ${registerPaths.size} register module(s) from --globals ${globals}`,
187
+ );
188
+ }
189
+ return injectPath ?? undefined;
190
+ }
191
+
192
+ /**
193
+ * Post-processing: prepend GJS shebang and mark the output file executable.
194
+ * Only runs for GJS app builds with a resolvable single outfile.
195
+ */
196
+ private async applyShebang(
197
+ outfile: string | undefined,
198
+ verbose: boolean | undefined,
199
+ ): Promise<void> {
200
+ if (!outfile) {
201
+ if (verbose)
202
+ console.warn(
203
+ "[gjsify] --shebang skipped: no single outfile (use --outfile for GJS executables)",
204
+ );
205
+ return;
206
+ }
207
+
208
+ const content = await readFile(outfile, "utf-8");
209
+ if (content.startsWith("#!")) {
210
+ if (verbose)
211
+ console.debug(
212
+ `[gjsify] --shebang skipped: ${outfile} already starts with a shebang`,
213
+ );
214
+ } else {
215
+ await writeFile(outfile, GJS_SHEBANG + content);
216
+ }
217
+ await chmod(outfile, 0o755);
218
+ if (verbose)
219
+ console.debug(
220
+ `[gjsify] --shebang: wrote shebang + chmod 0o755 to ${outfile}`,
221
+ );
222
+ }
223
+
224
+ /** Application mode */
225
+ async buildApp(app: App = "gjs"): Promise<RolldownOutput[]> {
226
+ const {
227
+ verbose,
228
+ typescript,
229
+ exclude,
230
+ library: pkg,
231
+ aliases,
232
+ excludeGlobals,
233
+ } = this.configData;
234
+
235
+ const userBundler = normalizeBundlerOptions(this.configData);
236
+
237
+ const formatRaw =
238
+ (userBundler.output?.format as "esm" | "cjs" | "iife" | undefined) ??
239
+ (userBundler.output?.file?.endsWith(".cjs") ? "cjs" : "esm");
240
+ // The orchestrator only handles esm/cjs (iife is not a GJS / Node /
241
+ // browser-bundle target we support). Coerce.
242
+ const format: "esm" | "cjs" = formatRaw === "iife" ? "esm" : formatRaw;
243
+
244
+ // Set default outfile if no outdir is set
245
+ let outfile = userBundler.output?.file;
246
+ let outdir = userBundler.output?.dir;
247
+ if (!outfile && !outdir && (pkg?.main || pkg?.module)) {
248
+ const candidate =
249
+ format === "cjs"
250
+ ? pkg.main ?? pkg.module
251
+ : pkg.module ?? pkg.main;
252
+ if (candidate && isUnsafeDefaultOutput(candidate)) {
253
+ throw new Error(
254
+ `gjsify build: refusing to default --outfile to ${candidate} ` +
255
+ `(would overwrite a TypeScript source file). Pass --outfile/--outdir ` +
256
+ `explicitly, or set "gjsify.bundler.output.file" in package.json.`,
257
+ );
258
+ }
259
+ outfile = candidate;
260
+ }
261
+
262
+ const { consoleShim, globals } = this.configData;
263
+
264
+ const userExternal = Array.isArray(userBundler.external)
265
+ ? (userBundler.external as string[])
266
+ : undefined;
267
+ const userBanner = typeof userBundler.output?.banner === "string"
268
+ ? (userBundler.output.banner as string)
269
+ : undefined;
270
+
271
+ const pluginOpts: PluginOptions = {
272
+ debug: verbose,
273
+ app,
274
+ format,
275
+ exclude,
276
+ reflection: typescript?.reflection,
277
+ consoleShim,
278
+ ...(aliases ? { aliases } : {}),
279
+ };
280
+
281
+ const { autoMode, extras } = this.parseGlobalsValue(globals);
282
+
283
+ const pnp = await buildPnpPlugin();
284
+ const pnpPlugins: RolldownPluginOption[] = pnp ? [pnp] : [];
285
+
286
+ // --- Auto mode (with optional extras): iterative multi-pass build ---
287
+ if (app === "gjs" && autoMode) {
288
+ const gjsifyPluginFactory = async (opts: PluginOptions) => {
289
+ const cfg = await gjsifyPlugin(
290
+ {
291
+ input: userBundler.input,
292
+ output: { file: outfile, dir: outdir },
293
+ userExternal,
294
+ userBanner,
295
+ userAliases: aliases,
296
+ shebang: this.configData.shebang,
297
+ },
298
+ opts,
299
+ );
300
+ return cfg.plugins;
301
+ };
302
+
303
+ const { injectPath } = await detectAutoGlobals(
304
+ {
305
+ input: userBundler.input,
306
+ plugins: pnpPlugins,
307
+ external: userBundler.external,
308
+ transform: userBundler.transform,
309
+ format,
310
+ },
311
+ pluginOpts,
312
+ gjsifyPluginFactory,
313
+ verbose,
314
+ { extraGlobalsList: extras, excludeGlobals },
315
+ );
316
+
317
+ pluginOpts.autoGlobalsInject = injectPath;
318
+ } else if (extras) {
319
+ pluginOpts.autoGlobalsInject = await this.resolveGlobalsInject(
320
+ app,
321
+ extras,
322
+ verbose,
323
+ );
324
+ }
325
+
326
+ // Final build: orchestrator → rolldown → write
327
+ const cfg = await gjsifyPlugin(
328
+ {
329
+ input: userBundler.input,
330
+ output: { file: outfile, dir: outdir },
331
+ userExternal,
332
+ userBanner,
333
+ userAliases: aliases,
334
+ shebang: this.configData.shebang,
335
+ },
336
+ pluginOpts,
337
+ );
338
+
339
+ const merged = mergeBundlerOptions(cfg.options as BundlerOptions, userBundler);
340
+ const finalOpts: BundlerOptions = {
341
+ ...merged,
342
+ plugins: [...pnpPlugins, ...cfg.plugins],
343
+ };
344
+
345
+ const build = await rolldown(finalOpts);
346
+ let writeResult: RolldownOutput;
347
+ try {
348
+ writeResult = await build.write(finalOpts.output ?? {});
349
+ } finally {
350
+ await build.close();
351
+ }
352
+
353
+ if (app === "gjs" && this.configData.shebang) {
354
+ await this.applyShebang(outfile, verbose);
355
+ }
356
+
357
+ return [writeResult];
358
+ }
359
+
360
+ async start(buildType: { library?: boolean; app?: App } = { app: "gjs" }) {
361
+ if (buildType.library) {
362
+ return await this.buildLibrary();
363
+ }
364
+ return await this.buildApp(buildType.app);
365
+ }
366
+ }
367
+
368
+ interface OneLibraryBuildArgs {
369
+ pluginOpts: PluginOptions;
370
+ userBundler: BundlerOptions;
371
+ output: { file?: string; dir?: string };
372
+ userAliases?: Record<string, string>;
373
+ pnpPlugins: RolldownPluginOption[];
374
+ }
375
+
376
+ async function runOneLibraryBuild(args: OneLibraryBuildArgs): Promise<RolldownOutput> {
377
+ const cfg = await gjsifyPlugin(
378
+ {
379
+ input: args.userBundler.input,
380
+ output: args.output,
381
+ userAliases: args.userAliases,
382
+ },
383
+ args.pluginOpts,
384
+ );
385
+
386
+ const merged = mergeBundlerOptions(cfg.options as BundlerOptions, args.userBundler);
387
+ const finalOpts: BundlerOptions = {
388
+ ...merged,
389
+ plugins: [...args.pnpPlugins, ...cfg.plugins],
390
+ };
391
+
392
+ const build = await rolldown(finalOpts);
393
+ try {
394
+ return await build.write(finalOpts.output ?? {});
395
+ } finally {
396
+ await build.close();
397
+ }
377
398
  }