Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: respect esbuild minify config #8754

Merged
merged 10 commits into from Jun 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/config/build-options.md
Expand Up @@ -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'`.

Expand Down
2 changes: 2 additions & 0 deletions docs/config/shared-options.md
Expand Up @@ -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
Expand Down
184 changes: 184 additions & 0 deletions 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
}
Expand Up @@ -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\\"]),
Expand Down Expand Up @@ -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\\"]),
Expand Down
115 changes: 96 additions & 19 deletions packages/vite/src/node/plugins/esbuild.ts
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<TransformResult, 'map'> & {
Expand Down Expand Up @@ -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) {
Expand All @@ -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))
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down