From 5edf9a8dffddbe80f58ef6e5982f9289305e7cc2 Mon Sep 17 00:00:00 2001 From: EGOIST <0x142857@gmail.com> Date: Fri, 10 Dec 2021 16:34:26 +0800 Subject: [PATCH] feat: add bundle size reporter --- package.json | 4 ++-- src/esbuild/index.ts | 15 ++++++++------ src/index.ts | 4 +++- src/lib/report-size.ts | 35 ++++++++++++++++++++++++++++++++ src/log.ts | 4 ++-- src/plugin.ts | 39 +++++++++++++++++++++++++----------- src/plugins/size-reporter.ts | 22 ++++++++++++++++++++ src/rollup.ts | 17 +++++++++++++++- 8 files changed, 116 insertions(+), 24 deletions(-) create mode 100644 src/lib/report-size.ts create mode 100644 src/plugins/size-reporter.ts diff --git a/package.json b/package.json index 9e319aeb..3b3b2453 100644 --- a/package.json +++ b/package.json @@ -44,9 +44,9 @@ "postcss-load-config": "^3.0.1", "resolve-from": "^5.0.0", "rollup": "^2.60.0", - "tree-kill": "^1.2.2", "source-map": "^0.7.3", - "sucrase": "^3.20.3" + "sucrase": "^3.20.3", + "tree-kill": "^1.2.2" }, "devDependencies": { "@rollup/plugin-json": "^4.1.0", diff --git a/src/esbuild/index.ts b/src/esbuild/index.ts index 73c822cc..7a8bb239 100644 --- a/src/esbuild/index.ts +++ b/src/esbuild/index.ts @@ -89,12 +89,19 @@ export async function runEsbuild( const loader = options.loader || {} const injectShims = options.shims !== false + pluginContainer.setContext({ + format, + splitting, + options, + logger, + }) + const esbuildPlugins: Array = [ format === 'cjs' && nodeProtocolPlugin(), { name: 'modify-options', setup(build) { - pluginContainer.modifyEsbuildOptions(build.initialOptions, { format }) + pluginContainer.modifyEsbuildOptions(build.initialOptions) if (options.esbuildOptions) { options.esbuildOptions(build.initialOptions, { format }) } @@ -231,11 +238,7 @@ export async function runEsbuild( await pluginContainer.buildFinished({ files: result.outputFiles, - context: { - format, - splitting, - options, - }, + metafile: result.metafile, }) } diff --git a/src/index.ts b/src/index.ts index 2e7e7c65..992a3fc1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,7 @@ import { shebang } from './plugins/shebang' import { cjsSplitting } from './plugins/cjs-splitting' import { PluginContainer } from './plugin' import { es5 } from './plugins/es5' +import { sizeReporter } from './plugins/size-reporter' export type { Format, Options } @@ -185,9 +186,10 @@ export async function build(_options: Options) { ...options.format.map(async (format, index) => { const pluginContainer = new PluginContainer([ shebang(), + ...(options.plugins || []), cjsSplitting(), es5(), - ...(options.plugins || []), + sizeReporter(), ]) await pluginContainer.buildStarted() await runEsbuild(options, { diff --git a/src/lib/report-size.ts b/src/lib/report-size.ts new file mode 100644 index 00000000..3c87fa5c --- /dev/null +++ b/src/lib/report-size.ts @@ -0,0 +1,35 @@ +import * as colors from 'colorette' +import { Logger } from '../log' + +const prettyBytes = (bytes: number) => { + const unit = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + const exp = Math.floor(Math.log(bytes) / Math.log(1024)) + return `${(bytes / Math.pow(1024, exp)).toFixed(2)} ${unit[exp]}` +} + +const getLengthOfLongestString = (strings: string[]) => { + return strings.reduce((max, str) => { + return Math.max(max, str.length) + }, 0) +} + +const padRight = (str: string, maxLength: number) => { + return str + ' '.repeat(maxLength - str.length) +} + +export const reportSize = ( + logger: Logger, + format: string, + files: { [name: string]: number } +) => { + const filenames = Object.keys(files) + const maxLength = getLengthOfLongestString(filenames) + 1 + for (const name of filenames) { + logger.success( + format, + `${colors.bold(padRight(name, maxLength))}${colors.green( + prettyBytes(files[name]) + )}` + ) + } +} diff --git a/src/log.ts b/src/log.ts index 4cfe41bf..23f4e158 100644 --- a/src/log.ts +++ b/src/log.ts @@ -2,7 +2,7 @@ import * as colors from 'colorette' type LOG_TYPE = 'info' | 'success' | 'error' | 'warn' -const colorize = (type: LOG_TYPE, data: any, onlyImportant = false) => { +export const colorize = (type: LOG_TYPE, data: any, onlyImportant = false) => { if (onlyImportant && (type === 'info' || type === 'success')) return data const color = @@ -26,7 +26,7 @@ export const makeLabel = ( colorize(type, input.toUpperCase()), ] .filter(Boolean) - .join(colors.dim(' ')) + .join(' ') } let silent = false diff --git a/src/plugin.ts b/src/plugin.ts index 75718184..d4054344 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,8 +1,9 @@ import path from 'path' -import { OutputFile, BuildOptions as EsbuildOptions } from 'esbuild' +import { OutputFile, BuildOptions as EsbuildOptions, Metafile } from 'esbuild' import { SourceMapConsumer, SourceMapGenerator, RawSourceMap } from 'source-map' import { Format, NormalizedOptions } from '.' import { outputFile } from './fs' +import { Logger } from './log' export type ChunkInfo = { type: 'chunk' @@ -37,12 +38,15 @@ export type RenderChunk = ( | void > -export type BuildStart = () => MaybePromise -export type BuildEnd = () => MaybePromise +export type BuildStart = (this: PluginContext) => MaybePromise +export type BuildEnd = ( + this: PluginContext, + ctx: { metafile?: Metafile } +) => MaybePromise export type ModifyEsbuildOptions = ( - options: EsbuildOptions, - context: { format: Format } + this: PluginContext, + options: EsbuildOptions ) => void export type Plugin = { @@ -61,6 +65,7 @@ export type PluginContext = { format: Format splitting?: boolean options: NormalizedOptions + logger: Logger } const parseSourceMap = (map?: string | object | null) => { @@ -72,15 +77,25 @@ const isCSS = (path: string) => /\.css$/.test(path) export class PluginContainer { plugins: Plugin[] + context?: PluginContext constructor(plugins: Plugin[]) { this.plugins = plugins } - modifyEsbuildOptions(options: EsbuildOptions, context: { format: Format }) { + setContext(context: PluginContext) { + this.context = context + } + + getContext() { + if (!this.context) throw new Error(`Plugin context is not set`) + return this.context + } + + modifyEsbuildOptions(options: EsbuildOptions) { for (const plugin of this.plugins) { if (plugin.esbuildOptions) { - plugin.esbuildOptions(options, context) + plugin.esbuildOptions.call(this.getContext(), options) } } } @@ -88,17 +103,17 @@ export class PluginContainer { async buildStarted() { for (const plugin of this.plugins) { if (plugin.buildStart) { - await plugin.buildStart() + await plugin.buildStart.call(this.getContext()) } } } async buildFinished({ files, - context, + metafile, }: { files: OutputFile[] - context: PluginContext + metafile?: Metafile }) { await Promise.all( files.map(async (file) => { @@ -118,7 +133,7 @@ export class PluginContainer { for (const plugin of this.plugins) { if (info.type === 'chunk' && plugin.renderChunk) { const result = await plugin.renderChunk.call( - context, + this.getContext(), info.code, info ) @@ -162,7 +177,7 @@ export class PluginContainer { for (const plugin of this.plugins) { if (plugin.buildEnd) { - await plugin.buildEnd() + await plugin.buildEnd.call(this.getContext(), { metafile }) } } } diff --git a/src/plugins/size-reporter.ts b/src/plugins/size-reporter.ts new file mode 100644 index 00000000..c70d1028 --- /dev/null +++ b/src/plugins/size-reporter.ts @@ -0,0 +1,22 @@ +import { Plugin } from '../plugin' +import { reportSize } from '../lib/report-size' + +export const sizeReporter = (): Plugin => { + return { + name: 'size-reporter', + + buildEnd({ metafile }) { + if (!metafile) return + reportSize( + this.logger, + this.format, + Object.keys(metafile.outputs).reduce((res, name) => { + return { + ...res, + [name]: metafile!.outputs[name].bytes, + } + }, {}) + ) + }, + } +} diff --git a/src/rollup.ts b/src/rollup.ts index a005bbac..a8e50eec 100644 --- a/src/rollup.ts +++ b/src/rollup.ts @@ -10,6 +10,7 @@ import { TsResolveOptions, tsResolvePlugin } from './rollup/ts-resolve' import { createLogger, setSilent } from './log' import { getDeps } from './load' import path from 'path' +import { reportSize } from './lib/report-size' const logger = createLogger() @@ -177,8 +178,22 @@ async function runRollup(options: RollupConfig) { } logger.info('dts', 'Build start') const bundle = await rollup(options.inputConfig) - await bundle.write(options.outputConfig) + const result = await bundle.write(options.outputConfig) logger.success('dts', `⚡️ Build success in ${getDuration()}`) + reportSize( + logger, + 'dts', + result.output.reduce((res, info) => { + const name = path.relative( + process.cwd(), + path.join(options.outputConfig.dir || '.', info.fileName) + ) + return { + ...res, + [name]: info.type === 'chunk' ? info.code.length : info.source.length, + } + }, {}) + ) } catch (error) { logger.error('dts', 'Build error') parentPort?.postMessage('error')