verify-npm-install-smoke.mjs 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. import { existsSync } from 'node:fs'
  2. import { mkdir, 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 { loadEcosystemReleaseContext } from './lib/ecosystem-packages.mjs'
  7. const scriptDir = dirname(fileURLToPath(import.meta.url))
  8. const sourceRoot = resolve(scriptDir, '..')
  9. const args = process.argv.slice(2)
  10. const hasArg = name => args.includes(name)
  11. const readArg = (name, fallback) => {
  12. const direct = args.find(arg => arg.startsWith(`${name}=`))
  13. if (direct) {
  14. return direct.slice(name.length + 1)
  15. }
  16. const index = args.indexOf(name)
  17. return index >= 0 ? args[index + 1] : fallback
  18. }
  19. const npmSpec = readArg('--npm', process.env.FILE_VIEWER_NPM_SMOKE_NPM || 'npm@11.17.0')
  20. const smokeRoot = resolve(
  21. sourceRoot,
  22. readArg('--tmp-dir', process.env.FILE_VIEWER_NPM_SMOKE_DIR || '.release/npm-install-smoke')
  23. )
  24. const keepTemp = hasArg('--keep-temp')
  25. const registryOnly = hasArg('--registry-only')
  26. const tarballOnly = hasArg('--tarball-only')
  27. const installTargets = ['@file-viewer/vue3', '@file-viewer/preset-office']
  28. const smokeImports = ['@file-viewer/vue3', '@file-viewer/preset-office']
  29. const { entries } = await loadEcosystemReleaseContext(sourceRoot)
  30. const entryByPackageName = new Map(entries.map(entry => [entry.packageName, entry]))
  31. function assert(condition, message) {
  32. if (!condition) {
  33. throw new Error(message)
  34. }
  35. }
  36. function npmCommandArgs(commandArgs) {
  37. if (npmSpec === 'npm') {
  38. return { command: 'npm', args: commandArgs }
  39. }
  40. return { command: 'npx', args: ['--yes', npmSpec, ...commandArgs] }
  41. }
  42. function runNpm(commandArgs, cwd, options = {}) {
  43. const { command, args: npmArgs } = npmCommandArgs(commandArgs)
  44. return run(command, npmArgs, cwd, options)
  45. }
  46. function run(command, commandArgs, cwd = sourceRoot, options = {}) {
  47. console.log(`$ ${[command, ...commandArgs].join(' ')}`)
  48. const result = spawnSync(command, commandArgs, {
  49. cwd,
  50. encoding: 'utf8',
  51. stdio: options.capture ? 'pipe' : 'inherit',
  52. env: {
  53. ...process.env,
  54. npm_config_loglevel: process.env.npm_config_loglevel || 'error',
  55. npm_config_audit: 'false',
  56. npm_config_fund: 'false'
  57. },
  58. timeout: Number(process.env.FILE_VIEWER_NPM_SMOKE_TIMEOUT_MS || 600_000)
  59. })
  60. if (result.error) {
  61. throw new Error(
  62. [
  63. `Command failed in ${cwd}: ${command} ${commandArgs.join(' ')}`,
  64. result.error.message,
  65. result.stdout,
  66. result.stderr
  67. ].filter(Boolean).join('\n')
  68. )
  69. }
  70. if (result.status !== 0) {
  71. throw new Error(
  72. [
  73. `Command failed in ${cwd}: ${command} ${commandArgs.join(' ')}`,
  74. result.stdout,
  75. result.stderr
  76. ].filter(Boolean).join('\n')
  77. )
  78. }
  79. return result.stdout?.trim() || ''
  80. }
  81. function runtimeDependencyNames(packageJson) {
  82. return Object.keys({
  83. ...(packageJson.dependencies || {}),
  84. ...(packageJson.optionalDependencies || {})
  85. }).sort()
  86. }
  87. function collectLocalDependencyClosure(entry, collected = new Map()) {
  88. for (const dependencyName of runtimeDependencyNames(entry.packageJson)) {
  89. const dependencyEntry = entryByPackageName.get(dependencyName)
  90. if (!dependencyEntry || collected.has(dependencyEntry.packageName)) {
  91. continue
  92. }
  93. collected.set(dependencyEntry.packageName, dependencyEntry)
  94. collectLocalDependencyClosure(dependencyEntry, collected)
  95. }
  96. return collected
  97. }
  98. function collectTargetEntries(packageNames) {
  99. const collected = new Map()
  100. for (const packageName of packageNames) {
  101. const entry = entryByPackageName.get(packageName)
  102. assert(entry, `Missing ecosystem package entry for ${packageName}`)
  103. collected.set(entry.packageName, entry)
  104. collectLocalDependencyClosure(entry).forEach(dependencyEntry => {
  105. collected.set(dependencyEntry.packageName, dependencyEntry)
  106. })
  107. }
  108. return [...collected.values()]
  109. }
  110. function toFileSpec(fromDir, targetFile) {
  111. const relativePath = relative(fromDir, targetFile).split(sep).join('/')
  112. return `file:${relativePath.startsWith('.') ? relativePath : `./${relativePath}`}`
  113. }
  114. async function writeSmokeProject(caseRoot, dependencies) {
  115. await mkdir(caseRoot, { recursive: true })
  116. await writeFile(
  117. join(caseRoot, 'package.json'),
  118. `${JSON.stringify(
  119. {
  120. name: `file-viewer-npm-install-smoke-${relative(smokeRoot, caseRoot).replace(/[\\/]/g, '-')}`,
  121. private: true,
  122. type: 'module',
  123. scripts: {
  124. smoke: 'node smoke.mjs'
  125. },
  126. dependencies
  127. },
  128. null,
  129. 2
  130. )}\n`
  131. )
  132. await writeFile(
  133. join(caseRoot, 'smoke.mjs'),
  134. `${smokeImports.map((packageName, index) => `import * as package${index} from '${packageName}'`).join('\n')}
  135. ${smokeImports.map((_packageName, index) => `if (!package${index}) throw new Error('Missing smoke import ${index}')`).join('\n')}
  136. console.log('[npm-install-smoke] Imports passed.')
  137. `
  138. )
  139. }
  140. async function runRegistrySmoke() {
  141. const caseRoot = join(smokeRoot, 'registry')
  142. await rm(caseRoot, { recursive: true, force: true })
  143. await writeSmokeProject(
  144. caseRoot,
  145. Object.fromEntries(installTargets.map(packageName => [packageName, 'latest']))
  146. )
  147. runNpm(['install', '--ignore-scripts', '--no-audit', '--no-fund'], caseRoot)
  148. runNpm(['run', 'smoke'], caseRoot)
  149. console.log(`[npm-install-smoke] Registry install passed with ${npmSpec}.`)
  150. }
  151. async function runTarballSmoke() {
  152. const tarballDir = join(smokeRoot, 'tarballs')
  153. const caseRoot = join(smokeRoot, 'tarball')
  154. await rm(tarballDir, { recursive: true, force: true })
  155. await rm(caseRoot, { recursive: true, force: true })
  156. await mkdir(tarballDir, { recursive: true })
  157. const tarballByPackageName = new Map()
  158. for (const entry of collectTargetEntries(installTargets)) {
  159. run('pnpm', ['-C', entry.packageDir, 'pack', '--pack-destination', tarballDir], sourceRoot, {
  160. capture: true
  161. })
  162. const tarball = join(tarballDir, entry.tarballName)
  163. assert(existsSync(tarball), `Expected tarball was not created: ${tarball}`)
  164. tarballByPackageName.set(entry.packageName, tarball)
  165. }
  166. await writeSmokeProject(
  167. caseRoot,
  168. Object.fromEntries(
  169. [...tarballByPackageName.entries()].map(([packageName, tarball]) => [
  170. packageName,
  171. toFileSpec(caseRoot, tarball)
  172. ])
  173. )
  174. )
  175. runNpm(['install', '--ignore-scripts', '--no-audit', '--no-fund'], caseRoot)
  176. runNpm(['run', 'smoke'], caseRoot)
  177. console.log(`[npm-install-smoke] Local tgz dependency-closure install passed with ${npmSpec}.`)
  178. }
  179. if (!keepTemp) {
  180. await rm(smokeRoot, { recursive: true, force: true })
  181. }
  182. await mkdir(smokeRoot, { recursive: true })
  183. if (!tarballOnly) {
  184. await runRegistrySmoke()
  185. }
  186. if (!registryOnly) {
  187. await runTarballSmoke()
  188. }
  189. console.log('[npm-install-smoke] All requested npm install smoke checks passed.')