zyrk 0.4.8 → 0.5.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/README.md +10 -5
- package/VERSION +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +347 -14
- package/dist/commands/native-export.d.ts +2 -0
- package/dist/commands/native-export.d.ts.map +1 -0
- package/dist/commands/native-export.js +192 -0
- package/dist/install/import-npm-workspace.d.ts +5 -0
- package/dist/install/import-npm-workspace.d.ts.map +1 -0
- package/dist/install/import-npm-workspace.js +283 -0
- package/dist/install/workspace-install.d.ts.map +1 -1
- package/dist/install/workspace-install.js +37 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -141,8 +141,10 @@ zyrk add package-name # edit this importer, preserve other select
|
|
|
141
141
|
|
|
142
142
|
Frozen/offline installs, live peers, patches, mutable registry packages, inspection,
|
|
143
143
|
and repair have workspace fixtures. Builds that reach live workspace sources are
|
|
144
|
-
blocked before execution.
|
|
145
|
-
|
|
144
|
+
blocked before execution. Existing npm v2/v3 workspace `package-lock.json` selections can be adopted with
|
|
145
|
+
`zyrk import --from npm`, preserving exact versions, aliases and live members.
|
|
146
|
+
Adoption validates the complete graph before project changes; npm shrinkwrap
|
|
147
|
+
precedence and unsupported bundled/external links fail explicitly. Workspace-root production exports include live member closures and assets. Recursive execution supports
|
|
146
148
|
member scripts; filters are exact names or paths, not arbitrary glob expressions.
|
|
147
149
|
|
|
148
150
|
### Run a one-off tool
|
|
@@ -230,9 +232,12 @@ node ./dist-runtime/dist/server.js
|
|
|
230
232
|
|
|
231
233
|
The exporter validates that links stay inside the output and excludes the host
|
|
232
234
|
store. Generic export supports shared production graphs and relocatable private
|
|
233
|
-
JavaScript outputs, including selected patches and generated files.
|
|
234
|
-
|
|
235
|
-
|
|
235
|
+
JavaScript outputs, including selected patches and generated files. Workspace-root exports include live production members, peer contexts, cycles
|
|
236
|
+
and assets. A restricted Linux x64 ELF64 N-API copy recipe supports dependency-free
|
|
237
|
+
addons using `napi_create_int32` and `napi_set_named_property` with matching
|
|
238
|
+
Node/ABI/build profiles. Native bytes are copied unchanged after loader validation.
|
|
239
|
+
Other native formats/API imports, external libraries, loader paths, host-path-dependent
|
|
240
|
+
outputs and arbitrary shell startup adapters remain unsupported. Exports validate active generation/profile metadata and reject
|
|
236
241
|
credentials or escaping links. Structural validation does not prove application health. Run an
|
|
237
242
|
application smoke test from the exported directory with host-store access removed.
|
|
238
243
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v0.
|
|
1
|
+
v0.5.0
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AAmEA,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC7B,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,MAAM,EAAE;QAAE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IAC1C,MAAM,EAAE;QAAE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IAC1C,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAwEF,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,MAAM,CAAC,CA0ZjB"}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
/** Self-contained
|
|
1
|
+
/** Self-contained runtime deployment and explicit local Docker commands. */
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
3
|
import { open, copyFile, chmod, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rename, rm, symlink, writeFile, } from "node:fs/promises";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { discoverWorkspaces, findWorkspaceRoot, } from "../install/workspaces.js";
|
|
6
|
+
import { readWorkspaceLock } from "../install/workspace-lock.js";
|
|
7
|
+
import { validateNativeExport } from "./native-export.js";
|
|
8
|
+
import { workspaceRegistryOccurrences, workspaceContextId, } from "../install/workspace-graph.js";
|
|
5
9
|
import { findProjectBoundary, resolveStoreRoot } from "../config/paths.js";
|
|
6
10
|
import { ZyrkError } from "../errors.js";
|
|
7
11
|
import { lockedGraph } from "../install/lock.js";
|
|
@@ -81,7 +85,8 @@ function output(options, result, human) {
|
|
|
81
85
|
: human + "\n");
|
|
82
86
|
}
|
|
83
87
|
export async function runDeployCommand(options) {
|
|
84
|
-
const boundary = await
|
|
88
|
+
const boundary = (await findWorkspaceRoot(options.cwd)) ??
|
|
89
|
+
(await findProjectBoundary(options.cwd));
|
|
85
90
|
if (!boundary)
|
|
86
91
|
throw error("ZYRK_MANIFEST_INVALID", "Cannot find a project to deploy.");
|
|
87
92
|
const project = await realpath(boundary);
|
|
@@ -125,7 +130,7 @@ export async function runDeployCommand(options) {
|
|
|
125
130
|
const rawLock = await readJson(path.join(project, "package-lock.json"));
|
|
126
131
|
if (manifest.workspaces !== undefined ||
|
|
127
132
|
(object(rawLock.zyrk) && rawLock.zyrk.version === 2))
|
|
128
|
-
|
|
133
|
+
return await exportWorkspace(options, project, manifest, state, out, parent, files, entry);
|
|
129
134
|
const doc = validateLockDoc(rawLock, "project package-lock.json");
|
|
130
135
|
const resolved = lockedGraph(doc);
|
|
131
136
|
const privateGraph = state.linker === "private";
|
|
@@ -168,6 +173,8 @@ export async function runDeployCommand(options) {
|
|
|
168
173
|
root: resolveStoreRoot({ env: options.env }).root,
|
|
169
174
|
});
|
|
170
175
|
store.requireInitialized();
|
|
176
|
+
if (within(store.root, out))
|
|
177
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Deployment output must be outside the managed store.");
|
|
171
178
|
const barrier = await acquireLock(path.join(store.paths.locksDir, "liveness.lock"), { name: "portable deployment retention" });
|
|
172
179
|
try {
|
|
173
180
|
const installedKeys = new Set(Array.isArray(state.contexts)
|
|
@@ -261,9 +268,7 @@ export async function runDeployCommand(options) {
|
|
|
261
268
|
await symlink(e.target, target);
|
|
262
269
|
}
|
|
263
270
|
else {
|
|
264
|
-
|
|
265
|
-
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Native addon runtime compatibility requires a validated deployment adapter.");
|
|
266
|
-
await copyFile(path.join(sourceDir, e.path), target);
|
|
271
|
+
await scanOrCopyPrivateFile(path.join(sourceDir, e.path), target, [project, store.root], privateBudget);
|
|
267
272
|
await chmod(target, e.mode === "exec" ? 0o755 : 0o644);
|
|
268
273
|
}
|
|
269
274
|
}
|
|
@@ -311,8 +316,7 @@ export async function runDeployCommand(options) {
|
|
|
311
316
|
const entryStat = await lstat(path.join(stage, entry)).catch(() => null);
|
|
312
317
|
if (!entryStat?.isFile())
|
|
313
318
|
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "The selected runtime Node entry is missing.");
|
|
314
|
-
|
|
315
|
-
await scanPrivateExport(stage, [project, store.root], privateBudget);
|
|
319
|
+
await scanPrivateExport(stage, [project, store.root], privateBudget);
|
|
316
320
|
await validateLinks(stage, stage);
|
|
317
321
|
await writeFile(path.join(stage, "zyrk-deploy.json"), JSON.stringify({
|
|
318
322
|
schemaVersion: 1,
|
|
@@ -473,7 +477,7 @@ async function docker(options, project, manifest) {
|
|
|
473
477
|
buildScript: build ?? null,
|
|
474
478
|
port: Number(portText),
|
|
475
479
|
}, null, 2) + "\n",
|
|
476
|
-
"ZYRK_DOCKER.md": `Review Dockerfile and .dockerignore before building. Runtime files: ${files.join(", ")}. Runtime entry: node ${entry}. Build script: ${build ?? "none"}. Container port: ${portText}.\n\nThe pinned CLI tarball must remain at its declared path. Dependencies are fetched with an optional BuildKit npmrc secret, then installed, built, and exported without networking or registry secrets. No host node_modules or store enters the runtime image. Adapt build inputs and credential exclusions to your project; generated files do not prove application health. Relocatable JavaScript output from ready private generations is supported, including patches and approved build output. Native
|
|
480
|
+
"ZYRK_DOCKER.md": `Review Dockerfile and .dockerignore before building. Runtime files: ${files.join(", ")}. Runtime entry: node ${entry}. Build script: ${build ?? "none"}. Container port: ${portText}.\n\nThe pinned CLI tarball must remain at its declared path. Dependencies are fetched with an optional BuildKit npmrc secret, then installed, built, and exported without networking or registry secrets. No host node_modules or store enters the runtime image. Adapt build inputs and credential exclusions to your project; generated files do not prove application health. Relocatable JavaScript output from ready private generations is supported, including patches and approved build output. Live workspace runtime closures and a restricted Linux ELF64 N-API native copy recipe are supported. The native v1 recipe is limited to Linux x64 ELF64 addons importing napi_create_int32 and napi_set_named_property (plus validated weak toolchain hooks), with exact installed Node/ABI/profile checks. It copies bytes unchanged and runs no package code during export. Native addons with external libraries, loader search paths, unknown formats or API imports, embedded absolute paths, and arbitrary shell start commands require another validated adapter and are rejected by this exporter. Workspace member payloads include their live files and assets except node_modules; credential filenames and links outside each source root are rejected.\n\nBuild: zyrk docker build --tag your-app:local\nRun: zyrk docker run --tag your-app:local --port ${portText}\nSupply private registry credentials to Docker BuildKit directly with docker build --secret id=npmrc,src=<credential-file> when required; zyrk never embeds them.\n`,
|
|
477
481
|
};
|
|
478
482
|
for (const name of Object.keys(contents))
|
|
479
483
|
if (await lstat(path.join(project, name)).catch(() => null))
|
|
@@ -667,8 +671,23 @@ async function scanOrCopyPrivateFile(source, target, forbidden, budget) {
|
|
|
667
671
|
const metadata = await input.stat();
|
|
668
672
|
if (!metadata.isFile())
|
|
669
673
|
throw error("ZYRK_DEPLOY_ESCAPE", "Private payload contains a special filesystem entry.");
|
|
670
|
-
|
|
671
|
-
|
|
674
|
+
let nativeSnapshot;
|
|
675
|
+
if (/\.node$/i.test(source)) {
|
|
676
|
+
if (metadata.size > 64 * 1024 * 1024)
|
|
677
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Native addon exceeds the validated export size budget.");
|
|
678
|
+
const bytes = Buffer.alloc(metadata.size);
|
|
679
|
+
let offset = 0;
|
|
680
|
+
while (offset < bytes.length) {
|
|
681
|
+
const read = await input.read(bytes, offset, bytes.length - offset, offset);
|
|
682
|
+
if (!read.bytesRead)
|
|
683
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Native addon changed during export validation.");
|
|
684
|
+
offset += read.bytesRead;
|
|
685
|
+
}
|
|
686
|
+
validateNativeExport(bytes);
|
|
687
|
+
// Scan and copy these exact validated bytes. Private source files remain
|
|
688
|
+
// mutable; reopening or rereading them would discard the validation.
|
|
689
|
+
nativeSnapshot = bytes;
|
|
690
|
+
}
|
|
672
691
|
if (target !== undefined)
|
|
673
692
|
output = await open(target, "wx", metadata.mode & 0o111 ? 0o755 : 0o644);
|
|
674
693
|
const needles = [
|
|
@@ -684,16 +703,23 @@ async function scanOrCopyPrivateFile(source, target, forbidden, budget) {
|
|
|
684
703
|
let fileBytes = 0;
|
|
685
704
|
const chunk = Buffer.allocUnsafe(1024 * 1024);
|
|
686
705
|
for (;;) {
|
|
687
|
-
|
|
706
|
+
let bytes;
|
|
707
|
+
if (nativeSnapshot) {
|
|
708
|
+
bytes = nativeSnapshot.subarray(fileBytes, fileBytes + chunk.length);
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
const read = await input.read(chunk, 0, chunk.length, null);
|
|
712
|
+
bytes = chunk.subarray(0, read.bytesRead);
|
|
713
|
+
}
|
|
714
|
+
const bytesRead = bytes.length;
|
|
688
715
|
if (!bytesRead)
|
|
689
716
|
break;
|
|
690
|
-
const bytes = chunk.subarray(0, bytesRead);
|
|
691
717
|
fileBytes += bytesRead;
|
|
692
718
|
budget.bytes += bytesRead;
|
|
693
719
|
checkPrivateBudget(budget);
|
|
694
720
|
if (fileBytes > 512 * 1024 * 1024)
|
|
695
721
|
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "A private generated file exceeds the deployment byte budget.");
|
|
696
|
-
if (first && nativeMagic(bytes))
|
|
722
|
+
if (first && nativeMagic(bytes) && !/\.node$/i.test(source))
|
|
697
723
|
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Native dependency outputs require a validated export adapter.");
|
|
698
724
|
first = false;
|
|
699
725
|
const inspect = Buffer.concat([previous, bytes]);
|
|
@@ -759,3 +785,310 @@ async function scanPrivateExport(root, forbidden, budget) {
|
|
|
759
785
|
await scanOrCopyPrivateFile(file, undefined, forbidden, budget);
|
|
760
786
|
}
|
|
761
787
|
}
|
|
788
|
+
/** Export the selected importer closure; live files are copied, never entered into CAS. */
|
|
789
|
+
async function exportWorkspace(options, project, manifest, state, out, parent, files, entry) {
|
|
790
|
+
const workspace = await discoverWorkspaces(project);
|
|
791
|
+
const locked = await readWorkspaceLock(project, workspace);
|
|
792
|
+
if (!locked ||
|
|
793
|
+
locked.doc.zyrk?.manifest !== workspace.manifestFingerprint ||
|
|
794
|
+
state.lockDigest !== sha512Hex(canonicalJson(locked.doc)))
|
|
795
|
+
throw error("ZYRK_LOCK_MISMATCH", "Workspace deployment manifests, lock and installed generation disagree.");
|
|
796
|
+
if (state.readiness !== "ready")
|
|
797
|
+
throw error("ZYRK_SCRIPT_BLOCKED", "Cannot export an unready workspace generation.");
|
|
798
|
+
if (state.platform !== process.platform || state.arch !== process.arch)
|
|
799
|
+
throw error("ZYRK_PLATFORM_UNSUPPORTED", "Workspace export requires the recorded target platform and architecture.");
|
|
800
|
+
const policy = object(manifest.zyrk) ? manifest.zyrk : {};
|
|
801
|
+
if (locked.doc.zyrk.scripts !== scriptPolicyFingerprint(policy.scripts))
|
|
802
|
+
throw error("ZYRK_LOCK_MISMATCH", "Workspace deployment build policy changed after installation.");
|
|
803
|
+
const origins = new Set(locked.graph.nodes
|
|
804
|
+
.filter((n) => n.kind === "registry")
|
|
805
|
+
.map((n) => n.origin));
|
|
806
|
+
if (origins.size > 1)
|
|
807
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Mixed-origin workspace export requires a validated registry adapter.");
|
|
808
|
+
if (origins.size === 1) {
|
|
809
|
+
const patches = await loadProjectPatches(project);
|
|
810
|
+
const expected = configFingerprint({
|
|
811
|
+
registry: [...origins][0],
|
|
812
|
+
linker: "shared",
|
|
813
|
+
platform: currentPlatform(),
|
|
814
|
+
...(Array.isArray(policy.mutablePackages)
|
|
815
|
+
? { mutablePackages: policy.mutablePackages }
|
|
816
|
+
: {}),
|
|
817
|
+
...(patches.length
|
|
818
|
+
? {
|
|
819
|
+
patches: sha512Hex(canonicalJson(patches.map((p) => ({
|
|
820
|
+
sourceId: p.sourceId,
|
|
821
|
+
digest: p.digest,
|
|
822
|
+
})))),
|
|
823
|
+
}
|
|
824
|
+
: {}),
|
|
825
|
+
});
|
|
826
|
+
if (locked.doc.zyrk.config !== expected)
|
|
827
|
+
throw error("ZYRK_LOCK_MISMATCH", "Workspace deployment configuration or patches changed after installation.");
|
|
828
|
+
}
|
|
829
|
+
const store = await Store.open({
|
|
830
|
+
root: resolveStoreRoot({ env: options.env }).root,
|
|
831
|
+
});
|
|
832
|
+
store.requireInitialized();
|
|
833
|
+
if (within(store.root, out))
|
|
834
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Deployment output must be outside the managed store.");
|
|
835
|
+
const barrier = await acquireLock(path.join(store.paths.locksDir, "liveness.lock"), { name: "workspace deployment retention" });
|
|
836
|
+
try {
|
|
837
|
+
const graph = locked.graph, byKey = new Map(graph.nodes.map((n) => [n.key, n]));
|
|
838
|
+
const selected = new Set();
|
|
839
|
+
const visit = (key) => {
|
|
840
|
+
if (selected.has(key))
|
|
841
|
+
return;
|
|
842
|
+
const n = byKey.get(key);
|
|
843
|
+
if (!n)
|
|
844
|
+
throw error("ZYRK_OFFLINE_MISSING", "Workspace runtime edge is missing.");
|
|
845
|
+
selected.add(key);
|
|
846
|
+
for (const e of n.edges)
|
|
847
|
+
if (!(n.kind === "local" && e.kind === "dev"))
|
|
848
|
+
visit(e.target);
|
|
849
|
+
};
|
|
850
|
+
const rootKey = graph.importers["."];
|
|
851
|
+
if (!rootKey)
|
|
852
|
+
throw error("ZYRK_LOCK_MISMATCH", "Workspace root importer is missing.");
|
|
853
|
+
visit(rootKey);
|
|
854
|
+
const directories = new Map();
|
|
855
|
+
for (const key of selected) {
|
|
856
|
+
const n = byKey.get(key);
|
|
857
|
+
if (n.kind === "local")
|
|
858
|
+
directories.set(key, workspace.importers.find((i) => i.id === n.importer).directory);
|
|
859
|
+
}
|
|
860
|
+
// Resolve recorded projection edges rather than enumerating arbitrary node_modules entries.
|
|
861
|
+
const queue = [...directories.keys()], checked = new Set();
|
|
862
|
+
while (queue.length) {
|
|
863
|
+
const key = queue.shift();
|
|
864
|
+
if (checked.has(key))
|
|
865
|
+
continue;
|
|
866
|
+
checked.add(key);
|
|
867
|
+
const n = byKey.get(key), directory = directories.get(key);
|
|
868
|
+
for (const e of n.edges) {
|
|
869
|
+
if (!selected.has(e.target) || (n.kind === "local" && e.kind === "dev"))
|
|
870
|
+
continue;
|
|
871
|
+
const slot = n.kind === "local" || e.name === n.name
|
|
872
|
+
? path.join(directory, "node_modules", e.name)
|
|
873
|
+
: path.join(directory, n.name.startsWith("@") ? "../.." : "..", e.name);
|
|
874
|
+
const actual = await realpath(slot).catch(() => null), meta = await lstat(slot).catch(() => null);
|
|
875
|
+
if (!actual ||
|
|
876
|
+
!meta?.isSymbolicLink() ||
|
|
877
|
+
(directories.has(e.target) && directories.get(e.target) !== actual))
|
|
878
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Workspace dependency projection disagrees with its recorded context.");
|
|
879
|
+
directories.set(e.target, actual);
|
|
880
|
+
queue.push(e.target);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
for (const [key, source] of directories)
|
|
884
|
+
if (key !== rootKey && (within(source, out) || within(out, source)))
|
|
885
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Deployment output overlaps a selected workspace runtime source.");
|
|
886
|
+
const registryNodes = workspaceRegistryOccurrences(graph);
|
|
887
|
+
const registryKeys = new Set(registryNodes.map((n) => n.key));
|
|
888
|
+
const privateKeys = new Set();
|
|
889
|
+
for (let changed = true; changed;) {
|
|
890
|
+
changed = false;
|
|
891
|
+
for (const n of registryNodes)
|
|
892
|
+
if (!privateKeys.has(n.key) &&
|
|
893
|
+
n.edges.some((e) => !registryKeys.has(e.target) || privateKeys.has(e.target))) {
|
|
894
|
+
privateKeys.add(n.key);
|
|
895
|
+
changed = true;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
const sourceGraph = createGraphDoc(registryNodes.map((n) => ({
|
|
899
|
+
...n,
|
|
900
|
+
deps: n.edges.filter((e) => registryKeys.has(e.target)),
|
|
901
|
+
})));
|
|
902
|
+
const pureGraph = createGraphDoc(registryNodes
|
|
903
|
+
.filter((n) => !privateKeys.has(n.key))
|
|
904
|
+
.map((n) => ({ ...n, deps: n.edges })));
|
|
905
|
+
const sharedDirectories = selectRealizationDirs(store, pureGraph);
|
|
906
|
+
const productionGraph = {
|
|
907
|
+
...graph,
|
|
908
|
+
nodes: graph.nodes.map((n) => ({
|
|
909
|
+
...n,
|
|
910
|
+
edges: n.edges.filter((e) => !(n.kind === "local" && e.kind === "dev")),
|
|
911
|
+
})),
|
|
912
|
+
};
|
|
913
|
+
const db = openStoreDb(store.paths.dbFile);
|
|
914
|
+
const registered = new Set();
|
|
915
|
+
try {
|
|
916
|
+
if (typeof state.generationId !== "string" ||
|
|
917
|
+
!/^gen1_[a-z2-7]+$/.test(state.generationId) ||
|
|
918
|
+
state.projectId !== `project1_${sha512Hex(project).slice(0, 32)}` ||
|
|
919
|
+
state.storeRoot !== store.root ||
|
|
920
|
+
state.workspaceRoot !== project)
|
|
921
|
+
throw error("ZYRK_STORE_CORRUPT", "Workspace generation identity is invalid.");
|
|
922
|
+
const generation = db
|
|
923
|
+
.prepare("SELECT readiness,state,graph_digest FROM generations WHERE generation_id=? AND project_id=?")
|
|
924
|
+
.get(state.generationId, state.projectId);
|
|
925
|
+
if (generation?.readiness !== "ready" ||
|
|
926
|
+
generation.state !== "active" ||
|
|
927
|
+
generation.graph_digest !== state.graphDigest)
|
|
928
|
+
throw error("ZYRK_STORE_CORRUPT", "Workspace deployment requires its active ready generation.");
|
|
929
|
+
const rows = db
|
|
930
|
+
.prepare("SELECT ri.path,ri.instance_id FROM generation_instances gi JOIN realization_instances ri ON gi.instance_id=ri.instance_id WHERE gi.generation_id=? AND ri.verification_state='verified'")
|
|
931
|
+
.all(state.generationId);
|
|
932
|
+
if (!Array.isArray(state.instances) ||
|
|
933
|
+
rows.length !== state.instances.length ||
|
|
934
|
+
rows.some((row) => !state.instances.includes(row.instance_id)))
|
|
935
|
+
throw error("ZYRK_STORE_CORRUPT", "Workspace instance metadata disagrees with its selected generation.");
|
|
936
|
+
rows.forEach((row) => registered.add(row.path));
|
|
937
|
+
}
|
|
938
|
+
finally {
|
|
939
|
+
db.close();
|
|
940
|
+
}
|
|
941
|
+
if (state.linker === "private" && !object(state.buildProfile))
|
|
942
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Workspace private generation has no supported build profile.");
|
|
943
|
+
const nativeProfile = state.buildProfile;
|
|
944
|
+
if (object(nativeProfile) &&
|
|
945
|
+
(typeof nativeProfile.candidate !== "string" ||
|
|
946
|
+
!/^[0-9a-f]{128}$/.test(nativeProfile.candidate) ||
|
|
947
|
+
state.privateDir !==
|
|
948
|
+
path.join(project, ".zyrk-local/generations", String(state.generationId))))
|
|
949
|
+
throw error("ZYRK_STORE_CORRUPT", "Workspace private generation profile or stable directory is invalid.");
|
|
950
|
+
if (object(nativeProfile) &&
|
|
951
|
+
(nativeProfile.node !== process.version ||
|
|
952
|
+
nativeProfile.abi !== process.versions.modules ||
|
|
953
|
+
nativeProfile.platform !== process.platform ||
|
|
954
|
+
nativeProfile.arch !== process.arch))
|
|
955
|
+
throw error("ZYRK_PLATFORM_UNSUPPORTED", "Workspace private artifacts require their exact recorded Node ABI and profile.");
|
|
956
|
+
for (const key of selected) {
|
|
957
|
+
const n = byKey.get(key), directory = directories.get(key);
|
|
958
|
+
if (!directory)
|
|
959
|
+
throw error("ZYRK_OFFLINE_MISSING", "A required workspace runtime package is not installed.");
|
|
960
|
+
if (n.kind === "registry") {
|
|
961
|
+
const context = n.name.startsWith("@")
|
|
962
|
+
? path.resolve(directory, "../../..")
|
|
963
|
+
: path.resolve(directory, "../..");
|
|
964
|
+
if (!registered.has(context) ||
|
|
965
|
+
(!within(store.paths.realizationsDir, context) &&
|
|
966
|
+
!within(path.join(project, ".zyrk-local/generations", String(state.generationId)), context)))
|
|
967
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Workspace registry dependency is outside its registered realization.");
|
|
968
|
+
if (within(store.paths.realizationsDir, context)) {
|
|
969
|
+
if (privateKeys.has(key) ||
|
|
970
|
+
packageDirIn(sharedDirectories.get(key), n.name) !== directory)
|
|
971
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Shared workspace registry dependency selects an unexpected context.");
|
|
972
|
+
await verifyRealization({
|
|
973
|
+
store,
|
|
974
|
+
graph: pureGraph,
|
|
975
|
+
rootKey: key,
|
|
976
|
+
contextId: contextIdFor(pureGraph, key),
|
|
977
|
+
realizationDirs: sharedDirectories,
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
else {
|
|
981
|
+
const expectedContexts = privateKeys.has(key)
|
|
982
|
+
? [
|
|
983
|
+
workspaceContextId(graph, key, String(state.projectId)),
|
|
984
|
+
workspaceContextId(productionGraph, key, String(state.projectId)),
|
|
985
|
+
]
|
|
986
|
+
: [contextIdFor(sourceGraph, key)];
|
|
987
|
+
if (!expectedContexts.some((id) => path.join(project, ".zyrk-local/generations", String(state.generationId), id) === context))
|
|
988
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Private workspace dependency selects an unexpected live context.");
|
|
989
|
+
}
|
|
990
|
+
const m = await readJson(path.join(directory, "package.json"));
|
|
991
|
+
if (m.name !== n.name || m.version !== n.version)
|
|
992
|
+
throw error("ZYRK_STORE_CORRUPT", "Workspace runtime package identity disagrees with its lock.");
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
const stage = await mkdtemp(path.join(parent, ".zyrk-deploy-"));
|
|
996
|
+
try {
|
|
997
|
+
const budget = { entries: 0, bytes: 0, started: Date.now() }, targets = new Map([[rootKey, stage]]);
|
|
998
|
+
for (const key of selected)
|
|
999
|
+
if (key !== rootKey)
|
|
1000
|
+
targets.set(key, packageDirIn(path.join(stage, "node_modules/.zyrk/portable", sha512Hex(key).slice(0, 40)), byKey.get(key).name));
|
|
1001
|
+
for (const file of files)
|
|
1002
|
+
await copyApplication(path.join(project, file), path.join(stage, file), project);
|
|
1003
|
+
for (const key of selected) {
|
|
1004
|
+
const n = byKey.get(key), target = targets.get(key), source = directories.get(key);
|
|
1005
|
+
if (key !== rootKey) {
|
|
1006
|
+
const skip = new Set();
|
|
1007
|
+
if (n.kind === "local")
|
|
1008
|
+
skip.add("node_modules");
|
|
1009
|
+
else
|
|
1010
|
+
for (const e of n.edges)
|
|
1011
|
+
if (e.name === n.name)
|
|
1012
|
+
skip.add(path.join("node_modules", ...e.name.split("/")));
|
|
1013
|
+
await copyPrivatePayload(source, target, source, target, skip, [project, store.root], budget);
|
|
1014
|
+
}
|
|
1015
|
+
const sourceManifest = n.kind === "local"
|
|
1016
|
+
? workspace.importers.find((i) => i.id === n.importer).manifest
|
|
1017
|
+
: await readJson(path.join(source, "package.json"));
|
|
1018
|
+
const exported = { ...sourceManifest };
|
|
1019
|
+
delete exported.devDependencies;
|
|
1020
|
+
delete exported.zyrk;
|
|
1021
|
+
delete exported.packageManager;
|
|
1022
|
+
delete exported.workspaces;
|
|
1023
|
+
await writeFile(path.join(target, "package.json"), JSON.stringify(exported, null, 2) + "\n");
|
|
1024
|
+
}
|
|
1025
|
+
for (const key of selected) {
|
|
1026
|
+
const n = byKey.get(key);
|
|
1027
|
+
for (const e of n.edges)
|
|
1028
|
+
if (selected.has(e.target) &&
|
|
1029
|
+
!(n.kind === "local" && e.kind === "dev")) {
|
|
1030
|
+
await relativeLink(path.join(targets.get(key), "node_modules", e.name), targets.get(e.target));
|
|
1031
|
+
const child = byKey.get(e.target);
|
|
1032
|
+
const rawBin = child.kind === "registry"
|
|
1033
|
+
? child.bin
|
|
1034
|
+
: workspace.importers.find((i) => i.id === child.importer)
|
|
1035
|
+
.manifest.bin;
|
|
1036
|
+
const bins = typeof rawBin === "string"
|
|
1037
|
+
? { [child.name.split("/").at(-1)]: rawBin }
|
|
1038
|
+
: object(rawBin)
|
|
1039
|
+
? rawBin
|
|
1040
|
+
: {};
|
|
1041
|
+
for (const [name, relative] of Object.entries(bins)) {
|
|
1042
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name) ||
|
|
1043
|
+
typeof relative !== "string")
|
|
1044
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Workspace executable declaration is unsafe.");
|
|
1045
|
+
const pkg = targets.get(e.target), file = path.resolve(pkg, relative);
|
|
1046
|
+
if (!within(pkg, file))
|
|
1047
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Workspace executable escapes its runtime package.");
|
|
1048
|
+
const metadata = await lstat(file).catch(() => null);
|
|
1049
|
+
if (!metadata?.isFile())
|
|
1050
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "A required workspace executable is missing.");
|
|
1051
|
+
const binDir = path.join(targets.get(key), "node_modules/.bin");
|
|
1052
|
+
await mkdir(binDir, { recursive: true });
|
|
1053
|
+
if (await lstat(path.join(binDir, name)).catch(() => null))
|
|
1054
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Workspace runtime executable names conflict.");
|
|
1055
|
+
if (metadata.mode & 0o111)
|
|
1056
|
+
await relativeLink(path.join(binDir, name), file);
|
|
1057
|
+
else
|
|
1058
|
+
await writeFile(path.join(binDir, name), '#!/bin/sh\nexec node "$(dirname "$0")"/' +
|
|
1059
|
+
shellQuote(path.relative(binDir, file).split(path.sep).join("/")) +
|
|
1060
|
+
' "$@"\n', { mode: 0o755 });
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
if (!(await lstat(path.join(stage, entry)).catch(() => null))?.isFile())
|
|
1065
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "The selected runtime Node entry is missing.");
|
|
1066
|
+
await scanPrivateExport(stage, [project, store.root], budget);
|
|
1067
|
+
await validateLinks(stage, stage);
|
|
1068
|
+
await writeFile(path.join(stage, "zyrk-deploy.json"), JSON.stringify({
|
|
1069
|
+
schemaVersion: 1,
|
|
1070
|
+
platform: process.platform,
|
|
1071
|
+
arch: process.arch,
|
|
1072
|
+
node: process.version,
|
|
1073
|
+
entry: ["node", entry],
|
|
1074
|
+
requirements: ["Node.js " + process.versions.node.split(".")[0]],
|
|
1075
|
+
packages: selected.size - 1,
|
|
1076
|
+
files,
|
|
1077
|
+
validation: "structural; application smoke test not run",
|
|
1078
|
+
}, null, 2) + "\n");
|
|
1079
|
+
await rename(stage, out);
|
|
1080
|
+
output(options, {
|
|
1081
|
+
packages: selected.size - 1,
|
|
1082
|
+
entry: ["node", entry],
|
|
1083
|
+
validation: "structural",
|
|
1084
|
+
}, `Exported ${selected.size - 1} workspace runtime packages. Start with: node ${entry}. Structural validation passed; no application smoke test ran.`);
|
|
1085
|
+
return 0;
|
|
1086
|
+
}
|
|
1087
|
+
finally {
|
|
1088
|
+
await rm(stage, { recursive: true, force: true });
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
finally {
|
|
1092
|
+
await barrier.release();
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-export.d.ts","sourceRoot":"","sources":["../../src/commands/native-export.ts"],"names":[],"mappings":"AAYA,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CA4MxD"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validated Linux ELF64 N-API copy recipe (§12.3.1).
|
|
3
|
+
*
|
|
4
|
+
* Only position-independent addons with no external dynamic-library imports,
|
|
5
|
+
* loader search paths or text relocations qualify. Undefined symbols are Node's
|
|
6
|
+
* tested integer/property entry points and the weak ELF toolchain hooks below.
|
|
7
|
+
* This recipe
|
|
8
|
+
* copies bytes unchanged; it never executes package code or rewrites binaries.
|
|
9
|
+
* The caller checks the exact build profile and scans host paths/credentials.
|
|
10
|
+
* Other native formats and dependency recipes require dedicated adapters.
|
|
11
|
+
*/
|
|
12
|
+
import { ZyrkError } from "../errors.js";
|
|
13
|
+
export function validateNativeExport(bytes) {
|
|
14
|
+
const fail = () => {
|
|
15
|
+
throw new ZyrkError("ZYRK_DEPLOY_NONRELOCATABLE", "Native artifact is outside the supported Linux ELF64 N-API export recipe.", {
|
|
16
|
+
remedy: "Use a position-independent N-API addon without external dynamic libraries or loader paths, or supply a validated target rebuild adapter.",
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
const range = (offset, size) => {
|
|
20
|
+
if (!Number.isSafeInteger(offset) ||
|
|
21
|
+
!Number.isSafeInteger(size) ||
|
|
22
|
+
offset < 0 ||
|
|
23
|
+
size < 0 ||
|
|
24
|
+
offset + size > bytes.length)
|
|
25
|
+
fail();
|
|
26
|
+
};
|
|
27
|
+
if (process.platform !== "linux" ||
|
|
28
|
+
process.arch !== "x64" ||
|
|
29
|
+
bytes.length < 64 ||
|
|
30
|
+
bytes.length > 64 * 1024 * 1024 ||
|
|
31
|
+
bytes.subarray(0, 4).toString("hex") !== "7f454c46" ||
|
|
32
|
+
bytes[4] !== 2 ||
|
|
33
|
+
bytes[5] !== 1 ||
|
|
34
|
+
bytes[6] !== 1)
|
|
35
|
+
fail();
|
|
36
|
+
if (bytes.readUInt16LE(16) !== 3 ||
|
|
37
|
+
bytes.readUInt16LE(18) !== 62 ||
|
|
38
|
+
bytes.readUInt16LE(52) !== 64 ||
|
|
39
|
+
bytes.readUInt32LE(20) !== 1)
|
|
40
|
+
fail();
|
|
41
|
+
const programs = Number(bytes.readBigUInt64LE(32)), programSize = bytes.readUInt16LE(54), programCount = bytes.readUInt16LE(56);
|
|
42
|
+
if (programSize !== 56 || programCount === 0 || programCount > 1024)
|
|
43
|
+
fail();
|
|
44
|
+
range(programs, programSize * programCount);
|
|
45
|
+
let loaderDynamic;
|
|
46
|
+
const segments = [];
|
|
47
|
+
for (let i = 0; i < programCount; i++) {
|
|
48
|
+
const p = programs + i * programSize, type = bytes.readUInt32LE(p), offset = Number(bytes.readBigUInt64LE(p + 8)), fileSize = Number(bytes.readBigUInt64LE(p + 32)), memorySize = Number(bytes.readBigUInt64LE(p + 40)), address = Number(bytes.readBigUInt64LE(p + 16));
|
|
49
|
+
if (!Number.isSafeInteger(address) ||
|
|
50
|
+
!Number.isSafeInteger(memorySize) ||
|
|
51
|
+
fileSize > memorySize)
|
|
52
|
+
fail();
|
|
53
|
+
range(offset, fileSize);
|
|
54
|
+
if (type === 3)
|
|
55
|
+
fail();
|
|
56
|
+
if (type === 1)
|
|
57
|
+
segments.push({ offset, size: fileSize, address });
|
|
58
|
+
if (type === 2) {
|
|
59
|
+
if (loaderDynamic)
|
|
60
|
+
fail();
|
|
61
|
+
if (fileSize !== memorySize)
|
|
62
|
+
fail();
|
|
63
|
+
loaderDynamic = { offset, size: fileSize, address };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const table = Number(bytes.readBigUInt64LE(40)), size = bytes.readUInt16LE(58), count = bytes.readUInt16LE(60);
|
|
67
|
+
if (size !== 64 || count === 0 || count > 4096)
|
|
68
|
+
fail();
|
|
69
|
+
range(table, size * count);
|
|
70
|
+
const section = (index) => {
|
|
71
|
+
if (index < 0 || index >= count)
|
|
72
|
+
fail();
|
|
73
|
+
const p = table + index * size;
|
|
74
|
+
return {
|
|
75
|
+
type: bytes.readUInt32LE(p + 4),
|
|
76
|
+
address: Number(bytes.readBigUInt64LE(p + 16)),
|
|
77
|
+
offset: Number(bytes.readBigUInt64LE(p + 24)),
|
|
78
|
+
size: Number(bytes.readBigUInt64LE(p + 32)),
|
|
79
|
+
link: bytes.readUInt32LE(p + 40),
|
|
80
|
+
entry: Number(bytes.readBigUInt64LE(p + 56)),
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
const mapped = (region) => {
|
|
84
|
+
if (!segments.some((segment) => region.offset >= segment.offset &&
|
|
85
|
+
region.offset + region.size <= segment.offset + segment.size &&
|
|
86
|
+
region.address === segment.address + region.offset - segment.offset))
|
|
87
|
+
fail();
|
|
88
|
+
};
|
|
89
|
+
const tags = new Map();
|
|
90
|
+
let symbols, strings;
|
|
91
|
+
let registration = false, api = false, dynamic = false;
|
|
92
|
+
const validatedApi = new Set([
|
|
93
|
+
"napi_create_int32",
|
|
94
|
+
"napi_set_named_property",
|
|
95
|
+
]);
|
|
96
|
+
const weakHooks = new Set([
|
|
97
|
+
"__cxa_finalize",
|
|
98
|
+
"__gmon_start__",
|
|
99
|
+
"_ITM_registerTMCloneTable",
|
|
100
|
+
"_ITM_deregisterTMCloneTable",
|
|
101
|
+
]);
|
|
102
|
+
for (let i = 0; i < count; i++) {
|
|
103
|
+
const s = section(i);
|
|
104
|
+
if (s.type !== 6 && s.type !== 11)
|
|
105
|
+
continue;
|
|
106
|
+
range(s.offset, s.size);
|
|
107
|
+
if (s.type === 6) {
|
|
108
|
+
if (dynamic ||
|
|
109
|
+
!loaderDynamic ||
|
|
110
|
+
loaderDynamic.offset !== s.offset ||
|
|
111
|
+
loaderDynamic.size !== s.size ||
|
|
112
|
+
loaderDynamic.address !== s.address)
|
|
113
|
+
fail();
|
|
114
|
+
mapped(s);
|
|
115
|
+
dynamic = true;
|
|
116
|
+
if (s.entry !== 16 || s.size % 16 !== 0)
|
|
117
|
+
fail();
|
|
118
|
+
let terminated = false;
|
|
119
|
+
for (let p = s.offset; p < s.offset + s.size; p += 16) {
|
|
120
|
+
const tag = bytes.readBigInt64LE(p), value = bytes.readBigUInt64LE(p + 8);
|
|
121
|
+
if (tag === 0n) {
|
|
122
|
+
terminated = true;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
if ([5n, 6n, 10n, 11n].includes(tag) && tags.has(tag))
|
|
126
|
+
fail();
|
|
127
|
+
tags.set(tag, value);
|
|
128
|
+
if ([
|
|
129
|
+
1n,
|
|
130
|
+
15n,
|
|
131
|
+
22n,
|
|
132
|
+
29n,
|
|
133
|
+
0x7fffffffn,
|
|
134
|
+
0x7ffffffdn,
|
|
135
|
+
0x6ffffefbn,
|
|
136
|
+
0x6ffffefcn,
|
|
137
|
+
].includes(tag) ||
|
|
138
|
+
(tag === 30n && (value & 4n) !== 0n))
|
|
139
|
+
fail();
|
|
140
|
+
}
|
|
141
|
+
if (!terminated)
|
|
142
|
+
fail();
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
if (symbols)
|
|
146
|
+
fail();
|
|
147
|
+
symbols = s;
|
|
148
|
+
mapped(s);
|
|
149
|
+
if (s.entry !== 24 || s.size % 24 !== 0)
|
|
150
|
+
fail();
|
|
151
|
+
const names = section(s.link);
|
|
152
|
+
strings = names;
|
|
153
|
+
mapped(names);
|
|
154
|
+
if (names.type !== 3)
|
|
155
|
+
fail();
|
|
156
|
+
range(names.offset, names.size);
|
|
157
|
+
for (let p = s.offset; p < s.offset + s.size; p += 24) {
|
|
158
|
+
const nameOffset = bytes.readUInt32LE(p), info = bytes[p + 4], index = bytes.readUInt16LE(p + 6);
|
|
159
|
+
if (nameOffset >= names.size)
|
|
160
|
+
fail();
|
|
161
|
+
const start = names.offset + nameOffset, end = bytes.indexOf(0, start);
|
|
162
|
+
if (end < start || end >= names.offset + names.size)
|
|
163
|
+
fail();
|
|
164
|
+
const name = bytes.subarray(start, end).toString("utf8");
|
|
165
|
+
if (index === 0 && name) {
|
|
166
|
+
if (validatedApi.has(name))
|
|
167
|
+
api = true;
|
|
168
|
+
else if (!(info >> 4 === 2 && weakHooks.has(name)))
|
|
169
|
+
fail();
|
|
170
|
+
}
|
|
171
|
+
if (name === "napi_register_module_v1" &&
|
|
172
|
+
index !== 0 &&
|
|
173
|
+
(info & 15) === 2)
|
|
174
|
+
registration = true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!dynamic || !registration || !api || !symbols || !strings)
|
|
179
|
+
return fail();
|
|
180
|
+
if (tags.get(6n) !== BigInt(symbols.address) ||
|
|
181
|
+
tags.get(11n) !== 24n ||
|
|
182
|
+
tags.get(5n) !== BigInt(strings.address) ||
|
|
183
|
+
tags.get(10n) !== BigInt(strings.size))
|
|
184
|
+
fail();
|
|
185
|
+
// The dependency-free recipe has no legitimate external filesystem prerequisite.
|
|
186
|
+
// Reject literal absolute path strings; encoded paths require another adapter.
|
|
187
|
+
for (const text of bytes.toString("latin1").split("\0")) {
|
|
188
|
+
if (/^[\x20-\x7e]{4,}$/.test(text) &&
|
|
189
|
+
/(?:^|[\s"'=])(?:\/[A-Za-z0-9_.~-]|[A-Za-z]:[\\/])/.test(text))
|
|
190
|
+
fail();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { NpmLockDoc } from "../lockfile/types.ts";
|
|
2
|
+
import type { WorkspaceProject } from "./workspaces.ts";
|
|
3
|
+
import type { WorkspaceGraph } from "./workspace-graph.ts";
|
|
4
|
+
export declare function importNpmWorkspaceLock(doc: NpmLockDoc, project: WorkspaceProject, origin: string): WorkspaceGraph;
|
|
5
|
+
//# sourceMappingURL=import-npm-workspace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import-npm-workspace.d.ts","sourceRoot":"","sources":["../../src/install/import-npm-workspace.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAQvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,KAAK,EACV,cAAc,EAGf,MAAM,sBAAsB,CAAC;AAE9B,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,gBAAgB,EACzB,MAAM,EAAE,MAAM,GACb,cAAc,CAyThB"}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/** Translate npm's physical virtual tree without selecting new registry versions. */
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { satisfies } from "semver";
|
|
4
|
+
import { validateLockDoc } from "../lockfile/read.js";
|
|
5
|
+
import { sha512Hex } from "../util/hash.js";
|
|
6
|
+
import { canonicalJson } from "../util/json.js";
|
|
7
|
+
import { importNpmLock } from "./import-npm.js";
|
|
8
|
+
import { lockMismatch, cleanUrl } from "./lock.js";
|
|
9
|
+
import { ZyrkError } from "../errors.js";
|
|
10
|
+
import { assertWorkspacePlatform } from "./workspace-graph.js";
|
|
11
|
+
export function importNpmWorkspaceLock(doc, project, origin) {
|
|
12
|
+
if (!doc ||
|
|
13
|
+
typeof doc !== "object" ||
|
|
14
|
+
Array.isArray(doc) ||
|
|
15
|
+
!doc.packages ||
|
|
16
|
+
typeof doc.packages !== "object" ||
|
|
17
|
+
Array.isArray(doc.packages) ||
|
|
18
|
+
Object.values(doc.packages).some((e) => !e || typeof e !== "object" || Array.isArray(e)))
|
|
19
|
+
throw lockMismatch("npm workspace lock has malformed package entries");
|
|
20
|
+
const portable = structuredClone(doc);
|
|
21
|
+
// Relative file declarations are representable only when they name a
|
|
22
|
+
// discovered live importer. Validate their targets before URL validation.
|
|
23
|
+
const importerIds = new Set(project.importers.map((i) => (i.id === "." ? "" : i.id)));
|
|
24
|
+
for (const [location, entry] of Object.entries(portable.packages ?? {}))
|
|
25
|
+
for (const group of [
|
|
26
|
+
"dependencies",
|
|
27
|
+
"devDependencies",
|
|
28
|
+
"optionalDependencies",
|
|
29
|
+
"peerDependencies",
|
|
30
|
+
])
|
|
31
|
+
if (entry[group] && typeof entry[group] === "object")
|
|
32
|
+
for (const [name, spec] of Object.entries(entry[group]))
|
|
33
|
+
if (typeof spec === "string" && spec.startsWith("file:")) {
|
|
34
|
+
const relative = spec.slice(5);
|
|
35
|
+
if (!importerIds.has(location) ||
|
|
36
|
+
relative.startsWith("/") ||
|
|
37
|
+
relative.includes("\\") ||
|
|
38
|
+
/^[A-Za-z]:/.test(relative) ||
|
|
39
|
+
!importerIds.has(path.posix.normalize(path.posix.join(location, relative))))
|
|
40
|
+
throw lockMismatch("npm file declaration is not a declared workspace");
|
|
41
|
+
entry[group][name] = "workspace:*";
|
|
42
|
+
}
|
|
43
|
+
// npm v2 also has a redundant legacy dependency view. Do not adopt a
|
|
44
|
+
// second authority; it is still scanned for unsafe metadata.
|
|
45
|
+
const sanitizeLegacyLinks = (value) => {
|
|
46
|
+
if (typeof value === "string" &&
|
|
47
|
+
value.startsWith("file:") &&
|
|
48
|
+
importerIds.has(value.slice(5)))
|
|
49
|
+
return "workspace:*";
|
|
50
|
+
if (Array.isArray(value))
|
|
51
|
+
return value.map(sanitizeLegacyLinks);
|
|
52
|
+
if (value && typeof value === "object")
|
|
53
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [
|
|
54
|
+
key,
|
|
55
|
+
sanitizeLegacyLinks(child),
|
|
56
|
+
]));
|
|
57
|
+
return value;
|
|
58
|
+
};
|
|
59
|
+
if (portable.dependencies)
|
|
60
|
+
portable.dependencies = sanitizeLegacyLinks(portable.dependencies);
|
|
61
|
+
validateLockDoc(portable, "package-lock.json");
|
|
62
|
+
if (doc.zyrk ||
|
|
63
|
+
![2, 3].includes(doc.lockfileVersion ?? 0) ||
|
|
64
|
+
!doc.packages ||
|
|
65
|
+
!cleanUrl(origin))
|
|
66
|
+
throw lockMismatch("workspace adoption needs an unannotated npm v2/v3 lock");
|
|
67
|
+
const packages = doc.packages;
|
|
68
|
+
const graph = { nodes: [], importers: {}, omissions: [] };
|
|
69
|
+
const locations = new Map();
|
|
70
|
+
const local = new Map(project.importers.map((i) => [i.id === "." ? "" : i.id, i]));
|
|
71
|
+
for (const [location, entry] of Object.entries(packages)) {
|
|
72
|
+
if (location.includes("\\") ||
|
|
73
|
+
location.startsWith("/") ||
|
|
74
|
+
location.split("/").some((s) => s === ".." || s === ".") ||
|
|
75
|
+
location.includes("\0"))
|
|
76
|
+
throw lockMismatch("unsafe npm package location");
|
|
77
|
+
if (entry.link === true &&
|
|
78
|
+
(typeof entry.resolved !== "string" ||
|
|
79
|
+
!local.has(entry.resolved) ||
|
|
80
|
+
entry.resolved === ""))
|
|
81
|
+
throw lockMismatch("npm link does not select a declared workspace");
|
|
82
|
+
if (entry.inBundle === true)
|
|
83
|
+
throw lockMismatch("bundled npm selections are unsupported");
|
|
84
|
+
}
|
|
85
|
+
for (const [location, importer] of local) {
|
|
86
|
+
const entry = packages[location];
|
|
87
|
+
if (!entry || entry.link)
|
|
88
|
+
throw lockMismatch("npm lock lacks a workspace importer");
|
|
89
|
+
for (const field of [
|
|
90
|
+
"dependencies",
|
|
91
|
+
"devDependencies",
|
|
92
|
+
"optionalDependencies",
|
|
93
|
+
"peerDependencies",
|
|
94
|
+
"peerDependenciesMeta",
|
|
95
|
+
"workspaces",
|
|
96
|
+
])
|
|
97
|
+
if (canonicalJson(entry[field] ?? {}) !==
|
|
98
|
+
canonicalJson(importer.manifest[field] ?? {}))
|
|
99
|
+
throw lockMismatch("npm workspace manifest declarations differ");
|
|
100
|
+
if (location &&
|
|
101
|
+
((entry.name !== undefined && entry.name !== importer.manifest.name) ||
|
|
102
|
+
entry.version !== importer.manifest.version))
|
|
103
|
+
throw lockMismatch("npm workspace identity differs");
|
|
104
|
+
const node = {
|
|
105
|
+
kind: "local",
|
|
106
|
+
key: `workspace:${importer.id}`,
|
|
107
|
+
name: importer.manifest.name ?? "workspace-root",
|
|
108
|
+
version: importer.manifest.version ?? "0.0.0",
|
|
109
|
+
importer: importer.id,
|
|
110
|
+
edges: [],
|
|
111
|
+
peers: {},
|
|
112
|
+
optionalPeers: [],
|
|
113
|
+
};
|
|
114
|
+
graph.importers[importer.id] = node.key;
|
|
115
|
+
graph.nodes.push(node);
|
|
116
|
+
locations.set(location, node);
|
|
117
|
+
}
|
|
118
|
+
const find = (from, name) => {
|
|
119
|
+
let current = from;
|
|
120
|
+
for (;;) {
|
|
121
|
+
if (!current.endsWith("node_modules")) {
|
|
122
|
+
const slot = `${current ? current + "/" : ""}node_modules/${name}`;
|
|
123
|
+
if (Object.hasOwn(packages, slot))
|
|
124
|
+
return slot;
|
|
125
|
+
}
|
|
126
|
+
if (!current)
|
|
127
|
+
return undefined;
|
|
128
|
+
current = current.includes("/")
|
|
129
|
+
? current.slice(0, current.lastIndexOf("/"))
|
|
130
|
+
: "";
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const populated = new Set();
|
|
134
|
+
const load = (location, name, spec, from) => {
|
|
135
|
+
const entry = packages[location];
|
|
136
|
+
if (entry.link === true) {
|
|
137
|
+
const target = locations.get(entry.resolved);
|
|
138
|
+
const range = spec.startsWith("workspace:") ? spec.slice(10) : spec;
|
|
139
|
+
if (spec.startsWith("file:")
|
|
140
|
+
? path.posix.normalize(path.posix.join(from, spec.slice(5))) !==
|
|
141
|
+
(target.kind === "local" ? target.importer : "")
|
|
142
|
+
: name !== target.name ||
|
|
143
|
+
(!["*", "^", "~"].includes(range) &&
|
|
144
|
+
!satisfies(target.version, range)))
|
|
145
|
+
throw lockMismatch("npm workspace link disagrees with requirement");
|
|
146
|
+
return target;
|
|
147
|
+
}
|
|
148
|
+
// Reuse the strict single-selection adapter's archive, alias, source and
|
|
149
|
+
// platform validation. Dependencies are wired below using npm locations.
|
|
150
|
+
const shallow = { ...entry };
|
|
151
|
+
delete shallow.dependencies;
|
|
152
|
+
delete shallow.optionalDependencies;
|
|
153
|
+
delete shallow.peerDependencies;
|
|
154
|
+
const selected = importNpmLock({
|
|
155
|
+
lockfileVersion: 3,
|
|
156
|
+
packages: { "": {}, [`node_modules/${name}`]: shallow },
|
|
157
|
+
}, { [name]: spec }, origin).nodes[0];
|
|
158
|
+
let node = locations.get(location);
|
|
159
|
+
if (!node) {
|
|
160
|
+
const { peerBindings: _bindings, ...source } = selected;
|
|
161
|
+
node = {
|
|
162
|
+
...source,
|
|
163
|
+
key: `${selected.name}@${selected.version}#${sha512Hex(location).slice(0, 16)}`,
|
|
164
|
+
kind: "registry",
|
|
165
|
+
edges: [],
|
|
166
|
+
peers: {},
|
|
167
|
+
optionalPeers: [],
|
|
168
|
+
};
|
|
169
|
+
assertWorkspacePlatform({ nodes: [node], importers: {}, omissions: [] });
|
|
170
|
+
graph.nodes.push(node);
|
|
171
|
+
locations.set(location, node);
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
populate(location, node);
|
|
175
|
+
}
|
|
176
|
+
catch (cause) {
|
|
177
|
+
populated.delete(location);
|
|
178
|
+
locations.delete(location);
|
|
179
|
+
graph.nodes = graph.nodes.filter((n) => n !== node);
|
|
180
|
+
throw cause;
|
|
181
|
+
}
|
|
182
|
+
return node;
|
|
183
|
+
};
|
|
184
|
+
const populate = (location, node) => {
|
|
185
|
+
if (populated.has(location))
|
|
186
|
+
return;
|
|
187
|
+
if (populated.size >= 10000)
|
|
188
|
+
throw lockMismatch("npm import resource limit exceeded");
|
|
189
|
+
populated.add(location);
|
|
190
|
+
const entry = packages[location];
|
|
191
|
+
const requirements = new Map();
|
|
192
|
+
for (const [field, kind] of [
|
|
193
|
+
["dependencies", "prod"],
|
|
194
|
+
["devDependencies", "dev"],
|
|
195
|
+
["optionalDependencies", "optional"],
|
|
196
|
+
["peerDependencies", "peer"],
|
|
197
|
+
]) {
|
|
198
|
+
if (kind === "dev" && node.kind !== "local")
|
|
199
|
+
continue;
|
|
200
|
+
const values = entry[field];
|
|
201
|
+
if (values === undefined)
|
|
202
|
+
continue;
|
|
203
|
+
if (!values || typeof values !== "object" || Array.isArray(values))
|
|
204
|
+
throw lockMismatch("invalid npm dependency declarations");
|
|
205
|
+
for (const [name, spec] of Object.entries(values)) {
|
|
206
|
+
if (typeof spec !== "string")
|
|
207
|
+
throw lockMismatch("invalid npm dependency requirement");
|
|
208
|
+
// A peer declaration does not replace an existing regular dependency.
|
|
209
|
+
if (kind !== "peer" || !requirements.has(name))
|
|
210
|
+
requirements.set(name, { spec, kind });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const [name, requirement] of requirements) {
|
|
214
|
+
const meta = entry.peerDependenciesMeta;
|
|
215
|
+
const optionalPeer = requirement.kind === "peer" && meta?.[name]?.optional === true;
|
|
216
|
+
if (optionalPeer)
|
|
217
|
+
node.optionalPeers.push(name);
|
|
218
|
+
const targetLocation = find(location, name);
|
|
219
|
+
if (targetLocation === undefined) {
|
|
220
|
+
if (optionalPeer) {
|
|
221
|
+
node.peers[name] = null;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (requirement.kind === "optional")
|
|
225
|
+
continue;
|
|
226
|
+
throw lockMismatch("npm workspace lock is missing a required selection");
|
|
227
|
+
}
|
|
228
|
+
let target;
|
|
229
|
+
try {
|
|
230
|
+
target = load(targetLocation, name, requirement.spec, location);
|
|
231
|
+
}
|
|
232
|
+
catch (cause) {
|
|
233
|
+
if (requirement.kind === "optional" &&
|
|
234
|
+
cause instanceof ZyrkError &&
|
|
235
|
+
cause.code === "ZYRK_PLATFORM_UNSUPPORTED") {
|
|
236
|
+
graph.omissions.push({ name, parent: node.key, reason: "platform" });
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
throw cause;
|
|
240
|
+
}
|
|
241
|
+
node.edges.push({ name, target: target.key, ...requirement });
|
|
242
|
+
if (requirement.kind === "peer")
|
|
243
|
+
node.peers[name] = target.key;
|
|
244
|
+
}
|
|
245
|
+
// npm allows the same name in dependencies and peerDependencies. Its
|
|
246
|
+
// ordinary edge keeps its kind, but the peer range and binding still apply.
|
|
247
|
+
const peers = entry.peerDependencies;
|
|
248
|
+
const meta = entry.peerDependenciesMeta;
|
|
249
|
+
for (const [name, spec] of Object.entries(peers ?? {})) {
|
|
250
|
+
if (Object.hasOwn(node.peers, name))
|
|
251
|
+
continue;
|
|
252
|
+
const optional = meta?.[name]?.optional === true;
|
|
253
|
+
if (optional)
|
|
254
|
+
node.optionalPeers.push(name);
|
|
255
|
+
const targetLocation = find(location, name);
|
|
256
|
+
if (targetLocation === undefined) {
|
|
257
|
+
if (!optional)
|
|
258
|
+
throw lockMismatch("npm workspace lock is missing a required peer");
|
|
259
|
+
node.peers[name] = null;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const target = load(targetLocation, name, spec, location);
|
|
263
|
+
node.peers[name] = target.key;
|
|
264
|
+
if (!node.edges.some((edge) => edge.name === name))
|
|
265
|
+
node.edges.push({ name, target: target.key, kind: "peer", spec });
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
for (const [location] of local)
|
|
269
|
+
populate(location, locations.get(location));
|
|
270
|
+
const reachable = new Set(), pending = Object.values(graph.importers), byKey = new Map(graph.nodes.map((n) => [n.key, n]));
|
|
271
|
+
while (pending.length) {
|
|
272
|
+
const key = pending.pop();
|
|
273
|
+
if (reachable.has(key))
|
|
274
|
+
continue;
|
|
275
|
+
reachable.add(key);
|
|
276
|
+
for (const edge of byKey.get(key).edges)
|
|
277
|
+
pending.push(edge.target);
|
|
278
|
+
}
|
|
279
|
+
graph.nodes = graph.nodes.filter((n) => reachable.has(n.key));
|
|
280
|
+
graph.omissions = graph.omissions.filter((o) => reachable.has(o.parent));
|
|
281
|
+
graph.nodes.sort((a, b) => a.key.localeCompare(b.key));
|
|
282
|
+
return graph;
|
|
283
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-install.d.ts","sourceRoot":"","sources":["../../src/install/workspace-install.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"workspace-install.d.ts","sourceRoot":"","sources":["../../src/install/workspace-install.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAyH9D,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CAopBxB"}
|
|
@@ -4,6 +4,7 @@ import { constants } from "node:fs";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { discoverWorkspaces, findWorkspaceRoot, assertNoLiveSourceBuilds, } from "./workspaces.js";
|
|
6
6
|
import { resolveWorkspaceGraph, workspaceRegistryOccurrences, workspaceContextId, assertWorkspacePlatform, } from "./workspace-graph.js";
|
|
7
|
+
import { importNpmWorkspaceLock } from "./import-npm-workspace.js";
|
|
7
8
|
import { readWorkspaceLock, workspaceLockDoc } from "./workspace-lock.js";
|
|
8
9
|
import { publishWorkspaceProjection, recoverWorkspaceProjection, workspaceProjectionPaths, } from "./workspace-transaction.js";
|
|
9
10
|
import { ZyrkError } from "../errors.js";
|
|
@@ -48,10 +49,40 @@ async function realDirectory(directory) {
|
|
|
48
49
|
if (!st.isDirectory() || st.isSymbolicLink())
|
|
49
50
|
throw lockMismatch("workspace control parent must be a real directory");
|
|
50
51
|
}
|
|
52
|
+
async function readNpmWorkspaceImport(root, project, options) {
|
|
53
|
+
if (options.frozen || options.offline)
|
|
54
|
+
throw lockMismatch("npm workspace adoption needs a non-frozen online install");
|
|
55
|
+
if (await pathExists(path.join(root, "npm-shrinkwrap.json")))
|
|
56
|
+
throw lockMismatch("npm-shrinkwrap.json takes npm precedence; workspace adoption currently supports package-lock.json only");
|
|
57
|
+
let npmDoc;
|
|
58
|
+
try {
|
|
59
|
+
npmDoc = JSON.parse(await readFile(path.join(root, "package-lock.json"), "utf8"));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw lockMismatch("npm workspace adoption needs an existing readable JSON lock");
|
|
63
|
+
}
|
|
64
|
+
const graph = importNpmWorkspaceLock(npmDoc, project, options.registry.origin);
|
|
65
|
+
// Prove the complete graph and ordinary npm projection are representable
|
|
66
|
+
// before any project state is created. Actual policy fingerprints are added
|
|
67
|
+
// under the install lock after policy validation.
|
|
68
|
+
workspaceLockDoc(project, graph, {
|
|
69
|
+
manifest: project.manifestFingerprint,
|
|
70
|
+
scripts: scriptPolicyFingerprint(undefined),
|
|
71
|
+
config: configFingerprint({
|
|
72
|
+
registry: options.registry.origin,
|
|
73
|
+
linker: "shared",
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
return graph;
|
|
77
|
+
}
|
|
51
78
|
export async function installWorkspaceProject(options) {
|
|
52
79
|
const root = await findWorkspaceRoot(options.projectDir);
|
|
53
80
|
if (!root)
|
|
54
81
|
throw lockMismatch("no authoritative workspace root");
|
|
82
|
+
// Refuse untranslatable locks before creating control directories or
|
|
83
|
+
// recovering a previous generation. Revalidate under the project lock below.
|
|
84
|
+
if (options.importNpm)
|
|
85
|
+
await readNpmWorkspaceImport(root, await discoverWorkspaces(root), options);
|
|
55
86
|
await realDirectory(path.join(root, ".zyrk-local"));
|
|
56
87
|
const held = await acquireLock(path.join(root, ".zyrk-local/project.lock"), {
|
|
57
88
|
name: "workspace install",
|
|
@@ -66,8 +97,6 @@ export async function installWorkspaceProject(options) {
|
|
|
66
97
|
i.id,
|
|
67
98
|
await readFile(path.join(i.directory, "package.json"), "utf8"),
|
|
68
99
|
])));
|
|
69
|
-
if (options.importNpm)
|
|
70
|
-
unsupported("Workspace npm-lock adoption needs its multi-importer adapter.");
|
|
71
100
|
const mutation = await prepareWorkspaceMutation(project, options);
|
|
72
101
|
if (manifest.overrides !== undefined ||
|
|
73
102
|
manifest.bundledDependencies !== undefined ||
|
|
@@ -110,7 +139,12 @@ export async function installWorkspaceProject(options) {
|
|
|
110
139
|
...(patchDigest ? { patches: patchDigest } : {}),
|
|
111
140
|
}),
|
|
112
141
|
};
|
|
113
|
-
|
|
142
|
+
const imported = options.importNpm
|
|
143
|
+
? await readNpmWorkspaceImport(root, project, options)
|
|
144
|
+
: undefined;
|
|
145
|
+
let existing = options.importNpm
|
|
146
|
+
? null
|
|
147
|
+
: await readWorkspaceLock(root, project, options.registry.origin), graph = imported;
|
|
114
148
|
if (existing) {
|
|
115
149
|
const ext = existing.doc.zyrk;
|
|
116
150
|
if (!mutation.changed &&
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zyrk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Storage-efficient Node package manager: one verified store per user, shared across projects through links.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@npmcli/arborist": "10.0.3",
|
|
35
|
-
"@types/node": "^
|
|
35
|
+
"@types/node": "^26.6.5",
|
|
36
36
|
"@types/semver": "^7.8.0",
|
|
37
37
|
"prettier": "3.9.10",
|
|
38
38
|
"typescript": "^7.0.2",
|