@vltpkg/graph 1.3.2 → 1.3.4
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/ideal/append-nodes.d.ts +1 -1
- package/dist/ideal/append-nodes.js +13 -13
- package/dist/ideal/build.js +4 -0
- package/dist/ideal/prefetch-resolve.d.ts +15 -0
- package/dist/ideal/prefetch-resolve.js +75 -0
- package/dist/node.d.ts +1 -0
- package/dist/node.js +15 -4
- package/dist/reify/add-edge.js +10 -8
- package/dist/reify/bin-chmod.js +2 -1
- package/dist/reify/build.js +9 -5
- package/dist/reify/delete-edge.js +5 -3
- package/dist/reify/delete-nodes.js +2 -1
- package/dist/reify/extract-node.js +8 -17
- package/dist/reify/index.js +4 -0
- package/dist/reify/internal-hoist.js +9 -9
- package/dist/reify/optional-fail.d.ts +11 -0
- package/dist/reify/optional-fail.js +36 -0
- package/dist/reify/symlink-sync.d.ts +5 -0
- package/dist/reify/symlink-sync.js +16 -0
- package/dist/reify/unshare.d.ts +7 -0
- package/dist/reify/unshare.js +59 -0
- package/package.json +21 -21
|
@@ -18,7 +18,7 @@ import type { ExplicitAddMap, TransientAddMap, TransientRemoveMap } from './type
|
|
|
18
18
|
*
|
|
19
19
|
* 1. Process all deps at the current level in parallel
|
|
20
20
|
* 2. After each level, run `postPlacementPeerCheck` to handle peer contexts
|
|
21
|
-
* 3. Collect child deps for the next level
|
|
21
|
+
* 3. Collect child deps for the next level, each node once
|
|
22
22
|
* 4. Repeat until no more deps to process
|
|
23
23
|
*
|
|
24
24
|
* **Peer Context Isolation**: Each workspace importer gets its own peer context
|
|
@@ -349,7 +349,10 @@ const fetchManifestsForDeps = async (packageInfo, graph, fromNode, deps, scurry,
|
|
|
349
349
|
// this is the entry point to fetch calls to retrieve manifests
|
|
350
350
|
// from the build ideal graph point of view
|
|
351
351
|
: packageInfo
|
|
352
|
-
|
|
352
|
+
// an explicit tag add revalidates before use
|
|
353
|
+
.manifest(spec, isExplicit ?
|
|
354
|
+
{ from }
|
|
355
|
+
: { from, backgroundRevalidate: true })
|
|
353
356
|
.then(manifest => manifest)
|
|
354
357
|
.catch((er) => {
|
|
355
358
|
// optional deps ignored if inaccessible, but a dep that tried
|
|
@@ -618,7 +621,7 @@ const processPlacementTasks = async (graph, options, placementTasks, add, modifi
|
|
|
618
621
|
*
|
|
619
622
|
* 1. Process all deps at the current level in parallel
|
|
620
623
|
* 2. After each level, run `postPlacementPeerCheck` to handle peer contexts
|
|
621
|
-
* 3. Collect child deps for the next level
|
|
624
|
+
* 3. Collect child deps for the next level, each node once
|
|
622
625
|
* 4. Repeat until no more deps to process
|
|
623
626
|
*
|
|
624
627
|
* **Peer Context Isolation**: Each workspace importer gets its own peer context
|
|
@@ -677,8 +680,6 @@ export const appendNodes = async (packageInfo, graph, fromNode, deps, scurry, op
|
|
|
677
680
|
// Fetch all manifests at this level in parallel without mutating the graph.
|
|
678
681
|
// This phase is read-only to avoid race conditions from network timing.
|
|
679
682
|
const fetchResults = await Promise.all(currentLevelDeps.map(async ({ node, deps: nodeDeps, modifierRefs: nodeModifierRefs, peerContext, depth, }) => {
|
|
680
|
-
// Cycle prevention: mark as seen when starting to process
|
|
681
|
-
seen.add(node.id);
|
|
682
683
|
// Fetch manifests and collect tasks (no graph mutations)
|
|
683
684
|
const result = await fetchManifestsForDeps(packageInfo, graph, node,
|
|
684
685
|
// Sort by name for deterministic ordering (reproducible builds)
|
|
@@ -737,17 +738,16 @@ export const appendNodes = async (packageInfo, graph, fromNode, deps, scurry, op
|
|
|
737
738
|
// ============================================================
|
|
738
739
|
// STEP 3: COLLECT CHILD DEPS FOR NEXT LEVEL
|
|
739
740
|
// ============================================================
|
|
741
|
+
/* c8 ignore next */
|
|
742
|
+
const nextDepth = (currentLevelDeps[0]?.depth ?? 0) + 1;
|
|
740
743
|
for (const childDepsToProcess of levelResults) {
|
|
741
744
|
for (const childDep of childDepsToProcess) {
|
|
742
|
-
//
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
depth: currentDepth + 1,
|
|
749
|
-
});
|
|
750
|
-
}
|
|
745
|
+
// seen at enqueue: skips cycles and same-level duplicates
|
|
746
|
+
// (first in sorted order wins)
|
|
747
|
+
if (seen.has(childDep.node.id))
|
|
748
|
+
continue;
|
|
749
|
+
seen.add(childDep.node.id);
|
|
750
|
+
nextLevelDeps.push({ ...childDep, depth: nextDepth });
|
|
751
751
|
}
|
|
752
752
|
}
|
|
753
753
|
// Advance to next BFS level
|
package/dist/ideal/build.js
CHANGED
|
@@ -3,6 +3,7 @@ import { graphStep } from '@vltpkg/output';
|
|
|
3
3
|
import { load as loadActual } from "../actual/load.js";
|
|
4
4
|
import { load as loadVirtual } from "../lockfile/load.js";
|
|
5
5
|
import { buildIdealFromStartingGraph } from "./build-ideal-from-starting-graph.js";
|
|
6
|
+
import { prefetchResolve } from "./prefetch-resolve.js";
|
|
6
7
|
import { isPathSecurityError } from "../path-security-error.js";
|
|
7
8
|
import { splitDepID } from '@vltpkg/dep-id';
|
|
8
9
|
const getMap = (m) => m ?? new Map();
|
|
@@ -60,6 +61,9 @@ export const build = async (options) => {
|
|
|
60
61
|
monorepo,
|
|
61
62
|
});
|
|
62
63
|
}
|
|
64
|
+
// Start resolving the importers' dependencies server-side, one request
|
|
65
|
+
// to the default registry. manifest() picks the answers up as they land.
|
|
66
|
+
prefetchResolve(graph, packageInfo, options);
|
|
63
67
|
const res = await buildIdealFromStartingGraph({
|
|
64
68
|
...options,
|
|
65
69
|
scurry,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PackageInfoClient } from '@vltpkg/package-info';
|
|
2
|
+
import type { SpecOptions } from '@vltpkg/spec';
|
|
3
|
+
import type { Graph } from '../graph.ts';
|
|
4
|
+
/**
|
|
5
|
+
* Start a server-side resolve for the importers' dependencies, one request
|
|
6
|
+
* to the default registry, without waiting for it: the request overlaps
|
|
7
|
+
* whatever the build does before it asks for its first manifest.
|
|
8
|
+
*
|
|
9
|
+
* Fails soft in every direction: a registry that does not serve the
|
|
10
|
+
* endpoint, a request that errors, and a spec it does not resolve all
|
|
11
|
+
* leave the per-name path to handle it, exactly as before.
|
|
12
|
+
*/
|
|
13
|
+
export declare const prefetchResolve: (graph: Graph, packageInfo: PackageInfoClient, options: SpecOptions & {
|
|
14
|
+
modifiers?: unknown;
|
|
15
|
+
}) => number;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { detectLibc } from '@vltpkg/pick-manifest';
|
|
2
|
+
/**
|
|
3
|
+
* Start a server-side resolve for the importers' dependencies, one request
|
|
4
|
+
* to the default registry, without waiting for it: the request overlaps
|
|
5
|
+
* whatever the build does before it asks for its first manifest.
|
|
6
|
+
*
|
|
7
|
+
* Fails soft in every direction: a registry that does not serve the
|
|
8
|
+
* endpoint, a request that errors, and a spec it does not resolve all
|
|
9
|
+
* leave the per-name path to handle it, exactly as before.
|
|
10
|
+
*/
|
|
11
|
+
export const prefetchResolve = (graph, packageInfo, options) => {
|
|
12
|
+
// a modifier can swap any spec mid-graph, taking the server's whole
|
|
13
|
+
// closure off the client's real one; those installs resolve locally
|
|
14
|
+
if (options.modifiers)
|
|
15
|
+
return 0;
|
|
16
|
+
const registry = options.registry;
|
|
17
|
+
if (!registry)
|
|
18
|
+
return 0;
|
|
19
|
+
const scopes = Object.keys(options['scoped-registries'] ?? {});
|
|
20
|
+
// a name under a scoped registry is that registry's; it is not sent to
|
|
21
|
+
// this one at all, and `stop` keeps the server from walking into it
|
|
22
|
+
const scoped = (name) => scopes.some(scope => name.startsWith(`${scope}/`));
|
|
23
|
+
const roots = [];
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
for (const importer of graph.importers) {
|
|
26
|
+
const manifest = importer.manifest;
|
|
27
|
+
if (!manifest)
|
|
28
|
+
continue;
|
|
29
|
+
for (const field of [
|
|
30
|
+
'dependencies',
|
|
31
|
+
'devDependencies',
|
|
32
|
+
'optionalDependencies',
|
|
33
|
+
]) {
|
|
34
|
+
const deps = manifest[field];
|
|
35
|
+
if (!deps)
|
|
36
|
+
continue;
|
|
37
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
38
|
+
if (typeof spec !== 'string')
|
|
39
|
+
continue;
|
|
40
|
+
// protocols, workspace and file specs are the client's to resolve
|
|
41
|
+
if (spec.includes(':') || scoped(name))
|
|
42
|
+
continue;
|
|
43
|
+
const key = `${name}@${spec}`;
|
|
44
|
+
if (seen.has(key))
|
|
45
|
+
continue;
|
|
46
|
+
seen.add(key);
|
|
47
|
+
roots.push({ name, spec });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!roots.length)
|
|
52
|
+
return 0;
|
|
53
|
+
const have = [];
|
|
54
|
+
for (const node of graph.nodes.values()) {
|
|
55
|
+
if (node.manifest &&
|
|
56
|
+
node.name &&
|
|
57
|
+
node.version &&
|
|
58
|
+
!scoped(node.name)) {
|
|
59
|
+
have.push(`${node.name}@${node.version}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
packageInfo.prefetchResolve(registry, {
|
|
63
|
+
roots,
|
|
64
|
+
have,
|
|
65
|
+
...(scopes.length ? { stop: { scopes } } : {}),
|
|
66
|
+
platform: {
|
|
67
|
+
os: process.platform,
|
|
68
|
+
cpu: process.arch,
|
|
69
|
+
node: process.versions.node,
|
|
70
|
+
// undefined never serializes, so no libc means the field is absent
|
|
71
|
+
libc: detectLibc(),
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
return roots.length;
|
|
75
|
+
};
|
package/dist/node.d.ts
CHANGED
|
@@ -180,6 +180,7 @@ export declare class Node implements NodeLike {
|
|
|
180
180
|
set location(location: string);
|
|
181
181
|
/**
|
|
182
182
|
* The resolved location of the node in the file system.
|
|
183
|
+
* Memoized per location and scurry cwd.
|
|
183
184
|
*/
|
|
184
185
|
resolvedLocation(scurry: PathScurry): string;
|
|
185
186
|
/**
|
package/dist/node.js
CHANGED
|
@@ -16,6 +16,7 @@ export class Node {
|
|
|
16
16
|
}
|
|
17
17
|
#options;
|
|
18
18
|
#location;
|
|
19
|
+
#resolvedLoc;
|
|
19
20
|
#rawManifest;
|
|
20
21
|
#optional = false;
|
|
21
22
|
/**
|
|
@@ -243,9 +244,17 @@ export class Node {
|
|
|
243
244
|
}
|
|
244
245
|
/**
|
|
245
246
|
* The resolved location of the node in the file system.
|
|
247
|
+
* Memoized per location and scurry cwd.
|
|
246
248
|
*/
|
|
247
249
|
resolvedLocation(scurry) {
|
|
248
|
-
|
|
250
|
+
const loc = this.location;
|
|
251
|
+
const { cwd } = scurry;
|
|
252
|
+
const m = this.#resolvedLoc;
|
|
253
|
+
if (m?.loc === loc && m.cwd === cwd)
|
|
254
|
+
return m.path;
|
|
255
|
+
const path = cwd.resolve(loc).fullpath();
|
|
256
|
+
this.#resolvedLoc = { loc, cwd, path };
|
|
257
|
+
return path;
|
|
249
258
|
}
|
|
250
259
|
/**
|
|
251
260
|
* The location of the node_modules folder where this node's edgesOut
|
|
@@ -257,9 +266,11 @@ export class Node {
|
|
|
257
266
|
*/
|
|
258
267
|
nodeModules(scurry) {
|
|
259
268
|
const loc = this.resolvedLocation(scurry);
|
|
260
|
-
|
|
261
|
-
loc.substring(0, loc.length - this.name.length - 1)
|
|
262
|
-
|
|
269
|
+
if (this.inVltStore()) {
|
|
270
|
+
return loc.substring(0, loc.length - this.name.length - 1);
|
|
271
|
+
}
|
|
272
|
+
const { sep } = scurry.cwd;
|
|
273
|
+
return (loc.endsWith(sep) ? loc : loc + sep) + 'node_modules';
|
|
263
274
|
}
|
|
264
275
|
constructor(options, id, manifest, spec, name, version) {
|
|
265
276
|
this.#options = options;
|
package/dist/reify/add-edge.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { cmdShimIfExists } from '@vltpkg/cmd-shim';
|
|
2
|
-
import {
|
|
2
|
+
import { symlinkSync } from 'node:fs';
|
|
3
3
|
import { dirname, relative, resolve } from 'node:path';
|
|
4
|
+
import { symlinkSyncMkdirp } from "./symlink-sync.js";
|
|
4
5
|
const clobberSymlink = async (target, link, remover, type = 'file') => {
|
|
5
6
|
// On Windows, directory symlinks require elevated privileges.
|
|
6
7
|
// Use junctions instead, which work without special permissions.
|
|
@@ -9,9 +10,8 @@ const clobberSymlink = async (target, link, remover, type = 'file') => {
|
|
|
9
10
|
const symlinkTarget = symlinkType === 'junction' ?
|
|
10
11
|
resolve(dirname(link), target)
|
|
11
12
|
: target;
|
|
12
|
-
await mkdir(dirname(link), { recursive: true });
|
|
13
13
|
try {
|
|
14
|
-
|
|
14
|
+
symlinkSyncMkdirp(symlinkTarget, link, symlinkType);
|
|
15
15
|
}
|
|
16
16
|
catch (er) {
|
|
17
17
|
/* c8 ignore start */
|
|
@@ -23,7 +23,7 @@ const clobberSymlink = async (target, link, remover, type = 'file') => {
|
|
|
23
23
|
await remover.rm(link);
|
|
24
24
|
try {
|
|
25
25
|
// then try to create it again
|
|
26
|
-
|
|
26
|
+
symlinkSync(symlinkTarget, link, symlinkType);
|
|
27
27
|
/* c8 ignore start */
|
|
28
28
|
}
|
|
29
29
|
catch (er) {
|
|
@@ -44,8 +44,10 @@ const clobberSymlink = async (target, link, remover, type = 'file') => {
|
|
|
44
44
|
export const addEdge = async (edge, scurry, remover, bins) => {
|
|
45
45
|
if (!edge.to)
|
|
46
46
|
return;
|
|
47
|
-
const
|
|
48
|
-
const
|
|
47
|
+
const { sep } = scurry.cwd;
|
|
48
|
+
const nm = edge.from.nodeModules(scurry);
|
|
49
|
+
const binRoot = nm + sep + '.bin';
|
|
50
|
+
const path = nm + sep + edge.spec.name.replace('/', sep);
|
|
49
51
|
const promises = [];
|
|
50
52
|
const target = relative(dirname(path), edge.to.resolvedLocation(scurry));
|
|
51
53
|
// can only parallelize this on posix, because the win32 shims
|
|
@@ -57,8 +59,8 @@ export const addEdge = async (edge, scurry, remover, bins) => {
|
|
|
57
59
|
promises.push(p);
|
|
58
60
|
if (bins) {
|
|
59
61
|
for (const [key, val] of Object.entries(bins)) {
|
|
60
|
-
const link =
|
|
61
|
-
const absTarget =
|
|
62
|
+
const link = resolve(binRoot, key);
|
|
63
|
+
const absTarget = resolve(path, val);
|
|
62
64
|
const target = relative(binRoot, absTarget);
|
|
63
65
|
// TODO: bash/cmd/ps1 shims on Windows
|
|
64
66
|
promises.push(process.platform === 'win32' ?
|
package/dist/reify/bin-chmod.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { statSync } from 'node:fs';
|
|
2
2
|
import { chmod } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
3
4
|
/**
|
|
4
5
|
* Make all the packages' bins executable.
|
|
5
6
|
*/
|
|
@@ -18,7 +19,7 @@ export const binChmod = async (node, scurry) => {
|
|
|
18
19
|
if (!node.bins)
|
|
19
20
|
return;
|
|
20
21
|
for (const bin of Object.values(node.bins)) {
|
|
21
|
-
const path =
|
|
22
|
+
const path = join(node.resolvedLocation(scurry), bin);
|
|
22
23
|
let mode;
|
|
23
24
|
try {
|
|
24
25
|
mode = statSync(path).mode;
|
package/dist/reify/build.js
CHANGED
|
@@ -8,6 +8,7 @@ import { optionalFail } from "./optional-fail.js";
|
|
|
8
8
|
import { binChmod } from "./bin-chmod.js";
|
|
9
9
|
import { hasBindingGyp } from "./binding-gyp.js";
|
|
10
10
|
import { scriptsManifest } from "./scripts-manifest.js";
|
|
11
|
+
import { unshare } from "./unshare.js";
|
|
11
12
|
export const build = async (diff, packageJson, scurry, allowScriptsNodes) => {
|
|
12
13
|
const graph = diff.to;
|
|
13
14
|
const nodes = nonEmptyList([...graph.importers]);
|
|
@@ -81,27 +82,30 @@ const visit = async (packageJson, scurry, node, signal, _path) => {
|
|
|
81
82
|
const hasImplicitInstall = hasBindingGyp(node, scurry) && !install && !preinstall;
|
|
82
83
|
// if it has install script or binding.gyp (implicit install), run it
|
|
83
84
|
const runInstall = !!(install || preinstall || postinstall) || hasImplicitInstall;
|
|
85
|
+
// if it's an importer or git, run prepare
|
|
86
|
+
const prepable = node.id.startsWith('git') || node.importer || !node.inVltStore();
|
|
87
|
+
const runPrepare = !!(prepare || preprepare || postprepare) && prepable;
|
|
88
|
+
// store links share inodes with the global store
|
|
89
|
+
if ((runInstall || runPrepare) && node.inVltStore())
|
|
90
|
+
await unshare(dir);
|
|
84
91
|
if (runInstall) {
|
|
85
92
|
await run({
|
|
86
93
|
signal,
|
|
87
94
|
arg0: 'install',
|
|
88
95
|
ignoreMissing: true,
|
|
89
96
|
packageJson,
|
|
90
|
-
cwd:
|
|
97
|
+
cwd: dir,
|
|
91
98
|
projectRoot: node.projectRoot,
|
|
92
99
|
manifest,
|
|
93
100
|
});
|
|
94
101
|
}
|
|
95
|
-
// if it's an importer or git, run prepare
|
|
96
|
-
const prepable = node.id.startsWith('git') || node.importer || !node.inVltStore();
|
|
97
|
-
const runPrepare = !!(prepare || preprepare || postprepare) && prepable;
|
|
98
102
|
if (runPrepare) {
|
|
99
103
|
await run({
|
|
100
104
|
signal,
|
|
101
105
|
arg0: 'prepare',
|
|
102
106
|
ignoreMissing: true,
|
|
103
107
|
packageJson,
|
|
104
|
-
cwd:
|
|
108
|
+
cwd: dir,
|
|
105
109
|
projectRoot: node.projectRoot,
|
|
106
110
|
manifest,
|
|
107
111
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
1
2
|
const rmBinPosix = (remover, bin) => {
|
|
2
3
|
return [remover.rm(bin)];
|
|
3
4
|
};
|
|
@@ -11,15 +12,16 @@ const rmBinWin32 = (remover, bin) => {
|
|
|
11
12
|
const rmBin = process.platform === 'win32' ? rmBinWin32 : rmBinPosix;
|
|
12
13
|
export const deleteEdge = async (edge, scurry, remover) => {
|
|
13
14
|
const { spec: { name }, to, } = edge;
|
|
15
|
+
const { sep } = scurry.cwd;
|
|
14
16
|
const nm = edge.from.nodeModules(scurry);
|
|
15
|
-
const path =
|
|
16
|
-
const binRoot =
|
|
17
|
+
const path = nm + sep + name.replace('/', sep);
|
|
18
|
+
const binRoot = nm + sep + '.bin';
|
|
17
19
|
const promises = [];
|
|
18
20
|
promises.push(remover.rm(path));
|
|
19
21
|
const bins = to?.bins;
|
|
20
22
|
if (bins) {
|
|
21
23
|
for (const key of Object.keys(bins)) {
|
|
22
|
-
const bin =
|
|
24
|
+
const bin = resolve(binRoot, key);
|
|
23
25
|
promises.push(...rmBin(remover, bin));
|
|
24
26
|
}
|
|
25
27
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { deleteEdge } from "./delete-edge.js";
|
|
2
2
|
export const deleteNodes = (diff, remover, scurry) => {
|
|
3
3
|
const store = scurry.resolve('node_modules/.vlt');
|
|
4
|
+
const { sep } = scurry.cwd;
|
|
4
5
|
const rmActions = [];
|
|
5
6
|
for (const node of diff.nodes.delete) {
|
|
6
7
|
// do not delete workspaces or link targets
|
|
7
8
|
if (!node.inVltStore())
|
|
8
9
|
continue;
|
|
9
|
-
rmActions.push(remover.rm(
|
|
10
|
+
rmActions.push(remover.rm(store + sep + node.id));
|
|
10
11
|
for (const edge of node.edgesIn) {
|
|
11
12
|
rmActions.push(deleteEdge(edge, scurry, remover));
|
|
12
13
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { hydrate } from '@vltpkg/dep-id';
|
|
2
|
-
import { platformCheck } from '@vltpkg/pick-manifest';
|
|
3
2
|
import { asManifest, normalizeManifest } from '@vltpkg/types';
|
|
4
|
-
import {
|
|
3
|
+
import { lstatSync } from 'node:fs';
|
|
4
|
+
import { isUnsupported, optionalFail } from "./optional-fail.js";
|
|
5
5
|
import { removeOptionalSubgraph } from "../remove-optional-subgraph.js";
|
|
6
6
|
/**
|
|
7
7
|
* Returns a function that handles removing
|
|
@@ -20,18 +20,13 @@ const getOptionalFailedNodeRemover = (node, diff) => {
|
|
|
20
20
|
*/
|
|
21
21
|
export const extractNode = async (node, scurry, remover, options, packageInfo, diff) => {
|
|
22
22
|
node.extracted = true;
|
|
23
|
-
const { manifest = {} } = node;
|
|
24
23
|
const target = node.resolvedLocation(scurry);
|
|
25
24
|
const from = scurry.resolve('');
|
|
26
25
|
const spec = hydrate(node.id, node.name, options);
|
|
27
26
|
const removeOptionalFailedNode = getOptionalFailedNodeRemover(node, diff);
|
|
28
27
|
const { integrity, resolved } = node;
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
// Check if we should skip this node due to platform incompatibility or deprecation
|
|
32
|
-
if (removeOptionalFailedNode &&
|
|
33
|
-
(manifest.deprecated ||
|
|
34
|
-
!platformCheck(platformData, process.version, process.platform, process.arch))) {
|
|
28
|
+
// skip optional nodes that are deprecated or can't run here
|
|
29
|
+
if (removeOptionalFailedNode && isUnsupported(node)) {
|
|
35
30
|
removeOptionalFailedNode();
|
|
36
31
|
return {
|
|
37
32
|
success: false,
|
|
@@ -39,18 +34,11 @@ export const extractNode = async (node, scurry, remover, options, packageInfo, d
|
|
|
39
34
|
error: new Error('Platform check failed or package deprecated'),
|
|
40
35
|
};
|
|
41
36
|
}
|
|
42
|
-
// the registry manifest can declare install scripts that the
|
|
43
|
-
// tarball's package.json, read by the global store, lacks
|
|
44
|
-
const { scripts, hasInstallScript } = manifest;
|
|
45
37
|
const extractOptions = {
|
|
46
38
|
from,
|
|
47
39
|
integrity,
|
|
48
40
|
resolved,
|
|
49
41
|
fromLockfile: node.resolvedFromLockfile,
|
|
50
|
-
installScripts: !!(hasInstallScript ||
|
|
51
|
-
scripts?.install ||
|
|
52
|
-
scripts?.preinstall ||
|
|
53
|
-
scripts?.postinstall),
|
|
54
42
|
};
|
|
55
43
|
const extracted = (r) => {
|
|
56
44
|
// Store computed integrity for git/remote deps
|
|
@@ -69,7 +57,10 @@ export const extractNode = async (node, scurry, remover, options, packageInfo, d
|
|
|
69
57
|
return { success: true, node };
|
|
70
58
|
};
|
|
71
59
|
try {
|
|
72
|
-
|
|
60
|
+
// nothing to move away on a fresh tree
|
|
61
|
+
if (lstatSync(target, { throwIfNoEntry: false })) {
|
|
62
|
+
await remover.rm(target);
|
|
63
|
+
}
|
|
73
64
|
if (removeOptionalFailedNode) {
|
|
74
65
|
try {
|
|
75
66
|
return extracted(await packageInfo.extract(spec, target, extractOptions));
|
package/dist/reify/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { deleteEdges } from "./delete-edges.js";
|
|
|
11
11
|
import { checkNeededBuild } from "./check-needed-build.js";
|
|
12
12
|
import { deleteNodes } from "./delete-nodes.js";
|
|
13
13
|
import { internalHoist } from "./internal-hoist.js";
|
|
14
|
+
import { pruneUnsupportedOptional } from "./optional-fail.js";
|
|
14
15
|
import { rollback } from "./rollback.js";
|
|
15
16
|
import { updatePackageJson } from "./update-importers-package-json.js";
|
|
16
17
|
import { copyFileSync } from 'node:fs';
|
|
@@ -144,6 +145,9 @@ const reify_ = async (options, diff, remover, saveImportersPackageJson) => {
|
|
|
144
145
|
// any failed/removed optional deps are not reflected in the lockfile
|
|
145
146
|
// data as it is saved.
|
|
146
147
|
const lfData = lockfileData(options);
|
|
148
|
+
// unsupported optional deps are dropped only after that, so the
|
|
149
|
+
// lockfile keeps them for other platforms
|
|
150
|
+
pruneUnsupportedOptional(diff);
|
|
147
151
|
const actions = addNodes(diff, scurry, remover, options, packageInfo).concat(deleteEdges(diff, scurry, remover));
|
|
148
152
|
// need to wait, so that the nodes exist to link to
|
|
149
153
|
if (actions.length)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { splitDepID } from '@vltpkg/dep-id';
|
|
2
2
|
import { Version } from '@vltpkg/semver';
|
|
3
|
-
import { mkdir
|
|
3
|
+
import { mkdir } from 'node:fs/promises';
|
|
4
4
|
import { dirname, relative } from 'node:path';
|
|
5
|
+
import { symlinkSyncMkdirp } from "./symlink-sync.js";
|
|
5
6
|
export const pickNodeToHoist = (nodes) => {
|
|
6
7
|
let pick = undefined;
|
|
7
8
|
let pickNode = undefined;
|
|
@@ -107,16 +108,15 @@ export const internalHoist = async (graph, options, remover) => {
|
|
|
107
108
|
}
|
|
108
109
|
}
|
|
109
110
|
await Promise.all(removes);
|
|
110
|
-
const
|
|
111
|
+
const hoistPath = hoistDir.fullpath();
|
|
112
|
+
const vlt = scurry.resolve('node_modules/.vlt');
|
|
113
|
+
const { sep } = scurry.cwd;
|
|
114
|
+
const native = (n) => n.replace('/', sep);
|
|
111
115
|
for (const [name, { name: nodeName, id }] of links) {
|
|
112
|
-
const target =
|
|
113
|
-
const path =
|
|
114
|
-
|
|
115
|
-
await mkdir(dirname(path), { recursive: true });
|
|
116
|
-
}
|
|
117
|
-
symlinks.push(symlink(relative(dirname(path), target), path, 'dir'));
|
|
116
|
+
const target = vlt + sep + id + sep + 'node_modules' + sep + native(nodeName);
|
|
117
|
+
const path = hoistPath + sep + native(name);
|
|
118
|
+
symlinkSyncMkdirp(relative(dirname(path), target), path, 'dir');
|
|
118
119
|
}
|
|
119
|
-
await Promise.all(symlinks);
|
|
120
120
|
};
|
|
121
121
|
const checkExisting = async (name, entry, links, removes, scurry, remover) => {
|
|
122
122
|
const target = await entry.readlink();
|
|
@@ -13,3 +13,14 @@ export declare function optionalFail(diff: Diff, node: Node & {
|
|
|
13
13
|
optional: false;
|
|
14
14
|
}): undefined;
|
|
15
15
|
export declare function optionalFail(diff: Diff, node: Node): (() => void) | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* True if the node is deprecated or can't run on this platform.
|
|
18
|
+
* Uses lockfile platform data when present, else the manifest.
|
|
19
|
+
*/
|
|
20
|
+
export declare const isUnsupported: (node: Node) => boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Drop optional adds that can't install here, and deps only they
|
|
23
|
+
* need, before anything is extracted. Lockfile data must be taken
|
|
24
|
+
* first, so it keeps them for other platforms.
|
|
25
|
+
*/
|
|
26
|
+
export declare const pruneUnsupportedOptional: (diff: Diff) => void;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { platformCheck } from '@vltpkg/pick-manifest';
|
|
1
2
|
import { removeOptionalSubgraph } from "../remove-optional-subgraph.js";
|
|
2
3
|
export function optionalFail(diff, node) {
|
|
3
4
|
return node.isOptional() ? () => del(diff, node) : undefined;
|
|
@@ -13,3 +14,38 @@ const del = (diff, node) => {
|
|
|
13
14
|
diff.nodes.add.delete(node);
|
|
14
15
|
diff.nodes.delete.add(node);
|
|
15
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* True if the node is deprecated or can't run on this platform.
|
|
19
|
+
* Uses lockfile platform data when present, else the manifest.
|
|
20
|
+
*/
|
|
21
|
+
export const isUnsupported = (node) => !!node.manifest?.deprecated ||
|
|
22
|
+
!platformCheck(node.platform ?? node.manifest ?? {}, process.version, process.platform, process.arch);
|
|
23
|
+
/**
|
|
24
|
+
* Drop optional adds that can't install here, and deps only they
|
|
25
|
+
* need, before anything is extracted. Lockfile data must be taken
|
|
26
|
+
* first, so it keeps them for other platforms.
|
|
27
|
+
*/
|
|
28
|
+
export const pruneUnsupportedOptional = (diff) => {
|
|
29
|
+
let pruned = false;
|
|
30
|
+
for (const node of diff.nodes.add) {
|
|
31
|
+
if (node.inVltStore() &&
|
|
32
|
+
!node.extracted &&
|
|
33
|
+
node.isOptional() &&
|
|
34
|
+
isUnsupported(node)) {
|
|
35
|
+
del(diff, node);
|
|
36
|
+
pruned = true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!pruned)
|
|
40
|
+
return;
|
|
41
|
+
for (const node of diff.to.gc().values()) {
|
|
42
|
+
diff.nodes.add.delete(node);
|
|
43
|
+
diff.nodes.delete.add(node);
|
|
44
|
+
}
|
|
45
|
+
// edges out of removed nodes keep their `to`; never link them
|
|
46
|
+
for (const edge of diff.edges.add) {
|
|
47
|
+
if (diff.to.nodes.get(edge.from.id) !== edge.from) {
|
|
48
|
+
diff.edges.add.delete(edge);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { mkdirSync, symlinkSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* `symlinkSync`, creating the parent dir on ENOENT.
|
|
5
|
+
*/
|
|
6
|
+
export const symlinkSyncMkdirp = (target, link, type) => {
|
|
7
|
+
try {
|
|
8
|
+
symlinkSync(target, link, type);
|
|
9
|
+
}
|
|
10
|
+
catch (er) {
|
|
11
|
+
if (er.code !== 'ENOENT')
|
|
12
|
+
throw er;
|
|
13
|
+
mkdirSync(dirname(link), { recursive: true });
|
|
14
|
+
symlinkSync(target, link, type);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replace every file of `dir` that is hardlinked (e.g. from the global
|
|
3
|
+
* store) by a private copy, so a script run in it cannot write into
|
|
4
|
+
* the store. package.json goes last: a private package.json means
|
|
5
|
+
* nothing is shared, which the global store link keeps true.
|
|
6
|
+
*/
|
|
7
|
+
export declare const unshare: (dir: string) => Promise<void>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { lstatSync, readdirSync, rmSync } from 'node:fs';
|
|
3
|
+
import { chmod, readFile, rename, rm, writeFile, } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { callLimit } from 'promise-call-limit';
|
|
6
|
+
const TMP = '.vlt-unshare-';
|
|
7
|
+
const own = async (p, mode, tmp) => {
|
|
8
|
+
const body = await readFile(p);
|
|
9
|
+
try {
|
|
10
|
+
await writeFile(tmp, body, { mode: mode & 0o777, flag: 'wx' });
|
|
11
|
+
// same modes as a copy from the store
|
|
12
|
+
if (mode & 0o111)
|
|
13
|
+
await chmod(tmp, mode & 0o777);
|
|
14
|
+
// atomic: p is never missing
|
|
15
|
+
await rename(tmp, p);
|
|
16
|
+
}
|
|
17
|
+
catch (er) {
|
|
18
|
+
await rm(tmp, { force: true });
|
|
19
|
+
throw er;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Replace every file of `dir` that is hardlinked (e.g. from the global
|
|
24
|
+
* store) by a private copy, so a script run in it cannot write into
|
|
25
|
+
* the store. package.json goes last: a private package.json means
|
|
26
|
+
* nothing is shared, which the global store link keeps true.
|
|
27
|
+
*/
|
|
28
|
+
export const unshare = async (dir) => {
|
|
29
|
+
const pjPath = join(dir, 'package.json');
|
|
30
|
+
const pj = lstatSync(pjPath, { throwIfNoEntry: false });
|
|
31
|
+
if (!pj || pj.nlink < 2)
|
|
32
|
+
return;
|
|
33
|
+
// random per call: never clashes with another run's tmp
|
|
34
|
+
const tag = `${TMP}${randomBytes(4).toString('hex')}-`;
|
|
35
|
+
let n = 0;
|
|
36
|
+
const shared = [];
|
|
37
|
+
// by hand: a recursive readdir follows dir symlinks
|
|
38
|
+
const walk = (d) => {
|
|
39
|
+
for (const ent of readdirSync(d, { withFileTypes: true })) {
|
|
40
|
+
const p = join(d, ent.name);
|
|
41
|
+
if (ent.isDirectory())
|
|
42
|
+
walk(p);
|
|
43
|
+
else if (ent.isFile() && p !== pjPath) {
|
|
44
|
+
const { mode, nlink } = lstatSync(p);
|
|
45
|
+
if (nlink > 1) {
|
|
46
|
+
const tmp = join(d, `${tag}${n++}`);
|
|
47
|
+
shared.push(() => own(p, mode, tmp));
|
|
48
|
+
}
|
|
49
|
+
else if (ent.name.startsWith(TMP)) {
|
|
50
|
+
// a killed run's tmp copy
|
|
51
|
+
rmSync(p);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
walk(dir);
|
|
57
|
+
await callLimit(shared, { limit: 16, rejectLate: true });
|
|
58
|
+
await own(pjPath, pj.mode, join(dir, `${tag}${n}`));
|
|
59
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vltpkg/graph",
|
|
3
3
|
"description": "A library that helps understanding & expressing what happens on an install",
|
|
4
|
-
"version": "1.3.
|
|
4
|
+
"version": "1.3.4",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/vltpkg/vltpkg.git",
|
|
@@ -13,26 +13,26 @@
|
|
|
13
13
|
"url": "http://vlt.sh"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@vltpkg/cmd-shim": "1.3.
|
|
17
|
-
"@vltpkg/dep-id": "1.3.
|
|
18
|
-
"@vltpkg/dss-breadcrumb": "1.3.
|
|
19
|
-
"@vltpkg/error-cause": "1.3.
|
|
20
|
-
"@vltpkg/fast-split": "1.3.
|
|
21
|
-
"@vltpkg/graph-run": "1.3.
|
|
22
|
-
"@vltpkg/init": "1.3.
|
|
23
|
-
"@vltpkg/output": "1.3.
|
|
24
|
-
"@vltpkg/package-info": "1.3.
|
|
25
|
-
"@vltpkg/package-json": "1.3.
|
|
26
|
-
"@vltpkg/pick-manifest": "1.3.
|
|
27
|
-
"@vltpkg/query": "1.3.
|
|
28
|
-
"@vltpkg/rollback-remove": "1.3.
|
|
29
|
-
"@vltpkg/run": "1.3.
|
|
30
|
-
"@vltpkg/satisfies": "1.3.
|
|
31
|
-
"@vltpkg/security-archive": "1.3.
|
|
32
|
-
"@vltpkg/spec": "1.3.
|
|
33
|
-
"@vltpkg/types": "1.3.
|
|
34
|
-
"@vltpkg/vlt-json": "1.3.
|
|
35
|
-
"@vltpkg/workspaces": "1.3.
|
|
16
|
+
"@vltpkg/cmd-shim": "1.3.4",
|
|
17
|
+
"@vltpkg/dep-id": "1.3.4",
|
|
18
|
+
"@vltpkg/dss-breadcrumb": "1.3.4",
|
|
19
|
+
"@vltpkg/error-cause": "1.3.4",
|
|
20
|
+
"@vltpkg/fast-split": "1.3.4",
|
|
21
|
+
"@vltpkg/graph-run": "1.3.4",
|
|
22
|
+
"@vltpkg/init": "1.3.4",
|
|
23
|
+
"@vltpkg/output": "1.3.4",
|
|
24
|
+
"@vltpkg/package-info": "1.3.4",
|
|
25
|
+
"@vltpkg/package-json": "1.3.4",
|
|
26
|
+
"@vltpkg/pick-manifest": "1.3.4",
|
|
27
|
+
"@vltpkg/query": "1.3.4",
|
|
28
|
+
"@vltpkg/rollback-remove": "1.3.4",
|
|
29
|
+
"@vltpkg/run": "1.3.4",
|
|
30
|
+
"@vltpkg/satisfies": "1.3.4",
|
|
31
|
+
"@vltpkg/security-archive": "1.3.4",
|
|
32
|
+
"@vltpkg/spec": "1.3.4",
|
|
33
|
+
"@vltpkg/types": "1.3.4",
|
|
34
|
+
"@vltpkg/vlt-json": "1.3.4",
|
|
35
|
+
"@vltpkg/workspaces": "1.3.4",
|
|
36
36
|
"path-scurry": "^2.0.1",
|
|
37
37
|
"promise-call-limit": "^3.0.2"
|
|
38
38
|
},
|