@gjsify/cli 0.1.13 → 0.2.0

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.
@@ -131,7 +131,7 @@ export class BuildAction {
131
131
  }
132
132
  /** Application mode */
133
133
  async buildApp(app = 'gjs') {
134
- const { verbose, esbuild, typescript, exclude, library: pgk } = this.configData;
134
+ const { verbose, esbuild, typescript, exclude, library: pgk, aliases } = this.configData;
135
135
  const format = esbuild?.format ?? (esbuild?.outfile?.endsWith('.cjs') ? 'cjs' : 'esm');
136
136
  // Set default outfile if no outdir is set
137
137
  if (esbuild && !esbuild?.outfile && !esbuild?.outdir && (pgk?.main || pgk?.module)) {
@@ -145,6 +145,7 @@ export class BuildAction {
145
145
  exclude,
146
146
  reflection: typescript?.reflection,
147
147
  consoleShim,
148
+ ...(aliases ? { aliases } : {}),
148
149
  };
149
150
  const { autoMode, extras } = this.parseGlobalsValue(globals);
150
151
  // --- Auto mode (with optional extras): iterative multi-pass build ---
@@ -97,6 +97,26 @@ export const buildCommand = {
97
97
  type: 'boolean',
98
98
  normalize: true,
99
99
  default: false
100
+ })
101
+ .option('external', {
102
+ description: "Module names that should NOT be bundled. Repeat the flag or pass a comma-separated list (e.g. --external typedoc,prettier). Globs are forwarded to esbuild as-is. See https://esbuild.github.io/api/#external",
103
+ array: true,
104
+ type: 'string',
105
+ default: [],
106
+ coerce: (arg) => arg.flatMap((v) => v.split(',').map((s) => s.trim()).filter(Boolean)),
107
+ })
108
+ .option('define', {
109
+ description: "Substitute compile-time constants. Each entry is KEY=VALUE where VALUE is a JS expression (string literals must be quoted: --define VERSION='\"1.2.3\"'). Repeat the flag or pass comma-separated. See https://esbuild.github.io/api/#define",
110
+ array: true,
111
+ type: 'string',
112
+ default: [],
113
+ })
114
+ .option('alias', {
115
+ description: "Map module specifiers at bundle time. Each entry is FROM=TO (e.g. --alias typedoc=@gjsify/empty). Layered on top of the built-in alias map. Useful for stubbing heavy deps the test scenario never executes.",
116
+ array: true,
117
+ type: 'string',
118
+ default: [],
119
+ coerce: (arg) => arg.flatMap((v) => v.split(',').map((s) => s.trim()).filter(Boolean)),
100
120
  });
101
121
  },
102
122
  handler: async (args) => {
package/lib/config.js CHANGED
@@ -75,13 +75,40 @@ export class Config {
75
75
  configData.shebang = cliArgs.shebang;
76
76
  merge(configData.library ??= {}, pkg, configData.library);
77
77
  merge(configData.typescript ??= {}, tsConfig, configData.typescript);
78
+ // Parse `KEY=VALUE` style flags into Record<string, string>.
79
+ // - `--define`: VALUE is a JS expression (string literals must be
80
+ // pre-quoted by the caller, e.g. `'"1.2.3"'`).
81
+ // - `--alias`: VALUE is the substitute module specifier.
82
+ const parseKvPairs = (entries, flag) => {
83
+ const out = {};
84
+ for (const entry of entries) {
85
+ const idx = entry.indexOf('=');
86
+ if (idx === -1) {
87
+ throw new Error(`Invalid --${flag} value '${entry}'. Expected KEY=VALUE.`);
88
+ }
89
+ const key = entry.slice(0, idx).trim();
90
+ const value = entry.slice(idx + 1);
91
+ if (!key) {
92
+ throw new Error(`Invalid --${flag} value '${entry}'. Empty key.`);
93
+ }
94
+ out[key] = value;
95
+ }
96
+ return out;
97
+ };
98
+ const defineMap = parseKvPairs(cliArgs.define ?? [], 'define');
99
+ const aliasMap = parseKvPairs(cliArgs.alias ?? [], 'alias');
100
+ if (Object.keys(aliasMap).length) {
101
+ configData.aliases = { ...(configData.aliases ?? {}), ...aliasMap };
102
+ }
78
103
  merge(configData.esbuild ??= {}, {
79
104
  format: cliArgs.format,
80
105
  minify: cliArgs.minify,
81
106
  entryPoints: cliArgs.entryPoints,
82
107
  outfile: cliArgs.outfile,
83
108
  outdir: cliArgs.outdir,
84
- logLevel: cliArgs.logLevel || 'warning'
109
+ logLevel: cliArgs.logLevel || 'warning',
110
+ ...(cliArgs.external?.length ? { external: cliArgs.external } : {}),
111
+ ...(Object.keys(defineMap).length ? { define: defineMap } : {}),
85
112
  });
86
113
  if (configData.verbose)
87
114
  console.debug("configData", configData);
@@ -60,4 +60,33 @@ export interface CliBuildOptions {
60
60
  * `--outfile`. Default: false.
61
61
  */
62
62
  shebang?: boolean;
63
+ /**
64
+ * Module names that should NOT be bundled. Each name remains as a literal
65
+ * `import`/`require` in the output and is resolved by the runtime against
66
+ * its own `node_modules` (or equivalent) at execution time.
67
+ *
68
+ * Repeat the flag or pass a comma-separated value:
69
+ * `--external typedoc,prettier --external typescript`. Glob-style wildcards
70
+ * (`@inquirer/*`, `lodash-*`) are forwarded as-is to esbuild.
71
+ *
72
+ * @see https://esbuild.github.io/api/#external
73
+ */
74
+ external?: string[];
75
+ /**
76
+ * Substitute compile-time constants in the bundle. Each entry is a
77
+ * `KEY=VALUE` pair where `VALUE` is an arbitrary JS expression — string
78
+ * literals must be quoted (`--define VERSION='"1.2.3"'`). Useful for
79
+ * upstream packages that read a build-time constant via
80
+ * `typeof __FOO__ !== 'undefined'`.
81
+ *
82
+ * @see https://esbuild.github.io/api/#define
83
+ */
84
+ define?: string[];
85
+ /**
86
+ * Map module specifiers to alternative targets at bundle time. Each entry
87
+ * is `FROM=TO` where `FROM` is the imported package name and `TO` is the
88
+ * substitute (typically `@gjsify/empty` to drop a heavy dep that the test
89
+ * scenario never executes). Layered on top of the built-in alias map.
90
+ */
91
+ alias?: string[];
63
92
  }
@@ -22,4 +22,9 @@ export interface ConfigData {
22
22
  * Prepend GJS shebang to output and mark executable. See CliBuildOptions.
23
23
  */
24
24
  shebang?: boolean;
25
+ /**
26
+ * Extra module aliases layered on top of the built-in alias map.
27
+ * Comes from `gjsify build --alias FROM=TO`.
28
+ */
29
+ aliases?: Record<string, string>;
25
30
  }
@@ -5,11 +5,17 @@ export interface NativePackage {
5
5
  prebuildsDir: string;
6
6
  }
7
7
  /**
8
- * Walk up the directory tree from `startDir` looking for a node_modules directory.
9
- * Returns all native packages found in the first node_modules encountered.
8
+ * Walk up the directory tree from `startDir` and merge native packages found
9
+ * in every `node_modules` encountered.
10
10
  *
11
- * Note: This intentionally stops at the first node_modules to match how Node.js
12
- * resolves packages from the perspective of the calling project.
11
+ * We keep walking past the first node_modules because yarn v4 / pnpm hoisting
12
+ * puts a project's direct deps in a local node_modules (often just `.cache/`
13
+ * or a subset) while hoisted transitive deps live in a root `node_modules`
14
+ * higher up. Node's own resolver also walks the chain — returning only the
15
+ * first hit would miss root-hoisted native packages.
16
+ *
17
+ * Deduplication: the first match for a given package name wins (closer
18
+ * node_modules shadows outer ones), matching Node.js resolution semantics.
13
19
  */
14
20
  export declare function detectNativePackages(startDir: string): NativePackage[];
15
21
  /**
@@ -85,27 +85,39 @@ function checkPackage(pkgDir, name, arch) {
85
85
  return { name, prebuildsDir };
86
86
  }
87
87
  /**
88
- * Walk up the directory tree from `startDir` looking for a node_modules directory.
89
- * Returns all native packages found in the first node_modules encountered.
88
+ * Walk up the directory tree from `startDir` and merge native packages found
89
+ * in every `node_modules` encountered.
90
90
  *
91
- * Note: This intentionally stops at the first node_modules to match how Node.js
92
- * resolves packages from the perspective of the calling project.
91
+ * We keep walking past the first node_modules because yarn v4 / pnpm hoisting
92
+ * puts a project's direct deps in a local node_modules (often just `.cache/`
93
+ * or a subset) while hoisted transitive deps live in a root `node_modules`
94
+ * higher up. Node's own resolver also walks the chain — returning only the
95
+ * first hit would miss root-hoisted native packages.
96
+ *
97
+ * Deduplication: the first match for a given package name wins (closer
98
+ * node_modules shadows outer ones), matching Node.js resolution semantics.
93
99
  */
94
100
  export function detectNativePackages(startDir) {
95
101
  const arch = nodeArchToLinuxArch(process.arch);
102
+ const merged = [];
103
+ const seen = new Set();
96
104
  let dir = resolve(startDir);
97
- // Walk up to filesystem root
98
105
  while (true) {
99
106
  const nodeModulesDir = join(dir, 'node_modules');
100
107
  if (existsSync(nodeModulesDir)) {
101
- return scanNodeModules(nodeModulesDir, arch);
108
+ for (const pkg of scanNodeModules(nodeModulesDir, arch)) {
109
+ if (seen.has(pkg.name))
110
+ continue;
111
+ seen.add(pkg.name);
112
+ merged.push(pkg);
113
+ }
102
114
  }
103
115
  const parent = resolve(dir, '..');
104
116
  if (parent === dir)
105
117
  break; // reached filesystem root
106
118
  dir = parent;
107
119
  }
108
- return [];
120
+ return merged;
109
121
  }
110
122
  /** Walk up from dir to find the nearest package.json. */
111
123
  function findNearestPackageJson(startDir) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.1.13",
3
+ "version": "0.2.0",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -23,24 +23,23 @@
23
23
  "cli"
24
24
  ],
25
25
  "dependencies": {
26
- "@gjsify/create-app": "^0.1.13",
27
- "@gjsify/esbuild-plugin-gjsify": "^0.1.13",
28
- "@gjsify/example-dom-adwaita-package-builder": "^0.1.13",
29
- "@gjsify/example-dom-canvas2d-fireworks": "^0.1.13",
30
- "@gjsify/example-dom-excalibur-jelly-jumper": "^0.1.13",
31
- "@gjsify/example-dom-three-geometry-teapot": "^0.1.13",
32
- "@gjsify/example-dom-three-postprocessing-pixel": "^0.1.13",
33
- "@gjsify/example-node-express-webserver": "^0.1.13",
34
- "@gjsify/node-polyfills": "^0.1.13",
35
- "@gjsify/web-polyfills": "^0.1.13",
26
+ "@gjsify/create-app": "^0.2.0",
27
+ "@gjsify/esbuild-plugin-gjsify": "^0.2.0",
28
+ "@gjsify/example-dom-canvas2d-fireworks": "^0.2.0",
29
+ "@gjsify/example-dom-excalibur-jelly-jumper": "^0.2.0",
30
+ "@gjsify/example-dom-three-geometry-teapot": "^0.2.0",
31
+ "@gjsify/example-dom-three-postprocessing-pixel": "^0.2.0",
32
+ "@gjsify/example-node-express-webserver": "^0.2.0",
33
+ "@gjsify/node-polyfills": "^0.2.0",
34
+ "@gjsify/web-polyfills": "^0.2.0",
36
35
  "cosmiconfig": "^9.0.1",
37
36
  "esbuild": "^0.28.0",
38
37
  "get-tsconfig": "^4.14.0",
39
- "pkg-types": "^2.3.0",
38
+ "pkg-types": "^2.3.1",
40
39
  "yargs": "^18.0.0"
41
40
  },
42
41
  "devDependencies": {
43
42
  "@types/yargs": "^17.0.35",
44
- "typescript": "^6.0.2"
43
+ "typescript": "^6.0.3"
45
44
  }
46
45
  }
@@ -149,7 +149,7 @@ export class BuildAction {
149
149
  /** Application mode */
150
150
  async buildApp(app: App = 'gjs') {
151
151
 
152
- const { verbose, esbuild, typescript, exclude, library: pgk } = this.configData;
152
+ const { verbose, esbuild, typescript, exclude, library: pgk, aliases } = this.configData;
153
153
 
154
154
  const format: 'esm' | 'cjs' = (esbuild?.format as 'esm' | 'cjs') ?? (esbuild?.outfile?.endsWith('.cjs') ? 'cjs' : 'esm');
155
155
 
@@ -167,6 +167,7 @@ export class BuildAction {
167
167
  exclude,
168
168
  reflection: typescript?.reflection,
169
169
  consoleShim,
170
+ ...(aliases ? { aliases } : {}),
170
171
  };
171
172
 
172
173
  const { autoMode, extras } = this.parseGlobalsValue(globals);
@@ -100,6 +100,26 @@ export const buildCommand: Command<any, CliBuildOptions> = {
100
100
  normalize: true,
101
101
  default: false
102
102
  })
103
+ .option('external', {
104
+ description: "Module names that should NOT be bundled. Repeat the flag or pass a comma-separated list (e.g. --external typedoc,prettier). Globs are forwarded to esbuild as-is. See https://esbuild.github.io/api/#external",
105
+ array: true,
106
+ type: 'string',
107
+ default: [] as string[],
108
+ coerce: (arg: string[]) => arg.flatMap((v) => v.split(',').map((s) => s.trim()).filter(Boolean)),
109
+ })
110
+ .option('define', {
111
+ description: "Substitute compile-time constants. Each entry is KEY=VALUE where VALUE is a JS expression (string literals must be quoted: --define VERSION='\"1.2.3\"'). Repeat the flag or pass comma-separated. See https://esbuild.github.io/api/#define",
112
+ array: true,
113
+ type: 'string',
114
+ default: [] as string[],
115
+ })
116
+ .option('alias', {
117
+ description: "Map module specifiers at bundle time. Each entry is FROM=TO (e.g. --alias typedoc=@gjsify/empty). Layered on top of the built-in alias map. Useful for stubbing heavy deps the test scenario never executes.",
118
+ array: true,
119
+ type: 'string',
120
+ default: [] as string[],
121
+ coerce: (arg: string[]) => arg.flatMap((v) => v.split(',').map((s) => s.trim()).filter(Boolean)),
122
+ })
103
123
  },
104
124
  handler: async (args) => {
105
125
  const config = new Config();
package/src/config.ts CHANGED
@@ -87,13 +87,42 @@ export class Config {
87
87
 
88
88
  merge(configData.library ??= {}, pkg, configData.library);
89
89
  merge(configData.typescript ??= {}, tsConfig, configData.typescript);
90
+
91
+ // Parse `KEY=VALUE` style flags into Record<string, string>.
92
+ // - `--define`: VALUE is a JS expression (string literals must be
93
+ // pre-quoted by the caller, e.g. `'"1.2.3"'`).
94
+ // - `--alias`: VALUE is the substitute module specifier.
95
+ const parseKvPairs = (entries: readonly string[], flag: string): Record<string, string> => {
96
+ const out: Record<string, string> = {};
97
+ for (const entry of entries) {
98
+ const idx = entry.indexOf('=');
99
+ if (idx === -1) {
100
+ throw new Error(`Invalid --${flag} value '${entry}'. Expected KEY=VALUE.`);
101
+ }
102
+ const key = entry.slice(0, idx).trim();
103
+ const value = entry.slice(idx + 1);
104
+ if (!key) {
105
+ throw new Error(`Invalid --${flag} value '${entry}'. Empty key.`);
106
+ }
107
+ out[key] = value;
108
+ }
109
+ return out;
110
+ };
111
+ const defineMap = parseKvPairs(cliArgs.define ?? [], 'define');
112
+ const aliasMap = parseKvPairs(cliArgs.alias ?? [], 'alias');
113
+ if (Object.keys(aliasMap).length) {
114
+ configData.aliases = { ...(configData.aliases ?? {}), ...aliasMap };
115
+ }
116
+
90
117
  merge(configData.esbuild ??= {}, {
91
118
  format: cliArgs.format,
92
119
  minify: cliArgs.minify,
93
120
  entryPoints: cliArgs.entryPoints,
94
121
  outfile: cliArgs.outfile,
95
122
  outdir: cliArgs.outdir,
96
- logLevel: cliArgs.logLevel || 'warning'
123
+ logLevel: cliArgs.logLevel || 'warning',
124
+ ...(cliArgs.external?.length ? { external: cliArgs.external } : {}),
125
+ ...(Object.keys(defineMap).length ? { define: defineMap } : {}),
97
126
  });
98
127
 
99
128
  if(configData.verbose) console.debug("configData", configData);
@@ -61,4 +61,33 @@ export interface CliBuildOptions {
61
61
  * `--outfile`. Default: false.
62
62
  */
63
63
  shebang?: boolean;
64
+ /**
65
+ * Module names that should NOT be bundled. Each name remains as a literal
66
+ * `import`/`require` in the output and is resolved by the runtime against
67
+ * its own `node_modules` (or equivalent) at execution time.
68
+ *
69
+ * Repeat the flag or pass a comma-separated value:
70
+ * `--external typedoc,prettier --external typescript`. Glob-style wildcards
71
+ * (`@inquirer/*`, `lodash-*`) are forwarded as-is to esbuild.
72
+ *
73
+ * @see https://esbuild.github.io/api/#external
74
+ */
75
+ external?: string[];
76
+ /**
77
+ * Substitute compile-time constants in the bundle. Each entry is a
78
+ * `KEY=VALUE` pair where `VALUE` is an arbitrary JS expression — string
79
+ * literals must be quoted (`--define VERSION='"1.2.3"'`). Useful for
80
+ * upstream packages that read a build-time constant via
81
+ * `typeof __FOO__ !== 'undefined'`.
82
+ *
83
+ * @see https://esbuild.github.io/api/#define
84
+ */
85
+ define?: string[];
86
+ /**
87
+ * Map module specifiers to alternative targets at bundle time. Each entry
88
+ * is `FROM=TO` where `FROM` is the imported package name and `TO` is the
89
+ * substitute (typically `@gjsify/empty` to drop a heavy dep that the test
90
+ * scenario never executes). Layered on top of the built-in alias map.
91
+ */
92
+ alias?: string[];
64
93
  }
@@ -23,4 +23,9 @@ export interface ConfigData {
23
23
  * Prepend GJS shebang to output and mark executable. See CliBuildOptions.
24
24
  */
25
25
  shebang?: boolean;
26
+ /**
27
+ * Extra module aliases layered on top of the built-in alias map.
28
+ * Comes from `gjsify build --alias FROM=TO`.
29
+ */
30
+ aliases?: Record<string, string>;
26
31
  }
@@ -94,28 +94,39 @@ function checkPackage(pkgDir: string, name: string, arch: string): NativePackage
94
94
  }
95
95
 
96
96
  /**
97
- * Walk up the directory tree from `startDir` looking for a node_modules directory.
98
- * Returns all native packages found in the first node_modules encountered.
97
+ * Walk up the directory tree from `startDir` and merge native packages found
98
+ * in every `node_modules` encountered.
99
99
  *
100
- * Note: This intentionally stops at the first node_modules to match how Node.js
101
- * resolves packages from the perspective of the calling project.
100
+ * We keep walking past the first node_modules because yarn v4 / pnpm hoisting
101
+ * puts a project's direct deps in a local node_modules (often just `.cache/`
102
+ * or a subset) while hoisted transitive deps live in a root `node_modules`
103
+ * higher up. Node's own resolver also walks the chain — returning only the
104
+ * first hit would miss root-hoisted native packages.
105
+ *
106
+ * Deduplication: the first match for a given package name wins (closer
107
+ * node_modules shadows outer ones), matching Node.js resolution semantics.
102
108
  */
103
109
  export function detectNativePackages(startDir: string): NativePackage[] {
104
110
  const arch = nodeArchToLinuxArch(process.arch);
111
+ const merged: NativePackage[] = [];
112
+ const seen = new Set<string>();
105
113
  let dir = resolve(startDir);
106
114
 
107
- // Walk up to filesystem root
108
115
  while (true) {
109
116
  const nodeModulesDir = join(dir, 'node_modules');
110
117
  if (existsSync(nodeModulesDir)) {
111
- return scanNodeModules(nodeModulesDir, arch);
118
+ for (const pkg of scanNodeModules(nodeModulesDir, arch)) {
119
+ if (seen.has(pkg.name)) continue;
120
+ seen.add(pkg.name);
121
+ merged.push(pkg);
122
+ }
112
123
  }
113
124
  const parent = resolve(dir, '..');
114
125
  if (parent === dir) break; // reached filesystem root
115
126
  dir = parent;
116
127
  }
117
128
 
118
- return [];
129
+ return merged;
119
130
  }
120
131
 
121
132
  /** Walk up from dir to find the nearest package.json. */