@gjsify/cli 0.56.0 → 0.58.0
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/dist/cli.gjs.mjs +133 -133
- package/lib/commands/flatpak/sources.d.ts +13 -0
- package/lib/commands/flatpak/sources.js +33 -1
- package/lib/commands/install.d.ts +1 -0
- package/lib/commands/install.js +17 -4
- package/lib/utils/install-backend-native.d.ts +27 -0
- package/lib/utils/install-backend-native.js +65 -11
- package/lib/utils/install-focus.d.ts +41 -0
- package/lib/utils/install-focus.js +80 -0
- package/package.json +20 -20
|
@@ -6,6 +6,19 @@ interface FlatpakSourcesOptions {
|
|
|
6
6
|
out?: string;
|
|
7
7
|
cacheRoot?: string;
|
|
8
8
|
printModule?: boolean;
|
|
9
|
+
focus?: string[];
|
|
9
10
|
}
|
|
11
|
+
/** One vendored tarball: its download URL + its SRI integrity (`sha512-…`). */
|
|
12
|
+
interface Tarball {
|
|
13
|
+
url: string;
|
|
14
|
+
integrity: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* `--focus` (ADR 0102): the tarballs the focused workspace closure installs. The same
|
|
18
|
+
* `planFocus` + `reachableInstallPaths` pair `gjsify install --focus` uses, so the
|
|
19
|
+
* Flatpak cache holds exactly what the offline install reads. Workspace-named
|
|
20
|
+
* packages are dropped as the installer drops them (they are symlinks, not fetches).
|
|
21
|
+
*/
|
|
22
|
+
export declare function focusedTarballs(lockfile: string, focus: string[]): Tarball[];
|
|
10
23
|
export declare const flatpakSourcesCommand: Command<unknown, FlatpakSourcesOptions>;
|
|
11
24
|
export {};
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
20
20
|
import { dirname, join, resolve } from 'node:path';
|
|
21
21
|
import { Buffer } from 'node:buffer';
|
|
22
|
+
import { discoverWorkspaces } from '@gjsify/workspace';
|
|
23
|
+
import { planFocus } from '../../utils/install-focus.js';
|
|
24
|
+
import { parseSpecName, readLockfileNodes, reachableInstallPaths } from '../../utils/install-backend-native.js';
|
|
22
25
|
import { FLATPAK_SOURCES_FILE } from './utils.js';
|
|
23
26
|
/** Checksum algorithms flatpak-builder accepts on a `file` source. */
|
|
24
27
|
const FLATPAK_ALGOS = new Set(['sha256', 'sha512']);
|
|
@@ -187,6 +190,25 @@ function defaultLockfile(cwd) {
|
|
|
187
190
|
}
|
|
188
191
|
return 'gjsify-lock.json'; // surfaces a clear "not found" later
|
|
189
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* `--focus` (ADR 0102): the tarballs the focused workspace closure installs. The same
|
|
195
|
+
* `planFocus` + `reachableInstallPaths` pair `gjsify install --focus` uses, so the
|
|
196
|
+
* Flatpak cache holds exactly what the offline install reads. Workspace-named
|
|
197
|
+
* packages are dropped as the installer drops them (they are symlinks, not fetches).
|
|
198
|
+
*/
|
|
199
|
+
export function focusedTarballs(lockfile, focus) {
|
|
200
|
+
const root = dirname(lockfile);
|
|
201
|
+
const workspaces = discoverWorkspaces(root, { includeRoot: true });
|
|
202
|
+
const plan = planFocus(workspaces, focus, 'gjsify flatpak sources');
|
|
203
|
+
const nodes = readLockfileNodes(lockfile);
|
|
204
|
+
if (!nodes)
|
|
205
|
+
throw new Error(`gjsify flatpak sources: ${lockfile} is not a readable gjsify-lock.json.`);
|
|
206
|
+
const reachable = reachableInstallPaths(nodes, plan.specs.map(parseSpecName));
|
|
207
|
+
const workspaceNames = new Set(workspaces.map((w) => w.name));
|
|
208
|
+
return nodes
|
|
209
|
+
.filter((n) => reachable.has(n.installPath) && !workspaceNames.has(n.name))
|
|
210
|
+
.flatMap((n) => (n.tarballUrl && n.integrity ? [{ url: n.tarballUrl, integrity: n.integrity }] : []));
|
|
211
|
+
}
|
|
190
212
|
function parseLockfile(path, type) {
|
|
191
213
|
const text = readFileSync(path, 'utf-8');
|
|
192
214
|
if (type === 'yarn')
|
|
@@ -228,6 +250,11 @@ export const flatpakSourcesCommand = {
|
|
|
228
250
|
'Tarballs land at <cache-root>/gjsify/tarballs/v1/<algo>/<shard>/<hex>.tgz.',
|
|
229
251
|
type: 'string',
|
|
230
252
|
default: 'flatpak-gjsify-cache',
|
|
253
|
+
})
|
|
254
|
+
.option('focus', {
|
|
255
|
+
description: 'Only the tarballs the named workspaces (package names), the workspaces they depend on, and their external dependencies need, as `gjsify install --focus` installs them. gjsify-lock.json only.',
|
|
256
|
+
type: 'string',
|
|
257
|
+
array: true,
|
|
231
258
|
})
|
|
232
259
|
.option('print-module', {
|
|
233
260
|
description: 'Also print a ready-to-paste flatpak manifest module snippet to stderr',
|
|
@@ -243,7 +270,12 @@ export const flatpakSourcesCommand = {
|
|
|
243
270
|
'Run `gjsify install` (or npm/yarn/pnpm) first, or pass --lockfile.');
|
|
244
271
|
}
|
|
245
272
|
const type = args.type ?? detectType(lockfile);
|
|
246
|
-
const
|
|
273
|
+
const focus = args.focus;
|
|
274
|
+
if (focus && focus.length > 0 && type !== 'gjsify') {
|
|
275
|
+
throw new Error(`gjsify flatpak sources: --focus reads the per-package edges of gjsify-lock.json; ` +
|
|
276
|
+
`${type} lockfiles are not supported with it.`);
|
|
277
|
+
}
|
|
278
|
+
const tarballs = focus && focus.length > 0 ? focusedTarballs(lockfile, focus) : parseLockfile(lockfile, type);
|
|
247
279
|
const cacheRoot = args.cacheRoot ?? 'flatpak-gjsify-cache';
|
|
248
280
|
// Dedupe by content hash — the same tarball is locked at many install
|
|
249
281
|
// paths; flatpak rejects duplicate dest/filename pairs.
|
package/lib/commands/install.js
CHANGED
|
@@ -31,6 +31,7 @@ import { acquireInstallLock } from '../utils/install-lock.js';
|
|
|
31
31
|
import { nodeBinary } from '../utils/run-node.js';
|
|
32
32
|
import { spawnToCompletion } from '../utils/spawn.js';
|
|
33
33
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
34
|
+
import { planFocus } from '../utils/install-focus.js';
|
|
34
35
|
import { binDirOnPath, BUNDLER_ENGINE_PACKAGE, defaultGlobalLayout, hasBundlerEngineInstalled, installGjsEnginePackages, linkGlobalBins, specToPackageName, } from '../utils/install-global.js';
|
|
35
36
|
import { resolveHostPlatform } from '../utils/platform-check.js';
|
|
36
37
|
import { pruneAfterInstall } from '../utils/prune-prefix.js';
|
|
@@ -86,6 +87,11 @@ export const installCommand = {
|
|
|
86
87
|
description: 'Re-resolve every dependency to the newest version satisfying its range and rewrite the lockfile (bumps in-range transitive deps). Without it, a resolve preserves versions already pinned in the lockfile and only resolves new/changed deps — the npm/yarn/pnpm default. Mirrors yarn install --mode=update-lockfile.',
|
|
87
88
|
type: 'boolean',
|
|
88
89
|
default: false,
|
|
90
|
+
})
|
|
91
|
+
.option('focus', {
|
|
92
|
+
description: 'Install only the named workspaces (package names), the workspaces they depend on, and their external dependencies, as if they were the only members of the monorepo (yarn workspaces focus). Reads gjsify-lock.json and never rewrites it; fails if it is missing or stale. Workspace projects only.',
|
|
93
|
+
type: 'string',
|
|
94
|
+
array: true,
|
|
89
95
|
})
|
|
90
96
|
.option('verbose', {
|
|
91
97
|
description: 'Verbose install logging.',
|
|
@@ -665,6 +671,10 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
665
671
|
throw new Error(`gjsify install: ${cwd} has a "workspaces" field but no workspaces were discovered`);
|
|
666
672
|
}
|
|
667
673
|
const byName = new Map(workspaces.map((w) => [w.name, w]));
|
|
674
|
+
// `--focus`: the members that are installed. The lockfile and its drift check
|
|
675
|
+
// still cover EVERY member (ADR 0102 § 2), so `externalSpecs` below stays the full set.
|
|
676
|
+
const focusPlan = args.focus && args.focus.length > 0 ? planFocus(workspaces, args.focus, 'gjsify install') : undefined;
|
|
677
|
+
const active = focusPlan ? new Set(focusPlan.workspaces) : undefined;
|
|
668
678
|
const externalSpecs = new Set();
|
|
669
679
|
// `"<name>@<range>"` → the members that declared it. Feeds the resolver's
|
|
670
680
|
// version-conflict warning so it can attribute both sides of a conflict.
|
|
@@ -691,6 +701,8 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
691
701
|
// Only an EXPLICIT out-of-workspace protocol opts out.
|
|
692
702
|
const explicitOverride = /^(link|file|portal|git\+|https?):/.test(spec);
|
|
693
703
|
if (!explicitOverride) {
|
|
704
|
+
if (active && !active.has(ws))
|
|
705
|
+
continue;
|
|
694
706
|
symlinks.push({
|
|
695
707
|
fromWorkspaceName: ws.name,
|
|
696
708
|
depName,
|
|
@@ -729,7 +741,7 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
729
741
|
// the workspace without its runner shim (`node_modules/.bin/gjsify`), and a
|
|
730
742
|
// stale shim is refreshed even when a later phase aborts. Rewritten at the end
|
|
731
743
|
// of this function so the GJS preamble reflects the installed tree.
|
|
732
|
-
writeWorkspaceBinShims(cwd, workspaces);
|
|
744
|
+
writeWorkspaceBinShims(cwd, focusPlan?.workspaces ?? workspaces);
|
|
733
745
|
// Per-requester links (`<requester>/node_modules/<dep>`) plus a root hoist of
|
|
734
746
|
// every workspace into `node_modules/<name>`, so transitive workspace deps
|
|
735
747
|
// resolve by Node's parent-walk from any descendant. Idempotent, hence callable
|
|
@@ -793,7 +805,7 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
793
805
|
// runtime with `Module not found`.
|
|
794
806
|
const rootBinDir = join(cwd, 'node_modules');
|
|
795
807
|
let rootHoisted = 0;
|
|
796
|
-
for (const ws of workspaces) {
|
|
808
|
+
for (const ws of focusPlan?.workspaces ?? workspaces) {
|
|
797
809
|
// The root workspace cannot symlink itself into its own node_modules.
|
|
798
810
|
if (ws.location === cwd)
|
|
799
811
|
continue;
|
|
@@ -843,7 +855,7 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
843
855
|
const wsLocalSpecs = new Map(); // wsLocation → name@range set
|
|
844
856
|
const droppedFromExternal = new Set();
|
|
845
857
|
if (extracted && extracted.scoped.size > 0) {
|
|
846
|
-
for (const ws of workspaces) {
|
|
858
|
+
for (const ws of focusPlan?.workspaces ?? workspaces) {
|
|
847
859
|
const wsManifest = ws.manifest;
|
|
848
860
|
for (const kind of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
849
861
|
const deps = wsManifest[kind];
|
|
@@ -914,6 +926,7 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
914
926
|
// Aggregated over EVERY member, because the specs are: a name any member
|
|
915
927
|
// declares as a plain dependency stays required.
|
|
916
928
|
optionalSpecs: optionalDependencyNames(workspaces.map((w) => w.manifest)),
|
|
929
|
+
focusSpecs: focusPlan?.specs,
|
|
917
930
|
};
|
|
918
931
|
await installPackages(rootOpts);
|
|
919
932
|
}
|
|
@@ -955,7 +968,7 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
955
968
|
// fresh checkout `detectNativePackages` finds nothing before extraction).
|
|
956
969
|
// Running last also keeps workspace shims authoritative over any same-named
|
|
957
970
|
// external bin the backend linked — yarn semantics: workspace bins win.
|
|
958
|
-
const wsBinsCreated = writeWorkspaceBinShims(cwd, workspaces);
|
|
971
|
+
const wsBinsCreated = writeWorkspaceBinShims(cwd, focusPlan?.workspaces ?? workspaces);
|
|
959
972
|
if (wsBinsCreated > 0) {
|
|
960
973
|
console.log(`gjsify install: linked ${wsBinsCreated} workspace bin(s) into node_modules/.bin/`);
|
|
961
974
|
}
|
|
@@ -108,6 +108,17 @@ export interface NativeInstallOptions extends InstallOptions {
|
|
|
108
108
|
* been written from the full resolved tree.
|
|
109
109
|
*/
|
|
110
110
|
linkedNames?: Set<string>;
|
|
111
|
+
/**
|
|
112
|
+
* `--focus` (ADR 0102): the external `"<name>@<range>"` specs of the focused
|
|
113
|
+
* workspace closure. Only the packages reachable from these are fetched and
|
|
114
|
+
* extracted. `specs` stays the FULL set, because the drift check compares it
|
|
115
|
+
* against the lockfile's `requested` and the lockfile stays complete.
|
|
116
|
+
*
|
|
117
|
+
* Implies a lockfile-authoritative install: a missing or stale lockfile throws
|
|
118
|
+
* instead of resolving (a resolve would reach the toolchains the focus excludes),
|
|
119
|
+
* and nothing is ever written back, so the file cannot become a subset.
|
|
120
|
+
*/
|
|
121
|
+
focusSpecs?: string[];
|
|
111
122
|
}
|
|
112
123
|
export declare function installPackagesNative(opts: NativeInstallOptions): Promise<InstalledTopLevel[]>;
|
|
113
124
|
/**
|
|
@@ -144,6 +155,7 @@ export declare function applyPlatformFilter(nodes: ResolvedNode[], target: Platf
|
|
|
144
155
|
* Pure detection — no script is run, so node-free-ness is preserved.
|
|
145
156
|
*/
|
|
146
157
|
export declare function warnMissingNativeBuilds(nodes: ResolvedNode[], prefix: string, log: Logger): void;
|
|
158
|
+
export declare function parseSpecName(spec: string): string;
|
|
147
159
|
/**
|
|
148
160
|
* A node's REQUIRED dependency edges: its `dependencies` minus every name its
|
|
149
161
|
* `optionalDependencies` also lists. The ONE place that decides an edge's kind —
|
|
@@ -209,6 +221,21 @@ export declare function requiredPeerEntries(node: Pick<ResolvedNode, 'dependenci
|
|
|
209
221
|
* composition, worth pinning without a registry.
|
|
210
222
|
*/
|
|
211
223
|
export declare function computeOptionalFlags(nodes: ResolvedNode[], requiredNames: Set<string>, log: Logger): void;
|
|
224
|
+
/**
|
|
225
|
+
* The `installPath`s reachable from the top-level packages named in `seedNames`, through
|
|
226
|
+
* `dependencies`, `optionalDependencies` and REQUIRED peers — what `--focus` (ADR 0102)
|
|
227
|
+
* keeps. Optional edges are followed on purpose: whether one installs is a per-host
|
|
228
|
+
* verdict (`applyPlatformFilter`), and a closure that left them out would depend on the
|
|
229
|
+
* host that computed it. An optional peer is not an edge, as the resolver never places it.
|
|
230
|
+
*
|
|
231
|
+
* Same walk as {@link computeOptionalFlags} — seeds are exact because a top-level spec
|
|
232
|
+
* always takes the root slot, and an edge resolves through {@link findVisible} the way
|
|
233
|
+
* the requester will at runtime. A seed with no placement is a workspace member (its
|
|
234
|
+
* symlink is wired elsewhere) or a name absent from the lockfile; both are skipped.
|
|
235
|
+
*/
|
|
236
|
+
export declare function reachableInstallPaths(nodes: readonly ResolvedNode[], seedNames: Iterable<string>): Set<string>;
|
|
237
|
+
/** The nodes the lockfile at `lockfilePath` describes, or null when it is missing or unreadable. */
|
|
238
|
+
export declare function readLockfileNodes(lockfilePath: string): ResolvedNode[] | null;
|
|
212
239
|
export declare function parseSpec(raw: string): ParsedSpec;
|
|
213
240
|
export declare function pickVersion(packument: Packument, range: string, preferred?: string): string | null;
|
|
214
241
|
/**
|
|
@@ -157,20 +157,23 @@ async function installPackagesNativeLocked(opts, npmrc, log) {
|
|
|
157
157
|
* resolve nothing.
|
|
158
158
|
*/
|
|
159
159
|
let skippedEdges = new Set();
|
|
160
|
-
if (opts.frozen) {
|
|
160
|
+
if (opts.frozen || opts.focusSpecs) {
|
|
161
161
|
// --immutable / --frozen: the lockfile is authoritative. Reject a missing,
|
|
162
162
|
// version-mismatched or drifted file — silently honouring a stale lockfile
|
|
163
|
-
// masks the real dep churn `--immutable` exists to catch.
|
|
163
|
+
// masks the real dep churn `--immutable` exists to catch. `--focus` is the
|
|
164
|
+
// same promise without the tree check: it reads the lockfile, never resolves.
|
|
165
|
+
const flag = opts.frozen ? '--immutable' : '--focus';
|
|
166
|
+
const retry = opts.frozen ? '(without --immutable)' : '(without --focus)';
|
|
164
167
|
if (!existingLock) {
|
|
165
|
-
throw new Error(`install:
|
|
166
|
-
`Run \`gjsify install\`
|
|
168
|
+
throw new Error(`install: ${flag} requires ${LOCKFILE_NAME} at ${opts.prefix} — none found. ` +
|
|
169
|
+
`Run \`gjsify install\` ${retry} to generate one and commit it.`);
|
|
167
170
|
}
|
|
168
171
|
const drift = describeLockfileDrift(existingLock, opts.specs);
|
|
169
172
|
if (drift) {
|
|
170
|
-
throw new Error(`install:
|
|
171
|
-
`Re-run \`gjsify install\`
|
|
173
|
+
throw new Error(`install: ${flag} but ${lockfilePath} is stale.\n${drift}\n` +
|
|
174
|
+
`Re-run \`gjsify install\` ${retry} to refresh the lockfile.`);
|
|
172
175
|
}
|
|
173
|
-
log('install:
|
|
176
|
+
log('install: %s, using lockfile (%d package(s))', flag, Object.keys(existingLock.packages).length);
|
|
174
177
|
nodes = lockfileToNodes(existingLock);
|
|
175
178
|
// ...and the tree must hold NOTHING ELSE. The installer only adds, so without
|
|
176
179
|
// this an undescribed package survives the install and exits 0 — the CI cache
|
|
@@ -178,8 +181,17 @@ async function installPackagesNativeLocked(opts, npmrc, log) {
|
|
|
178
181
|
// Before the download so a wrong tree costs seconds, not the extract phase,
|
|
179
182
|
// and with the FULL lockfile set: the workspace filter below removes nodes
|
|
180
183
|
// that ARE described, and judging against the smaller set would call them
|
|
181
|
-
// strangers.
|
|
182
|
-
|
|
184
|
+
// strangers. `--focus` alone skips it: a tree an earlier full install left is
|
|
185
|
+
// not extraneous, and nothing here promises the tree matches the lockfile.
|
|
186
|
+
if (opts.frozen)
|
|
187
|
+
assertNoExtraneous(opts.prefix, nodes);
|
|
188
|
+
// The focus narrows AFTER the extraneous check, which judges the full set, and
|
|
189
|
+
// before the optional-flag fixpoint, which must see only what will be installed.
|
|
190
|
+
if (opts.focusSpecs) {
|
|
191
|
+
const reachable = reachableInstallPaths(nodes, opts.focusSpecs.map(parseSpecName));
|
|
192
|
+
log('install: --focus keeps %d of %d lockfile package(s)', reachable.size, nodes.length);
|
|
193
|
+
nodes = nodes.filter((n) => reachable.has(n.installPath));
|
|
194
|
+
}
|
|
183
195
|
}
|
|
184
196
|
else if (!opts.refreshLockfile &&
|
|
185
197
|
existingLock &&
|
|
@@ -228,7 +240,7 @@ async function installPackagesNativeLocked(opts, npmrc, log) {
|
|
|
228
240
|
// before `writeLockfile` (so the persisted flag is final), before the platform
|
|
229
241
|
// verdict (which reads it for fatal vs. inert), and before the workspace filter
|
|
230
242
|
// below (which removes nodes and would truncate the walk).
|
|
231
|
-
computeOptionalFlags(nodes, requiredTopLevelNames(opts.specs, opts.optionalSpecs), log);
|
|
243
|
+
computeOptionalFlags(nodes, requiredTopLevelNames(opts.focusSpecs ?? opts.specs, opts.optionalSpecs), log);
|
|
232
244
|
assertRequiredEdgesResolved(nodes, skippedEdges);
|
|
233
245
|
if (resolved && opts.lockfile) {
|
|
234
246
|
// The FULL resolved set, inert nodes included — see the header note:
|
|
@@ -440,7 +452,7 @@ function topLevelResolutions(specs, nodes) {
|
|
|
440
452
|
}
|
|
441
453
|
return out;
|
|
442
454
|
}
|
|
443
|
-
function parseSpecName(spec) {
|
|
455
|
+
export function parseSpecName(spec) {
|
|
444
456
|
if (spec.startsWith('@')) {
|
|
445
457
|
const slash = spec.indexOf('/');
|
|
446
458
|
if (slash === -1)
|
|
@@ -957,6 +969,48 @@ export function computeOptionalFlags(nodes, requiredNames, log) {
|
|
|
957
969
|
}
|
|
958
970
|
log('install: optionality fixpoint — %d of %d node(s) reachable only via optional edges%s', nodes.length - required.size, nodes.length, corrected > 0 ? `; corrected ${corrected} incoming flag(s)` : '');
|
|
959
971
|
}
|
|
972
|
+
/**
|
|
973
|
+
* The `installPath`s reachable from the top-level packages named in `seedNames`, through
|
|
974
|
+
* `dependencies`, `optionalDependencies` and REQUIRED peers — what `--focus` (ADR 0102)
|
|
975
|
+
* keeps. Optional edges are followed on purpose: whether one installs is a per-host
|
|
976
|
+
* verdict (`applyPlatformFilter`), and a closure that left them out would depend on the
|
|
977
|
+
* host that computed it. An optional peer is not an edge, as the resolver never places it.
|
|
978
|
+
*
|
|
979
|
+
* Same walk as {@link computeOptionalFlags} — seeds are exact because a top-level spec
|
|
980
|
+
* always takes the root slot, and an edge resolves through {@link findVisible} the way
|
|
981
|
+
* the requester will at runtime. A seed with no placement is a workspace member (its
|
|
982
|
+
* symlink is wired elsewhere) or a name absent from the lockfile; both are skipped.
|
|
983
|
+
*/
|
|
984
|
+
export function reachableInstallPaths(nodes, seedNames) {
|
|
985
|
+
const byPath = new Map();
|
|
986
|
+
for (const node of nodes)
|
|
987
|
+
byPath.set(node.installPath, node);
|
|
988
|
+
const reached = new Set();
|
|
989
|
+
const worklist = [];
|
|
990
|
+
const enter = (node) => {
|
|
991
|
+
if (!node || reached.has(node.installPath))
|
|
992
|
+
return;
|
|
993
|
+
reached.add(node.installPath);
|
|
994
|
+
worklist.push(node);
|
|
995
|
+
};
|
|
996
|
+
for (const name of seedNames)
|
|
997
|
+
enter(byPath.get(`node_modules/${name}`));
|
|
998
|
+
for (let node = worklist.pop(); node; node = worklist.pop()) {
|
|
999
|
+
const edges = [
|
|
1000
|
+
...Object.keys(node.dependencies),
|
|
1001
|
+
...Object.keys(node.optionalDependencies),
|
|
1002
|
+
...requiredPeerEntries(node).map(([name]) => name),
|
|
1003
|
+
];
|
|
1004
|
+
for (const name of edges)
|
|
1005
|
+
enter(findVisible(node.installPath, name, byPath));
|
|
1006
|
+
}
|
|
1007
|
+
return reached;
|
|
1008
|
+
}
|
|
1009
|
+
/** The nodes the lockfile at `lockfilePath` describes, or null when it is missing or unreadable. */
|
|
1010
|
+
export function readLockfileNodes(lockfilePath) {
|
|
1011
|
+
const lock = readLockfile(lockfilePath);
|
|
1012
|
+
return lock ? lockfileToNodes(lock) : null;
|
|
1013
|
+
}
|
|
960
1014
|
/**
|
|
961
1015
|
* Re-judge every failure the resolve swallowed against the FIXPOINT flags, and throw
|
|
962
1016
|
* if one turned out to be required after all.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Workspace } from '@gjsify/workspace';
|
|
2
|
+
/** The three blocks `gjsify install` reads from a member; same list as install.ts. */
|
|
3
|
+
export declare const DEPENDENCY_BLOCKS: readonly ["dependencies", "devDependencies", "optionalDependencies"];
|
|
4
|
+
export type DependencyClass = {
|
|
5
|
+
kind: 'workspace';
|
|
6
|
+
target: Workspace;
|
|
7
|
+
}
|
|
8
|
+
/** `workspace:` against a name that is not a member: a typo or a missing package. */
|
|
9
|
+
| {
|
|
10
|
+
kind: 'missing-workspace';
|
|
11
|
+
}
|
|
12
|
+
/** `link:`/`file:`/`portal:`/`git+`/`http(s):` — neither linked nor fetched. */
|
|
13
|
+
| {
|
|
14
|
+
kind: 'ignored';
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'external';
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* What a `name: spec` entry of a member's manifest is. A name that matches a member is
|
|
20
|
+
* the member whatever the spec form (`workspace:`, `^1.2.3`, a dist-tag) — only an
|
|
21
|
+
* explicit out-of-workspace protocol opts out — because routing it to the registry would
|
|
22
|
+
* unpack a published tarball over the member's own source tree.
|
|
23
|
+
*/
|
|
24
|
+
export declare function classifyDependency(depName: string, spec: string, byName: ReadonlyMap<string, Workspace>): DependencyClass;
|
|
25
|
+
export interface FocusPlan {
|
|
26
|
+
/** The closure, in discovery order: the named workspaces and everything they depend on. */
|
|
27
|
+
workspaces: Workspace[];
|
|
28
|
+
/** `"<name>@<range>"` of every external dependency the closure's manifests declare. */
|
|
29
|
+
specs: string[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Resolve `--focus` names to the workspace closure and its external specs.
|
|
33
|
+
*
|
|
34
|
+
* Throws naming every unknown workspace — a typo must not become an install of
|
|
35
|
+
* nothing. Names are exact package names, the spelling the rest of the CLI uses; the
|
|
36
|
+
* root is a member like any other (Yarn's `workspaces focus` rule) and is in the
|
|
37
|
+
* closure only when named or depended on. A root without a `name` cannot be named.
|
|
38
|
+
*
|
|
39
|
+
* @param command label for error messages, e.g. `gjsify install`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function planFocus(workspaces: readonly Workspace[], focus: readonly string[], command: string): FocusPlan;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// `--focus <workspace…>` for `gjsify install` and `gjsify flatpak sources`
|
|
2
|
+
// (ADR 0102): act on the named workspaces, the workspaces they depend on, and the
|
|
3
|
+
// external packages those need — as if they were the only members of the monorepo.
|
|
4
|
+
//
|
|
5
|
+
// THE MANIFEST HALF lives here; the LOCKFILE HALF (`reachableInstallPaths`) lives in
|
|
6
|
+
// `install-backend-native.ts` beside `computeOptionalFlags`, because it reads the same
|
|
7
|
+
// graph through the same private helpers. `gjsify-lock.json` records package → package
|
|
8
|
+
// edges but no workspace → package edges, so the seeds of the walk come from the
|
|
9
|
+
// manifests (on disk wherever either command runs) and the lockfile only continues it.
|
|
10
|
+
//
|
|
11
|
+
// ONE classifier decides what a dependency IS, and `workspaceInstallLocked` uses the
|
|
12
|
+
// same one for the full install. The closure and the install cannot disagree about
|
|
13
|
+
// whether `@scope/foo: ^1.0.0` is a workspace edge or a registry package, and that
|
|
14
|
+
// agreement is what lets `flatpak sources --focus` fill exactly the cache the focused
|
|
15
|
+
// install later reads. `@gjsify/workspace`'s `buildDependencyGraph` is deliberately not
|
|
16
|
+
// used: it decides by "does the member's version satisfy the range", this installer by
|
|
17
|
+
// NAME (a name-matched member is always symlinked — install.ts, `localWorkspace`).
|
|
18
|
+
/** The three blocks `gjsify install` reads from a member; same list as install.ts. */
|
|
19
|
+
export const DEPENDENCY_BLOCKS = ['dependencies', 'devDependencies', 'optionalDependencies'];
|
|
20
|
+
/** Protocols that opt a dependency out of both the workspace link and the registry. */
|
|
21
|
+
const EXPLICIT_PROTOCOL = /^(link|file|portal|git\+|https?):/;
|
|
22
|
+
/**
|
|
23
|
+
* What a `name: spec` entry of a member's manifest is. A name that matches a member is
|
|
24
|
+
* the member whatever the spec form (`workspace:`, `^1.2.3`, a dist-tag) — only an
|
|
25
|
+
* explicit out-of-workspace protocol opts out — because routing it to the registry would
|
|
26
|
+
* unpack a published tarball over the member's own source tree.
|
|
27
|
+
*/
|
|
28
|
+
export function classifyDependency(depName, spec, byName) {
|
|
29
|
+
const target = byName.get(depName);
|
|
30
|
+
if (target && !EXPLICIT_PROTOCOL.test(spec))
|
|
31
|
+
return { kind: 'workspace', target };
|
|
32
|
+
if (spec.startsWith('workspace:'))
|
|
33
|
+
return { kind: 'missing-workspace' };
|
|
34
|
+
if (EXPLICIT_PROTOCOL.test(spec))
|
|
35
|
+
return { kind: 'ignored' };
|
|
36
|
+
return { kind: 'external' };
|
|
37
|
+
}
|
|
38
|
+
/** At most this many known names are listed in the unknown-name error. */
|
|
39
|
+
const KNOWN_NAMES_SHOWN = 20;
|
|
40
|
+
/**
|
|
41
|
+
* Resolve `--focus` names to the workspace closure and its external specs.
|
|
42
|
+
*
|
|
43
|
+
* Throws naming every unknown workspace — a typo must not become an install of
|
|
44
|
+
* nothing. Names are exact package names, the spelling the rest of the CLI uses; the
|
|
45
|
+
* root is a member like any other (Yarn's `workspaces focus` rule) and is in the
|
|
46
|
+
* closure only when named or depended on. A root without a `name` cannot be named.
|
|
47
|
+
*
|
|
48
|
+
* @param command label for error messages, e.g. `gjsify install`.
|
|
49
|
+
*/
|
|
50
|
+
export function planFocus(workspaces, focus, command) {
|
|
51
|
+
const byName = new Map(workspaces.map((w) => [w.name, w]));
|
|
52
|
+
const unknown = [...new Set(focus)].filter((name) => !byName.has(name));
|
|
53
|
+
if (unknown.length > 0) {
|
|
54
|
+
const known = [...byName.keys()].sort();
|
|
55
|
+
const shown = known.slice(0, KNOWN_NAMES_SHOWN).join(', ');
|
|
56
|
+
const more = known.length > KNOWN_NAMES_SHOWN ? `, … (${known.length - KNOWN_NAMES_SHOWN} more)` : '';
|
|
57
|
+
throw new Error(`${command}: --focus names unknown workspace(s): ${unknown.join(', ')}. ` +
|
|
58
|
+
`Pass a package name from a workspace's package.json. Known workspaces: ${shown}${more}.`);
|
|
59
|
+
}
|
|
60
|
+
// A Set grows while iterated, so each pass visits what the previous one added.
|
|
61
|
+
const closure = new Set(focus.map((name) => byName.get(name)));
|
|
62
|
+
const specs = new Set();
|
|
63
|
+
for (const ws of closure) {
|
|
64
|
+
for (const block of DEPENDENCY_BLOCKS) {
|
|
65
|
+
for (const [depName, spec] of Object.entries(ws.manifest[block] ?? {})) {
|
|
66
|
+
if (typeof spec !== 'string')
|
|
67
|
+
continue;
|
|
68
|
+
const cls = classifyDependency(depName, spec, byName);
|
|
69
|
+
if (cls.kind === 'workspace')
|
|
70
|
+
closure.add(cls.target);
|
|
71
|
+
else if (cls.kind === 'external')
|
|
72
|
+
specs.add(`${depName}@${spec}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
workspaces: workspaces.filter((w) => closure.has(w)),
|
|
78
|
+
specs: [...specs],
|
|
79
|
+
};
|
|
80
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.58.0",
|
|
4
4
|
"description": "CLI for Gjsify",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -142,22 +142,22 @@
|
|
|
142
142
|
"cli"
|
|
143
143
|
],
|
|
144
144
|
"dependencies": {
|
|
145
|
-
"@gjsify/blueprint": "^0.
|
|
146
|
-
"@gjsify/buffer": "^0.
|
|
147
|
-
"@gjsify/create-app": "^0.
|
|
148
|
-
"@gjsify/node-globals": "^0.
|
|
149
|
-
"@gjsify/node-polyfills": "^0.
|
|
150
|
-
"@gjsify/npm-registry": "^0.
|
|
151
|
-
"@gjsify/resolve-npm": "^0.
|
|
152
|
-
"@gjsify/rolldown-plugin-gjsify": "^0.
|
|
153
|
-
"@gjsify/vite-plugin-blueprint": "^0.
|
|
154
|
-
"@gjsify/rolldown-plugin-pnp": "^0.
|
|
155
|
-
"@gjsify/semver": "^0.
|
|
156
|
-
"@gjsify/tar": "^0.
|
|
157
|
-
"@gjsify/tsc": "^0.
|
|
158
|
-
"@gjsify/utils": "^0.
|
|
159
|
-
"@gjsify/web-polyfills": "^0.
|
|
160
|
-
"@gjsify/workspace": "^0.
|
|
145
|
+
"@gjsify/blueprint": "^0.58.0",
|
|
146
|
+
"@gjsify/buffer": "^0.58.0",
|
|
147
|
+
"@gjsify/create-app": "^0.58.0",
|
|
148
|
+
"@gjsify/node-globals": "^0.58.0",
|
|
149
|
+
"@gjsify/node-polyfills": "^0.58.0",
|
|
150
|
+
"@gjsify/npm-registry": "^0.58.0",
|
|
151
|
+
"@gjsify/resolve-npm": "^0.58.0",
|
|
152
|
+
"@gjsify/rolldown-plugin-gjsify": "^0.58.0",
|
|
153
|
+
"@gjsify/vite-plugin-blueprint": "^0.58.0",
|
|
154
|
+
"@gjsify/rolldown-plugin-pnp": "^0.58.0",
|
|
155
|
+
"@gjsify/semver": "^0.58.0",
|
|
156
|
+
"@gjsify/tar": "^0.58.0",
|
|
157
|
+
"@gjsify/tsc": "^0.58.0",
|
|
158
|
+
"@gjsify/utils": "^0.58.0",
|
|
159
|
+
"@gjsify/web-polyfills": "^0.58.0",
|
|
160
|
+
"@gjsify/workspace": "^0.58.0",
|
|
161
161
|
"acorn": "^8.17.0",
|
|
162
162
|
"cosmiconfig": "^9.0.2",
|
|
163
163
|
"get-tsconfig": "^4.14.0",
|
|
@@ -166,13 +166,13 @@
|
|
|
166
166
|
"yargs": "^18.0.0"
|
|
167
167
|
},
|
|
168
168
|
"devDependencies": {
|
|
169
|
-
"@gjsify/unit": "^0.
|
|
169
|
+
"@gjsify/unit": "^0.58.0",
|
|
170
170
|
"@types/yargs": "^17.0.35",
|
|
171
171
|
"typescript": "^6.0.3"
|
|
172
172
|
},
|
|
173
173
|
"peerDependencies": {
|
|
174
|
-
"@gjsify/oxfmt-native": "^0.
|
|
175
|
-
"@gjsify/rolldown-native": "^0.
|
|
174
|
+
"@gjsify/oxfmt-native": "^0.58.0",
|
|
175
|
+
"@gjsify/rolldown-native": "^0.58.0"
|
|
176
176
|
},
|
|
177
177
|
"peerDependenciesMeta": {
|
|
178
178
|
"@gjsify/oxfmt-native": {
|