@gjsify/cli 0.3.2 → 0.3.4

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