From 8b77695870d16e030392fe2ef9826140e61c6e82 Mon Sep 17 00:00:00 2001 From: Bjorn Lu Date: Sat, 25 Jun 2022 19:31:40 +0800 Subject: [PATCH] feat: respect esbuild minify config (#8754) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 翠 / green --- docs/config/build-options.md | 2 +- docs/config/shared-options.md | 2 + .../node/__tests__/plugins/esbuild.spec.ts | 184 ++++++++++++++++++ .../__snapshots__/fixture.test.ts.snap | 2 + packages/vite/src/node/plugins/esbuild.ts | 115 +++++++++-- 5 files changed, 285 insertions(+), 20 deletions(-) create mode 100644 packages/vite/src/node/__tests__/plugins/esbuild.spec.ts diff --git a/docs/config/build-options.md b/docs/config/build-options.md index afe60c86052b36..e4882e111c51ee 100644 --- a/docs/config/build-options.md +++ b/docs/config/build-options.md @@ -145,7 +145,7 @@ Produce SSR-oriented build. The value can be a string to directly specify the SS Set to `false` to disable minification, or specify the minifier to use. The default is [esbuild](https://github.com/evanw/esbuild) which is 20 ~ 40x faster than terser and only 1 ~ 2% worse compression. [Benchmarks](https://github.com/privatenumber/minification-benchmarks) -Note the `build.minify` option is not available when using the `'es'` format in lib mode. +Note the `build.minify` option does not minify whitespaces when using the `'es'` format in lib mode, as it removes pure annotations and break tree-shaking. Terser must be installed when it is set to `'terser'`. diff --git a/docs/config/shared-options.md b/docs/config/shared-options.md index 2ae42cb88500c4..774c35df42a5a5 100644 --- a/docs/config/shared-options.md +++ b/docs/config/shared-options.md @@ -280,6 +280,8 @@ export default defineConfig({ }) ``` +When [`build.minify`](./build-options.md#build-minify) is `true`, you can configure to only minify [certain aspects](https://esbuild.github.io/api/#minify) of the code by setting either of `esbuild.minifyIdentifiers`, `esbuild.minifySyntax`, and `esbuild.minifyWhitespace` to `true`. Note the `esbuild.minify` option can't be used to override `build.minify`. + Set to `false` to disable esbuild transforms. ## assetsInclude diff --git a/packages/vite/src/node/__tests__/plugins/esbuild.spec.ts b/packages/vite/src/node/__tests__/plugins/esbuild.spec.ts new file mode 100644 index 00000000000000..50e9183b46e64c --- /dev/null +++ b/packages/vite/src/node/__tests__/plugins/esbuild.spec.ts @@ -0,0 +1,184 @@ +import { describe, expect, test } from 'vitest' +import type { ResolvedConfig, UserConfig } from '../../config' +import { resolveEsbuildTranspileOptions } from '../../plugins/esbuild' + +describe('resolveEsbuildTranspileOptions', () => { + test('resolve default', () => { + const options = resolveEsbuildTranspileOptions( + defineResolvedConfig({ + build: { + target: 'es2020', + minify: 'esbuild' + }, + esbuild: { + keepNames: true + } + }), + 'es' + ) + expect(options).toEqual({ + target: 'es2020', + format: 'esm', + keepNames: true, + minify: true, + treeShaking: true + }) + }) + + test('resolve esnext no minify', () => { + const options = resolveEsbuildTranspileOptions( + defineResolvedConfig({ + build: { + target: 'esnext', + minify: false + }, + esbuild: { + keepNames: true + } + }), + 'es' + ) + expect(options).toEqual(null) + }) + + test('resolve no minify', () => { + const options = resolveEsbuildTranspileOptions( + defineResolvedConfig({ + build: { + target: 'es2020', + minify: false + }, + esbuild: { + keepNames: true + } + }), + 'es' + ) + expect(options).toEqual({ + target: 'es2020', + format: 'esm', + keepNames: true, + minify: false, + minifyIdentifiers: false, + minifySyntax: false, + minifyWhitespace: false, + treeShaking: false + }) + }) + + test('resolve es lib', () => { + const options = resolveEsbuildTranspileOptions( + defineResolvedConfig({ + build: { + minify: 'esbuild', + lib: { + entry: './somewhere.js' + } + }, + esbuild: { + keepNames: true + } + }), + 'es' + ) + expect(options).toEqual({ + target: undefined, + format: 'esm', + keepNames: true, + minify: false, + minifyIdentifiers: true, + minifySyntax: true, + minifyWhitespace: false, + treeShaking: true + }) + }) + + test('resolve cjs lib', () => { + const options = resolveEsbuildTranspileOptions( + defineResolvedConfig({ + build: { + minify: 'esbuild', + lib: { + entry: './somewhere.js' + } + }, + esbuild: { + keepNames: true + } + }), + 'cjs' + ) + expect(options).toEqual({ + target: undefined, + format: 'cjs', + keepNames: true, + minify: true, + treeShaking: true + }) + }) + + test('resolve es lib with specific minify options', () => { + const options = resolveEsbuildTranspileOptions( + defineResolvedConfig({ + build: { + minify: 'esbuild', + lib: { + entry: './somewhere.js' + } + }, + esbuild: { + keepNames: true, + minifyIdentifiers: true, + minifyWhitespace: true + } + }), + 'es' + ) + expect(options).toEqual({ + target: undefined, + format: 'esm', + keepNames: true, + minify: false, + minifyIdentifiers: true, + minifyWhitespace: false, + treeShaking: true + }) + }) + + test('resolve cjs lib with specific minify options', () => { + const options = resolveEsbuildTranspileOptions( + defineResolvedConfig({ + build: { + minify: 'esbuild', + lib: { + entry: './somewhere.js' + } + }, + esbuild: { + keepNames: true, + minifyIdentifiers: true, + minifyWhitespace: true, + treeShaking: true + } + }), + 'cjs' + ) + expect(options).toEqual({ + target: undefined, + format: 'cjs', + keepNames: true, + minify: false, + minifyIdentifiers: true, + minifyWhitespace: true, + treeShaking: true + }) + }) +}) + +/** + * Helper for `resolveEsbuildTranspileOptions` to created resolved config with types. + * Note: The function only uses `build.target`, `build.minify` and `esbuild` options. + */ +function defineResolvedConfig(config: UserConfig): ResolvedConfig { + return config as any +} diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/__snapshots__/fixture.test.ts.snap b/packages/vite/src/node/__tests__/plugins/importGlob/__snapshots__/fixture.test.ts.snap index 61ff6eeb4d2fc7..4dc2f50a8f881d 100644 --- a/packages/vite/src/node/__tests__/plugins/importGlob/__snapshots__/fixture.test.ts.snap +++ b/packages/vite/src/node/__tests__/plugins/importGlob/__snapshots__/fixture.test.ts.snap @@ -57,6 +57,7 @@ export const parent = Object.assign({ export const rootMixedRelative = Object.assign({ \\"/css.spec.ts\\": () => import(\\"../../css.spec.ts?url\\").then(m => m[\\"default\\"]), \\"/define.spec.ts\\": () => import(\\"../../define.spec.ts?url\\").then(m => m[\\"default\\"]), +\\"/esbuild.spec.ts\\": () => import(\\"../../esbuild.spec.ts?url\\").then(m => m[\\"default\\"]), \\"/import.spec.ts\\": () => import(\\"../../import.spec.ts?url\\").then(m => m[\\"default\\"]), \\"/importGlob/fixture-b/a.ts\\": () => import(\\"../fixture-b/a.ts?url\\").then(m => m[\\"default\\"]), \\"/importGlob/fixture-b/b.ts\\": () => import(\\"../fixture-b/b.ts?url\\").then(m => m[\\"default\\"]), @@ -131,6 +132,7 @@ export const parent = Object.assign({ export const rootMixedRelative = Object.assign({ \\"/css.spec.ts\\": () => import(\\"../../css.spec.ts?url&lang.ts\\").then(m => m[\\"default\\"]), \\"/define.spec.ts\\": () => import(\\"../../define.spec.ts?url&lang.ts\\").then(m => m[\\"default\\"]), +\\"/esbuild.spec.ts\\": () => import(\\"../../esbuild.spec.ts?url&lang.ts\\").then(m => m[\\"default\\"]), \\"/import.spec.ts\\": () => import(\\"../../import.spec.ts?url&lang.ts\\").then(m => m[\\"default\\"]), \\"/importGlob/fixture-b/a.ts\\": () => import(\\"../fixture-b/a.ts?url&lang.ts\\").then(m => m[\\"default\\"]), \\"/importGlob/fixture-b/b.ts\\": () => import(\\"../fixture-b/b.ts?url&lang.ts\\").then(m => m[\\"default\\"]), diff --git a/packages/vite/src/node/plugins/esbuild.ts b/packages/vite/src/node/plugins/esbuild.ts index 10484369f0d710..ee1c36db651849 100644 --- a/packages/vite/src/node/plugins/esbuild.ts +++ b/packages/vite/src/node/plugins/esbuild.ts @@ -8,7 +8,7 @@ import type { } from 'esbuild' import { transform } from 'esbuild' import type { RawSourceMap } from '@ampproject/remapping' -import type { SourceMap } from 'rollup' +import type { InternalModuleFormat, SourceMap } from 'rollup' import type { TSConfckParseOptions, TSConfckParseResult } from 'tsconfck' import { TSConfckParseError, findAll, parse } from 'tsconfck' import { @@ -37,6 +37,10 @@ export interface ESBuildOptions extends TransformOptions { include?: string | RegExp | string[] | RegExp[] exclude?: string | RegExp | string[] | RegExp[] jsxInject?: string + /** + * This option is not respected. Use `build.minify` instead. + */ + minify?: never } export type ESBuildTransformResult = Omit & { @@ -170,6 +174,17 @@ export function esbuildPlugin(options: ESBuildOptions = {}): Plugin { options.exclude || /\.js$/ ) + // Remove optimization options for dev as we only need to transpile them, + // and for build as the final optimization is in `buildEsbuildPlugin` + const transformOptions: TransformOptions = { + ...options, + minify: false, + minifyIdentifiers: false, + minifySyntax: false, + minifyWhitespace: false, + treeShaking: false + } + return { name: 'vite:esbuild', configureServer(_server) { @@ -188,7 +203,7 @@ export function esbuildPlugin(options: ESBuildOptions = {}): Plugin { }, async transform(code, id) { if (filter(id) || filter(cleanUrl(id))) { - const result = await transformWithEsbuild(code, id, options) + const result = await transformWithEsbuild(code, id, transformOptions) if (result.warnings.length) { result.warnings.forEach((m) => { this.warn(prettifyMessage(m, code)) @@ -236,27 +251,13 @@ export const buildEsbuildPlugin = (config: ResolvedConfig): Plugin => { return null } - const target = config.build.target - const minify = config.build.minify === 'esbuild' + const options = resolveEsbuildTranspileOptions(config, opts.format) - if ((!target || target === 'esnext') && !minify) { + if (!options) { return null } - const res = await transformWithEsbuild(code, chunk.fileName, { - ...config.esbuild, - target: target || undefined, - ...(minify - ? { - // Do not minify ES lib output since that would remove pure annotations - // and break tree-shaking - // https://github.com/vuejs/core/issues/2860#issuecomment-926882793 - minify: !(config.build.lib && opts.format === 'es'), - treeShaking: true, - format: rollupToEsbuildFormatMap[opts.format] - } - : undefined) - }) + const res = await transformWithEsbuild(code, chunk.fileName, options) if (config.build.lib) { // #7188, esbuild adds helpers out of the UMD and IIFE wrappers, and the @@ -282,6 +283,82 @@ export const buildEsbuildPlugin = (config: ResolvedConfig): Plugin => { } } +export function resolveEsbuildTranspileOptions( + config: ResolvedConfig, + format: InternalModuleFormat +): TransformOptions | null { + const target = config.build.target + const minify = config.build.minify === 'esbuild' + + if ((!target || target === 'esnext') && !minify) { + return null + } + + // Do not minify whitespace for ES lib output since that would remove + // pure annotations and break tree-shaking + // https://github.com/vuejs/core/issues/2860#issuecomment-926882793 + const isEsLibBuild = config.build.lib && format === 'es' + const options: TransformOptions = { + ...config.esbuild, + target: target || undefined, + format: rollupToEsbuildFormatMap[format] + } + + // If no minify, disable all minify options + if (!minify) { + return { + ...options, + minify: false, + minifyIdentifiers: false, + minifySyntax: false, + minifyWhitespace: false, + treeShaking: false + } + } + + // If user enable fine-grain minify options, minify with their options instead + if ( + options.minifyIdentifiers || + options.minifySyntax || + options.minifyWhitespace + ) { + if (isEsLibBuild) { + // Disable minify whitespace as it breaks tree-shaking + return { + ...options, + minify: false, + minifyWhitespace: false, + treeShaking: true + } + } else { + return { + ...options, + minify: false, + treeShaking: true + } + } + } + + // Else apply default minify options + if (isEsLibBuild) { + // Minify all except whitespace as it breaks tree-shaking + return { + ...options, + minify: false, + minifyIdentifiers: true, + minifySyntax: true, + minifyWhitespace: false, + treeShaking: true + } + } else { + return { + ...options, + minify: true, + treeShaking: true + } + } +} + function prettifyMessage(m: Message, code: string): string { let res = colors.yellow(m.text) if (m.location) {