From 81ffbc548c3d5f9db1f040c360167f95963674d6 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 5 May 2020 22:35:41 -0400 Subject: [PATCH] feat: tsx? support for build --- bin/vite.js | 2 ++ src/node/build.ts | 29 +++++++++++------- src/node/buildPluginEsbuild.ts | 52 +++++++++++++++++++++++++++++++++ src/node/esbuildService.ts | 42 ++++++++++---------------- src/node/serverPluginEsbuild.ts | 29 +++++------------- src/node/serverPluginVue.ts | 4 +-- 6 files changed, 96 insertions(+), 62 deletions(-) create mode 100644 src/node/buildPluginEsbuild.ts diff --git a/bin/vite.js b/bin/vite.js index 2ee26280656e07..19075af7195590 100755 --- a/bin/vite.js +++ b/bin/vite.js @@ -20,6 +20,8 @@ if (argv._[0] === 'build') { }) .catch((err) => { console.error(chalk.red(`[vite] Build errored out.`)) + // TODO pretty print this + // rollup errors contain helpful information console.log(err) }) } else { diff --git a/src/node/build.ts b/src/node/build.ts index 686090c7e93ab9..1d10d2abb3cfd0 100644 --- a/src/node/build.ts +++ b/src/node/build.ts @@ -16,7 +16,7 @@ import { createBuildResolvePlugin } from './buildPluginResolve' import { createBuildHtmlPlugin } from './buildPluginHtml' import { createBuildCssPlugin } from './buildPluginCss' import { createBuildAssetPlugin } from './buildPluginAsset' -import { createMinifyPlugin } from './esbuildService' +import { createEsbuildPlugin } from './buildPluginEsbuild' export interface BuildOptions { /** @@ -75,6 +75,13 @@ export interface BuildOptions { * https://github.com/vuejs/rollup-plugin-vue/blob/next/src/index.ts */ rollupPluginVueOptions?: Partial + /** + * Configure what to use for jsx factory and fragment + */ + jsx?: { + factory?: string + fragment?: string + } /** * Whether to emit index.html */ @@ -136,6 +143,7 @@ export async function build(options: BuildOptions = {}): Promise { rollupInputOptions = {}, rollupOutputOptions = {}, rollupPluginVueOptions = {}, + jsx = {}, emitIndex = true, emitAssets = true, write = true, @@ -158,14 +166,6 @@ export async function build(options: BuildOptions = {}): Promise { resolver ) - // terser is used by default for better compression, but the user can also - // opt-in to use esbuild which is orders of magnitude faster. - const minifyPlugin = minify - ? minify === 'esbuild' - ? await createMinifyPlugin() - : require('rollup-plugin-terser').terser() - : null - // lazy require rollup so that we don't load it when only using the dev server // importing it just for the types const rollup = require('rollup').rollup as typeof Rollup @@ -180,6 +180,8 @@ export async function build(options: BuildOptions = {}): Promise { createBuildResolvePlugin(root, cdn, [root, ...srcRoots], resolver), // vite:html ...(htmlPlugin ? [htmlPlugin] : []), + // vite:esbuild + await createEsbuildPlugin(minify === 'esbuild', jsx), // vue require('rollup-plugin-vue')({ transformAssetUrls: { @@ -213,8 +215,13 @@ export async function build(options: BuildOptions = {}): Promise { ), // vite:asset createBuildAssetPlugin(publicBasePath, assetsDir, assetsInlineLimit), - // minify - ...(minifyPlugin ? [minifyPlugin] : []) + // minify with terser + // this is the default which has better compression, but slow + // the user can opt-in to use esbuild which is much faster but results + // in ~8-10% larger file size. + ...(minify && minify !== 'esbuild' + ? [require('rollup-plugin-terser').terser()] + : []) ], onwarn(warning, warn) { if (warning.code !== 'CIRCULAR_DEPENDENCY') { diff --git a/src/node/buildPluginEsbuild.ts b/src/node/buildPluginEsbuild.ts new file mode 100644 index 00000000000000..c5b75c6721966f --- /dev/null +++ b/src/node/buildPluginEsbuild.ts @@ -0,0 +1,52 @@ +import { Plugin } from 'rollup' +import { startService, Service } from 'esbuild' +import { tjsxRE, transformWithService } from './esbuildService' + +export const createEsbuildPlugin = async ( + minify: boolean, + jsx: { + factory?: string + fragment?: string + } +): Promise => { + let service: Service | undefined + + const jsxConfig = { + jsxFactory: jsx.factory, + jsxFragment: jsx.fragment + } + + return { + name: 'vite:esbuild', + + async transform(code, file) { + if (tjsxRE.test(file)) { + return transformWithService( + service || (service = await startService()), + code, + file, + { ...jsxConfig } + ) + } + }, + + async renderChunk(code, chunk) { + if (minify) { + return transformWithService( + service || (service = await startService()), + code, + chunk.fileName, + { + minify: true + } + ) + } else { + return null + } + }, + + generateBundle() { + service && service.stop() + } + } +} diff --git a/src/node/esbuildService.ts b/src/node/esbuildService.ts index 696650958d82e9..12d5564cdc0ad3 100644 --- a/src/node/esbuildService.ts +++ b/src/node/esbuildService.ts @@ -1,5 +1,7 @@ +import path from 'path' import { startService, Service, TransformOptions } from 'esbuild' -import { Plugin } from 'rollup' + +export const tjsxRE = /\.(tsx?|jsx)$/ // Note: when the esbuild service is held in a module level variable, it // somehow prevents the build process from exiting even after explicitly @@ -20,23 +22,27 @@ const ensureService = async () => { // transform used in server plugins with a more friendly API export const transform = async ( code: string, - options: TransformOptions, - operation: string + file: string, + options: TransformOptions = {} ) => { - return _transform(await ensureService(), code, options, operation) + return transformWithService(await ensureService(), code, file, options) } // trasnform that takes the service via arguments, used in build plugins -const _transform = async ( +export const transformWithService = async ( service: Service, code: string, - options: TransformOptions, - operation: string + file: string, + options: TransformOptions = {} ) => { try { + if (!options.loader) { + options.loader = path.extname(file).slice(1) as any + } + options.sourcemap = true const result = await service.transform(code, options) if (result.warnings.length) { - console.error(`[vite] warnings while ${operation} with esbuild:`) + console.error(`[vite] warnings while transforming ${file} with esbuild:`) // TODO pretty print this result.warnings.forEach((w) => console.error(w)) } @@ -45,7 +51,7 @@ const _transform = async ( map: result.jsSourceMap || '' } } catch (e) { - console.error(`[vite] error while ${operation} with esbuild:`) + console.error(`[vite] error while transforming ${file} with esbuild:`) console.error(e) return { code: '', @@ -53,21 +59,3 @@ const _transform = async ( } } } - -export const createMinifyPlugin = async (): Promise => { - const service = await startService() - return { - name: 'vite:minify', - async renderChunk(code, chunk) { - return _transform( - service, - code, - { minify: true }, - `minifying ${chunk.fileName}` - ) - }, - generateBundle() { - service.stop() - } - } -} diff --git a/src/node/serverPluginEsbuild.ts b/src/node/serverPluginEsbuild.ts index bd1741caa38677..1517a0d9f2a20a 100644 --- a/src/node/serverPluginEsbuild.ts +++ b/src/node/serverPluginEsbuild.ts @@ -1,38 +1,25 @@ import { Plugin } from './server' import { readBody, isImportRequest, genSourceMapString } from './utils' -import { TransformOptions } from 'esbuild' -import { transform } from './esbuildService' - -const testRE = /\.(tsx?|jsx)$/ +import { tjsxRE, transform } from './esbuildService' export const esbuildPlugin: Plugin = ({ app, watcher, jsxConfig }) => { app.use(async (ctx, next) => { await next() - if (isImportRequest(ctx) && ctx.body && testRE.test(ctx.path)) { + if (isImportRequest(ctx) && ctx.body && tjsxRE.test(ctx.path)) { ctx.type = 'js' - let options: TransformOptions = {} - if (ctx.path.endsWith('.ts')) { - options = { loader: 'ts' } - } else if (ctx.path.endsWith('tsx')) { - options = { loader: 'tsx', ...jsxConfig } - } else if (ctx.path.endsWith('jsx')) { - options = { loader: 'jsx', ...jsxConfig } - } const src = await readBody(ctx.body) - const { code, map } = await transform( - src!, - options, - `transpiling ${ctx.path}` - ) - ctx.body = code + const { code, map } = await transform(src!, ctx.path, jsxConfig) + let res = code if (map) { - ctx.body += genSourceMapString(map) + res = res.replace(/\/\/# sourceMappingURL.*/, '') + res += genSourceMapString(map) } + ctx.body = res } }) watcher.on('change', (file) => { - if (testRE.test(file)) { + if (tjsxRE.test(file)) { watcher.handleJSReload(file) } }) diff --git a/src/node/serverPluginVue.ts b/src/node/serverPluginVue.ts index cc308b64131f4a..c014f5244b0406 100644 --- a/src/node/serverPluginVue.ts +++ b/src/node/serverPluginVue.ts @@ -153,9 +153,7 @@ async function compileSFCMain( if (descriptor.script) { let content = descriptor.script.content if (descriptor.script.lang === 'ts') { - content = ( - await transform(content, { loader: 'ts' }, `transpiling ${publicPath}`) - ).code + content = (await transform(content, publicPath, { loader: 'ts' })).code } code += content.replace(`export default`, 'const __script =')