/** * Copyright 2023-present DreamNum Co., Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* eslint-disable max-lines-per-function */ import path from 'node:path'; import process from 'node:process'; import { brotliCompressSync, gzipSync } from 'node:zlib'; import fs from 'fs-extra'; type OutputBucketKey = 'cjs' | 'es' | 'types' | 'umd'; interface IPackageJson { name: string; } interface IFileSizeSummary { brotliBytes: number; bucket: OutputBucketKey; gzipBytes: number; path: string; rawBytes: number; } interface IDirectorySizeSummary { brotliBytes: number; fileCount: number; gzipBytes: number; largestFile: IFileSizeSummary | null; rawBytes: number; } interface IWorkspacePackage { dir: string; packageJson: IPackageJson; } interface IPackageSizeEntry { built: boolean; name: string; outputs: Record; path: string; totals: IDirectorySizeSummary; } const ROOT_DIR = process.cwd(); const ARTIFACTS_DIR = path.join(ROOT_DIR, 'artifacts'); const PACKAGES_DIR = path.join(ROOT_DIR, 'packages'); const OUTPUT_DIRECTORIES: Record = { cjs: path.join('lib', 'cjs'), es: path.join('lib', 'es'), types: path.join('lib', 'types'), umd: path.join('lib', 'umd'), }; function createEmptySummary(): IDirectorySizeSummary { return { brotliBytes: 0, fileCount: 0, gzipBytes: 0, largestFile: null, rawBytes: 0, }; } function measureFile(filePath: string, bucket: OutputBucketKey): IFileSizeSummary { const fileBuffer = fs.readFileSync(filePath); return { brotliBytes: brotliCompressSync(fileBuffer).byteLength, bucket, gzipBytes: gzipSync(fileBuffer).byteLength, path: path.relative(ROOT_DIR, filePath), rawBytes: fileBuffer.byteLength, }; } function summarizeFiles(files: IFileSizeSummary[]): IDirectorySizeSummary { const summary = createEmptySummary(); for (const file of files) { summary.fileCount += 1; summary.rawBytes += file.rawBytes; summary.gzipBytes += file.gzipBytes; summary.brotliBytes += file.brotliBytes; if (!summary.largestFile || file.rawBytes > summary.largestFile.rawBytes) { summary.largestFile = file; } } return summary; } function summarizeDirectoryTotals(summaries: IDirectorySizeSummary[]): IDirectorySizeSummary { const merged = createEmptySummary(); for (const summary of summaries) { merged.fileCount += summary.fileCount; merged.rawBytes += summary.rawBytes; merged.gzipBytes += summary.gzipBytes; merged.brotliBytes += summary.brotliBytes; if (!merged.largestFile || (summary.largestFile && summary.largestFile.rawBytes > merged.largestFile.rawBytes)) { merged.largestFile = summary.largestFile; } } return merged; } function listFiles(targetDir: string, bucket: OutputBucketKey): IFileSizeSummary[] { if (!fs.existsSync(targetDir)) { return []; } const files: IFileSizeSummary[] = []; function walk(currentDir: string) { for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) { const entryPath = path.join(currentDir, entry.name); if (entry.isDirectory()) { walk(entryPath); continue; } files.push(measureFile(entryPath, bucket)); } } walk(targetDir); return files; } function readWorkspacePackages(): IWorkspacePackage[] { if (!fs.existsSync(PACKAGES_DIR)) { return []; } return fs.readdirSync(PACKAGES_DIR, { withFileTypes: true }) .filter((entry) => entry.isDirectory()) .map((entry) => path.join(PACKAGES_DIR, entry.name)) .map((dir) => { const packageJsonPath = path.join(dir, 'package.json'); if (!fs.existsSync(packageJsonPath)) { return null; } return { dir, packageJson: fs.readJSONSync(packageJsonPath) as IPackageJson, }; }) .filter((pkg): pkg is IWorkspacePackage => Boolean(pkg)) .sort((left, right) => left.packageJson.name.localeCompare(right.packageJson.name)); } function analyzePackage(pkg: IWorkspacePackage): IPackageSizeEntry { const outputFiles = Object.fromEntries( Object.entries(OUTPUT_DIRECTORIES).map(([bucket, relativeDir]) => { return [bucket, listFiles(path.join(pkg.dir, relativeDir), bucket as OutputBucketKey)]; }) ) as Record; const outputs = Object.fromEntries( Object.entries(outputFiles).map(([bucket, files]) => { return [bucket, files.length > 0 ? summarizeFiles(files) : null]; }) ) as Record; const files = Object.values(outputFiles).flat(); return { built: files.length > 0, name: pkg.packageJson.name, outputs, path: path.relative(ROOT_DIR, pkg.dir), totals: summarizeFiles(files), }; } function buildPackageSizesReport(packages: IWorkspacePackage[]) { const entries = packages.map(analyzePackage); return { generatedAt: new Date().toISOString(), packages: entries, scope: 'packages/*', summary: { builtPackageCount: entries.filter((entry) => entry.built).length, totalPackageCount: entries.length, totals: summarizeDirectoryTotals(entries.map((entry) => entry.totals)), }, }; } function formatBytes(bytes: number) { if (bytes < 1024) { return `${bytes} B`; } const units = ['KB', 'MB', 'GB']; let value = bytes / 1024; let unitIndex = 0; while (value >= 1024 && unitIndex < units.length - 1) { value /= 1024; unitIndex += 1; } return `${value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)} ${units[unitIndex]}`; } function buildVisualizationHtml(report: ReturnType) { const rows = [...report.packages].sort((left, right) => right.totals.rawBytes - left.totals.rawBytes); const maxBytes = Math.max(...rows.map((row) => row.totals.rawBytes), 1); const cards = rows.map((row) => { const width = `${(row.totals.rawBytes / maxBytes) * 100}%`; const outputs = (Object.entries(row.outputs) as Array<[OutputBucketKey, IDirectorySizeSummary | null]>) .filter(([, summary]) => Boolean(summary)) .map(([bucket, summary]) => { return `${bucket}: ${formatBytes(summary?.rawBytes ?? 0)}`; }) .join(''); return `

${row.name}

${row.path}

${formatBytes(row.totals.rawBytes)}
${row.totals.fileCount} files gzip ${formatBytes(row.totals.gzipBytes)} brotli ${formatBytes(row.totals.brotliBytes)}
${outputs}
`; }).join(''); return ` Package Sizes

Package Size Report

${report.summary.totalPackageCount} packages in scope
${report.summary.builtPackageCount} packages with build output
${report.generatedAt} generated at
${cards}
`; } function main() { const report = buildPackageSizesReport(readWorkspacePackages()); fs.ensureDirSync(ARTIFACTS_DIR); fs.writeJSONSync(path.join(ARTIFACTS_DIR, 'package-sizes.json'), report, { EOL: '\n', spaces: 4, }); fs.writeFileSync( path.join(ARTIFACTS_DIR, 'package-sizes.html'), buildVisualizationHtml(report), 'utf8' ); console.log(`Wrote ${path.relative(ROOT_DIR, path.join(ARTIFACTS_DIR, 'package-sizes.json'))}`); console.log(`Wrote ${path.relative(ROOT_DIR, path.join(ARTIFACTS_DIR, 'package-sizes.html'))}`); } main();