@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,107 @@
|
|
|
1
|
+
import { posix } from 'node:path';
|
|
2
|
+
import { readManifest } from "../adapters/package-manifest.js";
|
|
3
|
+
import { compileWorkspacePatterns } from "../config/glob.js";
|
|
4
|
+
import { readWorkspacePatterns } from "../config/root-config.js";
|
|
5
|
+
const ALWAYS_SKIPPED_DIRECTORIES = new Set(['node_modules', '.git']);
|
|
6
|
+
const GITIGNORE_FILE = '.gitignore';
|
|
7
|
+
const PNPM_LOCKFILE = 'pnpm-lock.yaml';
|
|
8
|
+
const NPM_LOCKFILES = ['package-lock.json', 'npm-shrinkwrap.json'];
|
|
9
|
+
const PNPM_WORKSPACE_FILE = 'pnpm-workspace.yaml';
|
|
10
|
+
const MANIFEST_FILE = 'package.json';
|
|
11
|
+
const NO_MEMBERS = () => false;
|
|
12
|
+
function createIgnoreMatcher(gitignoreText) {
|
|
13
|
+
const names = new Set();
|
|
14
|
+
const anchoredPaths = new Set();
|
|
15
|
+
for (const rawLine of (gitignoreText ?? '').split(/\r?\n/)) {
|
|
16
|
+
const line = rawLine.trim();
|
|
17
|
+
if (line === '' || line.startsWith('#') || line.startsWith('!') || /[*?[\\]/.test(line))
|
|
18
|
+
continue;
|
|
19
|
+
const withoutTrailingSlash = line.replace(/\/+$/, '');
|
|
20
|
+
if (withoutTrailingSlash.startsWith('/')) {
|
|
21
|
+
anchoredPaths.add(withoutTrailingSlash.slice(1));
|
|
22
|
+
}
|
|
23
|
+
else if (withoutTrailingSlash.includes('/')) {
|
|
24
|
+
anchoredPaths.add(withoutTrailingSlash);
|
|
25
|
+
}
|
|
26
|
+
else if (withoutTrailingSlash !== '') {
|
|
27
|
+
names.add(withoutTrailingSlash);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return (path) => anchoredPaths.has(path) || names.has(posix.basename(path));
|
|
31
|
+
}
|
|
32
|
+
function isIgnoredBy(rules, path) {
|
|
33
|
+
return rules.some((rule) => isInside(path, rule.directory) && rule.matches(posix.relative(rule.directory, path)));
|
|
34
|
+
}
|
|
35
|
+
function packageManagerFromField(field) {
|
|
36
|
+
if (typeof field !== 'string')
|
|
37
|
+
return 'unknown';
|
|
38
|
+
const name = field.split('@')[0];
|
|
39
|
+
return name === 'pnpm' || name === 'npm' ? name : 'unknown';
|
|
40
|
+
}
|
|
41
|
+
async function describeRoot(reader, facts) {
|
|
42
|
+
const { path, files } = facts;
|
|
43
|
+
const inDirectory = (name) => posix.join(path, name);
|
|
44
|
+
const base = { id: path, path, importerPaths: [path] };
|
|
45
|
+
if (files.has(PNPM_LOCKFILE))
|
|
46
|
+
return { ...base, manager: 'pnpm', lockfile: inDirectory(PNPM_LOCKFILE) };
|
|
47
|
+
const npmLockfile = NPM_LOCKFILES.find((name) => files.has(name));
|
|
48
|
+
if (npmLockfile)
|
|
49
|
+
return { ...base, manager: 'npm', lockfile: inDirectory(npmLockfile) };
|
|
50
|
+
if (files.has(PNPM_WORKSPACE_FILE))
|
|
51
|
+
return { ...base, manager: 'pnpm' };
|
|
52
|
+
if (!files.has(MANIFEST_FILE))
|
|
53
|
+
return null;
|
|
54
|
+
const result = await readManifest(reader, path);
|
|
55
|
+
const record = 'manifest' in result ? result.manifest.record : {};
|
|
56
|
+
if (record.workspaces !== undefined)
|
|
57
|
+
return { ...base, manager: 'npm' };
|
|
58
|
+
return { ...base, manager: packageManagerFromField(record.packageManager) };
|
|
59
|
+
}
|
|
60
|
+
async function createMemberMatcher(reader, root) {
|
|
61
|
+
const patterns = await readWorkspacePatterns(reader, root.path);
|
|
62
|
+
if (patterns.length === 0)
|
|
63
|
+
return NO_MEMBERS;
|
|
64
|
+
const matchesPatterns = compileWorkspacePatterns(patterns);
|
|
65
|
+
return (path) => path !== root.path && matchesPatterns(posix.relative(root.path, path));
|
|
66
|
+
}
|
|
67
|
+
function isInside(directory, ancestor) {
|
|
68
|
+
return ancestor === '.' || directory.startsWith(`${ancestor}/`);
|
|
69
|
+
}
|
|
70
|
+
export async function findRoots(reader, startPath = '.') {
|
|
71
|
+
const ignoreRules = [{ directory: '.', matches: createIgnoreMatcher(await reader.readText(GITIGNORE_FILE)) }];
|
|
72
|
+
const foundRoots = [];
|
|
73
|
+
const manifestDirectories = [];
|
|
74
|
+
const queue = [posix.normalize(startPath)];
|
|
75
|
+
for (let index = 0; index < queue.length; index++) {
|
|
76
|
+
const directory = queue[index];
|
|
77
|
+
const entries = await reader.listDir(directory);
|
|
78
|
+
const files = new Set(entries.filter((entry) => entry.kind === 'file').map((entry) => entry.name));
|
|
79
|
+
if (directory !== '.' && files.has(GITIGNORE_FILE)) {
|
|
80
|
+
ignoreRules.push({ directory, matches: createIgnoreMatcher(await reader.readText(posix.join(directory, GITIGNORE_FILE))) });
|
|
81
|
+
}
|
|
82
|
+
const subdirectories = entries
|
|
83
|
+
.filter((entry) => entry.kind === 'dir' && !ALWAYS_SKIPPED_DIRECTORIES.has(entry.name))
|
|
84
|
+
.map((entry) => posix.join(directory, entry.name))
|
|
85
|
+
.filter((path) => !isIgnoredBy(ignoreRules, path))
|
|
86
|
+
.sort();
|
|
87
|
+
queue.push(...subdirectories);
|
|
88
|
+
if (files.has(MANIFEST_FILE))
|
|
89
|
+
manifestDirectories.push(directory);
|
|
90
|
+
const enclosing = foundRoots.some((root) => isInside(directory, root.descriptor.path) && root.isMember(directory));
|
|
91
|
+
if (enclosing && files.has(MANIFEST_FILE))
|
|
92
|
+
continue;
|
|
93
|
+
const descriptor = await describeRoot(reader, { path: directory, files });
|
|
94
|
+
if (descriptor)
|
|
95
|
+
foundRoots.push({ descriptor, isMember: await createMemberMatcher(reader, descriptor) });
|
|
96
|
+
}
|
|
97
|
+
for (const root of foundRoots) {
|
|
98
|
+
const members = manifestDirectories
|
|
99
|
+
.filter((path) => isInside(path, root.descriptor.path) && root.isMember(path))
|
|
100
|
+
.filter((path) => !isOwnedByOtherRoot(path, root, foundRoots));
|
|
101
|
+
root.descriptor.importerPaths = [root.descriptor.path, ...members.sort()];
|
|
102
|
+
}
|
|
103
|
+
return foundRoots.map((root) => root.descriptor).sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
104
|
+
}
|
|
105
|
+
function isOwnedByOtherRoot(path, candidate, allRoots) {
|
|
106
|
+
return allRoots.some((other) => other !== candidate && other.descriptor.path === path);
|
|
107
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { npmConfigEnv, parseNpmrcEntries } from "../config/npmrc.js";
|
|
2
|
+
const CREDENTIAL_FIELDS = ['_authToken', '_auth', '_password', 'username'];
|
|
3
|
+
const UNSCOPED_SECRET_FIELDS = ['_authToken', '_auth', '_password'];
|
|
4
|
+
const WEB_PROTOCOLS = ['http:', 'https:'];
|
|
5
|
+
const SCOPED_REGISTRY_KEY = /^@[^:]+:registry$/;
|
|
6
|
+
function splitCredentialKey(key) {
|
|
7
|
+
if (!key.startsWith('//'))
|
|
8
|
+
return undefined;
|
|
9
|
+
const separatorIndex = key.lastIndexOf(':');
|
|
10
|
+
const prefix = key.slice(0, separatorIndex);
|
|
11
|
+
const field = key.slice(separatorIndex + 1);
|
|
12
|
+
const knownField = CREDENTIAL_FIELDS.find((candidate) => candidate === field);
|
|
13
|
+
if (separatorIndex === -1 || knownField === undefined)
|
|
14
|
+
return undefined;
|
|
15
|
+
return { prefix: prefix.endsWith('/') ? prefix : `${prefix}/`, field: knownField };
|
|
16
|
+
}
|
|
17
|
+
function encodeBasic(username, password) {
|
|
18
|
+
return `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
|
19
|
+
}
|
|
20
|
+
function decodeBase64(value) {
|
|
21
|
+
return Buffer.from(value, 'base64').toString('utf8');
|
|
22
|
+
}
|
|
23
|
+
function addFieldSecrets(fields, secrets) {
|
|
24
|
+
for (const value of Object.values(fields))
|
|
25
|
+
secrets.add(value);
|
|
26
|
+
if (fields._auth !== undefined) {
|
|
27
|
+
const decodedAuth = decodeBase64(fields._auth);
|
|
28
|
+
secrets.add(decodedAuth);
|
|
29
|
+
secrets.add(decodedAuth.slice(decodedAuth.indexOf(':') + 1));
|
|
30
|
+
}
|
|
31
|
+
if (fields._password !== undefined)
|
|
32
|
+
secrets.add(decodeBase64(fields._password));
|
|
33
|
+
}
|
|
34
|
+
function authorizationFromFields(fields, secrets) {
|
|
35
|
+
addFieldSecrets(fields, secrets);
|
|
36
|
+
const decodedPassword = fields._password === undefined ? undefined : decodeBase64(fields._password);
|
|
37
|
+
if (fields._authToken !== undefined)
|
|
38
|
+
return `Bearer ${fields._authToken}`;
|
|
39
|
+
if (fields._auth !== undefined)
|
|
40
|
+
return `Basic ${fields._auth}`;
|
|
41
|
+
if (fields.username !== undefined && decodedPassword !== undefined)
|
|
42
|
+
return encodeBasic(fields.username, decodedPassword);
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
function decodeUserinfo(value) {
|
|
46
|
+
try {
|
|
47
|
+
return decodeURIComponent(value);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function userinfoAuthorization(registry, secrets) {
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = new URL(registry);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
if (parsed.username === '')
|
|
62
|
+
return undefined;
|
|
63
|
+
const password = decodeUserinfo(parsed.password);
|
|
64
|
+
secrets.add(password);
|
|
65
|
+
const path = parsed.pathname.endsWith('/') ? parsed.pathname : `${parsed.pathname}/`;
|
|
66
|
+
return { prefix: `//${parsed.host}${path}`, authorization: encodeBasic(decodeUserinfo(parsed.username), password) };
|
|
67
|
+
}
|
|
68
|
+
function addUnscopedSecrets(entries, secrets) {
|
|
69
|
+
for (const { key, value } of entries) {
|
|
70
|
+
const field = UNSCOPED_SECRET_FIELDS.find((candidate) => candidate === key);
|
|
71
|
+
if (field !== undefined && value !== '')
|
|
72
|
+
addFieldSecrets({ [field]: value }, secrets);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function collectFields(entries, fieldsByPrefix) {
|
|
76
|
+
for (const { key, value } of entries) {
|
|
77
|
+
const credentialKey = splitCredentialKey(key);
|
|
78
|
+
if (credentialKey === undefined)
|
|
79
|
+
continue;
|
|
80
|
+
if (value === '')
|
|
81
|
+
continue;
|
|
82
|
+
const fields = fieldsByPrefix.get(credentialKey.prefix) ?? {};
|
|
83
|
+
fields[credentialKey.field] = value;
|
|
84
|
+
fieldsByPrefix.set(credentialKey.prefix, fields);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function registryValues(entries, env) {
|
|
88
|
+
const fromEntries = entries
|
|
89
|
+
.filter(({ key }) => key === 'registry' || SCOPED_REGISTRY_KEY.test(key))
|
|
90
|
+
.map(({ value }) => value);
|
|
91
|
+
const fromEnv = npmConfigEnv(env, 'registry');
|
|
92
|
+
return fromEnv === undefined ? fromEntries : [...fromEntries, fromEnv];
|
|
93
|
+
}
|
|
94
|
+
function candidatePrefixes(url) {
|
|
95
|
+
let parsed;
|
|
96
|
+
try {
|
|
97
|
+
parsed = new URL(url);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
if (!WEB_PROTOCOLS.includes(parsed.protocol) || !parsed.pathname.startsWith('/'))
|
|
103
|
+
return [];
|
|
104
|
+
const rootPrefix = `//${parsed.host}/`;
|
|
105
|
+
const candidates = [`//${parsed.host}${parsed.pathname}`];
|
|
106
|
+
let current = candidates[0];
|
|
107
|
+
while (current !== rootPrefix) {
|
|
108
|
+
const withoutTrailingSlash = current.endsWith('/') ? current.slice(0, -1) : current;
|
|
109
|
+
current = withoutTrailingSlash.slice(0, withoutTrailingSlash.lastIndexOf('/') + 1);
|
|
110
|
+
candidates.push(current);
|
|
111
|
+
}
|
|
112
|
+
return candidates;
|
|
113
|
+
}
|
|
114
|
+
export function readRegistryCredentials(sources, secrets) {
|
|
115
|
+
const entries = [
|
|
116
|
+
...parseNpmrcEntries(sources.userNpmrc ?? '', sources.env),
|
|
117
|
+
...parseNpmrcEntries(sources.projectNpmrc ?? '', sources.env),
|
|
118
|
+
];
|
|
119
|
+
addUnscopedSecrets(entries, secrets);
|
|
120
|
+
const fieldsByPrefix = new Map();
|
|
121
|
+
collectFields(entries, fieldsByPrefix);
|
|
122
|
+
const authorizationByPrefix = new Map();
|
|
123
|
+
for (const registry of registryValues(entries, sources.env)) {
|
|
124
|
+
const fromUserinfo = userinfoAuthorization(registry, secrets);
|
|
125
|
+
if (fromUserinfo !== undefined)
|
|
126
|
+
authorizationByPrefix.set(fromUserinfo.prefix, fromUserinfo.authorization);
|
|
127
|
+
}
|
|
128
|
+
for (const [prefix, fields] of fieldsByPrefix) {
|
|
129
|
+
const authorization = authorizationFromFields(fields, secrets);
|
|
130
|
+
if (authorization !== undefined)
|
|
131
|
+
authorizationByPrefix.set(prefix, authorization);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
authorizationFor: (url) => {
|
|
135
|
+
for (const candidate of candidatePrefixes(url)) {
|
|
136
|
+
const authorization = authorizationByPrefix.get(candidate);
|
|
137
|
+
if (authorization !== undefined)
|
|
138
|
+
return authorization;
|
|
139
|
+
}
|
|
140
|
+
return undefined;
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export const ENRICHMENT_CACHE_FORMAT = 1;
|
|
5
|
+
export const ENRICHMENT_TTL_MS = 43_200_000;
|
|
6
|
+
const KEY_SEPARATOR = '\0';
|
|
7
|
+
const DIRECTORY_SEGMENTS_BY_KIND = {
|
|
8
|
+
registry: ['registry'],
|
|
9
|
+
'osv-matches': ['osv', 'matches'],
|
|
10
|
+
'osv-packages': ['osv', 'packages'],
|
|
11
|
+
'registry-versions': ['registry-versions'],
|
|
12
|
+
};
|
|
13
|
+
export function createEnrichmentCache(dir) {
|
|
14
|
+
const directoryOf = (kind) => path.join(dir, ...DIRECTORY_SEGMENTS_BY_KIND[kind]);
|
|
15
|
+
const fileOf = (kind, keyParts) => {
|
|
16
|
+
const hash = createHash('sha256')
|
|
17
|
+
.update([String(ENRICHMENT_CACHE_FORMAT), ...keyParts].join(KEY_SEPARATOR))
|
|
18
|
+
.digest('hex');
|
|
19
|
+
return path.join(directoryOf(kind), `${hash}.json`);
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
async read(kind, keyParts) {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(await readFile(fileOf(kind, keyParts), 'utf8'));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
async write(kind, keyParts, value) {
|
|
31
|
+
await mkdir(directoryOf(kind), { recursive: true });
|
|
32
|
+
const file = fileOf(kind, keyParts);
|
|
33
|
+
const temporaryFile = `${file}.${randomUUID()}.tmp`;
|
|
34
|
+
await writeFile(temporaryFile, JSON.stringify(value));
|
|
35
|
+
await rename(temporaryFile, file);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function isFresh(fetchedAt, now, ttlMs = ENRICHMENT_TTL_MS) {
|
|
40
|
+
const fetchedAtMs = Date.parse(fetchedAt);
|
|
41
|
+
return Number.isFinite(fetchedAtMs) && now - fetchedAtMs < ttlMs;
|
|
42
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
import { PLACEHOLDER_PREFIX } from "../model.js";
|
|
3
|
+
import { versionKey } from "./osv-client.js";
|
|
4
|
+
const KEY_SEPARATOR = '\0';
|
|
5
|
+
export function isTrackable(root) {
|
|
6
|
+
return root.status === 'ready' && root.config !== undefined;
|
|
7
|
+
}
|
|
8
|
+
export function isPlaceholder(node) {
|
|
9
|
+
return node.id.startsWith(PLACEHOLDER_PREFIX);
|
|
10
|
+
}
|
|
11
|
+
export function isRegistryPackage(node) {
|
|
12
|
+
return node.nonRegistry !== true && !isPlaceholder(node);
|
|
13
|
+
}
|
|
14
|
+
function scopeOf(name) {
|
|
15
|
+
const slash = name.indexOf('/');
|
|
16
|
+
return name.startsWith('@') && slash > 0 ? name.slice(0, slash) : undefined;
|
|
17
|
+
}
|
|
18
|
+
export function inPrivateScope(name, privateScopes) {
|
|
19
|
+
const scope = scopeOf(name);
|
|
20
|
+
return scope !== undefined && privateScopes.has(scope);
|
|
21
|
+
}
|
|
22
|
+
export function hasKnownVersion(version) {
|
|
23
|
+
return semver.valid(version) !== null;
|
|
24
|
+
}
|
|
25
|
+
export function registryUrlOf(config, name) {
|
|
26
|
+
const scope = scopeOf(name);
|
|
27
|
+
const configured = (scope === undefined ? undefined : config.scopedRegistries[scope]) ?? config.registry;
|
|
28
|
+
return configured.endsWith('/') ? configured : `${configured}/`;
|
|
29
|
+
}
|
|
30
|
+
export function registryKeyOf(registryUrl, name) {
|
|
31
|
+
return `${registryUrl}${KEY_SEPARATOR}${name}`;
|
|
32
|
+
}
|
|
33
|
+
function addRegistryTarget(registry, root, node) {
|
|
34
|
+
const registryUrl = registryUrlOf(root.config, node.name);
|
|
35
|
+
const key = registryKeyOf(registryUrl, node.name);
|
|
36
|
+
const existing = registry.get(key);
|
|
37
|
+
if (existing !== undefined) {
|
|
38
|
+
existing.versionsInUse.add(node.version);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
registry.set(key, { key, registryUrl, name: node.name, versionsInUse: new Set([node.version]), credentialsRoot: root });
|
|
42
|
+
}
|
|
43
|
+
export function planKeys(roots, privateScopesSoFar) {
|
|
44
|
+
const trackable = roots.filter(isTrackable);
|
|
45
|
+
const privateScopes = new Set([...privateScopesSoFar, ...trackable.flatMap((root) => Object.keys(root.config.scopedRegistries))]);
|
|
46
|
+
const registry = new Map();
|
|
47
|
+
const versions = new Map();
|
|
48
|
+
const skipped = new Set();
|
|
49
|
+
for (const root of trackable) {
|
|
50
|
+
for (const node of root.packages.values()) {
|
|
51
|
+
if (!isRegistryPackage(node))
|
|
52
|
+
continue;
|
|
53
|
+
addRegistryTarget(registry, root, node);
|
|
54
|
+
const key = versionKey(node.name, node.version);
|
|
55
|
+
if (versions.has(key) || skipped.has(key))
|
|
56
|
+
continue;
|
|
57
|
+
if (inPrivateScope(node.name, privateScopes) || !hasKnownVersion(node.version))
|
|
58
|
+
skipped.add(key);
|
|
59
|
+
else
|
|
60
|
+
versions.set(key, { key, name: node.name, version: node.version });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
roots: trackable,
|
|
65
|
+
registry,
|
|
66
|
+
versions,
|
|
67
|
+
skippedVersions: skipped.size,
|
|
68
|
+
privateScopes,
|
|
69
|
+
waitsForScanningRoots: roots.some((root) => root.status === 'scanning'),
|
|
70
|
+
};
|
|
71
|
+
}
|