@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,203 @@
|
|
|
1
|
+
export const DIFF_CONTEXT_LINES = 3;
|
|
2
|
+
export const MAX_EDIT_DISTANCE = 4000;
|
|
3
|
+
const NO_NEWLINE_MARKER = '\';
|
|
4
|
+
const LINE_PREFIX = { equal: ' ', remove: '-', add: '+' };
|
|
5
|
+
function splitLines(text) {
|
|
6
|
+
if (text === null || text === '')
|
|
7
|
+
return { lines: [], keys: [], lastLineUnterminated: false };
|
|
8
|
+
const lines = text.split('\n');
|
|
9
|
+
const lastLineUnterminated = lines[lines.length - 1] !== '';
|
|
10
|
+
if (!lastLineUnterminated)
|
|
11
|
+
lines.pop();
|
|
12
|
+
const keys = lines.slice();
|
|
13
|
+
if (lastLineUnterminated)
|
|
14
|
+
keys[keys.length - 1] += '\n';
|
|
15
|
+
return { lines, keys, lastLineUnterminated };
|
|
16
|
+
}
|
|
17
|
+
function commonPrefixLength(before, after) {
|
|
18
|
+
const limit = Math.min(before.length, after.length);
|
|
19
|
+
let length = 0;
|
|
20
|
+
while (length < limit && before[length] === after[length])
|
|
21
|
+
length++;
|
|
22
|
+
return length;
|
|
23
|
+
}
|
|
24
|
+
function commonSuffixLength(before, after, prefix) {
|
|
25
|
+
const limit = Math.min(before.length, after.length) - prefix;
|
|
26
|
+
let length = 0;
|
|
27
|
+
while (length < limit && before[before.length - 1 - length] === after[after.length - 1 - length])
|
|
28
|
+
length++;
|
|
29
|
+
return length;
|
|
30
|
+
}
|
|
31
|
+
function replacementOperations(removedCount, addedCount, oldOffset, newOffset) {
|
|
32
|
+
const operations = [];
|
|
33
|
+
for (let index = 0; index < removedCount; index++) {
|
|
34
|
+
operations.push({ kind: 'remove', oldIndex: oldOffset + index, newIndex: newOffset });
|
|
35
|
+
}
|
|
36
|
+
for (let index = 0; index < addedCount; index++) {
|
|
37
|
+
operations.push({ kind: 'add', oldIndex: oldOffset + removedCount, newIndex: newOffset + index });
|
|
38
|
+
}
|
|
39
|
+
return operations;
|
|
40
|
+
}
|
|
41
|
+
function myersTrace(before, after, maxDistance) {
|
|
42
|
+
const beforeLength = before.length;
|
|
43
|
+
const afterLength = after.length;
|
|
44
|
+
const limit = Math.min(beforeLength + afterLength, maxDistance);
|
|
45
|
+
const offset = limit + 1;
|
|
46
|
+
const furthest = new Int32Array(2 * limit + 3);
|
|
47
|
+
const trace = [];
|
|
48
|
+
for (let distance = 0; distance <= limit; distance++) {
|
|
49
|
+
const snapshot = new Int32Array(distance + 1);
|
|
50
|
+
for (let diagonal = -distance; diagonal <= distance; diagonal += 2) {
|
|
51
|
+
let x;
|
|
52
|
+
if (diagonal === -distance || (diagonal !== distance && furthest[offset + diagonal - 1] < furthest[offset + diagonal + 1])) {
|
|
53
|
+
x = furthest[offset + diagonal + 1];
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
x = furthest[offset + diagonal - 1] + 1;
|
|
57
|
+
}
|
|
58
|
+
let y = x - diagonal;
|
|
59
|
+
while (x < beforeLength && y < afterLength && before[x] === after[y]) {
|
|
60
|
+
x++;
|
|
61
|
+
y++;
|
|
62
|
+
}
|
|
63
|
+
furthest[offset + diagonal] = x;
|
|
64
|
+
snapshot[(diagonal + distance) / 2] = x;
|
|
65
|
+
if (x >= beforeLength && y >= afterLength) {
|
|
66
|
+
trace.push(snapshot);
|
|
67
|
+
return trace;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
trace.push(snapshot);
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
function operationsFromTrace(trace, beforeLength, afterLength) {
|
|
75
|
+
const reversed = [];
|
|
76
|
+
let x = beforeLength;
|
|
77
|
+
let y = afterLength;
|
|
78
|
+
for (let distance = trace.length - 1; distance >= 0; distance--) {
|
|
79
|
+
const diagonal = x - y;
|
|
80
|
+
let previousX = 0;
|
|
81
|
+
let previousY = 0;
|
|
82
|
+
let editFromRemoval = false;
|
|
83
|
+
if (distance > 0) {
|
|
84
|
+
const previous = trace[distance - 1];
|
|
85
|
+
const valueAt = (k) => previous[(k + distance - 1) / 2];
|
|
86
|
+
const cameFromAbove = diagonal === -distance || (diagonal !== distance && valueAt(diagonal - 1) < valueAt(diagonal + 1));
|
|
87
|
+
const previousDiagonal = cameFromAbove ? diagonal + 1 : diagonal - 1;
|
|
88
|
+
previousX = valueAt(previousDiagonal);
|
|
89
|
+
previousY = previousX - previousDiagonal;
|
|
90
|
+
editFromRemoval = !cameFromAbove;
|
|
91
|
+
}
|
|
92
|
+
const snakeStartX = distance > 0 ? (editFromRemoval ? previousX + 1 : previousX) : 0;
|
|
93
|
+
while (x > snakeStartX) {
|
|
94
|
+
x--;
|
|
95
|
+
y--;
|
|
96
|
+
reversed.push({ kind: 'equal', oldIndex: x, newIndex: y });
|
|
97
|
+
}
|
|
98
|
+
if (distance > 0) {
|
|
99
|
+
if (editFromRemoval) {
|
|
100
|
+
reversed.push({ kind: 'remove', oldIndex: previousX, newIndex: previousY });
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
reversed.push({ kind: 'add', oldIndex: previousX, newIndex: previousY });
|
|
104
|
+
}
|
|
105
|
+
x = previousX;
|
|
106
|
+
y = previousY;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return reversed.reverse();
|
|
110
|
+
}
|
|
111
|
+
function diffOperations(before, after) {
|
|
112
|
+
const prefix = commonPrefixLength(before, after);
|
|
113
|
+
const suffix = commonSuffixLength(before, after, prefix);
|
|
114
|
+
const middleBefore = before.slice(prefix, before.length - suffix);
|
|
115
|
+
const middleAfter = after.slice(prefix, after.length - suffix);
|
|
116
|
+
const operations = [];
|
|
117
|
+
for (let index = 0; index < prefix; index++)
|
|
118
|
+
operations.push({ kind: 'equal', oldIndex: index, newIndex: index });
|
|
119
|
+
const trace = myersTrace(middleBefore, middleAfter, MAX_EDIT_DISTANCE);
|
|
120
|
+
const middle = trace
|
|
121
|
+
? operationsFromTrace(trace, middleBefore.length, middleAfter.length)
|
|
122
|
+
: replacementOperations(middleBefore.length, middleAfter.length, 0, 0);
|
|
123
|
+
for (const operation of middle) {
|
|
124
|
+
operations.push({ kind: operation.kind, oldIndex: operation.oldIndex + prefix, newIndex: operation.newIndex + prefix });
|
|
125
|
+
}
|
|
126
|
+
for (let index = 0; index < suffix; index++) {
|
|
127
|
+
operations.push({
|
|
128
|
+
kind: 'equal',
|
|
129
|
+
oldIndex: before.length - suffix + index,
|
|
130
|
+
newIndex: after.length - suffix + index
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return operations;
|
|
134
|
+
}
|
|
135
|
+
function hunkRanges(operations) {
|
|
136
|
+
const ranges = [];
|
|
137
|
+
let current = null;
|
|
138
|
+
for (let index = 0; index < operations.length; index++) {
|
|
139
|
+
if (operations[index].kind === 'equal')
|
|
140
|
+
continue;
|
|
141
|
+
const start = Math.max(0, index - DIFF_CONTEXT_LINES);
|
|
142
|
+
const end = Math.min(operations.length, index + DIFF_CONTEXT_LINES + 1);
|
|
143
|
+
if (current && start <= current[1]) {
|
|
144
|
+
current[1] = end;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
current = [start, end];
|
|
148
|
+
ranges.push(current);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return ranges;
|
|
152
|
+
}
|
|
153
|
+
function rangeText(start, count) {
|
|
154
|
+
if (count === 1)
|
|
155
|
+
return String(start);
|
|
156
|
+
return `${start},${count}`;
|
|
157
|
+
}
|
|
158
|
+
function hunkHeader(slice) {
|
|
159
|
+
const oldCount = slice.filter((operation) => operation.kind !== 'add').length;
|
|
160
|
+
const newCount = slice.filter((operation) => operation.kind !== 'remove').length;
|
|
161
|
+
const oldStart = oldCount === 0 ? slice[0].oldIndex : slice[0].oldIndex + 1;
|
|
162
|
+
const newStart = newCount === 0 ? slice[0].newIndex : slice[0].newIndex + 1;
|
|
163
|
+
return `@@ -${rangeText(oldStart, oldCount)} +${rangeText(newStart, newCount)} @@`;
|
|
164
|
+
}
|
|
165
|
+
function renderHunk(slice, before, after) {
|
|
166
|
+
const output = [hunkHeader(slice)];
|
|
167
|
+
for (const operation of slice) {
|
|
168
|
+
const source = operation.kind === 'add' ? after : before;
|
|
169
|
+
const index = operation.kind === 'add' ? operation.newIndex : operation.oldIndex;
|
|
170
|
+
output.push(LINE_PREFIX[operation.kind] + source.lines[index]);
|
|
171
|
+
if (source.lastLineUnterminated && index === source.lines.length - 1)
|
|
172
|
+
output.push(NO_NEWLINE_MARKER);
|
|
173
|
+
}
|
|
174
|
+
return output;
|
|
175
|
+
}
|
|
176
|
+
export function unifiedDiff(path, beforeText, afterText) {
|
|
177
|
+
if (beforeText === afterText)
|
|
178
|
+
return { diff: '', added: 0, removed: 0 };
|
|
179
|
+
const before = splitLines(beforeText);
|
|
180
|
+
const after = splitLines(afterText);
|
|
181
|
+
const operations = diffOperations(before.keys, after.keys);
|
|
182
|
+
const ranges = hunkRanges(operations);
|
|
183
|
+
if (ranges.length === 0)
|
|
184
|
+
return { diff: '', added: 0, removed: 0 };
|
|
185
|
+
const output = [
|
|
186
|
+
beforeText === null ? '--- /dev/null' : `--- a/${path}`,
|
|
187
|
+
afterText === null ? '+++ /dev/null' : `+++ b/${path}`
|
|
188
|
+
];
|
|
189
|
+
for (const [start, end] of ranges)
|
|
190
|
+
output.push(...renderHunk(operations.slice(start, end), before, after));
|
|
191
|
+
const added = operations.filter((operation) => operation.kind === 'add').length;
|
|
192
|
+
const removed = operations.filter((operation) => operation.kind === 'remove').length;
|
|
193
|
+
return { diff: output.join('\n') + '\n', added, removed };
|
|
194
|
+
}
|
|
195
|
+
export function truncateUtf8(text, maxBytes) {
|
|
196
|
+
const bytes = Buffer.from(text, 'utf8');
|
|
197
|
+
if (bytes.length <= maxBytes)
|
|
198
|
+
return { text, truncated: false };
|
|
199
|
+
const lastLineBreak = bytes.subarray(0, maxBytes).lastIndexOf(0x0a);
|
|
200
|
+
if (lastLineBreak < 0)
|
|
201
|
+
return { text: '', truncated: true };
|
|
202
|
+
return { text: bytes.subarray(0, lastLineBreak + 1).toString('utf8'), truncated: true };
|
|
203
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
import { ParentLinkResolver } from "../query/dependents.js";
|
|
3
|
+
import { nodeMeta, requestedRange, worstSeverity } from "../query/enrichment-lookup.js";
|
|
4
|
+
import { ALL_KINDS, findNode } from "../query/lookup.js";
|
|
5
|
+
import { normalizedRequestedRange } from "../query/node-fix.js";
|
|
6
|
+
import { isAffected } from "../query/versions.js";
|
|
7
|
+
import { relockTarget } from "./fix-options.js";
|
|
8
|
+
import { importerLabel } from "./labels.js";
|
|
9
|
+
import { catalogNameOf, rewriteRange } from "./range-style.js";
|
|
10
|
+
import { setRangeStep } from "./set-range.js";
|
|
11
|
+
const DECLARING_FIELDS = ['dependencies', 'devDependencies', 'optionalDependencies'];
|
|
12
|
+
const PEER_FIELD = 'peerDependencies';
|
|
13
|
+
const ALIAS_PREFIX = 'npm:';
|
|
14
|
+
const VERSION_PLACEHOLDER = '<version>';
|
|
15
|
+
const LATEST_TAG = 'latest';
|
|
16
|
+
const DEFAULT_CATALOG = 'default';
|
|
17
|
+
const MAJOR_UPDATE = 'major update';
|
|
18
|
+
const EMPTY_SELECTION = 'Select at least one workspace package';
|
|
19
|
+
function isRecord(value) {
|
|
20
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
function declaredRange(manifest, field, name) {
|
|
23
|
+
const section = manifest[field];
|
|
24
|
+
if (!isRecord(section))
|
|
25
|
+
return undefined;
|
|
26
|
+
const range = section[name];
|
|
27
|
+
return typeof range === 'string' ? range : undefined;
|
|
28
|
+
}
|
|
29
|
+
function templateOf(specifier) {
|
|
30
|
+
const rewritten = rewriteRange(specifier, VERSION_PLACEHOLDER);
|
|
31
|
+
return rewritten.editable ? { editable: true, template: rewritten.specifier } : { editable: false, reason: rewritten.reason };
|
|
32
|
+
}
|
|
33
|
+
function catalogEntryOf(root, entry) {
|
|
34
|
+
const catalog = catalogNameOf(entry.specifier);
|
|
35
|
+
return catalog === undefined ? entry.specifier : root.config?.catalogs[catalog]?.[entry.key];
|
|
36
|
+
}
|
|
37
|
+
function editability(root, entry) {
|
|
38
|
+
const resolved = catalogEntryOf(root, entry);
|
|
39
|
+
if (resolved !== undefined)
|
|
40
|
+
return templateOf(resolved);
|
|
41
|
+
return { editable: false, reason: `the ${catalogNameOf(entry.specifier)} catalog has no entry for ${entry.key}` };
|
|
42
|
+
}
|
|
43
|
+
function aliasedPackageOf(specifier) {
|
|
44
|
+
if (!specifier.startsWith(ALIAS_PREFIX))
|
|
45
|
+
return undefined;
|
|
46
|
+
const versionSeparator = specifier.lastIndexOf('@');
|
|
47
|
+
return versionSeparator <= ALIAS_PREFIX.length ? specifier.slice(ALIAS_PREFIX.length) : specifier.slice(ALIAS_PREFIX.length, versionSeparator);
|
|
48
|
+
}
|
|
49
|
+
function declaresPackage(root, entry, name) {
|
|
50
|
+
const aliased = aliasedPackageOf(catalogEntryOf(root, entry) ?? entry.specifier);
|
|
51
|
+
return aliased === undefined ? entry.key === name : aliased === name;
|
|
52
|
+
}
|
|
53
|
+
function manifestEntries(manifest, field) {
|
|
54
|
+
const section = manifest[field];
|
|
55
|
+
if (!isRecord(section))
|
|
56
|
+
return [];
|
|
57
|
+
return Object.entries(section).flatMap(([key, specifier]) => (typeof specifier === 'string' ? [{ key, specifier }] : []));
|
|
58
|
+
}
|
|
59
|
+
function declaringEntry(root, manifest, name) {
|
|
60
|
+
for (const field of DECLARING_FIELDS) {
|
|
61
|
+
const declaring = manifestEntries(manifest, field).filter((entry) => declaresPackage(root, entry, name));
|
|
62
|
+
const entry = declaring.find((candidate) => candidate.key === name) ?? declaring[0];
|
|
63
|
+
if (entry !== undefined)
|
|
64
|
+
return { field, entry };
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
function declaringImporter(root, id, manifest, name) {
|
|
69
|
+
const declaring = declaringEntry(root, manifest, name);
|
|
70
|
+
if (declaring === undefined)
|
|
71
|
+
return undefined;
|
|
72
|
+
const { field, entry } = declaring;
|
|
73
|
+
const peerRange = declaredRange(manifest, PEER_FIELD, entry.key);
|
|
74
|
+
return {
|
|
75
|
+
id,
|
|
76
|
+
field,
|
|
77
|
+
...(entry.key === name ? {} : { alias: entry.key }),
|
|
78
|
+
specifier: entry.specifier,
|
|
79
|
+
...editability(root, entry),
|
|
80
|
+
...(peerRange === undefined ? {} : { peerRange }),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function declaringImporters(root, name, manifests) {
|
|
84
|
+
return root.importers.flatMap((importer) => {
|
|
85
|
+
const manifest = manifests.get(importer.id);
|
|
86
|
+
const declaring = manifest === undefined ? undefined : declaringImporter(root, importer.id, manifest, name);
|
|
87
|
+
return declaring === undefined ? [] : [declaring];
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function dependsDirectly(importer, node) {
|
|
91
|
+
return importer.deps.some((edge) => edge.to === node.id && edge.kind !== 'peer');
|
|
92
|
+
}
|
|
93
|
+
function parentRanges(view, root, node, meta) {
|
|
94
|
+
return new ParentLinkResolver(root, ALL_KINDS).of(node).flatMap((link) => {
|
|
95
|
+
const parent = findNode(root, link.id);
|
|
96
|
+
return parent === undefined ? [] : [normalizedRequestedRange(root, node, meta, requestedRange(view, root, parent, link.range, node))];
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function newerReleases(meta, current) {
|
|
100
|
+
if (semver.valid(current) === null)
|
|
101
|
+
return [];
|
|
102
|
+
return meta.versions.filter((version) => semver.valid(version) !== null && semver.prerelease(version) === null && semver.gt(version, current));
|
|
103
|
+
}
|
|
104
|
+
function latestOf(meta, current) {
|
|
105
|
+
const latest = meta.distTags[LATEST_TAG];
|
|
106
|
+
return latest !== undefined && semver.valid(latest) !== null && latest !== current ? latest : undefined;
|
|
107
|
+
}
|
|
108
|
+
function packageAdvisories(view, name) {
|
|
109
|
+
const loaded = view.advisoriesOf(name);
|
|
110
|
+
return loaded.state === 'ok' || loaded.state === 'stale' ? loaded.advisories : [];
|
|
111
|
+
}
|
|
112
|
+
function publishedVersion(meta, advisories, version) {
|
|
113
|
+
const severity = worstSeverity(advisories.filter((advisory) => isAffected(version, advisory)).map((advisory) => advisory.severity));
|
|
114
|
+
return {
|
|
115
|
+
version,
|
|
116
|
+
...(severity === undefined ? {} : { severity }),
|
|
117
|
+
...(meta.deprecated[version] === undefined ? {} : { deprecated: true }),
|
|
118
|
+
...(semver.prerelease(version) === null ? {} : { prerelease: true }),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function registryChoices(view, root, node) {
|
|
122
|
+
const meta = nodeMeta(view, root, node);
|
|
123
|
+
if (meta === undefined)
|
|
124
|
+
return { versions: [] };
|
|
125
|
+
const wanted = relockTarget(parentRanges(view, root, node, meta), newerReleases(meta, node.version));
|
|
126
|
+
const latest = latestOf(meta, node.version);
|
|
127
|
+
const advisories = packageAdvisories(view, node.name);
|
|
128
|
+
return {
|
|
129
|
+
...(wanted === undefined ? {} : { wanted }),
|
|
130
|
+
...(latest === undefined ? {} : { latest }),
|
|
131
|
+
versions: meta.versions
|
|
132
|
+
.filter((version) => semver.valid(version) !== null)
|
|
133
|
+
.sort(semver.rcompare)
|
|
134
|
+
.map((version) => publishedVersion(meta, advisories, version)),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
export function updateActions(context, node, manifests) {
|
|
138
|
+
const { view, root } = context;
|
|
139
|
+
if (!root.importers.some((importer) => dependsDirectly(importer, node)))
|
|
140
|
+
return undefined;
|
|
141
|
+
return { name: node.name, current: node.version, ...registryChoices(view, root, node), importers: declaringImporters(root, node.name, manifests) };
|
|
142
|
+
}
|
|
143
|
+
function majorWarnings(current, target) {
|
|
144
|
+
if (semver.valid(current) === null || semver.valid(target) === null)
|
|
145
|
+
return [];
|
|
146
|
+
return semver.major(target) > semver.major(current) ? [MAJOR_UPDATE] : [];
|
|
147
|
+
}
|
|
148
|
+
function peerWarnings(selected) {
|
|
149
|
+
return selected.flatMap((importer) => (importer.peerRange === undefined ? [] : [`${importerLabel(importer.id)}: peerDependencies ${importer.peerRange} unchanged`]));
|
|
150
|
+
}
|
|
151
|
+
function locksCurrent(root, importer, update) {
|
|
152
|
+
return importer.deps.some((edge) => {
|
|
153
|
+
const target = root.packages.get(edge.to);
|
|
154
|
+
return edge.kind !== 'peer' && target?.name === update.name && target.version === update.current;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function lockingImporters(root, update) {
|
|
158
|
+
return root.importers.filter((importer) => locksCurrent(root, importer, update)).map((importer) => importer.id);
|
|
159
|
+
}
|
|
160
|
+
function wantedPlan(update, root) {
|
|
161
|
+
const wanted = update.wanted;
|
|
162
|
+
if (wanted === undefined)
|
|
163
|
+
return { error: `${update.name} has no newer version within its ranges` };
|
|
164
|
+
return {
|
|
165
|
+
goal: { kind: 'update', name: update.name, importers: lockingImporters(root, update), target: wanted },
|
|
166
|
+
steps: [{ kind: 'relock', name: update.name, from: update.current, to: wanted, reason: `every requesting range accepts ${wanted}` }],
|
|
167
|
+
warnings: majorWarnings(update.current, wanted),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function targetVersion(update, choice, version) {
|
|
171
|
+
const target = choice === 'latest' ? update.latest : version;
|
|
172
|
+
if (target === undefined)
|
|
173
|
+
return { error: choice === 'latest' ? `${update.name} has no latest version to update to` : `Select a version of ${update.name}` };
|
|
174
|
+
if (!update.versions.some((entry) => entry.version === target))
|
|
175
|
+
return { error: `${target} is not a published version of ${update.name}` };
|
|
176
|
+
if (target === update.current)
|
|
177
|
+
return { error: `${target} is the current version` };
|
|
178
|
+
return { version: target };
|
|
179
|
+
}
|
|
180
|
+
function selectedImporters(update, ids) {
|
|
181
|
+
const requested = new Set(ids);
|
|
182
|
+
if (requested.size === 0)
|
|
183
|
+
return { error: EMPTY_SELECTION };
|
|
184
|
+
for (const id of requested) {
|
|
185
|
+
const declaring = update.importers.find((importer) => importer.id === id);
|
|
186
|
+
if (declaring === undefined)
|
|
187
|
+
return { error: `${id} does not declare ${update.name}` };
|
|
188
|
+
if (!declaring.editable)
|
|
189
|
+
return { error: `${id}: ${declaring.reason}` };
|
|
190
|
+
}
|
|
191
|
+
return { importers: update.importers.filter((importer) => requested.has(importer.id)) };
|
|
192
|
+
}
|
|
193
|
+
function catalogEntryKey(step) {
|
|
194
|
+
return step.place === 'catalog' ? `${step.catalog ?? DEFAULT_CATALOG}\0${step.dependency}` : undefined;
|
|
195
|
+
}
|
|
196
|
+
function rangeReason(choice, update, target) {
|
|
197
|
+
return choice === 'latest' ? `${target} is the latest version of ${update.name}` : `${target} is the selected version of ${update.name}`;
|
|
198
|
+
}
|
|
199
|
+
function rangePlan(update, root, choice, target, selected) {
|
|
200
|
+
const reason = rangeReason(choice, update, target);
|
|
201
|
+
const steps = [];
|
|
202
|
+
const editedCatalogEntries = new Set();
|
|
203
|
+
for (const importer of selected) {
|
|
204
|
+
const dependency = importer.alias ?? update.name;
|
|
205
|
+
const result = setRangeStep(root, { importer: importer.id, field: importer.field, dependency, specifier: importer.specifier, target, reason });
|
|
206
|
+
if ('unavailable' in result)
|
|
207
|
+
return { error: result.unavailable };
|
|
208
|
+
const catalogEntry = catalogEntryKey(result.step);
|
|
209
|
+
if (catalogEntry !== undefined) {
|
|
210
|
+
if (editedCatalogEntries.has(catalogEntry))
|
|
211
|
+
continue;
|
|
212
|
+
editedCatalogEntries.add(catalogEntry);
|
|
213
|
+
}
|
|
214
|
+
steps.push(result.step);
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
goal: { kind: 'update', name: update.name, importers: selected.map((importer) => importer.id), target },
|
|
218
|
+
steps,
|
|
219
|
+
warnings: [...majorWarnings(update.current, target), ...peerWarnings(selected)],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
export function updateSteps(update, root, choice, version, importers) {
|
|
223
|
+
if (choice === 'wanted')
|
|
224
|
+
return wantedPlan(update, root);
|
|
225
|
+
const target = targetVersion(update, choice, version);
|
|
226
|
+
if ('error' in target)
|
|
227
|
+
return target;
|
|
228
|
+
const selected = selectedImporters(update, importers);
|
|
229
|
+
if ('error' in selected)
|
|
230
|
+
return selected;
|
|
231
|
+
return rangePlan(update, root, choice, target.version, selected.importers);
|
|
232
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { isMap, isScalar, parseDocument, YAMLMap } from 'yaml';
|
|
2
|
+
export class WorkspaceEditError extends Error {
|
|
3
|
+
}
|
|
4
|
+
const UNINDENTED_SEQUENCE = /^( *)[^\s#-][^\n]*:[ \t]*(?:#[^\n]*)?\r?\n\1- /m;
|
|
5
|
+
const INDENTED_MAPPING_LINE = /^( +)[^\s#-][^\n]*:/m;
|
|
6
|
+
const DEFAULT_INDENT = 2;
|
|
7
|
+
function indentOf(source) {
|
|
8
|
+
return INDENTED_MAPPING_LINE.exec(source)?.[1].length ?? DEFAULT_INDENT;
|
|
9
|
+
}
|
|
10
|
+
function prepareIntermediateMaps(document, path) {
|
|
11
|
+
if (document.contents !== null && !isMap(document.contents)) {
|
|
12
|
+
throw new WorkspaceEditError('The workspace file is not a map');
|
|
13
|
+
}
|
|
14
|
+
for (let length = 1; length < path.length; length++) {
|
|
15
|
+
const prefix = path.slice(0, length);
|
|
16
|
+
const node = document.getIn(prefix, true);
|
|
17
|
+
if (node === undefined)
|
|
18
|
+
return;
|
|
19
|
+
if (isScalar(node) && node.value === null)
|
|
20
|
+
document.setIn(prefix, new YAMLMap());
|
|
21
|
+
else if (!isMap(node))
|
|
22
|
+
throw new WorkspaceEditError(`${prefix.join('.')} is not a map`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function setWorkspaceString(text, path, value) {
|
|
26
|
+
const source = text ?? '';
|
|
27
|
+
const document = parseDocument(source);
|
|
28
|
+
if (document.errors.length > 0)
|
|
29
|
+
throw new WorkspaceEditError(document.errors[0].message);
|
|
30
|
+
prepareIntermediateMaps(document, path);
|
|
31
|
+
document.setIn(path, value);
|
|
32
|
+
return document.toString({ lineWidth: 0, indent: indentOf(source), indentSeq: !UNINDENTED_SEQUENCE.test(source) });
|
|
33
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
4
|
+
const KILL_GRACE_MS = 5000;
|
|
5
|
+
const TIMED_OUT_EXIT_CODE = 124;
|
|
6
|
+
const NOT_STARTED_EXIT_CODE = 1;
|
|
7
|
+
function terminate(child) {
|
|
8
|
+
if (child.pid === undefined)
|
|
9
|
+
return;
|
|
10
|
+
child.kill('SIGTERM');
|
|
11
|
+
setTimeout(() => child.kill('SIGKILL'), KILL_GRACE_MS).unref();
|
|
12
|
+
}
|
|
13
|
+
export function createSpawnRunner(options) {
|
|
14
|
+
const running = new Set();
|
|
15
|
+
const run = (command, args, cwd) => new Promise((resolve, reject) => {
|
|
16
|
+
const child = spawn(command, args, { cwd, shell: false });
|
|
17
|
+
running.add(child);
|
|
18
|
+
let timedOut = false;
|
|
19
|
+
const timer = setTimeout(() => {
|
|
20
|
+
timedOut = true;
|
|
21
|
+
terminate(child);
|
|
22
|
+
}, options.timeoutMs);
|
|
23
|
+
const settle = () => {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
running.delete(child);
|
|
26
|
+
};
|
|
27
|
+
const stdoutChunks = [];
|
|
28
|
+
const stderrChunks = [];
|
|
29
|
+
child.stdout.on('data', (chunk) => stdoutChunks.push(chunk));
|
|
30
|
+
child.stderr.on('data', (chunk) => stderrChunks.push(chunk));
|
|
31
|
+
child.on('error', (error) => {
|
|
32
|
+
settle();
|
|
33
|
+
reject(error);
|
|
34
|
+
});
|
|
35
|
+
child.on('close', (code) => {
|
|
36
|
+
settle();
|
|
37
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf8');
|
|
38
|
+
resolve({
|
|
39
|
+
code: timedOut ? TIMED_OUT_EXIT_CODE : (code ?? 1),
|
|
40
|
+
stdout: Buffer.concat(stdoutChunks).toString('utf8'),
|
|
41
|
+
stderr: timedOut ? `${stderr}\n${command} timed out after ${options.timeoutMs} ms` : stderr,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
return Object.assign(run, {
|
|
46
|
+
killRunning() {
|
|
47
|
+
for (const child of running)
|
|
48
|
+
terminate(child);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function createLineSplitter(onLine) {
|
|
53
|
+
const decoder = new StringDecoder('utf8');
|
|
54
|
+
let pending = '';
|
|
55
|
+
const emit = (line) => onLine?.(line.endsWith('\r') ? line.slice(0, -1) : line);
|
|
56
|
+
return {
|
|
57
|
+
write(chunk) {
|
|
58
|
+
const text = decoder.write(chunk);
|
|
59
|
+
const parts = (pending + text).split('\n');
|
|
60
|
+
pending = parts.pop() ?? '';
|
|
61
|
+
for (const line of parts)
|
|
62
|
+
emit(line);
|
|
63
|
+
return text;
|
|
64
|
+
},
|
|
65
|
+
flush() {
|
|
66
|
+
const rest = pending + decoder.end();
|
|
67
|
+
pending = '';
|
|
68
|
+
if (rest !== '')
|
|
69
|
+
emit(rest);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function createProcessRunner() {
|
|
74
|
+
const running = new Set();
|
|
75
|
+
const run = (request) => {
|
|
76
|
+
if (request.signal?.aborted)
|
|
77
|
+
return Promise.resolve({ code: NOT_STARTED_EXIT_CODE, stdout: '', stderr: '', timedOut: false, cancelled: true });
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
const child = spawn(request.command, [...request.args], { cwd: request.cwd, env: request.env, shell: false });
|
|
80
|
+
running.add(child);
|
|
81
|
+
let timedOut = false;
|
|
82
|
+
let cancelled = false;
|
|
83
|
+
const timer = setTimeout(() => {
|
|
84
|
+
timedOut = true;
|
|
85
|
+
terminate(child);
|
|
86
|
+
}, request.timeoutMs);
|
|
87
|
+
const cancel = () => {
|
|
88
|
+
cancelled = true;
|
|
89
|
+
terminate(child);
|
|
90
|
+
};
|
|
91
|
+
request.signal?.addEventListener('abort', cancel, { once: true });
|
|
92
|
+
const settle = () => {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
request.signal?.removeEventListener('abort', cancel);
|
|
95
|
+
running.delete(child);
|
|
96
|
+
};
|
|
97
|
+
const stdoutSplitter = createLineSplitter(request.onLine);
|
|
98
|
+
const stderrSplitter = createLineSplitter(request.onLine);
|
|
99
|
+
let stdout = '';
|
|
100
|
+
let stderr = '';
|
|
101
|
+
child.stdout.on('data', (chunk) => {
|
|
102
|
+
stdout += stdoutSplitter.write(chunk);
|
|
103
|
+
});
|
|
104
|
+
child.stderr.on('data', (chunk) => {
|
|
105
|
+
stderr += stderrSplitter.write(chunk);
|
|
106
|
+
});
|
|
107
|
+
child.on('error', (error) => {
|
|
108
|
+
settle();
|
|
109
|
+
reject(error);
|
|
110
|
+
});
|
|
111
|
+
child.on('close', (code) => {
|
|
112
|
+
settle();
|
|
113
|
+
stdoutSplitter.flush();
|
|
114
|
+
stderrSplitter.flush();
|
|
115
|
+
resolve({ code: timedOut ? TIMED_OUT_EXIT_CODE : (code ?? 1), stdout, stderr, timedOut, cancelled });
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
run,
|
|
121
|
+
killRunning() {
|
|
122
|
+
for (const child of running)
|
|
123
|
+
terminate(child);
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
export const spawnRunner = createSpawnRunner({ timeoutMs: DEFAULT_TIMEOUT_MS });
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { IMPORTER_BLOCKS, declaredDependencies, importerKeyOf, isNonRegistrySpec, readManifest } from "./package-manifest.js";
|
|
2
|
+
export async function buildManifestOnly(reader, root, builder) {
|
|
3
|
+
for (const importerPath of root.importerPaths) {
|
|
4
|
+
const importerKey = importerKeyOf(root, importerPath);
|
|
5
|
+
const read = await readManifest(reader, importerPath);
|
|
6
|
+
if ('problem' in read) {
|
|
7
|
+
builder.warn(read.problem);
|
|
8
|
+
builder.addImporter(importerKey, importerKey);
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
const importerId = builder.addImporter(importerKey, read.manifest.name ?? importerKey);
|
|
12
|
+
for (const { name, range, kind } of declaredDependencies(read.manifest, IMPORTER_BLOCKS)) {
|
|
13
|
+
const packageId = `${name}@${range}`;
|
|
14
|
+
builder.addPackage(packageId, name, range);
|
|
15
|
+
if (isNonRegistrySpec(range))
|
|
16
|
+
builder.markNonRegistry(packageId);
|
|
17
|
+
builder.addEdge(importerId, { to: packageId, kind, range });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|