@gjsify/cli 0.1.8 → 0.1.10

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.
@@ -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
  };
@@ -83,14 +83,16 @@ export const showcaseCommand: Command<any, ShowcaseOptions> = {
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
  }
@@ -1,11 +1,23 @@
1
1
  // System dependency checker for gjsify CLI.
2
2
  // Uses execFileSync with explicit argument arrays — no shell injection possible.
3
3
  // All binary names are hardcoded constants, never derived from user input.
4
+ //
5
+ // Severity model:
6
+ // - 'required' → must be present, exit code 1 if missing
7
+ // - 'optional' → nice to have, only warned about, exit code stays 0
8
+ //
9
+ // Conditional checking: when a project's package.json is reachable from cwd,
10
+ // optional system deps are only checked if the corresponding @gjsify/* package
11
+ // is in the project's dependency tree. A user with only @gjsify/fs in their
12
+ // project never sees a warning about libmanette.
4
13
 
5
14
  import { execFileSync } from 'node:child_process';
6
- import { join } from 'node:path';
15
+ import { join, resolve } from 'node:path';
7
16
  import { createRequire } from 'node:module';
8
17
  import { pathToFileURL } from 'node:url';
18
+ import { readFileSync, existsSync, readdirSync } from 'node:fs';
19
+
20
+ export type DepSeverity = 'required' | 'optional';
9
21
 
10
22
  export interface DepCheck {
11
23
  /** Stable key used for install command lookup, e.g. "gjs" */
@@ -15,6 +27,10 @@ export interface DepCheck {
15
27
  found: boolean;
16
28
  /** Version string when found, e.g. "1.86.0" */
17
29
  version?: string;
30
+ /** required = exit 1 if missing, optional = warn only */
31
+ severity: DepSeverity;
32
+ /** For optional deps: which @gjsify/* packages need this lib */
33
+ requiredBy?: string[];
18
34
  }
19
35
 
20
36
  export type PackageManager = 'apt' | 'dnf' | 'pacman' | 'zypper' | 'apk' | 'unknown';
@@ -29,18 +45,32 @@ function tryExecFile(binary: string, args: string[]): string | null {
29
45
  }
30
46
 
31
47
  /** Check if a binary exists and optionally capture its version output. */
32
- function checkBinary(id: string, name: string, binary: string, versionArgs: string[], parseVersion?: (out: string) => string): DepCheck {
48
+ function checkBinary(
49
+ id: string,
50
+ name: string,
51
+ binary: string,
52
+ versionArgs: string[],
53
+ severity: DepSeverity,
54
+ parseVersion?: (out: string) => string,
55
+ requiredBy?: string[],
56
+ ): DepCheck {
33
57
  const out = tryExecFile(binary, versionArgs);
34
- if (out === null) return { id, name, found: false };
58
+ if (out === null) return { id, name, found: false, severity, requiredBy };
35
59
  const version = parseVersion ? parseVersion(out) : out.split('\n')[0] ?? out;
36
- return { id, name, found: true, version };
60
+ return { id, name, found: true, version, severity, requiredBy };
37
61
  }
38
62
 
39
63
  /** Check a pkg-config library. pkg-config --modversion returns version on stdout. */
40
- function checkPkgConfig(id: string, name: string, libName: string): DepCheck {
64
+ function checkPkgConfig(
65
+ id: string,
66
+ name: string,
67
+ libName: string,
68
+ severity: DepSeverity,
69
+ requiredBy?: string[],
70
+ ): DepCheck {
41
71
  const version = tryExecFile('pkg-config', ['--modversion', libName]);
42
- if (version === null) return { id, name, found: false };
43
- return { id, name, found: true, version: version.split('\n')[0] };
72
+ if (version === null) return { id, name, found: false, severity, requiredBy };
73
+ return { id, name, found: true, version: version.split('\n')[0], severity, requiredBy };
44
74
  }
45
75
 
46
76
  /**
@@ -48,21 +78,28 @@ function checkPkgConfig(id: string, name: string, libName: string): DepCheck {
48
78
  * back to the CLI's own node_modules. This way a locally installed version
49
79
  * takes precedence, but npx usage still works via the CLI's own dependencies.
50
80
  */
51
- function checkNpmPackage(id: string, name: string, packageName: string, cwd: string): DepCheck {
81
+ function checkNpmPackage(
82
+ id: string,
83
+ name: string,
84
+ packageName: string,
85
+ cwd: string,
86
+ severity: DepSeverity,
87
+ requiredBy?: string[],
88
+ ): DepCheck {
52
89
  // 1. Try user's project
53
90
  try {
54
91
  const requireFromCwd = createRequire(pathToFileURL(join(cwd, '_check_.js')).href);
55
92
  requireFromCwd.resolve(packageName);
56
- return { id, name, found: true };
93
+ return { id, name, found: true, severity, requiredBy };
57
94
  } catch { /* not in project, try CLI fallback */ }
58
95
 
59
96
  // 2. Fallback: CLI's own node_modules
60
97
  try {
61
98
  const requireFromCli = createRequire(import.meta.url);
62
99
  requireFromCli.resolve(packageName);
63
- return { id, name, found: true };
100
+ return { id, name, found: true, severity, requiredBy };
64
101
  } catch {
65
- return { id, name, found: false };
102
+ return { id, name, found: false, severity, requiredBy };
66
103
  }
67
104
  }
68
105
 
@@ -75,72 +112,219 @@ export function detectPackageManager(): PackageManager {
75
112
  return 'unknown';
76
113
  }
77
114
 
115
+ // ---------------------------------------------------------------------------
116
+ // @gjsify/* package → system dependency mapping
117
+ // ---------------------------------------------------------------------------
118
+ //
119
+ // Lists which OPTIONAL system dependencies each gjsify package needs at
120
+ // runtime. Required deps (gjs, gtk4, libsoup3, libadwaita, etc.) are checked
121
+ // unconditionally because they're needed by the core toolchain.
122
+ //
123
+ // Used by `runAllChecks(cwd)` to skip optional deps for packages the user
124
+ // isn't using. If a project doesn't depend on @gjsify/gamepad, libmanette
125
+ // is not checked (and won't appear as a warning).
126
+
127
+ interface OptionalDep {
128
+ /** DepCheck id */
129
+ id: string;
130
+ /** Display name */
131
+ name: string;
132
+ /** pkg-config library name */
133
+ pkgName: string;
134
+ }
135
+
136
+ /** Optional system deps keyed by their DepCheck id. */
137
+ const OPTIONAL_DEPS: Record<string, OptionalDep> = {
138
+ manette: { id: 'manette', name: 'libmanette', pkgName: 'manette-0.2' },
139
+ gstreamer: { id: 'gstreamer', name: 'GStreamer', pkgName: 'gstreamer-1.0' },
140
+ 'gst-app': { id: 'gst-app', name: 'GStreamer App', pkgName: 'gstreamer-app-1.0' },
141
+ 'gdk-pixbuf':{ id: 'gdk-pixbuf', name: 'GdkPixbuf', pkgName: 'gdk-pixbuf-2.0' },
142
+ pango: { id: 'pango', name: 'Pango', pkgName: 'pango' },
143
+ pangocairo: { id: 'pangocairo', name: 'PangoCairo', pkgName: 'pangocairo' },
144
+ webkitgtk: { id: 'webkitgtk', name: 'WebKitGTK', pkgName: 'webkitgtk-6.0' },
145
+ cairo: { id: 'cairo', name: 'Cairo', pkgName: 'cairo' },
146
+ };
147
+
148
+ /**
149
+ * Map of @gjsify/* package name → ids of OPTIONAL_DEPS this package needs.
150
+ * Generated by walking each package's `gi://` imports (excluding the always-
151
+ * available trio GLib/GObject/Gio).
152
+ *
153
+ * Used to compute the set of optional deps to check for a given project.
154
+ */
155
+ const PACKAGE_DEPS: Record<string, string[]> = {
156
+ '@gjsify/gamepad': ['manette'],
157
+ '@gjsify/webaudio': ['gstreamer', 'gst-app'],
158
+ '@gjsify/iframe': ['webkitgtk'],
159
+ '@gjsify/canvas2d': ['gdk-pixbuf', 'pango', 'pangocairo', 'cairo'],
160
+ '@gjsify/canvas2d-core': ['gdk-pixbuf', 'pango', 'pangocairo', 'cairo'],
161
+ '@gjsify/dom-elements': ['gdk-pixbuf'],
162
+ // @gjsify/webgl, @gjsify/event-bridge only need gtk4/gdk which are
163
+ // already in the required set, so they don't need optional entries.
164
+ };
165
+
166
+ /** Walk up from cwd looking for the nearest package.json. */
167
+ function findProjectRoot(cwd: string): string | null {
168
+ let dir = resolve(cwd);
169
+ while (true) {
170
+ if (existsSync(join(dir, 'package.json'))) return dir;
171
+ const parent = resolve(dir, '..');
172
+ if (parent === dir) return null;
173
+ dir = parent;
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Discover which @gjsify/* packages a project depends on (transitively),
179
+ * by walking the node_modules tree from the project root. Returns a Set of
180
+ * package names like '@gjsify/webgl', '@gjsify/canvas2d'.
181
+ *
182
+ * If the project root cannot be determined or has no node_modules, returns
183
+ * `null` to signal "check all optional deps" — this matches the historical
184
+ * behaviour where `gjsify check` outside any project shows the full list.
185
+ */
186
+ function discoverGjsifyPackages(cwd: string): Set<string> | null {
187
+ const root = findProjectRoot(cwd);
188
+ if (!root) return null;
189
+
190
+ const nodeModulesDir = join(root, 'node_modules', '@gjsify');
191
+ if (!existsSync(nodeModulesDir)) {
192
+ // Project exists but no @gjsify/* installed → only need core checks.
193
+ return new Set();
194
+ }
195
+
196
+ // Read top-level package.json to determine if @gjsify/cli is the only
197
+ // dep (in which case we still want to warn about everything the CLI
198
+ // transitively brings in). Otherwise scan node_modules for installed packages.
199
+ const pkgJsonPath = join(root, 'package.json');
200
+ let topPkg: Record<string, unknown> = {};
201
+ try {
202
+ topPkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
203
+ } catch { /* ignore */ }
204
+
205
+ const directDeps = {
206
+ ...(topPkg.dependencies as Record<string, string> | undefined),
207
+ ...(topPkg.devDependencies as Record<string, string> | undefined),
208
+ };
209
+
210
+ const found = new Set<string>();
211
+ try {
212
+ for (const entry of readdirSync(nodeModulesDir, { withFileTypes: true })) {
213
+ if (entry.isDirectory() || entry.isSymbolicLink()) {
214
+ found.add(`@gjsify/${entry.name}`);
215
+ }
216
+ }
217
+ } catch { /* ignore */ }
218
+
219
+ // Also include direct deps even if node_modules walk failed
220
+ for (const dep of Object.keys(directDeps)) {
221
+ if (dep.startsWith('@gjsify/')) found.add(dep);
222
+ }
223
+
224
+ return found;
225
+ }
226
+
227
+ /**
228
+ * Compute the set of optional dep ids that should be checked for the
229
+ * current project. Returns null to indicate "check all" (no project context).
230
+ */
231
+ function computeNeededOptionalDeps(cwd: string): Set<string> | null {
232
+ const installedPackages = discoverGjsifyPackages(cwd);
233
+ if (installedPackages === null) return null; // no project → check everything
234
+
235
+ const needed = new Set<string>();
236
+ for (const pkg of installedPackages) {
237
+ const deps = PACKAGE_DEPS[pkg];
238
+ if (deps) for (const id of deps) needed.add(id);
239
+ }
240
+ return needed;
241
+ }
242
+
78
243
  /**
79
244
  * Run all dependency checks. Used by `gjsify check` to show full system status.
245
+ *
246
+ * Required deps (gjs, gtk4, libsoup3, libadwaita, gobject-introspection,
247
+ * blueprint-compiler, pkg-config, meson) are always checked.
248
+ *
249
+ * Optional deps are checked conditionally based on which @gjsify/* packages
250
+ * the project (resolved from cwd) actually consumes. If no project context
251
+ * is available, all optional deps are checked.
80
252
  */
81
253
  export function runAllChecks(cwd: string): DepCheck[] {
82
- return [...runMinimalChecks(), ...runExtraChecks(cwd)];
254
+ const needed = computeNeededOptionalDeps(cwd);
255
+ return [...runMinimalChecks(), ...runRequiredChecks(cwd), ...runOptionalChecks(needed, cwd)];
83
256
  }
84
257
 
85
258
  /**
86
- * Minimal checks needed to run any GJS example (GJS binary only).
259
+ * Minimal checks needed to run any GJS example (Node + GJS binaries only).
87
260
  * Used by `gjsify showcase` for examples that have no native deps.
88
261
  */
89
262
  export function runMinimalChecks(): DepCheck[] {
90
263
  const results: DepCheck[] = [];
91
264
 
92
265
  // Node.js — always present
93
- results.push({ id: 'nodejs', name: 'Node.js', found: true, version: process.version });
266
+ results.push({ id: 'nodejs', name: 'Node.js', found: true, version: process.version, severity: 'required' });
94
267
 
95
268
  // GJS
96
- results.push(checkBinary('gjs', 'GJS', 'gjs', ['--version'],
269
+ results.push(checkBinary('gjs', 'GJS', 'gjs', ['--version'], 'required',
97
270
  (out) => out.replace(/^GJS\s+/i, '').split('\n')[0] ?? out));
98
271
 
99
272
  return results;
100
273
  }
101
274
 
102
- /** Check gwebgl npm package (project first, CLI fallback). */
275
+ /** Check gwebgl npm package (project first, CLI fallback). Optional — only needed by @gjsify/webgl users. */
103
276
  export function checkGwebgl(cwd: string): DepCheck {
104
- return checkNpmPackage('gwebgl', 'gwebgl (@gjsify/webgl)', '@gjsify/webgl', cwd);
277
+ return checkNpmPackage('gwebgl', 'gwebgl (@gjsify/webgl)', '@gjsify/webgl', cwd, 'optional', ['@gjsify/webgl']);
105
278
  }
106
279
 
107
280
  /**
108
- * Extra checks for development and full system audit.
281
+ * Required system dependencies always checked, missing → exit 1.
282
+ * Includes the core build toolchain (pkg-config, meson, blueprint-compiler)
283
+ * and the foundational libraries (gtk4, libadwaita, libsoup3,
284
+ * gobject-introspection) that nearly every gjsify app needs.
109
285
  */
110
- function runExtraChecks(cwd: string): DepCheck[] {
286
+ function runRequiredChecks(_cwd: string): DepCheck[] {
111
287
  const results: DepCheck[] = [];
112
288
 
113
- // Blueprint Compiler
289
+ // Build toolchain
114
290
  results.push(checkBinary('blueprint-compiler', 'Blueprint Compiler',
115
- 'blueprint-compiler', ['--version']));
291
+ 'blueprint-compiler', ['--version'], 'required'));
292
+ results.push(checkBinary('pkg-config', 'pkg-config', 'pkg-config', ['--version'], 'required'));
293
+ results.push(checkBinary('meson', 'Meson', 'meson', ['--version'], 'required'));
116
294
 
117
- // pkg-config (needed for library checks)
118
- results.push(checkBinary('pkg-config', 'pkg-config', 'pkg-config', ['--version']));
295
+ // Foundational libraries
296
+ results.push(checkPkgConfig('gtk4', 'GTK4', 'gtk4', 'required'));
297
+ results.push(checkPkgConfig('libadwaita', 'libadwaita', 'libadwaita-1', 'required'));
298
+ results.push(checkPkgConfig('libsoup3', 'libsoup3', 'libsoup-3.0', 'required'));
299
+ results.push(checkPkgConfig('gobject-introspection', 'GObject Introspection',
300
+ 'gobject-introspection-1.0', 'required'));
119
301
 
120
- // Meson (for building native extensions)
121
- results.push(checkBinary('meson', 'Meson', 'meson', ['--version']));
302
+ return results;
303
+ }
122
304
 
123
- // GTK4
124
- results.push(checkPkgConfig('gtk4', 'GTK4', 'gtk4'));
305
+ /**
306
+ * Optional system dependencies — only checked if the corresponding @gjsify/*
307
+ * package is in use. Missing optional deps generate warnings, not errors.
308
+ *
309
+ * @param needed Set of optional dep ids to check, or null to check all.
310
+ * @param cwd Used to resolve the gwebgl npm package check.
311
+ */
312
+ function runOptionalChecks(needed: Set<string> | null, cwd: string): DepCheck[] {
313
+ const results: DepCheck[] = [];
125
314
 
126
- // libadwaita
127
- results.push(checkPkgConfig('libadwaita', 'libadwaita', 'libadwaita-1'));
315
+ for (const [id, dep] of Object.entries(OPTIONAL_DEPS)) {
316
+ if (needed !== null && !needed.has(id)) continue;
128
317
 
129
- // libsoup3
130
- results.push(checkPkgConfig('libsoup3', 'libsoup3', 'libsoup-3.0'));
318
+ const requiredBy = Object.entries(PACKAGE_DEPS)
319
+ .filter(([, ids]) => ids.includes(id))
320
+ .map(([pkg]) => pkg);
131
321
 
132
- // WebKitGTK — try 4.1 first, fall back to 4.0
133
- const webkitCheck = checkPkgConfig('webkitgtk', 'WebKitGTK', 'webkit2gtk-4.1');
134
- if (webkitCheck.found) {
135
- results.push(webkitCheck);
136
- } else {
137
- results.push(checkPkgConfig('webkitgtk', 'WebKitGTK', 'webkitgtk-6.0'));
322
+ results.push(checkPkgConfig(dep.id, dep.name, dep.pkgName, 'optional', requiredBy));
138
323
  }
139
324
 
140
- // GObject Introspection
141
- results.push(checkPkgConfig('gobject-introspection', 'GObject Introspection', 'gobject-introspection-1.0'));
142
-
143
- // gwebgl — project first, CLI fallback.
325
+ // gwebgl npm package — special case (not a pkg-config lib).
326
+ // Always reported (the npm package is bundled with the CLI), but marked
327
+ // optional because only @gjsify/webgl users need it.
144
328
  results.push(checkGwebgl(cwd));
145
329
 
146
330
  return results;
@@ -157,8 +341,15 @@ const PM_PACKAGES: Record<PackageManager, Partial<Record<string, string>>> = {
157
341
  gtk4: 'libgtk-4-dev',
158
342
  libadwaita: 'libadwaita-1-dev',
159
343
  libsoup3: 'libsoup-3.0-dev',
160
- webkitgtk: 'libwebkit2gtk-4.1-dev',
344
+ webkitgtk: 'libwebkit2gtk-6.0-dev',
161
345
  'gobject-introspection': 'gobject-introspection libgirepository1.0-dev',
346
+ manette: 'libmanette-0.2-0 gir1.2-manette-0.2',
347
+ gstreamer: 'libgstreamer1.0-dev',
348
+ 'gst-app': 'libgstreamer-plugins-base1.0-dev gir1.2-gst-plugins-base-1.0',
349
+ 'gdk-pixbuf': 'libgdk-pixbuf-2.0-dev',
350
+ pango: 'libpango1.0-dev',
351
+ pangocairo: 'libpango1.0-dev',
352
+ cairo: 'libcairo2-dev',
162
353
  },
163
354
  dnf: {
164
355
  gjs: 'gjs',
@@ -170,6 +361,13 @@ const PM_PACKAGES: Record<PackageManager, Partial<Record<string, string>>> = {
170
361
  libsoup3: 'libsoup3-devel',
171
362
  webkitgtk: 'webkitgtk6.0-devel',
172
363
  'gobject-introspection': 'gobject-introspection-devel',
364
+ manette: 'libmanette-devel',
365
+ gstreamer: 'gstreamer1-devel',
366
+ 'gst-app': 'gstreamer1-plugins-base-devel',
367
+ 'gdk-pixbuf': 'gdk-pixbuf2-devel',
368
+ pango: 'pango-devel',
369
+ pangocairo: 'pango-devel',
370
+ cairo: 'cairo-devel',
173
371
  },
174
372
  pacman: {
175
373
  gjs: 'gjs',
@@ -179,8 +377,15 @@ const PM_PACKAGES: Record<PackageManager, Partial<Record<string, string>>> = {
179
377
  gtk4: 'gtk4',
180
378
  libadwaita: 'libadwaita',
181
379
  libsoup3: 'libsoup3',
182
- webkitgtk: 'webkit2gtk-4.1',
380
+ webkitgtk: 'webkitgtk-6.0',
183
381
  'gobject-introspection': 'gobject-introspection',
382
+ manette: 'libmanette',
383
+ gstreamer: 'gstreamer',
384
+ 'gst-app': 'gst-plugins-base',
385
+ 'gdk-pixbuf': 'gdk-pixbuf2',
386
+ pango: 'pango',
387
+ pangocairo: 'pango',
388
+ cairo: 'cairo',
184
389
  },
185
390
  zypper: {
186
391
  gjs: 'gjs',
@@ -190,8 +395,15 @@ const PM_PACKAGES: Record<PackageManager, Partial<Record<string, string>>> = {
190
395
  gtk4: 'gtk4-devel',
191
396
  libadwaita: 'libadwaita-devel',
192
397
  libsoup3: 'libsoup-3_0-devel',
193
- webkitgtk: 'webkit2gtk3-devel',
398
+ webkitgtk: 'webkitgtk6_0-devel',
194
399
  'gobject-introspection': 'gobject-introspection-devel',
400
+ manette: 'libmanette-0_2-0-devel',
401
+ gstreamer: 'gstreamer-devel',
402
+ 'gst-app': 'gstreamer-plugins-base-devel',
403
+ 'gdk-pixbuf': 'gdk-pixbuf-devel',
404
+ pango: 'pango-devel',
405
+ pangocairo: 'pango-devel',
406
+ cairo: 'cairo-devel',
195
407
  },
196
408
  apk: {
197
409
  gjs: 'gjs',
@@ -201,8 +413,15 @@ const PM_PACKAGES: Record<PackageManager, Partial<Record<string, string>>> = {
201
413
  gtk4: 'gtk4.0-dev',
202
414
  libadwaita: 'libadwaita-dev',
203
415
  libsoup3: 'libsoup3-dev',
204
- webkitgtk: 'webkit2gtk-4.1-dev',
416
+ webkitgtk: 'webkit2gtk-6.0-dev',
205
417
  'gobject-introspection': 'gobject-introspection-dev',
418
+ manette: 'libmanette-dev',
419
+ gstreamer: 'gstreamer-dev',
420
+ 'gst-app': 'gst-plugins-base-dev',
421
+ 'gdk-pixbuf': 'gdk-pixbuf-dev',
422
+ pango: 'pango-dev',
423
+ pangocairo: 'pango-dev',
424
+ cairo: 'cairo-dev',
206
425
  },
207
426
  unknown: {},
208
427
  };
@@ -135,6 +135,10 @@ function findNearestPackageJson(startDir: string): string | null {
135
135
  * Reads the nearest package.json to discover dependencies, then checks each
136
136
  * for gjsify native prebuilds metadata.
137
137
  *
138
+ * Also checks the **nearest package.json itself** — a workspace package may
139
+ * have its own prebuilds (e.g. `@gjsify/webgl` running its own test) and
140
+ * never list itself in dependencies.
141
+ *
138
142
  * This complements detectNativePackages() (filesystem walk from CWD) by using
139
143
  * require.resolve() — which handles hoisting, workspaces, and nested node_modules.
140
144
  */
@@ -152,6 +156,14 @@ export function resolveNativePackages(fromFilePath: string): NativePackage[] {
152
156
  const pkg = readPackageJson(nearestPkgJson);
153
157
  if (!pkg) return results;
154
158
 
159
+ // Check the nearest package itself (e.g. @gjsify/webgl running its own
160
+ // test bundle — webgl never lists itself in dependencies)
161
+ const ownName = typeof pkg['name'] === 'string' ? pkg['name'] as string : '';
162
+ if (ownName) {
163
+ const ownNative = checkPackage(dirname(nearestPkgJson), ownName, arch);
164
+ if (ownNative) results.push(ownNative);
165
+ }
166
+
155
167
  const deps = pkg['dependencies'] as Record<string, string> | undefined;
156
168
  if (!deps) return results;
157
169