@gjsify/cli 0.4.27 → 0.4.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/cli.gjs.mjs +34 -32
  2. package/lib/actions/barrels-generate.js +1 -5
  3. package/lib/actions/build.d.ts +3 -3
  4. package/lib/actions/build.js +56 -64
  5. package/lib/bundler-pick.d.ts +3 -3
  6. package/lib/bundler-pick.js +5 -6
  7. package/lib/commands/build.js +37 -31
  8. package/lib/commands/check.js +3 -3
  9. package/lib/commands/fix.js +33 -23
  10. package/lib/commands/flatpak/build.js +6 -2
  11. package/lib/commands/flatpak/check.js +9 -3
  12. package/lib/commands/flatpak/ci.js +1 -2
  13. package/lib/commands/flatpak/deps.js +1 -2
  14. package/lib/commands/flatpak/diff.js +2 -6
  15. package/lib/commands/flatpak/init.js +19 -19
  16. package/lib/commands/flatpak/release.js +2 -2
  17. package/lib/commands/flatpak/scaffold.js +3 -11
  18. package/lib/commands/flatpak/sync-flathub.js +4 -8
  19. package/lib/commands/flatpak/utils.js +1 -6
  20. package/lib/commands/foreach.js +5 -14
  21. package/lib/commands/format.js +54 -41
  22. package/lib/commands/gettext.js +2 -10
  23. package/lib/commands/gresource.js +2 -8
  24. package/lib/commands/gsettings.js +1 -3
  25. package/lib/commands/install.js +13 -6
  26. package/lib/commands/lint.d.ts +1 -1
  27. package/lib/commands/lint.js +22 -22
  28. package/lib/commands/pack.js +29 -17
  29. package/lib/commands/publish.d.ts +1 -0
  30. package/lib/commands/publish.js +113 -21
  31. package/lib/commands/run.js +2 -6
  32. package/lib/commands/self-update.js +36 -8
  33. package/lib/commands/showcase.js +1 -1
  34. package/lib/commands/system-check.js +8 -11
  35. package/lib/commands/test.js +12 -8
  36. package/lib/commands/uninstall.js +1 -3
  37. package/lib/commands/upgrade.d.ts +1 -1
  38. package/lib/commands/upgrade.js +109 -120
  39. package/lib/commands/workspace.js +1 -3
  40. package/lib/config.js +18 -13
  41. package/lib/index.js +21 -0
  42. package/lib/templates/install.mjs.tmpl +20 -14
  43. package/lib/templates/oxfmtrc.tmpl +54 -0
  44. package/lib/templates/oxlintrc.json.tmpl +35 -0
  45. package/lib/types/config-data.d.ts +23 -13
  46. package/lib/utils/check-system-deps.js +10 -4
  47. package/lib/utils/detect-native-packages.js +1 -1
  48. package/lib/utils/dlx-cache.js +2 -7
  49. package/lib/utils/install-backend-native.d.ts +2 -2
  50. package/lib/utils/install-backend-native.js +72 -63
  51. package/lib/utils/install-backend.d.ts +13 -0
  52. package/lib/utils/install-backend.js +2 -1
  53. package/lib/utils/install-global.js +1 -3
  54. package/lib/utils/normalize-bundler-options.js +52 -17
  55. package/lib/utils/oxc-resolve.d.ts +63 -0
  56. package/lib/utils/oxc-resolve.js +264 -0
  57. package/lib/utils/pkg-json-edit.js +1 -6
  58. package/lib/utils/run-gjs.js +1 -4
  59. package/lib/utils/run-lifecycle-script.js +3 -7
  60. package/lib/utils/workspace-root.js +3 -1
  61. package/package.json +17 -17
  62. package/lib/templates/biome.json.tmpl +0 -79
  63. package/lib/utils/biome-resolve.d.ts +0 -47
  64. package/lib/utils/biome-resolve.js +0 -204
@@ -0,0 +1,54 @@
1
+ {
2
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
3
+ "useTabs": false,
4
+ "tabWidth": 4,
5
+ "printWidth": 120,
6
+ "singleQuote": true,
7
+ "jsxSingleQuote": false,
8
+ "quoteProps": "as-needed",
9
+ "trailingComma": "all",
10
+ "semi": true,
11
+ "arrowParens": "always",
12
+ "bracketSameLine": false,
13
+ "bracketSpacing": true,
14
+ "ignorePatterns": [
15
+ "**/*.json",
16
+ "**/*.jsonc",
17
+ "**/*.json5",
18
+ "**/*.css",
19
+ "**/*.scss",
20
+ "**/*.sass",
21
+ "**/*.less",
22
+ "**/*.html",
23
+ "**/*.htm",
24
+ "**/*.md",
25
+ "**/*.mdx",
26
+ "**/*.yml",
27
+ "**/*.yaml",
28
+ "**/*.vue",
29
+ "**/*.svelte",
30
+ "**/*.toml",
31
+ "**/*.graphql",
32
+ "**/*.gql",
33
+ "**/node_modules",
34
+ "**/dist",
35
+ "**/lib",
36
+ "**/build",
37
+ "**/build-dir",
38
+ "**/builddir",
39
+ "**/flatpak-build",
40
+ "**/.flatpak-builder",
41
+ "**/repo",
42
+ "**/coverage",
43
+ "**/refs",
44
+ "**/@types",
45
+ "**/templates",
46
+ "**/prebuilds",
47
+ "**/cli.gjs.mjs",
48
+ "**/test.gjs.mjs",
49
+ "**/test.node.mjs",
50
+ "**/*.gresource",
51
+ "**/*.compiled",
52
+ "**/*.metainfo.xml"
53
+ ]
54
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
3
+ "categories": {
4
+ "correctness": "error"
5
+ },
6
+ "rules": {
7
+ "typescript/consistent-type-imports": "warn",
8
+ "typescript/no-explicit-any": "warn",
9
+ "typescript/no-non-null-assertion": "off",
10
+ "unicorn/prefer-node-protocol": "error",
11
+ "eslint/no-unused-vars": "warn"
12
+ },
13
+ "ignorePatterns": [
14
+ "**/node_modules",
15
+ "**/dist",
16
+ "**/lib",
17
+ "**/build",
18
+ "**/build-dir",
19
+ "**/builddir",
20
+ "**/flatpak-build",
21
+ "**/.flatpak-builder",
22
+ "**/repo",
23
+ "**/coverage",
24
+ "**/refs",
25
+ "**/@types",
26
+ "**/templates",
27
+ "**/prebuilds",
28
+ "**/cli.gjs.mjs",
29
+ "**/test.gjs.mjs",
30
+ "**/test.node.mjs",
31
+ "**/*.gresource",
32
+ "**/*.compiled",
33
+ "**/*.metainfo.xml"
34
+ ]
35
+ }
@@ -165,20 +165,30 @@ export interface ConfigData {
165
165
  }>;
166
166
  /**
167
167
  * Extension → loader-kind map for files Rolldown does not classify
168
- * natively. Currently only `'text'` is implemented — the file's content
169
- * becomes the JS string default export (`export default "<content>"`).
170
- * Replaces the legacy esbuild `loader: { '.ui': 'text' }` pattern.
168
+ * natively. Replaces the legacy esbuild `loader: { '.ui': 'text', '.png': 'dataurl' }`
169
+ * pattern.
170
+ *
171
+ * Loader kinds:
172
+ * `'text'` — file contents as a JS string default export
173
+ * (`export default "<source>"`). Good for GLSL shaders,
174
+ * `.ui` GtkBuilder XML, `.asm`, etc.
175
+ * `'dataurl'` — `data:<mime>;base64,<b64>` string default export.
176
+ * MIME is inferred from the extension (.png → image/png,
177
+ * .jpg → image/jpeg, .gif → image/gif, .svg → image/svg+xml,
178
+ * .webp → image/webp, fallback application/octet-stream).
179
+ * Good for Excalibur's `ImageSource` and any library that
180
+ * accepts a data: URL rather than a separate asset file.
171
181
  *
172
182
  * Example:
173
183
  * ```jsonc
174
- * "loaders": { ".ui": "text", ".asm": "text" }
184
+ * "loaders": { ".ui": "text", ".glsl": "text", ".png": "dataurl" }
175
185
  * ```
176
186
  *
177
187
  * Lives at the top level (not under `bundler`) so it doesn't leak into
178
- * Rolldown's options on pass-through; the CLI converts it into a
179
- * `text-loader` plugin prepended to the bundler's plugin chain.
188
+ * Rolldown's options on pass-through; the CLI converts it into a plugin
189
+ * prepended to the bundler's plugin chain.
180
190
  */
181
- loaders?: Record<string, 'text'>;
191
+ loaders?: Record<string, 'text' | 'dataurl'>;
182
192
  /**
183
193
  * Flatpak-related configuration consumed by `gjsify flatpak <sub>`.
184
194
  * Lives in its own top-level namespace so the bundler config doesn't
@@ -188,8 +198,8 @@ export interface ConfigData {
188
198
  flatpak?: ConfigDataFlatpak;
189
199
  /**
190
200
  * Format/lint config consumed by `gjsify format` / `gjsify lint` /
191
- * `gjsify fix`. Thin shell — Biome's own `biome.json` is the real
192
- * configuration file; we only need a pointer here.
201
+ * `gjsify fix`. Thin shell — oxc's own `.oxfmtrc.json` / `.oxlintrc.json`
202
+ * are the real configuration files; we only need a pointer here.
193
203
  */
194
204
  format?: ConfigDataFormat;
195
205
  /**
@@ -198,12 +208,12 @@ export interface ConfigData {
198
208
  */
199
209
  test?: ConfigDataTest;
200
210
  }
201
- /** Optional pointer to a non-default biome.json. */
211
+ /** Optional pointer to a non-default oxc config file. */
202
212
  export interface ConfigDataFormat {
203
213
  /**
204
- * Path to biome.json. Default: walks up from cwd to find one;
205
- * falls back to the recommended template shipped with `gjsify`
206
- * (writable via `gjsify format --init`).
214
+ * Path to an `.oxfmtrc.json` / `.oxlintrc.json`. Default: walks up from
215
+ * cwd to find one; falls back to the recommended templates shipped with
216
+ * `gjsify` (writable via `gjsify format --init`).
207
217
  */
208
218
  configPath?: string;
209
219
  }
@@ -29,7 +29,7 @@ function checkBinary(id, name, binary, versionArgs, severity, parseVersion, requ
29
29
  const out = tryExecFile(binary, versionArgs);
30
30
  if (out === null)
31
31
  return { id, name, found: false, severity, requiredBy };
32
- const version = parseVersion ? parseVersion(out) : out.split('\n')[0] ?? out;
32
+ const version = parseVersion ? parseVersion(out) : (out.split('\n')[0] ?? out);
33
33
  return { id, name, found: true, version, severity, requiredBy };
34
34
  }
35
35
  /** Check a pkg-config library. pkg-config --modversion returns version on stdout. */
@@ -51,7 +51,9 @@ function checkNpmPackage(id, name, packageName, cwd, severity, requiredBy) {
51
51
  requireFromCwd.resolve(packageName);
52
52
  return { id, name, found: true, severity, requiredBy };
53
53
  }
54
- catch { /* not in project, try CLI fallback */ }
54
+ catch {
55
+ /* not in project, try CLI fallback */
56
+ }
55
57
  // 2. Fallback: CLI's own node_modules
56
58
  try {
57
59
  const requireFromCli = createRequire(import.meta.url);
@@ -156,7 +158,9 @@ function discoverGjsifyPackages(cwd) {
156
158
  try {
157
159
  topPkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
158
160
  }
159
- catch { /* ignore */ }
161
+ catch {
162
+ /* ignore */
163
+ }
160
164
  const directDeps = {
161
165
  ...topPkg.dependencies,
162
166
  ...topPkg.devDependencies,
@@ -169,7 +173,9 @@ function discoverGjsifyPackages(cwd) {
169
173
  }
170
174
  }
171
175
  }
172
- catch { /* ignore */ }
176
+ catch {
177
+ /* ignore */
178
+ }
173
179
  // Also include direct deps even if node_modules walk failed
174
180
  for (const dep of Object.keys(directDeps)) {
175
181
  if (dep.startsWith('@gjsify/'))
@@ -131,7 +131,7 @@ export function detectNativePackages(startDir) {
131
131
  * Prepends the new paths to any existing values from the environment.
132
132
  */
133
133
  export function buildNativeEnv(packages) {
134
- const dirs = packages.map(p => p.prebuildsDir);
134
+ const dirs = packages.map((p) => p.prebuildsDir);
135
135
  const existing_ld = process.env['LD_LIBRARY_PATH'] ?? '';
136
136
  const existing_gi = process.env['GI_TYPELIB_PATH'] ?? '';
137
137
  const LD_LIBRARY_PATH = [...dirs, ...(existing_ld ? [existing_ld] : [])].join(':');
@@ -84,13 +84,8 @@ export function symlinkSwap(cacheDir, prepareDir) {
84
84
  const linkPath = join(cacheDir, 'pkg');
85
85
  const tmpName = `pkg.tmp-${Date.now().toString(16)}-${process.pid.toString(16)}`;
86
86
  const tmpLink = join(cacheDir, tmpName);
87
- try {
88
- symlinkSync(prepareDir, tmpLink, 'dir');
89
- }
90
- catch (err) {
91
- // If we cannot even create the tmp link, give up.
92
- throw err;
93
- }
87
+ // If we cannot even create the tmp link, give up (the error propagates).
88
+ symlinkSync(prepareDir, tmpLink, 'dir');
94
89
  try {
95
90
  renameSync(tmpLink, linkPath);
96
91
  }
@@ -1,5 +1,5 @@
1
- import { type Packument } from "@gjsify/npm-registry";
2
- import type { InstallOptions } from "./install-backend.ts";
1
+ import { type Packument } from '@gjsify/npm-registry';
2
+ import type { InstallOptions } from './install-backend.ts';
3
3
  interface ParsedSpec {
4
4
  name: string;
5
5
  range: string;
@@ -13,18 +13,18 @@
13
13
  //
14
14
  // Out of scope (still deferred): peerDependencies validation,
15
15
  // lifecycle scripts, git/file specs.
16
- import * as fs from "node:fs";
17
- import * as path from "node:path";
18
- import * as os from "node:os";
19
- import { Range, SemVer, maxSatisfying, satisfies, } from "@gjsify/semver";
20
- import { DEFAULT_REGISTRY, fetchPackument, fetchTarball, parseNpmrc, } from "@gjsify/npm-registry";
21
- import { extractTarball } from "@gjsify/tar";
22
- const DEFAULT_CONCURRENCY = Number(process.env.GJSIFY_INSTALL_CONCURRENCY ?? "8") || 8;
23
- const LOCKFILE_NAME = "gjsify-lock.json";
16
+ import * as fs from 'node:fs';
17
+ import * as path from 'node:path';
18
+ import * as os from 'node:os';
19
+ import { Range, SemVer, maxSatisfying, satisfies } from '@gjsify/semver';
20
+ import { DEFAULT_REGISTRY, fetchPackument, fetchTarball, parseNpmrc, } from '@gjsify/npm-registry';
21
+ import { extractTarball } from '@gjsify/tar';
22
+ const DEFAULT_CONCURRENCY = Number(process.env.GJSIFY_INSTALL_CONCURRENCY ?? '8') || 8;
23
+ const LOCKFILE_NAME = 'gjsify-lock.json';
24
24
  const LOCKFILE_VERSION = 2;
25
25
  export async function installPackagesNative(opts) {
26
26
  if (opts.specs.length === 0) {
27
- throw new Error("installPackagesNative: empty specs list");
27
+ throw new Error('installPackagesNative: empty specs list');
28
28
  }
29
29
  fs.mkdirSync(opts.prefix, { recursive: true });
30
30
  const npmrc = await loadNpmrc(opts);
@@ -47,25 +47,25 @@ export async function installPackagesNative(opts) {
47
47
  throw new Error(`install: --immutable but ${lockfilePath} is stale.\n${drift}\n` +
48
48
  `Re-run \`gjsify install\` (without --immutable) to refresh the lockfile.`);
49
49
  }
50
- log("install: --immutable, using lockfile (%d package(s))", Object.keys(existingLock.packages).length);
50
+ log('install: --immutable, using lockfile (%d package(s))', Object.keys(existingLock.packages).length);
51
51
  nodes = lockfileToNodes(existingLock);
52
52
  }
53
53
  else if (existingLock && lockfileMatchesRequest(existingLock, opts.specs)) {
54
- log("install: using lockfile (%d package(s))", Object.keys(existingLock.packages).length);
54
+ log('install: using lockfile (%d package(s))', Object.keys(existingLock.packages).length);
55
55
  nodes = lockfileToNodes(existingLock);
56
56
  }
57
57
  else {
58
- log("install: resolving %d top-level spec(s) → %s", opts.specs.length, opts.prefix);
59
- nodes = await resolveDeps(opts.specs, npmrc, log, opts.overrides);
58
+ log('install: resolving %d top-level spec(s) → %s', opts.specs.length, opts.prefix);
59
+ nodes = await resolveDeps(opts.specs, npmrc, log, opts.overrides, opts.skipDeps);
60
60
  if (opts.lockfile) {
61
61
  writeLockfile(lockfilePath, opts.specs, nodes);
62
- log("install: wrote %s (%d entries)", LOCKFILE_NAME, nodes.length);
62
+ log('install: wrote %s (%d entries)', LOCKFILE_NAME, nodes.length);
63
63
  }
64
64
  }
65
- log("install: downloading %d tarball(s)", nodes.length);
65
+ log('install: downloading %d tarball(s)', nodes.length);
66
66
  await downloadAndExtractAll(nodes, opts.prefix, npmrc, log);
67
67
  await linkBins(nodes, opts.prefix, log);
68
- log("install: done");
68
+ log('install: done');
69
69
  // Surface the top-level requested packages so callers can update
70
70
  // package.json with the resolved version (mirrors `npm install --save`
71
71
  // behavior). Sub-deps are not included.
@@ -94,14 +94,14 @@ function topLevelResolutions(specs, nodes) {
94
94
  return out;
95
95
  }
96
96
  function parseSpecName(spec) {
97
- if (spec.startsWith("@")) {
98
- const slash = spec.indexOf("/");
97
+ if (spec.startsWith('@')) {
98
+ const slash = spec.indexOf('/');
99
99
  if (slash === -1)
100
100
  return spec;
101
- const at = spec.indexOf("@", slash + 1);
101
+ const at = spec.indexOf('@', slash + 1);
102
102
  return at === -1 ? spec : spec.slice(0, at);
103
103
  }
104
- const at = spec.indexOf("@");
104
+ const at = spec.indexOf('@');
105
105
  return at === -1 ? spec : spec.slice(0, at);
106
106
  }
107
107
  /**
@@ -119,7 +119,7 @@ function parseSpecName(spec) {
119
119
  * the root. Each placement returns a `ResolvedNode` whose `installPath`
120
120
  * captures where it lives in the tree.
121
121
  */
122
- async function resolveDeps(specs, npmrc, log, overrides) {
122
+ async function resolveDeps(specs, npmrc, log, overrides, skipDeps) {
123
123
  const applyOverride = (name, range) => {
124
124
  if (!overrides)
125
125
  return range;
@@ -128,7 +128,7 @@ async function resolveDeps(specs, npmrc, log, overrides) {
128
128
  return range;
129
129
  if (override === range)
130
130
  return range;
131
- log("install: override %s %s → %s", name, range, override);
131
+ log('install: override %s %s → %s', name, range, override);
132
132
  return override;
133
133
  };
134
134
  const packumentCache = new Map();
@@ -139,7 +139,7 @@ async function resolveDeps(specs, npmrc, log, overrides) {
139
139
  const fresh = fetchPackument(name, {
140
140
  npmrc,
141
141
  onRetry: ({ attempt, error, delayMs }) => {
142
- log("packument %s: retry %d after %dms (%s)", name, attempt, delayMs, errMsg(error));
142
+ log('packument %s: retry %d after %dms (%s)', name, attempt, delayMs, errMsg(error));
143
143
  },
144
144
  });
145
145
  packumentCache.set(name, fresh);
@@ -200,19 +200,31 @@ async function resolveDeps(specs, npmrc, log, overrides) {
200
200
  if (installPath === `node_modules/${edge.name}`) {
201
201
  root.set(edge.name, node);
202
202
  }
203
- log("resolve: %s@%s ← %s (at %s)", edge.name, version, edge.range, installPath);
204
- for (const [depName, depRange] of Object.entries(node.dependencies)) {
205
- queue.push({ from: installPath, name: depName, range: applyOverride(depName, depRange), required: true });
206
- }
207
- for (const [depName, depRange] of Object.entries(node.optionalDependencies)) {
208
- queue.push({ from: installPath, name: depName, range: applyOverride(depName, depRange), required: false });
203
+ log('resolve: %s@%s ← %s (at %s)', edge.name, version, edge.range, installPath);
204
+ if (!skipDeps) {
205
+ for (const [depName, depRange] of Object.entries(node.dependencies)) {
206
+ queue.push({
207
+ from: installPath,
208
+ name: depName,
209
+ range: applyOverride(depName, depRange),
210
+ required: true,
211
+ });
212
+ }
213
+ for (const [depName, depRange] of Object.entries(node.optionalDependencies)) {
214
+ queue.push({
215
+ from: installPath,
216
+ name: depName,
217
+ range: applyOverride(depName, depRange),
218
+ required: false,
219
+ });
220
+ }
209
221
  }
210
222
  }
211
223
  catch (e) {
212
224
  // Optional deps that fail to resolve are skipped — yarn/npm
213
225
  // behavior. Required deps re-throw.
214
226
  if (!edge.required) {
215
- log("resolve: optional dep %s@%s skipped (%s)", edge.name, edge.range, e.message);
227
+ log('resolve: optional dep %s@%s skipped (%s)', edge.name, edge.range, e.message);
216
228
  continue;
217
229
  }
218
230
  throw e;
@@ -243,12 +255,12 @@ function findVisible(requesterPath, name, byPath) {
243
255
  // eslint-disable-next-line no-constant-condition
244
256
  while (true) {
245
257
  // Find the deepest `/node_modules/<pkg>` in `p`, strip it.
246
- const idx = p.lastIndexOf("/node_modules/");
258
+ const idx = p.lastIndexOf('/node_modules/');
247
259
  if (idx < 0)
248
260
  break;
249
261
  p = p.slice(0, idx);
250
262
  candidates.push(`${p}/node_modules/${name}`);
251
- if (p === "")
263
+ if (p === '')
252
264
  break;
253
265
  }
254
266
  }
@@ -299,10 +311,10 @@ function readLockfile(lockfilePath) {
299
311
  if (!fs.existsSync(lockfilePath))
300
312
  return null;
301
313
  try {
302
- const parsed = JSON.parse(fs.readFileSync(lockfilePath, "utf-8"));
314
+ const parsed = JSON.parse(fs.readFileSync(lockfilePath, 'utf-8'));
303
315
  if (parsed.lockfileVersion !== LOCKFILE_VERSION)
304
316
  return null;
305
- if (!parsed.packages || typeof parsed.packages !== "object")
317
+ if (!parsed.packages || typeof parsed.packages !== 'object')
306
318
  return null;
307
319
  return parsed;
308
320
  }
@@ -328,7 +340,7 @@ function writeLockfile(lockfilePath, specs, nodes) {
328
340
  requested: [...specs],
329
341
  packages,
330
342
  };
331
- fs.writeFileSync(lockfilePath, JSON.stringify(lockfile, null, 2) + "\n");
343
+ fs.writeFileSync(lockfilePath, JSON.stringify(lockfile, null, 2) + '\n');
332
344
  }
333
345
  function lockfileToNodes(lockfile) {
334
346
  return Object.entries(lockfile.packages).map(([installPath, entry]) => ({
@@ -347,8 +359,8 @@ function lockfileToNodes(lockfile) {
347
359
  function nameFromInstallPath(installPath) {
348
360
  // Last `node_modules/` boundary, then the rest is the package name
349
361
  // (single segment unscoped, or `@scope/pkg` scoped).
350
- const idx = installPath.lastIndexOf("/node_modules/");
351
- const after = idx < 0 ? installPath.replace(/^node_modules\//, "") : installPath.slice(idx + "/node_modules/".length);
362
+ const idx = installPath.lastIndexOf('/node_modules/');
363
+ const after = idx < 0 ? installPath.replace(/^node_modules\//, '') : installPath.slice(idx + '/node_modules/'.length);
352
364
  return after;
353
365
  }
354
366
  function lockfileMatchesRequest(lockfile, specs) {
@@ -379,10 +391,10 @@ function describeLockfileDrift(lockfile, specs) {
379
391
  return null;
380
392
  const lines = [];
381
393
  if (added.length > 0)
382
- lines.push(` + ${added.sort().join("\n + ")}`);
394
+ lines.push(` + ${added.sort().join('\n + ')}`);
383
395
  if (removed.length > 0)
384
- lines.push(` - ${removed.sort().join("\n - ")}`);
385
- return lines.join("\n");
396
+ lines.push(` - ${removed.sort().join('\n - ')}`);
397
+ return lines.join('\n');
386
398
  }
387
399
  // Exported for unit-testing — keep the function name + signature
388
400
  // stable, the install-backend itself still calls it via the local
@@ -398,25 +410,25 @@ export function parseSpec(raw) {
398
410
  // shipped only prereleases (4.0.0-rc.17 is the `latest` tag, no
399
411
  // stable 4.x yet) and `*` was selecting the abandoned 3.3.0
400
412
  // instead.
401
- if (raw.startsWith("@")) {
402
- const slash = raw.indexOf("/");
413
+ if (raw.startsWith('@')) {
414
+ const slash = raw.indexOf('/');
403
415
  if (slash < 0)
404
416
  throw new Error(`Invalid spec (scoped name without slash): ${raw}`);
405
- const at = raw.indexOf("@", slash);
417
+ const at = raw.indexOf('@', slash);
406
418
  if (at < 0)
407
- return { name: raw, range: "latest" };
408
- return { name: raw.slice(0, at), range: raw.slice(at + 1) || "latest" };
419
+ return { name: raw, range: 'latest' };
420
+ return { name: raw.slice(0, at), range: raw.slice(at + 1) || 'latest' };
409
421
  }
410
- const at = raw.indexOf("@");
422
+ const at = raw.indexOf('@');
411
423
  if (at < 0)
412
- return { name: raw, range: "latest" };
413
- return { name: raw.slice(0, at), range: raw.slice(at + 1) || "latest" };
424
+ return { name: raw, range: 'latest' };
425
+ return { name: raw.slice(0, at), range: raw.slice(at + 1) || 'latest' };
414
426
  }
415
427
  // Exported for unit-testing. Internal API.
416
428
  export function pickVersion(packument, range) {
417
429
  // dist-tag fast path: `latest`, `next`, ...
418
- if (packument["dist-tags"][range])
419
- return packument["dist-tags"][range];
430
+ if (packument['dist-tags'][range])
431
+ return packument['dist-tags'][range];
420
432
  // Validate range early so a typo fails loudly.
421
433
  let parsedRange;
422
434
  try {
@@ -440,8 +452,7 @@ async function downloadAndExtractAll(nodes, prefix, npmrc, log) {
440
452
  // Sort by install-path depth ascending so parents extract before
441
453
  // children. Extracting a parent on top of an existing child would
442
454
  // wipe out the child.
443
- const queue = [...nodes].sort((a, b) => depth(a.installPath) - depth(b.installPath) ||
444
- (a.installPath < b.installPath ? -1 : 1));
455
+ const queue = [...nodes].sort((a, b) => depth(a.installPath) - depth(b.installPath) || (a.installPath < b.installPath ? -1 : 1));
445
456
  const workers = [];
446
457
  const concurrency = Math.max(1, Math.min(DEFAULT_CONCURRENCY, queue.length));
447
458
  // Parents (depth 1) are extracted serially first to avoid concurrent
@@ -475,12 +486,12 @@ async function downloadAndExtractAll(nodes, prefix, npmrc, log) {
475
486
  }
476
487
  async function extractOne(node, prefix, npmrc, log) {
477
488
  const dest = path.join(prefix, node.installPath);
478
- log("fetch: %s@%s ← %s (→ %s)", node.name, node.version, node.tarballUrl, node.installPath);
489
+ log('fetch: %s@%s ← %s (→ %s)', node.name, node.version, node.tarballUrl, node.installPath);
479
490
  const bytes = await fetchTarball(node.tarballUrl, {
480
491
  npmrc,
481
492
  integrity: node.integrity,
482
493
  onRetry: ({ attempt, error, delayMs }) => {
483
- log("tarball %s@%s: retry %d after %dms (%s)", node.name, node.version, attempt, delayMs, errMsg(error));
494
+ log('tarball %s@%s: retry %d after %dms (%s)', node.name, node.version, attempt, delayMs, errMsg(error));
484
495
  },
485
496
  });
486
497
  fs.rmSync(dest, { recursive: true, force: true });
@@ -490,7 +501,7 @@ async function extractOne(node, prefix, npmrc, log) {
490
501
  function depth(installPath) {
491
502
  // Count `node_modules/` segments to know nesting depth.
492
503
  // `node_modules/foo` = 1, `node_modules/foo/node_modules/bar` = 2, etc.
493
- return installPath.split("/node_modules/").length;
504
+ return installPath.split('/node_modules/').length;
494
505
  }
495
506
  async function linkBins(nodes, prefix, log) {
496
507
  // Only root-level packages publish bins into the top-level
@@ -498,7 +509,7 @@ async function linkBins(nodes, prefix, log) {
498
509
  // direct dependents through the nested .bin (npm matches this) — we
499
510
  // omit nested-bin linking for now since no consumer of the install
500
511
  // backend depends on it (gjsify's own use cases all hit root bins).
501
- const binDir = path.join(prefix, "node_modules", ".bin");
512
+ const binDir = path.join(prefix, 'node_modules', '.bin');
502
513
  let created = 0;
503
514
  for (const node of nodes) {
504
515
  if (!node.bin)
@@ -534,15 +545,13 @@ async function linkBins(nodes, prefix, log) {
534
545
  }
535
546
  }
536
547
  if (created > 0)
537
- log("bin: linked %d entry(ies) under .bin/", created);
548
+ log('bin: linked %d entry(ies) under .bin/', created);
538
549
  }
539
550
  function normalizeBin(pkgName, bin) {
540
551
  const out = new Map();
541
- if (typeof bin === "string") {
552
+ if (typeof bin === 'string') {
542
553
  // String form is shorthand for `{ <last-segment-of-pkgName>: <bin> }`.
543
- const baseName = pkgName.startsWith("@")
544
- ? pkgName.slice(pkgName.indexOf("/") + 1)
545
- : pkgName;
554
+ const baseName = pkgName.startsWith('@') ? pkgName.slice(pkgName.indexOf('/') + 1) : pkgName;
546
555
  out.set(baseName, bin);
547
556
  return out;
548
557
  }
@@ -563,11 +572,11 @@ async function loadNpmrc(opts) {
563
572
  // workspace-root one too; the gjsify project-local case is what users
564
573
  // hit most often (mock-registry tests, scoped-registry overrides), so
565
574
  // we cover that explicitly.
566
- for (const candidate of [path.join(home, ".npmrc"), path.join(opts.prefix, ".npmrc")]) {
575
+ for (const candidate of [path.join(home, '.npmrc'), path.join(opts.prefix, '.npmrc')]) {
567
576
  if (!fs.existsSync(candidate))
568
577
  continue;
569
578
  try {
570
- const projectParsed = parseNpmrc(fs.readFileSync(candidate, "utf-8"));
579
+ const projectParsed = parseNpmrc(fs.readFileSync(candidate, 'utf-8'));
571
580
  parsed = { ...parsed, ...projectParsed, scopes: { ...parsed.scopes, ...projectParsed.scopes } };
572
581
  }
573
582
  catch (e) {
@@ -27,6 +27,19 @@ export interface InstallOptions {
27
27
  * forcing `typescript@~5.9` across every `typescript@*` devDep.
28
28
  */
29
29
  overrides?: Record<string, string>;
30
+ /**
31
+ * Native backend only: skip transitive dependency resolution and only
32
+ * install the top-level requested packages. Use this when the packages
33
+ * are self-contained bundles whose declared `dependencies` are either
34
+ * bundled into the artifact (e.g. `@gjsify/cli`'s GJS bundle) or
35
+ * workspace-only packages not published to npm separately. Setting this
36
+ * avoids spurious packument fetches for workspace-internal packages.
37
+ *
38
+ * Has no effect when `frozen: true` (the lockfile already contains the
39
+ * full resolved tree and is used verbatim) or when `GJSIFY_INSTALL_BACKEND=npm`
40
+ * (npm does its own resolution and does not consult this flag).
41
+ */
42
+ skipDeps?: boolean;
30
43
  }
31
44
  export interface InstallResult {
32
45
  /** Top-level packages that were requested, with the version each
@@ -40,7 +40,8 @@ async function installViaNpm({ prefix, specs, verbose, registry }) {
40
40
  '--no-package-lock',
41
41
  '--no-audit',
42
42
  '--no-fund',
43
- '--prefix', prefix,
43
+ '--prefix',
44
+ prefix,
44
45
  ...(verbose ? ['--loglevel', 'verbose'] : ['--loglevel', 'warn']),
45
46
  ...specs,
46
47
  ];
@@ -161,9 +161,7 @@ function pickBinMap(pkgName, pkgJson) {
161
161
  function normalizeBin(pkgName, bin) {
162
162
  const out = new Map();
163
163
  if (typeof bin === 'string') {
164
- const baseName = pkgName.startsWith('@')
165
- ? pkgName.slice(pkgName.indexOf('/') + 1)
166
- : pkgName;
164
+ const baseName = pkgName.startsWith('@') ? pkgName.slice(pkgName.indexOf('/') + 1) : pkgName;
167
165
  out.set(baseName, bin);
168
166
  return out;
169
167
  }