| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351 |
- import { spawnSync } from 'node:child_process'
- import { dirname, resolve } from 'node:path'
- import { fileURLToPath } from 'node:url'
- import { loadEcosystemReleaseContext } from './lib/ecosystem-packages.mjs'
- const scriptDir = dirname(fileURLToPath(import.meta.url))
- const sourceRoot = resolve(scriptDir, '..')
- const { entries } = await loadEcosystemReleaseContext(sourceRoot)
- const packageBudgets = {
- '@file-viewer/core': {
- maxPackedBytes: 130_000,
- maxUnpackedBytes: 700_000,
- maxFiles: 100,
- maxDirectRuntimeDependencies: 1,
- maxExternalDependencyClosure: 1,
- maxLocalPackageClosure: 1
- },
- '@file-viewer/vue3': {
- maxPackedBytes: 1_900_000,
- maxUnpackedBytes: 7_500_000,
- maxFiles: 45,
- maxDirectRuntimeDependencies: 3,
- maxExternalDependencyClosure: 41,
- maxLocalPackageClosure: 3
- },
- '@file-viewer/web': {
- maxPackedBytes: 35_000_000,
- maxUnpackedBytes: 125_000_000,
- maxFiles: 2_900,
- maxDirectRuntimeDependencies: 1,
- maxExternalDependencyClosure: 39,
- maxLocalPackageClosure: 3
- },
- '@file-viewer/preset-all': {
- maxPackedBytes: 10_000,
- maxUnpackedBytes: 32_000,
- maxFiles: 9,
- maxDirectRuntimeDependencies: 20,
- maxExternalDependencyClosure: 41,
- maxLocalPackageClosure: 25
- },
- '@file-viewer/vite-plugin': {
- maxPackedBytes: 30_000,
- maxUnpackedBytes: 100_000,
- maxFiles: 12,
- maxDirectRuntimeDependencies: 0,
- maxExternalDependencyClosure: 0,
- maxLocalPackageClosure: 1
- },
- '@file-viewer/pptx': {
- maxPackedBytes: 130_000,
- maxUnpackedBytes: 600_000,
- maxFiles: 24,
- maxDirectRuntimeDependencies: 5,
- maxExternalDependencyClosure: 5,
- maxLocalPackageClosure: 1
- },
- '@file-viewer/eda-layout': {
- maxPackedBytes: 25_000,
- maxUnpackedBytes: 90_000,
- maxFiles: 10,
- maxDirectRuntimeDependencies: 0,
- maxExternalDependencyClosure: 0,
- maxLocalPackageClosure: 1
- },
- '@file-viewer/eda-orcad': {
- maxPackedBytes: 25_000,
- maxUnpackedBytes: 90_000,
- maxFiles: 10,
- maxDirectRuntimeDependencies: 0,
- maxExternalDependencyClosure: 0,
- maxLocalPackageClosure: 1
- },
- '@file-viewer/geometry-engine': {
- maxPackedBytes: 28_000,
- maxUnpackedBytes: 110_000,
- maxFiles: 10,
- maxDirectRuntimeDependencies: 0,
- maxExternalDependencyClosure: 0,
- maxLocalPackageClosure: 1
- },
- '@file-viewer/renderer-3d': {
- maxPackedBytes: 38_000,
- maxUnpackedBytes: 180_000,
- maxFiles: 18,
- maxDirectRuntimeDependencies: 3,
- maxExternalDependencyClosure: 4,
- maxLocalPackageClosure: 3
- },
- '@file-viewer/renderer-ofd': {
- maxPackedBytes: 140_000,
- maxUnpackedBytes: 520_000,
- maxFiles: 34,
- maxDirectRuntimeDependencies: 3,
- maxExternalDependencyClosure: 39,
- maxLocalPackageClosure: 3
- },
- '@file-viewer/renderer-word': {
- maxPackedBytes: 35_000,
- maxUnpackedBytes: 160_000,
- maxFiles: 18,
- maxDirectRuntimeDependencies: 5,
- maxExternalDependencyClosure: 39,
- maxLocalPackageClosure: 3
- },
- '@file-viewer/renderer-spreadsheet': {
- maxPackedBytes: 45_000,
- maxUnpackedBytes: 180_000,
- maxFiles: 30,
- maxDirectRuntimeDependencies: 4,
- maxExternalDependencyClosure: 39,
- maxLocalPackageClosure: 3
- },
- '@file-viewer/renderer-drawing': {
- maxPackedBytes: 35_000,
- maxUnpackedBytes: 160_000,
- maxFiles: 18,
- maxDirectRuntimeDependencies: 6,
- maxExternalDependencyClosure: 6,
- maxLocalPackageClosure: 3
- },
- '@file-viewer/renderer-text': {
- maxPackedBytes: 35_000,
- maxUnpackedBytes: 160_000,
- maxFiles: 18,
- maxDirectRuntimeDependencies: 5,
- maxExternalDependencyClosure: 5,
- maxLocalPackageClosure: 3
- },
- '@file-viewer/renderer-data': {
- maxPackedBytes: 35_000,
- maxUnpackedBytes: 160_000,
- maxFiles: 18,
- maxDirectRuntimeDependencies: 5,
- maxExternalDependencyClosure: 39,
- maxLocalPackageClosure: 3
- },
- // Typst intentionally ships a self-hosted default font set so source files can
- // render inside offline/intranet deployments without falling back to public CDN
- // font downloads. Keep this explicit so future size growth is still visible.
- '@file-viewer/renderer-typst': {
- maxPackedBytes: 6_200_000,
- maxUnpackedBytes: 9_200_000,
- maxFiles: 30,
- maxDirectRuntimeDependencies: 4,
- maxExternalDependencyClosure: 4,
- maxLocalPackageClosure: 3
- }
- }
- const kindBudgets = {
- renderer: {
- maxPackedBytes: 35_000,
- maxUnpackedBytes: 160_000,
- maxFiles: 18,
- maxDirectRuntimeDependencies: 4
- },
- 'standard-wrapper': {
- maxPackedBytes: 30_000,
- maxUnpackedBytes: 120_000,
- maxFiles: 12,
- maxDirectRuntimeDependencies: 3
- },
- preset: {
- maxPackedBytes: 10_000,
- maxUnpackedBytes: 30_000,
- maxFiles: 10,
- maxDirectRuntimeDependencies: 20
- }
- }
- const entriesByPackageName = new Map(entries.map((entry) => [entry.packageName, entry]))
- const packageNames = new Set(entriesByPackageName.keys())
- function run(command, commandArgs, options = {}) {
- const result = spawnSync(command, commandArgs, {
- cwd: options.cwd || sourceRoot,
- encoding: 'utf8',
- stdio: options.capture ? 'pipe' : 'inherit',
- env: {
- ...process.env,
- npm_config_loglevel: 'error'
- }
- })
- if (result.status !== 0) {
- throw new Error(`Command failed: ${command} ${commandArgs.join(' ')}`)
- }
- return result.stdout || ''
- }
- function parseNpmPackJson(output, packageName) {
- const jsonStart = output.indexOf('[')
- if (jsonStart === -1) {
- throw new Error(`${packageName} npm pack dry-run did not return JSON output`)
- }
- const parsed = JSON.parse(output.slice(jsonStart))
- if (!Array.isArray(parsed) || parsed.length !== 1) {
- throw new Error(`${packageName} npm pack dry-run returned an unexpected payload`)
- }
- return parsed[0]
- }
- function readDryRunPack(entry) {
- const output = run('npm', ['pack', '--dry-run', '--json'], {
- cwd: entry.absoluteDir,
- capture: true
- })
- return parseNpmPackJson(output, entry.packageName)
- }
- function runtimeDependencyNames(packageJson) {
- return Object.keys({
- ...(packageJson.dependencies || {}),
- ...(packageJson.optionalDependencies || {})
- }).sort()
- }
- function collectInstallClosure(
- packageName,
- state = {
- external: new Set(),
- local: new Set(),
- visiting: new Set()
- }
- ) {
- if (state.visiting.has(packageName)) {
- return state
- }
- const entry = entriesByPackageName.get(packageName)
- if (!entry) {
- state.external.add(packageName)
- return state
- }
- state.local.add(packageName)
- state.visiting.add(packageName)
- for (const dependencyName of runtimeDependencyNames(entry.packageJson)) {
- collectInstallClosure(dependencyName, state)
- }
- state.visiting.delete(packageName)
- return state
- }
- function resolveBudget(entry) {
- return packageBudgets[entry.packageName] || kindBudgets[entry.kind] || null
- }
- 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 assertBudget(metric, value, max, entry, errors) {
- if (max === undefined) {
- return
- }
- if (value > max) {
- errors.push(`${entry.packageName} ${metric} ${value} exceeds budget ${max}`)
- }
- }
- const reports = []
- const errors = []
- for (const entry of entries) {
- const budget = resolveBudget(entry)
- if (!budget) {
- continue
- }
- const pack = readDryRunPack(entry)
- const runtimeDependencies = runtimeDependencyNames(entry.packageJson)
- const closure = collectInstallClosure(entry.packageName)
- const externalClosure = [...closure.external].filter((name) => !packageNames.has(name)).sort()
- const localClosure = [...closure.local].sort()
- const report = {
- packageName: entry.packageName,
- kind: entry.kind,
- packedBytes: pack.size,
- unpackedBytes: pack.unpackedSize,
- fileCount: pack.files.length,
- directRuntimeDependencyCount: runtimeDependencies.length,
- externalDependencyClosureCount: externalClosure.length,
- localPackageClosureCount: localClosure.length
- }
- reports.push(report)
- assertBudget('packedBytes', report.packedBytes, budget.maxPackedBytes, entry, errors)
- assertBudget('unpackedBytes', report.unpackedBytes, budget.maxUnpackedBytes, entry, errors)
- assertBudget('fileCount', report.fileCount, budget.maxFiles, entry, errors)
- assertBudget(
- 'directRuntimeDependencyCount',
- report.directRuntimeDependencyCount,
- budget.maxDirectRuntimeDependencies,
- entry,
- errors
- )
- assertBudget(
- 'externalDependencyClosureCount',
- report.externalDependencyClosureCount,
- budget.maxExternalDependencyClosure,
- entry,
- errors
- )
- assertBudget(
- 'localPackageClosureCount',
- report.localPackageClosureCount,
- budget.maxLocalPackageClosure,
- entry,
- errors
- )
- }
- if (errors.length) {
- console.error('[install-budget] Failed')
- errors.forEach((error) => console.error(` - ${error}`))
- console.error('\nCurrent install budget report:')
- reports.forEach((report) => {
- console.error(
- ` - ${report.packageName}: packed ${formatBytes(report.packedBytes)}, unpacked ${formatBytes(report.unpackedBytes)}, files ${report.fileCount}, direct deps ${report.directRuntimeDependencyCount}, external closure ${report.externalDependencyClosureCount}, local closure ${report.localPackageClosureCount}`
- )
- })
- process.exitCode = 1
- } else {
- const tracked = reports.filter((report) =>
- [
- '@file-viewer/core',
- '@file-viewer/vue3',
- '@file-viewer/web',
- '@file-viewer/preset-lite',
- '@file-viewer/preset-office',
- '@file-viewer/preset-engineering',
- '@file-viewer/preset-all',
- '@file-viewer/vite-plugin'
- ].includes(report.packageName)
- )
- tracked.forEach((report) => {
- console.log(
- `[install-budget] ${report.packageName}: packed ${formatBytes(report.packedBytes)}, unpacked ${formatBytes(report.unpackedBytes)}, files ${report.fileCount}, direct deps ${report.directRuntimeDependencyCount}, external closure ${report.externalDependencyClosureCount}, local closure ${report.localPackageClosureCount}.`
- )
- })
- console.log(`[install-budget] Passed ${reports.length} package install surface budgets.`)
- }
|