zyrk 0.2.3 → 0.3.5
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/README.md +75 -22
- package/VERSION +1 -1
- package/package.json +4 -4
- package/src/cli/main.ts +87 -2
- package/src/commands/dependencies.ts +154 -0
- package/src/commands/deploy.ts +777 -0
- package/src/commands/maintenance.ts +260 -0
- package/src/commands/patch.ts +408 -0
- package/src/commands/project.ts +598 -0
- package/src/commands/store.ts +689 -0
- package/src/install/import-npm.ts +177 -0
- package/src/install/lock.ts +176 -0
- package/src/install/metadata.ts +122 -0
- package/src/install/registry.ts +88 -0
- package/src/install/run.ts +760 -0
- package/src/lockfile/types.ts +5 -0
- package/src/lockfile/write.ts +61 -0
- package/src/project/projection.ts +172 -10
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
import { registerGeneration } from "./metadata.ts";
|
|
2
|
+
import { randomId } from "../util/ids.ts";
|
|
3
|
+
import { importNpmLock } from "./import-npm.ts";
|
|
4
|
+
/** Native project install: resolve -> verify -> realize -> journaled publication. */
|
|
5
|
+
import { readFile, realpath, lstat, mkdir, rm } from "node:fs/promises";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import npa from "npm-package-arg";
|
|
8
|
+
import { satisfies } from "semver";
|
|
9
|
+
import { ZyrkError } from "../errors.ts";
|
|
10
|
+
import { RegistryClient } from "../fetch/registry.ts";
|
|
11
|
+
import { extractTarball } from "../extract/tarball.ts";
|
|
12
|
+
import { commitExtractedSource } from "../store/source.ts";
|
|
13
|
+
import { Store } from "../store/layout.ts";
|
|
14
|
+
import { Transaction } from "../store/txn.ts";
|
|
15
|
+
import { readSourceManifest, sourceManifestPath } from "../store/manifests.ts";
|
|
16
|
+
import { realizeGraph } from "../store/realize.ts";
|
|
17
|
+
import { createGraphDoc, graphHash } from "../store/graph.ts";
|
|
18
|
+
import {
|
|
19
|
+
resolveRequirements,
|
|
20
|
+
type FinalizedGraph,
|
|
21
|
+
} from "../resolve/resolver.ts";
|
|
22
|
+
import { parseRequirement } from "../resolve/spec.ts";
|
|
23
|
+
import { readLockfile, assertFrozenCompatible } from "../lockfile/read.ts";
|
|
24
|
+
import { writeLockfile } from "../lockfile/write.ts";
|
|
25
|
+
import {
|
|
26
|
+
configFingerprint,
|
|
27
|
+
manifestFingerprint,
|
|
28
|
+
scriptPolicyFingerprint,
|
|
29
|
+
type ManifestDeclaration,
|
|
30
|
+
} from "../lockfile/fingerprints.ts";
|
|
31
|
+
import {
|
|
32
|
+
installProjection,
|
|
33
|
+
installMetadata,
|
|
34
|
+
recoverProjection,
|
|
35
|
+
PROJECT_STATE_DIR,
|
|
36
|
+
type ProjectionEntry,
|
|
37
|
+
} from "../project/projection.ts";
|
|
38
|
+
import { acquireLock } from "../util/locks.ts";
|
|
39
|
+
import { isErrno, pathExists, writeFileAtomic } from "../util/fsx.ts";
|
|
40
|
+
import { sha512Hex } from "../util/hash.ts";
|
|
41
|
+
import { canonicalJson } from "../util/json.ts";
|
|
42
|
+
import { lockedGraph, packageName, cleanUrl, lockMismatch } from "./lock.ts";
|
|
43
|
+
|
|
44
|
+
export type InstallOptions = {
|
|
45
|
+
projectDir: string;
|
|
46
|
+
store: Store;
|
|
47
|
+
registry: RegistryClient;
|
|
48
|
+
specs?: readonly string[];
|
|
49
|
+
saveDev?: boolean;
|
|
50
|
+
saveOptional?: boolean;
|
|
51
|
+
importNpm?: boolean;
|
|
52
|
+
mutation?: {
|
|
53
|
+
remove?: readonly string[];
|
|
54
|
+
update?: readonly string[];
|
|
55
|
+
latest?: boolean;
|
|
56
|
+
};
|
|
57
|
+
saveExact?: boolean;
|
|
58
|
+
frozen?: boolean;
|
|
59
|
+
offline?: boolean;
|
|
60
|
+
ignoreScripts?: boolean;
|
|
61
|
+
lockfileOnly?: boolean;
|
|
62
|
+
fetchOnly?: boolean;
|
|
63
|
+
omitDev?: boolean;
|
|
64
|
+
};
|
|
65
|
+
export type InstallResult = {
|
|
66
|
+
readiness: "ready" | "intentionally-unbuilt" | "lockfile-only" | "fetched";
|
|
67
|
+
packages: number;
|
|
68
|
+
reused: number;
|
|
69
|
+
warnings: string[];
|
|
70
|
+
generationId?: string;
|
|
71
|
+
};
|
|
72
|
+
type Manifest = ManifestDeclaration & {
|
|
73
|
+
name?: string;
|
|
74
|
+
version?: string;
|
|
75
|
+
scripts?: Record<string, string>;
|
|
76
|
+
zyrk?: unknown;
|
|
77
|
+
[key: string]: unknown;
|
|
78
|
+
};
|
|
79
|
+
function error(
|
|
80
|
+
code:
|
|
81
|
+
| "ZYRK_MANIFEST_INVALID"
|
|
82
|
+
| "ZYRK_NOT_IMPLEMENTED"
|
|
83
|
+
| "ZYRK_POLICY_DENIED"
|
|
84
|
+
| "ZYRK_USAGE"
|
|
85
|
+
| "ZYRK_OFFLINE_MISSING",
|
|
86
|
+
message: string,
|
|
87
|
+
): ZyrkError {
|
|
88
|
+
return new ZyrkError(code, message, {
|
|
89
|
+
remedy:
|
|
90
|
+
"Review package.json and the documented zyrk install options, then retry.",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function object(value: unknown): value is Record<string, unknown> {
|
|
94
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
95
|
+
}
|
|
96
|
+
async function readManifest(
|
|
97
|
+
project: string,
|
|
98
|
+
): Promise<{ doc: Manifest; text: string }> {
|
|
99
|
+
let doc: unknown, text: string;
|
|
100
|
+
try {
|
|
101
|
+
text = await readFile(path.join(project, "package.json"), "utf8");
|
|
102
|
+
doc = JSON.parse(text);
|
|
103
|
+
} catch {
|
|
104
|
+
throw error(
|
|
105
|
+
"ZYRK_MANIFEST_INVALID",
|
|
106
|
+
"Cannot read the project's package.json as JSON.",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (!object(doc))
|
|
110
|
+
throw error(
|
|
111
|
+
"ZYRK_MANIFEST_INVALID",
|
|
112
|
+
"Project package.json must be an object.",
|
|
113
|
+
);
|
|
114
|
+
for (const group of [
|
|
115
|
+
"dependencies",
|
|
116
|
+
"devDependencies",
|
|
117
|
+
"optionalDependencies",
|
|
118
|
+
"peerDependencies",
|
|
119
|
+
] as const) {
|
|
120
|
+
const entries = doc[group];
|
|
121
|
+
if (entries === undefined) continue;
|
|
122
|
+
if (
|
|
123
|
+
!object(entries) ||
|
|
124
|
+
Object.entries(entries).some(
|
|
125
|
+
([name, spec]) => !packageName(name) || typeof spec !== "string",
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
throw error(
|
|
129
|
+
"ZYRK_MANIFEST_INVALID",
|
|
130
|
+
`Project ${group} must contain valid package names and string specifiers.`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (
|
|
134
|
+
doc.workspaces !== undefined ||
|
|
135
|
+
doc.overrides !== undefined ||
|
|
136
|
+
doc.bundledDependencies !== undefined ||
|
|
137
|
+
doc.bundleDependencies !== undefined
|
|
138
|
+
)
|
|
139
|
+
throw error(
|
|
140
|
+
"ZYRK_NOT_IMPLEMENTED",
|
|
141
|
+
"Project workspaces, overrides, and bundled dependencies are not supported by install yet.",
|
|
142
|
+
);
|
|
143
|
+
if (
|
|
144
|
+
doc.zyrk !== undefined &&
|
|
145
|
+
(!object(doc.zyrk) ||
|
|
146
|
+
Object.keys(doc.zyrk).some(
|
|
147
|
+
(key) => !["scripts", "audit"].includes(key),
|
|
148
|
+
) ||
|
|
149
|
+
(doc.zyrk.scripts !== undefined &&
|
|
150
|
+
canonicalJson(doc.zyrk.scripts) !==
|
|
151
|
+
canonicalJson({ default: "deny", allow: [], deny: [] })))
|
|
152
|
+
)
|
|
153
|
+
throw error(
|
|
154
|
+
"ZYRK_POLICY_DENIED",
|
|
155
|
+
"Project zyrk policy contains unsupported settings; lifecycle approval and mutable-package configuration are not implemented yet.",
|
|
156
|
+
);
|
|
157
|
+
return { doc: doc as Manifest, text };
|
|
158
|
+
}
|
|
159
|
+
function requirements(manifest: Manifest): Record<string, string> {
|
|
160
|
+
return {
|
|
161
|
+
...manifest.devDependencies,
|
|
162
|
+
...manifest.dependencies,
|
|
163
|
+
...manifest.optionalDependencies,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function scriptPolicy(manifest: Manifest): unknown {
|
|
167
|
+
return object(manifest.zyrk) ? manifest.zyrk.scripts : undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function installProject(
|
|
171
|
+
options: InstallOptions,
|
|
172
|
+
): Promise<InstallResult> {
|
|
173
|
+
const project = await realpath(options.projectDir);
|
|
174
|
+
// A local control-directory link would redirect locks and recovery writes.
|
|
175
|
+
const local = path.join(project, PROJECT_STATE_DIR);
|
|
176
|
+
try {
|
|
177
|
+
const stat = await lstat(local);
|
|
178
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
179
|
+
throw error(
|
|
180
|
+
"ZYRK_POLICY_DENIED",
|
|
181
|
+
"Project control directory must be a real directory.",
|
|
182
|
+
);
|
|
183
|
+
} catch (cause) {
|
|
184
|
+
if (!isErrno(cause, "ENOENT")) throw cause;
|
|
185
|
+
await mkdir(local, { mode: 0o700, recursive: true });
|
|
186
|
+
const created = await lstat(local);
|
|
187
|
+
if (!created.isDirectory() || created.isSymbolicLink())
|
|
188
|
+
throw error(
|
|
189
|
+
"ZYRK_POLICY_DENIED",
|
|
190
|
+
"Project control directory must be a real directory.",
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
const held = await acquireLock(path.join(local, "project.lock"), {
|
|
194
|
+
name: "project install",
|
|
195
|
+
});
|
|
196
|
+
let txn: Transaction | undefined;
|
|
197
|
+
let barrier: Awaited<ReturnType<typeof acquireLock>> | undefined;
|
|
198
|
+
let registration: Awaited<ReturnType<typeof registerGeneration>> | undefined;
|
|
199
|
+
try {
|
|
200
|
+
await recoverProjection(project);
|
|
201
|
+
const { doc: manifest, text: originalManifest } =
|
|
202
|
+
await readManifest(project);
|
|
203
|
+
const specs = options.specs ?? [];
|
|
204
|
+
const mutation = options.mutation;
|
|
205
|
+
if (mutation?.remove) {
|
|
206
|
+
for (const name of mutation.remove) {
|
|
207
|
+
if (!packageName(name))
|
|
208
|
+
throw error(
|
|
209
|
+
"ZYRK_USAGE",
|
|
210
|
+
"Remove needs valid direct dependency names.",
|
|
211
|
+
);
|
|
212
|
+
let found = false;
|
|
213
|
+
for (const group of [
|
|
214
|
+
"dependencies",
|
|
215
|
+
"devDependencies",
|
|
216
|
+
"optionalDependencies",
|
|
217
|
+
"peerDependencies",
|
|
218
|
+
] as const) {
|
|
219
|
+
if (manifest[group]?.[name] !== undefined) {
|
|
220
|
+
delete manifest[group]![name];
|
|
221
|
+
found = true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (!found)
|
|
225
|
+
throw error(
|
|
226
|
+
"ZYRK_USAGE",
|
|
227
|
+
"Cannot remove a package that is not a direct dependency.",
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const updating = mutation?.update !== undefined;
|
|
232
|
+
const updateNames = new Set(
|
|
233
|
+
mutation?.update?.length
|
|
234
|
+
? mutation.update
|
|
235
|
+
: Object.keys(requirements(manifest)),
|
|
236
|
+
);
|
|
237
|
+
if (updating)
|
|
238
|
+
for (const name of updateNames)
|
|
239
|
+
if (requirements(manifest)[name] === undefined)
|
|
240
|
+
throw error(
|
|
241
|
+
"ZYRK_USAGE",
|
|
242
|
+
"Cannot update a package that is not a direct dependency.",
|
|
243
|
+
);
|
|
244
|
+
if (
|
|
245
|
+
(options.frozen || options.offline) &&
|
|
246
|
+
(specs.length > 0 || mutation !== undefined)
|
|
247
|
+
)
|
|
248
|
+
throw error(
|
|
249
|
+
"ZYRK_USAGE",
|
|
250
|
+
"Adding package specifications cannot be combined with frozen or offline installation.",
|
|
251
|
+
);
|
|
252
|
+
for (const spec of specs) {
|
|
253
|
+
let parsed: ReturnType<typeof npa>;
|
|
254
|
+
try {
|
|
255
|
+
parsed = npa(spec);
|
|
256
|
+
} catch {
|
|
257
|
+
throw error(
|
|
258
|
+
"ZYRK_USAGE",
|
|
259
|
+
"Cannot parse the requested package specification.",
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
if (
|
|
263
|
+
!parsed.name ||
|
|
264
|
+
!packageName(parsed.name) ||
|
|
265
|
+
!["version", "range", "tag", "alias"].includes(parsed.type)
|
|
266
|
+
)
|
|
267
|
+
throw error(
|
|
268
|
+
"ZYRK_USAGE",
|
|
269
|
+
"Install accepts registry packages and npm aliases only.",
|
|
270
|
+
);
|
|
271
|
+
const group = options.saveOptional
|
|
272
|
+
? "optionalDependencies"
|
|
273
|
+
: options.saveDev
|
|
274
|
+
? "devDependencies"
|
|
275
|
+
: "dependencies";
|
|
276
|
+
const requested =
|
|
277
|
+
parsed.rawSpec === "" || parsed.rawSpec === "*"
|
|
278
|
+
? "latest"
|
|
279
|
+
: parsed.rawSpec;
|
|
280
|
+
manifest[group] = { ...manifest[group], [parsed.name]: requested };
|
|
281
|
+
for (const other of [
|
|
282
|
+
"dependencies",
|
|
283
|
+
"devDependencies",
|
|
284
|
+
"optionalDependencies",
|
|
285
|
+
] as const)
|
|
286
|
+
if (other !== group && manifest[other])
|
|
287
|
+
delete manifest[other]![parsed.name];
|
|
288
|
+
}
|
|
289
|
+
const allDeps = requirements(manifest);
|
|
290
|
+
for (const [name, spec] of Object.entries(allDeps))
|
|
291
|
+
parseRequirement(name, spec);
|
|
292
|
+
const fingerprints = () => ({
|
|
293
|
+
manifest: manifestFingerprint(manifest),
|
|
294
|
+
scripts: scriptPolicyFingerprint(scriptPolicy(manifest)),
|
|
295
|
+
config: configFingerprint({
|
|
296
|
+
registry: options.registry.origin,
|
|
297
|
+
linker: "shared",
|
|
298
|
+
}),
|
|
299
|
+
});
|
|
300
|
+
const lockPath = path.join(project, "package-lock.json");
|
|
301
|
+
const existing = (await readLockfile(lockPath))?.doc;
|
|
302
|
+
let resolved: FinalizedGraph | undefined;
|
|
303
|
+
if (options.importNpm && (!existing || existing.zyrk))
|
|
304
|
+
throw lockMismatch(
|
|
305
|
+
"import --from npm requires an existing npm-only lockfile",
|
|
306
|
+
);
|
|
307
|
+
if (existing && !existing.zyrk) {
|
|
308
|
+
if (options.frozen || options.offline || specs.length)
|
|
309
|
+
throw lockMismatch(
|
|
310
|
+
"npm-only lockfile requires a non-frozen adoption install before adding packages",
|
|
311
|
+
);
|
|
312
|
+
resolved = importNpmLock(existing, allDeps, options.registry.origin);
|
|
313
|
+
}
|
|
314
|
+
if (
|
|
315
|
+
existing?.zyrk &&
|
|
316
|
+
specs.length === 0 &&
|
|
317
|
+
!updating &&
|
|
318
|
+
!mutation?.remove?.length
|
|
319
|
+
) {
|
|
320
|
+
let compatible = true;
|
|
321
|
+
try {
|
|
322
|
+
assertFrozenCompatible(existing, fingerprints());
|
|
323
|
+
} catch (cause) {
|
|
324
|
+
if (options.frozen) throw cause;
|
|
325
|
+
compatible = false;
|
|
326
|
+
}
|
|
327
|
+
if (compatible) resolved = lockedGraph(existing);
|
|
328
|
+
}
|
|
329
|
+
if (!resolved) {
|
|
330
|
+
if (options.frozen) throw lockMismatch("missing compatible lockfile");
|
|
331
|
+
if (options.offline)
|
|
332
|
+
throw error(
|
|
333
|
+
"ZYRK_OFFLINE_MISSING",
|
|
334
|
+
"Offline installation requires an existing compatible zyrk lockfile.",
|
|
335
|
+
);
|
|
336
|
+
const prior = existing?.zyrk ? lockedGraph(existing) : undefined;
|
|
337
|
+
const fresh = new Set(specs.map((spec) => npa(spec).name));
|
|
338
|
+
resolved = await resolveRequirements({
|
|
339
|
+
provider: options.registry,
|
|
340
|
+
requirements: Object.entries(allDeps).map(([name, spec]) => {
|
|
341
|
+
if (updating && updateNames.has(name)) {
|
|
342
|
+
if (mutation?.latest) {
|
|
343
|
+
const parsed = parseRequirement(name, spec);
|
|
344
|
+
return {
|
|
345
|
+
name,
|
|
346
|
+
spec:
|
|
347
|
+
parsed.kind === "alias"
|
|
348
|
+
? `npm:${parsed.realName}@latest`
|
|
349
|
+
: "latest",
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
return { name, spec };
|
|
353
|
+
}
|
|
354
|
+
const previous = prior?.nodes.find((node) =>
|
|
355
|
+
prior.roots.some(
|
|
356
|
+
(root) => root.name === name && root.target === node.key,
|
|
357
|
+
),
|
|
358
|
+
);
|
|
359
|
+
const parsed = parseRequirement(name, spec);
|
|
360
|
+
if (
|
|
361
|
+
!fresh.has(name) &&
|
|
362
|
+
previous &&
|
|
363
|
+
previous.name === parsed.realName &&
|
|
364
|
+
parsed.targetKind !== "tag" &&
|
|
365
|
+
satisfies(previous.version, parsed.targetSpec)
|
|
366
|
+
)
|
|
367
|
+
return {
|
|
368
|
+
name,
|
|
369
|
+
spec:
|
|
370
|
+
parsed.kind === "alias"
|
|
371
|
+
? `npm:${previous.name}@${previous.version}`
|
|
372
|
+
: previous.version,
|
|
373
|
+
};
|
|
374
|
+
return { name, spec };
|
|
375
|
+
}),
|
|
376
|
+
});
|
|
377
|
+
if (updating && mutation?.latest)
|
|
378
|
+
for (const name of updateNames) {
|
|
379
|
+
const selected = resolved.nodes.find((node) =>
|
|
380
|
+
resolved!.roots.some(
|
|
381
|
+
(root) => root.name === name && root.target === node.key,
|
|
382
|
+
),
|
|
383
|
+
);
|
|
384
|
+
if (!selected) throw lockMismatch("updated root disappeared");
|
|
385
|
+
for (const group of [
|
|
386
|
+
"dependencies",
|
|
387
|
+
"devDependencies",
|
|
388
|
+
"optionalDependencies",
|
|
389
|
+
] as const)
|
|
390
|
+
if (manifest[group]?.[name] !== undefined) {
|
|
391
|
+
const original = parseRequirement(name, manifest[group]![name]!);
|
|
392
|
+
const range = options.saveExact
|
|
393
|
+
? selected.version
|
|
394
|
+
: `^${selected.version}`;
|
|
395
|
+
manifest[group]![name] =
|
|
396
|
+
original.kind === "alias"
|
|
397
|
+
? `npm:${selected.name}@${range}`
|
|
398
|
+
: range;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// Save selected tags/bare requests as ranges; never persist an unbound latest.
|
|
403
|
+
for (const spec of specs) {
|
|
404
|
+
const parsed = npa(spec);
|
|
405
|
+
const selected = resolved.nodes.find((n) =>
|
|
406
|
+
resolved!.roots.some(
|
|
407
|
+
(r) => r.name === parsed.name && r.target === n.key,
|
|
408
|
+
),
|
|
409
|
+
);
|
|
410
|
+
if (!selected || !parsed.name)
|
|
411
|
+
throw error(
|
|
412
|
+
"ZYRK_MANIFEST_INVALID",
|
|
413
|
+
"Resolution lost an explicitly requested package.",
|
|
414
|
+
);
|
|
415
|
+
const group = options.saveOptional
|
|
416
|
+
? "optionalDependencies"
|
|
417
|
+
: options.saveDev
|
|
418
|
+
? "devDependencies"
|
|
419
|
+
: "dependencies";
|
|
420
|
+
const declared = manifest[group]?.[parsed.name];
|
|
421
|
+
if (
|
|
422
|
+
declared !== undefined &&
|
|
423
|
+
parsed.type !== "alias" &&
|
|
424
|
+
(parsed.type === "tag" ||
|
|
425
|
+
parsed.type === "version" ||
|
|
426
|
+
parsed.rawSpec === "*")
|
|
427
|
+
)
|
|
428
|
+
manifest[group]![parsed.name] = options.saveExact
|
|
429
|
+
? selected.version
|
|
430
|
+
: `^${selected.version}`;
|
|
431
|
+
}
|
|
432
|
+
// Validate fresh and locked graphs identically, including URL credentials.
|
|
433
|
+
for (const node of resolved.nodes) {
|
|
434
|
+
if (
|
|
435
|
+
!packageName(node.name) ||
|
|
436
|
+
!cleanUrl(node.origin) ||
|
|
437
|
+
!cleanUrl(node.tarballUrl) ||
|
|
438
|
+
node.origin !== options.registry.origin
|
|
439
|
+
)
|
|
440
|
+
throw error(
|
|
441
|
+
"ZYRK_POLICY_DENIED",
|
|
442
|
+
"Selected source has an unsupported or credential-bearing registry origin or archive URL.",
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
const selectedRoots = resolved.roots.filter(
|
|
446
|
+
(r) =>
|
|
447
|
+
!options.omitDev ||
|
|
448
|
+
manifest.dependencies?.[r.name] !== undefined ||
|
|
449
|
+
manifest.optionalDependencies?.[r.name] !== undefined,
|
|
450
|
+
);
|
|
451
|
+
const byKey = new Map(resolved.nodes.map((n) => [n.key, n]));
|
|
452
|
+
const activeKeys = new Set<string>();
|
|
453
|
+
const visit = (key: string): void => {
|
|
454
|
+
if (activeKeys.has(key)) return;
|
|
455
|
+
activeKeys.add(key);
|
|
456
|
+
const n = byKey.get(key);
|
|
457
|
+
if (!n) throw lockMismatch("missing selected node");
|
|
458
|
+
for (const e of n.edges) visit(e.target);
|
|
459
|
+
};
|
|
460
|
+
for (const r of selectedRoots) visit(r.target);
|
|
461
|
+
const activeNodes = resolved.nodes.filter((n) => activeKeys.has(n.key));
|
|
462
|
+
for (const n of activeNodes) {
|
|
463
|
+
for (const [list, current] of [
|
|
464
|
+
[n.platform.os, process.platform],
|
|
465
|
+
[n.platform.cpu, process.arch],
|
|
466
|
+
] as const) {
|
|
467
|
+
if (
|
|
468
|
+
list &&
|
|
469
|
+
(list.includes(`!${current}`) ||
|
|
470
|
+
(list.some((x) => !x.startsWith("!")) &&
|
|
471
|
+
!list.includes(current) &&
|
|
472
|
+
!list.includes("any")))
|
|
473
|
+
)
|
|
474
|
+
throw new ZyrkError(
|
|
475
|
+
"ZYRK_PLATFORM_UNSUPPORTED",
|
|
476
|
+
`Cannot install ${n.name}@${n.version} on the selected platform.`,
|
|
477
|
+
{ remedy: "Select a package version supporting this OS and CPU." },
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
if (n.platform.libc?.length)
|
|
481
|
+
throw error(
|
|
482
|
+
"ZYRK_NOT_IMPLEMENTED",
|
|
483
|
+
"libc-constrained packages require platform selection support that is not implemented yet.",
|
|
484
|
+
);
|
|
485
|
+
if (n.engines?.node && !satisfies(process.versions.node, n.engines.node))
|
|
486
|
+
throw new ZyrkError(
|
|
487
|
+
"ZYRK_PLATFORM_UNSUPPORTED",
|
|
488
|
+
`Cannot install ${n.name}@${n.version} on this Node.js version.`,
|
|
489
|
+
{
|
|
490
|
+
remedy:
|
|
491
|
+
"Use a Node.js version satisfying the package's engines.node range.",
|
|
492
|
+
},
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
barrier = await acquireLock(
|
|
496
|
+
path.join(options.store.paths.locksDir, "liveness.lock"),
|
|
497
|
+
{ name: "store liveness" },
|
|
498
|
+
);
|
|
499
|
+
txn = await Transaction.begin(options.store, "install");
|
|
500
|
+
const candidateLock = path.join(txn.dir, "package-lock.json");
|
|
501
|
+
const doc = await writeLockfile({
|
|
502
|
+
path: candidateLock,
|
|
503
|
+
name: typeof manifest.name === "string" ? manifest.name : "",
|
|
504
|
+
version:
|
|
505
|
+
typeof manifest.version === "string" ? manifest.version : "0.0.0",
|
|
506
|
+
manifestDeps: {
|
|
507
|
+
...manifest.dependencies,
|
|
508
|
+
...manifest.optionalDependencies,
|
|
509
|
+
},
|
|
510
|
+
manifestDevDeps: manifest.devDependencies ?? {},
|
|
511
|
+
resolution: resolved,
|
|
512
|
+
fingerprints: fingerprints(),
|
|
513
|
+
existing: existing ?? null,
|
|
514
|
+
});
|
|
515
|
+
const files: Array<{
|
|
516
|
+
name: "package.json" | "package-lock.json";
|
|
517
|
+
contents: string;
|
|
518
|
+
}> = [];
|
|
519
|
+
if (!options.frozen)
|
|
520
|
+
files.push({
|
|
521
|
+
name: "package-lock.json",
|
|
522
|
+
contents: await readFile(candidateLock, "utf8"),
|
|
523
|
+
});
|
|
524
|
+
if (
|
|
525
|
+
specs.length ||
|
|
526
|
+
mutation?.remove?.length ||
|
|
527
|
+
(updating && mutation?.latest)
|
|
528
|
+
)
|
|
529
|
+
files.push({
|
|
530
|
+
name: "package.json",
|
|
531
|
+
contents: `${JSON.stringify(manifest, null, 2)}\n`,
|
|
532
|
+
});
|
|
533
|
+
const warnings: string[] = [];
|
|
534
|
+
for (const stage of ["preinstall", "install", "postinstall"])
|
|
535
|
+
if (manifest.scripts?.[stage])
|
|
536
|
+
warnings.push(
|
|
537
|
+
`Root ${stage} hook was not run; invoke it explicitly after installation.`,
|
|
538
|
+
);
|
|
539
|
+
if (options.lockfileOnly) {
|
|
540
|
+
// Metadata-only publication uses the same recoverable journal but preserves node_modules.
|
|
541
|
+
await installMetadata(project, files);
|
|
542
|
+
return {
|
|
543
|
+
readiness: "lockfile-only",
|
|
544
|
+
packages: resolved.nodes.length,
|
|
545
|
+
reused: 0,
|
|
546
|
+
warnings,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
await txn.setState("fetching");
|
|
550
|
+
const unique = new Map(activeNodes.map((n) => [n.sourceId, n]));
|
|
551
|
+
for (const node of unique.values()) {
|
|
552
|
+
if (await pathExists(sourceManifestPath(options.store, node.sourceId))) {
|
|
553
|
+
await readSourceManifest(options.store, node.sourceId);
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
if (options.offline)
|
|
557
|
+
throw error(
|
|
558
|
+
"ZYRK_OFFLINE_MISSING",
|
|
559
|
+
`Verified source for ${node.name}@${node.version} is missing from the store.`,
|
|
560
|
+
);
|
|
561
|
+
const tarball = path.join(txn.dir, `${node.sourceId}.tgz`);
|
|
562
|
+
const download = await options.registry.downloadTarball({
|
|
563
|
+
url: node.tarballUrl,
|
|
564
|
+
expectedIntegrity: node.integrity,
|
|
565
|
+
destPath: tarball,
|
|
566
|
+
label: `${node.name}@${node.version}`,
|
|
567
|
+
});
|
|
568
|
+
const extracted = await extractTarball({
|
|
569
|
+
tarballPath: tarball,
|
|
570
|
+
destDir: path.join(txn.dir, node.sourceId),
|
|
571
|
+
});
|
|
572
|
+
const source = await commitExtractedSource({
|
|
573
|
+
store: options.store,
|
|
574
|
+
extraction: extracted,
|
|
575
|
+
name: node.name,
|
|
576
|
+
version: node.version,
|
|
577
|
+
origin: node.origin,
|
|
578
|
+
integrity: download.verifiedSri,
|
|
579
|
+
archiveBytes: download.bytes,
|
|
580
|
+
});
|
|
581
|
+
if (source.sourceId !== node.sourceId)
|
|
582
|
+
throw lockMismatch(
|
|
583
|
+
"download identity disagrees with the locked source",
|
|
584
|
+
);
|
|
585
|
+
await rm(tarball, { force: true });
|
|
586
|
+
}
|
|
587
|
+
await txn.setState("sources-committed");
|
|
588
|
+
if (options.fetchOnly)
|
|
589
|
+
return {
|
|
590
|
+
readiness: "fetched",
|
|
591
|
+
packages: activeNodes.length,
|
|
592
|
+
reused: 0,
|
|
593
|
+
warnings,
|
|
594
|
+
};
|
|
595
|
+
const graph = createGraphDoc(
|
|
596
|
+
activeNodes.map((n) => ({ ...n, deps: n.edges })),
|
|
597
|
+
);
|
|
598
|
+
const realized = await realizeGraph({ store: options.store, graph });
|
|
599
|
+
let unbuilt = false;
|
|
600
|
+
for (const node of activeNodes) {
|
|
601
|
+
const dir = realized.packageDirByKey.get(node.key)!;
|
|
602
|
+
let pkg: unknown;
|
|
603
|
+
try {
|
|
604
|
+
pkg = JSON.parse(
|
|
605
|
+
await readFile(path.join(dir, "package.json"), "utf8"),
|
|
606
|
+
);
|
|
607
|
+
} catch {
|
|
608
|
+
throw error(
|
|
609
|
+
"ZYRK_MANIFEST_INVALID",
|
|
610
|
+
"A selected package has no readable package.json.",
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
if (
|
|
614
|
+
!object(pkg) ||
|
|
615
|
+
pkg.name !== node.name ||
|
|
616
|
+
pkg.version !== node.version
|
|
617
|
+
)
|
|
618
|
+
throw error(
|
|
619
|
+
"ZYRK_MANIFEST_INVALID",
|
|
620
|
+
"A package archive disagrees with its selected name or version.",
|
|
621
|
+
);
|
|
622
|
+
const scripts = object(pkg.scripts) ? pkg.scripts : {};
|
|
623
|
+
const stages = ["preinstall", "install", "postinstall"].filter(
|
|
624
|
+
(stage) => typeof scripts[stage] === "string" && scripts[stage].trim(),
|
|
625
|
+
);
|
|
626
|
+
if (await pathExists(path.join(dir, "binding.gyp")))
|
|
627
|
+
stages.push("binding.gyp");
|
|
628
|
+
if (stages.length) {
|
|
629
|
+
if (!options.ignoreScripts)
|
|
630
|
+
throw new ZyrkError(
|
|
631
|
+
"ZYRK_SCRIPT_BLOCKED",
|
|
632
|
+
`Installation of ${node.name}@${node.version} is blocked by dependency build policy (${stages.join(", ")}).`,
|
|
633
|
+
{
|
|
634
|
+
remedy:
|
|
635
|
+
"Review the dependency; --ignore-scripts explicitly installs it as intentionally unbuilt. Approved private builds are not implemented yet.",
|
|
636
|
+
},
|
|
637
|
+
);
|
|
638
|
+
unbuilt = true;
|
|
639
|
+
warnings.push(
|
|
640
|
+
`${node.name}@${node.version}: skipped ${stages.join(", ")}; intentionally unbuilt.`,
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const entries: ProjectionEntry[] = selectedRoots.map((r) => ({
|
|
645
|
+
name: r.name,
|
|
646
|
+
linkTarget: realized.packageDirByKey.get(r.target)!,
|
|
647
|
+
}));
|
|
648
|
+
const bins: Record<string, string> = Object.create(null) as Record<
|
|
649
|
+
string,
|
|
650
|
+
string
|
|
651
|
+
>;
|
|
652
|
+
for (const r of selectedRoots) {
|
|
653
|
+
const node = byKey.get(r.target)!;
|
|
654
|
+
const dir = realized.packageDirByKey.get(r.target)!;
|
|
655
|
+
for (const [name, target] of Object.entries(node.bin)) {
|
|
656
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name))
|
|
657
|
+
throw error(
|
|
658
|
+
"ZYRK_MANIFEST_INVALID",
|
|
659
|
+
"A dependency declares an unsafe executable name.",
|
|
660
|
+
);
|
|
661
|
+
const script = path.resolve(dir, target);
|
|
662
|
+
const actual = await realpath(script).catch(() => "");
|
|
663
|
+
if (
|
|
664
|
+
!actual.startsWith(`${dir}${path.sep}`) ||
|
|
665
|
+
!(await lstat(actual)).isFile()
|
|
666
|
+
)
|
|
667
|
+
throw error(
|
|
668
|
+
"ZYRK_MANIFEST_INVALID",
|
|
669
|
+
"A dependency executable is missing or escapes its package.",
|
|
670
|
+
);
|
|
671
|
+
if (bins[name] && bins[name] !== script)
|
|
672
|
+
throw error(
|
|
673
|
+
"ZYRK_MANIFEST_INVALID",
|
|
674
|
+
`Direct dependencies declare conflicting executable ${name}.`,
|
|
675
|
+
);
|
|
676
|
+
// Never chmod a shared inode. A shell shim executes non-executable JS with Node.
|
|
677
|
+
bins[name] = script;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
// The durable root exists before links publish; interrupted installs may retain an extra root.
|
|
681
|
+
const projectId = `project1_${sha512Hex(project).slice(0, 32)}`;
|
|
682
|
+
const digest = graphHash(graph);
|
|
683
|
+
await writeFileAtomic(
|
|
684
|
+
path.join(options.store.paths.graphsDir, `${digest}.json`),
|
|
685
|
+
JSON.stringify(graph),
|
|
686
|
+
{ mode: 0o600 },
|
|
687
|
+
);
|
|
688
|
+
await writeFileAtomic(
|
|
689
|
+
path.join(options.store.paths.pinsDir, `${projectId}-${digest}.json`),
|
|
690
|
+
JSON.stringify({
|
|
691
|
+
kind: "generation",
|
|
692
|
+
projectId,
|
|
693
|
+
project,
|
|
694
|
+
graph: digest,
|
|
695
|
+
instances: [...realized.realizationDirByKey.values()],
|
|
696
|
+
sources: [...unique.keys()],
|
|
697
|
+
}),
|
|
698
|
+
{ mode: 0o600 },
|
|
699
|
+
);
|
|
700
|
+
if (
|
|
701
|
+
(await readFile(path.join(project, "package.json"), "utf8")) !==
|
|
702
|
+
originalManifest
|
|
703
|
+
)
|
|
704
|
+
throw lockMismatch("package.json changed while installation was running");
|
|
705
|
+
await txn.setState("committing");
|
|
706
|
+
const readiness = unbuilt
|
|
707
|
+
? ("intentionally-unbuilt" as const)
|
|
708
|
+
: ("ready" as const);
|
|
709
|
+
const generationId = `gen1_${randomId("", 12)}`;
|
|
710
|
+
registration = await registerGeneration({
|
|
711
|
+
store: options.store,
|
|
712
|
+
project,
|
|
713
|
+
projectId,
|
|
714
|
+
generationId,
|
|
715
|
+
graph,
|
|
716
|
+
realized,
|
|
717
|
+
roots: selectedRoots.map((r) => r.target),
|
|
718
|
+
readiness,
|
|
719
|
+
});
|
|
720
|
+
const published = await installProjection(project, entries, {
|
|
721
|
+
generationId,
|
|
722
|
+
lockHeld: true,
|
|
723
|
+
projectId,
|
|
724
|
+
bins,
|
|
725
|
+
files,
|
|
726
|
+
readiness,
|
|
727
|
+
metadata: {
|
|
728
|
+
storeRoot: options.store.paths.root,
|
|
729
|
+
instances: registration.instances,
|
|
730
|
+
activeContexts: registration.contexts,
|
|
731
|
+
lockDigest: sha512Hex(canonicalJson(doc)),
|
|
732
|
+
graphDigest: digest,
|
|
733
|
+
platform: process.platform,
|
|
734
|
+
arch: process.arch,
|
|
735
|
+
node: process.version,
|
|
736
|
+
contexts: [...realized.realizationDirByKey.keys()],
|
|
737
|
+
},
|
|
738
|
+
});
|
|
739
|
+
registration.activate();
|
|
740
|
+
await txn.setState("committed");
|
|
741
|
+
return {
|
|
742
|
+
readiness,
|
|
743
|
+
packages: activeNodes.length,
|
|
744
|
+
reused: realized.reused.length,
|
|
745
|
+
warnings,
|
|
746
|
+
generationId: published.generationId,
|
|
747
|
+
};
|
|
748
|
+
} catch (cause) {
|
|
749
|
+
await recoverProjection(project);
|
|
750
|
+
throw cause;
|
|
751
|
+
} finally {
|
|
752
|
+
registration?.close();
|
|
753
|
+
try {
|
|
754
|
+
if (txn) await txn.finish();
|
|
755
|
+
} finally {
|
|
756
|
+
await barrier?.release();
|
|
757
|
+
await held.release();
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|