@gjsify/cli 0.3.2 → 0.3.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.
@@ -1,6 +1,6 @@
1
- import type { ConfigData } from '../types/index.js';
2
- import type { App } from '@gjsify/esbuild-plugin-gjsify';
3
- import { BuildOptions, BuildResult, Plugin } from 'esbuild';
1
+ import type { ConfigData } from "../types/index.js";
2
+ import type { App } from "@gjsify/esbuild-plugin-gjsify";
3
+ import { BuildOptions, BuildResult, Plugin } from "esbuild";
4
4
  export declare class BuildAction {
5
5
  readonly configData: ConfigData;
6
6
  constructor(configData?: ConfigData);
@@ -1,9 +1,9 @@
1
- import { build } from 'esbuild';
2
- import { gjsifyPlugin } from '@gjsify/esbuild-plugin-gjsify';
3
- import { resolveGlobalsList, writeRegisterInjectFile, detectAutoGlobals } from '@gjsify/esbuild-plugin-gjsify/globals';
4
- import { dirname, extname, join } from 'node:path';
5
- import { chmod, readFile, writeFile } from 'node:fs/promises';
6
- import { existsSync } from 'node:fs';
1
+ import { build } from "esbuild";
2
+ import { gjsifyPlugin } from "@gjsify/esbuild-plugin-gjsify";
3
+ import { resolveGlobalsList, writeRegisterInjectFile, detectAutoGlobals, } from "@gjsify/esbuild-plugin-gjsify/globals";
4
+ import { dirname, extname, join } from "node:path";
5
+ import { chmod, readFile, writeFile } from "node:fs/promises";
6
+ import { existsSync } from "node:fs";
7
7
  const GJS_SHEBANG = "#!/usr/bin/env -S gjs -m\n";
8
8
  /** Walk up from dir until .pnp.cjs is found; return its directory or null. */
9
9
  function findPnpRoot(dir) {
@@ -21,13 +21,36 @@ function findPnpRoot(dir) {
21
21
  * If the current project uses Yarn PnP, return the official
22
22
  * @yarnpkg/esbuild-plugin-pnp plugin so esbuild can resolve
23
23
  * modules from zip archives without manual extraction.
24
+ *
25
+ * Custom onResolve: fall through on UNDECLARED_DEPENDENCY errors so the
26
+ * gjsify alias plugin can handle bare specifiers (e.g. `abort-controller`)
27
+ * that PnP can't resolve from the inject file's issuer context but that
28
+ * gjsify maps to `@gjsify/*` packages the project DOES have available.
24
29
  */
25
30
  async function getPnpPlugin() {
26
31
  if (!findPnpRoot(process.cwd()))
27
32
  return null;
28
33
  try {
29
34
  const { pnpPlugin } = await import("@yarnpkg/esbuild-plugin-pnp");
30
- return pnpPlugin();
35
+ return pnpPlugin({
36
+ onResolve: async (_args, { resolvedPath, error, watchFiles }) => {
37
+ if (resolvedPath !== null) {
38
+ return { namespace: "pnp", path: resolvedPath, watchFiles };
39
+ }
40
+ // UNDECLARED_DEPENDENCY: package exists transitively but isn't
41
+ // in the issuer's direct deps. Fall through so the gjsify alias
42
+ // plugin can resolve it (e.g. bare → @gjsify/* mappings).
43
+ if (error?.pnpCode ===
44
+ "UNDECLARED_DEPENDENCY") {
45
+ return null;
46
+ }
47
+ return {
48
+ external: true,
49
+ errors: error ? [{ text: error.message }] : [],
50
+ watchFiles,
51
+ };
52
+ },
53
+ });
31
54
  }
32
55
  catch {
33
56
  return null;
@@ -40,7 +63,7 @@ export class BuildAction {
40
63
  }
41
64
  getEsBuildDefaults() {
42
65
  const defaults = {
43
- allowOverwrite: true
66
+ allowOverwrite: true,
44
67
  };
45
68
  return defaults;
46
69
  }
@@ -59,7 +82,9 @@ export class BuildAction {
59
82
  const pnpPlugins = pnpPlugin ? [pnpPlugin] : [];
60
83
  const results = [];
61
84
  if (multipleBuilds) {
62
- const moduleFormat = moduleOutdir.includes("/cjs") || moduleOutExt === ".cjs" ? "cjs" : "esm";
85
+ const moduleFormat = moduleOutdir.includes("/cjs") || moduleOutExt === ".cjs"
86
+ ? "cjs"
87
+ : "esm";
63
88
  results.push(await build({
64
89
  ...this.getEsBuildDefaults(),
65
90
  ...esbuild,
@@ -67,7 +92,13 @@ export class BuildAction {
67
92
  outdir: moduleOutdir,
68
93
  plugins: [
69
94
  ...pnpPlugins,
70
- gjsifyPlugin({ debug: verbose, library: moduleFormat, exclude, reflection: typescript?.reflection, jsExtension: moduleOutExt }),
95
+ gjsifyPlugin({
96
+ debug: verbose,
97
+ library: moduleFormat,
98
+ exclude,
99
+ reflection: typescript?.reflection,
100
+ jsExtension: moduleOutExt,
101
+ }),
71
102
  ],
72
103
  }));
73
104
  const mainFormat = mainOutdir.includes("/cjs") || mainOutExt === ".cjs" ? "cjs" : "esm";
@@ -78,7 +109,13 @@ export class BuildAction {
78
109
  outdir: mainOutdir,
79
110
  plugins: [
80
111
  ...pnpPlugins,
81
- gjsifyPlugin({ debug: verbose, library: mainFormat, exclude, reflection: typescript?.reflection, jsExtension: mainOutdir }),
112
+ gjsifyPlugin({
113
+ debug: verbose,
114
+ library: mainFormat,
115
+ exclude,
116
+ reflection: typescript?.reflection,
117
+ jsExtension: mainOutdir,
118
+ }),
82
119
  ],
83
120
  }));
84
121
  }
@@ -86,7 +123,8 @@ export class BuildAction {
86
123
  const outfilePath = esbuild?.outfile || library?.module || library?.main;
87
124
  const outExt = outfilePath ? extname(outfilePath) : ".js";
88
125
  const outdir = esbuild?.outdir || (outfilePath ? dirname(outfilePath) : undefined);
89
- const format = esbuild?.format ?? (outdir?.includes("/cjs") || outExt === ".cjs" ? "cjs" : "esm");
126
+ const format = esbuild?.format ??
127
+ (outdir?.includes("/cjs") || outExt === ".cjs" ? "cjs" : "esm");
90
128
  results.push(await build({
91
129
  ...this.getEsBuildDefaults(),
92
130
  ...esbuild,
@@ -94,7 +132,13 @@ export class BuildAction {
94
132
  outdir,
95
133
  plugins: [
96
134
  ...pnpPlugins,
97
- gjsifyPlugin({ debug: verbose, library: format, exclude, reflection: typescript?.reflection, jsExtension: outExt }),
135
+ gjsifyPlugin({
136
+ debug: verbose,
137
+ library: format,
138
+ exclude,
139
+ reflection: typescript?.reflection,
140
+ jsExtension: outExt,
141
+ }),
98
142
  ],
99
143
  }));
100
144
  }
@@ -111,12 +155,15 @@ export class BuildAction {
111
155
  */
112
156
  parseGlobalsValue(value) {
113
157
  if (value === undefined)
114
- return { autoMode: true, extras: '' };
115
- if (value === 'none' || value === '')
116
- return { autoMode: false, extras: '' };
117
- const tokens = value.split(',').map(t => t.trim()).filter(Boolean);
118
- const hasAuto = tokens.includes('auto');
119
- const extras = tokens.filter(t => t !== 'auto').join(',');
158
+ return { autoMode: true, extras: "" };
159
+ if (value === "none" || value === "")
160
+ return { autoMode: false, extras: "" };
161
+ const tokens = value
162
+ .split(",")
163
+ .map((t) => t.trim())
164
+ .filter(Boolean);
165
+ const hasAuto = tokens.includes("auto");
166
+ const extras = tokens.filter((t) => t !== "auto").join(",");
120
167
  return { autoMode: hasAuto, extras };
121
168
  }
122
169
  /**
@@ -128,7 +175,7 @@ export class BuildAction {
128
175
  * The auto path is handled in `buildApp` via the two-pass build.
129
176
  */
130
177
  async resolveGlobalsInject(app, globals, verbose) {
131
- if (app !== 'gjs')
178
+ if (app !== "gjs")
132
179
  return undefined;
133
180
  if (!globals)
134
181
  return undefined;
@@ -148,11 +195,11 @@ export class BuildAction {
148
195
  async applyShebang(outfile, verbose) {
149
196
  if (!outfile) {
150
197
  if (verbose)
151
- console.warn('[gjsify] --shebang skipped: no single outfile (use --outfile for GJS executables)');
198
+ console.warn("[gjsify] --shebang skipped: no single outfile (use --outfile for GJS executables)");
152
199
  return;
153
200
  }
154
- const content = await readFile(outfile, 'utf-8');
155
- if (content.startsWith('#!')) {
201
+ const content = await readFile(outfile, "utf-8");
202
+ if (content.startsWith("#!")) {
156
203
  if (verbose)
157
204
  console.debug(`[gjsify] --shebang skipped: ${outfile} already starts with a shebang`);
158
205
  }
@@ -165,11 +212,18 @@ export class BuildAction {
165
212
  }
166
213
  /** Application mode */
167
214
  async buildApp(app = "gjs") {
168
- const { verbose, esbuild, typescript, exclude, library: pgk, aliases, excludeGlobals } = this.configData;
169
- const format = esbuild?.format ?? (esbuild?.outfile?.endsWith(".cjs") ? "cjs" : "esm");
215
+ const { verbose, esbuild, typescript, exclude, library: pgk, aliases, excludeGlobals, } = this.configData;
216
+ const format = esbuild?.format ??
217
+ (esbuild?.outfile?.endsWith(".cjs") ? "cjs" : "esm");
170
218
  // Set default outfile if no outdir is set
171
- if (esbuild && !esbuild?.outfile && !esbuild?.outdir && (pgk?.main || pgk?.module)) {
172
- esbuild.outfile = esbuild?.format === "cjs" ? pgk.main || pgk.module : pgk.module || pgk.main;
219
+ if (esbuild &&
220
+ !esbuild?.outfile &&
221
+ !esbuild?.outdir &&
222
+ (pgk?.main || pgk?.module)) {
223
+ esbuild.outfile =
224
+ esbuild?.format === "cjs"
225
+ ? pgk.main || pgk.module
226
+ : pgk.module || pgk.main;
173
227
  }
174
228
  const { consoleShim, globals } = this.configData;
175
229
  const pluginOpts = {
@@ -189,7 +243,12 @@ export class BuildAction {
189
243
  // statically see a global (e.g. Excalibur indirects globalThis via
190
244
  // BrowserComponent.nativeComponent). Common pattern: --globals auto,dom
191
245
  if (app === "gjs" && autoMode) {
192
- const { injectPath } = await detectAutoGlobals({ ...this.getEsBuildDefaults(), ...esbuild, format, plugins: pnpPlugins }, pluginOpts, verbose, { extraGlobalsList: extras, excludeGlobals });
246
+ const { injectPath } = await detectAutoGlobals({
247
+ ...this.getEsBuildDefaults(),
248
+ ...esbuild,
249
+ format,
250
+ plugins: pnpPlugins,
251
+ }, pluginOpts, verbose, { extraGlobalsList: extras, excludeGlobals });
193
252
  const result = await build({
194
253
  ...this.getEsBuildDefaults(),
195
254
  ...esbuild,
@@ -208,7 +267,9 @@ export class BuildAction {
208
267
  return [result];
209
268
  }
210
269
  // --- Explicit list (no `auto` token) or none mode ---
211
- const autoGlobalsInject = extras ? await this.resolveGlobalsInject(app, extras, verbose) : undefined;
270
+ const autoGlobalsInject = extras
271
+ ? await this.resolveGlobalsInject(app, extras, verbose)
272
+ : undefined;
212
273
  const result = await build({
213
274
  ...this.getEsBuildDefaults(),
214
275
  ...esbuild,
@@ -226,7 +287,7 @@ export class BuildAction {
226
287
  }
227
288
  return [result];
228
289
  }
229
- async start(buildType = { app: 'gjs' }) {
290
+ async start(buildType = { app: "gjs" }) {
230
291
  const results = [];
231
292
  if (buildType.library) {
232
293
  results.push(...(await this.buildLibrary()));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -23,15 +23,15 @@
23
23
  "cli"
24
24
  ],
25
25
  "dependencies": {
26
- "@gjsify/create-app": "^0.3.2",
27
- "@gjsify/esbuild-plugin-gjsify": "^0.3.2",
28
- "@gjsify/example-dom-canvas2d-fireworks": "^0.3.2",
29
- "@gjsify/example-dom-excalibur-jelly-jumper": "^0.3.2",
30
- "@gjsify/example-dom-three-geometry-teapot": "^0.3.2",
31
- "@gjsify/example-dom-three-postprocessing-pixel": "^0.3.2",
32
- "@gjsify/example-node-express-webserver": "^0.3.2",
33
- "@gjsify/node-polyfills": "^0.3.2",
34
- "@gjsify/web-polyfills": "^0.3.2",
26
+ "@gjsify/create-app": "^0.3.3",
27
+ "@gjsify/esbuild-plugin-gjsify": "^0.3.3",
28
+ "@gjsify/example-dom-canvas2d-fireworks": "^0.3.3",
29
+ "@gjsify/example-dom-excalibur-jelly-jumper": "^0.3.3",
30
+ "@gjsify/example-dom-three-geometry-teapot": "^0.3.3",
31
+ "@gjsify/example-dom-three-postprocessing-pixel": "^0.3.3",
32
+ "@gjsify/example-node-express-webserver": "^0.3.3",
33
+ "@gjsify/node-polyfills": "^0.3.3",
34
+ "@gjsify/web-polyfills": "^0.3.3",
35
35
  "@yarnpkg/esbuild-plugin-pnp": "^3.0.0-rc.15",
36
36
  "cosmiconfig": "^9.0.1",
37
37
  "esbuild": "^0.28.0",
@@ -1,280 +1,380 @@
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';
5
- import { resolveGlobalsList, writeRegisterInjectFile, detectAutoGlobals } from '@gjsify/esbuild-plugin-gjsify/globals';
6
- import { dirname, extname, join } from 'node:path';
7
- import { chmod, readFile, writeFile } from 'node:fs/promises';
8
- import { existsSync } from 'node:fs';
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";
5
+ import {
6
+ resolveGlobalsList,
7
+ writeRegisterInjectFile,
8
+ detectAutoGlobals,
9
+ } from "@gjsify/esbuild-plugin-gjsify/globals";
10
+ import { dirname, extname, join } from "node:path";
11
+ import { chmod, readFile, writeFile } from "node:fs/promises";
12
+ import { existsSync } from "node:fs";
9
13
 
10
14
  const GJS_SHEBANG = "#!/usr/bin/env -S gjs -m\n";
11
15
 
12
16
  /** Walk up from dir until .pnp.cjs is found; return its directory or null. */
13
17
  function findPnpRoot(dir: string): string | null {
14
- let current = dir;
15
- while (true) {
16
- if (existsSync(join(current, ".pnp.cjs"))) return current;
17
- const parent = dirname(current);
18
- if (parent === current) return null;
19
- current = parent;
20
- }
18
+ let current = dir;
19
+ while (true) {
20
+ if (existsSync(join(current, ".pnp.cjs"))) return current;
21
+ const parent = dirname(current);
22
+ if (parent === current) return null;
23
+ current = parent;
24
+ }
21
25
  }
22
26
 
23
27
  /**
24
28
  * If the current project uses Yarn PnP, return the official
25
29
  * @yarnpkg/esbuild-plugin-pnp plugin so esbuild can resolve
26
30
  * modules from zip archives without manual extraction.
31
+ *
32
+ * Custom onResolve: fall through on UNDECLARED_DEPENDENCY errors so the
33
+ * gjsify alias plugin can handle bare specifiers (e.g. `abort-controller`)
34
+ * that PnP can't resolve from the inject file's issuer context but that
35
+ * gjsify maps to `@gjsify/*` packages the project DOES have available.
27
36
  */
28
37
  async function getPnpPlugin(): Promise<Plugin | null> {
29
- if (!findPnpRoot(process.cwd())) return null;
30
- try {
31
- const { pnpPlugin } = await import("@yarnpkg/esbuild-plugin-pnp");
32
- return pnpPlugin();
33
- } catch {
34
- return null;
35
- }
38
+ if (!findPnpRoot(process.cwd())) return null;
39
+ try {
40
+ const { pnpPlugin } = await import("@yarnpkg/esbuild-plugin-pnp");
41
+ return pnpPlugin({
42
+ onResolve: async (_args, { resolvedPath, error, watchFiles }) => {
43
+ if (resolvedPath !== null) {
44
+ return { namespace: "pnp", path: resolvedPath, watchFiles };
45
+ }
46
+ // UNDECLARED_DEPENDENCY: package exists transitively but isn't
47
+ // in the issuer's direct deps. Fall through so the gjsify alias
48
+ // plugin can resolve it (e.g. bare → @gjsify/* mappings).
49
+ if (
50
+ (error as { pnpCode?: string } | null)?.pnpCode ===
51
+ "UNDECLARED_DEPENDENCY"
52
+ ) {
53
+ return null;
54
+ }
55
+ return {
56
+ external: true,
57
+ errors: error ? [{ text: error.message }] : [],
58
+ watchFiles,
59
+ };
60
+ },
61
+ });
62
+ } catch {
63
+ return null;
64
+ }
36
65
  }
37
66
 
38
67
  export class BuildAction {
39
- constructor(readonly configData: ConfigData = {}) {
40
-
41
- }
42
-
43
- getEsBuildDefaults() {
44
- const defaults: BuildOptions = {
45
- allowOverwrite: true
46
- }
47
- return defaults;
48
- }
49
-
50
- /** Library mode */
51
- async buildLibrary() {
52
- let { verbose, library, esbuild, typescript, exclude } = this.configData;
53
- library ||= {};
54
- esbuild ||= {};
55
- typescript ||= {};
56
-
57
- const moduleOutdir = library?.module ? dirname(library.module) : undefined;
58
- const mainOutdir = library?.main ? dirname(library.main) : undefined;
59
-
60
- const moduleOutExt = library.module ? extname(library.module) : ".js";
61
- const mainOutExt = library.main ? extname(library.main) : ".js";
62
-
63
- const multipleBuilds = moduleOutdir && mainOutdir && moduleOutdir !== mainOutdir;
64
-
65
- const pnpPlugin = await getPnpPlugin();
66
- const pnpPlugins: Plugin[] = pnpPlugin ? [pnpPlugin] : [];
67
-
68
- const results: BuildResult[] = [];
69
-
70
- if (multipleBuilds) {
71
- const moduleFormat = moduleOutdir.includes("/cjs") || moduleOutExt === ".cjs" ? "cjs" : "esm";
72
- results.push(
73
- await build({
74
- ...this.getEsBuildDefaults(),
75
- ...esbuild,
76
- format: moduleFormat,
77
- outdir: moduleOutdir,
78
- plugins: [
79
- ...pnpPlugins,
80
- gjsifyPlugin({ debug: verbose, library: moduleFormat, exclude, reflection: typescript?.reflection, jsExtension: moduleOutExt }),
81
- ],
82
- }),
83
- );
84
-
85
- const mainFormat = mainOutdir.includes("/cjs") || mainOutExt === ".cjs" ? "cjs" : "esm";
86
- results.push(
87
- await build({
88
- ...this.getEsBuildDefaults(),
89
- ...esbuild,
90
- format: moduleFormat,
91
- outdir: mainOutdir,
92
- plugins: [
93
- ...pnpPlugins,
94
- gjsifyPlugin({ debug: verbose, library: mainFormat, exclude, reflection: typescript?.reflection, jsExtension: mainOutdir }),
95
- ],
96
- }),
97
- );
98
- } else {
99
- const outfilePath = esbuild?.outfile || library?.module || library?.main;
100
- const outExt = outfilePath ? extname(outfilePath) : ".js";
101
- const outdir = esbuild?.outdir || (outfilePath ? dirname(outfilePath) : undefined);
102
- const format: "esm" | "cjs" = (esbuild?.format as "esm" | "cjs") ?? (outdir?.includes("/cjs") || outExt === ".cjs" ? "cjs" : "esm");
103
- results.push(
104
- await build({
105
- ...this.getEsBuildDefaults(),
106
- ...esbuild,
107
- format,
108
- outdir,
109
- plugins: [
110
- ...pnpPlugins,
111
- gjsifyPlugin({ debug: verbose, library: format, exclude, reflection: typescript?.reflection, jsExtension: outExt }),
112
- ],
113
- }),
114
- );
115
- }
116
- return results;
117
- }
118
-
119
- /**
120
- * Parse the `--globals` value into { autoMode, extras }.
121
- * - `auto` → { autoMode: true, extras: '' }
122
- * - `auto,dom` → { autoMode: true, extras: 'dom' }
123
- * - `auto,dom,fetch` → { autoMode: true, extras: 'dom,fetch' }
124
- * - `dom,fetch` → { autoMode: false, extras: 'dom,fetch' }
125
- * - `none` / `` → { autoMode: false, extras: '' }
126
- * - `undefined` → { autoMode: true, extras: '' } (default)
127
- */
128
- private parseGlobalsValue(value: string | undefined): { autoMode: boolean; extras: string } {
129
- if (value === undefined) return { autoMode: true, extras: '' };
130
- if (value === 'none' || value === '') return { autoMode: false, extras: '' };
131
-
132
- const tokens = value.split(',').map(t => t.trim()).filter(Boolean);
133
- const hasAuto = tokens.includes('auto');
134
- const extras = tokens.filter(t => t !== 'auto').join(',');
135
-
136
- return { autoMode: hasAuto, extras };
137
- }
138
-
139
- /**
140
- * Resolve the `--globals` CLI list into a pre-computed inject stub path
141
- * that the esbuild plugin will append to its `inject` list. Only runs
142
- * for `--app gjs` — Node and browser builds rely on native globals.
143
- *
144
- * Used only for the explicit-only path (no `auto` token in the value).
145
- * The auto path is handled in `buildApp` via the two-pass build.
146
- */
147
- private async resolveGlobalsInject(
148
- app: App,
149
- globals: string,
150
- verbose: boolean | undefined,
151
- ): Promise<string | undefined> {
152
- if (app !== 'gjs') return undefined;
153
- if (!globals) return undefined;
154
-
155
- const registerPaths = resolveGlobalsList(globals);
156
- if (registerPaths.size === 0) return undefined;
157
-
158
- const injectPath = await writeRegisterInjectFile(registerPaths, process.cwd());
159
- if (verbose && injectPath) {
160
- console.debug(
161
- `[gjsify] globals: injected ${registerPaths.size} register module(s) from --globals ${globals}`,
162
- );
163
- }
164
- return injectPath ?? undefined;
165
- }
166
-
167
- /**
168
- * Post-processing: prepend GJS shebang and mark the output file executable.
169
- * Only runs for GJS app builds with a resolvable single outfile.
170
- */
171
- private async applyShebang(outfile: string | undefined, verbose: boolean | undefined): Promise<void> {
172
- if (!outfile) {
173
- if (verbose) console.warn('[gjsify] --shebang skipped: no single outfile (use --outfile for GJS executables)');
174
- return;
175
- }
176
-
177
- const content = await readFile(outfile, 'utf-8');
178
- if (content.startsWith('#!')) {
179
- if (verbose) console.debug(`[gjsify] --shebang skipped: ${outfile} already starts with a shebang`);
180
- } else {
181
- await writeFile(outfile, GJS_SHEBANG + content);
182
- }
183
- await chmod(outfile, 0o755);
184
- if (verbose) console.debug(`[gjsify] --shebang: wrote shebang + chmod 0o755 to ${outfile}`);
185
- }
186
-
187
- /** Application mode */
188
- async buildApp(app: App = "gjs") {
189
- const { verbose, esbuild, typescript, exclude, library: pgk, aliases, excludeGlobals } = this.configData;
190
-
191
- const format: "esm" | "cjs" = (esbuild?.format as "esm" | "cjs") ?? (esbuild?.outfile?.endsWith(".cjs") ? "cjs" : "esm");
192
-
193
- // Set default outfile if no outdir is set
194
- if (esbuild && !esbuild?.outfile && !esbuild?.outdir && (pgk?.main || pgk?.module)) {
195
- esbuild.outfile = esbuild?.format === "cjs" ? pgk.main || pgk.module : pgk.module || pgk.main;
196
- }
197
-
198
- const { consoleShim, globals } = this.configData;
199
-
200
- const pluginOpts = {
201
- debug: verbose,
202
- app,
203
- format,
204
- exclude,
205
- reflection: typescript?.reflection,
206
- consoleShim,
207
- ...(aliases ? { aliases } : {}),
208
- };
209
-
210
- const { autoMode, extras } = this.parseGlobalsValue(globals);
211
-
212
- const pnpPlugin = await getPnpPlugin();
213
- const pnpPlugins: Plugin[] = pnpPlugin ? [pnpPlugin] : [];
214
-
215
- // --- Auto mode (with optional extras): iterative multi-pass build ---
216
- // The extras token is used for cases where the detector cannot
217
- // statically see a global (e.g. Excalibur indirects globalThis via
218
- // BrowserComponent.nativeComponent). Common pattern: --globals auto,dom
219
- if (app === "gjs" && autoMode) {
220
- const { injectPath } = await detectAutoGlobals(
221
- { ...this.getEsBuildDefaults(), ...esbuild, format, plugins: pnpPlugins },
222
- pluginOpts,
223
- verbose,
224
- { extraGlobalsList: extras, excludeGlobals },
225
- );
226
-
227
- const result = await build({
228
- ...this.getEsBuildDefaults(),
229
- ...esbuild,
230
- format,
231
- plugins: [
232
- ...pnpPlugins,
233
- gjsifyPlugin({
234
- ...pluginOpts,
235
- autoGlobalsInject: injectPath,
236
- }),
237
- ],
238
- });
239
-
240
- if (app === "gjs" && this.configData.shebang) {
241
- await this.applyShebang(esbuild?.outfile, verbose);
242
- }
243
-
244
- return [result];
245
- }
246
-
247
- // --- Explicit list (no `auto` token) or none mode ---
248
- const autoGlobalsInject = extras ? await this.resolveGlobalsInject(app, extras, verbose) : undefined;
249
-
250
- const result = await build({
251
- ...this.getEsBuildDefaults(),
252
- ...esbuild,
253
- format,
254
- plugins: [
255
- ...pnpPlugins,
256
- gjsifyPlugin({
257
- ...pluginOpts,
258
- autoGlobalsInject,
259
- }),
260
- ],
261
- });
262
-
263
- if (app === "gjs" && this.configData.shebang) {
264
- await this.applyShebang(esbuild?.outfile, verbose);
265
- }
266
-
267
- return [result];
268
- }
269
-
270
- async start(buildType: {library?: boolean, app?: App} = {app: 'gjs'}) {
271
- const results: BuildResult[] = [];
272
- if(buildType.library) {
273
- results.push(...(await this.buildLibrary()));
274
- } else {
275
- results.push(...(await this.buildApp(buildType.app)));
276
- }
277
-
278
- return results;
279
- }
280
- }
68
+ constructor(readonly configData: ConfigData = {}) {}
69
+
70
+ getEsBuildDefaults() {
71
+ const defaults: BuildOptions = {
72
+ allowOverwrite: true,
73
+ };
74
+ return defaults;
75
+ }
76
+
77
+ /** Library mode */
78
+ async buildLibrary() {
79
+ let { verbose, library, esbuild, typescript, exclude } = this.configData;
80
+ library ||= {};
81
+ esbuild ||= {};
82
+ typescript ||= {};
83
+
84
+ const moduleOutdir = library?.module ? dirname(library.module) : undefined;
85
+ const mainOutdir = library?.main ? dirname(library.main) : undefined;
86
+
87
+ const moduleOutExt = library.module ? extname(library.module) : ".js";
88
+ const mainOutExt = library.main ? extname(library.main) : ".js";
89
+
90
+ const multipleBuilds =
91
+ moduleOutdir && mainOutdir && moduleOutdir !== mainOutdir;
92
+
93
+ const pnpPlugin = await getPnpPlugin();
94
+ const pnpPlugins: Plugin[] = pnpPlugin ? [pnpPlugin] : [];
95
+
96
+ const results: BuildResult[] = [];
97
+
98
+ if (multipleBuilds) {
99
+ const moduleFormat =
100
+ moduleOutdir.includes("/cjs") || moduleOutExt === ".cjs"
101
+ ? "cjs"
102
+ : "esm";
103
+ results.push(
104
+ await build({
105
+ ...this.getEsBuildDefaults(),
106
+ ...esbuild,
107
+ format: moduleFormat,
108
+ outdir: moduleOutdir,
109
+ plugins: [
110
+ ...pnpPlugins,
111
+ gjsifyPlugin({
112
+ debug: verbose,
113
+ library: moduleFormat,
114
+ exclude,
115
+ reflection: typescript?.reflection,
116
+ jsExtension: moduleOutExt,
117
+ }),
118
+ ],
119
+ }),
120
+ );
121
+
122
+ const mainFormat =
123
+ mainOutdir.includes("/cjs") || mainOutExt === ".cjs" ? "cjs" : "esm";
124
+ results.push(
125
+ await build({
126
+ ...this.getEsBuildDefaults(),
127
+ ...esbuild,
128
+ format: moduleFormat,
129
+ outdir: mainOutdir,
130
+ plugins: [
131
+ ...pnpPlugins,
132
+ gjsifyPlugin({
133
+ debug: verbose,
134
+ library: mainFormat,
135
+ exclude,
136
+ reflection: typescript?.reflection,
137
+ jsExtension: mainOutdir,
138
+ }),
139
+ ],
140
+ }),
141
+ );
142
+ } else {
143
+ const outfilePath = esbuild?.outfile || library?.module || library?.main;
144
+ const outExt = outfilePath ? extname(outfilePath) : ".js";
145
+ const outdir =
146
+ esbuild?.outdir || (outfilePath ? dirname(outfilePath) : undefined);
147
+ const format: "esm" | "cjs" =
148
+ (esbuild?.format as "esm" | "cjs") ??
149
+ (outdir?.includes("/cjs") || outExt === ".cjs" ? "cjs" : "esm");
150
+ results.push(
151
+ await build({
152
+ ...this.getEsBuildDefaults(),
153
+ ...esbuild,
154
+ format,
155
+ outdir,
156
+ plugins: [
157
+ ...pnpPlugins,
158
+ gjsifyPlugin({
159
+ debug: verbose,
160
+ library: format,
161
+ exclude,
162
+ reflection: typescript?.reflection,
163
+ jsExtension: outExt,
164
+ }),
165
+ ],
166
+ }),
167
+ );
168
+ }
169
+ return results;
170
+ }
171
+
172
+ /**
173
+ * Parse the `--globals` value into { autoMode, extras }.
174
+ * - `auto` → { autoMode: true, extras: '' }
175
+ * - `auto,dom` → { autoMode: true, extras: 'dom' }
176
+ * - `auto,dom,fetch` → { autoMode: true, extras: 'dom,fetch' }
177
+ * - `dom,fetch` → { autoMode: false, extras: 'dom,fetch' }
178
+ * - `none` / `` → { autoMode: false, extras: '' }
179
+ * - `undefined` → { autoMode: true, extras: '' } (default)
180
+ */
181
+ private parseGlobalsValue(value: string | undefined): {
182
+ autoMode: boolean;
183
+ extras: string;
184
+ } {
185
+ if (value === undefined) return { autoMode: true, extras: "" };
186
+ if (value === "none" || value === "")
187
+ return { autoMode: false, extras: "" };
188
+
189
+ const tokens = value
190
+ .split(",")
191
+ .map((t) => t.trim())
192
+ .filter(Boolean);
193
+ const hasAuto = tokens.includes("auto");
194
+ const extras = tokens.filter((t) => t !== "auto").join(",");
195
+
196
+ return { autoMode: hasAuto, extras };
197
+ }
198
+
199
+ /**
200
+ * Resolve the `--globals` CLI list into a pre-computed inject stub path
201
+ * that the esbuild plugin will append to its `inject` list. Only runs
202
+ * for `--app gjs` Node and browser builds rely on native globals.
203
+ *
204
+ * Used only for the explicit-only path (no `auto` token in the value).
205
+ * The auto path is handled in `buildApp` via the two-pass build.
206
+ */
207
+ private async resolveGlobalsInject(
208
+ app: App,
209
+ globals: string,
210
+ verbose: boolean | undefined,
211
+ ): Promise<string | undefined> {
212
+ if (app !== "gjs") return undefined;
213
+ if (!globals) return undefined;
214
+
215
+ const registerPaths = resolveGlobalsList(globals);
216
+ if (registerPaths.size === 0) return undefined;
217
+
218
+ const injectPath = await writeRegisterInjectFile(
219
+ registerPaths,
220
+ process.cwd(),
221
+ );
222
+ if (verbose && injectPath) {
223
+ console.debug(
224
+ `[gjsify] globals: injected ${registerPaths.size} register module(s) from --globals ${globals}`,
225
+ );
226
+ }
227
+ return injectPath ?? undefined;
228
+ }
229
+
230
+ /**
231
+ * Post-processing: prepend GJS shebang and mark the output file executable.
232
+ * Only runs for GJS app builds with a resolvable single outfile.
233
+ */
234
+ private async applyShebang(
235
+ outfile: string | undefined,
236
+ verbose: boolean | undefined,
237
+ ): Promise<void> {
238
+ if (!outfile) {
239
+ if (verbose)
240
+ console.warn(
241
+ "[gjsify] --shebang skipped: no single outfile (use --outfile for GJS executables)",
242
+ );
243
+ return;
244
+ }
245
+
246
+ const content = await readFile(outfile, "utf-8");
247
+ if (content.startsWith("#!")) {
248
+ if (verbose)
249
+ console.debug(
250
+ `[gjsify] --shebang skipped: ${outfile} already starts with a shebang`,
251
+ );
252
+ } else {
253
+ await writeFile(outfile, GJS_SHEBANG + content);
254
+ }
255
+ await chmod(outfile, 0o755);
256
+ if (verbose)
257
+ console.debug(
258
+ `[gjsify] --shebang: wrote shebang + chmod 0o755 to ${outfile}`,
259
+ );
260
+ }
261
+
262
+ /** Application mode */
263
+ async buildApp(app: App = "gjs") {
264
+ const {
265
+ verbose,
266
+ esbuild,
267
+ typescript,
268
+ exclude,
269
+ library: pgk,
270
+ aliases,
271
+ excludeGlobals,
272
+ } = this.configData;
273
+
274
+ const format: "esm" | "cjs" =
275
+ (esbuild?.format as "esm" | "cjs") ??
276
+ (esbuild?.outfile?.endsWith(".cjs") ? "cjs" : "esm");
277
+
278
+ // Set default outfile if no outdir is set
279
+ if (
280
+ esbuild &&
281
+ !esbuild?.outfile &&
282
+ !esbuild?.outdir &&
283
+ (pgk?.main || pgk?.module)
284
+ ) {
285
+ esbuild.outfile =
286
+ esbuild?.format === "cjs"
287
+ ? pgk.main || pgk.module
288
+ : pgk.module || pgk.main;
289
+ }
290
+
291
+ const { consoleShim, globals } = this.configData;
292
+
293
+ const pluginOpts = {
294
+ debug: verbose,
295
+ app,
296
+ format,
297
+ exclude,
298
+ reflection: typescript?.reflection,
299
+ consoleShim,
300
+ ...(aliases ? { aliases } : {}),
301
+ };
302
+
303
+ const { autoMode, extras } = this.parseGlobalsValue(globals);
304
+
305
+ const pnpPlugin = await getPnpPlugin();
306
+ const pnpPlugins: Plugin[] = pnpPlugin ? [pnpPlugin] : [];
307
+
308
+ // --- Auto mode (with optional extras): iterative multi-pass build ---
309
+ // The extras token is used for cases where the detector cannot
310
+ // statically see a global (e.g. Excalibur indirects globalThis via
311
+ // BrowserComponent.nativeComponent). Common pattern: --globals auto,dom
312
+ if (app === "gjs" && autoMode) {
313
+ const { injectPath } = await detectAutoGlobals(
314
+ {
315
+ ...this.getEsBuildDefaults(),
316
+ ...esbuild,
317
+ format,
318
+ plugins: pnpPlugins,
319
+ },
320
+ pluginOpts,
321
+ verbose,
322
+ { extraGlobalsList: extras, excludeGlobals },
323
+ );
324
+
325
+ const result = await build({
326
+ ...this.getEsBuildDefaults(),
327
+ ...esbuild,
328
+ format,
329
+ plugins: [
330
+ ...pnpPlugins,
331
+ gjsifyPlugin({
332
+ ...pluginOpts,
333
+ autoGlobalsInject: injectPath,
334
+ }),
335
+ ],
336
+ });
337
+
338
+ if (app === "gjs" && this.configData.shebang) {
339
+ await this.applyShebang(esbuild?.outfile, verbose);
340
+ }
341
+
342
+ return [result];
343
+ }
344
+
345
+ // --- Explicit list (no `auto` token) or none mode ---
346
+ const autoGlobalsInject = extras
347
+ ? await this.resolveGlobalsInject(app, extras, verbose)
348
+ : undefined;
349
+
350
+ const result = await build({
351
+ ...this.getEsBuildDefaults(),
352
+ ...esbuild,
353
+ format,
354
+ plugins: [
355
+ ...pnpPlugins,
356
+ gjsifyPlugin({
357
+ ...pluginOpts,
358
+ autoGlobalsInject,
359
+ }),
360
+ ],
361
+ });
362
+
363
+ if (app === "gjs" && this.configData.shebang) {
364
+ await this.applyShebang(esbuild?.outfile, verbose);
365
+ }
366
+
367
+ return [result];
368
+ }
369
+
370
+ async start(buildType: { library?: boolean; app?: App } = { app: "gjs" }) {
371
+ const results: BuildResult[] = [];
372
+ if (buildType.library) {
373
+ results.push(...(await this.buildLibrary()));
374
+ } else {
375
+ results.push(...(await this.buildApp(buildType.app)));
376
+ }
377
+
378
+ return results;
379
+ }
380
+ }