@gjsify/cli 0.1.7 → 0.1.9

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.
Files changed (36) hide show
  1. package/lib/actions/build.d.ts +19 -0
  2. package/lib/actions/build.js +80 -11
  3. package/lib/commands/build.js +6 -0
  4. package/lib/commands/check.js +41 -12
  5. package/lib/commands/create.d.ts +6 -0
  6. package/lib/commands/create.js +15 -0
  7. package/lib/commands/index.d.ts +1 -0
  8. package/lib/commands/index.js +1 -0
  9. package/lib/commands/showcase.js +35 -33
  10. package/lib/config.js +2 -0
  11. package/lib/index.js +2 -1
  12. package/lib/types/cli-build-options.d.ts +7 -0
  13. package/lib/types/config-data.d.ts +5 -0
  14. package/lib/utils/check-system-deps.d.ts +14 -2
  15. package/lib/utils/check-system-deps.js +214 -46
  16. package/lib/utils/detect-native-packages.d.ts +4 -0
  17. package/lib/utils/detect-native-packages.js +12 -0
  18. package/lib/utils/discover-showcases.d.ts +19 -0
  19. package/lib/utils/{discover-examples.js → discover-showcases.js} +14 -15
  20. package/lib/utils/run-gjs.js +9 -0
  21. package/package.json +10 -50
  22. package/src/actions/build.ts +99 -13
  23. package/src/commands/build.ts +6 -0
  24. package/src/commands/check.ts +45 -12
  25. package/src/commands/create.ts +21 -0
  26. package/src/commands/index.ts +2 -1
  27. package/src/commands/showcase.ts +36 -34
  28. package/src/config.ts +1 -0
  29. package/src/index.ts +2 -1
  30. package/src/types/cli-build-options.ts +8 -1
  31. package/src/types/config-data.ts +6 -1
  32. package/src/utils/check-system-deps.ts +264 -45
  33. package/src/utils/detect-native-packages.ts +12 -0
  34. package/src/utils/{discover-examples.ts → discover-showcases.ts} +18 -19
  35. package/src/utils/run-gjs.ts +11 -0
  36. package/lib/utils/discover-examples.d.ts +0 -19
@@ -2,6 +2,7 @@ import type { ConfigData } from '../types/index.js';
2
2
  import type { App } from '@gjsify/esbuild-plugin-gjsify';
3
3
  import { build, BuildOptions, BuildResult } from 'esbuild';
4
4
  import { gjsifyPlugin } from '@gjsify/esbuild-plugin-gjsify';
5
+ import { resolveGlobalsList, writeRegisterInjectFile, detectAutoGlobals } from '@gjsify/esbuild-plugin-gjsify/globals';
5
6
  import { dirname, extname } from 'path';
6
7
 
7
8
  export class BuildAction {
@@ -74,6 +75,54 @@ export class BuildAction {
74
75
  return results;
75
76
  }
76
77
 
78
+ /**
79
+ * Parse the `--globals` value into { autoMode, extras }.
80
+ * - `auto` → { autoMode: true, extras: '' }
81
+ * - `auto,dom` → { autoMode: true, extras: 'dom' }
82
+ * - `auto,dom,fetch` → { autoMode: true, extras: 'dom,fetch' }
83
+ * - `dom,fetch` → { autoMode: false, extras: 'dom,fetch' }
84
+ * - `none` / `` → { autoMode: false, extras: '' }
85
+ * - `undefined` → { autoMode: true, extras: '' } (default)
86
+ */
87
+ private parseGlobalsValue(value: string | undefined): { autoMode: boolean; extras: string } {
88
+ if (value === undefined) return { autoMode: true, extras: '' };
89
+ if (value === 'none' || value === '') return { autoMode: false, extras: '' };
90
+
91
+ const tokens = value.split(',').map(t => t.trim()).filter(Boolean);
92
+ const hasAuto = tokens.includes('auto');
93
+ const extras = tokens.filter(t => t !== 'auto').join(',');
94
+
95
+ return { autoMode: hasAuto, extras };
96
+ }
97
+
98
+ /**
99
+ * Resolve the `--globals` CLI list into a pre-computed inject stub path
100
+ * that the esbuild plugin will append to its `inject` list. Only runs
101
+ * for `--app gjs` — Node and browser builds rely on native globals.
102
+ *
103
+ * Used only for the explicit-only path (no `auto` token in the value).
104
+ * The auto path is handled in `buildApp` via the two-pass build.
105
+ */
106
+ private async resolveGlobalsInject(
107
+ app: App,
108
+ globals: string,
109
+ verbose: boolean | undefined,
110
+ ): Promise<string | undefined> {
111
+ if (app !== 'gjs') return undefined;
112
+ if (!globals) return undefined;
113
+
114
+ const registerPaths = resolveGlobalsList(globals);
115
+ if (registerPaths.size === 0) return undefined;
116
+
117
+ const injectPath = await writeRegisterInjectFile(registerPaths, process.cwd());
118
+ if (verbose && injectPath) {
119
+ console.debug(
120
+ `[gjsify] globals: injected ${registerPaths.size} register module(s) from --globals ${globals}`,
121
+ );
122
+ }
123
+ return injectPath ?? undefined;
124
+ }
125
+
77
126
  /** Application mode */
78
127
  async buildApp(app: App = 'gjs') {
79
128
 
@@ -81,31 +130,68 @@ export class BuildAction {
81
130
 
82
131
  const format: 'esm' | 'cjs' = (esbuild?.format as 'esm' | 'cjs') ?? (esbuild?.outfile?.endsWith('.cjs') ? 'cjs' : 'esm');
83
132
 
84
- // Set default outfile if no outdir is set
133
+ // Set default outfile if no outdir is set
85
134
  if(esbuild && !esbuild?.outfile && !esbuild?.outdir && (pgk?.main || pgk?.module)) {
86
135
  esbuild.outfile = esbuild?.format === 'cjs' ? pgk.main || pgk.module : pgk.module || pgk.main;
87
136
  }
88
137
 
89
- const { consoleShim } = this.configData;
138
+ const { consoleShim, globals } = this.configData;
139
+
140
+ const pluginOpts = {
141
+ debug: verbose,
142
+ app,
143
+ format,
144
+ exclude,
145
+ reflection: typescript?.reflection,
146
+ consoleShim,
147
+ };
148
+
149
+ const { autoMode, extras } = this.parseGlobalsValue(globals);
150
+
151
+ // --- Auto mode (with optional extras): iterative multi-pass build ---
152
+ // The extras token is used for cases where the detector cannot
153
+ // statically see a global (e.g. Excalibur indirects globalThis via
154
+ // BrowserComponent.nativeComponent). Common pattern: --globals auto,dom
155
+ if (app === 'gjs' && autoMode) {
156
+ const { injectPath } = await detectAutoGlobals(
157
+ { ...this.getEsBuildDefaults(), ...esbuild, format },
158
+ pluginOpts,
159
+ verbose,
160
+ { extraGlobalsList: extras },
161
+ );
162
+
163
+ const result = await build({
164
+ ...this.getEsBuildDefaults(),
165
+ ...esbuild,
166
+ format,
167
+ plugins: [
168
+ gjsifyPlugin({
169
+ ...pluginOpts,
170
+ autoGlobalsInject: injectPath,
171
+ }),
172
+ ],
173
+ });
174
+
175
+ return [result];
176
+ }
177
+
178
+ // --- Explicit list (no `auto` token) or none mode ---
179
+ const autoGlobalsInject = extras
180
+ ? await this.resolveGlobalsInject(app, extras, verbose)
181
+ : undefined;
182
+
90
183
  const result = await build({
91
184
  ...this.getEsBuildDefaults(),
92
185
  ...esbuild,
93
186
  format,
94
187
  plugins: [
95
- gjsifyPlugin({debug: verbose, app, format, exclude, reflection: typescript?.reflection, consoleShim}),
188
+ gjsifyPlugin({
189
+ ...pluginOpts,
190
+ autoGlobalsInject,
191
+ }),
96
192
  ]
97
193
  });
98
194
 
99
- // See https://esbuild.github.io/api/#metafile
100
- // TODO add cli options for this
101
- // if(result.metafile) {
102
- // const outFile = esbuild?.outfile ? esbuild.outfile + '.meta.json' : 'meta.json';
103
- // await writeFile(outFile, JSON.stringify(result.metafile));
104
-
105
- // let text = await analyzeMetafile(result.metafile)
106
- // console.log(text)
107
- // }
108
-
109
195
  return [result];
110
196
  }
111
197
 
@@ -88,6 +88,12 @@ export const buildCommand: Command<any, CliBuildOptions> = {
88
88
  normalize: true,
89
89
  default: true
90
90
  })
91
+ .option('globals', {
92
+ description: "Comma-separated list of global identifiers, 'auto' (default) to detect automatically from the bundled output, or 'none' to disable. The 'auto' token may be combined with explicit identifiers/groups (e.g. 'auto,dom') for cases where the detector cannot statically see a global because it's accessed via indirection. Each identifier is mapped to the corresponding `@gjsify/<pkg>/register` module and injected into the bundle. See the CLI Reference docs for the full list of known identifiers. Only applies to GJS app builds.",
93
+ type: 'string',
94
+ normalize: true,
95
+ default: 'auto'
96
+ })
91
97
  },
92
98
  handler: async (args) => {
93
99
  const config = new Config();
@@ -7,7 +7,7 @@ interface CheckOptions {
7
7
 
8
8
  export const checkCommand: Command<any, CheckOptions> = {
9
9
  command: 'check',
10
- description: 'Check that all required system dependencies (GJS, GTK4, Blueprint Compiler, etc.) are installed.',
10
+ description: 'Check that required system dependencies (GJS, GTK4, libsoup3, ) are installed. Optional dependencies are detected only when their @gjsify/* package is in your project.',
11
11
  builder: (yargs) => {
12
12
  return yargs
13
13
  .option('json', {
@@ -19,36 +19,69 @@ export const checkCommand: Command<any, CheckOptions> = {
19
19
  handler: async (args) => {
20
20
  const results = runAllChecks(process.cwd());
21
21
  const pm = detectPackageManager();
22
- const missing = results.filter(r => !r.found);
22
+ const missingRequired = results.filter(r => !r.found && r.severity === 'required');
23
+ const missingOptional = results.filter(r => !r.found && r.severity === 'optional');
24
+ const allMissing = [...missingRequired, ...missingOptional];
23
25
 
24
26
  if (args.json) {
25
27
  console.log(JSON.stringify({ packageManager: pm, deps: results }, null, 2));
26
- process.exit(missing.length > 0 ? 1 : 0);
28
+ // Only required deps influence the exit code.
29
+ process.exit(missingRequired.length > 0 ? 1 : 0);
27
30
  return;
28
31
  }
29
32
 
30
33
  console.log('System dependency check\n');
31
- for (const dep of results) {
32
- const icon = dep.found ? '✓' : '';
33
- const ver = dep.version ? ` (${dep.version})` : '';
34
- console.log(` ${icon} ${dep.name}${ver}`);
34
+
35
+ const required = results.filter(r => r.severity === 'required');
36
+ const optional = results.filter(r => r.severity === 'optional');
37
+
38
+ if (required.length > 0) {
39
+ console.log('Required:');
40
+ for (const dep of required) {
41
+ const icon = dep.found ? '✓' : '✗';
42
+ const ver = dep.version ? ` (${dep.version})` : '';
43
+ console.log(` ${icon} ${dep.name}${ver}`);
44
+ }
45
+ }
46
+
47
+ if (optional.length > 0) {
48
+ console.log('\nOptional:');
49
+ for (const dep of optional) {
50
+ // ⚠ for missing-but-needed-by-installed-packages, ○ for missing-but-not-needed (shouldn't appear in conditional mode)
51
+ const icon = dep.found ? '✓' : '⚠';
52
+ const ver = dep.version ? ` (${dep.version})` : '';
53
+ const requiredBy = dep.requiredBy && dep.requiredBy.length > 0
54
+ ? ` — needed by ${dep.requiredBy.join(', ')}`
55
+ : '';
56
+ console.log(` ${icon} ${dep.name}${ver}${requiredBy}`);
57
+ }
35
58
  }
36
59
 
37
60
  console.log(`\nPackage manager: ${pm}`);
38
61
 
39
- if (missing.length === 0) {
62
+ if (allMissing.length === 0) {
40
63
  console.log('\nAll dependencies found.');
41
64
  return;
42
65
  }
43
66
 
44
- console.log(`\nMissing: ${missing.map(d => d.name).join(', ')}`);
45
- const cmd = buildInstallCommand(pm, missing);
67
+ if (missingRequired.length > 0) {
68
+ console.log(`\nMissing required: ${missingRequired.map(d => d.name).join(', ')}`);
69
+ }
70
+ if (missingOptional.length > 0) {
71
+ console.log(`Missing optional: ${missingOptional.map(d => d.name).join(', ')}`);
72
+ }
73
+
74
+ const cmd = buildInstallCommand(pm, allMissing);
46
75
  if (cmd) {
47
- console.log(`\nTo install missing dependencies:\n ${cmd}`);
76
+ console.log(`\nTo install:\n ${cmd}`);
48
77
  } else {
49
78
  console.log('\nNo install command available for your package manager. Install manually.');
50
79
  }
51
80
 
52
- process.exit(1);
81
+ // Exit non-zero ONLY if a required dependency is missing.
82
+ // Optional deps that are missing but needed by an installed @gjsify/*
83
+ // package generate a warning but keep exit code 0 — the user can still
84
+ // build/run code paths that don't touch the optional library.
85
+ process.exit(missingRequired.length > 0 ? 1 : 0);
53
86
  },
54
87
  };
@@ -0,0 +1,21 @@
1
+ import { createProject } from '@gjsify/create-app';
2
+ import type { Command } from '../types/index.js';
3
+
4
+ interface CreateOptions {
5
+ 'project-name': string;
6
+ }
7
+
8
+ export const createCommand: Command<any, CreateOptions> = {
9
+ command: 'create [project-name]',
10
+ description: 'Scaffold a new Gjsify project in a new directory.',
11
+ builder: (yargs) => {
12
+ return yargs.positional('project-name', {
13
+ describe: 'Name of the project directory to create',
14
+ type: 'string',
15
+ default: 'my-gjs-app',
16
+ });
17
+ },
18
+ handler: async (args) => {
19
+ await createProject(args['project-name']);
20
+ },
21
+ };
@@ -2,4 +2,5 @@ export * from './build.js';
2
2
  export * from './run.js';
3
3
  export * from './info.js';
4
4
  export * from './check.js';
5
- export * from './showcase.js';
5
+ export * from './showcase.js';
6
+ export * from './create.js';
@@ -1,5 +1,5 @@
1
1
  import type { Command } from '../types/index.js';
2
- import { discoverExamples, findExample } from '../utils/discover-examples.js';
2
+ import { discoverShowcases, findShowcase } from '../utils/discover-showcases.js';
3
3
  import { runMinimalChecks, checkGwebgl, detectPackageManager, buildInstallCommand } from '../utils/check-system-deps.js';
4
4
  import { runGjsBundle } from '../utils/run-gjs.js';
5
5
 
@@ -11,11 +11,11 @@ interface ShowcaseOptions {
11
11
 
12
12
  export const showcaseCommand: Command<any, ShowcaseOptions> = {
13
13
  command: 'showcase [name]',
14
- description: 'List or run built-in gjsify example applications.',
14
+ description: 'List or run built-in gjsify showcase applications.',
15
15
  builder: (yargs) => {
16
16
  return yargs
17
17
  .positional('name', {
18
- description: 'Example name to run (omit to list all)',
18
+ description: 'Showcase name to run (omit to list all)',
19
19
  type: 'string',
20
20
  })
21
21
  .option('json', {
@@ -24,7 +24,7 @@ export const showcaseCommand: Command<any, ShowcaseOptions> = {
24
24
  default: false,
25
25
  })
26
26
  .option('list', {
27
- description: 'List available examples',
27
+ description: 'List available showcases',
28
28
  type: 'boolean',
29
29
  default: false,
30
30
  });
@@ -32,73 +32,75 @@ export const showcaseCommand: Command<any, ShowcaseOptions> = {
32
32
  handler: async (args) => {
33
33
  // List mode: no name given, or --list flag
34
34
  if (!args.name || args.list) {
35
- const examples = discoverExamples();
35
+ const showcases = discoverShowcases();
36
36
 
37
37
  if (args.json) {
38
- console.log(JSON.stringify(examples, null, 2));
38
+ console.log(JSON.stringify(showcases, null, 2));
39
39
  return;
40
40
  }
41
41
 
42
- if (examples.length === 0) {
43
- console.log('No examples found. Example packages may not be installed.');
42
+ if (showcases.length === 0) {
43
+ console.log('No showcases found. Showcase packages may not be installed.');
44
44
  return;
45
45
  }
46
46
 
47
47
  // Group by category
48
- const grouped = new Map<string, typeof examples>();
49
- for (const ex of examples) {
50
- const list = grouped.get(ex.category) ?? [];
51
- list.push(ex);
52
- grouped.set(ex.category, list);
48
+ const grouped = new Map<string, typeof showcases>();
49
+ for (const sc of showcases) {
50
+ const list = grouped.get(sc.category) ?? [];
51
+ list.push(sc);
52
+ grouped.set(sc.category, list);
53
53
  }
54
54
 
55
- console.log('Available gjsify examples:\n');
55
+ console.log('Available gjsify showcases:\n');
56
56
  for (const [category, list] of grouped) {
57
57
  console.log(` ${category.toUpperCase()}:`);
58
58
  const maxNameLen = Math.max(...list.map(e => e.name.length));
59
- for (const ex of list) {
60
- const pad = ' '.repeat(maxNameLen - ex.name.length + 2);
61
- const desc = ex.description ? `${pad}${ex.description}` : '';
62
- console.log(` ${ex.name}${desc}`);
59
+ for (const sc of list) {
60
+ const pad = ' '.repeat(maxNameLen - sc.name.length + 2);
61
+ const desc = sc.description ? `${pad}${sc.description}` : '';
62
+ console.log(` ${sc.name}${desc}`);
63
63
  }
64
64
  console.log('');
65
65
  }
66
66
 
67
- console.log('Run an example: gjsify showcase <name>');
67
+ console.log('Run a showcase: gjsify showcase <name>');
68
68
  return;
69
69
  }
70
70
 
71
- // Run mode: find the example
72
- const example = findExample(args.name);
73
- if (!example) {
74
- console.error(`Unknown example: "${args.name}"`);
75
- console.error('Run "gjsify showcase" to list available examples.');
71
+ // Run mode: find the showcase
72
+ const showcase = findShowcase(args.name);
73
+ if (!showcase) {
74
+ console.error(`Unknown showcase: "${args.name}"`);
75
+ console.error('Run "gjsify showcase" to list available showcases.');
76
76
  process.exit(1);
77
77
  }
78
78
 
79
- // System dependency check before running — only check what this example needs.
80
- // All examples need GJS; WebGL examples additionally need gwebgl prebuilds.
79
+ // System dependency check before running — only check what this showcase needs.
80
+ // All showcases need GJS; WebGL showcases additionally need gwebgl prebuilds.
81
81
  const results = runMinimalChecks();
82
- const needsWebgl = example.packageName.includes('webgl') || example.packageName.includes('three');
82
+ const needsWebgl = showcase.packageName.includes('webgl') || showcase.packageName.includes('three');
83
83
  if (needsWebgl) {
84
84
  results.push(checkGwebgl(process.cwd()));
85
85
  }
86
- const missing = results.filter(r => !r.found);
87
- if (missing.length > 0) {
86
+ // Hard-fail only on missing REQUIRED deps (gjs, gwebgl is required if needsWebgl).
87
+ // For showcase, gwebgl is treated as required because the bundle won't run without it.
88
+ const missingHard = results.filter(r => !r.found && (r.severity === 'required' || r.id === 'gwebgl'));
89
+ if (missingHard.length > 0) {
88
90
  console.error('Missing system dependencies:\n');
89
- for (const dep of missing) {
91
+ for (const dep of missingHard) {
90
92
  console.error(` ✗ ${dep.name}`);
91
93
  }
92
94
  const pm = detectPackageManager();
93
- const cmd = buildInstallCommand(pm, missing);
95
+ const cmd = buildInstallCommand(pm, missingHard);
94
96
  if (cmd) {
95
97
  console.error(`\nInstall with:\n ${cmd}`);
96
98
  }
97
99
  process.exit(1);
98
100
  }
99
101
 
100
- // Run the example via shared GJS runner
101
- console.log(`Running example: ${example.name}\n`);
102
- await runGjsBundle(example.bundlePath);
102
+ // Run the showcase via shared GJS runner
103
+ console.log(`Running showcase: ${showcase.name}\n`);
104
+ await runGjsBundle(showcase.bundlePath);
103
105
  },
104
106
  };
package/src/config.ts CHANGED
@@ -82,6 +82,7 @@ export class Config {
82
82
  configData.verbose = cliArgs.verbose || false;
83
83
  configData.exclude = cliArgs.exclude || [];
84
84
  if (cliArgs.consoleShim !== undefined) configData.consoleShim = cliArgs.consoleShim;
85
+ if (cliArgs.globals !== undefined) configData.globals = cliArgs.globals;
85
86
 
86
87
  merge(configData.library ??= {}, pkg, configData.library);
87
88
  merge(configData.typescript ??= {}, tsConfig, configData.typescript);
package/src/index.ts CHANGED
@@ -2,13 +2,14 @@
2
2
  import yargs from 'yargs'
3
3
  import { hideBin } from 'yargs/helpers'
4
4
 
5
- import { buildCommand as build, runCommand as run, infoCommand as info, checkCommand as check, showcaseCommand as showcase } from './commands/index.js'
5
+ import { buildCommand as build, runCommand as run, infoCommand as info, checkCommand as check, showcaseCommand as showcase, createCommand as create } from './commands/index.js'
6
6
  import { APP_NAME } from './constants.js'
7
7
 
8
8
  void yargs(hideBin(process.argv))
9
9
  .scriptName(APP_NAME)
10
10
  .strict()
11
11
  // .usage(Config.usage)
12
+ .command(create.command, create.description, create.builder, create.handler)
12
13
  .command(build.command, build.description, build.builder, build.handler)
13
14
  .command(run.command, run.description, run.builder, run.handler)
14
15
  .command(info.command, info.description, info.builder, info.handler)
@@ -48,4 +48,11 @@ export interface CliBuildOptions {
48
48
  * Use --no-console-shim to disable. Only applies to GJS app builds. Default: true.
49
49
  */
50
50
  consoleShim?: boolean;
51
- }
51
+ /**
52
+ * Comma-separated list of global identifiers your code needs (e.g.
53
+ * `"fetch,Buffer,process,URL,crypto"`). Each identifier is mapped to the
54
+ * corresponding `@gjsify/<pkg>/register` module and injected into the
55
+ * bundle. Only applies to GJS app builds.
56
+ */
57
+ globals?: string;
58
+ }
@@ -14,4 +14,9 @@ export interface ConfigData {
14
14
  * Only applies to GJS app builds. Default: true.
15
15
  */
16
16
  consoleShim?: boolean;
17
- }
17
+ /**
18
+ * Comma-separated list of global identifiers to register in the bundle.
19
+ * See CliBuildOptions for format.
20
+ */
21
+ globals?: string;
22
+ }