@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,227 @@
1
+ #!/usr/bin/env node
2
+ import { realpathSync } from 'node:fs';
3
+ import { readFile, stat } from 'node:fs/promises';
4
+ import { homedir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { parseArgs } from 'node:util';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { createActionService } from "../actions/action-service.js";
9
+ import { createProcessRunner, spawnRunner } from "../adapters/command-runner.js";
10
+ import { removeResolveTempDirs } from "../adapters/resolve-today.js";
11
+ import { serializeScan } from "../graph/serialize.js";
12
+ import { userNpmrcPath } from "../config/npmrc.js";
13
+ import { processSecrets } from "../config/secrets.js";
14
+ import { createEnrichmentCache } from "../enrich/enrichment-cache.js";
15
+ import { createEnrichmentService } from "../enrich/enrichment-service.js";
16
+ import { createHttpClient } from "../enrich/http.js";
17
+ import { OSV_DEFAULT_URL } from "../enrich/osv-client.js";
18
+ import { createScanCache } from "../scan/scan-cache.js";
19
+ import { killGitProcesses } from "../git/git-process.js";
20
+ import { isGitRepository, openGitRepository } from "../git/git-repository.js";
21
+ import { startScanSession } from "../scan/scan-session.js";
22
+ import { createSourceRegistry } from "../scan/source-registry.js";
23
+ import { createBranches } from "../server/branches.js";
24
+ import { startServer } from "../server/server.js";
25
+ import { createFsReader } from "../sources/file-reader.js";
26
+ import { createGitReader } from "../sources/git-reader.js";
27
+ import { openBrowser } from "./open-browser.js";
28
+ import { readPackageVersion } from "./package-version.js";
29
+ import { USAGE_HINT, USAGE_TEXT } from "./usage-text.js";
30
+ const EXIT_OK = 0;
31
+ const EXIT_PORT_IN_USE = 1;
32
+ const EXIT_USAGE = 2;
33
+ const OPTIONS = {
34
+ port: { type: 'string' },
35
+ 'no-open': { type: 'boolean' },
36
+ json: { type: 'boolean' },
37
+ offline: { type: 'boolean' },
38
+ help: { type: 'boolean', short: 'h' },
39
+ version: { type: 'boolean', short: 'v' },
40
+ };
41
+ async function isDirectory(target) {
42
+ try {
43
+ return (await stat(target)).isDirectory();
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
49
+ async function readUserNpmrc(env) {
50
+ try {
51
+ return await readFile(userNpmrcPath(env, env.HOME ?? homedir()), 'utf8');
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ function parsePort(raw) {
58
+ if (raw === undefined)
59
+ return undefined;
60
+ const port = Number(raw);
61
+ return Number.isInteger(port) && port >= 0 && port <= 65535 ? port : null;
62
+ }
63
+ function startEnrichment(setup) {
64
+ const abort = new AbortController();
65
+ return createEnrichmentService({
66
+ reader: setup.reader,
67
+ env: setup.env,
68
+ userNpmrc: setup.userNpmrc,
69
+ http: createHttpClient({ userAgent: `deptrail/${readPackageVersion()} node/${process.version}`, signal: abort.signal }),
70
+ osvUrl: setup.env.DEPTRAIL_OSV_URL ?? OSV_DEFAULT_URL,
71
+ cache: createEnrichmentCache(setup.cacheDir),
72
+ secrets: processSecrets,
73
+ offline: setup.offline,
74
+ abort,
75
+ });
76
+ }
77
+ async function openCommitSession(git, commit, sessionOptions, onRootPublished) {
78
+ if (!isGitRepository(git))
79
+ throw new Error(git.unavailable);
80
+ const index = await git.tree(commit);
81
+ return startScanSession({
82
+ ...sessionOptions,
83
+ reader: createGitReader(index, (blobId) => git.readBlob(blobId)),
84
+ useInstalled: false,
85
+ expandNpmrcVariables: false,
86
+ skippedSubmodules: index.submodules,
87
+ onRootPublished,
88
+ });
89
+ }
90
+ function waitForTermination(shutdown) {
91
+ return new Promise((resolve) => {
92
+ const stop = () => {
93
+ process.off('SIGINT', stop);
94
+ process.off('SIGTERM', stop);
95
+ shutdown().finally(resolve);
96
+ };
97
+ process.on('SIGINT', stop);
98
+ process.on('SIGTERM', stop);
99
+ });
100
+ }
101
+ export async function main(argv, env, io) {
102
+ let parsed;
103
+ try {
104
+ parsed = parseArgs({ args: argv, options: OPTIONS, allowPositionals: true });
105
+ }
106
+ catch (error) {
107
+ io.stderr(`${error instanceof Error ? error.message : String(error)}\n${USAGE_HINT}\n`);
108
+ return EXIT_USAGE;
109
+ }
110
+ if (parsed.values.help) {
111
+ io.stdout(USAGE_TEXT);
112
+ return EXIT_OK;
113
+ }
114
+ if (parsed.values.version) {
115
+ io.stdout(`${readPackageVersion()}\n`);
116
+ return EXIT_OK;
117
+ }
118
+ const port = parsePort(parsed.values.port);
119
+ if (port === null) {
120
+ io.stderr('Invalid --port: expected an integer between 0 and 65535\n');
121
+ return EXIT_USAGE;
122
+ }
123
+ const repoPath = path.resolve(parsed.positionals[0] ?? '.');
124
+ if (!(await isDirectory(repoPath))) {
125
+ io.stderr(`Not a directory: ${repoPath}\n`);
126
+ return EXIT_USAGE;
127
+ }
128
+ const reader = createFsReader(repoPath);
129
+ const userNpmrc = await readUserNpmrc(env);
130
+ const cacheDir = env.DEPTRAIL_CACHE_DIR ?? path.join(homedir(), '.cache', 'deptrail');
131
+ const sessionOptions = { repoPath, reader, runner: spawnRunner, cache: createScanCache(cacheDir), env, userNpmrc };
132
+ if (parsed.values.json) {
133
+ const session = await startScanSession(sessionOptions);
134
+ await session.ready;
135
+ io.stdout(`${JSON.stringify(serializeScan(session.scan))}\n`);
136
+ return EXIT_OK;
137
+ }
138
+ const enrichment = startEnrichment({ reader, env, userNpmrc, cacheDir, offline: parsed.values.offline === true });
139
+ let actions;
140
+ let sources;
141
+ const session = await startScanSession({
142
+ ...sessionOptions,
143
+ onRootPublished: (roots) => {
144
+ if (sources)
145
+ sources.rootsChanged();
146
+ else
147
+ enrichment.track(roots);
148
+ actions?.track(roots);
149
+ },
150
+ });
151
+ const git = await openGitRepository({ cwd: repoPath, env, redact: (text) => processSecrets.redact(text) });
152
+ const registry = createSourceRegistry({
153
+ working: session,
154
+ openSession: (commit) => openCommitSession(git, commit, sessionOptions, () => registry.rootsChanged()),
155
+ onRootsChanged: (roots) => enrichment.track(roots),
156
+ });
157
+ sources = registry;
158
+ const closeGit = () => {
159
+ registry.close();
160
+ if (isGitRepository(git))
161
+ git.close();
162
+ killGitProcesses();
163
+ };
164
+ const actionService = createActionService({
165
+ repoPath,
166
+ session,
167
+ enrichment,
168
+ runner: createProcessRunner(),
169
+ env,
170
+ offline: parsed.values.offline === true,
171
+ secrets: processSecrets,
172
+ });
173
+ actions = actionService;
174
+ actionService.track(session.scan.roots);
175
+ let server;
176
+ try {
177
+ server = await startServer({
178
+ session,
179
+ enrichment,
180
+ actions: actionService,
181
+ branches: createBranches(git, registry),
182
+ port,
183
+ staticDir: fileURLToPath(new URL('../ui', import.meta.url)),
184
+ errorStream: { write: io.stderr },
185
+ });
186
+ }
187
+ catch (error) {
188
+ enrichment.close();
189
+ closeGit();
190
+ await actionService.close();
191
+ if (error.code === 'EADDRINUSE' && port !== undefined && port !== 0) {
192
+ io.stderr(`Port ${port} is in use\n`);
193
+ return EXIT_PORT_IN_USE;
194
+ }
195
+ throw error;
196
+ }
197
+ io.stdout(`deptrail: ${server.url}\n`);
198
+ if (!parsed.values['no-open'])
199
+ io.openBrowser(server.url);
200
+ await waitForTermination(async () => {
201
+ enrichment.close();
202
+ spawnRunner.killRunning();
203
+ closeGit();
204
+ await Promise.all([removeResolveTempDirs(), server.close(), actionService.close()]);
205
+ });
206
+ return EXIT_OK;
207
+ }
208
+ function isInvokedDirectly() {
209
+ if (process.argv[1] === undefined)
210
+ return false;
211
+ try {
212
+ return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
213
+ }
214
+ catch {
215
+ return false;
216
+ }
217
+ }
218
+ if (isInvokedDirectly()) {
219
+ main(process.argv.slice(2), process.env, {
220
+ stdout: (text) => process.stdout.write(text),
221
+ stderr: (text) => process.stderr.write(text),
222
+ openBrowser,
223
+ }).then((code) => process.exit(code), (error) => {
224
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
225
+ process.exit(1);
226
+ });
227
+ }
@@ -0,0 +1,14 @@
1
+ import { spawn } from 'node:child_process';
2
+ export function browserCommand(platform, url) {
3
+ if (platform === 'darwin')
4
+ return { command: 'open', args: [url] };
5
+ if (platform === 'win32')
6
+ return { command: 'cmd', args: ['/c', 'start', '""', url] };
7
+ return { command: 'xdg-open', args: [url] };
8
+ }
9
+ export function openBrowser(url) {
10
+ const { command, args } = browserCommand(process.platform, url);
11
+ const child = spawn(command, args, { stdio: 'ignore', detached: true });
12
+ child.on('error', () => { });
13
+ child.unref();
14
+ }
@@ -0,0 +1,6 @@
1
+ import { readFileSync } from 'node:fs';
2
+ const PACKAGE_MANIFEST = new URL('../../package.json', import.meta.url);
3
+ export function readPackageVersion() {
4
+ const manifest = JSON.parse(readFileSync(PACKAGE_MANIFEST, 'utf8'));
5
+ return manifest.version;
6
+ }
@@ -0,0 +1,27 @@
1
+ export const USAGE_HINT = 'Run deptrail --help to see the options.';
2
+ export const USAGE_TEXT = `Usage: deptrail [path] [options]
3
+
4
+ Explores the dependency trees of every JavaScript root in a repository and
5
+ serves a local viewer of their issues, dependents and fixes.
6
+
7
+ Arguments:
8
+ path Repository to scan. Defaults to the current directory.
9
+
10
+ Options:
11
+ --port <n> Port for the local server. Defaults to a free port.
12
+ --no-open Print the viewer URL without opening a browser.
13
+ --json Print the normalized scan as JSON and exit, without
14
+ starting the server or using the network.
15
+ --offline Send no requests to registries or OSV.dev.
16
+ -h, --help Show this help and exit.
17
+ -v, --version Print the version and exit.
18
+
19
+ Environment:
20
+ DEPTRAIL_CACHE_DIR Cache directory instead of ~/.cache/deptrail.
21
+ DEPTRAIL_OSV_URL Base URL of an OSV-compatible API instead of
22
+ https://api.osv.dev.
23
+ npm_config_registry Default registry, as with npm.
24
+ npm_config_userconfig User config file to read instead of ~/.npmrc.
25
+
26
+ Documentation: https://github.com/loomhelix/deptrail#readme
27
+ `;
@@ -0,0 +1,119 @@
1
+ const ANY_SEGMENTS = Symbol('any-segments');
2
+ const PARENT_SEGMENT = '..';
3
+ const HIDDEN_PREFIX = '.';
4
+ const NEGATION_PREFIX = '!';
5
+ const CLASS_NEGATIONS = new Set(['!', '^']);
6
+ function braceGroupAt(pattern, start) {
7
+ const alternatives = [];
8
+ let depth = 0;
9
+ let alternativeStart = start + 1;
10
+ for (let index = start; index < pattern.length; index++) {
11
+ const character = pattern[index];
12
+ if (character === '{')
13
+ depth++;
14
+ if (character === ',' && depth === 1) {
15
+ alternatives.push(pattern.slice(alternativeStart, index));
16
+ alternativeStart = index + 1;
17
+ }
18
+ if (character === '}' && --depth === 0) {
19
+ alternatives.push(pattern.slice(alternativeStart, index));
20
+ return alternatives.length > 1 ? { start, end: index, alternatives } : undefined;
21
+ }
22
+ }
23
+ return undefined;
24
+ }
25
+ function firstBraceGroup(pattern) {
26
+ for (let start = pattern.indexOf('{'); start !== -1; start = pattern.indexOf('{', start + 1)) {
27
+ const group = braceGroupAt(pattern, start);
28
+ if (group !== undefined)
29
+ return group;
30
+ }
31
+ return undefined;
32
+ }
33
+ function expandBraces(pattern) {
34
+ const group = firstBraceGroup(pattern);
35
+ if (group === undefined)
36
+ return [pattern];
37
+ const prefix = pattern.slice(0, group.start);
38
+ const suffix = pattern.slice(group.end + 1);
39
+ return group.alternatives.flatMap((alternative) => expandBraces(`${prefix}${alternative}${suffix}`));
40
+ }
41
+ function expandPattern(rawPattern) {
42
+ const negated = rawPattern.startsWith(NEGATION_PREFIX);
43
+ const body = negated ? rawPattern.slice(NEGATION_PREFIX.length) : rawPattern;
44
+ return expandBraces(body).map((expanded) => (negated ? `${NEGATION_PREFIX}${expanded}` : expanded));
45
+ }
46
+ function escapeLiteral(text) {
47
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
48
+ }
49
+ function classToRegExp(segment, open) {
50
+ const negated = CLASS_NEGATIONS.has(segment[open + 1] ?? '');
51
+ const bodyStart = negated ? open + 2 : open + 1;
52
+ const close = segment.indexOf(']', bodyStart + 1);
53
+ if (close === -1)
54
+ return undefined;
55
+ const body = segment.slice(bodyStart, close).replace(/[\\\]^]/g, '\\$&');
56
+ return { source: `[${negated ? '^' : ''}${body}]`, next: close + 1 };
57
+ }
58
+ function segmentToRegExp(segment) {
59
+ let source = '';
60
+ let index = 0;
61
+ while (index < segment.length) {
62
+ const character = segment[index];
63
+ const characterClass = character === '[' ? classToRegExp(segment, index) : undefined;
64
+ if (characterClass !== undefined) {
65
+ source += characterClass.source;
66
+ index = characterClass.next;
67
+ continue;
68
+ }
69
+ if (character === '*')
70
+ source += '[^/]*';
71
+ else if (character === '?')
72
+ source += '[^/]';
73
+ else
74
+ source += escapeLiteral(character);
75
+ index++;
76
+ }
77
+ const parentGuard = segment === PARENT_SEGMENT ? '' : '(?!\\.\\.$)';
78
+ const hiddenGuard = segment.startsWith(HIDDEN_PREFIX) ? '' : '(?!\\.)';
79
+ return new RegExp(`^${parentGuard}${hiddenGuard}${source}$`);
80
+ }
81
+ function compilePattern(rawPattern) {
82
+ const negated = rawPattern.startsWith('!');
83
+ const body = (negated ? rawPattern.slice(1) : rawPattern).replace(/^\.\//, '').replace(/\/+$/, '');
84
+ const segments = body
85
+ .split('/')
86
+ .filter((segment) => segment !== '')
87
+ .map((segment) => (segment === '**' ? ANY_SEGMENTS : segmentToRegExp(segment)));
88
+ return { negated, segments };
89
+ }
90
+ function matchesSegments(pattern, patternIndex, path, pathIndex) {
91
+ if (patternIndex === pattern.length)
92
+ return pathIndex === path.length;
93
+ const current = pattern[patternIndex];
94
+ if (current === ANY_SEGMENTS) {
95
+ for (let skipTo = pathIndex; skipTo <= path.length; skipTo++) {
96
+ if (matchesSegments(pattern, patternIndex + 1, path, skipTo))
97
+ return true;
98
+ if (path[skipTo]?.startsWith(HIDDEN_PREFIX))
99
+ return false;
100
+ }
101
+ return false;
102
+ }
103
+ if (pathIndex === path.length || !current.test(path[pathIndex]))
104
+ return false;
105
+ return matchesSegments(pattern, patternIndex + 1, path, pathIndex + 1);
106
+ }
107
+ export function compileWorkspacePatterns(patterns) {
108
+ const compiled = patterns.flatMap(expandPattern).map(compilePattern);
109
+ return (relPath) => {
110
+ const pathSegments = relPath.split('/').filter((segment) => segment !== '');
111
+ let included = false;
112
+ for (const pattern of compiled) {
113
+ if (pattern.negated === included && matchesSegments(pattern.segments, 0, pathSegments, 0)) {
114
+ included = !pattern.negated;
115
+ }
116
+ }
117
+ return included;
118
+ };
119
+ }
@@ -0,0 +1,116 @@
1
+ import { join } from 'node:path';
2
+ const AUTH_KEY_SUFFIXES = ['_authToken', '_auth', '_password'];
3
+ const SCOPED_REGISTRY_KEY = /^(@[^:]+):registry$/;
4
+ const SCOPED_AUTH_KEY_PREFIX = '//';
5
+ const NPM_CONFIG_PREFIX = 'npm_config_';
6
+ const HOME_PREFIX = '~/';
7
+ const COMMENT_CHARACTERS = ';#';
8
+ const VARIABLE_REFERENCE = /\$\{[^}]+\}/;
9
+ function expandVariables(value, env) {
10
+ return value.replace(/\$\{([^}]+)\}/g, (_match, name) => env[name] ?? '');
11
+ }
12
+ const URL_USERINFO = /^([a-z][a-z0-9+.-]*:\/\/)[^/?#]*@/i;
13
+ const URL_USERINFO_IN_TEXT = /(?<![a-z0-9+.-])([a-z][a-z0-9+.-]*:\/\/)[^/?#\n]*@/gi;
14
+ const PROTOCOL_RELATIVE_USERINFO = /(^|[\s'"(])\/\/[^/?#\s@]+@/g;
15
+ const AUTH_ASSIGNMENT = /(_authToken|_auth|_password|npm_token)(\s*[=:]\s*)\S+/gi;
16
+ const BEARER_TOKEN = /\bBearer\s+\S+/gi;
17
+ const REDACTED = '[redacted]';
18
+ export function stripRegistryCredentials(url) {
19
+ const stripped = url.replace(URL_USERINFO, '$1');
20
+ return { url: stripped, hadCredentials: stripped !== url };
21
+ }
22
+ export function redactCredentials(text) {
23
+ return text
24
+ .replace(URL_USERINFO_IN_TEXT, '$1')
25
+ .replace(PROTOCOL_RELATIVE_USERINFO, '$1//')
26
+ .replace(AUTH_ASSIGNMENT, `$1$2${REDACTED}`)
27
+ .replace(BEARER_TOKEN, `Bearer ${REDACTED}`);
28
+ }
29
+ function cutAtInlineComment(value) {
30
+ let kept = '';
31
+ for (let index = 0; index < value.length; index++) {
32
+ const character = value[index];
33
+ const next = value[index + 1];
34
+ if (character === '\\' && next !== undefined && COMMENT_CHARACTERS.includes(next)) {
35
+ kept += next;
36
+ index++;
37
+ }
38
+ else if (COMMENT_CHARACTERS.includes(character)) {
39
+ break;
40
+ }
41
+ else {
42
+ kept += character;
43
+ }
44
+ }
45
+ return kept.trim();
46
+ }
47
+ function unquoteValue(rawValue) {
48
+ const isWrappedIn = (quote) => rawValue.length >= 2 && rawValue.startsWith(quote) && rawValue.endsWith(quote);
49
+ if (isWrappedIn('"')) {
50
+ try {
51
+ return String(JSON.parse(rawValue));
52
+ }
53
+ catch {
54
+ return rawValue;
55
+ }
56
+ }
57
+ if (isWrappedIn("'"))
58
+ return rawValue.slice(1, -1);
59
+ return cutAtInlineComment(rawValue);
60
+ }
61
+ export function parseNpmrcEntries(text, env) {
62
+ const entries = [];
63
+ for (const rawLine of text.split(/\r?\n/)) {
64
+ const line = rawLine.trim();
65
+ if (line === '' || line.startsWith(';') || line.startsWith('#'))
66
+ continue;
67
+ const separatorIndex = line.indexOf('=');
68
+ if (separatorIndex === -1)
69
+ continue;
70
+ const key = line.slice(0, separatorIndex).trim();
71
+ const value = expandVariables(unquoteValue(line.slice(separatorIndex + 1).trim()), env);
72
+ entries.push({ key, value });
73
+ }
74
+ return entries;
75
+ }
76
+ export function withoutVariableReferences(text) {
77
+ return text
78
+ .split(/\r?\n/)
79
+ .filter((line) => !VARIABLE_REFERENCE.test(line))
80
+ .join('\n');
81
+ }
82
+ export function npmConfigEnv(env, name) {
83
+ const exactKey = `${NPM_CONFIG_PREFIX}${name}`;
84
+ if (env[exactKey] !== undefined)
85
+ return env[exactKey];
86
+ const matchingKey = Object.keys(env).find((key) => key.toLowerCase() === exactKey && env[key] !== undefined);
87
+ return matchingKey === undefined ? undefined : env[matchingKey];
88
+ }
89
+ export function userNpmrcPath(env, home) {
90
+ const configured = npmConfigEnv(env, 'userconfig');
91
+ if (configured === undefined || configured === '')
92
+ return join(home, '.npmrc');
93
+ return configured.startsWith(HOME_PREFIX) ? join(home, configured.slice(HOME_PREFIX.length)) : configured;
94
+ }
95
+ export function parseNpmrc(text, env) {
96
+ const values = { scopedRegistries: {}, hasAuth: false, unscopedAuthKeys: [] };
97
+ for (const { key, value } of parseNpmrcEntries(text, env)) {
98
+ if (AUTH_KEY_SUFFIXES.some((suffix) => key.endsWith(suffix))) {
99
+ values.hasAuth = true;
100
+ if (!key.startsWith(SCOPED_AUTH_KEY_PREFIX))
101
+ values.unscopedAuthKeys.push(key);
102
+ continue;
103
+ }
104
+ const scopedMatch = SCOPED_REGISTRY_KEY.exec(key);
105
+ if (key !== 'registry' && !scopedMatch)
106
+ continue;
107
+ const registry = stripRegistryCredentials(value);
108
+ if (registry.hadCredentials)
109
+ values.hasAuth = true;
110
+ if (!scopedMatch)
111
+ values.registry = registry.url;
112
+ else if (registry.url !== '')
113
+ values.scopedRegistries[scopedMatch[1]] = registry.url;
114
+ }
115
+ return values;
116
+ }
@@ -0,0 +1,103 @@
1
+ import { posix } from 'node:path';
2
+ import { parse } from 'yaml';
3
+ import { readManifest } from "../adapters/package-manifest.js";
4
+ import { npmConfigEnv, parseNpmrc, stripRegistryCredentials, withoutVariableReferences } from "./npmrc.js";
5
+ const DEFAULT_REGISTRY = 'https://registry.npmjs.org/';
6
+ const DEFAULT_CATALOG = 'default';
7
+ const NESTED_SELF_KEY = '.';
8
+ const USER_NPMRC_LABEL = 'user .npmrc';
9
+ const PROJECT_NPMRC_LABEL = 'project .npmrc';
10
+ function asRecord(value) {
11
+ return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : null;
12
+ }
13
+ function asStringArray(value) {
14
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === 'string') : [];
15
+ }
16
+ function stringEntries(value) {
17
+ const record = asRecord(value);
18
+ if (!record)
19
+ return {};
20
+ return Object.fromEntries(Object.entries(record).filter(([, entry]) => typeof entry === 'string'));
21
+ }
22
+ function flattenOverrides(value, parentSelector, flattened) {
23
+ const record = asRecord(value);
24
+ if (!record)
25
+ return;
26
+ for (const [key, entry] of Object.entries(record)) {
27
+ if (typeof entry === 'string') {
28
+ const selector = key === NESTED_SELF_KEY && parentSelector !== null ? parentSelector : joinSelector(parentSelector, key);
29
+ flattened[selector] = entry;
30
+ }
31
+ else {
32
+ flattenOverrides(entry, joinSelector(parentSelector, key), flattened);
33
+ }
34
+ }
35
+ }
36
+ function joinSelector(parentSelector, key) {
37
+ return parentSelector === null ? key : `${parentSelector} > ${key}`;
38
+ }
39
+ function unscopedAuthWarnings(npmrc, label) {
40
+ return npmrc.unscopedAuthKeys.map((key) => `${key} in ${label} is not scoped to a registry and is ignored`);
41
+ }
42
+ async function readWorkspaceYaml(reader, rootPath) {
43
+ const text = await reader.readText(posix.join(rootPath, 'pnpm-workspace.yaml'));
44
+ if (text === null)
45
+ return {};
46
+ try {
47
+ return asRecord(parse(text)) ?? {};
48
+ }
49
+ catch {
50
+ return {};
51
+ }
52
+ }
53
+ async function readManifestRecord(reader, rootPath) {
54
+ const result = await readManifest(reader, rootPath);
55
+ return 'manifest' in result ? result.manifest.record : {};
56
+ }
57
+ function workspacePatternsOf(workspaceYaml, manifest) {
58
+ const fromYaml = asStringArray(workspaceYaml.packages);
59
+ if (fromYaml.length > 0)
60
+ return fromYaml;
61
+ const workspaces = manifest.workspaces;
62
+ return Array.isArray(workspaces) ? asStringArray(workspaces) : asStringArray(asRecord(workspaces)?.packages);
63
+ }
64
+ export async function readWorkspacePatterns(reader, rootPath) {
65
+ const workspaceYaml = await readWorkspaceYaml(reader, rootPath);
66
+ const manifest = await readManifestRecord(reader, rootPath);
67
+ return workspacePatternsOf(workspaceYaml, manifest);
68
+ }
69
+ function catalogsOf(workspaceYaml) {
70
+ const catalogs = {};
71
+ const defaultCatalog = stringEntries(workspaceYaml.catalog);
72
+ if (Object.keys(defaultCatalog).length > 0)
73
+ catalogs[DEFAULT_CATALOG] = defaultCatalog;
74
+ for (const [name, entries] of Object.entries(asRecord(workspaceYaml.catalogs) ?? {})) {
75
+ catalogs[name] = stringEntries(entries);
76
+ }
77
+ return catalogs;
78
+ }
79
+ function overridesOf(workspaceYaml, manifest) {
80
+ const overrides = {};
81
+ flattenOverrides(workspaceYaml.overrides, null, overrides);
82
+ flattenOverrides(manifest.overrides, null, overrides);
83
+ flattenOverrides(asRecord(manifest.pnpm)?.overrides, null, overrides);
84
+ return overrides;
85
+ }
86
+ export async function readRootConfig(reader, rootPath, sources) {
87
+ const projectNpmrcText = await reader.readText(posix.join(rootPath, '.npmrc'));
88
+ const userNpmrc = parseNpmrc(sources.userNpmrc ?? '', sources.env);
89
+ const projectText = projectNpmrcText ?? '';
90
+ const projectNpmrc = parseNpmrc(sources.expandProjectVariables === false ? withoutVariableReferences(projectText) : projectText, sources.env);
91
+ const workspaceYaml = await readWorkspaceYaml(reader, rootPath);
92
+ const manifest = await readManifestRecord(reader, rootPath);
93
+ const envRegistry = stripRegistryCredentials(npmConfigEnv(sources.env, 'registry') ?? '');
94
+ return {
95
+ registry: envRegistry.url || projectNpmrc.registry || userNpmrc.registry || DEFAULT_REGISTRY,
96
+ scopedRegistries: { ...userNpmrc.scopedRegistries, ...projectNpmrc.scopedRegistries },
97
+ hasAuth: userNpmrc.hasAuth || projectNpmrc.hasAuth || envRegistry.hadCredentials,
98
+ catalogs: catalogsOf(workspaceYaml),
99
+ overrides: overridesOf(workspaceYaml, manifest),
100
+ workspacePatterns: workspacePatternsOf(workspaceYaml, manifest),
101
+ warnings: [...unscopedAuthWarnings(userNpmrc, USER_NPMRC_LABEL), ...unscopedAuthWarnings(projectNpmrc, PROJECT_NPMRC_LABEL)],
102
+ };
103
+ }
@@ -0,0 +1,18 @@
1
+ import { redactCredentials } from "./npmrc.js";
2
+ export const MIN_SECRET_LENGTH = 6;
3
+ const REDACTED = '[redacted]';
4
+ export function createSecretSet() {
5
+ const known = new Set();
6
+ return {
7
+ add(value) {
8
+ if (value.length >= MIN_SECRET_LENGTH)
9
+ known.add(value);
10
+ },
11
+ redact(text) {
12
+ const longestFirst = [...known].sort((left, right) => right.length - left.length);
13
+ const withoutKnown = longestFirst.reduce((current, secret) => current.split(secret).join(REDACTED), text);
14
+ return redactCredentials(withoutKnown);
15
+ },
16
+ };
17
+ }
18
+ export const processSecrets = createSecretSet();