verify-package-paths.ts 3.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. /**
  2. * Find stale root-relative `packages/...` references in repo-authored prose and
  3. * TypeScript. A missing path is reported only when it names a real package leaf;
  4. * globs, placeholders, hypothetical packages, and unbuilt `lib/` output are
  5. * outside the check.
  6. */
  7. import { existsSync, readdirSync } from 'node:fs'
  8. import { resolve } from 'node:path'
  9. import { findReferenceViolations, uniqueRepoFiles, type ReferenceViolation as Violation } from './repo-files.ts'
  10. const root = resolve(import.meta.dirname, '..')
  11. /** Markdown + repo-authored TypeScript that may cite package paths. */
  12. const PATTERNS = [
  13. 'README.md',
  14. 'docs/**/*.md',
  15. 'packages/*/*.md',
  16. 'packages/*/*/*.md',
  17. 'AGENTS.md',
  18. 'packages/AGENTS.md',
  19. 'packages/**/*.ts',
  20. 'examples/**/*.ts',
  21. ]
  22. /** Paths excluded from the scan: built output and vendored upstream source. */
  23. const isExcluded = (p: string): boolean =>
  24. p.includes('/lib/') || p.endsWith('.d.ts') || p.startsWith('vendor/')
  25. /**
  26. * Directory names of every real package, `packages/<group>/<pkg>`. A broken
  27. * reference is only flagged when one of its segments is in this set — that is
  28. * what scopes the gate to DRIFT (a moved real package) rather than typos or
  29. * not-yet-existing packages named in a proposal.
  30. */
  31. function realPackageNames(): Set<string> {
  32. const names = new Set<string>()
  33. const pkgRoot = resolve(root, 'packages')
  34. for (const group of readdirSync(pkgRoot, { withFileTypes: true })) {
  35. if (!group.isDirectory()) continue
  36. for (const pkg of readdirSync(resolve(pkgRoot, group.name), { withFileTypes: true })) {
  37. if (pkg.isDirectory()) names.add(pkg.name)
  38. }
  39. }
  40. return names
  41. }
  42. const packageNames = realPackageNames()
  43. /**
  44. * Match a `packages/<path>` reference token. The character class is plain path
  45. * characters only, so a glob (`*`), placeholder (`<`, `>`), or brace expansion
  46. * (`{`, `}`, `,`) terminates the match before those chars and is never probed —
  47. * those are patterns, not real paths. A trailing `.`/`/` (e.g. a sentence-ending
  48. * period) is trimmed before the existence check.
  49. */
  50. const PKG_REF = /\bpackages\/[A-Za-z0-9._/-]+/g
  51. function isDriftedPackageReference(ref: string): boolean {
  52. if (existsSync(resolve(root, ref))) return false
  53. // Ignore unbuilt `lib/` paths only under an existing depth-two package root:
  54. // CI runs this gate before build, while stale group-less paths must still fail.
  55. const parts = ref.split('/')
  56. const libAt = parts.indexOf('lib')
  57. if (libAt === 3 && existsSync(resolve(root, parts.slice(0, 3).join('/')))) return false
  58. // A missing reference is drift only when a path segment names a live package.
  59. return ref.split('/').slice(1).some(segment => packageNames.has(segment))
  60. }
  61. /** Find missing package references whose path names a live package; bare paths, typos, and illustrative skeletons do not count. */
  62. function findViolations(absPath: string): Violation[] {
  63. return findReferenceViolations(
  64. root,
  65. absPath,
  66. PKG_REF,
  67. // Remove trailing separators or sentence punctuation matched greedily.
  68. ref => ref.replace(/[./]+$/, ''),
  69. isDriftedPackageReference,
  70. )
  71. }
  72. const files = uniqueRepoFiles(root, PATTERNS, isExcluded)
  73. const all = files.flatMap(file => findViolations(file.real))
  74. const checked = files.length
  75. if (all.length === 0) {
  76. console.log(`verify-package-paths: ${checked} file(s) checked, all packages/* references resolve.`)
  77. process.exit(0)
  78. }
  79. console.error('verify-package-paths: broken packages/* references found (target does not exist):')
  80. for (const v of all) {
  81. console.error(` ${v.file}:${v.line} ${v.ref}`)
  82. }
  83. process.exit(1)