verify-built-package-invariants.mjs 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. /** Verify every compiled companion through its staged package self-reference under plain Node. */
  2. import {
  3. copyFileSync,
  4. cpSync,
  5. existsSync,
  6. globSync,
  7. mkdirSync,
  8. mkdtempSync,
  9. readFileSync,
  10. rmSync,
  11. writeFileSync,
  12. } from 'node:fs'
  13. import { dirname, resolve } from 'node:path'
  14. import { pathToFileURL } from 'node:url'
  15. const repositoryRoot = resolve(import.meta.dirname, '..')
  16. const options = parseOptions(process.argv.slice(2))
  17. const packagesRoot = resolve(options.get('--packages-root') ?? repositoryRoot)
  18. const loaderUrl = options.get('--loader-url')
  19. ?? pathToFileURL(resolve(repositoryRoot, 'vendor/loader/lib/index.js')).href
  20. const failures = []
  21. const manifests = globSync('packages/*/*/package.json', { cwd: packagesRoot }).sort()
  22. const { default: Loader } = await import(loaderUrl)
  23. const loader = Object.create(Loader.prototype)
  24. for (const manifestPath of manifests) {
  25. const packageDir = dirname(resolve(packagesRoot, manifestPath))
  26. const manifest = JSON.parse(readFileSync(resolve(packagesRoot, manifestPath), 'utf8'))
  27. const packageName = manifest.name
  28. if (typeof packageName !== 'string' || packageName.length === 0) {
  29. failures.push(`${manifestPath}: missing package name`)
  30. continue
  31. }
  32. const invariantExport = manifest.exports?.['./invariant']
  33. if (typeof invariantExport !== 'object'
  34. || invariantExport.default !== './lib/invariant.js'
  35. || !manifest.files?.includes('lib/invariant.js')) {
  36. failures.push(`${packageName}: manifest does not publish ./lib/invariant.js as ./invariant`)
  37. continue
  38. }
  39. // Keep the staged view below its owning package so Node reaches the real
  40. // pnpm dependency links. Junctioning node_modules elsewhere breaks pnpm's
  41. // relative workspace links on Windows. Copy the manifest-declared lib view
  42. // so a companion that imports an undeclared runtime chunk fails here.
  43. const stagedPackageDir = mkdtempSync(resolve(packageDir, '.dsh-built-invariant-'))
  44. try {
  45. copyFileSync(resolve(packageDir, 'package.json'), resolve(stagedPackageDir, 'package.json'))
  46. copyDeclaredLibFiles(packageDir, stagedPackageDir, manifest.files)
  47. const probePath = resolve(stagedPackageDir, 'probe.mjs')
  48. writeFileSync(
  49. probePath,
  50. `import * as companion from ${JSON.stringify(`${packageName}/invariant`)}\nexport default companion\n`,
  51. )
  52. const { default: companion } = await import(pathToFileURL(probePath).href)
  53. if ('default' in companion) throw new Error('companion has a default export')
  54. const unwrapped = loader.unwrapExports(companion)
  55. if (unwrapped !== companion) throw new Error('Loader collapsed the companion namespace')
  56. if (typeof unwrapped.name !== 'string') throw new Error('companion name is missing')
  57. if (!Array.isArray(unwrapped.inject) || !unwrapped.inject.includes('invariants')) {
  58. throw new Error('companion does not inject invariants')
  59. }
  60. if (typeof unwrapped.apply !== 'function') throw new Error('companion apply is missing')
  61. } catch (error) {
  62. failures.push(`${packageName}: ${error instanceof Error ? error.message : String(error)}`)
  63. } finally {
  64. rmSync(stagedPackageDir, { recursive: true, force: true })
  65. }
  66. }
  67. if (failures.length > 0) {
  68. console.error('verify-built-package-invariants: compiled companion failures:')
  69. for (const failure of failures) console.error(` ${failure}`)
  70. process.exit(1)
  71. }
  72. console.log(`verify-built-package-invariants: ${manifests.length} compiled companion(s) passed plain-Node Loader checks.`)
  73. function parseOptions(args) {
  74. const allowed = new Set(['--packages-root', '--loader-url'])
  75. const parsed = new Map()
  76. for (let index = 0; index < args.length; index += 2) {
  77. const name = args[index]
  78. const value = args[index + 1]
  79. if (!allowed.has(name) || value === undefined || value.startsWith('--')) {
  80. throw new Error(`verify-built-package-invariants: expected [--packages-root PATH] [--loader-url URL], got ${JSON.stringify(args)}.`)
  81. }
  82. if (parsed.has(name)) throw new Error(`verify-built-package-invariants: duplicate option ${name}.`)
  83. parsed.set(name, value)
  84. }
  85. return parsed
  86. }
  87. function copyDeclaredLibFiles(packageDir, stagedPackageDir, files) {
  88. for (const pattern of files) {
  89. if (!pattern.startsWith('lib/')) continue
  90. for (const relativePath of globSync(pattern, { cwd: packageDir })) {
  91. const source = resolve(packageDir, relativePath)
  92. if (!existsSync(source)) continue
  93. const target = resolve(stagedPackageDir, relativePath)
  94. mkdirSync(dirname(target), { recursive: true })
  95. cpSync(source, target, { recursive: true })
  96. }
  97. }
  98. }