verify-vendored-links.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. /**
  2. * Verify that pnpm-lock.yaml resolves every vendored package name to its
  3. * workspace `link:` — never a registry copy. `linkWorkspacePackages: true`
  4. * (pnpm-workspace.yaml) makes matching upstream semver ranges resolve to the
  5. * pinned vendored sources; a registry copy of the same name coexisting with
  6. * the vendored one silently forks the framework layer (vendor/README.md).
  7. */
  8. import { readdir, readFile } from 'node:fs/promises'
  9. import { join, resolve } from 'node:path'
  10. import * as yaml from 'js-yaml'
  11. const root = resolve(import.meta.dirname, '..')
  12. async function vendoredNames(): Promise<Set<string>> {
  13. const names = new Set<string>()
  14. for (const entry of await readdir(join(root, 'vendor'), { withFileTypes: true })) {
  15. if (!entry.isDirectory()) continue
  16. let manifest: { name?: string }
  17. try {
  18. manifest = JSON.parse(await readFile(join(root, 'vendor', entry.name, 'package.json'), 'utf8')) as { name?: string }
  19. } catch {
  20. continue // not a package directory (e.g. vendor/README.md siblings)
  21. }
  22. if (manifest.name !== undefined) names.add(manifest.name)
  23. }
  24. return names
  25. }
  26. interface Lockfile {
  27. importers?: Record<string, Record<string, unknown>>
  28. packages?: Record<string, unknown>
  29. snapshots?: Record<string, unknown>
  30. }
  31. const names = await vendoredNames()
  32. if (names.size === 0) throw new Error('verify-vendored-links: no vendored package manifests found under vendor/')
  33. const lockfile = yaml.load(await readFile(join(root, 'pnpm-lock.yaml'), 'utf8')) as Lockfile
  34. const violations: string[] = []
  35. // Importer resolutions: every dependency entry naming a vendored package must
  36. // resolve to a link:, or the build silently uses a registry copy.
  37. for (const [importer, sections] of Object.entries(lockfile.importers ?? {})) {
  38. for (const [section, dependencies] of Object.entries(sections)) {
  39. if (typeof dependencies !== 'object' || dependencies === null) continue
  40. for (const [dependency, entry] of Object.entries(dependencies as Record<string, { version?: string }>)) {
  41. if (!names.has(dependency)) continue
  42. const version = entry.version ?? ''
  43. if (!version.startsWith('link:')) {
  44. violations.push(`${importer} ${section}.${dependency} resolves to ${JSON.stringify(version)} (expected link:)`)
  45. }
  46. }
  47. }
  48. }
  49. // Package/snapshot keys: a registry copy materializes as a `<name>@<version>`
  50. // key; vendored names must never appear there at all.
  51. for (const section of ['packages', 'snapshots'] as const) {
  52. for (const key of Object.keys(lockfile[section] ?? {})) {
  53. const atIndex = key.lastIndexOf('@')
  54. if (atIndex <= 0) continue
  55. const packageName = key.slice(0, atIndex)
  56. if (names.has(packageName)) violations.push(`${section} entry ${key} is a registry copy of a vendored package`)
  57. }
  58. }
  59. if (violations.length > 0) {
  60. console.error(`verify-vendored-links: ${String(violations.length)} lockfile resolution(s) bypass the vendored workspaces:`)
  61. for (const violation of violations) console.error(` - ${violation}`)
  62. process.exit(1)
  63. }
  64. console.log(`verify-vendored-links: all ${String(names.size)} vendored package names resolve to workspace links.`)