@gjsify/cli 0.3.4 → 0.3.6

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.
@@ -43,13 +43,16 @@ async function getPnpPlugin() {
43
43
  // as direct deps, so we can resolve them as relay issuers from here.
44
44
  const gjsifyIssuer = fileURLToPath(import.meta.url);
45
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).
46
+ // @gjsify/* packages as direct deps. Resolving from their package.json
47
+ // paths allows PnP to use them as issuers sub-path imports
48
+ // (`@gjsify/foo/register/bar`) then resolve through the polyfill's
49
+ // dep graph. Resolve to package.json (always present, exports-agnostic)
50
+ // rather than main/module (the polyfills meta packages have no main).
48
51
  const requireFromGjsify = createRequire(gjsifyIssuer);
49
52
  const relayIssuers = [];
50
53
  for (const pkg of ["@gjsify/node-polyfills", "@gjsify/web-polyfills"]) {
51
54
  try {
52
- relayIssuers.push(requireFromGjsify.resolve(pkg));
55
+ relayIssuers.push(requireFromGjsify.resolve(`${pkg}/package.json`));
53
56
  }
54
57
  catch {
55
58
  // polyfills package not in dep tree — relay won't cover it
@@ -57,10 +60,16 @@ async function getPnpPlugin() {
57
60
  }
58
61
  let pnpApi = null;
59
62
  try {
60
- // pnpapi has no npm package — it is a virtual module injected by Yarn PnP
63
+ // pnpapi has no npm package — it is a virtual CJS module injected by
64
+ // Yarn PnP. `await import()` of a CJS module yields the ESM namespace
65
+ // `{ default, "module.exports" }`, NOT the exports object — so
66
+ // `mod.resolveRequest` is `undefined`. Unwrap `.default` (the CJS
67
+ // exports) before use, falling back to the namespace itself for ESM
68
+ // builds of pnpapi (none today, but defensive).
61
69
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
62
70
  // @ts-expect-error
63
- pnpApi = (await import("pnpapi"));
71
+ const mod = await import("pnpapi");
72
+ pnpApi = (mod.default ?? mod);
64
73
  }
65
74
  catch {
66
75
  // Not in a PnP runtime (shouldn't happen since findPnpRoot passed)
@@ -93,10 +93,9 @@ export const buildCommand = {
93
93
  default: 'auto'
94
94
  })
95
95
  .option('shebang', {
96
- description: "Prepend a `#!/usr/bin/env -S gjs -m` shebang to the output and mark it executable (chmod 755). Only applies to GJS app builds with a single --outfile.",
96
+ description: "Prepend a `#!/usr/bin/env -S gjs -m` shebang to the output and mark it executable (chmod 755). Only applies to GJS app builds with a single --outfile. Default: false (use --shebang to enable, or set `shebang: true` in `.gjsifyrc.js`).",
97
97
  type: 'boolean',
98
- normalize: true,
99
- default: false
98
+ normalize: true
100
99
  })
101
100
  .option('external', {
102
101
  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",
@@ -0,0 +1,11 @@
1
+ import type { Command } from '../types/index.js';
2
+ interface DlxOptions {
3
+ spec: string;
4
+ binOrArg?: string;
5
+ extraArgs?: string[];
6
+ 'cache-max-age': number;
7
+ verbose: boolean;
8
+ registry?: string;
9
+ }
10
+ export declare const dlxCommand: Command<any, DlxOptions>;
11
+ export {};
@@ -0,0 +1,117 @@
1
+ // `gjsify dlx <package> [bin] [-- args...]` — runs the GJS bundle of an
2
+ // npm-published package without persisting it in the user's project.
3
+ //
4
+ // Cardinal rule: dlx is a **GJS-bundle runner**, not a generic bin runner.
5
+ // It always invokes `gjs -m <bundle>` via the existing `runGjsBundle()` util.
6
+ // Packages without a GJS entry (no `gjsify.main`/`gjsify.bin`, no fallback
7
+ // `main`) fail loudly.
8
+ //
9
+ // Cache: $XDG_CACHE_HOME/gjsify/dlx/<sha256>/ with TTL (default 7d, override
10
+ // via --cache-max-age=<minutes>). Cache hit on second run skips `npm install`
11
+ // entirely. Layout + atomic-swap pattern adapted from pnpm's dlx implementation
12
+ // (refs/pnpm/exec/commands/src/dlx.ts).
13
+ import { runGjsBundle } from '../utils/run-gjs.js';
14
+ import { parseSpec } from '../utils/parse-spec.js';
15
+ import { resolveGjsEntry } from '../utils/resolve-gjs-entry.js';
16
+ import { cacheDirFor, createCacheKey, getValidCachedPkg, makePrepareDir, resolveInstalledPkgDir, symlinkSwap, } from '../utils/dlx-cache.js';
17
+ import { installPackages } from '../utils/install-backend.js';
18
+ export const dlxCommand = {
19
+ command: 'dlx <spec> [binOrArg] [extraArgs..]',
20
+ description: 'Run the GJS bundle of an npm-published package without installing it locally.',
21
+ builder: (yargs) => yargs
22
+ .positional('spec', {
23
+ description: 'Package spec (`name`, `name@version`, `@scope/name@spec`, or local path).',
24
+ type: 'string',
25
+ demandOption: true,
26
+ })
27
+ .positional('binOrArg', {
28
+ description: 'Optional bin name when the package defines `gjsify.bin` with multiple entries; otherwise treated as the first argument forwarded to the bundle.',
29
+ type: 'string',
30
+ })
31
+ .positional('extraArgs', {
32
+ description: 'Extra args forwarded to `gjs -m <bundle>`.',
33
+ type: 'string',
34
+ array: true,
35
+ })
36
+ .option('cache-max-age', {
37
+ description: 'Cache TTL in minutes. Defaults to 7 days. Use 0 to bypass cache.',
38
+ type: 'number',
39
+ default: 60 * 24 * 7,
40
+ })
41
+ .option('verbose', {
42
+ description: 'Verbose logging (passes --loglevel verbose to npm).',
43
+ type: 'boolean',
44
+ default: false,
45
+ })
46
+ .option('registry', {
47
+ description: 'Registry URL override.',
48
+ type: 'string',
49
+ }),
50
+ handler: async (args) => {
51
+ const parsed = parseSpec(args.spec);
52
+ const { pkgDir, cachedPkgName } = await ensurePkgDir(parsed, {
53
+ verbose: args.verbose,
54
+ registry: args.registry,
55
+ cacheMaxAge: args['cache-max-age'],
56
+ });
57
+ // Bin / args disambiguation:
58
+ // gjsify dlx <pkg> → no bin, no args
59
+ // gjsify dlx <pkg> mybin → bin if package has gjsify.bin[mybin], else arg
60
+ // gjsify dlx <pkg> mybin -- arg1 arg2 → bin + extra args
61
+ // gjsify dlx <pkg> -- arg1 arg2 → no bin, extra args
62
+ const { binName, extraArgs } = splitBinAndArgs(pkgDir, args.binOrArg, args.extraArgs ?? []);
63
+ const entry = resolveGjsEntry(pkgDir, binName);
64
+ if (entry.fromFallback) {
65
+ console.warn(`[gjsify dlx] package "${cachedPkgName ?? parsed.kind}" has no \`gjsify\` field — falling back to package.json#main. Add \`gjsify.main\` to silence.`);
66
+ }
67
+ await runGjsBundle(entry.bundlePath, extraArgs);
68
+ },
69
+ };
70
+ async function ensurePkgDir(parsed, opts) {
71
+ if (parsed.kind === 'local') {
72
+ return { pkgDir: parsed.path, cachedPkgName: null };
73
+ }
74
+ const cacheKey = createCacheKey({ packages: [parsed.spec] });
75
+ const cacheDir = cacheDirFor(cacheKey);
76
+ const cached = opts.cacheMaxAge > 0 ? getValidCachedPkg(cacheDir, opts.cacheMaxAge) : undefined;
77
+ if (cached) {
78
+ return {
79
+ pkgDir: resolveInstalledPkgDir(cached, parsed.name),
80
+ cachedPkgName: parsed.name,
81
+ };
82
+ }
83
+ const prepareDir = makePrepareDir(cacheDir);
84
+ await installPackages({
85
+ prefix: prepareDir,
86
+ specs: [parsed.spec],
87
+ verbose: opts.verbose,
88
+ registry: opts.registry,
89
+ });
90
+ const liveTarget = symlinkSwap(cacheDir, prepareDir);
91
+ return {
92
+ pkgDir: resolveInstalledPkgDir(liveTarget, parsed.name),
93
+ cachedPkgName: parsed.name,
94
+ };
95
+ }
96
+ import { existsSync, readFileSync } from 'node:fs';
97
+ import { join } from 'node:path';
98
+ function splitBinAndArgs(pkgDir, binOrArg, extraArgs) {
99
+ if (!binOrArg) {
100
+ return { binName: null, extraArgs };
101
+ }
102
+ const pkgJsonPath = join(pkgDir, 'package.json');
103
+ if (existsSync(pkgJsonPath)) {
104
+ try {
105
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
106
+ const bins = pkg.gjsify?.bin;
107
+ if (bins && Object.prototype.hasOwnProperty.call(bins, binOrArg)) {
108
+ return { binName: binOrArg, extraArgs };
109
+ }
110
+ }
111
+ catch {
112
+ // Fall through to treating as an arg.
113
+ }
114
+ }
115
+ // Not a known bin — treat the positional as the first argv to the bundle.
116
+ return { binName: null, extraArgs: [binOrArg, ...extraArgs] };
117
+ }
@@ -6,3 +6,5 @@ export * from './showcase.js';
6
6
  export * from './create.js';
7
7
  export * from './gresource.js';
8
8
  export * from './gettext.js';
9
+ export * from './dlx.js';
10
+ export * from './install.js';
@@ -6,3 +6,5 @@ export * from './showcase.js';
6
6
  export * from './create.js';
7
7
  export * from './gresource.js';
8
8
  export * from './gettext.js';
9
+ export * from './dlx.js';
10
+ export * from './install.js';
@@ -0,0 +1,10 @@
1
+ import type { Command } from '../types/index.js';
2
+ interface InstallOptions {
3
+ packages?: string[];
4
+ 'save-dev'?: boolean;
5
+ 'save-peer'?: boolean;
6
+ 'save-optional'?: boolean;
7
+ verbose: boolean;
8
+ }
9
+ export declare const installCommand: Command<any, InstallOptions>;
10
+ export {};
@@ -0,0 +1,99 @@
1
+ // `gjsify install [pkg...]` — thin npm wrapper with gjsify-aware post-checks.
2
+ //
3
+ // The actual install is delegated to `npm install` in the user's project root
4
+ // (no `--prefix` rewrite, unlike `gjsify dlx`). After install completes we run
5
+ // `runMinimalChecks()` so missing system deps (gjs, gtk4, libsoup, ...) surface
6
+ // immediately, and report any installed `@gjsify/*` packages that ship native
7
+ // prebuilds so users know they can use `gjsify run` to wire `LD_LIBRARY_PATH` /
8
+ // `GI_TYPELIB_PATH` automatically.
9
+ //
10
+ // Modes:
11
+ // gjsify install → project install (npm install)
12
+ // gjsify install <pkg> [<pkg>...] → add package(s) (npm install <pkg>...)
13
+ import { spawn } from 'node:child_process';
14
+ import { buildInstallCommand, detectPackageManager, runMinimalChecks, } from '../utils/check-system-deps.js';
15
+ import { detectNativePackages } from '../utils/detect-native-packages.js';
16
+ export const installCommand = {
17
+ command: 'install [packages..]',
18
+ description: 'Install npm dependencies in the current project, then run gjsify-aware post-checks.',
19
+ builder: (yargs) => yargs
20
+ .positional('packages', {
21
+ description: 'Optional package specs. With none, runs a full project install.',
22
+ type: 'string',
23
+ array: true,
24
+ })
25
+ .option('save-dev', { type: 'boolean', alias: 'D' })
26
+ .option('save-peer', { type: 'boolean' })
27
+ .option('save-optional', { type: 'boolean', alias: 'O' })
28
+ .option('verbose', {
29
+ description: 'Verbose npm logging.',
30
+ type: 'boolean',
31
+ default: false,
32
+ }),
33
+ handler: async (args) => {
34
+ const npmArgs = ['install'];
35
+ if (args['save-dev'])
36
+ npmArgs.push('--save-dev');
37
+ if (args['save-peer'])
38
+ npmArgs.push('--save-peer');
39
+ if (args['save-optional'])
40
+ npmArgs.push('--save-optional');
41
+ if (args.verbose)
42
+ npmArgs.push('--loglevel', 'verbose');
43
+ if (args.packages && args.packages.length > 0) {
44
+ npmArgs.push(...args.packages);
45
+ }
46
+ await spawnNpm(npmArgs);
47
+ await runPostInstallChecks();
48
+ },
49
+ };
50
+ async function spawnNpm(npmArgs) {
51
+ return new Promise((resolve, reject) => {
52
+ const child = spawn('npm', npmArgs, { stdio: 'inherit' });
53
+ child.on('close', (code) => {
54
+ if (code === 0)
55
+ resolve();
56
+ else
57
+ reject(new Error(`npm install exited with code ${code}`));
58
+ });
59
+ child.on('error', (err) => {
60
+ const code = err.code;
61
+ const msg = code === 'ENOENT'
62
+ ? 'npm not found on PATH — install Node.js first.'
63
+ : `npm install failed: ${err.message}`;
64
+ reject(new Error(msg));
65
+ });
66
+ }).catch((err) => {
67
+ console.error(err.message);
68
+ process.exit(1);
69
+ });
70
+ }
71
+ async function runPostInstallChecks() {
72
+ console.log('\n--- gjsify post-install checks ---');
73
+ // 1. System deps that GJS apps typically need.
74
+ const results = runMinimalChecks();
75
+ const missing = results.filter((r) => !r.found && r.severity === 'required');
76
+ if (missing.length > 0) {
77
+ console.warn('Missing required system dependencies:\n');
78
+ for (const dep of missing) {
79
+ console.warn(` ✗ ${dep.name}`);
80
+ }
81
+ const pm = detectPackageManager();
82
+ const cmd = buildInstallCommand(pm, missing);
83
+ if (cmd)
84
+ console.warn(`\nInstall with:\n ${cmd}`);
85
+ }
86
+ else {
87
+ console.log('System dependencies OK.');
88
+ }
89
+ // 2. Surface @gjsify/* packages with native prebuilds — `gjsify run`
90
+ // will set LD_LIBRARY_PATH / GI_TYPELIB_PATH for these automatically.
91
+ const native = detectNativePackages(process.cwd());
92
+ if (native.length > 0) {
93
+ console.log(`\nDetected ${native.length} @gjsify/* package(s) with native prebuilds:`);
94
+ for (const pkg of native) {
95
+ console.log(` • ${pkg.name}`);
96
+ }
97
+ console.log('\nUse `gjsify run <bundle>` to launch with LD_LIBRARY_PATH/GI_TYPELIB_PATH set.');
98
+ }
99
+ }
@@ -1,26 +1,25 @@
1
1
  import { discoverShowcases, findShowcase } from '../utils/discover-showcases.js';
2
- import { runMinimalChecks, checkGwebgl, detectPackageManager, buildInstallCommand } from '../utils/check-system-deps.js';
3
- import { runGjsBundle } from '../utils/run-gjs.js';
2
+ import { runMinimalChecks, checkGwebgl, detectPackageManager, buildInstallCommand, } from '../utils/check-system-deps.js';
3
+ import { spawn } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
4
5
  export const showcaseCommand = {
5
6
  command: 'showcase [name]',
6
- description: 'List or run built-in gjsify showcase applications.',
7
- builder: (yargs) => {
8
- return yargs
9
- .positional('name', {
10
- description: 'Showcase name to run (omit to list all)',
11
- type: 'string',
12
- })
13
- .option('json', {
14
- description: 'Output as JSON',
15
- type: 'boolean',
16
- default: false,
17
- })
18
- .option('list', {
19
- description: 'List available showcases',
20
- type: 'boolean',
21
- default: false,
22
- });
23
- },
7
+ description: 'List or run curated gjsify showcase applications.',
8
+ builder: (yargs) => yargs
9
+ .positional('name', {
10
+ description: 'Showcase name to run (omit to list all)',
11
+ type: 'string',
12
+ })
13
+ .option('json', {
14
+ description: 'Output as JSON',
15
+ type: 'boolean',
16
+ default: false,
17
+ })
18
+ .option('list', {
19
+ description: 'List available showcases',
20
+ type: 'boolean',
21
+ default: false,
22
+ }),
24
23
  handler: async (args) => {
25
24
  // List mode: no name given, or --list flag
26
25
  if (!args.name || args.list) {
@@ -30,10 +29,9 @@ export const showcaseCommand = {
30
29
  return;
31
30
  }
32
31
  if (showcases.length === 0) {
33
- console.log('No showcases found. Showcase packages may not be installed.');
32
+ console.log('No showcases found. The CLI ships a curated list in `showcases.json`; if it is missing the CLI install is incomplete.');
34
33
  return;
35
34
  }
36
- // Group by category
37
35
  const grouped = new Map();
38
36
  for (const sc of showcases) {
39
37
  const list = grouped.get(sc.category) ?? [];
@@ -43,7 +41,7 @@ export const showcaseCommand = {
43
41
  console.log('Available gjsify showcases:\n');
44
42
  for (const [category, list] of grouped) {
45
43
  console.log(` ${category.toUpperCase()}:`);
46
- const maxNameLen = Math.max(...list.map(e => e.name.length));
44
+ const maxNameLen = Math.max(...list.map((e) => e.name.length));
47
45
  for (const sc of list) {
48
46
  const pad = ' '.repeat(maxNameLen - sc.name.length + 2);
49
47
  const desc = sc.description ? `${pad}${sc.description}` : '';
@@ -54,23 +52,18 @@ export const showcaseCommand = {
54
52
  console.log('Run a showcase: gjsify showcase <name>');
55
53
  return;
56
54
  }
57
- // Run mode: find the showcase
58
55
  const showcase = findShowcase(args.name);
59
56
  if (!showcase) {
60
57
  console.error(`Unknown showcase: "${args.name}"`);
61
58
  console.error('Run "gjsify showcase" to list available showcases.');
62
59
  process.exit(1);
63
60
  }
64
- // System dependency check before running — only check what this showcase needs.
65
- // All showcases need GJS; WebGL showcases additionally need gwebgl prebuilds.
61
+ // System dependency check before delegating — only what this showcase needs.
66
62
  const results = runMinimalChecks();
67
- const needsWebgl = showcase.packageName.includes('webgl') || showcase.packageName.includes('three');
68
- if (needsWebgl) {
63
+ if (showcase.needsWebgl) {
69
64
  results.push(checkGwebgl(process.cwd()));
70
65
  }
71
- // Hard-fail only on missing REQUIRED deps (gjs, gwebgl is required if needsWebgl).
72
- // For showcase, gwebgl is treated as required because the bundle won't run without it.
73
- const missingHard = results.filter(r => !r.found && (r.severity === 'required' || r.id === 'gwebgl'));
66
+ const missingHard = results.filter((r) => !r.found && (r.severity === 'required' || r.id === 'gwebgl'));
74
67
  if (missingHard.length > 0) {
75
68
  console.error('Missing system dependencies:\n');
76
69
  for (const dep of missingHard) {
@@ -83,8 +76,27 @@ export const showcaseCommand = {
83
76
  }
84
77
  process.exit(1);
85
78
  }
86
- // Run the showcase via shared GJS runner
87
- console.log(`Running showcase: ${showcase.name}\n`);
88
- await runGjsBundle(showcase.bundlePath);
79
+ // Delegate to `gjsify dlx <package>` same npm-cache, same atomic
80
+ // symlink-swap, same `gjsify.main` resolution. Re-spawning the CLI
81
+ // keeps the dlx logic in one place.
82
+ console.log(`Running showcase: ${showcase.name} (via gjsify dlx)\n`);
83
+ const cliBin = fileURLToPath(new URL('../index.js', import.meta.url));
84
+ const child = spawn(process.execPath, [cliBin, 'dlx', showcase.packageName], {
85
+ stdio: 'inherit',
86
+ });
87
+ await new Promise((resolvePromise, reject) => {
88
+ child.on('close', (code) => {
89
+ if (code !== 0) {
90
+ reject(new Error(`gjsify dlx exited with code ${code}`));
91
+ }
92
+ else {
93
+ resolvePromise();
94
+ }
95
+ });
96
+ child.on('error', reject);
97
+ }).catch((err) => {
98
+ console.error(err.message);
99
+ process.exit(1);
100
+ });
89
101
  },
90
102
  };
package/lib/index.js CHANGED
@@ -1,14 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import yargs from 'yargs';
3
3
  import { hideBin } from 'yargs/helpers';
4
- import { buildCommand as build, runCommand as run, infoCommand as info, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, } from './commands/index.js';
4
+ import { buildCommand as build, runCommand as run, infoCommand as info, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, dlxCommand as dlx, installCommand as install, } from './commands/index.js';
5
5
  import { APP_NAME } from './constants.js';
6
6
  void yargs(hideBin(process.argv))
7
7
  .scriptName(APP_NAME)
8
8
  .strict()
9
9
  .command(create.command, create.description, create.builder, create.handler)
10
+ .command(install.command, install.description, install.builder, install.handler)
10
11
  .command(build.command, build.description, build.builder, build.handler)
11
12
  .command(run.command, run.description, run.builder, run.handler)
13
+ .command(dlx.command, dlx.description, dlx.builder, dlx.handler)
12
14
  .command(info.command, info.description, info.builder, info.handler)
13
15
  .command(check.command, check.description, check.builder, check.handler)
14
16
  .command(showcase.command, showcase.description, showcase.builder, showcase.handler)
@@ -96,8 +96,13 @@ const PACKAGE_DEPS = {
96
96
  '@gjsify/canvas2d': ['gdk-pixbuf', 'pango', 'pangocairo', 'cairo'],
97
97
  '@gjsify/canvas2d-core': ['gdk-pixbuf', 'pango', 'pangocairo', 'cairo'],
98
98
  '@gjsify/dom-elements': ['gdk-pixbuf'],
99
- // @gjsify/webgl, @gjsify/event-bridge only need gtk4/gdk which are
100
- // already in the required set, so they don't need optional entries.
99
+ // @gjsify/webgl needs the gwebgl npm package (Vala prebuild) — handled
100
+ // as a special-case checkNpmPackage rather than checkPkgConfig in
101
+ // runOptionalChecks. Mapping it here so its presence in the project's
102
+ // dep tree triggers the check.
103
+ '@gjsify/webgl': ['gwebgl'],
104
+ // @gjsify/event-bridge only needs gtk4/gdk which are already in the
105
+ // required set, so it doesn't need an optional entry.
101
106
  };
102
107
  /** Walk up from cwd looking for the nearest package.json. */
103
108
  function findProjectRoot(cwd) {
@@ -241,10 +246,16 @@ function runOptionalChecks(needed, cwd) {
241
246
  .map(([pkg]) => pkg);
242
247
  results.push(checkPkgConfig(dep.id, dep.name, dep.pkgName, 'optional', requiredBy));
243
248
  }
244
- // gwebgl npm package — special case (not a pkg-config lib).
245
- // Always reported (the npm package is bundled with the CLI), but marked
246
- // optional because only @gjsify/webgl users need it.
247
- results.push(checkGwebgl(cwd));
249
+ // gwebgl npm package — special case (not a pkg-config lib). Only checked
250
+ // when the project directly or transitively depends on @gjsify/webgl
251
+ // since the CLI tarball no longer ships any showcase example packages,
252
+ // gwebgl is not part of the CLI's own dep tree, so reporting it for every
253
+ // project would always be `found: false`. `needed === null` means "check
254
+ // everything" (no project context).
255
+ const hasWebglDep = needed === null || needed.has('gwebgl');
256
+ if (hasWebglDep) {
257
+ results.push(checkGwebgl(cwd));
258
+ }
248
259
  return results;
249
260
  }
250
261
  // Per-package-manager install package names, keyed by dep id.
@@ -5,14 +5,15 @@ export interface ShowcaseInfo {
5
5
  packageName: string;
6
6
  /** Category: "dom" or "node" */
7
7
  category: string;
8
- /** Description from showcase's package.json */
8
+ /** Description for the list view */
9
9
  description: string;
10
- /** Absolute path to the GJS bundle (resolved from "main" field) */
11
- bundlePath: string;
10
+ /** Whether the showcase needs the gwebgl native prebuild. */
11
+ needsWebgl: boolean;
12
12
  }
13
13
  /**
14
- * Discover all installed showcase packages by scanning the CLI's own dependencies.
15
- * Returns showcases sorted by category then name.
14
+ * Read the curated showcase list from `showcases.json`. Returns showcases
15
+ * sorted by category then name. An empty list (or missing manifest) yields
16
+ * an empty array — `gjsify showcase` then prints the empty-state message.
16
17
  */
17
18
  export declare function discoverShowcases(): ShowcaseInfo[];
18
19
  /** Find a single showcase by short name. */
@@ -1,68 +1,46 @@
1
- // Dynamic discovery of installed showcase packages (@gjsify/example-*).
2
- // Scans the CLI's own package.json dependencies at runtime.
3
- import { readFileSync } from 'node:fs';
1
+ // Static discovery of showcase packages from `showcases.json`.
2
+ //
3
+ // Earlier versions read showcases from the CLI's own `package.json#dependencies`
4
+ // — every showcase had to be a direct CLI dependency. That made the CLI tarball
5
+ // blow up with each new showcase and required a CLI rebuild to publish a new
6
+ // one. Static manifest decouples both: the CLI reads the manifest at runtime,
7
+ // `gjsify showcase <name>` delegates to `gjsify dlx <package>`.
8
+ import { existsSync, readFileSync } from 'node:fs';
4
9
  import { dirname, join } from 'node:path';
5
- import { createRequire } from 'node:module';
6
10
  import { fileURLToPath } from 'node:url';
7
- const EXAMPLE_PREFIX = '@gjsify/example-';
8
- /** Extract short name and category from package name. */
9
- function parseShowcaseName(packageName) {
10
- // @gjsify/example-dom-three-postprocessing-pixel → category=dom, name=three-postprocessing-pixel
11
- const suffix = packageName.slice(EXAMPLE_PREFIX.length);
12
- const dashIdx = suffix.indexOf('-');
13
- if (dashIdx === -1)
14
- return null;
15
- return {
16
- category: suffix.slice(0, dashIdx),
17
- name: suffix.slice(dashIdx + 1),
18
- };
11
+ function manifestPath() {
12
+ // `showcases.json` lives at the package root: ../../showcases.json from lib/utils/.
13
+ return join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'showcases.json');
19
14
  }
20
15
  /**
21
- * Discover all installed showcase packages by scanning the CLI's own dependencies.
22
- * Returns showcases sorted by category then name.
16
+ * Read the curated showcase list from `showcases.json`. Returns showcases
17
+ * sorted by category then name. An empty list (or missing manifest) yields
18
+ * an empty array — `gjsify showcase` then prints the empty-state message.
23
19
  */
24
20
  export function discoverShowcases() {
25
- const require = createRequire(import.meta.url);
26
- const cliPkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
27
- let cliPkg;
21
+ const path = manifestPath();
22
+ if (!existsSync(path))
23
+ return [];
24
+ let manifest;
28
25
  try {
29
- cliPkg = JSON.parse(readFileSync(cliPkgPath, 'utf-8'));
26
+ manifest = JSON.parse(readFileSync(path, 'utf-8'));
30
27
  }
31
28
  catch {
32
29
  return [];
33
30
  }
34
- const deps = cliPkg['dependencies'];
35
- if (!deps)
31
+ if (!Array.isArray(manifest.showcases))
36
32
  return [];
37
- const showcases = [];
38
- for (const packageName of Object.keys(deps)) {
39
- if (!packageName.startsWith(EXAMPLE_PREFIX))
40
- continue;
41
- const parsed = parseShowcaseName(packageName);
42
- if (!parsed)
43
- continue;
44
- try {
45
- const pkgJsonPath = require.resolve(`${packageName}/package.json`);
46
- const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
47
- const main = pkg['main'];
48
- if (!main)
49
- continue;
50
- showcases.push({
51
- name: parsed.name,
52
- packageName,
53
- category: parsed.category,
54
- description: pkg['description'] ?? '',
55
- bundlePath: join(dirname(pkgJsonPath), main),
56
- });
57
- }
58
- catch {
59
- // Package listed as dep but not resolvable — skip silently
60
- }
61
- }
33
+ const showcases = manifest.showcases.map((e) => ({
34
+ name: e.name,
35
+ packageName: e.package,
36
+ category: e.category,
37
+ description: e.description ?? '',
38
+ needsWebgl: Boolean(e.needsWebgl),
39
+ }));
62
40
  showcases.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
63
41
  return showcases;
64
42
  }
65
43
  /** Find a single showcase by short name. */
66
44
  export function findShowcase(name) {
67
- return discoverShowcases().find(e => e.name === name);
45
+ return discoverShowcases().find((e) => e.name === name);
68
46
  }
@@ -0,0 +1,34 @@
1
+ interface CacheKeyOpts {
2
+ packages: string[];
3
+ registries?: Record<string, string>;
4
+ }
5
+ /** Stable, sorted JSON hash of inputs. */
6
+ export declare function createCacheKey(opts: CacheKeyOpts): string;
7
+ /** $XDG_CACHE_HOME/gjsify/dlx — created if missing. */
8
+ export declare function dlxCacheRoot(): string;
9
+ /** Per-key cache directory: <root>/<sha>. */
10
+ export declare function cacheDirFor(cacheKey: string): string;
11
+ /** A fresh prepare directory under the per-key cache, named timestamp-pid. */
12
+ export declare function makePrepareDir(cacheDir: string): string;
13
+ /**
14
+ * If <cacheDir>/pkg points to a target whose mtime + maxAge < now, return its
15
+ * realpath. Returns undefined when the link doesn't exist, isn't a symlink,
16
+ * has been removed, or has expired.
17
+ */
18
+ export declare function getValidCachedPkg(cacheDir: string, maxAgeMinutes?: number): string | undefined;
19
+ /**
20
+ * Atomically swap `<cacheDir>/pkg` to point at `prepareDir`.
21
+ *
22
+ * Strategy:
23
+ * 1. Create new symlink `<cacheDir>/pkg-<rand>` → prepareDir.
24
+ * 2. `rename(pkg-<rand>, pkg)` — POSIX guarantees rename-over-existing is atomic.
25
+ *
26
+ * Returns the realpath of the new live target. EBUSY/EEXIST indicates a race
27
+ * — a parallel process won, return its realpath.
28
+ */
29
+ export declare function symlinkSwap(cacheDir: string, prepareDir: string): string;
30
+ /** Clean up `<cacheDir>/<oldPrepareDir>` siblings older than `maxAgeMinutes`. */
31
+ export declare function cleanupStalePrepareDirs(cacheDir: string, _maxAgeMinutes?: number): void;
32
+ /** Resolve absolute path to the installed package's directory inside cache. */
33
+ export declare function resolveInstalledPkgDir(cachedRoot: string, pkgName: string): string;
34
+ export {};