@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.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +421 -0
  3. package/dist/actions/action-error.js +11 -0
  4. package/dist/actions/action-jobs.js +199 -0
  5. package/dist/actions/action-service.js +268 -0
  6. package/dist/actions/apply.js +120 -0
  7. package/dist/actions/availability.js +79 -0
  8. package/dist/actions/change-sets.js +117 -0
  9. package/dist/actions/dry-run.js +287 -0
  10. package/dist/actions/edge-moves.js +108 -0
  11. package/dist/actions/edit-plan.js +179 -0
  12. package/dist/actions/file-scope.js +118 -0
  13. package/dist/actions/fix-options.js +141 -0
  14. package/dist/actions/goal-check.js +80 -0
  15. package/dist/actions/install.js +27 -0
  16. package/dist/actions/job-registry.js +104 -0
  17. package/dist/actions/json-edit.js +189 -0
  18. package/dist/actions/labels.js +35 -0
  19. package/dist/actions/node-actions.js +102 -0
  20. package/dist/actions/overrides-location.js +59 -0
  21. package/dist/actions/parent-chain.js +210 -0
  22. package/dist/actions/range-style.js +38 -0
  23. package/dist/actions/recipes.js +31 -0
  24. package/dist/actions/root-files.js +30 -0
  25. package/dist/actions/sandbox.js +175 -0
  26. package/dist/actions/set-range.js +59 -0
  27. package/dist/actions/shared-packages.js +50 -0
  28. package/dist/actions/text-diff.js +203 -0
  29. package/dist/actions/types.js +1 -0
  30. package/dist/actions/update-options.js +232 -0
  31. package/dist/actions/workspace-edit.js +33 -0
  32. package/dist/adapters/command-runner.js +127 -0
  33. package/dist/adapters/manifest-only.js +20 -0
  34. package/dist/adapters/node-modules.js +148 -0
  35. package/dist/adapters/npm-lock.js +149 -0
  36. package/dist/adapters/package-manifest.js +57 -0
  37. package/dist/adapters/pnpm-lock.js +177 -0
  38. package/dist/adapters/resolve-today.js +93 -0
  39. package/dist/cli/cli.js +227 -0
  40. package/dist/cli/open-browser.js +14 -0
  41. package/dist/cli/package-version.js +6 -0
  42. package/dist/cli/usage-text.js +27 -0
  43. package/dist/config/glob.js +119 -0
  44. package/dist/config/npmrc.js +116 -0
  45. package/dist/config/root-config.js +103 -0
  46. package/dist/config/secrets.js +18 -0
  47. package/dist/discovery/find-roots.js +107 -0
  48. package/dist/enrich/credentials.js +143 -0
  49. package/dist/enrich/enrichment-cache.js +42 -0
  50. package/dist/enrich/enrichment-keys.js +71 -0
  51. package/dist/enrich/enrichment-service.js +399 -0
  52. package/dist/enrich/enrichment-status.js +86 -0
  53. package/dist/enrich/enrichment-store.js +143 -0
  54. package/dist/enrich/http.js +201 -0
  55. package/dist/enrich/on-demand.js +220 -0
  56. package/dist/enrich/osv-client.js +260 -0
  57. package/dist/enrich/registry-client.js +123 -0
  58. package/dist/enrich/types.js +1 -0
  59. package/dist/git/blob-reader.js +180 -0
  60. package/dist/git/git-commands.js +32 -0
  61. package/dist/git/git-process.js +71 -0
  62. package/dist/git/git-repository.js +136 -0
  63. package/dist/git/ref-list.js +78 -0
  64. package/dist/git/tree-index.js +87 -0
  65. package/dist/graph/global-index.js +19 -0
  66. package/dist/graph/root-graph-builder.js +114 -0
  67. package/dist/graph/serialize.js +19 -0
  68. package/dist/model.js +2 -0
  69. package/dist/query/children.js +38 -0
  70. package/dist/query/compare-shape.js +13 -0
  71. package/dist/query/compare.js +245 -0
  72. package/dist/query/decorations.js +121 -0
  73. package/dist/query/dependents.js +135 -0
  74. package/dist/query/duplicates.js +23 -0
  75. package/dist/query/enrichment-lookup.js +78 -0
  76. package/dist/query/fixes.js +60 -0
  77. package/dist/query/graph-diff.js +42 -0
  78. package/dist/query/issues.js +235 -0
  79. package/dist/query/lookup.js +49 -0
  80. package/dist/query/node-fix.js +46 -0
  81. package/dist/query/package-insight.js +112 -0
  82. package/dist/query/search.js +61 -0
  83. package/dist/query/subgraph.js +107 -0
  84. package/dist/query/versions.js +103 -0
  85. package/dist/scan/build-root.js +137 -0
  86. package/dist/scan/cache-shape.js +52 -0
  87. package/dist/scan/scan-cache.js +32 -0
  88. package/dist/scan/scan-session.js +127 -0
  89. package/dist/scan/source-registry.js +55 -0
  90. package/dist/server/auth.js +45 -0
  91. package/dist/server/branches.js +105 -0
  92. package/dist/server/error-log.js +9 -0
  93. package/dist/server/root-segment.js +13 -0
  94. package/dist/server/router.js +474 -0
  95. package/dist/server/server.js +166 -0
  96. package/dist/server/source-key.js +1 -0
  97. package/dist/server/static.js +59 -0
  98. package/dist/sources/file-reader.js +55 -0
  99. package/dist/sources/git-reader.js +45 -0
  100. package/dist/ui/assets/index-C4uyB5v3.css +1 -0
  101. package/dist/ui/assets/index-DYAG2LM-.js +51 -0
  102. package/dist/ui/index.html +15 -0
  103. package/package.json +72 -0
@@ -0,0 +1,112 @@
1
+ import semver from 'semver';
2
+ import { IMPORTER_PREFIX, PLACEHOLDER_PREFIX } from "../model.js";
3
+ import { advisoriesOfMatch, nodeAdvisories, nodeMeta, worstSeverity } from "./enrichment-lookup.js";
4
+ import { requireNode } from "./lookup.js";
5
+ import { edgeFixes, nodeFix, normalizedRequestedRange } from "./node-fix.js";
6
+ import { outdatedLevel, wantedVersion } from "./versions.js";
7
+ export const REQUESTED_CAP = 50;
8
+ function insightAdvisory(advisory) {
9
+ const item = {
10
+ id: advisory.id,
11
+ aliases: advisory.aliases,
12
+ summary: advisory.summary,
13
+ severity: advisory.severity,
14
+ malicious: advisory.malicious,
15
+ url: advisory.url,
16
+ fixedIn: advisory.fixedIn,
17
+ };
18
+ if (advisory.cvss !== undefined)
19
+ item.cvss = advisory.cvss;
20
+ return item;
21
+ }
22
+ function registrySection(registry, node) {
23
+ if (registry.state === 'failed')
24
+ return { state: 'failed', message: registry.reason };
25
+ if (registry.state === 'not-found')
26
+ return { state: 'not-found', fetchedAt: registry.fetchedAt };
27
+ if (registry.state !== 'ok' && registry.state !== 'stale')
28
+ return { state: registry.state };
29
+ const { meta } = registry;
30
+ const section = { state: registry.state, fetchedAt: registry.fetchedAt };
31
+ const latest = meta.distTags.latest;
32
+ if (latest !== undefined)
33
+ section.latest = latest;
34
+ const level = outdatedLevel(node.version, latest);
35
+ if (level !== undefined)
36
+ section.outdatedLevel = level;
37
+ const deprecated = meta.deprecated[node.version];
38
+ if (deprecated !== undefined)
39
+ section.deprecated = deprecated;
40
+ if (semver.valid(node.version) !== null && !meta.versions.includes(node.version))
41
+ section.versionMissing = true;
42
+ return section;
43
+ }
44
+ function vulnerabilitiesSection(match, advisories) {
45
+ if (match.state === 'ok' || match.state === 'stale') {
46
+ return { state: match.state, fetchedAt: match.fetchedAt, items: advisories.map(insightAdvisory) };
47
+ }
48
+ if (match.state === 'skipped')
49
+ return { state: 'skipped', reason: match.reason, items: [] };
50
+ if (match.state === 'failed')
51
+ return { state: 'failed', message: match.reason, items: [] };
52
+ return { state: match.state, items: [] };
53
+ }
54
+ function requestedSection(view, root, node, kinds, vulnerable) {
55
+ const meta = nodeMeta(view, root, node);
56
+ const all = edgeFixes(view, root, node, kinds);
57
+ const requested = all.slice(0, REQUESTED_CAP).map(({ parent, range, fix }) => {
58
+ const row = { parent, range: range.range, rangeSource: range.source };
59
+ const normalizedRange = normalizedRequestedRange(root, node, meta, range);
60
+ if (normalizedRange !== undefined)
61
+ row.normalizedRange = normalizedRange;
62
+ const wanted = meta ? wantedVersion(meta.versions, normalizedRange) : undefined;
63
+ if (wanted !== undefined)
64
+ row.wanted = wanted;
65
+ if (vulnerable) {
66
+ row.fix = fix.kind;
67
+ if (fix.version !== undefined)
68
+ row.fixVersion = fix.version;
69
+ }
70
+ return row;
71
+ });
72
+ return { requested, requestedTruncated: all.length - requested.length };
73
+ }
74
+ function versionSeverities(view, node, monolithVersions) {
75
+ const result = {};
76
+ for (const version of monolithVersions) {
77
+ if (version === node.version)
78
+ continue;
79
+ const advisories = advisoriesOfMatch(view, node.name, view.matchOfVersion(node.name, version));
80
+ const worst = worstSeverity(advisories.map((advisory) => advisory.severity));
81
+ if (worst !== undefined)
82
+ result[version] = worst;
83
+ }
84
+ return result;
85
+ }
86
+ export function packageInsight(view, root, nodeId, kinds, monolithVersions) {
87
+ const found = requireNode(root, nodeId);
88
+ if (found.id.startsWith(IMPORTER_PREFIX) || found.id.startsWith(PLACEHOLDER_PREFIX)) {
89
+ return {
90
+ revision: view.revision,
91
+ registry: { state: 'not-applicable' },
92
+ requested: [],
93
+ requestedTruncated: 0,
94
+ vulnerabilities: { state: 'not-applicable', items: [] },
95
+ versionSeverities: {},
96
+ };
97
+ }
98
+ const node = found;
99
+ const advisories = nodeAdvisories(view, root, node);
100
+ const vulnerable = advisories.length > 0;
101
+ const insight = {
102
+ revision: view.revision,
103
+ registry: registrySection(view.registryOf(root, node), node),
104
+ ...requestedSection(view, root, node, kinds, vulnerable),
105
+ vulnerabilities: vulnerabilitiesSection(view.matchOf(root, node), advisories),
106
+ versionSeverities: versionSeverities(view, node, monolithVersions),
107
+ };
108
+ const fix = nodeFix(view, root, node, kinds);
109
+ if (fix !== undefined)
110
+ insight.fix = fix;
111
+ return insight;
112
+ }
@@ -0,0 +1,61 @@
1
+ import semver from 'semver';
2
+ const DEFAULT_MAX_HITS = 50;
3
+ function compareText(left, right) {
4
+ return left < right ? -1 : left > right ? 1 : 0;
5
+ }
6
+ function compareVersions(left, right) {
7
+ const leftValid = semver.valid(left) !== null;
8
+ const rightValid = semver.valid(right) !== null;
9
+ if (leftValid && rightValid)
10
+ return semver.rcompare(left, right);
11
+ if (leftValid)
12
+ return -1;
13
+ if (rightValid)
14
+ return 1;
15
+ return compareText(left, right);
16
+ }
17
+ function parseQuery(query) {
18
+ const separator = query.lastIndexOf('@');
19
+ if (separator <= 0)
20
+ return { term: query };
21
+ return { term: query.slice(0, separator), range: query.slice(separator + 1) };
22
+ }
23
+ function groupByVersion(entries) {
24
+ const byVersion = new Map();
25
+ for (const entry of entries) {
26
+ const roots = byVersion.get(entry.version) ?? [];
27
+ roots.push({ rootId: entry.rootId, nodeId: entry.nodeId });
28
+ byVersion.set(entry.version, roots);
29
+ }
30
+ return [...byVersion]
31
+ .sort(([left], [right]) => compareVersions(left, right))
32
+ .map(([version, roots]) => ({
33
+ version,
34
+ roots: roots.sort((left, right) => compareText(left.rootId, right.rootId) || compareText(left.nodeId, right.nodeId)),
35
+ }));
36
+ }
37
+ function satisfiesRange(version, range) {
38
+ return semver.valid(version) !== null && semver.satisfies(version, range, { includePrerelease: true });
39
+ }
40
+ export function search(index, query, max = DEFAULT_MAX_HITS) {
41
+ const trimmed = query.trim();
42
+ if (trimmed === '')
43
+ return [];
44
+ const { term, range } = parseQuery(trimmed);
45
+ const needle = term.toLowerCase();
46
+ if (range !== undefined && semver.validRange(range) === null)
47
+ return [];
48
+ const hits = [];
49
+ for (const [name, entries] of index) {
50
+ const lowerName = name.toLowerCase();
51
+ if (range === undefined ? !lowerName.includes(needle) : lowerName !== needle)
52
+ continue;
53
+ const matching = range === undefined ? entries : entries.filter((entry) => satisfiesRange(entry.version, range));
54
+ if (matching.length === 0)
55
+ continue;
56
+ hits.push({ name, versions: groupByVersion(matching) });
57
+ }
58
+ const exactFirst = (hit) => (hit.name.toLowerCase() === needle ? 0 : 1);
59
+ hits.sort((left, right) => exactFirst(left) - exactFirst(right) || left.name.length - right.name.length || compareText(left.name, right.name));
60
+ return hits.slice(0, max);
61
+ }
@@ -0,0 +1,107 @@
1
+ import { ParentLinkResolver } from "./dependents.js";
2
+ import { passingEdges, requireNode, viewOf } from "./lookup.js";
3
+ class SubgraphWalk {
4
+ root;
5
+ options;
6
+ links;
7
+ visited = new Map();
8
+ queue = [];
9
+ truncated = false;
10
+ constructor(root, options) {
11
+ this.root = root;
12
+ this.options = options;
13
+ this.links = new ParentLinkResolver(root, options.kinds);
14
+ }
15
+ run(focus) {
16
+ this.visit(focus, 0);
17
+ for (let cursor = 0; cursor < this.queue.length; cursor++) {
18
+ const { node, layer } = this.queue[cursor];
19
+ if (this.canExpandUp(layer)) {
20
+ for (const parentId of this.parentIds(node))
21
+ this.visit(requireNode(this.root, parentId), layer - 1, true);
22
+ }
23
+ if (this.canExpandDown(layer)) {
24
+ for (const childId of this.childIds(node))
25
+ this.visit(requireNode(this.root, childId), layer + 1, true);
26
+ }
27
+ }
28
+ return this.queue;
29
+ }
30
+ has(id) {
31
+ return this.visited.has(id);
32
+ }
33
+ parentIds(node) {
34
+ return [...new Set(this.links.of(node).map((link) => link.id))];
35
+ }
36
+ childIds(node) {
37
+ return [...new Set(passingEdges(node, this.options.kinds).map((edge) => edge.to))];
38
+ }
39
+ canExpandUp(layer) {
40
+ return layer <= 0 && layer > -this.options.up;
41
+ }
42
+ canExpandDown(layer) {
43
+ return layer >= 0 && layer < this.options.down;
44
+ }
45
+ visit(node, layer, enforceLimit = false) {
46
+ if (this.visited.has(node.id))
47
+ return;
48
+ if (enforceLimit && this.queue.length >= this.options.limit) {
49
+ this.truncated = true;
50
+ return;
51
+ }
52
+ const visit = { node, layer };
53
+ this.visited.set(node.id, visit);
54
+ this.queue.push(visit);
55
+ }
56
+ }
57
+ function collectEdges(walk, visits, kinds) {
58
+ const edges = [];
59
+ const seen = new Set();
60
+ for (const { node } of visits) {
61
+ for (const edge of passingEdges(node, kinds)) {
62
+ if (!walk.has(edge.to))
63
+ continue;
64
+ const key = `${node.id}\u0000${edge.to}\u0000${edge.kind}`;
65
+ if (seen.has(key))
66
+ continue;
67
+ seen.add(key);
68
+ edges.push({ from: node.id, to: edge.to, kind: edge.kind });
69
+ }
70
+ }
71
+ return edges;
72
+ }
73
+ function countHidden(walk, ids) {
74
+ let hidden = 0;
75
+ for (const id of ids)
76
+ if (!walk.has(id))
77
+ hidden++;
78
+ return hidden;
79
+ }
80
+ function collectStubs(walk, visits) {
81
+ const stubs = [];
82
+ for (const { node, layer } of visits) {
83
+ if (layer <= 0) {
84
+ const hidden = countHidden(walk, walk.parentIds(node));
85
+ if (hidden > 0)
86
+ stubs.push({ at: node.id, side: 'up', hidden });
87
+ }
88
+ if (layer >= 0) {
89
+ const hidden = countHidden(walk, walk.childIds(node));
90
+ if (hidden > 0)
91
+ stubs.push({ at: node.id, side: 'down', hidden });
92
+ }
93
+ }
94
+ return stubs;
95
+ }
96
+ export function subgraph(root, id, options) {
97
+ const focus = requireNode(root, id);
98
+ const walk = new SubgraphWalk(root, options);
99
+ const visits = walk.run(focus);
100
+ return {
101
+ focus: id,
102
+ nodes: visits.map(({ node, layer }) => ({ node: viewOf(node), layer })),
103
+ edges: collectEdges(walk, visits, options.kinds),
104
+ stubs: collectStubs(walk, visits),
105
+ truncated: walk.truncated,
106
+ };
107
+ }
@@ -0,0 +1,103 @@
1
+ import semver from 'semver';
2
+ import { isNonRegistrySpec } from "../adapters/package-manifest.js";
3
+ const LOCAL_SPEC_PREFIXES = ['workspace:', 'link:', 'file:'];
4
+ const NPM_ALIAS_PREFIX = 'npm:';
5
+ const CATALOG_PREFIX = 'catalog:';
6
+ const DEFAULT_CATALOG = 'default';
7
+ const LATEST_TAG = 'latest';
8
+ function resolveCatalogRange(range, context) {
9
+ const catalogName = range.slice(CATALOG_PREFIX.length) || DEFAULT_CATALOG;
10
+ const resolved = context.catalogs[catalogName]?.[context.name];
11
+ if (resolved === undefined || resolved.startsWith(CATALOG_PREFIX))
12
+ return undefined;
13
+ return normalizeRange(resolved, context);
14
+ }
15
+ function resolveAliasRange(range, context) {
16
+ const versionSeparator = range.lastIndexOf('@');
17
+ const aliased = versionSeparator > NPM_ALIAS_PREFIX.length ? range.slice(versionSeparator + 1) : LATEST_TAG;
18
+ if (aliased.startsWith(NPM_ALIAS_PREFIX))
19
+ return undefined;
20
+ return normalizeRange(aliased, context);
21
+ }
22
+ export function normalizeRange(range, context) {
23
+ if (LOCAL_SPEC_PREFIXES.some((prefix) => range.startsWith(prefix)) || isNonRegistrySpec(range))
24
+ return undefined;
25
+ if (range.startsWith(CATALOG_PREFIX))
26
+ return resolveCatalogRange(range, context);
27
+ if (range.startsWith(NPM_ALIAS_PREFIX))
28
+ return resolveAliasRange(range, context);
29
+ if (semver.validRange(range) !== null)
30
+ return range;
31
+ const tagged = context.distTags?.[range];
32
+ return tagged !== undefined && semver.valid(tagged) !== null ? tagged : undefined;
33
+ }
34
+ export function wantedVersion(versions, range) {
35
+ if (range === undefined || semver.validRange(range) === null)
36
+ return undefined;
37
+ return semver.maxSatisfying(versions.filter((version) => semver.valid(version) !== null), range) ?? undefined;
38
+ }
39
+ export function outdatedLevel(current, latest) {
40
+ if (latest === undefined || semver.valid(current) === null || semver.valid(latest) === null)
41
+ return undefined;
42
+ if (semver.prerelease(current) !== null || !semver.gt(latest, current))
43
+ return undefined;
44
+ const difference = semver.diff(current, latest);
45
+ if (difference === 'major' || difference === 'premajor')
46
+ return 'major';
47
+ if (difference === 'minor' || difference === 'preminor')
48
+ return 'minor';
49
+ return 'patch';
50
+ }
51
+ function eventVersion(event) {
52
+ if ('introduced' in event)
53
+ return event.introduced;
54
+ if ('fixed' in event)
55
+ return event.fixed;
56
+ return event.last_affected;
57
+ }
58
+ function isUsableEvent(event) {
59
+ const version = eventVersion(event);
60
+ if (typeof version !== 'string')
61
+ return false;
62
+ if ('introduced' in event && version === '0')
63
+ return true;
64
+ return semver.valid(version) !== null;
65
+ }
66
+ function compareEventVersions(left, right) {
67
+ if (left === right)
68
+ return 0;
69
+ if (left === '0')
70
+ return -1;
71
+ if (right === '0')
72
+ return 1;
73
+ return semver.compare(left, right);
74
+ }
75
+ function sortedEvents(events) {
76
+ return events
77
+ .filter(isUsableEvent)
78
+ .sort((left, right) => compareEventVersions(eventVersion(left), eventVersion(right)));
79
+ }
80
+ export function isAffected(version, advisory) {
81
+ if (advisory.affected.some((entry) => entry.versions.includes(version)))
82
+ return true;
83
+ if (semver.valid(version) === null)
84
+ return false;
85
+ return advisory.affected.some((entry) => entry.ranges.some((range) => {
86
+ let affected = false;
87
+ for (const event of sortedEvents(range.events)) {
88
+ if ('introduced' in event && (event.introduced === '0' || semver.gte(version, event.introduced)))
89
+ affected = true;
90
+ if ('fixed' in event && semver.gte(version, event.fixed))
91
+ affected = false;
92
+ if ('last_affected' in event && semver.gt(version, event.last_affected))
93
+ affected = false;
94
+ }
95
+ return affected;
96
+ }));
97
+ }
98
+ export function safeVersions(meta, advisories) {
99
+ return meta.versions
100
+ .filter((version) => semver.valid(version) !== null && semver.prerelease(version) === null)
101
+ .filter((version) => !advisories.some((advisory) => isAffected(version, advisory)))
102
+ .sort(semver.compare);
103
+ }
@@ -0,0 +1,137 @@
1
+ import { posix } from 'node:path';
2
+ import { buildManifestOnly } from "../adapters/manifest-only.js";
3
+ import { parseNpmLock } from "../adapters/npm-lock.js";
4
+ import { readManifest } from "../adapters/package-manifest.js";
5
+ import { parsePnpmLock } from "../adapters/pnpm-lock.js";
6
+ import { walkNodeModules } from "../adapters/node-modules.js";
7
+ import { resolveToday } from "../adapters/resolve-today.js";
8
+ import { RootGraphBuilder } from "../graph/root-graph-builder.js";
9
+ const LOCKFILE_PARSERS = {
10
+ 'pnpm-lock.yaml': parsePnpmLock,
11
+ 'package-lock.json': parseNpmLock,
12
+ 'npm-shrinkwrap.json': parseNpmLock,
13
+ };
14
+ function newBuilder(root, source) {
15
+ return new RootGraphBuilder({ id: root.id, path: root.path, manager: root.manager, source });
16
+ }
17
+ export function messageOf(failure) {
18
+ return failure instanceof Error ? failure.message : String(failure);
19
+ }
20
+ export function failedRootGraph(root, error, warnings = []) {
21
+ return {
22
+ id: root.id,
23
+ path: root.path,
24
+ manager: root.manager,
25
+ source: root.lockfile ? 'lockfile' : 'manifest-only',
26
+ status: 'failed',
27
+ error,
28
+ importers: [],
29
+ packages: new Map(),
30
+ warnings,
31
+ };
32
+ }
33
+ function parseLockfileText(root, text, source, parser) {
34
+ const builder = newBuilder(root, source);
35
+ const { supported } = parser(text, builder);
36
+ const graph = builder.build();
37
+ if (supported)
38
+ return { graph, reason: '' };
39
+ return { graph: null, reason: graph.warnings.join('; ') || 'lockfile is not supported' };
40
+ }
41
+ export function parseRootLockfile(root, lockfileName, text) {
42
+ const parser = LOCKFILE_PARSERS[posix.basename(lockfileName)];
43
+ if (!parser)
44
+ return { error: `${lockfileName} has no supported lockfile format` };
45
+ try {
46
+ const attempt = parseLockfileText({ ...root, importerPaths: [] }, text, 'lockfile', parser);
47
+ return attempt.graph ? { graph: attempt.graph } : { error: attempt.reason };
48
+ }
49
+ catch (failure) {
50
+ return { error: messageOf(failure) };
51
+ }
52
+ }
53
+ async function attemptLockfile(root, reader) {
54
+ if (!root.lockfile)
55
+ return { graph: null, reason: 'no lockfile' };
56
+ const parser = LOCKFILE_PARSERS[posix.basename(root.lockfile)];
57
+ if (!parser)
58
+ return { graph: null, reason: `${root.lockfile} has no supported lockfile format` };
59
+ const text = await reader.readText(root.lockfile);
60
+ if (text === null)
61
+ return { graph: null, reason: `${root.lockfile} could not be read` };
62
+ return parseLockfileText(root, text, 'lockfile', parser);
63
+ }
64
+ async function attemptInstalled(root, reader) {
65
+ const builder = newBuilder(root, 'installed');
66
+ return (await walkNodeModules(reader, root, builder)) ? builder.build() : null;
67
+ }
68
+ async function attemptResolveToday(root, deps) {
69
+ if (!deps.allowResolve)
70
+ return { graph: null, reason: 'versions not resolved' };
71
+ const result = await resolveToday(deps.reader, root, deps.runner);
72
+ if ('error' in result)
73
+ return { graph: null, reason: `version resolution failed: ${result.error}` };
74
+ const parser = result.manager === 'pnpm' ? parsePnpmLock : parseNpmLock;
75
+ const attempt = parseLockfileText(root, result.lockfileText, 'resolved-today', parser);
76
+ if (attempt.graph)
77
+ return attempt;
78
+ return { graph: null, reason: `resolved ${result.manager} lockfile is not usable: ${attempt.reason}` };
79
+ }
80
+ async function attemptManifestOnly(root, reader) {
81
+ const builder = newBuilder(root, 'manifest-only');
82
+ await buildManifestOnly(reader, root, builder);
83
+ return builder.build();
84
+ }
85
+ function withFallbackWarning(graph, reasons) {
86
+ if (reasons.length === 0)
87
+ return graph;
88
+ const sourceLabel = {
89
+ lockfile: 'lockfile',
90
+ installed: 'installed node_modules',
91
+ 'resolved-today': 'versions resolved today',
92
+ 'manifest-only': 'package.json manifests only',
93
+ };
94
+ graph.warnings.push(`${reasons.join('; ')}; built from ${sourceLabel[graph.source]}`);
95
+ return graph;
96
+ }
97
+ async function useRepoRelativeImporters(root, graph, reader) {
98
+ await Promise.all(graph.importers.map(async (importer) => {
99
+ const rootRelativePath = importer.path;
100
+ importer.path = rootRelativePath === '.' ? root.path : posix.join(root.path, rootRelativePath);
101
+ if (importer.name !== rootRelativePath)
102
+ return;
103
+ const read = await readManifest(reader, importer.path);
104
+ if ('manifest' in read && read.manifest.name)
105
+ importer.name = read.manifest.name;
106
+ }));
107
+ }
108
+ async function cascade(root, deps, reasons) {
109
+ const fromLockfile = await attemptLockfile(root, deps.reader);
110
+ if (fromLockfile.graph)
111
+ return finish(root, fromLockfile.graph, reasons, deps);
112
+ reasons.push(fromLockfile.reason);
113
+ if (deps.allowInstalled !== false) {
114
+ const installed = await attemptInstalled(root, deps.reader);
115
+ if (installed)
116
+ return finish(root, installed, reasons, deps);
117
+ reasons.push('no installed node_modules');
118
+ }
119
+ const resolved = await attemptResolveToday(root, deps);
120
+ if (resolved.graph)
121
+ return finish(root, resolved.graph, reasons, deps);
122
+ reasons.push(resolved.reason);
123
+ return finish(root, await attemptManifestOnly(root, deps.reader), reasons, deps);
124
+ }
125
+ async function finish(root, graph, reasons, deps) {
126
+ await useRepoRelativeImporters(root, graph, deps.reader);
127
+ return withFallbackWarning(graph, reasons);
128
+ }
129
+ export async function buildRoot(root, deps) {
130
+ const reasons = [];
131
+ try {
132
+ return await cascade(root, deps, reasons);
133
+ }
134
+ catch (failure) {
135
+ return failedRootGraph(root, messageOf(failure), reasons.length > 0 ? [reasons.join('; ')] : []);
136
+ }
137
+ }
@@ -0,0 +1,52 @@
1
+ const STATUSES = new Set(['scanning', 'ready', 'failed']);
2
+ const SOURCES = new Set(['lockfile', 'installed', 'resolved-today', 'manifest-only']);
3
+ const MANAGERS = new Set(['pnpm', 'npm', 'unknown']);
4
+ const DEP_KINDS = new Set(['prod', 'dev', 'optional', 'peer']);
5
+ function isRecord(value) {
6
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
7
+ }
8
+ function isStringArray(value) {
9
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
10
+ }
11
+ function isEdge(value) {
12
+ return (isRecord(value) &&
13
+ typeof value.to === 'string' &&
14
+ typeof value.range === 'string' &&
15
+ typeof value.kind === 'string' &&
16
+ DEP_KINDS.has(value.kind));
17
+ }
18
+ function hasEdgesAndDependents(node) {
19
+ return Array.isArray(node.deps) && node.deps.every(isEdge) && isStringArray(node.dependents);
20
+ }
21
+ function isImporter(value) {
22
+ return (isRecord(value) &&
23
+ typeof value.id === 'string' &&
24
+ typeof value.path === 'string' &&
25
+ typeof value.name === 'string' &&
26
+ hasEdgesAndDependents(value));
27
+ }
28
+ function isPackage(value) {
29
+ return (isRecord(value) &&
30
+ typeof value.id === 'string' &&
31
+ typeof value.name === 'string' &&
32
+ typeof value.version === 'string' &&
33
+ (value.peerSuffix === undefined || typeof value.peerSuffix === 'string') &&
34
+ (value.nonRegistry === undefined || value.nonRegistry === true) &&
35
+ hasEdgesAndDependents(value));
36
+ }
37
+ export function isRootGraphShape(json) {
38
+ return (isRecord(json) &&
39
+ typeof json.id === 'string' &&
40
+ typeof json.path === 'string' &&
41
+ typeof json.manager === 'string' &&
42
+ MANAGERS.has(json.manager) &&
43
+ typeof json.status === 'string' &&
44
+ STATUSES.has(json.status) &&
45
+ typeof json.source === 'string' &&
46
+ SOURCES.has(json.source) &&
47
+ isStringArray(json.warnings) &&
48
+ Array.isArray(json.importers) &&
49
+ json.importers.every(isImporter) &&
50
+ Array.isArray(json.packages) &&
51
+ json.packages.every(isPackage));
52
+ }
@@ -0,0 +1,32 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { deserializeRootGraph, serializeRootGraph } from "../graph/serialize.js";
5
+ import { isRootGraphShape } from "./cache-shape.js";
6
+ const KEY_SEPARATOR = '\0';
7
+ export const CACHE_FORMAT = 2;
8
+ export function cacheKeyOf(repoPath, rootPath, lockfileText, format = CACHE_FORMAT) {
9
+ return createHash('sha256')
10
+ .update([String(format), repoPath, rootPath, lockfileText].join(KEY_SEPARATOR))
11
+ .digest('hex');
12
+ }
13
+ export function createScanCache(dir) {
14
+ const fileOf = (key) => path.join(dir, `${key}.json`);
15
+ return {
16
+ async get(key) {
17
+ try {
18
+ const json = JSON.parse(await readFile(fileOf(key), 'utf8'));
19
+ return isRootGraphShape(json) ? deserializeRootGraph(json) : null;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ },
25
+ async set(key, graph) {
26
+ await mkdir(dir, { recursive: true });
27
+ const temporaryFile = `${fileOf(key)}.${randomUUID()}.tmp`;
28
+ await writeFile(temporaryFile, JSON.stringify(serializeRootGraph(graph)));
29
+ await rename(temporaryFile, fileOf(key));
30
+ },
31
+ };
32
+ }