verify-renderer-standalone-smoke.mjs 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  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 {
  7. collectPackageEntrypoints,
  8. loadEcosystemReleaseContext
  9. } from './lib/ecosystem-packages.mjs'
  10. const scriptDir = dirname(fileURLToPath(import.meta.url))
  11. const sourceRoot = resolve(scriptDir, '..')
  12. const args = process.argv.slice(2)
  13. const pluginEnginePackages = new Set([
  14. '@file-viewer/pptx',
  15. '@file-viewer/eda-layout',
  16. '@file-viewer/eda-orcad',
  17. '@file-viewer/geometry-engine'
  18. ])
  19. const readArg = (name, fallback) => {
  20. const index = args.indexOf(name)
  21. return index >= 0 ? args[index + 1] : fallback
  22. }
  23. const selectedRenderers = args
  24. .filter(arg => arg.startsWith('--renderer='))
  25. .map(arg => arg.slice('--renderer='.length))
  26. const smokeAllRenderers = args.includes('--all')
  27. const smokeRoot = resolve(
  28. sourceRoot,
  29. readArg(
  30. '--tmp-dir',
  31. process.env.FILE_VIEWER_RENDERER_STANDALONE_SMOKE_DIR || '.release/renderer-standalone-smoke'
  32. )
  33. )
  34. const keepTemp = args.includes('--keep-temp')
  35. const commandTimeoutMs = Number(
  36. readArg('--timeout-ms', process.env.FILE_VIEWER_RENDERER_STANDALONE_TIMEOUT_MS || '300000')
  37. )
  38. const { entries } = await loadEcosystemReleaseContext(sourceRoot)
  39. const entryByPackageName = new Map(entries.map(entry => [entry.packageName, entry]))
  40. const entryById = new Map(entries.map(entry => [entry.id, entry]))
  41. const pluginRendererEntries = entries.filter(
  42. entry => entry.kind === 'renderer' && !pluginEnginePackages.has(entry.packageName)
  43. )
  44. function assert(condition, message) {
  45. if (!condition) {
  46. throw new Error(message)
  47. }
  48. }
  49. function run(command, commandArgs, cwd = sourceRoot, options = {}) {
  50. console.log(`$ ${[command, ...commandArgs].join(' ')}`)
  51. const result = spawnSync(command, commandArgs, {
  52. cwd,
  53. encoding: 'utf8',
  54. stdio: options.capture ? 'pipe' : 'inherit',
  55. timeout: options.timeoutMs ?? commandTimeoutMs,
  56. killSignal: 'SIGTERM',
  57. env: {
  58. ...process.env,
  59. npm_config_loglevel: 'error'
  60. }
  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. async function assertBuiltEntrypoints(entry) {
  84. for (const entrypoint of collectPackageEntrypoints(entry.packageJson)) {
  85. const absoluteEntrypoint = join(entry.absoluteDir, entrypoint)
  86. assert(
  87. existsSync(absoluteEntrypoint),
  88. `${entry.packageName} is missing ${entrypoint}. Build it before renderer standalone smoke.`
  89. )
  90. }
  91. }
  92. function localDependencyNames(packageJson) {
  93. const blocks = [
  94. packageJson.dependencies,
  95. packageJson.devDependencies,
  96. packageJson.optionalDependencies,
  97. packageJson.peerDependencies
  98. ].filter(Boolean)
  99. const names = new Set()
  100. for (const block of blocks) {
  101. for (const name of Object.keys(block)) {
  102. if (entryByPackageName.has(name)) {
  103. names.add(name)
  104. }
  105. }
  106. }
  107. return names
  108. }
  109. function collectLocalDependencyClosure(entry, collected = new Set()) {
  110. for (const dependencyName of localDependencyNames(entry.packageJson)) {
  111. if (collected.has(dependencyName)) {
  112. continue
  113. }
  114. const dependencyEntry = entryByPackageName.get(dependencyName)
  115. if (!dependencyEntry) {
  116. continue
  117. }
  118. collected.add(dependencyName)
  119. collectLocalDependencyClosure(dependencyEntry, collected)
  120. }
  121. return collected
  122. }
  123. function resolveRendererEntry(idOrName) {
  124. const normalized = idOrName.startsWith('renderer-') ? idOrName : `renderer-${idOrName}`
  125. const byId = entryById.get(normalized)
  126. if (byId?.kind === 'renderer' && !pluginEnginePackages.has(byId.packageName)) {
  127. return byId
  128. }
  129. const byPackageName = entryByPackageName.get(idOrName)
  130. if (byPackageName?.kind === 'renderer' && !pluginEnginePackages.has(byPackageName.packageName)) {
  131. return byPackageName
  132. }
  133. throw new Error(`Unknown renderer smoke target: ${idOrName}`)
  134. }
  135. function toFileSpec(fromDir, targetFile) {
  136. const relativePath = relative(fromDir, targetFile).split(sep).join('/')
  137. return `file:${relativePath.startsWith('.') ? relativePath : `./${relativePath}`}`
  138. }
  139. const coreEntry = entryByPackageName.get('@file-viewer/core')
  140. const vitePluginEntry = entryByPackageName.get('@file-viewer/vite-plugin')
  141. assert(coreEntry, 'Missing @file-viewer/core entry')
  142. assert(vitePluginEntry, 'Missing @file-viewer/vite-plugin entry')
  143. const rendererInputs = smokeAllRenderers
  144. ? pluginRendererEntries.map(entry => entry.renderer.id)
  145. : (selectedRenderers.length ? selectedRenderers : ['pdf'])
  146. const rendererEntries = [...new Map(rendererInputs.map(input => {
  147. const entry = resolveRendererEntry(input)
  148. return [entry.packageName, entry]
  149. })).values()]
  150. assert(rendererEntries.length, 'No renderer smoke targets were selected')
  151. const requiredEntries = new Map()
  152. for (const entry of [coreEntry, vitePluginEntry, ...rendererEntries]) {
  153. requiredEntries.set(entry.packageName, entry)
  154. collectLocalDependencyClosure(entry).forEach(packageName => {
  155. const dependencyEntry = entryByPackageName.get(packageName)
  156. if (dependencyEntry) {
  157. requiredEntries.set(dependencyEntry.packageName, dependencyEntry)
  158. }
  159. })
  160. }
  161. const tarballDir = join(smokeRoot, 'tarballs')
  162. const caseRoot = join(smokeRoot, 'case')
  163. if (!keepTemp) {
  164. await rm(smokeRoot, { recursive: true, force: true })
  165. }
  166. await mkdir(tarballDir, { recursive: true })
  167. await mkdir(caseRoot, { recursive: true })
  168. const tarballByPackageName = new Map()
  169. for (const entry of requiredEntries.values()) {
  170. await assertBuiltEntrypoints(entry)
  171. run('pnpm', ['-C', entry.packageDir, 'pack', '--pack-destination', tarballDir], sourceRoot, {
  172. capture: true
  173. })
  174. const tarball = join(tarballDir, entry.tarballName)
  175. assert(existsSync(tarball), `Expected tarball was not created: ${tarball}`)
  176. tarballByPackageName.set(entry.packageName, tarball)
  177. console.log(`Packed ${entry.packageName} -> ${entry.tarballName}`)
  178. }
  179. const dependencies = Object.fromEntries(
  180. [...requiredEntries.values()].map(entry => [
  181. entry.packageName,
  182. toFileSpec(caseRoot, tarballByPackageName.get(entry.packageName))
  183. ])
  184. )
  185. const smokeTargets = rendererEntries.map(entry => ({
  186. id: entry.renderer.id,
  187. packageName: entry.packageName
  188. }))
  189. await writeFile(
  190. join(caseRoot, 'package.json'),
  191. `${JSON.stringify(
  192. {
  193. name: 'file-viewer-renderer-standalone-smoke',
  194. private: true,
  195. type: 'module',
  196. scripts: {
  197. smoke: 'node smoke.mjs'
  198. },
  199. dependencies
  200. },
  201. null,
  202. 2
  203. )}\n`
  204. )
  205. await writeFile(
  206. join(caseRoot, 'smoke.mjs'),
  207. `import assert from 'node:assert/strict'
  208. import {
  209. createRendererRegistry,
  210. installFileViewerRendererPlugins
  211. } from '@file-viewer/core'
  212. import { fileViewerRenderers, resolveFileViewerRendererSelection } from '@file-viewer/vite-plugin'
  213. ${rendererEntries.map((entry, index) => `import renderer${index} from '${entry.packageName}'`).join('\n')}
  214. const rendererPlugins = [${rendererEntries.map((_entry, index) => `renderer${index}`).join(', ')}]
  215. const smokeTargets = ${JSON.stringify(smokeTargets, null, 2)}
  216. const registry = createRendererRegistry([])
  217. const handlers = new Map()
  218. await installFileViewerRendererPlugins({
  219. registry,
  220. plugins: rendererPlugins,
  221. registerHandler(registration) {
  222. handlers.set(registration.rendererId, registration.handler)
  223. }
  224. })
  225. assert.equal(rendererPlugins.length, smokeTargets.length)
  226. assert.equal(registry.hasExtension('docx'), smokeTargets.some(target => target.id === 'word'))
  227. for (const target of smokeTargets) {
  228. const selection = resolveFileViewerRendererSelection({ renderers: [target.id] }, process.cwd())
  229. assert.deepEqual(
  230. selection.renderers.map(item => item.packageName),
  231. [target.packageName],
  232. target.id + ' must map to its own standalone renderer package'
  233. )
  234. const rendererIds = selection.renderers.flatMap(item => item.rendererIds)
  235. assert.ok(rendererIds.length, target.id + ' must expose at least one core renderer id')
  236. for (const rendererId of rendererIds) {
  237. assert.ok(registry.getById(rendererId), target.packageName + ' must register definition ' + rendererId)
  238. assert.ok(handlers.has(rendererId), target.packageName + ' must register handler ' + rendererId)
  239. }
  240. const vitePlugin = fileViewerRenderers({ renderers: [target.id] })
  241. await vitePlugin.buildStart?.()
  242. const resolvedId = vitePlugin.resolveId?.('virtual:file-viewer-renderers')
  243. assert.equal(typeof resolvedId, 'string', target.id + ' virtual module must resolve')
  244. const virtualCode = await vitePlugin.load?.(resolvedId)
  245. assert.ok(
  246. virtualCode.includes(target.packageName),
  247. target.id + ' virtual module must import ' + target.packageName
  248. )
  249. for (const otherTarget of smokeTargets) {
  250. if (otherTarget.packageName !== target.packageName) {
  251. assert.ok(
  252. !virtualCode.includes(otherTarget.packageName),
  253. target.id + ' virtual module must not import unrelated package ' + otherTarget.packageName
  254. )
  255. }
  256. }
  257. }
  258. console.log('[renderer-standalone-smoke] Renderer plugin installation passed for ' + smokeTargets.map(target => target.packageName).join(', ') + '.')
  259. `
  260. )
  261. run('npm', [
  262. 'install',
  263. '--ignore-scripts',
  264. '--no-audit',
  265. '--no-fund',
  266. '--prefer-online',
  267. '--legacy-peer-deps',
  268. '--install-strategy=shallow'
  269. ], caseRoot)
  270. run('npm', ['run', 'smoke'], caseRoot)
  271. console.log(
  272. `[renderer-standalone-smoke] Verified ${rendererEntries.map(entry => entry.packageName).join(', ')} in an isolated install.`
  273. )