@microsoft/rayfin-docs 1.35.0 → 1.35.1

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.
@@ -120,12 +120,23 @@ export interface InstalledPackageVersionResolution {
120
120
  /** Optional policy applied to installed manifest versions. */
121
121
  export interface InstalledPackageVersionResolutionOptions {
122
122
  isVersionAllowed?: (version: string) => boolean;
123
+ /**
124
+ * Also resolve packages that are installed but reachable only as an *indirect*
125
+ * dependency — one a strict/isolated install links inside its dependent's
126
+ * scope rather than anywhere `from` can see (see
127
+ * {@link resolvePackageJsonTransitively}).
128
+ *
129
+ * Off by default
130
+ */
131
+ includeIndirect?: boolean;
123
132
  }
124
133
  /**
125
134
  * Resolve actual installed versions for a bounded candidate set.
126
135
  *
127
136
  * Candidates are deduplicated and sorted. Missing, malformed, or mismatched
128
137
  * manifests are omitted so telemetry collection remains best-effort.
138
+ *
139
+ * Resolves only what `from` can reach unless `includeIndirect` is set.
129
140
  */
130
141
  export declare function resolveInstalledPackageVersions(packageNames: readonly string[], from: string, options?: InstalledPackageVersionResolutionOptions): InstalledPackageVersionResolution;
131
142
  //# sourceMappingURL=discovery.d.ts.map
package/dist/discovery.js CHANGED
@@ -17,7 +17,7 @@
17
17
  * candidate list, workspace package roots, or all-installed node_modules
18
18
  * walking via `scanInstalledPackages: true`.
19
19
  */
20
- import { readFileSync, existsSync, statSync, readdirSync } from 'fs';
20
+ import { readFileSync, existsSync, realpathSync, statSync, readdirSync, } from 'fs';
21
21
  import { createRequire } from 'module';
22
22
  import { dirname, isAbsolute, join, resolve } from 'path';
23
23
  import { fileURLToPath } from 'url';
@@ -35,7 +35,6 @@ export function discoverRayfinDocsPackages(options) {
35
35
  const fromDir = options.from.startsWith('file:') || options.from.includes('://')
36
36
  ? dirname(fileURLToPath(options.from))
37
37
  : resolveExistingDir(options.from);
38
- const requireFromBase = createRequire(join(fromDir, 'noop.js'));
39
38
  const nodeModulesChain = buildNodeModulesChain(fromDir);
40
39
  const discovered = [];
41
40
  const notInstalled = [];
@@ -53,7 +52,7 @@ export function discoverRayfinDocsPackages(options) {
53
52
  packagesToRead = resolvePackageRootJsons(options.packageRoots);
54
53
  }
55
54
  else if (options.candidates !== undefined) {
56
- packagesToRead = resolveCandidatePackageJsons(options.candidates, requireFromBase, nodeModulesChain, notInstalled, untrustedSkipped, trust);
55
+ packagesToRead = resolveCandidatePackageJsons(options.candidates, fromDir, notInstalled, untrustedSkipped, trust);
57
56
  }
58
57
  else if (options.scanInstalledPackages) {
59
58
  packagesToRead = discoverInstalledPackageJsons(nodeModulesChain);
@@ -130,14 +129,19 @@ function resolvePackageRootJsons(packageRoots) {
130
129
  pkgJsonPath: join(resolveExistingDir(packageRoot), 'package.json'),
131
130
  }));
132
131
  }
133
- function resolveCandidatePackageJsons(candidates, requireFromBase, nodeModulesChain, notInstalled, untrustedSkipped, trust) {
132
+ /**
133
+ * Resolve explicitly named candidates. Direct-only by construction: docs
134
+ * discovery reports packages the caller named and trusts, so it must not reach
135
+ * through the dependency graph into packages nobody asked about.
136
+ */
137
+ function resolveCandidatePackageJsons(candidates, fromDir, notInstalled, untrustedSkipped, trust) {
134
138
  const resolved = [];
135
139
  for (const candidate of candidates) {
136
140
  if (!trust(candidate)) {
137
141
  untrustedSkipped.push(candidate);
138
142
  continue;
139
143
  }
140
- const pkgJsonPath = resolvePackageJson(requireFromBase, candidate, nodeModulesChain);
144
+ const pkgJsonPath = resolvePackageJsonFrom(fromDir, candidate);
141
145
  if (!pkgJsonPath) {
142
146
  notInstalled.push(candidate);
143
147
  continue;
@@ -300,8 +304,8 @@ export function docKindToModule(kind) {
300
304
  }
301
305
  /**
302
306
  * Compute the chain of `node_modules` ancestor directories from
303
- * `fromDir` upward, capped at 16 levels. Used by the tier-3 fallback
304
- * in {@link resolvePackageJson} to avoid re-walking the same ancestors
307
+ * `fromDir` upward, capped at 16 levels. Used by the ancestor tier in
308
+ * {@link resolvePackageJsonFrom} to avoid re-walking the same ancestors
305
309
  * once per candidate (`9 candidates × 16 walks = 144 stat calls` in
306
310
  * the original implementation; this caches the chain to 16 stats).
307
311
  */
@@ -317,18 +321,37 @@ function buildNodeModulesChain(fromDir) {
317
321
  }
318
322
  return chain;
319
323
  }
324
+ /**
325
+ * Dependency fields that describe packages actually present in an install.
326
+ *
327
+ * `devDependencies` is deliberately excluded: it is not installed for a
328
+ * transitive dependency, so following it would walk edges that do not exist in
329
+ * the consuming project's tree.
330
+ */
331
+ const INSTALLED_DEPENDENCY_FIELDS = [
332
+ 'dependencies',
333
+ 'optionalDependencies',
334
+ 'peerDependencies',
335
+ ];
336
+ /**
337
+ * Bounds on the dependency-graph search. The walk only runs for a package the
338
+ * direct tiers already failed to find, so these cap a rare path rather than the
339
+ * common one.
340
+ */
341
+ const MAX_TRANSITIVE_DEPTH = 4;
342
+ const MAX_TRANSITIVE_VISITS = 256;
320
343
  /**
321
344
  * Resolve actual installed versions for a bounded candidate set.
322
345
  *
323
346
  * Candidates are deduplicated and sorted. Missing, malformed, or mismatched
324
347
  * manifests are omitted so telemetry collection remains best-effort.
348
+ *
349
+ * Resolves only what `from` can reach unless `includeIndirect` is set.
325
350
  */
326
351
  export function resolveInstalledPackageVersions(packageNames, from, options = {}) {
327
352
  const fromDir = from.startsWith('file:') || from.includes('://')
328
353
  ? dirname(fileURLToPath(from))
329
354
  : resolveExistingDir(from);
330
- const requireFromBase = createRequire(join(fromDir, 'noop.js'));
331
- const nodeModulesChain = buildNodeModulesChain(fromDir);
332
355
  const packages = [];
333
356
  const unresolvedPackageNames = [];
334
357
  for (const packageName of [...new Set(packageNames)].sort()) {
@@ -336,7 +359,10 @@ export function resolveInstalledPackageVersions(packageNames, from, options = {}
336
359
  unresolvedPackageNames.push(packageName);
337
360
  continue;
338
361
  }
339
- const packageJsonPath = resolvePackageJson(requireFromBase, packageName, nodeModulesChain);
362
+ const packageJsonPath = resolvePackageJsonFrom(fromDir, packageName) ??
363
+ (options.includeIndirect
364
+ ? resolvePackageJsonTransitively(fromDir, packageName)
365
+ : undefined);
340
366
  if (!packageJsonPath) {
341
367
  unresolvedPackageNames.push(packageName);
342
368
  continue;
@@ -364,15 +390,21 @@ function isValidPackageName(packageName) {
364
390
  /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(packageName));
365
391
  }
366
392
  /**
367
- * Find a package's `package.json` even when exports hide it, the package is
368
- * ESM-only, or the install is hoisted above the resolution root.
393
+ * Find a package's `package.json` **as resolved from `fromDir`**, even when
394
+ * exports hide it, the package is ESM-only, or the install is hoisted above the
395
+ * resolution root.
369
396
  *
370
397
  * Packages with restricted exports often omit `./package.json`, causing direct
371
398
  * subpath resolution to fail. ESM-only packages may also hide their main entry
372
- * from CommonJS resolution, so the final tier searches the cached ancestor
399
+ * from CommonJS resolution, so the final tier searches the ancestor
373
400
  * `node_modules` chain used by npm, pnpm, and Yarn-style installs.
401
+ *
402
+ * Scoped to one directory so the dependency-graph walk can reuse it verbatim:
403
+ * "resolve X from package Y's location" is the same question as "resolve X from
404
+ * the project root", just asked somewhere else in the tree.
374
405
  */
375
- function resolvePackageJson(req, packageName, nodeModulesChain) {
406
+ function resolvePackageJsonFrom(fromDir, packageName) {
407
+ const req = createRequire(join(fromDir, 'noop.js'));
376
408
  // Tier 1: direct manifest resolution when exports allow the subpath.
377
409
  try {
378
410
  return req.resolve(`${packageName}/package.json`);
@@ -407,14 +439,126 @@ function resolvePackageJson(req, packageName, nodeModulesChain) {
407
439
  catch {
408
440
  // ESM-only package or similar: fall through to node_modules lookup.
409
441
  }
410
- // Tier 3: search cached ancestor node_modules roots, independent of exports.
411
- for (const dir of nodeModulesChain) {
442
+ // Tier 3: search ancestor node_modules roots, independent of exports.
443
+ for (const dir of buildNodeModulesChain(fromDir)) {
412
444
  const candidate = join(dir, 'node_modules', packageName, 'package.json');
413
445
  if (existsSync(candidate))
414
446
  return candidate;
415
447
  }
416
448
  return undefined;
417
449
  }
450
+ /**
451
+ * Find a package that is installed but *not reachable from `fromDir`* by
452
+ * searching outward through the dependency graph.
453
+ *
454
+ * A strict/isolated install (pnpm's default, and Yarn's `nmMode: hardlinks`)
455
+ * links a purely transitive package only inside the dependency scope of the
456
+ * package that asked for it — never the consumer's own `node_modules` chain.
457
+ * Resolution from `fromDir` therefore fails for it, correctly: nothing at the
458
+ * project root may import it. It is still the version that ships, because the
459
+ * bundler reaches it through the dependent that does.
460
+ *
461
+ * So the search moves to where the answer is: walk the installed dependency
462
+ * graph breadth-first and re-ask {@link resolvePackageJsonFrom} at each package's
463
+ * own real location. Node's resolver supplies all the layout knowledge —
464
+ * following the symlink out of pnpm's virtual store and then finding the target
465
+ * as its sibling — instead of this code hard-coding store paths, and it costs
466
+ * nothing on the common path because it only runs after a direct miss.
467
+ *
468
+ * Known limitation: Yarn Plug'n'Play has no `node_modules` tree at all, so
469
+ * neither this nor the direct tiers can see it without the `.pnp.cjs` API.
470
+ */
471
+ function resolvePackageJsonTransitively(fromDir, packageName) {
472
+ const visited = new Set();
473
+ let frontier = readInstalledDependencyNames(join(fromDir, 'package.json'));
474
+ // Not seeded from the manifest alone: a project whose own manifest is
475
+ // unreadable can still have an installed tree worth searching.
476
+ if (frontier.length === 0) {
477
+ frontier = readAncestorNodeModulesEntries(fromDir);
478
+ }
479
+ for (let depth = 0; depth < MAX_TRANSITIVE_DEPTH; depth += 1) {
480
+ const next = [];
481
+ for (const dependentName of frontier) {
482
+ if (visited.size >= MAX_TRANSITIVE_VISITS)
483
+ return undefined;
484
+ if (dependentName === packageName)
485
+ continue;
486
+ if (visited.has(dependentName))
487
+ continue;
488
+ visited.add(dependentName);
489
+ if (!isValidPackageName(dependentName))
490
+ continue;
491
+ const dependentManifest = resolvePackageJsonFrom(fromDir, dependentName);
492
+ if (!dependentManifest)
493
+ continue;
494
+ // The real location matters: resolving through the virtual-store symlink
495
+ // is what puts the target in scope as a sibling.
496
+ let dependentDir;
497
+ try {
498
+ dependentDir = realpathSync(dirname(dependentManifest));
499
+ }
500
+ catch {
501
+ continue;
502
+ }
503
+ const found = resolvePackageJsonFrom(dependentDir, packageName);
504
+ if (found)
505
+ return found;
506
+ next.push(...readInstalledDependencyNames(join(dependentDir, 'package.json')));
507
+ }
508
+ if (next.length === 0)
509
+ return undefined;
510
+ frontier = next;
511
+ }
512
+ return undefined;
513
+ }
514
+ /** Names declared in the dependency fields that describe an installed tree. */
515
+ function readInstalledDependencyNames(manifestPath) {
516
+ try {
517
+ const parsed = JSON.parse(readFileSync(manifestPath, 'utf8'));
518
+ const names = [];
519
+ for (const field of INSTALLED_DEPENDENCY_FIELDS) {
520
+ const value = parsed[field];
521
+ if (value && typeof value === 'object') {
522
+ names.push(...Object.keys(value));
523
+ }
524
+ }
525
+ return names;
526
+ }
527
+ catch {
528
+ return [];
529
+ }
530
+ }
531
+ /**
532
+ * Top-level package names installed in the nearest ancestor `node_modules`,
533
+ * used to seed the graph walk when no readable manifest declares anything.
534
+ */
535
+ function readAncestorNodeModulesEntries(fromDir) {
536
+ for (const dir of buildNodeModulesChain(fromDir)) {
537
+ const nodeModules = join(dir, 'node_modules');
538
+ if (!existsSync(nodeModules))
539
+ continue;
540
+ try {
541
+ const names = [];
542
+ for (const entry of readdirSync(nodeModules, { withFileTypes: true })) {
543
+ if (entry.name.startsWith('.'))
544
+ continue;
545
+ if (entry.name.startsWith('@')) {
546
+ const scopeDir = join(nodeModules, entry.name);
547
+ for (const scoped of readdirSync(scopeDir)) {
548
+ names.push(`${entry.name}/${scoped}`);
549
+ }
550
+ continue;
551
+ }
552
+ names.push(entry.name);
553
+ }
554
+ return names;
555
+ }
556
+ catch {
557
+ return [];
558
+ }
559
+ }
560
+ return [];
561
+ }
418
562
  /** Internal: ensure `from` exists for `createRequire` when given a dir. */
419
563
  function resolveExistingDir(from) {
420
564
  const absolute = resolve(from);
@@ -44,7 +44,6 @@ function discoverRayfinDocsPackages(options) {
44
44
  const fromDir = options.from.startsWith('file:') || options.from.includes('://')
45
45
  ? (0, path_1.dirname)((0, url_1.fileURLToPath)(options.from))
46
46
  : resolveExistingDir(options.from);
47
- const requireFromBase = (0, module_1.createRequire)((0, path_1.join)(fromDir, 'noop.js'));
48
47
  const nodeModulesChain = buildNodeModulesChain(fromDir);
49
48
  const discovered = [];
50
49
  const notInstalled = [];
@@ -62,7 +61,7 @@ function discoverRayfinDocsPackages(options) {
62
61
  packagesToRead = resolvePackageRootJsons(options.packageRoots);
63
62
  }
64
63
  else if (options.candidates !== undefined) {
65
- packagesToRead = resolveCandidatePackageJsons(options.candidates, requireFromBase, nodeModulesChain, notInstalled, untrustedSkipped, trust);
64
+ packagesToRead = resolveCandidatePackageJsons(options.candidates, fromDir, notInstalled, untrustedSkipped, trust);
66
65
  }
67
66
  else if (options.scanInstalledPackages) {
68
67
  packagesToRead = discoverInstalledPackageJsons(nodeModulesChain);
@@ -139,14 +138,19 @@ function resolvePackageRootJsons(packageRoots) {
139
138
  pkgJsonPath: (0, path_1.join)(resolveExistingDir(packageRoot), 'package.json'),
140
139
  }));
141
140
  }
142
- function resolveCandidatePackageJsons(candidates, requireFromBase, nodeModulesChain, notInstalled, untrustedSkipped, trust) {
141
+ /**
142
+ * Resolve explicitly named candidates. Direct-only by construction: docs
143
+ * discovery reports packages the caller named and trusts, so it must not reach
144
+ * through the dependency graph into packages nobody asked about.
145
+ */
146
+ function resolveCandidatePackageJsons(candidates, fromDir, notInstalled, untrustedSkipped, trust) {
143
147
  const resolved = [];
144
148
  for (const candidate of candidates) {
145
149
  if (!trust(candidate)) {
146
150
  untrustedSkipped.push(candidate);
147
151
  continue;
148
152
  }
149
- const pkgJsonPath = resolvePackageJson(requireFromBase, candidate, nodeModulesChain);
153
+ const pkgJsonPath = resolvePackageJsonFrom(fromDir, candidate);
150
154
  if (!pkgJsonPath) {
151
155
  notInstalled.push(candidate);
152
156
  continue;
@@ -309,8 +313,8 @@ function docKindToModule(kind) {
309
313
  }
310
314
  /**
311
315
  * Compute the chain of `node_modules` ancestor directories from
312
- * `fromDir` upward, capped at 16 levels. Used by the tier-3 fallback
313
- * in {@link resolvePackageJson} to avoid re-walking the same ancestors
316
+ * `fromDir` upward, capped at 16 levels. Used by the ancestor tier in
317
+ * {@link resolvePackageJsonFrom} to avoid re-walking the same ancestors
314
318
  * once per candidate (`9 candidates × 16 walks = 144 stat calls` in
315
319
  * the original implementation; this caches the chain to 16 stats).
316
320
  */
@@ -326,18 +330,37 @@ function buildNodeModulesChain(fromDir) {
326
330
  }
327
331
  return chain;
328
332
  }
333
+ /**
334
+ * Dependency fields that describe packages actually present in an install.
335
+ *
336
+ * `devDependencies` is deliberately excluded: it is not installed for a
337
+ * transitive dependency, so following it would walk edges that do not exist in
338
+ * the consuming project's tree.
339
+ */
340
+ const INSTALLED_DEPENDENCY_FIELDS = [
341
+ 'dependencies',
342
+ 'optionalDependencies',
343
+ 'peerDependencies',
344
+ ];
345
+ /**
346
+ * Bounds on the dependency-graph search. The walk only runs for a package the
347
+ * direct tiers already failed to find, so these cap a rare path rather than the
348
+ * common one.
349
+ */
350
+ const MAX_TRANSITIVE_DEPTH = 4;
351
+ const MAX_TRANSITIVE_VISITS = 256;
329
352
  /**
330
353
  * Resolve actual installed versions for a bounded candidate set.
331
354
  *
332
355
  * Candidates are deduplicated and sorted. Missing, malformed, or mismatched
333
356
  * manifests are omitted so telemetry collection remains best-effort.
357
+ *
358
+ * Resolves only what `from` can reach unless `includeIndirect` is set.
334
359
  */
335
360
  function resolveInstalledPackageVersions(packageNames, from, options = {}) {
336
361
  const fromDir = from.startsWith('file:') || from.includes('://')
337
362
  ? (0, path_1.dirname)((0, url_1.fileURLToPath)(from))
338
363
  : resolveExistingDir(from);
339
- const requireFromBase = (0, module_1.createRequire)((0, path_1.join)(fromDir, 'noop.js'));
340
- const nodeModulesChain = buildNodeModulesChain(fromDir);
341
364
  const packages = [];
342
365
  const unresolvedPackageNames = [];
343
366
  for (const packageName of [...new Set(packageNames)].sort()) {
@@ -345,7 +368,10 @@ function resolveInstalledPackageVersions(packageNames, from, options = {}) {
345
368
  unresolvedPackageNames.push(packageName);
346
369
  continue;
347
370
  }
348
- const packageJsonPath = resolvePackageJson(requireFromBase, packageName, nodeModulesChain);
371
+ const packageJsonPath = resolvePackageJsonFrom(fromDir, packageName) ??
372
+ (options.includeIndirect
373
+ ? resolvePackageJsonTransitively(fromDir, packageName)
374
+ : undefined);
349
375
  if (!packageJsonPath) {
350
376
  unresolvedPackageNames.push(packageName);
351
377
  continue;
@@ -373,15 +399,21 @@ function isValidPackageName(packageName) {
373
399
  /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(packageName));
374
400
  }
375
401
  /**
376
- * Find a package's `package.json` even when exports hide it, the package is
377
- * ESM-only, or the install is hoisted above the resolution root.
402
+ * Find a package's `package.json` **as resolved from `fromDir`**, even when
403
+ * exports hide it, the package is ESM-only, or the install is hoisted above the
404
+ * resolution root.
378
405
  *
379
406
  * Packages with restricted exports often omit `./package.json`, causing direct
380
407
  * subpath resolution to fail. ESM-only packages may also hide their main entry
381
- * from CommonJS resolution, so the final tier searches the cached ancestor
408
+ * from CommonJS resolution, so the final tier searches the ancestor
382
409
  * `node_modules` chain used by npm, pnpm, and Yarn-style installs.
410
+ *
411
+ * Scoped to one directory so the dependency-graph walk can reuse it verbatim:
412
+ * "resolve X from package Y's location" is the same question as "resolve X from
413
+ * the project root", just asked somewhere else in the tree.
383
414
  */
384
- function resolvePackageJson(req, packageName, nodeModulesChain) {
415
+ function resolvePackageJsonFrom(fromDir, packageName) {
416
+ const req = (0, module_1.createRequire)((0, path_1.join)(fromDir, 'noop.js'));
385
417
  // Tier 1: direct manifest resolution when exports allow the subpath.
386
418
  try {
387
419
  return req.resolve(`${packageName}/package.json`);
@@ -416,14 +448,126 @@ function resolvePackageJson(req, packageName, nodeModulesChain) {
416
448
  catch {
417
449
  // ESM-only package or similar: fall through to node_modules lookup.
418
450
  }
419
- // Tier 3: search cached ancestor node_modules roots, independent of exports.
420
- for (const dir of nodeModulesChain) {
451
+ // Tier 3: search ancestor node_modules roots, independent of exports.
452
+ for (const dir of buildNodeModulesChain(fromDir)) {
421
453
  const candidate = (0, path_1.join)(dir, 'node_modules', packageName, 'package.json');
422
454
  if ((0, fs_1.existsSync)(candidate))
423
455
  return candidate;
424
456
  }
425
457
  return undefined;
426
458
  }
459
+ /**
460
+ * Find a package that is installed but *not reachable from `fromDir`* by
461
+ * searching outward through the dependency graph.
462
+ *
463
+ * A strict/isolated install (pnpm's default, and Yarn's `nmMode: hardlinks`)
464
+ * links a purely transitive package only inside the dependency scope of the
465
+ * package that asked for it — never the consumer's own `node_modules` chain.
466
+ * Resolution from `fromDir` therefore fails for it, correctly: nothing at the
467
+ * project root may import it. It is still the version that ships, because the
468
+ * bundler reaches it through the dependent that does.
469
+ *
470
+ * So the search moves to where the answer is: walk the installed dependency
471
+ * graph breadth-first and re-ask {@link resolvePackageJsonFrom} at each package's
472
+ * own real location. Node's resolver supplies all the layout knowledge —
473
+ * following the symlink out of pnpm's virtual store and then finding the target
474
+ * as its sibling — instead of this code hard-coding store paths, and it costs
475
+ * nothing on the common path because it only runs after a direct miss.
476
+ *
477
+ * Known limitation: Yarn Plug'n'Play has no `node_modules` tree at all, so
478
+ * neither this nor the direct tiers can see it without the `.pnp.cjs` API.
479
+ */
480
+ function resolvePackageJsonTransitively(fromDir, packageName) {
481
+ const visited = new Set();
482
+ let frontier = readInstalledDependencyNames((0, path_1.join)(fromDir, 'package.json'));
483
+ // Not seeded from the manifest alone: a project whose own manifest is
484
+ // unreadable can still have an installed tree worth searching.
485
+ if (frontier.length === 0) {
486
+ frontier = readAncestorNodeModulesEntries(fromDir);
487
+ }
488
+ for (let depth = 0; depth < MAX_TRANSITIVE_DEPTH; depth += 1) {
489
+ const next = [];
490
+ for (const dependentName of frontier) {
491
+ if (visited.size >= MAX_TRANSITIVE_VISITS)
492
+ return undefined;
493
+ if (dependentName === packageName)
494
+ continue;
495
+ if (visited.has(dependentName))
496
+ continue;
497
+ visited.add(dependentName);
498
+ if (!isValidPackageName(dependentName))
499
+ continue;
500
+ const dependentManifest = resolvePackageJsonFrom(fromDir, dependentName);
501
+ if (!dependentManifest)
502
+ continue;
503
+ // The real location matters: resolving through the virtual-store symlink
504
+ // is what puts the target in scope as a sibling.
505
+ let dependentDir;
506
+ try {
507
+ dependentDir = (0, fs_1.realpathSync)((0, path_1.dirname)(dependentManifest));
508
+ }
509
+ catch {
510
+ continue;
511
+ }
512
+ const found = resolvePackageJsonFrom(dependentDir, packageName);
513
+ if (found)
514
+ return found;
515
+ next.push(...readInstalledDependencyNames((0, path_1.join)(dependentDir, 'package.json')));
516
+ }
517
+ if (next.length === 0)
518
+ return undefined;
519
+ frontier = next;
520
+ }
521
+ return undefined;
522
+ }
523
+ /** Names declared in the dependency fields that describe an installed tree. */
524
+ function readInstalledDependencyNames(manifestPath) {
525
+ try {
526
+ const parsed = JSON.parse((0, fs_1.readFileSync)(manifestPath, 'utf8'));
527
+ const names = [];
528
+ for (const field of INSTALLED_DEPENDENCY_FIELDS) {
529
+ const value = parsed[field];
530
+ if (value && typeof value === 'object') {
531
+ names.push(...Object.keys(value));
532
+ }
533
+ }
534
+ return names;
535
+ }
536
+ catch {
537
+ return [];
538
+ }
539
+ }
540
+ /**
541
+ * Top-level package names installed in the nearest ancestor `node_modules`,
542
+ * used to seed the graph walk when no readable manifest declares anything.
543
+ */
544
+ function readAncestorNodeModulesEntries(fromDir) {
545
+ for (const dir of buildNodeModulesChain(fromDir)) {
546
+ const nodeModules = (0, path_1.join)(dir, 'node_modules');
547
+ if (!(0, fs_1.existsSync)(nodeModules))
548
+ continue;
549
+ try {
550
+ const names = [];
551
+ for (const entry of (0, fs_1.readdirSync)(nodeModules, { withFileTypes: true })) {
552
+ if (entry.name.startsWith('.'))
553
+ continue;
554
+ if (entry.name.startsWith('@')) {
555
+ const scopeDir = (0, path_1.join)(nodeModules, entry.name);
556
+ for (const scoped of (0, fs_1.readdirSync)(scopeDir)) {
557
+ names.push(`${entry.name}/${scoped}`);
558
+ }
559
+ continue;
560
+ }
561
+ names.push(entry.name);
562
+ }
563
+ return names;
564
+ }
565
+ catch {
566
+ return [];
567
+ }
568
+ }
569
+ return [];
570
+ }
427
571
  /** Internal: ensure `from` exists for `createRequire` when given a dir. */
428
572
  function resolveExistingDir(from) {
429
573
  const absolute = (0, path_1.resolve)(from);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microsoft/rayfin-docs",
3
- "version": "1.35.0",
3
+ "version": "1.35.1",
4
4
  "description": "Rayfin docs indexing and search library — used by the MCP server, CLI, and other doc-facing surfaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",