build(tooling): [ENG-11] add module analysis without changing artifacts

This commit is contained in:
Harvey Zhao committed 2026-09-11 22:22:53 +08:00
1 parent 4a9a26d4fc
commit 5efc7f7699
11 files changed
+12673 -16

No files matched your search

+44
View File
@@ -0,0 +1,44 @@
import { Buffer } from 'node:buffer'
import { createHash } from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { gzipSync } from 'node:zlib'
export function createBuildAnalysis() {
const root = process.cwd()
const parent = path.join(root, 'refactor/.cache/build-analysis')
fs.mkdirSync(parent, { recursive: true })
const directory = fs.mkdtempSync(path.join(parent, 'run-'))
fs.writeFileSync(path.join(parent, 'latest.json'), `${JSON.stringify({ output: path.relative(root, directory).replaceAll('\\', '/') })}\n`)
return { root, directory }
}
export function buildAnalysisPlugin({ root, directory }, name, format) {
return {
name: 'artplayer-build-analysis',
generateBundle(_options, bundle) {
for (const chunk of Object.values(bundle).filter(item => item.type === 'chunk')) {
const modules = Object.entries(chunk.modules).map(([id, info]) => {
const normalized = id.replaceAll('\\', '/')
const dependency = normalized.lastIndexOf('/node_modules/')
const file = id.startsWith('\0')
? `virtual:${id.slice(1)}`
: dependency === -1 ? path.relative(root, id).replaceAll('\\', '/') : normalized.slice(dependency + 1)
return { file, renderedBytes: Buffer.byteLength(info.code || ''), renderedCodeUnits: info.renderedLength, originalCodeUnits: info.originalLength, renderedExports: info.renderedExports, removedExports: info.removedExports }
}).sort((a, b) => b.renderedBytes - a.renderedBytes || a.file.localeCompare(b.file))
const report = {
schemaVersion: 1,
name,
format,
node: process.version,
file: chunk.fileName,
artifact: { bytes: Buffer.byteLength(chunk.code), gzip9: gzipSync(chunk.code, { level: 9 }).length, sha256: createHash('sha256').update(chunk.code).digest('hex') },
note: 'Rendered UTF-8 bytes precede final chunk minification; they are not additive compressed sizes or download measurements. Rollup lengths are separately recorded as UTF-16 code units. Original code units describe Rollup input, not raw TypeScript.',
modules,
}
fs.writeFileSync(path.join(directory, `${name}-${format}.json`), `${JSON.stringify(report, null, 2)}\n`)
}
},
}
}
+13 -7
View File
@@ -3,6 +3,7 @@ import path from 'node:path'
import process from 'node:process'
import cpy from 'cpy'
import { build as viteBuild } from 'vite'
import { buildAnalysisPlugin, createBuildAnalysis } from './build-analysis.mjs'
import { getEntryFile, selectProjects } from './projects.js'
import { getGlobalName, getProjects, getViteBuildConfig } from './utils.js'
@@ -27,7 +28,7 @@ const BUILD_FORMATS = {
esm: { format: 'es', ext: '.mjs', target: 'es2020', minify: false },
}
async function build(name, targetName, clean = false) {
async function build(name, targetName, clean = false, analysis) {
const projectDir = projects[name]
const { version } = JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), 'utf-8'))
const distDir = path.join(projectDir, 'dist')
@@ -67,6 +68,8 @@ async function build(name, targetName, clean = false) {
max_line_len: false, // No line length limit
},
}
if (analysis)
config.build.rollupOptions.plugins.push(buildAnalysisPlugin(analysis, name, targetName))
await viteBuild({ root: projectDir, ...config })
@@ -77,16 +80,19 @@ async function build(name, targetName, clean = false) {
console.log(`✨ Built@${targetName} ${name}@${version} Time@${Date.now() - startTime}ms Size@${size}kb`)
}
async function buildProject(name) {
await build(name, 'main', true)
await build(name, 'legacy')
await build(name, 'esm')
async function buildProject(name, analysis) {
await build(name, 'main', true, analysis)
await build(name, 'legacy', false, analysis)
await build(name, 'esm', false, analysis)
}
async function runBuild() {
const { names } = await selectProjects(projects, 'build')
const { names, analyze } = await selectProjects(projects, 'build')
const analysis = analyze ? createBuildAnalysis() : undefined
for (const name of names)
await buildProject(name)
await buildProject(name, analysis)
if (analysis)
console.log(`Build analysis: ${analysis.directory}`)
if (names.length)
console.log(`✅ Finished building ${names.length} package(s)!`)
}
+6 -3
View File
@@ -20,9 +20,12 @@ export function getEntryFile(projectDir) {
export function parseProjects(args, projects, command) {
const names = []
let open = true
let analyze = false
for (const arg of args) {
if (arg === '--no-open' && command === 'dev')
open = false
else if (arg === '--analyze' && command === 'build')
analyze = true
else if (arg === '--help')
return { help: true, names: [], open }
else if (arg !== '--')
@@ -31,7 +34,7 @@ export function parseProjects(args, projects, command) {
if (names.includes('all')) {
if (command !== 'build' || names.length !== 1)
throw new Error('Use build all on its own; dev accepts one package')
return { names: Object.keys(projects), open }
return { names: Object.keys(projects), open, analyze }
}
for (const name of names) {
if (!Object.hasOwn(projects, name))
@@ -39,13 +42,13 @@ export function parseProjects(args, projects, command) {
}
if (command === 'dev' && names.length > 1)
throw new Error('dev accepts one package')
return { names: [...new Set(names)], open }
return { names: [...new Set(names)], open, analyze }
}
export async function selectProjects(projects, command) {
const selection = parseProjects(process.argv.slice(2), projects, command)
if (selection.help) {
console.log(command === 'build' ? 'Usage: yarn build [all | package ...]' : 'Usage: yarn dev [package] [--no-open]')
console.log(command === 'build' ? 'Usage: yarn build [all | package ...] [--analyze]' : 'Usage: yarn dev [package] [--no-open]')
return selection
}
if (!selection.names.length) {