verify-wrapper-standalone-builds.mjs 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import { existsSync } from 'node:fs'
  2. import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
  3. import { dirname, join, relative, resolve, sep } from 'node:path'
  4. import { spawnSync } from 'node:child_process'
  5. import { fileURLToPath } from 'node:url'
  6. import {
  7. collectPackageEntrypoints,
  8. loadEcosystemReleaseContext,
  9. readJson
  10. } from './lib/ecosystem-packages.mjs'
  11. const scriptDir = dirname(fileURLToPath(import.meta.url))
  12. const sourceRoot = resolve(scriptDir, '..')
  13. const args = process.argv.slice(2)
  14. const readArg = (name, fallback) => {
  15. const index = args.indexOf(name)
  16. return index >= 0 ? args[index + 1] : fallback
  17. }
  18. const outputRoot = resolve(
  19. sourceRoot,
  20. readArg(
  21. '--out-dir',
  22. process.env.FILE_VIEWER_COMPONENT_REPO_DIR ||
  23. process.env.FILE_VIEWER_WRAPPER_REPO_DIR ||
  24. '.release/component-repos'
  25. )
  26. )
  27. const smokeRoot = resolve(
  28. sourceRoot,
  29. readArg(
  30. '--tmp-dir',
  31. process.env.FILE_VIEWER_COMPONENT_STANDALONE_SMOKE_DIR ||
  32. process.env.FILE_VIEWER_WRAPPER_STANDALONE_SMOKE_DIR ||
  33. '.release/component-standalone-smoke'
  34. )
  35. )
  36. const selectedPackages = new Set(
  37. args
  38. .filter(arg => arg.startsWith('--package='))
  39. .map(arg => arg.slice('--package='.length))
  40. )
  41. const selectedIds = new Set(
  42. args
  43. .filter(arg => arg.startsWith('--id='))
  44. .map(arg => arg.slice('--id='.length))
  45. )
  46. const keepTemp = args.includes('--keep-temp')
  47. const packageManager = readArg('--package-manager', process.env.FILE_VIEWER_WRAPPER_STANDALONE_PM || 'npm')
  48. const commandTimeoutMs = Number(
  49. readArg('--timeout-ms', process.env.FILE_VIEWER_WRAPPER_STANDALONE_TIMEOUT_MS || '300000')
  50. )
  51. const { wrapperManifest, entries } = await loadEcosystemReleaseContext(sourceRoot)
  52. const entryByPackageName = new Map(entries.map(entry => [entry.packageName, entry]))
  53. const run = (command, commandArgs, cwd = sourceRoot, options = {}) => {
  54. console.log(`$ ${[command, ...commandArgs].join(' ')}`)
  55. const result = spawnSync(command, commandArgs, {
  56. cwd,
  57. encoding: 'utf8',
  58. stdio: options.quiet ? 'pipe' : 'inherit',
  59. timeout: options.timeoutMs ?? commandTimeoutMs,
  60. killSignal: 'SIGTERM'
  61. })
  62. if (result.error) {
  63. throw new Error(
  64. [
  65. `Command failed in ${cwd}: ${command} ${commandArgs.join(' ')}`,
  66. result.error.message,
  67. result.stdout,
  68. result.stderr
  69. ].filter(Boolean).join('\n')
  70. )
  71. }
  72. if (result.status !== 0) {
  73. throw new Error(
  74. [
  75. `Command failed in ${cwd}: ${command} ${commandArgs.join(' ')}`,
  76. result.stdout,
  77. result.stderr
  78. ].filter(Boolean).join('\n')
  79. )
  80. }
  81. return result.stdout?.trim() || ''
  82. }
  83. const writeJson = async (path, value) => {
  84. await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, 'utf8')
  85. }
  86. const toFileSpec = (fromDir, targetFile) => {
  87. const relativePath = relative(fromDir, targetFile).split(sep).join('/')
  88. return `file:${relativePath.startsWith('.') ? relativePath : `./${relativePath}`}`
  89. }
  90. const dependencyBlocks = packageJson => [
  91. packageJson.dependencies,
  92. packageJson.devDependencies,
  93. packageJson.optionalDependencies
  94. ].filter(Boolean)
  95. const localDependencyNames = packageJson => {
  96. const names = new Set()
  97. for (const block of dependencyBlocks(packageJson)) {
  98. for (const name of Object.keys(block)) {
  99. if (entryByPackageName.has(name)) {
  100. names.add(name)
  101. }
  102. }
  103. }
  104. return names
  105. }
  106. const collectLocalDependencyClosure = (packageJson, collected = new Set()) => {
  107. for (const dependencyName of localDependencyNames(packageJson)) {
  108. if (collected.has(dependencyName)) {
  109. continue
  110. }
  111. collected.add(dependencyName)
  112. const entry = entryByPackageName.get(dependencyName)
  113. if (entry) {
  114. collectLocalDependencyClosure(entry.packageJson, collected)
  115. }
  116. }
  117. return collected
  118. }
  119. const assertBuiltEntrypoints = async entry => {
  120. for (const entrypoint of collectPackageEntrypoints(entry.packageJson)) {
  121. const absoluteEntrypoint = join(entry.absoluteDir, entrypoint)
  122. if (!existsSync(absoluteEntrypoint)) {
  123. throw new Error(
  124. `${entry.packageName} is missing ${entrypoint}. Run pnpm --filter ${entry.packageName} build before standalone wrapper smoke.`
  125. )
  126. }
  127. }
  128. }
  129. const rewriteLocalDependencies = (packageJson, tarballByPackageName, repoDir, closure) => {
  130. for (const block of dependencyBlocks(packageJson)) {
  131. for (const dependencyName of Object.keys(block)) {
  132. const tarball = tarballByPackageName.get(dependencyName)
  133. if (tarball) {
  134. block[dependencyName] = toFileSpec(repoDir, tarball)
  135. }
  136. }
  137. }
  138. packageJson.devDependencies ??= {}
  139. for (const dependencyName of closure) {
  140. if (dependencyName === packageJson.name) {
  141. continue
  142. }
  143. if (
  144. packageJson.dependencies?.[dependencyName] ||
  145. packageJson.optionalDependencies?.[dependencyName] ||
  146. packageJson.devDependencies?.[dependencyName]
  147. ) {
  148. continue
  149. }
  150. const tarball = tarballByPackageName.get(dependencyName)
  151. if (tarball) {
  152. packageJson.devDependencies[dependencyName] = toFileSpec(repoDir, tarball)
  153. }
  154. }
  155. }
  156. const installArgsFor = name => {
  157. if (name === 'npm') {
  158. return [
  159. 'install',
  160. '--ignore-scripts',
  161. '--no-audit',
  162. '--no-fund',
  163. '--prefer-online',
  164. '--legacy-peer-deps',
  165. '--install-strategy=shallow'
  166. ]
  167. }
  168. if (name === 'pnpm') {
  169. return ['install', '--ignore-scripts', '--prefer-offline']
  170. }
  171. if (name === 'yarn') {
  172. return ['install', '--ignore-scripts']
  173. }
  174. throw new Error(`Unsupported package manager for standalone wrapper smoke: ${name}`)
  175. }
  176. const buildArgsFor = name => {
  177. if (name === 'npm') {
  178. return ['run', 'build']
  179. }
  180. if (name === 'pnpm') {
  181. return ['run', 'build']
  182. }
  183. if (name === 'yarn') {
  184. return ['build']
  185. }
  186. throw new Error(`Unsupported package manager for standalone wrapper smoke: ${name}`)
  187. }
  188. const wrappers = wrapperManifest.wrappers.filter(wrapper => {
  189. if (selectedPackages.size && !selectedPackages.has(wrapper.packageName)) {
  190. return false
  191. }
  192. if (selectedIds.size && !selectedIds.has(wrapper.id)) {
  193. return false
  194. }
  195. return true
  196. })
  197. if (!wrappers.length) {
  198. throw new Error('No wrappers selected for standalone build smoke.')
  199. }
  200. if (!existsSync(outputRoot)) {
  201. throw new Error(`Missing exported component repo directory: ${outputRoot}. Run pnpm components:export first.`)
  202. }
  203. const tarballDir = join(smokeRoot, 'tarballs')
  204. const caseRoot = join(smokeRoot, 'cases')
  205. if (!keepTemp) {
  206. await rm(smokeRoot, { recursive: true, force: true })
  207. }
  208. await mkdir(tarballDir, { recursive: true })
  209. await mkdir(caseRoot, { recursive: true })
  210. const dependencyClosure = new Set()
  211. for (const wrapper of wrappers) {
  212. const wrapperPackageJson = await readJson(join(outputRoot, wrapper.repository, 'package.json'))
  213. collectLocalDependencyClosure(wrapperPackageJson, dependencyClosure)
  214. }
  215. const tarballByPackageName = new Map()
  216. for (const packageName of dependencyClosure) {
  217. const entry = entryByPackageName.get(packageName)
  218. if (!entry) {
  219. continue
  220. }
  221. await assertBuiltEntrypoints(entry)
  222. run('pnpm', ['-C', entry.packageDir, 'pack', '--pack-destination', tarballDir], sourceRoot, { quiet: true })
  223. const tarball = join(tarballDir, entry.tarballName)
  224. if (!existsSync(tarball)) {
  225. throw new Error(`Expected local tarball was not created: ${tarball}`)
  226. }
  227. tarballByPackageName.set(packageName, tarball)
  228. console.log(`Packed ${entry.packageName} -> ${entry.tarballName}`)
  229. }
  230. for (const wrapper of wrappers) {
  231. const exportedRepoDir = join(outputRoot, wrapper.repository)
  232. const repoDir = join(caseRoot, wrapper.repository)
  233. await rm(repoDir, { recursive: true, force: true })
  234. await cp(exportedRepoDir, repoDir, { recursive: true, force: true })
  235. const packageJsonPath = join(repoDir, 'package.json')
  236. const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'))
  237. const closure = collectLocalDependencyClosure(packageJson)
  238. rewriteLocalDependencies(packageJson, tarballByPackageName, repoDir, closure)
  239. await writeJson(packageJsonPath, packageJson)
  240. run(packageManager, installArgsFor(packageManager), repoDir)
  241. run(packageManager, buildArgsFor(packageManager), repoDir)
  242. console.log(`Standalone build smoke passed for ${wrapper.packageName}`)
  243. }
  244. console.log(`Verified ${wrappers.length} standalone wrapper build${wrappers.length === 1 ? '' : 's'} with ${packageManager}.`)