diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 08c7f13f0f..616dcd1909 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -165,7 +165,7 @@ External packages **directly declared** only by repository tooling, test infrast | [`cytoscape`](https://github.com/cytoscape/cytoscape.js) | MIT | | [`cytoscape-cose-bilkent`](https://github.com/cytoscape/cytoscape.js-cose-bilkent) | MIT | | [`dayjs`](https://github.com/iamkun/dayjs) | MIT | -| [`debug`](https://github.com/visionmedia/debug) | MIT | +| [`debug`](https://github.com/debug-js/debug) | MIT | | [`esbuild`](https://github.com/evanw/esbuild) | MIT | | [`eslint-plugin-sonarjs`](https://github.com/SonarSource/SonarJS) | LGPL-3.0-only | | [`execa`](https://github.com/sindresorhus/execa) | MIT | diff --git a/scripts/gen-third-party-notices.spec.ts b/scripts/gen-third-party-notices.spec.ts index 68812143c3..33d8661aed 100644 --- a/scripts/gen-third-party-notices.spec.ts +++ b/scripts/gen-third-party-notices.spec.ts @@ -114,6 +114,24 @@ describe('virtualManifest', () => { } }) + it('selects the requested version when the store retains historical copies', () => { + const root = mkdtempSync(join(tmpdir(), 'dsh-notices-version-')) + try { + const name = '@scope/pkg' + const store = join(root, 'store') + for (const version of ['1.0.0', '2.0.0']) { + const manifestDir = join(store, `${name.replace('/', '+')}@${version}`, 'node_modules', name) + mkdirSync(manifestDir, { recursive: true }) + writeFileSync(join(manifestDir, 'package.json'), JSON.stringify({ name, version, license: 'MIT' })) + } + + expect(virtualManifest(store, name, '2.0.0')).toMatchObject({ name, version: '2.0.0' }) + expect(virtualManifest(store, name, '3.0.0')).toBeUndefined() + } finally { + rmSync(root, { recursive: true, force: true }) + } + }) + it('returns undefined when neither the prefix nor the content scan finds the package', () => { const root = mkdtempSync(join(tmpdir(), 'dsh-notices-miss-')) try { diff --git a/scripts/gen-third-party-notices.ts b/scripts/gen-third-party-notices.ts index 4f56031c1e..e076677055 100644 --- a/scripts/gen-third-party-notices.ts +++ b/scripts/gen-third-party-notices.ts @@ -9,7 +9,7 @@ */ import { existsSync, globSync, readdirSync, readFileSync, writeFileSync } from 'node:fs' -import { resolve } from 'node:path' +import { dirname, resolve } from 'node:path' import * as yaml from 'js-yaml' import { parse as parseToml, type TomlTableWithoutBigInt, type TomlValueWithoutBigInt } from 'smol-toml' import parseSpdx from 'spdx-expression-parse' @@ -248,38 +248,72 @@ export function claudeDistributionFromManifest( * * @param virtual - the `.pnpm` virtual store directory to scan. * @param name - the external package name, exactly as `node_modules` spells it. + * @param expectedVersion - exact version required when the store retains more than one. * @returns the parsed manifest, or `undefined` when neither the prefix match - * nor the content scan finds the package's `package.json`. + * nor the content scan finds the requested package version. */ -export function virtualManifest(virtual: string, name: string): VirtualManifest | undefined { +export function virtualManifest( + virtual: string, + name: string, + expectedVersion?: string, +): VirtualManifest | undefined { const prefix = `${name.replace('/', '+')}@` - const entry = readdirSync(virtual).find(dir => dir.startsWith(prefix)) - if (entry !== undefined) { - return JSON.parse(readFileSync(resolve(virtual, entry, 'node_modules', name, 'package.json'), 'utf8')) as VirtualManifest + const entries = readdirSync(virtual) + for (const entry of entries.filter(dir => dir.startsWith(prefix))) { + const manifest = JSON.parse(readFileSync(resolve(virtual, entry, 'node_modules', name, 'package.json'), 'utf8')) as VirtualManifest + if (expectedVersion === undefined || manifest.version === expectedVersion) return manifest } - for (const dir of readdirSync(virtual)) { + for (const dir of entries) { const candidate = resolve(virtual, dir, 'node_modules', name, 'package.json') if (existsSync(candidate)) { - return JSON.parse(readFileSync(candidate, 'utf8')) as VirtualManifest + const manifest = JSON.parse(readFileSync(candidate, 'utf8')) as VirtualManifest + if (expectedVersion === undefined || manifest.version === expectedVersion) return manifest } } return undefined } +const workspaceLinkedManifestCache = new Map() + +/** + * Resolve the package version selected for a declaring workspace instead of an + * unrelated historical version that still occupies the shared virtual store. + * @param name - external package identity. + * @returns the first current workspace link for that package, when installed. + */ +function workspaceLinkedManifest(name: string): VirtualManifest | undefined { + if (workspaceLinkedManifestCache.has(name)) return workspaceLinkedManifestCache.get(name) + for (const [path, manifest] of loadWorkspaceManifests().manifests) { + if (!ALL_KINDS.some(kind => name in (manifest[kind] ?? {}))) continue + const linked = resolve(root, dirname(path), 'node_modules', name, 'package.json') + if (!existsSync(linked)) continue + const found = JSON.parse(readFileSync(linked, 'utf8')) as VirtualManifest + workspaceLinkedManifestCache.set(name, found) + return found + } + workspaceLinkedManifestCache.set(name, undefined) + return undefined +} + /** Resolve one installed external package manifest from either pnpm store. */ -function installedManifest(name: string): VirtualManifest | undefined { +function installedManifest(name: string, expectedVersion?: string): VirtualManifest | undefined { + const linked = workspaceLinkedManifest(name) + if (linked !== undefined && (expectedVersion === undefined || linked.version === expectedVersion)) return linked let manifest: (Manifest & { license?: string; repository?: string | { url?: string }; homepage?: string }) | undefined // Workspace-local link farms can expose a dependency that is not linked at // the repository root; both are backed by the root workspace's lockfile. for (const store of ['node_modules', 'native/landlock-run/node_modules']) { const direct = resolve(root, store, name, 'package.json') if (existsSync(direct)) { - manifest = JSON.parse(readFileSync(direct, 'utf8')) as typeof manifest - break + const candidate = JSON.parse(readFileSync(direct, 'utf8')) as typeof manifest + if (expectedVersion === undefined || candidate?.version === expectedVersion) { + manifest = candidate + break + } } const virtual = resolve(root, store, '.pnpm') if (!existsSync(virtual)) continue - manifest = virtualManifest(virtual, name) + manifest = virtualManifest(virtual, name, expectedVersion) if (manifest !== undefined) break } return manifest @@ -308,7 +342,7 @@ function collectClaudeDistribution(): ClaudeDistribution { const distribution = claudeDistributionFromManifest(manifest) let installedPayloads = 0 for (const payload of distribution.payloads) { - const installed = installedManifest(payload.name) + const installed = installedManifest(payload.name, payload.version) if (installed === undefined) continue installedPayloads += 1 if (