verify-npm-install-layout.ts 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. /** Verify npm's physical package placement for two incompatible DSH releases. */
  2. import { readFileSync } from 'node:fs'
  3. import { posix, resolve } from 'node:path'
  4. import {
  5. buildRegistryIndex,
  6. resolveNpmPackageLock,
  7. type NpmLockPackage,
  8. type NpmPackageLock,
  9. type RegistryIndex,
  10. } from './benchmark-npm-resolution.ts'
  11. const DSH_PACKAGE = '@deepseek-ai/dsh'
  12. const CORDIS_PACKAGE = '@deepseek-ai/cordis'
  13. const NESTED_DSH_ALIAS = 'dsh-previous'
  14. const NESTED_DSH_PATH = `node_modules/${NESTED_DSH_ALIAS}`
  15. const DEPENDENCY_FIELDS = ['dependencies', 'optionalDependencies', 'peerDependencies'] as const
  16. const TIMEOUT_MS = 300_000
  17. /** Synthetic incompatible versions used to expose cross-release placement errors. */
  18. export const SYNTHETIC_DSH_VERSIONS = ['0.1.0', '0.2.0'] as const
  19. interface MutableRegistryManifest {
  20. name: string
  21. version: string
  22. dependencies?: Record<string, string>
  23. optionalDependencies?: Record<string, string>
  24. peerDependencies?: Record<string, string>
  25. [key: string]: unknown
  26. }
  27. /** Summary of a verified two-release npm layout. */
  28. export interface DshInstallLayoutSummary {
  29. readonly dshPackagesPerVersion: number
  30. readonly checkedDshEdges: number
  31. }
  32. function isDshPackage(name: string): boolean {
  33. return name === DSH_PACKAGE || name.startsWith(`${DSH_PACKAGE}-`)
  34. }
  35. function cloneForVersion(manifest: object, version: string): MutableRegistryManifest {
  36. const cloned = structuredClone(manifest) as MutableRegistryManifest
  37. cloned.version = version
  38. for (const field of DEPENDENCY_FIELDS) {
  39. const dependencies = cloned[field]
  40. if (dependencies === undefined) continue
  41. for (const name of Object.keys(dependencies)) {
  42. if (isDshPackage(name)) dependencies[name] = `^${version}`
  43. }
  44. }
  45. return cloned
  46. }
  47. /**
  48. * Replace the working release with two incompatible, internally consistent DSH releases.
  49. * @param index - Registry metadata containing the working release.
  50. * @param sourceVersion - Workspace version copied into each synthetic release.
  51. * @returns Registry metadata containing both synthetic DSH releases and unchanged external packages.
  52. */
  53. export function buildDualDshRegistry(index: RegistryIndex, sourceVersion: string): RegistryIndex {
  54. const output = new Map(index)
  55. let dshPackages = 0
  56. for (const [name, versions] of index) {
  57. if (!isDshPackage(name)) {
  58. output.set(name, versions)
  59. continue
  60. }
  61. const source = versions.get(sourceVersion)
  62. if (source === undefined) throw new Error(`${name} has no workspace version ${sourceVersion}`)
  63. dshPackages++
  64. output.set(name, new Map(SYNTHETIC_DSH_VERSIONS.map(version => [
  65. version,
  66. cloneForVersion(source, version),
  67. ])))
  68. }
  69. if (dshPackages === 0) throw new Error('registry contains no DSH packages')
  70. return output
  71. }
  72. function packageNameAtPath(path: string, manifest: NpmLockPackage): string | undefined {
  73. if (manifest.name !== undefined) return manifest.name
  74. const marker = 'node_modules/'
  75. const markerIndex = path.lastIndexOf(marker)
  76. if (markerIndex < 0) return undefined
  77. const segments = path.slice(markerIndex + marker.length).split('/')
  78. if (segments[0]?.startsWith('@')) {
  79. return segments[1] === undefined ? undefined : `${segments[0]}/${segments[1]}`
  80. }
  81. return segments[0]
  82. }
  83. function resolvePackagePath(
  84. packages: Readonly<Record<string, NpmLockPackage>>,
  85. sourcePath: string,
  86. dependency: string,
  87. ): string | undefined {
  88. let directory = sourcePath
  89. while (directory !== '.') {
  90. const candidate = posix.join(directory, 'node_modules', dependency)
  91. if (packages[candidate] !== undefined) return candidate
  92. directory = posix.dirname(directory)
  93. }
  94. const rootCandidate = posix.join('node_modules', dependency)
  95. return packages[rootCandidate] === undefined ? undefined : rootCandidate
  96. }
  97. function setDifference(left: ReadonlySet<string>, right: ReadonlySet<string>): string[] {
  98. return [...left].filter(value => !right.has(value)).sort()
  99. }
  100. /**
  101. * Assert that npm isolates both DSH releases while sharing the Cordis runtime.
  102. * @param packageLock - Metadata-only package lock produced by npm.
  103. * @returns Counts for the verified DSH packages and dependency edges.
  104. */
  105. export function assertDualDshInstallLayout(packageLock: NpmPackageLock): DshInstallLayoutSummary {
  106. const [nestedVersion, rootVersion] = SYNTHETIC_DSH_VERSIONS
  107. const errors: string[] = []
  108. const namesByVersion = new Map<string, Set<string>>([
  109. [nestedVersion, new Set()],
  110. [rootVersion, new Set()],
  111. ])
  112. const installed = Object.entries(packageLock.packages)
  113. let checkedDshEdges = 0
  114. for (const [path, manifest] of installed) {
  115. const name = packageNameAtPath(path, manifest)
  116. if (name === 'react' || name === 'react-dom') {
  117. errors.push(`${path}: ${name} is a browser build input, not a dependency of the synthetic DSH-only consumer`)
  118. }
  119. if (name === undefined || !isDshPackage(name)) continue
  120. const version = manifest.version
  121. if (version !== nestedVersion && version !== rootVersion) {
  122. errors.push(`${path}: expected DSH version ${nestedVersion} or ${rootVersion}, got ${String(version)}`)
  123. continue
  124. }
  125. namesByVersion.get(version)?.add(name)
  126. const expectedPath = version === rootVersion
  127. ? `node_modules/${name}`
  128. : name === DSH_PACKAGE
  129. ? NESTED_DSH_PATH
  130. : `${NESTED_DSH_PATH}/node_modules/${name}`
  131. if (path !== expectedPath) {
  132. errors.push(`${path}: expected ${name}@${version} at ${expectedPath}`)
  133. }
  134. for (const field of DEPENDENCY_FIELDS) {
  135. for (const dependency of Object.keys(manifest[field] ?? {})) {
  136. if (!isDshPackage(dependency)) continue
  137. const targetPath = resolvePackagePath(packageLock.packages, path, dependency)
  138. const optionalPeer = field === 'peerDependencies'
  139. && manifest.peerDependenciesMeta?.[dependency]?.optional === true
  140. if (targetPath === undefined) {
  141. if (field === 'optionalDependencies' || optionalPeer) continue
  142. errors.push(`${path}: ${field} ${dependency} does not resolve`)
  143. continue
  144. }
  145. checkedDshEdges++
  146. const targetVersion = packageLock.packages[targetPath]?.version
  147. if (targetVersion !== version) {
  148. errors.push(
  149. `${path}: ${field} ${dependency} resolves to ${targetPath}@${String(targetVersion)}, expected ${version}`,
  150. )
  151. }
  152. }
  153. }
  154. }
  155. const nestedNames = namesByVersion.get(nestedVersion) ?? new Set<string>()
  156. const rootNames = namesByVersion.get(rootVersion) ?? new Set<string>()
  157. if (!nestedNames.has(DSH_PACKAGE)) errors.push(`${NESTED_DSH_PATH}: missing ${DSH_PACKAGE}@${nestedVersion}`)
  158. if (!rootNames.has(DSH_PACKAGE)) errors.push(`node_modules/${DSH_PACKAGE}: missing ${DSH_PACKAGE}@${rootVersion}`)
  159. const onlyNested = setDifference(nestedNames, rootNames)
  160. const onlyRoot = setDifference(rootNames, nestedNames)
  161. if (onlyNested.length > 0) errors.push(`only ${nestedVersion} contains: ${onlyNested.join(', ')}`)
  162. if (onlyRoot.length > 0) errors.push(`only ${rootVersion} contains: ${onlyRoot.join(', ')}`)
  163. const cordisPaths = installed.flatMap(([path, manifest]) =>
  164. packageNameAtPath(path, manifest) === CORDIS_PACKAGE ? [path] : [])
  165. if (cordisPaths.length !== 1 || cordisPaths[0] !== `node_modules/${CORDIS_PACKAGE}`) {
  166. errors.push(`expected one shared ${CORDIS_PACKAGE} at node_modules/${CORDIS_PACKAGE}, got ${cordisPaths.join(', ')}`)
  167. }
  168. if (errors.length > 0) throw new Error(`invalid npm install layout:\n${errors.map(error => ` - ${error}`).join('\n')}`)
  169. return { dshPackagesPerVersion: rootNames.size, checkedDshEdges }
  170. }
  171. function workspaceVersion(root: string): string {
  172. const manifest = JSON.parse(readFileSync(resolve(root, 'apps/cli/package.json'), 'utf8')) as { version?: unknown }
  173. if (typeof manifest.version !== 'string') throw new Error('apps/cli/package.json has no string version')
  174. return manifest.version
  175. }
  176. async function main(): Promise<void> {
  177. const root = resolve(import.meta.dirname, '..')
  178. const index = buildDualDshRegistry(buildRegistryIndex(root), workspaceVersion(root))
  179. const [nestedVersion, rootVersion] = SYNTHETIC_DSH_VERSIONS
  180. const result = await resolveNpmPackageLock(index, {
  181. [DSH_PACKAGE]: rootVersion,
  182. [NESTED_DSH_ALIAS]: `npm:${DSH_PACKAGE}@${nestedVersion}`,
  183. }, TIMEOUT_MS)
  184. if (result.archiveRequests !== 0) throw new Error(`npm requested ${String(result.archiveRequests)} package archive(s)`)
  185. const summary = assertDualDshInstallLayout(result.packageLock)
  186. console.log(
  187. `verify-npm-install-layout: ${String(summary.dshPackagesPerVersion)} DSH package(s) per release and `
  188. + `${String(summary.checkedDshEdges)} internal edge(s) verified in ${(result.durationMs / 1000).toFixed(2)} s; `
  189. + `both releases share one Cordis installation; ${String(result.unknownPackages.length)} unavailable optional `
  190. + 'package name(s) ignored by npm.',
  191. )
  192. }
  193. if (import.meta.main) {
  194. try {
  195. await main()
  196. } catch (error) {
  197. console.error(`verify-npm-install-layout: ${error instanceof Error ? error.message : String(error)}`)
  198. process.exitCode = 1
  199. }
  200. }