@gjsify/cli 0.3.1 → 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 } 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);
@@ -34,7 +34,7 @@ export declare class BuildAction {
34
34
  /** Application mode */
35
35
  buildApp(app?: App): Promise<BuildResult<{
36
36
  format: "esm" | "cjs";
37
- plugins: import("esbuild").Plugin[];
37
+ plugins: Plugin[];
38
38
  bundle?: boolean;
39
39
  splitting?: boolean;
40
40
  preserveSymlinks?: boolean;
@@ -1,9 +1,61 @@
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 } from 'node:path';
5
- import { chmod, readFile, writeFile } from 'node:fs/promises';
6
- const GJS_SHEBANG = '#!/usr/bin/env -S gjs -m\n';
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
+ const GJS_SHEBANG = "#!/usr/bin/env -S gjs -m\n";
8
+ /** Walk up from dir until .pnp.cjs is found; return its directory or null. */
9
+ function findPnpRoot(dir) {
10
+ let current = dir;
11
+ while (true) {
12
+ if (existsSync(join(current, ".pnp.cjs")))
13
+ return current;
14
+ const parent = dirname(current);
15
+ if (parent === current)
16
+ return null;
17
+ current = parent;
18
+ }
19
+ }
20
+ /**
21
+ * If the current project uses Yarn PnP, return the official
22
+ * @yarnpkg/esbuild-plugin-pnp plugin so esbuild can resolve
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.
29
+ */
30
+ async function getPnpPlugin() {
31
+ if (!findPnpRoot(process.cwd()))
32
+ return null;
33
+ try {
34
+ const { pnpPlugin } = await import("@yarnpkg/esbuild-plugin-pnp");
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
+ });
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
7
59
  export class BuildAction {
8
60
  configData;
9
61
  constructor(configData = {}) {
@@ -11,7 +63,7 @@ export class BuildAction {
11
63
  }
12
64
  getEsBuildDefaults() {
13
65
  const defaults = {
14
- allowOverwrite: true
66
+ allowOverwrite: true,
15
67
  };
16
68
  return defaults;
17
69
  }
@@ -23,45 +75,71 @@ export class BuildAction {
23
75
  typescript ||= {};
24
76
  const moduleOutdir = library?.module ? dirname(library.module) : undefined;
25
77
  const mainOutdir = library?.main ? dirname(library.main) : undefined;
26
- const moduleOutExt = library.module ? extname(library.module) : '.js';
27
- const mainOutExt = library.main ? extname(library.main) : '.js';
28
- const multipleBuilds = moduleOutdir && mainOutdir && (moduleOutdir !== mainOutdir);
78
+ const moduleOutExt = library.module ? extname(library.module) : ".js";
79
+ const mainOutExt = library.main ? extname(library.main) : ".js";
80
+ const multipleBuilds = moduleOutdir && mainOutdir && moduleOutdir !== mainOutdir;
81
+ const pnpPlugin = await getPnpPlugin();
82
+ const pnpPlugins = pnpPlugin ? [pnpPlugin] : [];
29
83
  const results = [];
30
84
  if (multipleBuilds) {
31
- const moduleFormat = moduleOutdir.includes('/cjs') || moduleOutExt === '.cjs' ? 'cjs' : 'esm';
85
+ const moduleFormat = moduleOutdir.includes("/cjs") || moduleOutExt === ".cjs"
86
+ ? "cjs"
87
+ : "esm";
32
88
  results.push(await build({
33
89
  ...this.getEsBuildDefaults(),
34
90
  ...esbuild,
35
91
  format: moduleFormat,
36
92
  outdir: moduleOutdir,
37
93
  plugins: [
38
- gjsifyPlugin({ debug: verbose, library: moduleFormat, exclude, reflection: typescript?.reflection, jsExtension: moduleOutExt }),
39
- ]
94
+ ...pnpPlugins,
95
+ gjsifyPlugin({
96
+ debug: verbose,
97
+ library: moduleFormat,
98
+ exclude,
99
+ reflection: typescript?.reflection,
100
+ jsExtension: moduleOutExt,
101
+ }),
102
+ ],
40
103
  }));
41
- const mainFormat = mainOutdir.includes('/cjs') || mainOutExt === '.cjs' ? 'cjs' : 'esm';
104
+ const mainFormat = mainOutdir.includes("/cjs") || mainOutExt === ".cjs" ? "cjs" : "esm";
42
105
  results.push(await build({
43
106
  ...this.getEsBuildDefaults(),
44
107
  ...esbuild,
45
108
  format: moduleFormat,
46
109
  outdir: mainOutdir,
47
110
  plugins: [
48
- gjsifyPlugin({ debug: verbose, library: mainFormat, exclude, reflection: typescript?.reflection, jsExtension: mainOutdir })
49
- ]
111
+ ...pnpPlugins,
112
+ gjsifyPlugin({
113
+ debug: verbose,
114
+ library: mainFormat,
115
+ exclude,
116
+ reflection: typescript?.reflection,
117
+ jsExtension: mainOutdir,
118
+ }),
119
+ ],
50
120
  }));
51
121
  }
52
122
  else {
53
123
  const outfilePath = esbuild?.outfile || library?.module || library?.main;
54
- const outExt = outfilePath ? extname(outfilePath) : '.js';
124
+ const outExt = outfilePath ? extname(outfilePath) : ".js";
55
125
  const outdir = esbuild?.outdir || (outfilePath ? dirname(outfilePath) : undefined);
56
- const format = esbuild?.format ?? (outdir?.includes('/cjs') || outExt === '.cjs' ? 'cjs' : 'esm');
126
+ const format = esbuild?.format ??
127
+ (outdir?.includes("/cjs") || outExt === ".cjs" ? "cjs" : "esm");
57
128
  results.push(await build({
58
129
  ...this.getEsBuildDefaults(),
59
130
  ...esbuild,
60
131
  format,
61
132
  outdir,
62
133
  plugins: [
63
- gjsifyPlugin({ debug: verbose, library: format, exclude, reflection: typescript?.reflection, jsExtension: outExt })
64
- ]
134
+ ...pnpPlugins,
135
+ gjsifyPlugin({
136
+ debug: verbose,
137
+ library: format,
138
+ exclude,
139
+ reflection: typescript?.reflection,
140
+ jsExtension: outExt,
141
+ }),
142
+ ],
65
143
  }));
66
144
  }
67
145
  return results;
@@ -77,12 +155,15 @@ export class BuildAction {
77
155
  */
78
156
  parseGlobalsValue(value) {
79
157
  if (value === undefined)
80
- return { autoMode: true, extras: '' };
81
- if (value === 'none' || value === '')
82
- return { autoMode: false, extras: '' };
83
- const tokens = value.split(',').map(t => t.trim()).filter(Boolean);
84
- const hasAuto = tokens.includes('auto');
85
- 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(",");
86
167
  return { autoMode: hasAuto, extras };
87
168
  }
88
169
  /**
@@ -94,7 +175,7 @@ export class BuildAction {
94
175
  * The auto path is handled in `buildApp` via the two-pass build.
95
176
  */
96
177
  async resolveGlobalsInject(app, globals, verbose) {
97
- if (app !== 'gjs')
178
+ if (app !== "gjs")
98
179
  return undefined;
99
180
  if (!globals)
100
181
  return undefined;
@@ -114,11 +195,11 @@ export class BuildAction {
114
195
  async applyShebang(outfile, verbose) {
115
196
  if (!outfile) {
116
197
  if (verbose)
117
- 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)");
118
199
  return;
119
200
  }
120
- const content = await readFile(outfile, 'utf-8');
121
- if (content.startsWith('#!')) {
201
+ const content = await readFile(outfile, "utf-8");
202
+ if (content.startsWith("#!")) {
122
203
  if (verbose)
123
204
  console.debug(`[gjsify] --shebang skipped: ${outfile} already starts with a shebang`);
124
205
  }
@@ -130,12 +211,19 @@ export class BuildAction {
130
211
  console.debug(`[gjsify] --shebang: wrote shebang + chmod 0o755 to ${outfile}`);
131
212
  }
132
213
  /** Application mode */
133
- async buildApp(app = 'gjs') {
134
- const { verbose, esbuild, typescript, exclude, library: pgk, aliases } = this.configData;
135
- const format = esbuild?.format ?? (esbuild?.outfile?.endsWith('.cjs') ? 'cjs' : 'esm');
214
+ async buildApp(app = "gjs") {
215
+ const { verbose, esbuild, typescript, exclude, library: pgk, aliases, excludeGlobals, } = this.configData;
216
+ const format = esbuild?.format ??
217
+ (esbuild?.outfile?.endsWith(".cjs") ? "cjs" : "esm");
136
218
  // Set default outfile if no outdir is set
137
- if (esbuild && !esbuild?.outfile && !esbuild?.outdir && (pgk?.main || pgk?.module)) {
138
- 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;
139
227
  }
140
228
  const { consoleShim, globals } = this.configData;
141
229
  const pluginOpts = {
@@ -148,24 +236,32 @@ export class BuildAction {
148
236
  ...(aliases ? { aliases } : {}),
149
237
  };
150
238
  const { autoMode, extras } = this.parseGlobalsValue(globals);
239
+ const pnpPlugin = await getPnpPlugin();
240
+ const pnpPlugins = pnpPlugin ? [pnpPlugin] : [];
151
241
  // --- Auto mode (with optional extras): iterative multi-pass build ---
152
242
  // The extras token is used for cases where the detector cannot
153
243
  // statically see a global (e.g. Excalibur indirects globalThis via
154
244
  // BrowserComponent.nativeComponent). Common pattern: --globals auto,dom
155
- if (app === 'gjs' && autoMode) {
156
- const { injectPath } = await detectAutoGlobals({ ...this.getEsBuildDefaults(), ...esbuild, format }, pluginOpts, verbose, { extraGlobalsList: extras });
245
+ if (app === "gjs" && autoMode) {
246
+ const { injectPath } = await detectAutoGlobals({
247
+ ...this.getEsBuildDefaults(),
248
+ ...esbuild,
249
+ format,
250
+ plugins: pnpPlugins,
251
+ }, pluginOpts, verbose, { extraGlobalsList: extras, excludeGlobals });
157
252
  const result = await build({
158
253
  ...this.getEsBuildDefaults(),
159
254
  ...esbuild,
160
255
  format,
161
256
  plugins: [
257
+ ...pnpPlugins,
162
258
  gjsifyPlugin({
163
259
  ...pluginOpts,
164
260
  autoGlobalsInject: injectPath,
165
261
  }),
166
262
  ],
167
263
  });
168
- if (app === 'gjs' && this.configData.shebang) {
264
+ if (app === "gjs" && this.configData.shebang) {
169
265
  await this.applyShebang(esbuild?.outfile, verbose);
170
266
  }
171
267
  return [result];
@@ -179,18 +275,19 @@ export class BuildAction {
179
275
  ...esbuild,
180
276
  format,
181
277
  plugins: [
278
+ ...pnpPlugins,
182
279
  gjsifyPlugin({
183
280
  ...pluginOpts,
184
281
  autoGlobalsInject,
185
282
  }),
186
- ]
283
+ ],
187
284
  });
188
- if (app === 'gjs' && this.configData.shebang) {
285
+ if (app === "gjs" && this.configData.shebang) {
189
286
  await this.applyShebang(esbuild?.outfile, verbose);
190
287
  }
191
288
  return [result];
192
289
  }
193
- async start(buildType = { app: 'gjs' }) {
290
+ async start(buildType = { app: "gjs" }) {
194
291
  const results = [];
195
292
  if (buildType.library) {
196
293
  results.push(...(await this.buildLibrary()));
@@ -117,6 +117,11 @@ export const buildCommand = {
117
117
  type: 'string',
118
118
  default: [],
119
119
  coerce: (arg) => arg.flatMap((v) => v.split(',').map((s) => s.trim()).filter(Boolean)),
120
+ })
121
+ .option('exclude-globals', {
122
+ description: "Comma-separated global identifiers to remove from auto-detection results. Use for false positives from dead browser-compat code whose polyfills require unavailable native libraries (e.g. --exclude-globals fetch,XMLHttpRequest).",
123
+ type: 'string',
124
+ normalize: true,
120
125
  });
121
126
  },
122
127
  handler: async (args) => {
package/lib/config.js CHANGED
@@ -73,6 +73,14 @@ export class Config {
73
73
  configData.globals = cliArgs.globals;
74
74
  if (cliArgs.shebang !== undefined)
75
75
  configData.shebang = cliArgs.shebang;
76
+ if (cliArgs.excludeGlobals) {
77
+ const raw = Array.isArray(cliArgs.excludeGlobals)
78
+ ? cliArgs.excludeGlobals.join(',')
79
+ : String(cliArgs.excludeGlobals);
80
+ const ids = raw.split(',').map((s) => s.trim()).filter(Boolean);
81
+ if (ids.length)
82
+ configData.excludeGlobals = [...(configData.excludeGlobals ?? []), ...ids];
83
+ }
76
84
  merge(configData.library ??= {}, pkg, configData.library);
77
85
  merge(configData.typescript ??= {}, tsConfig, configData.typescript);
78
86
  // Parse `KEY=VALUE` style flags into Record<string, string>.
@@ -89,4 +89,11 @@ export interface CliBuildOptions {
89
89
  * scenario never executes). Layered on top of the built-in alias map.
90
90
  */
91
91
  alias?: string[];
92
+ /**
93
+ * Comma-separated global identifiers to remove from the auto-detected set.
94
+ * Useful for false positives from dead browser-compat code in npm deps
95
+ * whose polyfills require unavailable native libraries.
96
+ * Example: `--exclude-globals fetch,XMLHttpRequest`
97
+ */
98
+ excludeGlobals?: string[];
92
99
  }
@@ -27,4 +27,11 @@ export interface ConfigData {
27
27
  * Comes from `gjsify build --alias FROM=TO`.
28
28
  */
29
29
  aliases?: Record<string, string>;
30
+ /**
31
+ * Global identifiers to remove from the auto-detected set before writing
32
+ * the inject stub. Useful for false positives from dead browser-compat
33
+ * code in npm dependencies whose polyfills require unavailable native libs.
34
+ * Example: `["fetch", "XMLHttpRequest"]` excludes the HTTP polyfill stack.
35
+ */
36
+ excludeGlobals?: string[];
30
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -23,15 +23,16 @@
23
23
  "cli"
24
24
  ],
25
25
  "dependencies": {
26
- "@gjsify/create-app": "^0.3.1",
27
- "@gjsify/esbuild-plugin-gjsify": "^0.3.1",
28
- "@gjsify/example-dom-canvas2d-fireworks": "^0.3.1",
29
- "@gjsify/example-dom-excalibur-jelly-jumper": "^0.3.1",
30
- "@gjsify/example-dom-three-geometry-teapot": "^0.3.1",
31
- "@gjsify/example-dom-three-postprocessing-pixel": "^0.3.1",
32
- "@gjsify/example-node-express-webserver": "^0.3.1",
33
- "@gjsify/node-polyfills": "^0.3.1",
34
- "@gjsify/web-polyfills": "^0.3.1",
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
+ "@yarnpkg/esbuild-plugin-pnp": "^3.0.0-rc.15",
35
36
  "cosmiconfig": "^9.0.1",
36
37
  "esbuild": "^0.28.0",
37
38
  "get-tsconfig": "^4.14.0",
@@ -1,240 +1,380 @@
1
- import type { ConfigData } from '../types/index.js';
2
- import type { App } from '@gjsify/esbuild-plugin-gjsify';
3
- import { build, BuildOptions, BuildResult } 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 } from 'node:path';
7
- import { chmod, readFile, writeFile } from 'node:fs/promises';
8
-
9
- const GJS_SHEBANG = '#!/usr/bin/env -S gjs -m\n';
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";
13
+
14
+ const GJS_SHEBANG = "#!/usr/bin/env -S gjs -m\n";
15
+
16
+ /** Walk up from dir until .pnp.cjs is found; return its directory or null. */
17
+ function findPnpRoot(dir: string): string | null {
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
+ }
25
+ }
26
+
27
+ /**
28
+ * If the current project uses Yarn PnP, return the official
29
+ * @yarnpkg/esbuild-plugin-pnp plugin so esbuild can resolve
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.
36
+ */
37
+ async function getPnpPlugin(): Promise<Plugin | null> {
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
+ }
65
+ }
10
66
 
11
67
  export class BuildAction {
12
- constructor(readonly configData: ConfigData = {}) {
13
-
14
- }
15
-
16
- getEsBuildDefaults() {
17
- const defaults: BuildOptions = {
18
- allowOverwrite: true
19
- }
20
- return defaults;
21
- }
22
-
23
- /** Library mode */
24
- async buildLibrary() {
25
- let { verbose, library, esbuild, typescript, exclude } = this.configData;
26
- library ||= {};
27
- esbuild ||= {};
28
- typescript ||= {};
29
-
30
- const moduleOutdir = library?.module ? dirname(library.module) : undefined;
31
- const mainOutdir = library?.main ? dirname(library.main) : undefined;
32
-
33
- const moduleOutExt = library.module ? extname(library.module) : '.js';
34
- const mainOutExt = library.main ? extname(library.main) : '.js';
35
-
36
- const multipleBuilds = moduleOutdir && mainOutdir && (moduleOutdir !== mainOutdir);
37
-
38
- const results: BuildResult[] = [];
39
-
40
- if(multipleBuilds) {
41
-
42
- const moduleFormat = moduleOutdir.includes('/cjs') || moduleOutExt === '.cjs' ? 'cjs' : 'esm';
43
- results.push(await build({
44
- ...this.getEsBuildDefaults(),
45
- ...esbuild,
46
- format: moduleFormat,
47
- outdir: moduleOutdir,
48
- plugins: [
49
- gjsifyPlugin({debug: verbose, library: moduleFormat, exclude, reflection: typescript?.reflection, jsExtension: moduleOutExt}),
50
- ]
51
- }));
52
-
53
- const mainFormat = mainOutdir.includes('/cjs') || mainOutExt === '.cjs' ? 'cjs' : 'esm';
54
- results.push(await build({
55
- ...this.getEsBuildDefaults(),
56
- ...esbuild,
57
- format: moduleFormat,
58
- outdir: mainOutdir,
59
- plugins: [
60
- gjsifyPlugin({debug: verbose, library: mainFormat, exclude, reflection: typescript?.reflection, jsExtension: mainOutdir})
61
- ]
62
- }));
63
- } else {
64
- const outfilePath = esbuild?.outfile || library?.module || library?.main;
65
- const outExt = outfilePath ? extname(outfilePath) : '.js';
66
- const outdir = esbuild?.outdir || (outfilePath ? dirname(outfilePath) : undefined);
67
- const format: 'esm' | 'cjs' = (esbuild?.format as 'esm' | 'cjs') ?? (outdir?.includes('/cjs') || outExt === '.cjs' ? 'cjs' : 'esm');
68
- results.push(await build({
69
- ...this.getEsBuildDefaults(),
70
- ...esbuild,
71
- format,
72
- outdir,
73
- plugins: [
74
- gjsifyPlugin({debug: verbose, library: format, exclude, reflection: typescript?.reflection, jsExtension: outExt})
75
- ]
76
- }));
77
- }
78
- return results;
79
- }
80
-
81
- /**
82
- * Parse the `--globals` value into { autoMode, extras }.
83
- * - `auto` → { autoMode: true, extras: '' }
84
- * - `auto,dom` → { autoMode: true, extras: 'dom' }
85
- * - `auto,dom,fetch` → { autoMode: true, extras: 'dom,fetch' }
86
- * - `dom,fetch` → { autoMode: false, extras: 'dom,fetch' }
87
- * - `none` / `` → { autoMode: false, extras: '' }
88
- * - `undefined` → { autoMode: true, extras: '' } (default)
89
- */
90
- private parseGlobalsValue(value: string | undefined): { autoMode: boolean; extras: string } {
91
- if (value === undefined) return { autoMode: true, extras: '' };
92
- if (value === 'none' || value === '') return { autoMode: false, extras: '' };
93
-
94
- const tokens = value.split(',').map(t => t.trim()).filter(Boolean);
95
- const hasAuto = tokens.includes('auto');
96
- const extras = tokens.filter(t => t !== 'auto').join(',');
97
-
98
- return { autoMode: hasAuto, extras };
99
- }
100
-
101
- /**
102
- * Resolve the `--globals` CLI list into a pre-computed inject stub path
103
- * that the esbuild plugin will append to its `inject` list. Only runs
104
- * for `--app gjs` — Node and browser builds rely on native globals.
105
- *
106
- * Used only for the explicit-only path (no `auto` token in the value).
107
- * The auto path is handled in `buildApp` via the two-pass build.
108
- */
109
- private async resolveGlobalsInject(
110
- app: App,
111
- globals: string,
112
- verbose: boolean | undefined,
113
- ): Promise<string | undefined> {
114
- if (app !== 'gjs') return undefined;
115
- if (!globals) return undefined;
116
-
117
- const registerPaths = resolveGlobalsList(globals);
118
- if (registerPaths.size === 0) return undefined;
119
-
120
- const injectPath = await writeRegisterInjectFile(registerPaths, process.cwd());
121
- if (verbose && injectPath) {
122
- console.debug(
123
- `[gjsify] globals: injected ${registerPaths.size} register module(s) from --globals ${globals}`,
124
- );
125
- }
126
- return injectPath ?? undefined;
127
- }
128
-
129
- /**
130
- * Post-processing: prepend GJS shebang and mark the output file executable.
131
- * Only runs for GJS app builds with a resolvable single outfile.
132
- */
133
- private async applyShebang(outfile: string | undefined, verbose: boolean | undefined): Promise<void> {
134
- if (!outfile) {
135
- if (verbose) console.warn('[gjsify] --shebang skipped: no single outfile (use --outfile for GJS executables)');
136
- return;
137
- }
138
-
139
- const content = await readFile(outfile, 'utf-8');
140
- if (content.startsWith('#!')) {
141
- if (verbose) console.debug(`[gjsify] --shebang skipped: ${outfile} already starts with a shebang`);
142
- } else {
143
- await writeFile(outfile, GJS_SHEBANG + content);
144
- }
145
- await chmod(outfile, 0o755);
146
- if (verbose) console.debug(`[gjsify] --shebang: wrote shebang + chmod 0o755 to ${outfile}`);
147
- }
148
-
149
- /** Application mode */
150
- async buildApp(app: App = 'gjs') {
151
-
152
- const { verbose, esbuild, typescript, exclude, library: pgk, aliases } = this.configData;
153
-
154
- const format: 'esm' | 'cjs' = (esbuild?.format as 'esm' | 'cjs') ?? (esbuild?.outfile?.endsWith('.cjs') ? 'cjs' : 'esm');
155
-
156
- // Set default outfile if no outdir is set
157
- if(esbuild && !esbuild?.outfile && !esbuild?.outdir && (pgk?.main || pgk?.module)) {
158
- esbuild.outfile = esbuild?.format === 'cjs' ? pgk.main || pgk.module : pgk.module || pgk.main;
159
- }
160
-
161
- const { consoleShim, globals } = this.configData;
162
-
163
- const pluginOpts = {
164
- debug: verbose,
165
- app,
166
- format,
167
- exclude,
168
- reflection: typescript?.reflection,
169
- consoleShim,
170
- ...(aliases ? { aliases } : {}),
171
- };
172
-
173
- const { autoMode, extras } = this.parseGlobalsValue(globals);
174
-
175
- // --- Auto mode (with optional extras): iterative multi-pass build ---
176
- // The extras token is used for cases where the detector cannot
177
- // statically see a global (e.g. Excalibur indirects globalThis via
178
- // BrowserComponent.nativeComponent). Common pattern: --globals auto,dom
179
- if (app === 'gjs' && autoMode) {
180
- const { injectPath } = await detectAutoGlobals(
181
- { ...this.getEsBuildDefaults(), ...esbuild, format },
182
- pluginOpts,
183
- verbose,
184
- { extraGlobalsList: extras },
185
- );
186
-
187
- const result = await build({
188
- ...this.getEsBuildDefaults(),
189
- ...esbuild,
190
- format,
191
- plugins: [
192
- gjsifyPlugin({
193
- ...pluginOpts,
194
- autoGlobalsInject: injectPath,
195
- }),
196
- ],
197
- });
198
-
199
- if (app === 'gjs' && this.configData.shebang) {
200
- await this.applyShebang(esbuild?.outfile, verbose);
201
- }
202
-
203
- return [result];
204
- }
205
-
206
- // --- Explicit list (no `auto` token) or none mode ---
207
- const autoGlobalsInject = extras
208
- ? await this.resolveGlobalsInject(app, extras, verbose)
209
- : undefined;
210
-
211
- const result = await build({
212
- ...this.getEsBuildDefaults(),
213
- ...esbuild,
214
- format,
215
- plugins: [
216
- gjsifyPlugin({
217
- ...pluginOpts,
218
- autoGlobalsInject,
219
- }),
220
- ]
221
- });
222
-
223
- if (app === 'gjs' && this.configData.shebang) {
224
- await this.applyShebang(esbuild?.outfile, verbose);
225
- }
226
-
227
- return [result];
228
- }
229
-
230
- async start(buildType: {library?: boolean, app?: App} = {app: 'gjs'}) {
231
- const results: BuildResult[] = [];
232
- if(buildType.library) {
233
- results.push(...(await this.buildLibrary()));
234
- } else {
235
- results.push(...(await this.buildApp(buildType.app)));
236
- }
237
-
238
- return results;
239
- }
240
- }
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
+ }
@@ -120,6 +120,11 @@ export const buildCommand: Command<any, CliBuildOptions> = {
120
120
  default: [] as string[],
121
121
  coerce: (arg: string[]) => arg.flatMap((v) => v.split(',').map((s) => s.trim()).filter(Boolean)),
122
122
  })
123
+ .option('exclude-globals', {
124
+ description: "Comma-separated global identifiers to remove from auto-detection results. Use for false positives from dead browser-compat code whose polyfills require unavailable native libraries (e.g. --exclude-globals fetch,XMLHttpRequest).",
125
+ type: 'string',
126
+ normalize: true,
127
+ })
123
128
  },
124
129
  handler: async (args) => {
125
130
  const config = new Config();
package/src/config.ts CHANGED
@@ -84,6 +84,13 @@ export class Config {
84
84
  if (cliArgs.consoleShim !== undefined) configData.consoleShim = cliArgs.consoleShim;
85
85
  if (cliArgs.globals !== undefined) configData.globals = cliArgs.globals;
86
86
  if (cliArgs.shebang !== undefined) configData.shebang = cliArgs.shebang;
87
+ if (cliArgs.excludeGlobals) {
88
+ const raw = Array.isArray(cliArgs.excludeGlobals)
89
+ ? cliArgs.excludeGlobals.join(',')
90
+ : String(cliArgs.excludeGlobals);
91
+ const ids = raw.split(',').map((s: string) => s.trim()).filter(Boolean);
92
+ if (ids.length) configData.excludeGlobals = [...(configData.excludeGlobals ?? []), ...ids];
93
+ }
87
94
 
88
95
  merge(configData.library ??= {}, pkg, configData.library);
89
96
  merge(configData.typescript ??= {}, tsConfig, configData.typescript);
@@ -90,4 +90,11 @@ export interface CliBuildOptions {
90
90
  * scenario never executes). Layered on top of the built-in alias map.
91
91
  */
92
92
  alias?: string[];
93
+ /**
94
+ * Comma-separated global identifiers to remove from the auto-detected set.
95
+ * Useful for false positives from dead browser-compat code in npm deps
96
+ * whose polyfills require unavailable native libraries.
97
+ * Example: `--exclude-globals fetch,XMLHttpRequest`
98
+ */
99
+ excludeGlobals?: string[];
93
100
  }
@@ -28,4 +28,11 @@ export interface ConfigData {
28
28
  * Comes from `gjsify build --alias FROM=TO`.
29
29
  */
30
30
  aliases?: Record<string, string>;
31
+ /**
32
+ * Global identifiers to remove from the auto-detected set before writing
33
+ * the inject stub. Useful for false positives from dead browser-compat
34
+ * code in npm dependencies whose polyfills require unavailable native libs.
35
+ * Example: `["fetch", "XMLHttpRequest"]` excludes the HTTP polyfill stack.
36
+ */
37
+ excludeGlobals?: string[];
31
38
  }