verify-built-package-invariants.mjs 4.0 KB

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