@microsoft/rayfin-docs 1.35.0-alpha.1315 → 1.35.0-alpha.1331

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.
@@ -107,4 +107,25 @@ export declare function validateRayfinDocsManifest(pkgJson: unknown, packageName
107
107
  * docs" for end users.
108
108
  */
109
109
  export declare function docKindToModule(kind: DocKind): 'guide' | 'host' | 'ts-sdk';
110
+ /** One package and its actual installed manifest version. */
111
+ export interface InstalledPackageVersion {
112
+ name: string;
113
+ version: string;
114
+ }
115
+ /** Installed package versions plus candidates that could not be resolved. */
116
+ export interface InstalledPackageVersionResolution {
117
+ packages: InstalledPackageVersion[];
118
+ unresolvedPackageNames: string[];
119
+ }
120
+ /** Optional policy applied to installed manifest versions. */
121
+ export interface InstalledPackageVersionResolutionOptions {
122
+ isVersionAllowed?: (version: string) => boolean;
123
+ }
124
+ /**
125
+ * Resolve actual installed versions for a bounded candidate set.
126
+ *
127
+ * Candidates are deduplicated and sorted. Missing, malformed, or mismatched
128
+ * manifests are omitted so telemetry collection remains best-effort.
129
+ */
130
+ export declare function resolveInstalledPackageVersions(packageNames: readonly string[], from: string, options?: InstalledPackageVersionResolutionOptions): InstalledPackageVersionResolution;
110
131
  //# sourceMappingURL=discovery.d.ts.map
package/dist/discovery.js CHANGED
@@ -318,38 +318,71 @@ function buildNodeModulesChain(fromDir) {
318
318
  return chain;
319
319
  }
320
320
  /**
321
- * Find a package's `package.json` path even when the package's
322
- * `exports` field doesn't expose it as a subpath OR when the package
323
- * is ESM-only (`exports."."` is `{ "import": ... }` without a CJS
324
- * `default`/`require` branch).
321
+ * Resolve actual installed versions for a bounded candidate set.
325
322
  *
326
- * SDK packages with restricted `exports` typically don't list
327
- * `./package.json` and so `require.resolve('<pkg>/package.json')`
328
- * throws `ERR_PACKAGE_PATH_NOT_EXPORTED`. ESM-only packages additionally
329
- * fail `require.resolve('<pkg>')` (CJS resolution can't see the
330
- * `import:` branch). We finally fall back to walking the node_modules
331
- * tree from the resolution base - this matches what every npm/pnpm
332
- * tool does internally.
323
+ * Candidates are deduplicated and sorted. Missing, malformed, or mismatched
324
+ * manifests are omitted so telemetry collection remains best-effort.
325
+ */
326
+ export function resolveInstalledPackageVersions(packageNames, from, options = {}) {
327
+ const fromDir = from.startsWith('file:') || from.includes('://')
328
+ ? dirname(fileURLToPath(from))
329
+ : resolveExistingDir(from);
330
+ const requireFromBase = createRequire(join(fromDir, 'noop.js'));
331
+ const nodeModulesChain = buildNodeModulesChain(fromDir);
332
+ const packages = [];
333
+ const unresolvedPackageNames = [];
334
+ for (const packageName of [...new Set(packageNames)].sort()) {
335
+ if (!isValidPackageName(packageName)) {
336
+ unresolvedPackageNames.push(packageName);
337
+ continue;
338
+ }
339
+ const packageJsonPath = resolvePackageJson(requireFromBase, packageName, nodeModulesChain);
340
+ if (!packageJsonPath) {
341
+ unresolvedPackageNames.push(packageName);
342
+ continue;
343
+ }
344
+ try {
345
+ const manifest = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
346
+ if (manifest.name === packageName &&
347
+ typeof manifest.version === 'string' &&
348
+ manifest.version.length > 0 &&
349
+ (options.isVersionAllowed?.(manifest.version) ?? true)) {
350
+ packages.push({ name: packageName, version: manifest.version });
351
+ }
352
+ else {
353
+ unresolvedPackageNames.push(packageName);
354
+ }
355
+ }
356
+ catch {
357
+ unresolvedPackageNames.push(packageName);
358
+ }
359
+ }
360
+ return { packages, unresolvedPackageNames };
361
+ }
362
+ function isValidPackageName(packageName) {
363
+ return (packageName.length <= 214 &&
364
+ /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(packageName));
365
+ }
366
+ /**
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.
333
369
  *
334
- * Returns the absolute path to `package.json`, or `undefined` when the
335
- * package isn't installed at all.
370
+ * Packages with restricted exports often omit `./package.json`, causing direct
371
+ * 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
373
+ * `node_modules` chain used by npm, pnpm, and Yarn-style installs.
336
374
  */
337
375
  function resolvePackageJson(req, packageName, nodeModulesChain) {
338
- // Fast path: works when the package omits `exports` or explicitly
339
- // includes `./package.json` in it.
376
+ // Tier 1: direct manifest resolution when exports allow the subpath.
340
377
  try {
341
378
  return req.resolve(`${packageName}/package.json`);
342
379
  }
343
- catch (err) {
344
- const code = err?.code;
345
- if (code === 'MODULE_NOT_FOUND') {
346
- // Package isn't installed.
347
- return undefined;
348
- }
349
- // ERR_PACKAGE_PATH_NOT_EXPORTED or similar: keep going.
380
+ catch {
381
+ // MODULE_NOT_FOUND means only this subpath failed to resolve, not that the
382
+ // package is absent. Restricted exports and alternate layouts require the
383
+ // remaining tiers.
350
384
  }
351
- // Second fall-back: resolve the main entry, then walk up directories
352
- // searching for a package.json whose name matches.
385
+ // Tier 2: resolve the package entry, then walk upward to its named manifest.
353
386
  try {
354
387
  const entry = req.resolve(packageName);
355
388
  let dir = dirname(entry);
@@ -358,12 +391,11 @@ function resolvePackageJson(req, packageName, nodeModulesChain) {
358
391
  if (existsSync(candidate)) {
359
392
  try {
360
393
  const parsed = JSON.parse(readFileSync(candidate, 'utf8'));
361
- if (parsed.name === packageName) {
394
+ if (parsed.name === packageName)
362
395
  return candidate;
363
- }
364
396
  }
365
397
  catch {
366
- // keep walking
398
+ // Keep walking toward the package root.
367
399
  }
368
400
  }
369
401
  const parent = dirname(dir);
@@ -373,17 +405,13 @@ function resolvePackageJson(req, packageName, nodeModulesChain) {
373
405
  }
374
406
  }
375
407
  catch {
376
- // ESM-only package or similar: fall through to the node_modules walk.
408
+ // ESM-only package or similar: fall through to node_modules lookup.
377
409
  }
378
- // Third fall-back: walk the precomputed `node_modules` ancestor
379
- // chain looking for `<ancestor>/node_modules/<packageName>/package.json`.
380
- // This is what npm/pnpm/yarn tooling does internally and works
381
- // regardless of how the package's `exports` are configured.
410
+ // Tier 3: search cached ancestor node_modules roots, independent of exports.
382
411
  for (const dir of nodeModulesChain) {
383
412
  const candidate = join(dir, 'node_modules', packageName, 'package.json');
384
- if (existsSync(candidate)) {
413
+ if (existsSync(candidate))
385
414
  return candidate;
386
- }
387
415
  }
388
416
  return undefined;
389
417
  }
@@ -5,6 +5,7 @@ exports.hasExplicitDiscoverySource = hasExplicitDiscoverySource;
5
5
  exports.discoverRayfinDocsPackages = discoverRayfinDocsPackages;
6
6
  exports.validateRayfinDocsManifest = validateRayfinDocsManifest;
7
7
  exports.docKindToModule = docKindToModule;
8
+ exports.resolveInstalledPackageVersions = resolveInstalledPackageVersions;
8
9
  /**
9
10
  * Per-package docs discovery - Phase 2 of `docs-package-architecture`.
10
11
  *
@@ -326,38 +327,71 @@ function buildNodeModulesChain(fromDir) {
326
327
  return chain;
327
328
  }
328
329
  /**
329
- * Find a package's `package.json` path even when the package's
330
- * `exports` field doesn't expose it as a subpath OR when the package
331
- * is ESM-only (`exports."."` is `{ "import": ... }` without a CJS
332
- * `default`/`require` branch).
330
+ * Resolve actual installed versions for a bounded candidate set.
333
331
  *
334
- * SDK packages with restricted `exports` typically don't list
335
- * `./package.json` and so `require.resolve('<pkg>/package.json')`
336
- * throws `ERR_PACKAGE_PATH_NOT_EXPORTED`. ESM-only packages additionally
337
- * fail `require.resolve('<pkg>')` (CJS resolution can't see the
338
- * `import:` branch). We finally fall back to walking the node_modules
339
- * tree from the resolution base - this matches what every npm/pnpm
340
- * tool does internally.
332
+ * Candidates are deduplicated and sorted. Missing, malformed, or mismatched
333
+ * manifests are omitted so telemetry collection remains best-effort.
334
+ */
335
+ function resolveInstalledPackageVersions(packageNames, from, options = {}) {
336
+ const fromDir = from.startsWith('file:') || from.includes('://')
337
+ ? (0, path_1.dirname)((0, url_1.fileURLToPath)(from))
338
+ : resolveExistingDir(from);
339
+ const requireFromBase = (0, module_1.createRequire)((0, path_1.join)(fromDir, 'noop.js'));
340
+ const nodeModulesChain = buildNodeModulesChain(fromDir);
341
+ const packages = [];
342
+ const unresolvedPackageNames = [];
343
+ for (const packageName of [...new Set(packageNames)].sort()) {
344
+ if (!isValidPackageName(packageName)) {
345
+ unresolvedPackageNames.push(packageName);
346
+ continue;
347
+ }
348
+ const packageJsonPath = resolvePackageJson(requireFromBase, packageName, nodeModulesChain);
349
+ if (!packageJsonPath) {
350
+ unresolvedPackageNames.push(packageName);
351
+ continue;
352
+ }
353
+ try {
354
+ const manifest = JSON.parse((0, fs_1.readFileSync)(packageJsonPath, 'utf8'));
355
+ if (manifest.name === packageName &&
356
+ typeof manifest.version === 'string' &&
357
+ manifest.version.length > 0 &&
358
+ (options.isVersionAllowed?.(manifest.version) ?? true)) {
359
+ packages.push({ name: packageName, version: manifest.version });
360
+ }
361
+ else {
362
+ unresolvedPackageNames.push(packageName);
363
+ }
364
+ }
365
+ catch {
366
+ unresolvedPackageNames.push(packageName);
367
+ }
368
+ }
369
+ return { packages, unresolvedPackageNames };
370
+ }
371
+ function isValidPackageName(packageName) {
372
+ return (packageName.length <= 214 &&
373
+ /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(packageName));
374
+ }
375
+ /**
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.
341
378
  *
342
- * Returns the absolute path to `package.json`, or `undefined` when the
343
- * package isn't installed at all.
379
+ * Packages with restricted exports often omit `./package.json`, causing direct
380
+ * 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
382
+ * `node_modules` chain used by npm, pnpm, and Yarn-style installs.
344
383
  */
345
384
  function resolvePackageJson(req, packageName, nodeModulesChain) {
346
- // Fast path: works when the package omits `exports` or explicitly
347
- // includes `./package.json` in it.
385
+ // Tier 1: direct manifest resolution when exports allow the subpath.
348
386
  try {
349
387
  return req.resolve(`${packageName}/package.json`);
350
388
  }
351
- catch (err) {
352
- const code = err?.code;
353
- if (code === 'MODULE_NOT_FOUND') {
354
- // Package isn't installed.
355
- return undefined;
356
- }
357
- // ERR_PACKAGE_PATH_NOT_EXPORTED or similar: keep going.
389
+ catch {
390
+ // MODULE_NOT_FOUND means only this subpath failed to resolve, not that the
391
+ // package is absent. Restricted exports and alternate layouts require the
392
+ // remaining tiers.
358
393
  }
359
- // Second fall-back: resolve the main entry, then walk up directories
360
- // searching for a package.json whose name matches.
394
+ // Tier 2: resolve the package entry, then walk upward to its named manifest.
361
395
  try {
362
396
  const entry = req.resolve(packageName);
363
397
  let dir = (0, path_1.dirname)(entry);
@@ -366,12 +400,11 @@ function resolvePackageJson(req, packageName, nodeModulesChain) {
366
400
  if ((0, fs_1.existsSync)(candidate)) {
367
401
  try {
368
402
  const parsed = JSON.parse((0, fs_1.readFileSync)(candidate, 'utf8'));
369
- if (parsed.name === packageName) {
403
+ if (parsed.name === packageName)
370
404
  return candidate;
371
- }
372
405
  }
373
406
  catch {
374
- // keep walking
407
+ // Keep walking toward the package root.
375
408
  }
376
409
  }
377
410
  const parent = (0, path_1.dirname)(dir);
@@ -381,17 +414,13 @@ function resolvePackageJson(req, packageName, nodeModulesChain) {
381
414
  }
382
415
  }
383
416
  catch {
384
- // ESM-only package or similar: fall through to the node_modules walk.
417
+ // ESM-only package or similar: fall through to node_modules lookup.
385
418
  }
386
- // Third fall-back: walk the precomputed `node_modules` ancestor
387
- // chain looking for `<ancestor>/node_modules/<packageName>/package.json`.
388
- // This is what npm/pnpm/yarn tooling does internally and works
389
- // regardless of how the package's `exports` are configured.
419
+ // Tier 3: search cached ancestor node_modules roots, independent of exports.
390
420
  for (const dir of nodeModulesChain) {
391
421
  const candidate = (0, path_1.join)(dir, 'node_modules', packageName, 'package.json');
392
- if ((0, fs_1.existsSync)(candidate)) {
422
+ if ((0, fs_1.existsSync)(candidate))
393
423
  return candidate;
394
- }
395
424
  }
396
425
  return undefined;
397
426
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microsoft/rayfin-docs",
3
- "version": "1.35.0-alpha.1315",
3
+ "version": "1.35.0-alpha.1331",
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",