| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369 |
- import { existsSync } from 'node:fs'
- import { mkdir, readdir, rm, stat, writeFile } from 'node:fs/promises'
- import { dirname, join, relative, resolve, sep } from 'node:path'
- import { spawnSync } from 'node:child_process'
- import { performance } from 'node:perf_hooks'
- import { fileURLToPath } from 'node:url'
- import {
- collectPackageEntrypoints,
- loadEcosystemReleaseContext
- } from './lib/ecosystem-packages.mjs'
- const scriptDir = dirname(fileURLToPath(import.meta.url))
- const sourceRoot = resolve(scriptDir, '..')
- const args = process.argv.slice(2)
- const readArg = (name, fallback) => {
- const index = args.indexOf(name)
- return index >= 0 ? args[index + 1] : fallback
- }
- const readRepeatedArg = (name) =>
- args
- .filter(arg => arg.startsWith(`${name}=`))
- .map(arg => arg.slice(name.length + 1))
- const commandTimeoutMs = Number(
- readArg('--timeout-ms', process.env.FILE_VIEWER_COLD_INSTALL_TIMEOUT_MS || '600000')
- )
- const coldInstallRoot = resolve(
- sourceRoot,
- readArg(
- '--tmp-dir',
- process.env.FILE_VIEWER_COLD_INSTALL_DIR || '.release/cold-install-time'
- )
- )
- const keepTemp = args.includes('--keep-temp')
- const warnOnly = args.includes('--warn-only') || process.env.FILE_VIEWER_COLD_INSTALL_WARN_ONLY === '1'
- const jsonOutput = args.includes('--json')
- const budgetMultiplier = Number(process.env.FILE_VIEWER_COLD_INSTALL_BUDGET_MULTIPLIER || '1')
- const { entries } = await loadEcosystemReleaseContext(sourceRoot)
- const entryByPackageName = new Map(entries.map(entry => [entry.packageName, entry]))
- const targetDefinitions = [
- {
- id: 'core',
- label: 'Core only',
- dependencies: ['@file-viewer/core'],
- smokeImports: ['@file-viewer/core'],
- maxMs: 60_000
- },
- {
- id: 'vue3',
- label: 'Vue 3 wrapper only',
- dependencies: ['@file-viewer/vue3'],
- smokeImports: [],
- maxMs: 120_000
- },
- {
- id: 'vue3-lite',
- aliases: ['preset-lite', 'lite'],
- label: 'Vue 3 + lite preset',
- dependencies: ['@file-viewer/vue3', '@file-viewer/preset-lite'],
- smokeImports: ['@file-viewer/preset-lite'],
- maxMs: 150_000
- },
- {
- id: 'vue3-office',
- aliases: ['preset-office', 'office'],
- label: 'Vue 3 + office preset',
- dependencies: ['@file-viewer/vue3', '@file-viewer/preset-office'],
- smokeImports: ['@file-viewer/preset-office'],
- maxMs: 240_000
- },
- {
- id: 'vue3-engineering',
- aliases: ['preset-engineering', 'engineering'],
- label: 'Vue 3 + engineering preset',
- dependencies: ['@file-viewer/vue3', '@file-viewer/preset-engineering'],
- smokeImports: ['@file-viewer/preset-engineering'],
- maxMs: 300_000
- },
- {
- id: 'vue3-all',
- aliases: ['preset-all', 'all'],
- label: 'Vue 3 + full preset',
- dependencies: ['@file-viewer/vue3', '@file-viewer/preset-all'],
- smokeImports: ['@file-viewer/preset-all'],
- maxMs: 420_000
- }
- ]
- const targetById = new Map()
- for (const target of targetDefinitions) {
- targetById.set(target.id, target)
- for (const alias of target.aliases || []) {
- targetById.set(alias, target)
- }
- }
- const selectedTargetNames = readRepeatedArg('--target')
- const selectedTargets = selectedTargetNames.length
- ? [...new Map(selectedTargetNames.map(name => {
- const target = targetById.get(name)
- if (!target) {
- throw new Error(
- `Unknown cold install target "${name}". Known targets: ${targetDefinitions.map(item => item.id).join(', ')}`
- )
- }
- return [target.id, target]
- })).values()]
- : targetDefinitions
- function assert(condition, message) {
- if (!condition) {
- throw new Error(message)
- }
- }
- function run(command, commandArgs, cwd = sourceRoot, options = {}) {
- if (!jsonOutput) {
- console.log(`$ ${[command, ...commandArgs].join(' ')}`)
- }
- const captureOutput = options.capture || jsonOutput
- const result = spawnSync(command, commandArgs, {
- cwd,
- encoding: 'utf8',
- stdio: captureOutput ? 'pipe' : 'inherit',
- timeout: options.timeoutMs ?? commandTimeoutMs,
- killSignal: 'SIGTERM',
- env: {
- ...process.env,
- npm_config_loglevel: 'error'
- }
- })
- if (result.error) {
- throw new Error(
- [
- `Command failed in ${cwd}: ${command} ${commandArgs.join(' ')}`,
- result.error.message,
- result.stdout,
- result.stderr
- ].filter(Boolean).join('\n')
- )
- }
- if (result.status !== 0) {
- throw new Error(
- [
- `Command failed in ${cwd}: ${command} ${commandArgs.join(' ')}`,
- result.stdout,
- result.stderr
- ].filter(Boolean).join('\n')
- )
- }
- return result.stdout?.trim() || ''
- }
- function runtimeDependencyNames(packageJson) {
- return Object.keys({
- ...(packageJson.dependencies || {}),
- ...(packageJson.optionalDependencies || {})
- }).sort()
- }
- function collectLocalDependencyClosure(entry, collected = new Map()) {
- for (const dependencyName of runtimeDependencyNames(entry.packageJson)) {
- const dependencyEntry = entryByPackageName.get(dependencyName)
- if (!dependencyEntry || collected.has(dependencyEntry.packageName)) {
- continue
- }
- collected.set(dependencyEntry.packageName, dependencyEntry)
- collectLocalDependencyClosure(dependencyEntry, collected)
- }
- return collected
- }
- async function assertBuiltEntrypoints(entry) {
- for (const entrypoint of collectPackageEntrypoints(entry.packageJson)) {
- const absoluteEntrypoint = join(entry.absoluteDir, entrypoint)
- assert(
- existsSync(absoluteEntrypoint),
- `${entry.packageName} is missing ${entrypoint}. Build it before cold install verification.`
- )
- }
- }
- function toFileSpec(fromDir, targetFile) {
- const relativePath = relative(fromDir, targetFile).split(sep).join('/')
- return `file:${relativePath.startsWith('.') ? relativePath : `./${relativePath}`}`
- }
- async function directorySizeBytes(directory) {
- if (!existsSync(directory)) {
- return 0
- }
- let total = 0
- const items = await readdir(directory, { withFileTypes: true })
- for (const item of items) {
- const absolutePath = join(directory, item.name)
- if (item.isDirectory()) {
- total += await directorySizeBytes(absolutePath)
- } else {
- total += (await stat(absolutePath)).size
- }
- }
- return total
- }
- function formatBytes(value) {
- if (value < 1024) {
- return `${value} B`
- }
- if (value < 1024 * 1024) {
- return `${(value / 1024).toFixed(1)} KiB`
- }
- return `${(value / 1024 / 1024).toFixed(2)} MiB`
- }
- function formatMs(value) {
- if (value < 1000) {
- return `${Math.round(value)} ms`
- }
- return `${(value / 1000).toFixed(2)} s`
- }
- function collectTargetEntries(target) {
- const entriesByPackage = new Map()
- for (const packageName of target.dependencies) {
- const entry = entryByPackageName.get(packageName)
- assert(entry, `Missing package entry for cold install target ${target.id}: ${packageName}`)
- entriesByPackage.set(entry.packageName, entry)
- collectLocalDependencyClosure(entry).forEach(dependencyEntry => {
- entriesByPackage.set(dependencyEntry.packageName, dependencyEntry)
- })
- }
- return [...entriesByPackage.values()]
- }
- const requiredEntries = new Map()
- for (const target of selectedTargets) {
- for (const entry of collectTargetEntries(target)) {
- requiredEntries.set(entry.packageName, entry)
- }
- }
- const tarballDir = join(coldInstallRoot, 'tarballs')
- const casesDir = join(coldInstallRoot, 'cases')
- if (!keepTemp) {
- await rm(coldInstallRoot, { recursive: true, force: true })
- }
- await mkdir(tarballDir, { recursive: true })
- await mkdir(casesDir, { recursive: true })
- const tarballByPackageName = new Map()
- for (const entry of requiredEntries.values()) {
- await assertBuiltEntrypoints(entry)
- run('pnpm', ['-C', entry.packageDir, 'pack', '--pack-destination', tarballDir], sourceRoot, {
- capture: true
- })
- const tarball = join(tarballDir, entry.tarballName)
- assert(existsSync(tarball), `Expected tarball was not created: ${tarball}`)
- tarballByPackageName.set(entry.packageName, tarball)
- if (!jsonOutput) {
- console.log(`[cold-install] Packed ${entry.packageName} -> ${entry.tarballName}`)
- }
- }
- const reports = []
- const errors = []
- for (const target of selectedTargets) {
- const targetEntries = collectTargetEntries(target)
- const caseRoot = join(casesDir, target.id)
- await rm(caseRoot, { recursive: true, force: true })
- await mkdir(caseRoot, { recursive: true })
- const dependencies = Object.fromEntries(
- targetEntries.map(entry => [
- entry.packageName,
- toFileSpec(caseRoot, tarballByPackageName.get(entry.packageName))
- ])
- )
- await writeFile(
- join(caseRoot, 'package.json'),
- `${JSON.stringify(
- {
- name: `file-viewer-cold-install-${target.id}`,
- private: true,
- type: 'module',
- scripts: {
- smoke: 'node smoke.mjs'
- },
- dependencies
- },
- null,
- 2
- )}\n`
- )
- await writeFile(
- join(caseRoot, 'smoke.mjs'),
- `${target.smokeImports.map((packageName, index) => `import * as package${index} from '${packageName}'`).join('\n')}
- ${target.smokeImports.map((_packageName, index) => `if (!package${index}) throw new Error('Missing package ${index}')`).join('\n')}
- console.log('[cold-install] Smoke import passed for ${target.id}.')
- `
- )
- const startedAt = performance.now()
- run(
- 'npm',
- [
- 'install',
- '--ignore-scripts',
- '--no-audit',
- '--no-fund',
- '--prefer-online',
- '--legacy-peer-deps',
- '--install-strategy=shallow'
- ],
- caseRoot
- )
- const elapsedMs = performance.now() - startedAt
- if (target.smokeImports.length) {
- run('npm', ['run', 'smoke'], caseRoot)
- }
- const nodeModulesBytes = await directorySizeBytes(join(caseRoot, 'node_modules'))
- const maxMs = Math.round(target.maxMs * budgetMultiplier)
- const report = {
- id: target.id,
- label: target.label,
- dependencyCount: targetEntries.length,
- elapsedMs: Math.round(elapsedMs),
- maxMs,
- nodeModulesBytes
- }
- reports.push(report)
- if (elapsedMs > maxMs) {
- errors.push(
- `${target.id} cold install took ${formatMs(elapsedMs)}, exceeding budget ${formatMs(maxMs)}`
- )
- }
- }
- if (jsonOutput) {
- console.log(JSON.stringify({ reports, warnOnly }, null, 2))
- } else {
- for (const report of reports) {
- console.log(
- `[cold-install] ${report.id}: ${formatMs(report.elapsedMs)} / budget ${formatMs(report.maxMs)}, local packages ${report.dependencyCount}, node_modules ${formatBytes(report.nodeModulesBytes)}.`
- )
- }
- }
- if (errors.length) {
- const prefix = warnOnly ? '[cold-install] Warnings' : '[cold-install] Failed'
- console.error(prefix)
- errors.forEach(error => console.error(` - ${error}`))
- if (!warnOnly) {
- process.exitCode = 1
- }
- } else {
- if (!jsonOutput) {
- console.log(`[cold-install] Passed ${reports.length} cold install target${reports.length === 1 ? '' : 's'}.`)
- }
- }
|