@gjsify/cli 0.3.5 → 0.3.7
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.
- package/lib/actions/build.js +21 -107
- package/lib/commands/index.d.ts +1 -0
- package/lib/commands/index.js +1 -0
- package/lib/commands/install.d.ts +10 -0
- package/lib/commands/install.js +99 -0
- package/lib/commands/showcase.js +46 -34
- package/lib/index.js +2 -1
- package/lib/utils/check-system-deps.js +17 -6
- package/lib/utils/discover-showcases.d.ts +6 -5
- package/lib/utils/discover-showcases.js +28 -50
- package/package.json +6 -10
- package/showcases.json +40 -0
- package/src/actions/build.ts +21 -110
- package/src/commands/index.ts +2 -1
- package/src/commands/install.ts +116 -0
- package/src/commands/showcase.ts +40 -21
- package/src/index.ts +2 -0
- package/src/utils/check-system-deps.ts +17 -6
- package/src/utils/discover-showcases.ts +42 -53
package/lib/actions/build.js
CHANGED
|
@@ -1,116 +1,30 @@
|
|
|
1
1
|
import { build } from "esbuild";
|
|
2
2
|
import { gjsifyPlugin } from "@gjsify/esbuild-plugin-gjsify";
|
|
3
3
|
import { resolveGlobalsList, writeRegisterInjectFile, detectAutoGlobals, } from "@gjsify/esbuild-plugin-gjsify/globals";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { getBundleDir, rewriteContents } from "@gjsify/esbuild-plugin-gjsify";
|
|
5
|
+
import { getPnpPlugin } from "@gjsify/resolve-npm/pnp-relay";
|
|
6
|
+
import { dirname, extname } from "node:path";
|
|
6
7
|
import { chmod, readFile, writeFile } from "node:fs/promises";
|
|
7
|
-
import { existsSync } from "node:fs";
|
|
8
|
-
import { createRequire } from "node:module";
|
|
9
8
|
const GJS_SHEBANG = "#!/usr/bin/env -S gjs -m\n";
|
|
10
|
-
/** Walk up from dir until .pnp.cjs is found; return its directory or null. */
|
|
11
|
-
function findPnpRoot(dir) {
|
|
12
|
-
let current = dir;
|
|
13
|
-
while (true) {
|
|
14
|
-
if (existsSync(join(current, ".pnp.cjs")))
|
|
15
|
-
return current;
|
|
16
|
-
const parent = dirname(current);
|
|
17
|
-
if (parent === current)
|
|
18
|
-
return null;
|
|
19
|
-
current = parent;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
9
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
10
|
+
* Resolve the gjsify-flavoured PnP plugin. Anchors the relay on this file's
|
|
11
|
+
* URL so transitive `@gjsify/*` polyfills (reached via @gjsify/cli's deps on
|
|
12
|
+
* @gjsify/{node,web}-polyfills) are resolvable for external consumers without
|
|
13
|
+
* each one having to be a direct devDep.
|
|
26
14
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* node polyfills as direct deps including @gjsify/node-globals)
|
|
32
|
-
* 3. @gjsify/web-polyfills context (direct dep of @gjsify/cli, has all
|
|
33
|
-
* web polyfills as direct deps including @gjsify/fetch, @gjsify/abort-controller)
|
|
34
|
-
* For bare specifiers that the gjsify alias plugin maps (e.g. `abort-controller`),
|
|
35
|
-
* fall through so that plugin can handle the transformation first.
|
|
15
|
+
* Wires the @gjsify/esbuild-plugin-gjsify rewriter (`__filename`/`__dirname`
|
|
16
|
+
* injection for CJS code in node_modules) into the pnp plugin's onLoad —
|
|
17
|
+
* esbuild stops at the first matching onLoad, so the rewriter MUST run from
|
|
18
|
+
* inside the pnp plugin's onLoad rather than as a separate registration.
|
|
36
19
|
*/
|
|
37
|
-
async function
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// Two-hop relay: node-polyfills and web-polyfills have all individual
|
|
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).
|
|
51
|
-
const requireFromGjsify = createRequire(gjsifyIssuer);
|
|
52
|
-
const relayIssuers = [];
|
|
53
|
-
for (const pkg of ["@gjsify/node-polyfills", "@gjsify/web-polyfills"]) {
|
|
54
|
-
try {
|
|
55
|
-
relayIssuers.push(requireFromGjsify.resolve(`${pkg}/package.json`));
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
// polyfills package not in dep tree — relay won't cover it
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
let pnpApi = null;
|
|
62
|
-
try {
|
|
63
|
-
// pnpapi has no npm package — it is a virtual module injected by Yarn PnP
|
|
64
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
65
|
-
// @ts-expect-error
|
|
66
|
-
pnpApi = (await import("pnpapi"));
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
// Not in a PnP runtime (shouldn't happen since findPnpRoot passed)
|
|
70
|
-
}
|
|
71
|
-
return pnpPlugin({
|
|
72
|
-
onResolve: async (args, { resolvedPath, error, watchFiles }) => {
|
|
73
|
-
if (resolvedPath !== null) {
|
|
74
|
-
return { namespace: "pnp", path: resolvedPath, watchFiles };
|
|
75
|
-
}
|
|
76
|
-
if (error?.pnpCode ===
|
|
77
|
-
"UNDECLARED_DEPENDENCY") {
|
|
78
|
-
if (pnpApi !== null) {
|
|
79
|
-
// Try @gjsify/cli context first (covers @gjsify/* that are
|
|
80
|
-
// direct deps of cli's own deps — unlikely but fast check).
|
|
81
|
-
try {
|
|
82
|
-
const rp = pnpApi.resolveRequest(args.path, gjsifyIssuer);
|
|
83
|
-
if (rp !== null)
|
|
84
|
-
return { namespace: "pnp", path: rp, watchFiles };
|
|
85
|
-
}
|
|
86
|
-
catch { }
|
|
87
|
-
// Two-hop relay: resolve from node-polyfills / web-polyfills context
|
|
88
|
-
// which have the individual @gjsify/* packages as direct deps.
|
|
89
|
-
for (const relayIssuer of relayIssuers) {
|
|
90
|
-
try {
|
|
91
|
-
const rp = pnpApi.resolveRequest(args.path, relayIssuer);
|
|
92
|
-
if (rp !== null)
|
|
93
|
-
return { namespace: "pnp", path: rp, watchFiles };
|
|
94
|
-
}
|
|
95
|
-
catch { }
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
// Fall through — bare aliases (abort-controller, fetch/register/*)
|
|
99
|
-
// are handled by the gjsify alias plugin after this returns null,
|
|
100
|
-
// then the re-resolved @gjsify/* path goes through this hook again.
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
return {
|
|
104
|
-
external: true,
|
|
105
|
-
errors: error ? [{ text: error.message }] : [],
|
|
106
|
-
watchFiles,
|
|
107
|
-
};
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
20
|
+
async function buildPnpPlugin() {
|
|
21
|
+
return getPnpPlugin({
|
|
22
|
+
issuerUrl: import.meta.url,
|
|
23
|
+
transformContentsFactory: (build) => {
|
|
24
|
+
const bundleDir = getBundleDir(build);
|
|
25
|
+
return (args, contents) => rewriteContents(args, contents, bundleDir);
|
|
26
|
+
},
|
|
27
|
+
});
|
|
114
28
|
}
|
|
115
29
|
export class BuildAction {
|
|
116
30
|
configData;
|
|
@@ -134,7 +48,7 @@ export class BuildAction {
|
|
|
134
48
|
const moduleOutExt = library.module ? extname(library.module) : ".js";
|
|
135
49
|
const mainOutExt = library.main ? extname(library.main) : ".js";
|
|
136
50
|
const multipleBuilds = moduleOutdir && mainOutdir && moduleOutdir !== mainOutdir;
|
|
137
|
-
const pnpPlugin = await
|
|
51
|
+
const pnpPlugin = await buildPnpPlugin();
|
|
138
52
|
const pnpPlugins = pnpPlugin ? [pnpPlugin] : [];
|
|
139
53
|
const results = [];
|
|
140
54
|
if (multipleBuilds) {
|
|
@@ -292,7 +206,7 @@ export class BuildAction {
|
|
|
292
206
|
...(aliases ? { aliases } : {}),
|
|
293
207
|
};
|
|
294
208
|
const { autoMode, extras } = this.parseGlobalsValue(globals);
|
|
295
|
-
const pnpPlugin = await
|
|
209
|
+
const pnpPlugin = await buildPnpPlugin();
|
|
296
210
|
const pnpPlugins = pnpPlugin ? [pnpPlugin] : [];
|
|
297
211
|
// --- Auto mode (with optional extras): iterative multi-pass build ---
|
|
298
212
|
// The extras token is used for cases where the detector cannot
|
package/lib/commands/index.d.ts
CHANGED
package/lib/commands/index.js
CHANGED
|
@@ -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
|
+
}
|
package/lib/commands/showcase.js
CHANGED
|
@@ -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 {
|
|
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
|
|
7
|
-
builder: (yargs) =>
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
68
|
-
if (needsWebgl) {
|
|
63
|
+
if (showcase.needsWebgl) {
|
|
69
64
|
results.push(checkGwebgl(process.cwd()));
|
|
70
65
|
}
|
|
71
|
-
|
|
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
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
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,12 +1,13 @@
|
|
|
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, dlxCommand as dlx, } 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)
|
|
12
13
|
.command(dlx.command, dlx.description, dlx.builder, dlx.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
|
|
100
|
-
//
|
|
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
|
-
//
|
|
246
|
-
//
|
|
247
|
-
|
|
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
|
|
8
|
+
/** Description for the list view */
|
|
9
9
|
description: string;
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/** Whether the showcase needs the gwebgl native prebuild. */
|
|
11
|
+
needsWebgl: boolean;
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
*
|
|
22
|
-
*
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
const path = manifestPath();
|
|
22
|
+
if (!existsSync(path))
|
|
23
|
+
return [];
|
|
24
|
+
let manifest;
|
|
28
25
|
try {
|
|
29
|
-
|
|
26
|
+
manifest = JSON.parse(readFileSync(path, 'utf-8'));
|
|
30
27
|
}
|
|
31
28
|
catch {
|
|
32
29
|
return [];
|
|
33
30
|
}
|
|
34
|
-
|
|
35
|
-
if (!deps)
|
|
31
|
+
if (!Array.isArray(manifest.showcases))
|
|
36
32
|
return [];
|
|
37
|
-
const showcases =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "CLI for Gjsify",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -23,15 +23,11 @@
|
|
|
23
23
|
"cli"
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@gjsify/create-app": "^0.3.
|
|
27
|
-
"@gjsify/esbuild-plugin-gjsify": "^0.3.
|
|
28
|
-
"@gjsify/
|
|
29
|
-
"@gjsify/
|
|
30
|
-
"@gjsify/
|
|
31
|
-
"@gjsify/example-dom-three-postprocessing-pixel": "^0.3.5",
|
|
32
|
-
"@gjsify/example-node-express-webserver": "^0.3.5",
|
|
33
|
-
"@gjsify/node-polyfills": "^0.3.5",
|
|
34
|
-
"@gjsify/web-polyfills": "^0.3.5",
|
|
26
|
+
"@gjsify/create-app": "^0.3.7",
|
|
27
|
+
"@gjsify/esbuild-plugin-gjsify": "^0.3.7",
|
|
28
|
+
"@gjsify/node-polyfills": "^0.3.7",
|
|
29
|
+
"@gjsify/resolve-npm": "^0.3.7",
|
|
30
|
+
"@gjsify/web-polyfills": "^0.3.7",
|
|
35
31
|
"@yarnpkg/esbuild-plugin-pnp": "^3.0.0-rc.15",
|
|
36
32
|
"cosmiconfig": "^9.0.1",
|
|
37
33
|
"esbuild": "^0.28.0",
|
package/showcases.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"showcases": [
|
|
4
|
+
{
|
|
5
|
+
"name": "canvas2d-fireworks",
|
|
6
|
+
"category": "dom",
|
|
7
|
+
"package": "@gjsify/example-dom-canvas2d-fireworks",
|
|
8
|
+
"description": "Colorful fireworks Canvas 2D example with Adwaita controls",
|
|
9
|
+
"needsWebgl": false
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"name": "excalibur-jelly-jumper",
|
|
13
|
+
"category": "dom",
|
|
14
|
+
"package": "@gjsify/example-dom-excalibur-jelly-jumper",
|
|
15
|
+
"description": "Excalibur.js jelly-jumper game",
|
|
16
|
+
"needsWebgl": false
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "three-geometry-teapot",
|
|
20
|
+
"category": "dom",
|
|
21
|
+
"package": "@gjsify/example-dom-three-geometry-teapot",
|
|
22
|
+
"description": "Three.js Utah teapot",
|
|
23
|
+
"needsWebgl": true
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "three-postprocessing-pixel",
|
|
27
|
+
"category": "dom",
|
|
28
|
+
"package": "@gjsify/example-dom-three-postprocessing-pixel",
|
|
29
|
+
"description": "Three.js postprocessing pixel demo",
|
|
30
|
+
"needsWebgl": true
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "express-webserver",
|
|
34
|
+
"category": "node",
|
|
35
|
+
"package": "@gjsify/example-node-express-webserver",
|
|
36
|
+
"description": "Express web server on GJS via @gjsify/http",
|
|
37
|
+
"needsWebgl": false
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
package/src/actions/build.ts
CHANGED
|
@@ -7,121 +7,32 @@ import {
|
|
|
7
7
|
writeRegisterInjectFile,
|
|
8
8
|
detectAutoGlobals,
|
|
9
9
|
} from "@gjsify/esbuild-plugin-gjsify/globals";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { getBundleDir, rewriteContents } from "@gjsify/esbuild-plugin-gjsify";
|
|
11
|
+
import { getPnpPlugin } from "@gjsify/resolve-npm/pnp-relay";
|
|
12
|
+
import { dirname, extname } from "node:path";
|
|
12
13
|
import { chmod, readFile, writeFile } from "node:fs/promises";
|
|
13
|
-
import { existsSync } from "node:fs";
|
|
14
|
-
import { createRequire } from "node:module";
|
|
15
14
|
|
|
16
15
|
const GJS_SHEBANG = "#!/usr/bin/env -S gjs -m\n";
|
|
17
16
|
|
|
18
|
-
/** Walk up from dir until .pnp.cjs is found; return its directory or null. */
|
|
19
|
-
function findPnpRoot(dir: string): string | null {
|
|
20
|
-
let current = dir;
|
|
21
|
-
while (true) {
|
|
22
|
-
if (existsSync(join(current, ".pnp.cjs"))) return current;
|
|
23
|
-
const parent = dirname(current);
|
|
24
|
-
if (parent === current) return null;
|
|
25
|
-
current = parent;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
17
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
18
|
+
* Resolve the gjsify-flavoured PnP plugin. Anchors the relay on this file's
|
|
19
|
+
* URL so transitive `@gjsify/*` polyfills (reached via @gjsify/cli's deps on
|
|
20
|
+
* @gjsify/{node,web}-polyfills) are resolvable for external consumers without
|
|
21
|
+
* each one having to be a direct devDep.
|
|
33
22
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* node polyfills as direct deps including @gjsify/node-globals)
|
|
39
|
-
* 3. @gjsify/web-polyfills context (direct dep of @gjsify/cli, has all
|
|
40
|
-
* web polyfills as direct deps including @gjsify/fetch, @gjsify/abort-controller)
|
|
41
|
-
* For bare specifiers that the gjsify alias plugin maps (e.g. `abort-controller`),
|
|
42
|
-
* fall through so that plugin can handle the transformation first.
|
|
23
|
+
* Wires the @gjsify/esbuild-plugin-gjsify rewriter (`__filename`/`__dirname`
|
|
24
|
+
* injection for CJS code in node_modules) into the pnp plugin's onLoad —
|
|
25
|
+
* esbuild stops at the first matching onLoad, so the rewriter MUST run from
|
|
26
|
+
* inside the pnp plugin's onLoad rather than as a separate registration.
|
|
43
27
|
*/
|
|
44
|
-
async function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// Two-hop relay: node-polyfills and web-polyfills have all individual
|
|
54
|
-
// @gjsify/* packages as direct deps. Resolving from their package.json
|
|
55
|
-
// paths allows PnP to use them as issuers — sub-path imports
|
|
56
|
-
// (`@gjsify/foo/register/bar`) then resolve through the polyfill's
|
|
57
|
-
// dep graph. Resolve to package.json (always present, exports-agnostic)
|
|
58
|
-
// rather than main/module (the polyfills meta packages have no main).
|
|
59
|
-
const requireFromGjsify = createRequire(gjsifyIssuer);
|
|
60
|
-
const relayIssuers: string[] = [];
|
|
61
|
-
for (const pkg of ["@gjsify/node-polyfills", "@gjsify/web-polyfills"]) {
|
|
62
|
-
try {
|
|
63
|
-
relayIssuers.push(requireFromGjsify.resolve(`${pkg}/package.json`));
|
|
64
|
-
} catch {
|
|
65
|
-
// polyfills package not in dep tree — relay won't cover it
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// pnpapi is a virtual module injected by Yarn PnP at runtime.
|
|
70
|
-
type PnpApi = {
|
|
71
|
-
resolveRequest: (req: string, issuer: string) => string | null;
|
|
72
|
-
};
|
|
73
|
-
let pnpApi: PnpApi | null = null;
|
|
74
|
-
try {
|
|
75
|
-
// pnpapi has no npm package — it is a virtual module injected by Yarn PnP
|
|
76
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
77
|
-
// @ts-expect-error
|
|
78
|
-
pnpApi = (await import("pnpapi")) as PnpApi;
|
|
79
|
-
} catch {
|
|
80
|
-
// Not in a PnP runtime (shouldn't happen since findPnpRoot passed)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return pnpPlugin({
|
|
84
|
-
onResolve: async (args, { resolvedPath, error, watchFiles }) => {
|
|
85
|
-
if (resolvedPath !== null) {
|
|
86
|
-
return { namespace: "pnp", path: resolvedPath, watchFiles };
|
|
87
|
-
}
|
|
88
|
-
if (
|
|
89
|
-
(error as { pnpCode?: string } | null)?.pnpCode ===
|
|
90
|
-
"UNDECLARED_DEPENDENCY"
|
|
91
|
-
) {
|
|
92
|
-
if (pnpApi !== null) {
|
|
93
|
-
// Try @gjsify/cli context first (covers @gjsify/* that are
|
|
94
|
-
// direct deps of cli's own deps — unlikely but fast check).
|
|
95
|
-
try {
|
|
96
|
-
const rp = pnpApi.resolveRequest(args.path, gjsifyIssuer);
|
|
97
|
-
if (rp !== null)
|
|
98
|
-
return { namespace: "pnp", path: rp, watchFiles };
|
|
99
|
-
} catch {}
|
|
100
|
-
// Two-hop relay: resolve from node-polyfills / web-polyfills context
|
|
101
|
-
// which have the individual @gjsify/* packages as direct deps.
|
|
102
|
-
for (const relayIssuer of relayIssuers) {
|
|
103
|
-
try {
|
|
104
|
-
const rp = pnpApi.resolveRequest(args.path, relayIssuer);
|
|
105
|
-
if (rp !== null)
|
|
106
|
-
return { namespace: "pnp", path: rp, watchFiles };
|
|
107
|
-
} catch {}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
// Fall through — bare aliases (abort-controller, fetch/register/*)
|
|
111
|
-
// are handled by the gjsify alias plugin after this returns null,
|
|
112
|
-
// then the re-resolved @gjsify/* path goes through this hook again.
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
return {
|
|
116
|
-
external: true,
|
|
117
|
-
errors: error ? [{ text: error.message }] : [],
|
|
118
|
-
watchFiles,
|
|
119
|
-
};
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
} catch {
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
28
|
+
async function buildPnpPlugin(): Promise<Plugin | null> {
|
|
29
|
+
return getPnpPlugin({
|
|
30
|
+
issuerUrl: import.meta.url,
|
|
31
|
+
transformContentsFactory: (build) => {
|
|
32
|
+
const bundleDir = getBundleDir(build);
|
|
33
|
+
return (args, contents) => rewriteContents(args, contents, bundleDir);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
125
36
|
}
|
|
126
37
|
|
|
127
38
|
export class BuildAction {
|
|
@@ -150,7 +61,7 @@ export class BuildAction {
|
|
|
150
61
|
const multipleBuilds =
|
|
151
62
|
moduleOutdir && mainOutdir && moduleOutdir !== mainOutdir;
|
|
152
63
|
|
|
153
|
-
const pnpPlugin = await
|
|
64
|
+
const pnpPlugin = await buildPnpPlugin();
|
|
154
65
|
const pnpPlugins: Plugin[] = pnpPlugin ? [pnpPlugin] : [];
|
|
155
66
|
|
|
156
67
|
const results: BuildResult[] = [];
|
|
@@ -362,7 +273,7 @@ export class BuildAction {
|
|
|
362
273
|
|
|
363
274
|
const { autoMode, extras } = this.parseGlobalsValue(globals);
|
|
364
275
|
|
|
365
|
-
const pnpPlugin = await
|
|
276
|
+
const pnpPlugin = await buildPnpPlugin();
|
|
366
277
|
const pnpPlugins: Plugin[] = pnpPlugin ? [pnpPlugin] : [];
|
|
367
278
|
|
|
368
279
|
// --- Auto mode (with optional extras): iterative multi-pass build ---
|
package/src/commands/index.ts
CHANGED
|
@@ -0,0 +1,116 @@
|
|
|
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
|
+
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
import type { Command } from '../types/index.js';
|
|
16
|
+
import {
|
|
17
|
+
buildInstallCommand,
|
|
18
|
+
detectPackageManager,
|
|
19
|
+
runMinimalChecks,
|
|
20
|
+
} from '../utils/check-system-deps.js';
|
|
21
|
+
import { detectNativePackages } from '../utils/detect-native-packages.js';
|
|
22
|
+
|
|
23
|
+
interface InstallOptions {
|
|
24
|
+
packages?: string[];
|
|
25
|
+
'save-dev'?: boolean;
|
|
26
|
+
'save-peer'?: boolean;
|
|
27
|
+
'save-optional'?: boolean;
|
|
28
|
+
verbose: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const installCommand: Command<any, InstallOptions> = {
|
|
32
|
+
command: 'install [packages..]',
|
|
33
|
+
description:
|
|
34
|
+
'Install npm dependencies in the current project, then run gjsify-aware post-checks.',
|
|
35
|
+
builder: (yargs) =>
|
|
36
|
+
yargs
|
|
37
|
+
.positional('packages', {
|
|
38
|
+
description: 'Optional package specs. With none, runs a full project install.',
|
|
39
|
+
type: 'string',
|
|
40
|
+
array: true,
|
|
41
|
+
})
|
|
42
|
+
.option('save-dev', { type: 'boolean', alias: 'D' })
|
|
43
|
+
.option('save-peer', { type: 'boolean' })
|
|
44
|
+
.option('save-optional', { type: 'boolean', alias: 'O' })
|
|
45
|
+
.option('verbose', {
|
|
46
|
+
description: 'Verbose npm logging.',
|
|
47
|
+
type: 'boolean',
|
|
48
|
+
default: false,
|
|
49
|
+
}),
|
|
50
|
+
handler: async (args) => {
|
|
51
|
+
const npmArgs = ['install'];
|
|
52
|
+
if (args['save-dev']) npmArgs.push('--save-dev');
|
|
53
|
+
if (args['save-peer']) npmArgs.push('--save-peer');
|
|
54
|
+
if (args['save-optional']) npmArgs.push('--save-optional');
|
|
55
|
+
if (args.verbose) npmArgs.push('--loglevel', 'verbose');
|
|
56
|
+
if (args.packages && args.packages.length > 0) {
|
|
57
|
+
npmArgs.push(...args.packages);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await spawnNpm(npmArgs);
|
|
61
|
+
|
|
62
|
+
await runPostInstallChecks();
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
async function spawnNpm(npmArgs: string[]): Promise<void> {
|
|
67
|
+
return new Promise<void>((resolve, reject) => {
|
|
68
|
+
const child = spawn('npm', npmArgs, { stdio: 'inherit' });
|
|
69
|
+
child.on('close', (code) => {
|
|
70
|
+
if (code === 0) resolve();
|
|
71
|
+
else reject(new Error(`npm install exited with code ${code}`));
|
|
72
|
+
});
|
|
73
|
+
child.on('error', (err) => {
|
|
74
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
75
|
+
const msg = code === 'ENOENT'
|
|
76
|
+
? 'npm not found on PATH — install Node.js first.'
|
|
77
|
+
: `npm install failed: ${err.message}`;
|
|
78
|
+
reject(new Error(msg));
|
|
79
|
+
});
|
|
80
|
+
}).catch((err: Error) => {
|
|
81
|
+
console.error(err.message);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function runPostInstallChecks(): Promise<void> {
|
|
87
|
+
console.log('\n--- gjsify post-install checks ---');
|
|
88
|
+
|
|
89
|
+
// 1. System deps that GJS apps typically need.
|
|
90
|
+
const results = runMinimalChecks();
|
|
91
|
+
const missing = results.filter((r) => !r.found && r.severity === 'required');
|
|
92
|
+
if (missing.length > 0) {
|
|
93
|
+
console.warn('Missing required system dependencies:\n');
|
|
94
|
+
for (const dep of missing) {
|
|
95
|
+
console.warn(` ✗ ${dep.name}`);
|
|
96
|
+
}
|
|
97
|
+
const pm = detectPackageManager();
|
|
98
|
+
const cmd = buildInstallCommand(pm, missing);
|
|
99
|
+
if (cmd) console.warn(`\nInstall with:\n ${cmd}`);
|
|
100
|
+
} else {
|
|
101
|
+
console.log('System dependencies OK.');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 2. Surface @gjsify/* packages with native prebuilds — `gjsify run`
|
|
105
|
+
// will set LD_LIBRARY_PATH / GI_TYPELIB_PATH for these automatically.
|
|
106
|
+
const native = detectNativePackages(process.cwd());
|
|
107
|
+
if (native.length > 0) {
|
|
108
|
+
console.log(
|
|
109
|
+
`\nDetected ${native.length} @gjsify/* package(s) with native prebuilds:`,
|
|
110
|
+
);
|
|
111
|
+
for (const pkg of native) {
|
|
112
|
+
console.log(` • ${pkg.name}`);
|
|
113
|
+
}
|
|
114
|
+
console.log('\nUse `gjsify run <bundle>` to launch with LD_LIBRARY_PATH/GI_TYPELIB_PATH set.');
|
|
115
|
+
}
|
|
116
|
+
}
|
package/src/commands/showcase.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import type { Command } from '../types/index.js';
|
|
2
2
|
import { discoverShowcases, findShowcase } from '../utils/discover-showcases.js';
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
runMinimalChecks,
|
|
5
|
+
checkGwebgl,
|
|
6
|
+
detectPackageManager,
|
|
7
|
+
buildInstallCommand,
|
|
8
|
+
} from '../utils/check-system-deps.js';
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
5
11
|
|
|
6
12
|
interface ShowcaseOptions {
|
|
7
13
|
name?: string;
|
|
@@ -11,9 +17,9 @@ interface ShowcaseOptions {
|
|
|
11
17
|
|
|
12
18
|
export const showcaseCommand: Command<any, ShowcaseOptions> = {
|
|
13
19
|
command: 'showcase [name]',
|
|
14
|
-
description: 'List or run
|
|
15
|
-
builder: (yargs) =>
|
|
16
|
-
|
|
20
|
+
description: 'List or run curated gjsify showcase applications.',
|
|
21
|
+
builder: (yargs) =>
|
|
22
|
+
yargs
|
|
17
23
|
.positional('name', {
|
|
18
24
|
description: 'Showcase name to run (omit to list all)',
|
|
19
25
|
type: 'string',
|
|
@@ -27,8 +33,7 @@ export const showcaseCommand: Command<any, ShowcaseOptions> = {
|
|
|
27
33
|
description: 'List available showcases',
|
|
28
34
|
type: 'boolean',
|
|
29
35
|
default: false,
|
|
30
|
-
})
|
|
31
|
-
},
|
|
36
|
+
}),
|
|
32
37
|
handler: async (args) => {
|
|
33
38
|
// List mode: no name given, or --list flag
|
|
34
39
|
if (!args.name || args.list) {
|
|
@@ -40,11 +45,10 @@ export const showcaseCommand: Command<any, ShowcaseOptions> = {
|
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
if (showcases.length === 0) {
|
|
43
|
-
console.log('No showcases found.
|
|
48
|
+
console.log('No showcases found. The CLI ships a curated list in `showcases.json`; if it is missing the CLI install is incomplete.');
|
|
44
49
|
return;
|
|
45
50
|
}
|
|
46
51
|
|
|
47
|
-
// Group by category
|
|
48
52
|
const grouped = new Map<string, typeof showcases>();
|
|
49
53
|
for (const sc of showcases) {
|
|
50
54
|
const list = grouped.get(sc.category) ?? [];
|
|
@@ -55,7 +59,7 @@ export const showcaseCommand: Command<any, ShowcaseOptions> = {
|
|
|
55
59
|
console.log('Available gjsify showcases:\n');
|
|
56
60
|
for (const [category, list] of grouped) {
|
|
57
61
|
console.log(` ${category.toUpperCase()}:`);
|
|
58
|
-
const maxNameLen = Math.max(...list.map(e => e.name.length));
|
|
62
|
+
const maxNameLen = Math.max(...list.map((e) => e.name.length));
|
|
59
63
|
for (const sc of list) {
|
|
60
64
|
const pad = ' '.repeat(maxNameLen - sc.name.length + 2);
|
|
61
65
|
const desc = sc.description ? `${pad}${sc.description}` : '';
|
|
@@ -68,7 +72,6 @@ export const showcaseCommand: Command<any, ShowcaseOptions> = {
|
|
|
68
72
|
return;
|
|
69
73
|
}
|
|
70
74
|
|
|
71
|
-
// Run mode: find the showcase
|
|
72
75
|
const showcase = findShowcase(args.name);
|
|
73
76
|
if (!showcase) {
|
|
74
77
|
console.error(`Unknown showcase: "${args.name}"`);
|
|
@@ -76,16 +79,14 @@ export const showcaseCommand: Command<any, ShowcaseOptions> = {
|
|
|
76
79
|
process.exit(1);
|
|
77
80
|
}
|
|
78
81
|
|
|
79
|
-
// System dependency check before
|
|
80
|
-
// All showcases need GJS; WebGL showcases additionally need gwebgl prebuilds.
|
|
82
|
+
// System dependency check before delegating — only what this showcase needs.
|
|
81
83
|
const results = runMinimalChecks();
|
|
82
|
-
|
|
83
|
-
if (needsWebgl) {
|
|
84
|
+
if (showcase.needsWebgl) {
|
|
84
85
|
results.push(checkGwebgl(process.cwd()));
|
|
85
86
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
const missingHard = results.filter(
|
|
88
|
+
(r) => !r.found && (r.severity === 'required' || r.id === 'gwebgl'),
|
|
89
|
+
);
|
|
89
90
|
if (missingHard.length > 0) {
|
|
90
91
|
console.error('Missing system dependencies:\n');
|
|
91
92
|
for (const dep of missingHard) {
|
|
@@ -99,8 +100,26 @@ export const showcaseCommand: Command<any, ShowcaseOptions> = {
|
|
|
99
100
|
process.exit(1);
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
// Delegate to `gjsify dlx <package>` — same npm-cache, same atomic
|
|
104
|
+
// symlink-swap, same `gjsify.main` resolution. Re-spawning the CLI
|
|
105
|
+
// keeps the dlx logic in one place.
|
|
106
|
+
console.log(`Running showcase: ${showcase.name} (via gjsify dlx)\n`);
|
|
107
|
+
const cliBin = fileURLToPath(new URL('../index.js', import.meta.url));
|
|
108
|
+
const child = spawn(process.execPath, [cliBin, 'dlx', showcase.packageName], {
|
|
109
|
+
stdio: 'inherit',
|
|
110
|
+
});
|
|
111
|
+
await new Promise<void>((resolvePromise, reject) => {
|
|
112
|
+
child.on('close', (code) => {
|
|
113
|
+
if (code !== 0) {
|
|
114
|
+
reject(new Error(`gjsify dlx exited with code ${code}`));
|
|
115
|
+
} else {
|
|
116
|
+
resolvePromise();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
child.on('error', reject);
|
|
120
|
+
}).catch((err) => {
|
|
121
|
+
console.error(err.message);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
});
|
|
105
124
|
},
|
|
106
125
|
};
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
gresourceCommand as gresource,
|
|
13
13
|
gettextCommand as gettext,
|
|
14
14
|
dlxCommand as dlx,
|
|
15
|
+
installCommand as install,
|
|
15
16
|
} from './commands/index.js'
|
|
16
17
|
import { APP_NAME } from './constants.js'
|
|
17
18
|
|
|
@@ -19,6 +20,7 @@ void yargs(hideBin(process.argv))
|
|
|
19
20
|
.scriptName(APP_NAME)
|
|
20
21
|
.strict()
|
|
21
22
|
.command(create.command, create.description, create.builder, create.handler)
|
|
23
|
+
.command(install.command, install.description, install.builder, install.handler)
|
|
22
24
|
.command(build.command, build.description, build.builder, build.handler)
|
|
23
25
|
.command(run.command, run.description, run.builder, run.handler)
|
|
24
26
|
.command(dlx.command, dlx.description, dlx.builder, dlx.handler)
|
|
@@ -159,8 +159,13 @@ const PACKAGE_DEPS: Record<string, string[]> = {
|
|
|
159
159
|
'@gjsify/canvas2d': ['gdk-pixbuf', 'pango', 'pangocairo', 'cairo'],
|
|
160
160
|
'@gjsify/canvas2d-core': ['gdk-pixbuf', 'pango', 'pangocairo', 'cairo'],
|
|
161
161
|
'@gjsify/dom-elements': ['gdk-pixbuf'],
|
|
162
|
-
// @gjsify/webgl
|
|
163
|
-
//
|
|
162
|
+
// @gjsify/webgl needs the gwebgl npm package (Vala prebuild) — handled
|
|
163
|
+
// as a special-case checkNpmPackage rather than checkPkgConfig in
|
|
164
|
+
// runOptionalChecks. Mapping it here so its presence in the project's
|
|
165
|
+
// dep tree triggers the check.
|
|
166
|
+
'@gjsify/webgl': ['gwebgl'],
|
|
167
|
+
// @gjsify/event-bridge only needs gtk4/gdk which are already in the
|
|
168
|
+
// required set, so it doesn't need an optional entry.
|
|
164
169
|
};
|
|
165
170
|
|
|
166
171
|
/** Walk up from cwd looking for the nearest package.json. */
|
|
@@ -322,10 +327,16 @@ function runOptionalChecks(needed: Set<string> | null, cwd: string): DepCheck[]
|
|
|
322
327
|
results.push(checkPkgConfig(dep.id, dep.name, dep.pkgName, 'optional', requiredBy));
|
|
323
328
|
}
|
|
324
329
|
|
|
325
|
-
// gwebgl npm package — special case (not a pkg-config lib).
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
|
|
330
|
+
// gwebgl npm package — special case (not a pkg-config lib). Only checked
|
|
331
|
+
// when the project directly or transitively depends on @gjsify/webgl —
|
|
332
|
+
// since the CLI tarball no longer ships any showcase example packages,
|
|
333
|
+
// gwebgl is not part of the CLI's own dep tree, so reporting it for every
|
|
334
|
+
// project would always be `found: false`. `needed === null` means "check
|
|
335
|
+
// everything" (no project context).
|
|
336
|
+
const hasWebglDep = needed === null || needed.has('gwebgl');
|
|
337
|
+
if (hasWebglDep) {
|
|
338
|
+
results.push(checkGwebgl(cwd));
|
|
339
|
+
}
|
|
329
340
|
|
|
330
341
|
return results;
|
|
331
342
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
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>`.
|
|
3
8
|
|
|
4
|
-
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
10
|
import { dirname, join } from 'node:path';
|
|
6
|
-
import { createRequire } from 'node:module';
|
|
7
11
|
import { fileURLToPath } from 'node:url';
|
|
8
12
|
|
|
9
13
|
export interface ShowcaseInfo {
|
|
@@ -13,68 +17,53 @@ export interface ShowcaseInfo {
|
|
|
13
17
|
packageName: string;
|
|
14
18
|
/** Category: "dom" or "node" */
|
|
15
19
|
category: string;
|
|
16
|
-
/** Description
|
|
20
|
+
/** Description for the list view */
|
|
17
21
|
description: string;
|
|
18
|
-
/**
|
|
19
|
-
|
|
22
|
+
/** Whether the showcase needs the gwebgl native prebuild. */
|
|
23
|
+
needsWebgl: boolean;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
interface ManifestEntry {
|
|
27
|
+
name: string;
|
|
28
|
+
package: string;
|
|
29
|
+
category: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
needsWebgl?: boolean;
|
|
32
|
+
}
|
|
23
33
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return
|
|
31
|
-
category: suffix.slice(0, dashIdx),
|
|
32
|
-
name: suffix.slice(dashIdx + 1),
|
|
33
|
-
};
|
|
34
|
+
interface Manifest {
|
|
35
|
+
showcases: ManifestEntry[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function manifestPath(): string {
|
|
39
|
+
// `showcases.json` lives at the package root: ../../showcases.json from lib/utils/.
|
|
40
|
+
return join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'showcases.json');
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
44
|
+
* Read the curated showcase list from `showcases.json`. Returns showcases
|
|
45
|
+
* sorted by category then name. An empty list (or missing manifest) yields
|
|
46
|
+
* an empty array — `gjsify showcase` then prints the empty-state message.
|
|
39
47
|
*/
|
|
40
48
|
export function discoverShowcases(): ShowcaseInfo[] {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
const path = manifestPath();
|
|
50
|
+
if (!existsSync(path)) return [];
|
|
51
|
+
|
|
52
|
+
let manifest: Manifest;
|
|
44
53
|
try {
|
|
45
|
-
|
|
54
|
+
manifest = JSON.parse(readFileSync(path, 'utf-8')) as Manifest;
|
|
46
55
|
} catch {
|
|
47
56
|
return [];
|
|
48
57
|
}
|
|
58
|
+
if (!Array.isArray(manifest.showcases)) return [];
|
|
49
59
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const parsed = parseShowcaseName(packageName);
|
|
59
|
-
if (!parsed) continue;
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
const pkgJsonPath = require.resolve(`${packageName}/package.json`);
|
|
63
|
-
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')) as Record<string, unknown>;
|
|
64
|
-
const main = pkg['main'] as string | undefined;
|
|
65
|
-
if (!main) continue;
|
|
66
|
-
|
|
67
|
-
showcases.push({
|
|
68
|
-
name: parsed.name,
|
|
69
|
-
packageName,
|
|
70
|
-
category: parsed.category,
|
|
71
|
-
description: (pkg['description'] as string) ?? '',
|
|
72
|
-
bundlePath: join(dirname(pkgJsonPath), main),
|
|
73
|
-
});
|
|
74
|
-
} catch {
|
|
75
|
-
// Package listed as dep but not resolvable — skip silently
|
|
76
|
-
}
|
|
77
|
-
}
|
|
60
|
+
const showcases: ShowcaseInfo[] = manifest.showcases.map((e) => ({
|
|
61
|
+
name: e.name,
|
|
62
|
+
packageName: e.package,
|
|
63
|
+
category: e.category,
|
|
64
|
+
description: e.description ?? '',
|
|
65
|
+
needsWebgl: Boolean(e.needsWebgl),
|
|
66
|
+
}));
|
|
78
67
|
|
|
79
68
|
showcases.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
|
|
80
69
|
return showcases;
|
|
@@ -82,5 +71,5 @@ export function discoverShowcases(): ShowcaseInfo[] {
|
|
|
82
71
|
|
|
83
72
|
/** Find a single showcase by short name. */
|
|
84
73
|
export function findShowcase(name: string): ShowcaseInfo | undefined {
|
|
85
|
-
return discoverShowcases().find(e => e.name === name);
|
|
74
|
+
return discoverShowcases().find((e) => e.name === name);
|
|
86
75
|
}
|