@loomhelix/deptrail 0.1.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/LICENSE +21 -0
- package/README.md +421 -0
- package/dist/actions/action-error.js +11 -0
- package/dist/actions/action-jobs.js +199 -0
- package/dist/actions/action-service.js +268 -0
- package/dist/actions/apply.js +120 -0
- package/dist/actions/availability.js +79 -0
- package/dist/actions/change-sets.js +117 -0
- package/dist/actions/dry-run.js +287 -0
- package/dist/actions/edge-moves.js +108 -0
- package/dist/actions/edit-plan.js +179 -0
- package/dist/actions/file-scope.js +118 -0
- package/dist/actions/fix-options.js +141 -0
- package/dist/actions/goal-check.js +80 -0
- package/dist/actions/install.js +27 -0
- package/dist/actions/job-registry.js +104 -0
- package/dist/actions/json-edit.js +189 -0
- package/dist/actions/labels.js +35 -0
- package/dist/actions/node-actions.js +102 -0
- package/dist/actions/overrides-location.js +59 -0
- package/dist/actions/parent-chain.js +210 -0
- package/dist/actions/range-style.js +38 -0
- package/dist/actions/recipes.js +31 -0
- package/dist/actions/root-files.js +30 -0
- package/dist/actions/sandbox.js +175 -0
- package/dist/actions/set-range.js +59 -0
- package/dist/actions/shared-packages.js +50 -0
- package/dist/actions/text-diff.js +203 -0
- package/dist/actions/types.js +1 -0
- package/dist/actions/update-options.js +232 -0
- package/dist/actions/workspace-edit.js +33 -0
- package/dist/adapters/command-runner.js +127 -0
- package/dist/adapters/manifest-only.js +20 -0
- package/dist/adapters/node-modules.js +148 -0
- package/dist/adapters/npm-lock.js +149 -0
- package/dist/adapters/package-manifest.js +57 -0
- package/dist/adapters/pnpm-lock.js +177 -0
- package/dist/adapters/resolve-today.js +93 -0
- package/dist/cli/cli.js +227 -0
- package/dist/cli/open-browser.js +14 -0
- package/dist/cli/package-version.js +6 -0
- package/dist/cli/usage-text.js +27 -0
- package/dist/config/glob.js +119 -0
- package/dist/config/npmrc.js +116 -0
- package/dist/config/root-config.js +103 -0
- package/dist/config/secrets.js +18 -0
- package/dist/discovery/find-roots.js +107 -0
- package/dist/enrich/credentials.js +143 -0
- package/dist/enrich/enrichment-cache.js +42 -0
- package/dist/enrich/enrichment-keys.js +71 -0
- package/dist/enrich/enrichment-service.js +399 -0
- package/dist/enrich/enrichment-status.js +86 -0
- package/dist/enrich/enrichment-store.js +143 -0
- package/dist/enrich/http.js +201 -0
- package/dist/enrich/on-demand.js +220 -0
- package/dist/enrich/osv-client.js +260 -0
- package/dist/enrich/registry-client.js +123 -0
- package/dist/enrich/types.js +1 -0
- package/dist/git/blob-reader.js +180 -0
- package/dist/git/git-commands.js +32 -0
- package/dist/git/git-process.js +71 -0
- package/dist/git/git-repository.js +136 -0
- package/dist/git/ref-list.js +78 -0
- package/dist/git/tree-index.js +87 -0
- package/dist/graph/global-index.js +19 -0
- package/dist/graph/root-graph-builder.js +114 -0
- package/dist/graph/serialize.js +19 -0
- package/dist/model.js +2 -0
- package/dist/query/children.js +38 -0
- package/dist/query/compare-shape.js +13 -0
- package/dist/query/compare.js +245 -0
- package/dist/query/decorations.js +121 -0
- package/dist/query/dependents.js +135 -0
- package/dist/query/duplicates.js +23 -0
- package/dist/query/enrichment-lookup.js +78 -0
- package/dist/query/fixes.js +60 -0
- package/dist/query/graph-diff.js +42 -0
- package/dist/query/issues.js +235 -0
- package/dist/query/lookup.js +49 -0
- package/dist/query/node-fix.js +46 -0
- package/dist/query/package-insight.js +112 -0
- package/dist/query/search.js +61 -0
- package/dist/query/subgraph.js +107 -0
- package/dist/query/versions.js +103 -0
- package/dist/scan/build-root.js +137 -0
- package/dist/scan/cache-shape.js +52 -0
- package/dist/scan/scan-cache.js +32 -0
- package/dist/scan/scan-session.js +127 -0
- package/dist/scan/source-registry.js +55 -0
- package/dist/server/auth.js +45 -0
- package/dist/server/branches.js +105 -0
- package/dist/server/error-log.js +9 -0
- package/dist/server/root-segment.js +13 -0
- package/dist/server/router.js +474 -0
- package/dist/server/server.js +166 -0
- package/dist/server/source-key.js +1 -0
- package/dist/server/static.js +59 -0
- package/dist/sources/file-reader.js +55 -0
- package/dist/sources/git-reader.js +45 -0
- package/dist/ui/assets/index-C4uyB5v3.css +1 -0
- package/dist/ui/assets/index-DYAG2LM-.js +51 -0
- package/dist/ui/index.html +15 -0
- package/package.json +72 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { posix } from 'node:path';
|
|
2
|
+
import { IMPORTER_PREFIX } from "../model.js";
|
|
3
|
+
import { IMPORTER_BLOCKS, PACKAGE_BLOCKS, declaredDependencies, importerKeyOf, isNonRegistrySpec, readManifest, } from "./package-manifest.js";
|
|
4
|
+
const MODULES_DIRECTORY = 'node_modules';
|
|
5
|
+
const REPO_ROOT = '.';
|
|
6
|
+
export async function walkNodeModules(reader, root, builder) {
|
|
7
|
+
if (!(await anyImporterHasModules(reader, root)))
|
|
8
|
+
return false;
|
|
9
|
+
const walker = new InstalledWalker(reader, root, builder);
|
|
10
|
+
await walker.walkFromImporters();
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
async function anyImporterHasModules(reader, root) {
|
|
14
|
+
const checks = await Promise.all(root.importerPaths.map((importerPath) => reader.exists(posix.join(importerPath, MODULES_DIRECTORY))));
|
|
15
|
+
return checks.some(Boolean);
|
|
16
|
+
}
|
|
17
|
+
function ancestorsOf(directory) {
|
|
18
|
+
const ancestors = [];
|
|
19
|
+
let current = directory;
|
|
20
|
+
while (current !== REPO_ROOT && current !== '') {
|
|
21
|
+
ancestors.push(current);
|
|
22
|
+
current = posix.dirname(current);
|
|
23
|
+
}
|
|
24
|
+
ancestors.push(REPO_ROOT);
|
|
25
|
+
return ancestors;
|
|
26
|
+
}
|
|
27
|
+
class InstalledWalker {
|
|
28
|
+
reader;
|
|
29
|
+
root;
|
|
30
|
+
builder;
|
|
31
|
+
importerKeyByDirectory = new Map();
|
|
32
|
+
nodeIdByDirectory = new Map();
|
|
33
|
+
resolutionCache = new Map();
|
|
34
|
+
directoryListings = new Map();
|
|
35
|
+
pending = [];
|
|
36
|
+
constructor(reader, root, builder) {
|
|
37
|
+
this.reader = reader;
|
|
38
|
+
this.root = root;
|
|
39
|
+
this.builder = builder;
|
|
40
|
+
}
|
|
41
|
+
async walkFromImporters() {
|
|
42
|
+
for (const importerPath of this.root.importerPaths) {
|
|
43
|
+
this.importerKeyByDirectory.set(importerPath, importerKeyOf(this.root, importerPath));
|
|
44
|
+
}
|
|
45
|
+
for (const importerPath of this.root.importerPaths)
|
|
46
|
+
await this.enqueueImporter(importerPath);
|
|
47
|
+
for (let dependent = this.pending.pop(); dependent; dependent = this.pending.pop()) {
|
|
48
|
+
await this.linkDependencies(dependent);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async enqueueImporter(importerPath) {
|
|
52
|
+
const importerKey = importerKeyOf(this.root, importerPath);
|
|
53
|
+
const read = await readManifest(this.reader, importerPath);
|
|
54
|
+
if ('problem' in read) {
|
|
55
|
+
this.builder.warn(read.problem);
|
|
56
|
+
this.builder.addImporter(importerKey, importerKey);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const id = this.builder.addImporter(importerKey, read.manifest.name ?? importerKey);
|
|
60
|
+
this.pending.push({ id, directory: importerPath, manifest: read.manifest, blocks: IMPORTER_BLOCKS });
|
|
61
|
+
}
|
|
62
|
+
async linkDependencies(dependent) {
|
|
63
|
+
for (const declared of declaredDependencies(dependent.manifest, dependent.blocks)) {
|
|
64
|
+
const target = await this.resolveTarget(dependent.directory, declared);
|
|
65
|
+
if (target !== undefined)
|
|
66
|
+
this.builder.addEdge(dependent.id, { to: target, kind: declared.kind, range: declared.range });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async resolveTarget(fromDirectory, declared) {
|
|
70
|
+
const resolution = await this.resolve(fromDirectory, declared.name);
|
|
71
|
+
if (resolution === null)
|
|
72
|
+
return this.unresolvedTarget(declared);
|
|
73
|
+
if ('warning' in resolution) {
|
|
74
|
+
this.builder.warn(resolution.warning);
|
|
75
|
+
return this.unresolvedTarget(declared);
|
|
76
|
+
}
|
|
77
|
+
const importerKey = this.importerKeyByDirectory.get(resolution.directory);
|
|
78
|
+
if (importerKey !== undefined)
|
|
79
|
+
return IMPORTER_PREFIX + importerKey;
|
|
80
|
+
return this.registerPackage(resolution.directory, declared);
|
|
81
|
+
}
|
|
82
|
+
unresolvedTarget(declared) {
|
|
83
|
+
return declared.kind === 'prod' ? `${declared.name}@${declared.range}` : undefined;
|
|
84
|
+
}
|
|
85
|
+
async registerPackage(directory, declared) {
|
|
86
|
+
const known = this.nodeIdByDirectory.get(directory);
|
|
87
|
+
if (known !== undefined)
|
|
88
|
+
return known;
|
|
89
|
+
const read = await readManifest(this.reader, directory);
|
|
90
|
+
if ('problem' in read) {
|
|
91
|
+
this.builder.warn(read.problem);
|
|
92
|
+
return this.unresolvedTarget(declared);
|
|
93
|
+
}
|
|
94
|
+
const name = read.manifest.name ?? declared.name;
|
|
95
|
+
const version = read.manifest.version ?? '';
|
|
96
|
+
const id = `${name}@${version}`;
|
|
97
|
+
this.builder.addPackage(id, name, version);
|
|
98
|
+
if (isNonRegistrySpec(declared.range))
|
|
99
|
+
this.builder.markNonRegistry(id);
|
|
100
|
+
this.nodeIdByDirectory.set(directory, id);
|
|
101
|
+
this.pending.push({ id, directory, manifest: read.manifest, blocks: PACKAGE_BLOCKS });
|
|
102
|
+
return id;
|
|
103
|
+
}
|
|
104
|
+
async resolve(fromDirectory, name) {
|
|
105
|
+
const cacheKey = `${fromDirectory}\0${name}`;
|
|
106
|
+
const cached = this.resolutionCache.get(cacheKey);
|
|
107
|
+
if (cached !== undefined)
|
|
108
|
+
return cached;
|
|
109
|
+
const resolution = await this.search(fromDirectory, name);
|
|
110
|
+
this.resolutionCache.set(cacheKey, resolution);
|
|
111
|
+
return resolution;
|
|
112
|
+
}
|
|
113
|
+
async search(fromDirectory, name) {
|
|
114
|
+
const realDirectory = await this.reader.realpath(fromDirectory);
|
|
115
|
+
if (realDirectory === null)
|
|
116
|
+
return { warning: `Could not resolve ${fromDirectory} inside the repository` };
|
|
117
|
+
for (const ancestor of ancestorsOf(realDirectory)) {
|
|
118
|
+
if (posix.basename(ancestor) === MODULES_DIRECTORY)
|
|
119
|
+
continue;
|
|
120
|
+
const modulesDirectory = posix.join(ancestor, MODULES_DIRECTORY);
|
|
121
|
+
if (!(await this.hasEntry(modulesDirectory, name)))
|
|
122
|
+
continue;
|
|
123
|
+
const candidate = posix.join(modulesDirectory, name);
|
|
124
|
+
const realCandidate = await this.reader.realpath(candidate);
|
|
125
|
+
if (realCandidate === null)
|
|
126
|
+
return { warning: `Skipped ${candidate}: broken link, looping link or link outside the repository` };
|
|
127
|
+
return { directory: realCandidate };
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
async hasEntry(modulesDirectory, name) {
|
|
132
|
+
const slash = name.indexOf('/');
|
|
133
|
+
if (slash === -1)
|
|
134
|
+
return (await this.listing(modulesDirectory)).has(name);
|
|
135
|
+
const scope = name.slice(0, slash);
|
|
136
|
+
if (!(await this.listing(modulesDirectory)).has(scope))
|
|
137
|
+
return false;
|
|
138
|
+
return (await this.listing(posix.join(modulesDirectory, scope))).has(name.slice(slash + 1));
|
|
139
|
+
}
|
|
140
|
+
listing(directory) {
|
|
141
|
+
let listing = this.directoryListings.get(directory);
|
|
142
|
+
if (listing === undefined) {
|
|
143
|
+
listing = this.reader.listDir(directory).then((entries) => new Set(entries.map((entry) => entry.name)));
|
|
144
|
+
this.directoryListings.set(directory, listing);
|
|
145
|
+
}
|
|
146
|
+
return listing;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { IMPORTER_PREFIX } from "../model.js";
|
|
2
|
+
const ROOT_IMPORTER_PATH = '.';
|
|
3
|
+
const ROOT_KEY = '';
|
|
4
|
+
const FIRST_SUPPORTED_VERSION = 2;
|
|
5
|
+
const LATEST_SUPPORTED_VERSION = 3;
|
|
6
|
+
const MODULES_DIRECTORY = 'node_modules/';
|
|
7
|
+
const NESTED_MODULES_DIRECTORY = '/node_modules/';
|
|
8
|
+
const OPTIONAL_KINDS = new Set(['optional', 'peer']);
|
|
9
|
+
const IMPORTER_BLOCKS = [
|
|
10
|
+
['dependencies', 'prod'],
|
|
11
|
+
['devDependencies', 'dev'],
|
|
12
|
+
['optionalDependencies', 'optional'],
|
|
13
|
+
['peerDependencies', 'peer'],
|
|
14
|
+
];
|
|
15
|
+
const PACKAGE_BLOCKS = [
|
|
16
|
+
['dependencies', 'prod'],
|
|
17
|
+
['optionalDependencies', 'optional'],
|
|
18
|
+
['peerDependencies', 'peer'],
|
|
19
|
+
];
|
|
20
|
+
export function parseNpmLock(text, builder) {
|
|
21
|
+
const document = recordOf(JSON.parse(text));
|
|
22
|
+
if (!isSupportedVersion(document.lockfileVersion)) {
|
|
23
|
+
builder.warn(`npm lockfile version ${String(document.lockfileVersion)} is not supported`);
|
|
24
|
+
return { supported: false };
|
|
25
|
+
}
|
|
26
|
+
if (isNewerThanSupported(document.lockfileVersion)) {
|
|
27
|
+
builder.warn(`npm lockfile version ${String(document.lockfileVersion)} is newer than supported; results may be incomplete`);
|
|
28
|
+
}
|
|
29
|
+
const { importers, packages, index } = readLockContents(recordOf(document.packages));
|
|
30
|
+
for (const importer of importers)
|
|
31
|
+
builder.addImporter(importer.path, importer.name);
|
|
32
|
+
for (const record of packages) {
|
|
33
|
+
builder.addPackage(record.id, record.name, record.version);
|
|
34
|
+
if (isNonRegistryResolution(record.entry.resolved))
|
|
35
|
+
builder.markNonRegistry(record.id);
|
|
36
|
+
}
|
|
37
|
+
for (const importer of importers)
|
|
38
|
+
addDependencyEdges(builder, index, importer, IMPORTER_BLOCKS);
|
|
39
|
+
for (const record of packages)
|
|
40
|
+
addDependencyEdges(builder, index, record, PACKAGE_BLOCKS);
|
|
41
|
+
return { supported: true };
|
|
42
|
+
}
|
|
43
|
+
function isNonRegistryResolution(resolved) {
|
|
44
|
+
return typeof resolved === 'string' && !resolved.startsWith('http://') && !resolved.startsWith('https://');
|
|
45
|
+
}
|
|
46
|
+
function isSupportedVersion(lockfileVersion) {
|
|
47
|
+
const version = Number.parseFloat(String(lockfileVersion));
|
|
48
|
+
return Number.isFinite(version) && version >= FIRST_SUPPORTED_VERSION;
|
|
49
|
+
}
|
|
50
|
+
function isNewerThanSupported(lockfileVersion) {
|
|
51
|
+
return Number.parseFloat(String(lockfileVersion)) > LATEST_SUPPORTED_VERSION;
|
|
52
|
+
}
|
|
53
|
+
function readLockContents(packagesBlock) {
|
|
54
|
+
const importers = [];
|
|
55
|
+
const packages = [];
|
|
56
|
+
const nodeIdByKey = new Map();
|
|
57
|
+
const importerKeys = new Set();
|
|
58
|
+
for (const [key, value] of Object.entries(packagesBlock)) {
|
|
59
|
+
const entry = recordOf(value);
|
|
60
|
+
if (!isInstallPath(key)) {
|
|
61
|
+
importers.push(toImporterRecord(key, entry));
|
|
62
|
+
importerKeys.add(key);
|
|
63
|
+
}
|
|
64
|
+
else if (entry.link === true) {
|
|
65
|
+
if (typeof entry.resolved === 'string')
|
|
66
|
+
nodeIdByKey.set(key, IMPORTER_PREFIX + entry.resolved);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const record = toPackageRecord(key, entry);
|
|
70
|
+
packages.push(record);
|
|
71
|
+
nodeIdByKey.set(key, record.id);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { importers, packages, index: { nodeIdByKey, importerKeys } };
|
|
75
|
+
}
|
|
76
|
+
function isInstallPath(key) {
|
|
77
|
+
return key.startsWith(MODULES_DIRECTORY) || key.includes(NESTED_MODULES_DIRECTORY);
|
|
78
|
+
}
|
|
79
|
+
function toImporterRecord(key, entry) {
|
|
80
|
+
const path = key === ROOT_KEY ? ROOT_IMPORTER_PATH : key;
|
|
81
|
+
return { id: IMPORTER_PREFIX + path, key, entry, path, name: typeof entry.name === 'string' ? entry.name : path };
|
|
82
|
+
}
|
|
83
|
+
function toPackageRecord(key, entry) {
|
|
84
|
+
const name = typeof entry.name === 'string' ? entry.name : nameFromInstallPath(key);
|
|
85
|
+
const version = typeof entry.version === 'string' ? entry.version : '';
|
|
86
|
+
return { id: `${name}@${version}`, key, entry, name, version };
|
|
87
|
+
}
|
|
88
|
+
function nameFromInstallPath(key) {
|
|
89
|
+
const nestedStart = key.lastIndexOf(NESTED_MODULES_DIRECTORY);
|
|
90
|
+
const nameStart = nestedStart === -1 ? MODULES_DIRECTORY.length : nestedStart + NESTED_MODULES_DIRECTORY.length;
|
|
91
|
+
return key.slice(nameStart);
|
|
92
|
+
}
|
|
93
|
+
function addDependencyEdges(builder, index, record, blocks) {
|
|
94
|
+
let lookupPrefixes;
|
|
95
|
+
for (const [blockName, kind] of blocks) {
|
|
96
|
+
const declared = record.entry[blockName];
|
|
97
|
+
if (!isRecord(declared))
|
|
98
|
+
continue;
|
|
99
|
+
lookupPrefixes ??= modulesPrefixesFrom(record.key, index.importerKeys);
|
|
100
|
+
for (const [name, range] of Object.entries(declared)) {
|
|
101
|
+
if (typeof range !== 'string')
|
|
102
|
+
continue;
|
|
103
|
+
const to = findInstalled(index.nodeIdByKey, lookupPrefixes, name) ?? unresolvedTarget(kind, name, range);
|
|
104
|
+
if (to !== undefined)
|
|
105
|
+
builder.addEdge(record.id, { to, kind, range });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function modulesPrefixesFrom(key, importerKeys) {
|
|
110
|
+
const prefixes = [];
|
|
111
|
+
for (let scope = key; scope !== ROOT_KEY; scope = parentScope(scope, importerKeys)) {
|
|
112
|
+
prefixes.push(scope + NESTED_MODULES_DIRECTORY);
|
|
113
|
+
}
|
|
114
|
+
prefixes.push(MODULES_DIRECTORY);
|
|
115
|
+
return prefixes;
|
|
116
|
+
}
|
|
117
|
+
function parentScope(scope, importerKeys) {
|
|
118
|
+
const nestedStart = scope.lastIndexOf(NESTED_MODULES_DIRECTORY);
|
|
119
|
+
if (nestedStart !== -1)
|
|
120
|
+
return scope.slice(0, nestedStart);
|
|
121
|
+
if (scope.startsWith(MODULES_DIRECTORY))
|
|
122
|
+
return ROOT_KEY;
|
|
123
|
+
return nearestEnclosingImporter(scope, importerKeys);
|
|
124
|
+
}
|
|
125
|
+
function nearestEnclosingImporter(path, importerKeys) {
|
|
126
|
+
for (let end = path.lastIndexOf('/'); end > 0; end = path.lastIndexOf('/', end - 1)) {
|
|
127
|
+
const candidate = path.slice(0, end);
|
|
128
|
+
if (importerKeys.has(candidate))
|
|
129
|
+
return candidate;
|
|
130
|
+
}
|
|
131
|
+
return ROOT_KEY;
|
|
132
|
+
}
|
|
133
|
+
function findInstalled(nodeIdByKey, prefixes, name) {
|
|
134
|
+
for (const prefix of prefixes) {
|
|
135
|
+
const nodeId = nodeIdByKey.get(prefix + name);
|
|
136
|
+
if (nodeId !== undefined)
|
|
137
|
+
return nodeId;
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
function unresolvedTarget(kind, name, range) {
|
|
142
|
+
return OPTIONAL_KINDS.has(kind) ? undefined : `${name}@${range}`;
|
|
143
|
+
}
|
|
144
|
+
function isRecord(value) {
|
|
145
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
146
|
+
}
|
|
147
|
+
function recordOf(value) {
|
|
148
|
+
return isRecord(value) ? value : {};
|
|
149
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { posix } from 'node:path';
|
|
2
|
+
const MANIFEST_FILE = 'package.json';
|
|
3
|
+
const NON_REGISTRY_PREFIXES = ['git+', 'git:', 'github:', 'gitlab:', 'bitbucket:', 'gist:', 'file:', 'link:', 'workspace:', 'http:', 'https:'];
|
|
4
|
+
const GITHUB_SHORTHAND_PATTERN = /^[\w.-]+\/[\w.-]+(#.+)?$/;
|
|
5
|
+
export const IMPORTER_BLOCKS = [
|
|
6
|
+
['dependencies', 'prod'],
|
|
7
|
+
['devDependencies', 'dev'],
|
|
8
|
+
['optionalDependencies', 'optional'],
|
|
9
|
+
['peerDependencies', 'peer'],
|
|
10
|
+
];
|
|
11
|
+
export const PACKAGE_BLOCKS = [
|
|
12
|
+
['dependencies', 'prod'],
|
|
13
|
+
['optionalDependencies', 'optional'],
|
|
14
|
+
['peerDependencies', 'peer'],
|
|
15
|
+
];
|
|
16
|
+
export async function readManifest(reader, directory) {
|
|
17
|
+
const manifestPath = posix.join(directory, MANIFEST_FILE);
|
|
18
|
+
const text = await reader.readText(manifestPath);
|
|
19
|
+
if (text === null)
|
|
20
|
+
return { problem: `Could not read ${manifestPath}` };
|
|
21
|
+
try {
|
|
22
|
+
const record = JSON.parse(text);
|
|
23
|
+
if (typeof record !== 'object' || record === null || Array.isArray(record)) {
|
|
24
|
+
return { problem: `${manifestPath} is not a JSON object` };
|
|
25
|
+
}
|
|
26
|
+
const fields = record;
|
|
27
|
+
return {
|
|
28
|
+
manifest: {
|
|
29
|
+
record: fields,
|
|
30
|
+
name: typeof fields.name === 'string' ? fields.name : undefined,
|
|
31
|
+
version: typeof fields.version === 'string' ? fields.version : undefined,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return { problem: `Could not parse ${manifestPath}` };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function declaredDependencies(manifest, blocks) {
|
|
40
|
+
const declared = [];
|
|
41
|
+
for (const [blockName, kind] of blocks) {
|
|
42
|
+
const block = manifest.record[blockName];
|
|
43
|
+
if (typeof block !== 'object' || block === null || Array.isArray(block))
|
|
44
|
+
continue;
|
|
45
|
+
for (const [name, range] of Object.entries(block)) {
|
|
46
|
+
if (typeof range === 'string')
|
|
47
|
+
declared.push({ name, range, kind });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return declared;
|
|
51
|
+
}
|
|
52
|
+
export function isNonRegistrySpec(spec) {
|
|
53
|
+
return NON_REGISTRY_PREFIXES.some((prefix) => spec.startsWith(prefix)) || GITHUB_SHORTHAND_PATTERN.test(spec);
|
|
54
|
+
}
|
|
55
|
+
export function importerKeyOf(root, importerPath) {
|
|
56
|
+
return posix.relative(root.path, importerPath) || '.';
|
|
57
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { posix } from 'node:path';
|
|
2
|
+
import { valid } from 'semver';
|
|
3
|
+
import { parse } from 'yaml';
|
|
4
|
+
import { IMPORTER_PREFIX } from "../model.js";
|
|
5
|
+
const ROOT_IMPORTER_PATH = '.';
|
|
6
|
+
const LINK_PREFIX = 'link:';
|
|
7
|
+
const FIRST_SUPPORTED_VERSION = 6;
|
|
8
|
+
const FIRST_SNAPSHOT_VERSION = 9;
|
|
9
|
+
const LATEST_SUPPORTED_MAJOR_VERSION = 9;
|
|
10
|
+
const ALIAS_PATTERN = /^(?:@[^/@]+\/)?[^/@(]+@/;
|
|
11
|
+
const NON_REGISTRY_RESOLUTION_TYPES = new Set(['git', 'directory']);
|
|
12
|
+
const DOCUMENT_START = '---\n';
|
|
13
|
+
const DOCUMENT_SEPARATOR = '\n---\n';
|
|
14
|
+
const SKIP_DUPLICATE_KEY_CHECK = { uniqueKeys: false };
|
|
15
|
+
const IMPORTER_BLOCKS = [
|
|
16
|
+
['dependencies', 'prod'],
|
|
17
|
+
['devDependencies', 'dev'],
|
|
18
|
+
['optionalDependencies', 'optional'],
|
|
19
|
+
];
|
|
20
|
+
const PACKAGE_BLOCKS = [
|
|
21
|
+
['dependencies', 'prod'],
|
|
22
|
+
['optionalDependencies', 'optional'],
|
|
23
|
+
];
|
|
24
|
+
const SNAPSHOT_LAYOUT = {
|
|
25
|
+
toNodeId(dependencyName, value) {
|
|
26
|
+
return ALIAS_PATTERN.test(value) ? value : `${dependencyName}@${value}`;
|
|
27
|
+
},
|
|
28
|
+
readPackageRecords(document) {
|
|
29
|
+
const metadata = recordOf(document.packages);
|
|
30
|
+
return Object.entries(recordOf(document.snapshots)).map(([id, entry]) => {
|
|
31
|
+
const packageMetadata = recordOf(metadata[withoutPeerSuffix(id)]);
|
|
32
|
+
return {
|
|
33
|
+
id,
|
|
34
|
+
identity: identityOf(id, packageMetadata),
|
|
35
|
+
entry: recordOf(entry),
|
|
36
|
+
peerRanges: recordOf(packageMetadata.peerDependencies),
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
const PACKAGES_LAYOUT = {
|
|
42
|
+
toNodeId(dependencyName, value, knownPackages) {
|
|
43
|
+
if (knownPackages.has(value))
|
|
44
|
+
return value;
|
|
45
|
+
const slashedValue = value.startsWith('/') ? value : `/${value}`;
|
|
46
|
+
if (knownPackages.has(slashedValue))
|
|
47
|
+
return slashedValue;
|
|
48
|
+
if (value.startsWith('/') || ALIAS_PATTERN.test(value))
|
|
49
|
+
return slashedValue;
|
|
50
|
+
return `/${dependencyName}@${value}`;
|
|
51
|
+
},
|
|
52
|
+
readPackageRecords(document) {
|
|
53
|
+
return Object.entries(recordOf(document.packages)).map(([id, entry]) => {
|
|
54
|
+
const record = recordOf(entry);
|
|
55
|
+
return { id, identity: identityOf(id, record), entry: record, peerRanges: recordOf(record.peerDependencies) };
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
export function parsePnpmLock(text, builder) {
|
|
60
|
+
const document = recordOf(parse(extractMainDocument(text), SKIP_DUPLICATE_KEY_CHECK));
|
|
61
|
+
const layout = selectLayout(document.lockfileVersion);
|
|
62
|
+
if (!layout) {
|
|
63
|
+
builder.warn(`pnpm lockfile version ${String(document.lockfileVersion)} is not supported`);
|
|
64
|
+
return { supported: false };
|
|
65
|
+
}
|
|
66
|
+
if (isNewerThanSupported(document.lockfileVersion)) {
|
|
67
|
+
builder.warn(`pnpm lockfile version ${String(document.lockfileVersion)} is newer than supported; results may be incomplete`);
|
|
68
|
+
}
|
|
69
|
+
const importerSections = readImporterSections(document);
|
|
70
|
+
const packageRecords = layout.readPackageRecords(document);
|
|
71
|
+
const resolution = {
|
|
72
|
+
layout,
|
|
73
|
+
knownPackages: new Map(packageRecords.map((record) => [record.id, record.identity])),
|
|
74
|
+
};
|
|
75
|
+
for (const importerPath of Object.keys(importerSections))
|
|
76
|
+
builder.addImporter(importerPath, importerPath);
|
|
77
|
+
for (const record of packageRecords) {
|
|
78
|
+
builder.addPackage(record.id, record.identity.name, record.identity.version, record.identity.peerSuffix);
|
|
79
|
+
if (isNonRegistryRecord(record))
|
|
80
|
+
builder.markNonRegistry(record.id);
|
|
81
|
+
}
|
|
82
|
+
for (const [importerPath, section] of Object.entries(importerSections)) {
|
|
83
|
+
addImporterEdges(builder, resolution, importerPath, recordOf(section));
|
|
84
|
+
}
|
|
85
|
+
for (const record of packageRecords)
|
|
86
|
+
addPackageEdges(builder, resolution, record);
|
|
87
|
+
return { supported: true };
|
|
88
|
+
}
|
|
89
|
+
function isNonRegistryRecord(record) {
|
|
90
|
+
const resolutionType = recordOf(record.entry.resolution).type;
|
|
91
|
+
return valid(parsePackageKey(record.id).version) === null || NON_REGISTRY_RESOLUTION_TYPES.has(resolutionType);
|
|
92
|
+
}
|
|
93
|
+
function extractMainDocument(text) {
|
|
94
|
+
const normalized = text.replace(/\r\n/g, '\n');
|
|
95
|
+
if (!normalized.startsWith(DOCUMENT_START))
|
|
96
|
+
return normalized;
|
|
97
|
+
const separatorStart = normalized.indexOf(DOCUMENT_SEPARATOR);
|
|
98
|
+
return separatorStart === -1 ? normalized : normalized.slice(separatorStart + DOCUMENT_SEPARATOR.length);
|
|
99
|
+
}
|
|
100
|
+
function selectLayout(lockfileVersion) {
|
|
101
|
+
const version = Number.parseFloat(String(lockfileVersion));
|
|
102
|
+
if (!Number.isFinite(version) || version < FIRST_SUPPORTED_VERSION)
|
|
103
|
+
return undefined;
|
|
104
|
+
return version >= FIRST_SNAPSHOT_VERSION ? SNAPSHOT_LAYOUT : PACKAGES_LAYOUT;
|
|
105
|
+
}
|
|
106
|
+
function isNewerThanSupported(lockfileVersion) {
|
|
107
|
+
return Math.trunc(Number.parseFloat(String(lockfileVersion))) > LATEST_SUPPORTED_MAJOR_VERSION;
|
|
108
|
+
}
|
|
109
|
+
function readImporterSections(document) {
|
|
110
|
+
return isRecord(document.importers) ? document.importers : { [ROOT_IMPORTER_PATH]: document };
|
|
111
|
+
}
|
|
112
|
+
function addImporterEdges(builder, resolution, importerPath, section) {
|
|
113
|
+
const from = IMPORTER_PREFIX + importerPath;
|
|
114
|
+
for (const [blockName, kind] of IMPORTER_BLOCKS) {
|
|
115
|
+
for (const [name, dependency] of Object.entries(recordOf(section[blockName]))) {
|
|
116
|
+
const { specifier, version } = recordOf(dependency);
|
|
117
|
+
if (typeof version !== 'string')
|
|
118
|
+
continue;
|
|
119
|
+
const to = resolveTargetId(resolution, name, version, importerPath);
|
|
120
|
+
builder.addEdge(from, { to, kind, range: typeof specifier === 'string' ? specifier : version });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function addPackageEdges(builder, resolution, record) {
|
|
125
|
+
for (const [blockName, blockKind] of PACKAGE_BLOCKS) {
|
|
126
|
+
for (const [name, value] of Object.entries(recordOf(record.entry[blockName]))) {
|
|
127
|
+
if (typeof value !== 'string')
|
|
128
|
+
continue;
|
|
129
|
+
builder.addEdge(record.id, toPackageEdge(resolution, record.peerRanges, blockKind, name, value));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function toPackageEdge(resolution, peerRanges, blockKind, name, value) {
|
|
134
|
+
const to = resolveTargetId(resolution, name, value, ROOT_IMPORTER_PATH);
|
|
135
|
+
const peerRange = peerRanges[name];
|
|
136
|
+
if (typeof peerRange === 'string')
|
|
137
|
+
return { to, kind: 'peer', range: peerRange };
|
|
138
|
+
return { to, kind: blockKind, range: resolvedVersionOf(resolution, to, value) };
|
|
139
|
+
}
|
|
140
|
+
function resolvedVersionOf(resolution, targetId, value) {
|
|
141
|
+
if (targetId.startsWith(IMPORTER_PREFIX))
|
|
142
|
+
return value;
|
|
143
|
+
return (resolution.knownPackages.get(targetId) ?? parsePackageKey(targetId)).version;
|
|
144
|
+
}
|
|
145
|
+
function resolveTargetId(resolution, name, value, ownerPath) {
|
|
146
|
+
if (!value.startsWith(LINK_PREFIX))
|
|
147
|
+
return resolution.layout.toNodeId(name, value, resolution.knownPackages);
|
|
148
|
+
return IMPORTER_PREFIX + posix.join(ownerPath, value.slice(LINK_PREFIX.length));
|
|
149
|
+
}
|
|
150
|
+
function identityOf(key, declared) {
|
|
151
|
+
const parsed = parsePackageKey(key);
|
|
152
|
+
return {
|
|
153
|
+
...parsed,
|
|
154
|
+
name: typeof declared.name === 'string' ? declared.name : parsed.name,
|
|
155
|
+
version: typeof declared.version === 'string' ? declared.version : parsed.version,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function parsePackageKey(key) {
|
|
159
|
+
const unprefixed = key.startsWith('/') ? key.slice(1) : key;
|
|
160
|
+
const head = withoutPeerSuffix(unprefixed);
|
|
161
|
+
const versionSeparator = head.indexOf('@', 1);
|
|
162
|
+
const name = versionSeparator === -1 ? head : head.slice(0, versionSeparator);
|
|
163
|
+
const version = versionSeparator === -1 ? '' : head.slice(versionSeparator + 1);
|
|
164
|
+
if (head.length === unprefixed.length)
|
|
165
|
+
return { name, version };
|
|
166
|
+
return { name, version, peerSuffix: unprefixed.slice(head.length) };
|
|
167
|
+
}
|
|
168
|
+
function withoutPeerSuffix(key) {
|
|
169
|
+
const suffixStart = key.indexOf('(');
|
|
170
|
+
return suffixStart === -1 ? key : key.slice(0, suffixStart);
|
|
171
|
+
}
|
|
172
|
+
function isRecord(value) {
|
|
173
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
174
|
+
}
|
|
175
|
+
function recordOf(value) {
|
|
176
|
+
return isRecord(value) ? value : {};
|
|
177
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import path, { posix } from 'node:path';
|
|
4
|
+
import { processSecrets } from "../config/secrets.js";
|
|
5
|
+
const STDERR_TAIL_LENGTH = 400;
|
|
6
|
+
const activeTempDirs = new Set();
|
|
7
|
+
const INSTALL_COMMANDS = {
|
|
8
|
+
pnpm: { args: ['install', '--lockfile-only', '--ignore-scripts'], lockfile: 'pnpm-lock.yaml' },
|
|
9
|
+
npm: { args: ['install', '--package-lock-only', '--ignore-scripts'], lockfile: 'package-lock.json' },
|
|
10
|
+
};
|
|
11
|
+
function describeFailure(command, args, detail) {
|
|
12
|
+
const text = `${command} ${args.join(' ')} failed`;
|
|
13
|
+
return detail ? `${text}: ${detail}` : text;
|
|
14
|
+
}
|
|
15
|
+
function stderrTail(stderr) {
|
|
16
|
+
return processSecrets.redact(stderr).trim().slice(-STDERR_TAIL_LENGTH);
|
|
17
|
+
}
|
|
18
|
+
async function copyFromReader(reader, repoPath, tempDir, rootRelativePath) {
|
|
19
|
+
const text = await reader.readText(repoPath);
|
|
20
|
+
if (text === null)
|
|
21
|
+
return;
|
|
22
|
+
const target = path.join(tempDir, ...rootRelativePath.split('/'));
|
|
23
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
24
|
+
await writeFile(target, text);
|
|
25
|
+
}
|
|
26
|
+
async function stageRoot(reader, root, tempDir) {
|
|
27
|
+
const manifestPaths = root.importerPaths.map((importerPath) => {
|
|
28
|
+
const rootRelative = posix.relative(root.path, importerPath) || '.';
|
|
29
|
+
return {
|
|
30
|
+
repoPath: posix.join(importerPath, 'package.json'),
|
|
31
|
+
rootRelativePath: posix.join(rootRelative, 'package.json'),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
const rootFiles = ['.npmrc', 'pnpm-workspace.yaml'].map((name) => ({
|
|
35
|
+
repoPath: posix.join(root.path, name),
|
|
36
|
+
rootRelativePath: name,
|
|
37
|
+
}));
|
|
38
|
+
for (const file of [...manifestPaths, ...rootFiles]) {
|
|
39
|
+
await copyFromReader(reader, file.repoPath, tempDir, file.rootRelativePath);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function pnpmIsAvailable(runner, cwd) {
|
|
43
|
+
try {
|
|
44
|
+
return (await runner('pnpm', ['--version'], cwd)).code === 0;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function chooseManager(root, runner, cwd) {
|
|
51
|
+
if (root.manager === 'pnpm')
|
|
52
|
+
return 'pnpm';
|
|
53
|
+
if (root.manager === 'npm')
|
|
54
|
+
return 'npm';
|
|
55
|
+
return (await pnpmIsAvailable(runner, cwd)) ? 'pnpm' : 'npm';
|
|
56
|
+
}
|
|
57
|
+
async function runInstall(manager, runner, tempDir) {
|
|
58
|
+
const { args, lockfile } = INSTALL_COMMANDS[manager];
|
|
59
|
+
try {
|
|
60
|
+
const result = await runner(manager, args, tempDir);
|
|
61
|
+
if (result.code !== 0)
|
|
62
|
+
return { error: describeFailure(manager, args, stderrTail(result.stderr)) };
|
|
63
|
+
}
|
|
64
|
+
catch (failure) {
|
|
65
|
+
const code = failure.code;
|
|
66
|
+
return { error: describeFailure(manager, args, code === 'ENOENT' ? `${manager} was not found` : 'could not be started') };
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
return { manager, lockfileText: await readFile(path.join(tempDir, lockfile), 'utf8') };
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return { error: describeFailure(manager, args, `${lockfile} was not produced`) };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function removeTempDir(tempDir) {
|
|
76
|
+
activeTempDirs.delete(tempDir);
|
|
77
|
+
return rm(tempDir, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
export async function removeResolveTempDirs() {
|
|
80
|
+
await Promise.all([...activeTempDirs].map(removeTempDir));
|
|
81
|
+
}
|
|
82
|
+
export async function resolveToday(reader, root, runner) {
|
|
83
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), 'deptrail-'));
|
|
84
|
+
activeTempDirs.add(tempDir);
|
|
85
|
+
try {
|
|
86
|
+
await stageRoot(reader, root, tempDir);
|
|
87
|
+
const manager = await chooseManager(root, runner, tempDir);
|
|
88
|
+
return await runInstall(manager, runner, tempDir);
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
await removeTempDir(tempDir);
|
|
92
|
+
}
|
|
93
|
+
}
|