verify-built-package-invariants.mjs 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. /** Verify every packed companion through its package self-reference under plain Node. */
  2. import { spawnSync } from 'node:child_process'
  3. import {
  4. copyFileSync,
  5. globSync,
  6. mkdirSync,
  7. mkdtempSync,
  8. readFileSync,
  9. rmSync,
  10. } from 'node:fs'
  11. import { dirname, resolve } from 'node:path'
  12. import { pathToFileURL } from 'node:url'
  13. const root = resolve(import.meta.dirname, '..')
  14. const loaderUrl = pathToFileURL(resolve(root, 'vendor/loader/lib/index.js')).href
  15. const failures = []
  16. const manifests = globSync('packages/*/*/package.json', { cwd: root }).sort()
  17. const packArgs = ['pack', '--dry-run', '--json', '--ignore-scripts']
  18. // Windows cannot spawn npm's .cmd shim directly; setup-node installs this JS
  19. // entrypoint beside node.exe, so the probe stays shell-free on every runner.
  20. const npmInvocation = process.platform === 'win32'
  21. ? [process.execPath, [resolve(dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js'), ...packArgs]]
  22. : ['npm', packArgs]
  23. for (const manifestPath of manifests) {
  24. const packageDir = dirname(resolve(root, manifestPath))
  25. const manifest = JSON.parse(readFileSync(resolve(root, manifestPath), 'utf8'))
  26. const packageName = manifest.name
  27. if (typeof packageName !== 'string' || packageName.length === 0) {
  28. failures.push(`${manifestPath}: missing package name`)
  29. continue
  30. }
  31. const pack = spawnSync(npmInvocation[0], npmInvocation[1], {
  32. cwd: packageDir,
  33. encoding: 'utf8',
  34. })
  35. if (pack.status !== 0) {
  36. const detail = pack.error?.message
  37. ?? (pack.stderr.trim() || pack.stdout.trim() || `npm pack exited ${pack.status}`)
  38. failures.push(`${packageName}: ${detail}`)
  39. continue
  40. }
  41. let files
  42. try {
  43. const result = JSON.parse(pack.stdout)
  44. files = result[0]?.files
  45. if (!Array.isArray(files)) throw new Error('npm pack returned no file inventory')
  46. } catch (error) {
  47. failures.push(`${packageName}: cannot parse npm pack inventory: ${String(error)}`)
  48. continue
  49. }
  50. // Keep the packed view below its owning package so Node reaches the real
  51. // pnpm dependency links. Junctioning node_modules elsewhere breaks pnpm's
  52. // relative workspace links on Windows.
  53. const stagedPackageDir = mkdtempSync(resolve(packageDir, '.dsh-packed-invariant-'))
  54. try {
  55. for (const file of files) {
  56. if (typeof file.path !== 'string'
  57. || (file.path !== 'package.json' && !file.path.startsWith('lib/'))) continue
  58. const target = resolve(stagedPackageDir, file.path)
  59. mkdirSync(dirname(target), { recursive: true })
  60. copyFileSync(resolve(packageDir, file.path), target)
  61. }
  62. const probe = `
  63. const companion = await import(${JSON.stringify(`${packageName}/invariant`)});
  64. const { default: Loader } = await import(${JSON.stringify(loaderUrl)});
  65. if ('default' in companion) throw new Error('companion has a default export');
  66. const loader = Object.create(Loader.prototype);
  67. const unwrapped = loader.unwrapExports(companion);
  68. if (unwrapped !== companion) throw new Error('Loader collapsed the companion namespace');
  69. if (typeof unwrapped.name !== 'string') throw new Error('companion name is missing');
  70. if (!Array.isArray(unwrapped.inject) || !unwrapped.inject.includes('invariants')) {
  71. throw new Error('companion does not inject invariants');
  72. }
  73. if (typeof unwrapped.apply !== 'function') throw new Error('companion apply is missing');
  74. `
  75. const result = spawnSync(process.execPath, ['--input-type=module', '--eval', probe], {
  76. cwd: stagedPackageDir,
  77. encoding: 'utf8',
  78. })
  79. if (result.status !== 0) {
  80. const detail = result.error?.message
  81. ?? (result.stderr.trim() || result.stdout.trim() || `node exited ${result.status}`)
  82. failures.push(`${packageName}: ${detail}`)
  83. }
  84. } finally {
  85. rmSync(stagedPackageDir, { recursive: true, force: true })
  86. }
  87. }
  88. if (failures.length > 0) {
  89. console.error('verify-built-package-invariants: packed companion failures:')
  90. for (const failure of failures) console.error(` ${failure}`)
  91. process.exit(1)
  92. }
  93. console.log(`verify-built-package-invariants: ${manifests.length} packed companion(s) passed plain-Node Loader checks.`)