@openrewrite/rewrite 8.68.0-20251202-044649 → 8.68.0-20251202-154952

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 (45) hide show
  1. package/dist/javascript/assertions.d.ts +1 -0
  2. package/dist/javascript/assertions.d.ts.map +1 -1
  3. package/dist/javascript/assertions.js +82 -11
  4. package/dist/javascript/assertions.js.map +1 -1
  5. package/dist/javascript/dependency-workspace.d.ts +46 -5
  6. package/dist/javascript/dependency-workspace.d.ts.map +1 -1
  7. package/dist/javascript/dependency-workspace.js +70 -35
  8. package/dist/javascript/dependency-workspace.js.map +1 -1
  9. package/dist/javascript/index.d.ts +2 -0
  10. package/dist/javascript/index.d.ts.map +1 -1
  11. package/dist/javascript/index.js +2 -0
  12. package/dist/javascript/index.js.map +1 -1
  13. package/dist/javascript/node-resolution-result.d.ts +204 -0
  14. package/dist/javascript/node-resolution-result.d.ts.map +1 -0
  15. package/dist/javascript/node-resolution-result.js +723 -0
  16. package/dist/javascript/node-resolution-result.js.map +1 -0
  17. package/dist/javascript/package-json-parser.d.ts +143 -0
  18. package/dist/javascript/package-json-parser.d.ts.map +1 -0
  19. package/dist/javascript/package-json-parser.js +773 -0
  20. package/dist/javascript/package-json-parser.js.map +1 -0
  21. package/dist/javascript/templating/engine.js +1 -1
  22. package/dist/javascript/templating/engine.js.map +1 -1
  23. package/dist/json/parser.js +10 -1
  24. package/dist/json/parser.js.map +1 -1
  25. package/dist/json/tree.d.ts +1 -1
  26. package/dist/json/tree.js +1 -1
  27. package/dist/json/tree.js.map +1 -1
  28. package/dist/parser.d.ts +1 -1
  29. package/dist/parser.d.ts.map +1 -1
  30. package/dist/rpc/request/parse.d.ts +4 -0
  31. package/dist/rpc/request/parse.d.ts.map +1 -1
  32. package/dist/rpc/request/parse.js +17 -1
  33. package/dist/rpc/request/parse.js.map +1 -1
  34. package/dist/version.txt +1 -1
  35. package/package.json +5 -2
  36. package/src/javascript/assertions.ts +73 -15
  37. package/src/javascript/dependency-workspace.ts +124 -46
  38. package/src/javascript/index.ts +2 -0
  39. package/src/javascript/node-resolution-result.ts +905 -0
  40. package/src/javascript/package-json-parser.ts +845 -0
  41. package/src/javascript/templating/engine.ts +1 -1
  42. package/src/json/parser.ts +18 -1
  43. package/src/json/tree.ts +1 -1
  44. package/src/parser.ts +1 -1
  45. package/src/rpc/request/parse.ts +20 -2
@@ -0,0 +1,905 @@
1
+ /*
2
+ * Copyright 2025 the original author or authors.
3
+ * <p>
4
+ * Licensed under the Moderne Source Available License (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ * <p>
8
+ * https://docs.moderne.io/licensing/moderne-source-available-license
9
+ * <p>
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import {findMarker, Marker, Markers} from "../markers";
17
+ import {randomId, UUID} from "../uuid";
18
+ import {asRef} from "../reference";
19
+ import {RpcCodecs, RpcReceiveQueue, RpcSendQueue} from "../rpc";
20
+ import {castDraft, createDraft, finishDraft} from "immer";
21
+ import * as semver from "semver";
22
+ import * as fsp from "fs/promises";
23
+ import * as path from "path";
24
+ import {homedir} from "os";
25
+
26
+ export const NodeResolutionResultKind = "org.openrewrite.javascript.marker.NodeResolutionResult" as const;
27
+ export const DependencyKind = "org.openrewrite.javascript.marker.NodeResolutionResult$Dependency" as const;
28
+ export const ResolvedDependencyKind = "org.openrewrite.javascript.marker.NodeResolutionResult$ResolvedDependency" as const;
29
+
30
+ /**
31
+ * Parsed package.json content structure.
32
+ */
33
+ export interface PackageJsonContent {
34
+ readonly name?: string;
35
+ readonly version?: string;
36
+ readonly description?: string;
37
+ readonly dependencies?: Record<string, string>;
38
+ readonly devDependencies?: Record<string, string>;
39
+ readonly peerDependencies?: Record<string, string>;
40
+ readonly optionalDependencies?: Record<string, string>;
41
+ readonly bundledDependencies?: string[];
42
+ readonly bundleDependencies?: string[]; // Legacy alias
43
+ readonly engines?: Record<string, string>;
44
+ }
45
+
46
+ /**
47
+ * Package entry in a package-lock.json packages map.
48
+ */
49
+ export interface PackageLockEntry {
50
+ readonly version?: string;
51
+ readonly resolved?: string;
52
+ readonly integrity?: string;
53
+ readonly license?: string;
54
+ readonly dependencies?: Record<string, string>;
55
+ readonly devDependencies?: Record<string, string>;
56
+ readonly peerDependencies?: Record<string, string>;
57
+ readonly optionalDependencies?: Record<string, string>;
58
+ readonly engines?: Record<string, string> | string[]; // Can be legacy array format
59
+ }
60
+
61
+ /**
62
+ * Parsed package-lock.json content structure (npm lockfile v3 format).
63
+ */
64
+ export interface PackageLockContent {
65
+ readonly name?: string;
66
+ readonly version?: string;
67
+ readonly lockfileVersion?: number;
68
+ readonly packages?: Record<string, PackageLockEntry>;
69
+ }
70
+
71
+ /**
72
+ * Represents the package manager used by a Node.js project.
73
+ */
74
+ export const enum PackageManager {
75
+ Npm = 'Npm',
76
+ YarnClassic = 'YarnClassic',
77
+ YarnBerry = 'YarnBerry',
78
+ Pnpm = 'Pnpm',
79
+ Bun = 'Bun',
80
+ }
81
+
82
+ /**
83
+ * Represents the scope/source of an npmrc configuration.
84
+ * Listed from lowest to highest priority.
85
+ */
86
+ export const enum NpmrcScope {
87
+ Global = 'Global', // $PREFIX/etc/npmrc
88
+ User = 'User', // $HOME/.npmrc
89
+ Project = 'Project', // .npmrc in project root
90
+ }
91
+
92
+ export const NpmrcKind = "org.openrewrite.javascript.marker.NodeResolutionResult$Npmrc" as const;
93
+
94
+ /**
95
+ * Represents npm configuration from a specific scope.
96
+ * Multiple Npmrc objects can be collected (one per scope) to allow
97
+ * recipes to merge configurations or modify specific scopes.
98
+ */
99
+ export interface Npmrc {
100
+ readonly kind: typeof NpmrcKind;
101
+ readonly scope: NpmrcScope;
102
+ readonly properties: Record<string, string>;
103
+ }
104
+
105
+ /**
106
+ * Represents a dependency request as declared in package.json.
107
+ * This is what a package asks for (name + version constraint).
108
+ *
109
+ * When the same name+versionConstraint appears multiple times, the same
110
+ * Dependency instance is reused. This enables reference deduplication
111
+ * during RPC serialization via asRef().
112
+ */
113
+ export interface Dependency {
114
+ readonly kind: typeof DependencyKind;
115
+ readonly name: string; // Package name (e.g., "react")
116
+ readonly versionConstraint: string; // Version constraint (e.g., "^18.2.0")
117
+ readonly resolved?: ResolvedDependency; // The resolved version of this dependency
118
+ }
119
+
120
+ /**
121
+ * Represents a resolved dependency from package-lock.json.
122
+ * This is what was actually installed (name + resolved version + its own dependencies).
123
+ *
124
+ * Each ResolvedDependency's dependency arrays contain Dependency objects (requests),
125
+ * which can be looked up in NodeResolutionResult.resolvedDependencies to find their resolved versions.
126
+ */
127
+ export interface ResolvedDependency {
128
+ readonly kind: typeof ResolvedDependencyKind;
129
+ readonly name: string; // Package name (e.g., "react")
130
+ readonly version: string; // Actual resolved version (e.g., "18.3.1")
131
+
132
+ // This package's own dependency requests
133
+ readonly dependencies?: Dependency[];
134
+ readonly devDependencies?: Dependency[];
135
+ readonly peerDependencies?: Dependency[];
136
+ readonly optionalDependencies?: Dependency[];
137
+
138
+ // Node/npm version requirements for this package
139
+ readonly engines?: Record<string, string>;
140
+
141
+ // SPDX license identifier (e.g., "MIT", "Apache-2.0")
142
+ readonly license?: string;
143
+ }
144
+
145
+ /**
146
+ * Contains metadata about a Node.js project, parsed from package.json and package-lock.json.
147
+ * Attached as a marker to JS.CompilationUnit to provide dependency context for recipes.
148
+ *
149
+ * Similar to MavenResolutionResult marker, this allows recipes to:
150
+ * - Query project dependencies
151
+ * - Check if specific packages are in use
152
+ * - Modify dependencies programmatically
153
+ * - Understand the project structure
154
+ *
155
+ * The model separates requests (Dependency) from resolutions (ResolvedDependency):
156
+ * - The dependency arrays contain Dependency objects (what was requested)
157
+ * - The resolvedDependencies list contains what was actually installed
158
+ */
159
+ export interface NodeResolutionResult extends Marker {
160
+ readonly kind: typeof NodeResolutionResultKind;
161
+ readonly id: UUID;
162
+
163
+ // Project metadata from package.json
164
+ readonly name?: string; // Project name
165
+ readonly version?: string; // Project version
166
+ readonly description?: string; // Project description
167
+ readonly path: string; // Path to the package.json file
168
+
169
+ // Paths to workspace package.json files (only populated on workspace root)
170
+ readonly workspacePackagePaths?: string[];
171
+
172
+ // Dependency requests organized by scope (from package.json)
173
+ readonly dependencies: Dependency[]; // Regular dependencies
174
+ readonly devDependencies: Dependency[]; // Development dependencies
175
+ readonly peerDependencies: Dependency[]; // Peer dependencies
176
+ readonly optionalDependencies: Dependency[]; // Optional dependencies
177
+ readonly bundledDependencies: Dependency[]; // Bundled dependencies
178
+
179
+ // Resolved dependencies from lock file - what was actually installed
180
+ // Use getAllResolvedVersions() to look up by name (handles multiple versions)
181
+ readonly resolvedDependencies: ResolvedDependency[];
182
+
183
+ // The package manager used by the project (npm, yarn, pnpm, etc.)
184
+ readonly packageManager?: PackageManager;
185
+
186
+ // Node/npm version requirements
187
+ readonly engines?: Record<string, string>;
188
+
189
+ // npm configuration from various scopes (global, user, project, env)
190
+ readonly npmrcConfigs?: Npmrc[];
191
+ }
192
+
193
+ /**
194
+ * Creates a NodeResolutionResult marker from a package.json file.
195
+ * Should be called during parsing to attach to JS.CompilationUnit.
196
+ *
197
+ * All Dependency instances are wrapped with asRef() to enable
198
+ * reference deduplication during RPC serialization.
199
+ *
200
+ * @param path Path to the package.json file
201
+ * @param packageJsonContent Parsed package.json content
202
+ * @param packageLockContent Optional parsed package-lock.json content for resolution info
203
+ * @param workspacePackagePaths Optional resolved paths to workspace package.json files (only for workspace root)
204
+ * @param packageManager Optional package manager that was detected from lock file
205
+ * @param npmrcConfigs Optional npm configuration from various scopes
206
+ */
207
+ export function createNodeResolutionResultMarker(
208
+ path: string,
209
+ packageJsonContent: PackageJsonContent,
210
+ packageLockContent?: PackageLockContent,
211
+ workspacePackagePaths?: string[],
212
+ packageManager?: PackageManager,
213
+ npmrcConfigs?: Npmrc[]
214
+ ): NodeResolutionResult {
215
+ // Cache for deduplicating resolved dependencies with the same name+version.
216
+ const resolvedDependencyCache = new Map<string, ResolvedDependency>();
217
+
218
+ // Index from package name to all resolved versions (for O(1) semver fallback lookup)
219
+ const nameToResolved = new Map<string, ResolvedDependency[]>();
220
+
221
+ // Map from lock file path to ResolvedDependency for path-based lookups.
222
+ // e.g., "node_modules/is-odd" -> ResolvedDependency for is-odd@3.0.1
223
+ const pathToResolved = new Map<string, ResolvedDependency>();
224
+
225
+ // Cache for deduplicating dependencies with the same name+versionConstraint+contextPath.
226
+ const dependencyCache = new Map<string, Dependency>();
227
+
228
+ /**
229
+ * Normalizes the engines field from package-lock.json.
230
+ * Some older packages have engines as an array like ["node >=0.6.0"] instead of
231
+ * the standard object format {"node": ">=0.6.0"}.
232
+ */
233
+ function normalizeEngines(engines?: Record<string, string> | string[]): Record<string, string> | undefined {
234
+ if (!engines) return undefined;
235
+ if (Array.isArray(engines)) {
236
+ // Convert array format to object format
237
+ // e.g., ["node >=0.6.0"] -> {"node": ">=0.6.0"}
238
+ const result: Record<string, string> = {};
239
+ for (const entry of engines) {
240
+ const spaceIdx = entry.indexOf(' ');
241
+ if (spaceIdx > 0) {
242
+ const key = entry.substring(0, spaceIdx);
243
+ result[key] = entry.substring(spaceIdx + 1);
244
+ }
245
+ }
246
+ return Object.keys(result).length > 0 ? result : undefined;
247
+ }
248
+ return engines;
249
+ }
250
+
251
+ /**
252
+ * Extracts package name and optionally version from a package-lock.json path.
253
+ * e.g., "node_modules/@babel/core" -> { name: "@babel/core" }
254
+ * e.g., "node_modules/foo/node_modules/bar" -> { name: "bar" }
255
+ * e.g., "node_modules/is-odd@3.0.1" -> { name: "is-odd", version: "3.0.1" }
256
+ */
257
+ function extractPackageInfo(pkgPath: string): { name: string; version?: string } {
258
+ // For nested packages, we want the last package name
259
+ const nodeModulesIndex = pkgPath.lastIndexOf('node_modules/');
260
+ if (nodeModulesIndex === -1) return { name: pkgPath };
261
+
262
+ let nameWithVersion = pkgPath.slice(nodeModulesIndex + 'node_modules/'.length);
263
+
264
+ // Check if the path has a version suffix (e.g., "is-odd@3.0.1")
265
+ // Handle scoped packages (@scope/name@version) correctly
266
+ const atIndex = nameWithVersion.lastIndexOf('@');
267
+ if (atIndex > 0 && !nameWithVersion.substring(0, atIndex).includes('/')) {
268
+ // Not a scoped package, the @ is a version separator
269
+ return {
270
+ name: nameWithVersion.substring(0, atIndex),
271
+ version: nameWithVersion.substring(atIndex + 1)
272
+ };
273
+ } else if (atIndex > 0) {
274
+ // Could be scoped package with version: @scope/name@version
275
+ const parts = nameWithVersion.split('@');
276
+ if (parts.length === 3 && parts[0] === '') {
277
+ // @scope/name@version -> ["", "scope/name", "version"]
278
+ return {
279
+ name: `@${parts[1]}`,
280
+ version: parts[2]
281
+ };
282
+ }
283
+ }
284
+
285
+ return { name: nameWithVersion };
286
+ }
287
+
288
+ /**
289
+ * Creates or retrieves a ResolvedDependency from the cache.
290
+ * This ensures the same resolved package is reused across the dependency tree.
291
+ * Also maintains the nameToResolved index for O(1) name lookups.
292
+ */
293
+ function getOrCreateResolvedDependency(
294
+ name: string,
295
+ version: string,
296
+ pkgEntry?: PackageLockEntry
297
+ ): ResolvedDependency {
298
+ const key = `${name}@${version}`;
299
+ let resolved = resolvedDependencyCache.get(key);
300
+ if (!resolved) {
301
+ // Create a placeholder first - dependencies will be populated later
302
+ resolved = asRef({
303
+ kind: ResolvedDependencyKind,
304
+ name,
305
+ version,
306
+ dependencies: undefined,
307
+ devDependencies: undefined,
308
+ peerDependencies: undefined,
309
+ optionalDependencies: undefined,
310
+ engines: normalizeEngines(pkgEntry?.engines),
311
+ license: pkgEntry?.license,
312
+ });
313
+ resolvedDependencyCache.set(key, resolved);
314
+
315
+ // Maintain name index for O(1) lookup during semver fallback
316
+ const existing = nameToResolved.get(name);
317
+ if (existing) {
318
+ existing.push(resolved);
319
+ } else {
320
+ nameToResolved.set(name, [resolved]);
321
+ }
322
+ }
323
+ return resolved;
324
+ }
325
+
326
+ /**
327
+ * Resolves a dependency name from a given context path using Node.js-style resolution.
328
+ * Looks for the package in nested node_modules first, then walks up to parent directories.
329
+ * Falls back to semver matching when path-based resolution fails (e.g., for yarn/pnpm).
330
+ *
331
+ * @param name Package name to resolve
332
+ * @param versionConstraint Version constraint (e.g., "^3.0.1") for semver fallback
333
+ * @param contextPath The path of the parent package (e.g., "node_modules/is-even")
334
+ * Use "" for root-level dependencies
335
+ */
336
+ function resolveFromContext(
337
+ name: string,
338
+ versionConstraint: string,
339
+ contextPath: string
340
+ ): ResolvedDependency | undefined {
341
+ // Start from the context path and walk up looking for the package
342
+ let currentPath = contextPath;
343
+
344
+ while (true) {
345
+ // Try to find the package in node_modules at this level
346
+ const candidatePath = currentPath
347
+ ? `${currentPath}/node_modules/${name}`
348
+ : `node_modules/${name}`;
349
+
350
+ const resolved = pathToResolved.get(candidatePath);
351
+ if (resolved) {
352
+ return resolved;
353
+ }
354
+
355
+ // Walk up to parent directory
356
+ if (!currentPath) {
357
+ break; // Already at root
358
+ }
359
+
360
+ // Remove the last /node_modules/pkg segment to go up one level
361
+ const lastNodeModules = currentPath.lastIndexOf('/node_modules/');
362
+ if (lastNodeModules === -1) {
363
+ currentPath = ''; // Try root level next
364
+ } else {
365
+ currentPath = currentPath.substring(0, lastNodeModules);
366
+ }
367
+ }
368
+
369
+ // Fallback: use semver matching to find a version that satisfies the constraint
370
+ // This is needed for yarn/pnpm which don't encode nesting in their lock files
371
+ const candidates = nameToResolved.get(name);
372
+
373
+ if (!candidates || candidates.length === 0) {
374
+ return undefined;
375
+ }
376
+
377
+ if (candidates.length === 1) {
378
+ return candidates[0];
379
+ }
380
+
381
+ // Multiple versions - use semver to find the best match
382
+ // First try to find one that satisfies the constraint
383
+ for (const candidate of candidates) {
384
+ try {
385
+ if (semver.satisfies(candidate.version, versionConstraint)) {
386
+ return candidate;
387
+ }
388
+ } catch {
389
+ // Invalid semver, skip this candidate for matching
390
+ }
391
+ }
392
+
393
+ // If no exact match, return the highest version as fallback (O(n) linear scan)
394
+ let maxCandidate = candidates[0];
395
+ for (let i = 1; i < candidates.length; i++) {
396
+ try {
397
+ if (semver.compare(candidates[i].version, maxCandidate.version) > 0) {
398
+ maxCandidate = candidates[i];
399
+ }
400
+ } catch {
401
+ // Invalid semver, skip comparison
402
+ }
403
+ }
404
+ return maxCandidate;
405
+ }
406
+
407
+ /**
408
+ * Creates or retrieves a Dependency from the cache.
409
+ * Links to resolved dependency using path-based Node.js-style resolution.
410
+ *
411
+ * @param name Package name
412
+ * @param versionConstraint Version constraint from package.json
413
+ * @param contextPath The path context for resolution (parent package path)
414
+ */
415
+ function getOrCreateDependency(
416
+ name: string,
417
+ versionConstraint: string,
418
+ contextPath: string
419
+ ): Dependency {
420
+ // Resolve first to determine the actual resolved version
421
+ const resolved = resolveFromContext(name, versionConstraint, contextPath);
422
+
423
+ // Key by name, constraint, and resolved version (not context path).
424
+ // This allows sharing Dependency objects when different contexts resolve to the same version.
425
+ const resolvedKey = resolved ? `${resolved.name}@${resolved.version}` : 'unresolved';
426
+ const key = `${name}@${versionConstraint}@${resolvedKey}`;
427
+
428
+ let dep = dependencyCache.get(key);
429
+ if (!dep) {
430
+ dep = asRef({
431
+ kind: DependencyKind,
432
+ name,
433
+ versionConstraint,
434
+ resolved,
435
+ });
436
+ dependencyCache.set(key, dep);
437
+ }
438
+ return dep;
439
+ }
440
+
441
+ /**
442
+ * Parses dependencies from a Record, using the given context path for resolution.
443
+ */
444
+ function parseDependencies(
445
+ deps: Record<string, string> | undefined,
446
+ contextPath: string
447
+ ): Dependency[] {
448
+ if (!deps) return [];
449
+ return Object.entries(deps).map(([name, versionConstraint]) =>
450
+ getOrCreateDependency(name, versionConstraint, contextPath)
451
+ );
452
+ }
453
+
454
+ /**
455
+ * Parses bundled dependencies (array of names with no version constraint).
456
+ */
457
+ function parseBundledDependencies(
458
+ deps: string[] | undefined,
459
+ contextPath: string
460
+ ): Dependency[] {
461
+ if (!deps) return [];
462
+ return deps.map(name => getOrCreateDependency(name, '*', contextPath));
463
+ }
464
+
465
+ /**
466
+ * Parses package-lock.json to build a list of resolved dependencies.
467
+ * Two-pass approach:
468
+ * 1. First pass: Create all ResolvedDependency objects and build path->resolved map
469
+ * 2. Second pass: Populate dependencies using path-based resolution
470
+ */
471
+ function parseResolutions(
472
+ lockContent: PackageLockContent
473
+ ): ResolvedDependency[] {
474
+ if (!lockContent.packages) return [];
475
+
476
+ const packages = lockContent.packages;
477
+
478
+ // First pass: Create all ResolvedDependency placeholders and build path map
479
+ const packageInfos: Array<{path: string; name: string; version: string; entry: PackageLockEntry}> = [];
480
+ for (const [pkgPath, pkgEntry] of Object.entries(packages)) {
481
+ // Skip the root package (empty string key)
482
+ if (pkgPath === '') continue;
483
+
484
+ const pkgInfo = extractPackageInfo(pkgPath);
485
+ const name = pkgInfo.name;
486
+ // Use version from path if available, otherwise from entry
487
+ const version = pkgInfo.version || pkgEntry.version;
488
+
489
+ if (name && version) {
490
+ const resolved = getOrCreateResolvedDependency(name, version, pkgEntry);
491
+ pathToResolved.set(pkgPath, resolved);
492
+ packageInfos.push({path: pkgPath, name, version, entry: pkgEntry});
493
+ }
494
+ }
495
+
496
+ // Second pass: Populate dependencies using path-based resolution.
497
+ // Note: Using castDraft here is safe because all objects are created within this
498
+ // parsing context and haven't been returned to callers yet. The objects in
499
+ // resolvedDependencyCache are plain JS objects marked with asRef() for RPC
500
+ // reference deduplication, not frozen Immer drafts.
501
+ for (const {path: pkgPath, name, version, entry} of packageInfos) {
502
+ const key = `${name}@${version}`;
503
+ const resolved = resolvedDependencyCache.get(key);
504
+ if (resolved) {
505
+ const mutableResolved = castDraft(resolved);
506
+ if (entry.dependencies) {
507
+ mutableResolved.dependencies = parseDependencies(entry.dependencies, pkgPath);
508
+ }
509
+ if (entry.devDependencies) {
510
+ mutableResolved.devDependencies = parseDependencies(entry.devDependencies, pkgPath);
511
+ }
512
+ if (entry.peerDependencies) {
513
+ mutableResolved.peerDependencies = parseDependencies(entry.peerDependencies, pkgPath);
514
+ }
515
+ if (entry.optionalDependencies) {
516
+ mutableResolved.optionalDependencies = parseDependencies(entry.optionalDependencies, pkgPath);
517
+ }
518
+ }
519
+ }
520
+
521
+ // Return all unique resolved dependencies
522
+ return Array.from(resolvedDependencyCache.values());
523
+ }
524
+
525
+ // Parse resolved dependencies first (before dependencies) so we can link them
526
+ const resolvedDependencies = packageLockContent ? parseResolutions(packageLockContent) : [];
527
+
528
+ // Now parse dependencies from package.json - they resolve from root context ("")
529
+ const dependencies = parseDependencies(packageJsonContent.dependencies, '');
530
+ const devDependencies = parseDependencies(packageJsonContent.devDependencies, '');
531
+ const peerDependencies = parseDependencies(packageJsonContent.peerDependencies, '');
532
+ const optionalDependencies = parseDependencies(packageJsonContent.optionalDependencies, '');
533
+ const bundledDependencies = parseBundledDependencies(
534
+ packageJsonContent.bundledDependencies || packageJsonContent.bundleDependencies,
535
+ ''
536
+ );
537
+
538
+ return {
539
+ kind: NodeResolutionResultKind,
540
+ id: randomId(),
541
+ name: packageJsonContent.name,
542
+ version: packageJsonContent.version,
543
+ description: packageJsonContent.description,
544
+ path,
545
+ workspacePackagePaths,
546
+ dependencies,
547
+ devDependencies,
548
+ peerDependencies,
549
+ optionalDependencies,
550
+ bundledDependencies,
551
+ resolvedDependencies,
552
+ packageManager,
553
+ engines: packageJsonContent.engines,
554
+ npmrcConfigs,
555
+ };
556
+ }
557
+
558
+ /**
559
+ * Helper function to find a NodeResolutionResult marker on a compilation unit.
560
+ */
561
+ export function findNodeResolutionResult(cu: { markers: Markers }): NodeResolutionResult | undefined {
562
+ return findMarker<NodeResolutionResult>(cu, NodeResolutionResultKind);
563
+ }
564
+
565
+ /**
566
+ * Parses an .npmrc file content into a key-value map.
567
+ *
568
+ * .npmrc format:
569
+ * - Lines are key=value pairs
570
+ * - Lines starting with # or ; are comments
571
+ * - Empty lines are ignored
572
+ * - Values can contain ${VAR} or ${VAR:-default} for env variable substitution
573
+ */
574
+ function parseNpmrc(content: string): Record<string, string> {
575
+ const properties: Record<string, string> = {};
576
+
577
+ for (const line of content.split('\n')) {
578
+ const trimmed = line.trim();
579
+
580
+ // Skip comments and empty lines
581
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith(';')) {
582
+ continue;
583
+ }
584
+
585
+ // Parse key=value
586
+ const eqIndex = trimmed.indexOf('=');
587
+ if (eqIndex === -1) continue;
588
+
589
+ const key = trimmed.substring(0, eqIndex).trim();
590
+ const value = trimmed.substring(eqIndex + 1).trim();
591
+
592
+ if (key) {
593
+ properties[key] = value;
594
+ }
595
+ }
596
+
597
+ return properties;
598
+ }
599
+
600
+ /**
601
+ * Helper to check if a file exists asynchronously.
602
+ */
603
+ async function fileExists(filePath: string): Promise<boolean> {
604
+ try {
605
+ await fsp.access(filePath);
606
+ return true;
607
+ } catch {
608
+ return false;
609
+ }
610
+ }
611
+
612
+ /**
613
+ * Reads .npmrc configurations from all scope levels.
614
+ * Returns an array of Npmrc objects, one for each scope that has configuration.
615
+ *
616
+ * Scopes (from lowest to highest priority):
617
+ * - Global: $PREFIX/etc/npmrc (npm's installation directory)
618
+ * - User: $HOME/.npmrc (user's home directory)
619
+ * - Project: .npmrc in project root (sibling of package.json)
620
+ * - Env: npm_config_* environment variables
621
+ *
622
+ * @param projectDir The project directory containing package.json
623
+ * @returns Promise resolving to array of Npmrc objects for each scope with configuration
624
+ */
625
+ export async function readNpmrcConfigs(projectDir: string): Promise<Npmrc[]> {
626
+ const configs: Npmrc[] = [];
627
+
628
+ // 1. Global config: $PREFIX/etc/npmrc
629
+ // Try to get npm prefix from npm itself, fall back to common locations
630
+ const globalNpmrcPaths = [
631
+ // Try NPM_CONFIG_GLOBALCONFIG env var first
632
+ process.env.NPM_CONFIG_GLOBALCONFIG,
633
+ // Common global locations
634
+ '/usr/local/etc/npmrc',
635
+ '/etc/npmrc',
636
+ ].filter(Boolean) as string[];
637
+
638
+ // Also try to detect from npm prefix if node is installed
639
+ const nodeDir = process.execPath ? path.dirname(path.dirname(process.execPath)) : undefined;
640
+ if (nodeDir) {
641
+ globalNpmrcPaths.unshift(path.join(nodeDir, 'etc', 'npmrc'));
642
+ }
643
+
644
+ for (const globalPath of globalNpmrcPaths) {
645
+ if (await fileExists(globalPath)) {
646
+ try {
647
+ const content = await fsp.readFile(globalPath, 'utf-8');
648
+ const properties = parseNpmrc(content);
649
+ if (Object.keys(properties).length > 0) {
650
+ configs.push({
651
+ kind: NpmrcKind,
652
+ scope: NpmrcScope.Global,
653
+ properties
654
+ });
655
+ break; // Only use the first found global config
656
+ }
657
+ } catch {
658
+ // Ignore read errors
659
+ }
660
+ }
661
+ }
662
+
663
+ // 2. User config: $HOME/.npmrc
664
+ const userNpmrcPath = process.env.NPM_CONFIG_USERCONFIG || path.join(homedir(), '.npmrc');
665
+ if (await fileExists(userNpmrcPath)) {
666
+ try {
667
+ const content = await fsp.readFile(userNpmrcPath, 'utf-8');
668
+ const properties = parseNpmrc(content);
669
+ if (Object.keys(properties).length > 0) {
670
+ configs.push({
671
+ kind: NpmrcKind,
672
+ scope: NpmrcScope.User,
673
+ properties
674
+ });
675
+ }
676
+ } catch {
677
+ // Ignore read errors
678
+ }
679
+ }
680
+
681
+ // 3. Project config: .npmrc in project root
682
+ const projectNpmrcPath = path.join(projectDir, '.npmrc');
683
+ if (await fileExists(projectNpmrcPath)) {
684
+ try {
685
+ const content = await fsp.readFile(projectNpmrcPath, 'utf-8');
686
+ const properties = parseNpmrc(content);
687
+ if (Object.keys(properties).length > 0) {
688
+ configs.push({
689
+ kind: NpmrcKind,
690
+ scope: NpmrcScope.Project,
691
+ properties
692
+ });
693
+ }
694
+ } catch {
695
+ // Ignore read errors
696
+ }
697
+ }
698
+
699
+ // Note: We intentionally don't capture npm_config_* environment variables.
700
+ // While users can set config via env vars, npm also automatically injects
701
+ // many npm_config_* vars when running child processes, and there's no way
702
+ // to distinguish user-set vars from npm-injected ones at runtime.
703
+
704
+ return configs;
705
+ }
706
+
707
+ /**
708
+ * Helper functions for querying dependencies
709
+ */
710
+ export namespace NodeResolutionResultQueries {
711
+
712
+ /**
713
+ * Get all dependency requests from all scopes.
714
+ */
715
+ export function getAllDependencies(project: NodeResolutionResult): Dependency[] {
716
+ return [
717
+ ...project.dependencies,
718
+ ...project.devDependencies,
719
+ ...project.peerDependencies,
720
+ ...project.optionalDependencies,
721
+ ...project.bundledDependencies
722
+ ];
723
+ }
724
+
725
+ /**
726
+ * Check if project has a specific dependency request, optionally filtered by scope.
727
+ */
728
+ export function hasDependency(
729
+ project: NodeResolutionResult,
730
+ packageName: string,
731
+ scope?: 'dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies' | 'bundledDependencies'
732
+ ): boolean {
733
+ const deps = scope
734
+ ? project[scope]
735
+ : getAllDependencies(project);
736
+ return deps.some(dep => dep.name === packageName);
737
+ }
738
+
739
+ /**
740
+ * Find a specific dependency request by name across all scopes.
741
+ */
742
+ export function findDependency(
743
+ project: NodeResolutionResult,
744
+ packageName: string
745
+ ): Dependency | undefined {
746
+ return getAllDependencies(project).find(dep => dep.name === packageName);
747
+ }
748
+
749
+ /**
750
+ * Get all dependency requests matching a predicate.
751
+ */
752
+ export function findDependencies(
753
+ project: NodeResolutionResult,
754
+ predicate: (dep: Dependency) => boolean
755
+ ): Dependency[] {
756
+ return getAllDependencies(project).filter(predicate);
757
+ }
758
+
759
+ /**
760
+ * Get all resolved dependencies with a specific name (handles multiple versions).
761
+ * Returns an empty array if no versions are found.
762
+ *
763
+ * For navigation, prefer using the Dependency.resolved property:
764
+ * @example
765
+ * const express = project.dependencies.find(d => d.name === 'express')?.resolved;
766
+ * const accepts = express?.dependencies?.find(d => d.name === 'accepts')?.resolved;
767
+ */
768
+ export function getAllResolvedVersions(
769
+ project: NodeResolutionResult,
770
+ packageName: string
771
+ ): ResolvedDependency[] {
772
+ return project.resolvedDependencies.filter(r => r.name === packageName);
773
+ }
774
+ }
775
+
776
+ /**
777
+ * Register RPC codec for Npmrc.
778
+ */
779
+ RpcCodecs.registerCodec(NpmrcKind, {
780
+ async rpcReceive(before: Npmrc, q: RpcReceiveQueue): Promise<Npmrc> {
781
+ const draft = createDraft(before);
782
+ draft.kind = NpmrcKind;
783
+ draft.scope = await q.receive(before.scope);
784
+ draft.properties = await q.receive(before.properties);
785
+
786
+ return finishDraft(draft) as Npmrc;
787
+ },
788
+
789
+ async rpcSend(after: Npmrc, q: RpcSendQueue): Promise<void> {
790
+ await q.getAndSend(after, a => a.scope);
791
+ await q.getAndSend(after, a => a.properties);
792
+ }
793
+ });
794
+
795
+ /**
796
+ * Register RPC codec for Dependency.
797
+ */
798
+ RpcCodecs.registerCodec(DependencyKind, {
799
+ async rpcReceive(before: Dependency, q: RpcReceiveQueue): Promise<Dependency> {
800
+ const draft = createDraft(before);
801
+ draft.kind = DependencyKind;
802
+ draft.name = await q.receive(before.name);
803
+ draft.versionConstraint = await q.receive(before.versionConstraint);
804
+ draft.resolved = await q.receive(before.resolved);
805
+
806
+ return finishDraft(draft) as Dependency;
807
+ },
808
+
809
+ async rpcSend(after: Dependency, q: RpcSendQueue): Promise<void> {
810
+ await q.getAndSend(after, a => a.name);
811
+ await q.getAndSend(after, a => a.versionConstraint);
812
+ await q.getAndSend(after, a => a.resolved);
813
+ }
814
+ });
815
+
816
+ /**
817
+ * Register RPC codec for ResolvedDependency.
818
+ */
819
+ RpcCodecs.registerCodec(ResolvedDependencyKind, {
820
+ async rpcReceive(before: ResolvedDependency, q: RpcReceiveQueue): Promise<ResolvedDependency> {
821
+ const draft = createDraft(before);
822
+ draft.kind = ResolvedDependencyKind;
823
+ draft.name = await q.receive(before.name);
824
+ draft.version = await q.receive(before.version);
825
+ draft.dependencies = (await q.receiveList(before.dependencies)) || undefined;
826
+ draft.devDependencies = (await q.receiveList(before.devDependencies)) || undefined;
827
+ draft.peerDependencies = (await q.receiveList(before.peerDependencies)) || undefined;
828
+ draft.optionalDependencies = (await q.receiveList(before.optionalDependencies)) || undefined;
829
+ draft.engines = await q.receive(before.engines);
830
+ draft.license = await q.receive(before.license);
831
+
832
+ return finishDraft(draft) as ResolvedDependency;
833
+ },
834
+
835
+ async rpcSend(after: ResolvedDependency, q: RpcSendQueue): Promise<void> {
836
+ await q.getAndSend(after, a => a.name);
837
+ await q.getAndSend(after, a => a.version);
838
+ await q.getAndSendList(after, a => (a.dependencies || []).map(d => asRef(d)),
839
+ dep => `${dep.name}@${dep.versionConstraint}`);
840
+ await q.getAndSendList(after, a => (a.devDependencies || []).map(d => asRef(d)),
841
+ dep => `${dep.name}@${dep.versionConstraint}`);
842
+ await q.getAndSendList(after, a => (a.peerDependencies || []).map(d => asRef(d)),
843
+ dep => `${dep.name}@${dep.versionConstraint}`);
844
+ await q.getAndSendList(after, a => (a.optionalDependencies || []).map(d => asRef(d)),
845
+ dep => `${dep.name}@${dep.versionConstraint}`);
846
+ await q.getAndSend(after, a => a.engines);
847
+ await q.getAndSend(after, a => a.license);
848
+ }
849
+ });
850
+
851
+ /**
852
+ * Register RPC codec for NodeResolutionResult marker.
853
+ * This handles serialization/deserialization for communication between JS and Java.
854
+ */
855
+ RpcCodecs.registerCodec(NodeResolutionResultKind, {
856
+ async rpcReceive(before: NodeResolutionResult, q: RpcReceiveQueue): Promise<NodeResolutionResult> {
857
+ const draft = createDraft(before);
858
+ draft.id = await q.receive(before.id);
859
+ draft.name = await q.receive(before.name);
860
+ draft.version = await q.receive(before.version);
861
+ draft.description = await q.receive(before.description);
862
+ draft.path = await q.receive(before.path);
863
+ draft.workspacePackagePaths = await q.receive(before.workspacePackagePaths);
864
+
865
+ draft.dependencies = (await q.receiveList(before.dependencies)) || [];
866
+ draft.devDependencies = (await q.receiveList(before.devDependencies)) || [];
867
+ draft.peerDependencies = (await q.receiveList(before.peerDependencies)) || [];
868
+ draft.optionalDependencies = (await q.receiveList(before.optionalDependencies)) || [];
869
+ draft.bundledDependencies = (await q.receiveList(before.bundledDependencies)) || [];
870
+ draft.resolvedDependencies = (await q.receiveList(before.resolvedDependencies)) || [];
871
+
872
+ draft.packageManager = await q.receive(before.packageManager);
873
+ draft.engines = await q.receive(before.engines);
874
+ draft.npmrcConfigs = (await q.receiveList(before.npmrcConfigs)) || undefined;
875
+
876
+ return finishDraft(draft) as NodeResolutionResult;
877
+ },
878
+
879
+ async rpcSend(after: NodeResolutionResult, q: RpcSendQueue): Promise<void> {
880
+ await q.getAndSend(after, a => a.id);
881
+ await q.getAndSend(after, a => a.name);
882
+ await q.getAndSend(after, a => a.version);
883
+ await q.getAndSend(after, a => a.description);
884
+ await q.getAndSend(after, a => a.path);
885
+ await q.getAndSend(after, a => a.workspacePackagePaths);
886
+
887
+ await q.getAndSendList(after, a => a.dependencies.map(d => asRef(d)),
888
+ dep => `${dep.name}@${dep.versionConstraint}`);
889
+ await q.getAndSendList(after, a => a.devDependencies.map(d => asRef(d)),
890
+ dep => `${dep.name}@${dep.versionConstraint}`);
891
+ await q.getAndSendList(after, a => a.peerDependencies.map(d => asRef(d)),
892
+ dep => `${dep.name}@${dep.versionConstraint}`);
893
+ await q.getAndSendList(after, a => a.optionalDependencies.map(d => asRef(d)),
894
+ dep => `${dep.name}@${dep.versionConstraint}`);
895
+ await q.getAndSendList(after, a => a.bundledDependencies.map(d => asRef(d)),
896
+ dep => `${dep.name}@${dep.versionConstraint}`);
897
+ await q.getAndSendList(after, a => a.resolvedDependencies.map(r => asRef(r)),
898
+ resolved => `${resolved.name}@${resolved.version}`);
899
+
900
+ await q.getAndSend(after, a => a.packageManager);
901
+ await q.getAndSend(after, a => a.engines);
902
+ await q.getAndSendList(after, a => a.npmrcConfigs || [],
903
+ npmrc => npmrc.scope);
904
+ }
905
+ });