feat(build): default build target to 'modules' with dynamic import polyfill

This commit is contained in:
Evan You 2021-01-05 17:25:17 -05:00
parent 0fad96e5cb
commit 756e90ff9b
10 changed files with 221 additions and 22 deletions

View File

@ -276,12 +276,31 @@ export default ({ command, mode }) => {
### build.target
- **Type:** `string`
- **Default:** `es2020`
- **Default:** `'modules'`
- **Related:** [Browser Compatibility](/guide/build#browser-compatibility)
Browser compatibility target for the final bundle. The transform is performed with esbuild and the lowest supported target is `es2015`. The target can also be a browser with version, e.g. `chrome58` or `safari11`, or an array of multiple target strings.
Browser compatibility target for the final bundle. The default value is a Vite special value, `'modules'`, which targets [browsers with native ES module support](https://caniuse.com/es6-module).
Note the build will fail if the code contains features that cannot be safely transpiled by `esbuild`. See [esbuid docs](https://esbuild.github.io/api/#target) for more details.
Another special value is 'esnext' - which only performs minimal trasnpiling (for minification compat) and assumes native dynamic imports support.
The transform is performed with esbuild and the value should be a valid [esbuild target option](https://esbuild.github.io/api/#target). Custom targets can either be a ES version (e.g. `es2015`), a browser with version (e.g. `chrome58`), or an array of multiple target strings.
Note the build will fail if the code contains features that cannot be safely transpiled by esbuild. See [esbuid docs](https://esbuild.github.io/content-types/#javascript) for more details.
### build.dynamicImportPolyfill
- **Type:** `boolean`
- **Default:** `true` unless `build.target` is `'esnext'`
Whether to automatically inject [dynamic import polyfill](https://github.com/GoogleChromeLabs/dynamic-import-polyfill).
The polyfill is auto injected into the proxy module of each `index.html` entry. If the build is configured to use a non-html custom entry via `build.rollupOptions.input`, then it is necessary to manually import the polyfill in your custom entry:
```js
import 'vite/dynamic-import-polyfill'
```
Note: the polyfill does **not** apply to [Library Mode](/guide/build#library-mode). If you need to support browsers without native dynamic import, you should probably avoid using it in your library.
### build.outDir

View File

@ -17,6 +17,13 @@ If you want to serve the HTML using a traditional backend (e.g. Rails, Laravel)
}
```
Also remember to add the [dynamic import polyfill](/config/#build-dynamicimportpolyfill) to your entry, since it will no longer be auto-injected:
```js
// add the beginning of your app entry
import 'vite/dynamic-import-polyfill'
```
2. For development, inject the following in your server's HTML template (substitute `http://localhost:3000` with the local URL Vite is running at):
```html
@ -31,6 +38,6 @@ If you want to serve the HTML using a traditional backend (e.g. Rails, Laravel)
```html
<!-- if production -->
<link rel="stylesheet" href="/assets/{{ manifest['style.css'].file }}" />
<link rel="stylesheet" href="/assets/{{ manifest['index.css'].file }}" />
<script type="module" src="/assets/{{ manifest['index.js'].file }}"></script>
```

View File

@ -4,7 +4,18 @@ When it is time to deploy your app for production, simply run the `vite build` c
## Browser Compatibility
The production bundle assumes a baseline support for [Native ES modules dynamic imports](https://caniuse.com/es6-module-dynamic-import). By default, all code is minimally transpiled with target `es2020` (only for terser minification compatibility). You can specify the target range via the [`build.target` config option](/config/#build-target), where the lowest target available is `es2015`.
The production bundle assumes a baseline support for modern JavaScript. By default, all code is transpiled targeting [browsers with native ESM script tag support](https://caniuse.com/es6-module):
- Chrome >=61
- Firefox >=60
- Safari >=11
- Edge >=16
A lightweight [dynamic import polyfill](https://github.com/GoogleChromeLabs/dynamic-import-polyfill) is also automatically injected.
You can specify custom targets via the [`build.target` config option](/config/#build-target), where the lowest target is `es2015`.
Note that Vite only handles syntax transforms and **does not cover polyfills**. You can check out [Polyfill.io](https://polyfill.io/v3/) to build custom polyfill bundles.
Legacy browsers _can_ be supported via plugins that post-process the build output for compatibility (e.g. [`@rollup/plugin-babel`](https://github.com/rollup/plugins/tree/master/packages/babel) + [`@babel/preset-env`](https://babeljs.io/docs/en/babel-preset-env) + [SystemJS](https://github.com/systemjs/systemjs)). This is not a built-in feature, but there is plan to provide an official plugin that automatically emits a separate legacy bundle.

View File

@ -44,6 +44,6 @@ Ensuring optimal output and behavioral consistency between the dev server and th
## Browser Support
- Vite requires [native ES module support](https://caniuse.com/#feat=es6-module) during development.
- Vite requires [native ESM dynamic import support](https://caniuse.com/es6-module-dynamic-import) during development.
- The production build assumes a baseline support for [Native ES modules dynamic imports](https://caniuse.com/es6-module-dynamic-import). Legacy browsers can be supported via plugins that post-process the build output for compatibility. More details in the [Building for Production](./build) section.
- The production build assumes a baseline support for [Native ESM via script tags](https://caniuse.com/es6-module), similar to [`targets.esmodules` of `@babel/preset-env`](https://babeljs.io/docs/en/babel-preset-env#targetsesmodules). Legacy browsers can be supported via plugins that post-process the build output for compatibility. More details in the [Building for Production](./build) section.

View File

@ -35,10 +35,28 @@ export interface BuildOptions {
base?: string
/**
* Compatibility transform target. The transform is performed with esbuild
* and the lowest supported target is es2015/es6. Default: es2020
* https://esbuild.github.io/api/#target
* and the lowest supported target is es2015/es6. Note this only handles
* syntax transformation and does not cover polyfills (except for dynamic
* import)
*
* Default: 'modules' - Similar to `@babel/preset-env`'s targets.esmodules,
* transpile targeting browsers that natively support es module imports. Also
* injects a light-weight dynamic import polyfill.
* https://caniuse.com/es6-module
*
* Another special value is 'esnext' - which only performs minimal trasnpiling
* (for minification compat) and assumes native dynamic imports support.
*
* For custom targets, see https://esbuild.github.io/api/#target and
* https://esbuild.github.io/content-types/#javascript for more details.
*/
target?: TransformOptions['target'] | false
target?: 'modules' | TransformOptions['target'] | false
/**
* Whether to inject dynamic import polyfill. Defaults to `true`, unless
* `target` is `'esnext'`.
* Note: does not apply to library mode.
*/
polyfillDynamicImport?: boolean
/**
* Directory relative from `root` where build output will be placed. If the
* directory exists, it will be removed before the build.
@ -136,7 +154,8 @@ export function resolveBuildOptions(
): Required<BuildOptions> {
const resolved: Required<BuildOptions> = {
base: '/',
target: 'es2019',
target: 'modules',
polyfillDynamicImport: raw?.target !== 'esnext' && !raw?.lib,
outDir: 'dist',
assetsDir: 'assets',
assetsInlineLimit: 4096,
@ -153,6 +172,15 @@ export function resolveBuildOptions(
...raw
}
// handle special build targets
if (resolved.target === 'modules') {
// https://caniuse.com/es6-module
resolved.target = ['es2019', 'edge16', 'firefox60', 'chrome61', 'safari11']
} else if (resolved.target === 'esnext' && resolved.minify !== 'esbuild') {
// esnext + terser: limit to es2019 so it can be minified by terser
resolved.target = 'es2019'
}
// ensure base ending slash
resolved.base = resolved.base.replace(/([^/])$/, '$1/')

View File

@ -93,24 +93,21 @@ cli
// build
cli
.command('build [root]')
.option(
'--entry <file>',
`[string] entry file for build (default: index.html)`
)
.option('--base <path>', `[string] public base path (default: /)`)
.option('--outDir <dir>', `[string]  output directory (default: dist)`)
.option('--base <path>', `[string] public base path (default: /)`)
.option('--target <target>', `[string] transpile target (default: 'modules')`)
.option('--outDir <dir>', `[string] output directory (default: dist)`)
.option(
'--assetsDir <dir>',
`[string] directory under outDir to place assets in (default: _assets)`
`[string] directory under outDir to place assets in (default: _assets)`
)
.option(
'--assetsInlineLimit <number>',
`[number] static asset base64 inline threshold in bytes (default: 4096)`
`[number] static asset base64 inline threshold in bytes (default: 4096)`
)
.option('--ssr', `[boolean] build for server-side rendering`)
.option('--ssr', `[boolean] build for server-side rendering`)
.option(
'--sourcemap',
`[boolean] output source maps for build (default: false)`
`[boolean] output source maps for build (default: false)`
)
.option(
'--minify [minifier]',

View File

@ -0,0 +1,126 @@
import { ResolvedConfig } from '..'
import { Plugin } from '../plugin'
export const polyfillId = 'vite/dynamic-import-polyfill'
export function dynamicImportPolyfillPlugin(config: ResolvedConfig): Plugin {
const skip = config.command === 'serve' || config.build.ssr
let polyfillLoaded = false
return {
name: 'vite:dynamic-import-polyfill',
resolveId(id) {
if (id === polyfillId) {
return id
}
},
load(id) {
if (id === polyfillId) {
if (skip) {
return ''
}
polyfillLoaded = true
return (
polyfill.toString() +
`;polyfill(${JSON.stringify(config.build.base)});`
)
}
},
renderDynamicImport() {
if (skip) {
return null
}
if (!polyfillLoaded) {
throw new Error(
`Vite's dynamic import polyfill is enabled but was never imported. This ` +
`should only happen when using custom non-html rollup inputs. Make ` +
`sure to add \`import "${polyfillId}"\` as the first statement in ` +
`your custom entry.`
)
}
return {
left: '__import__(',
right: ')'
}
}
}
}
/**
The following polyfill function is meant to run in the browser and adapted from
https://github.com/GoogleChromeLabs/dynamic-import-polyfill
MIT License
Copyright (c) 2018 uupaa and 2019 Google LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
*/
declare const self: any
declare const location: any
declare const document: any
declare const URL: any
declare const Blob: any
function polyfill(modulePath = '.', importFunctionName = '__import__') {
try {
self[importFunctionName] = new Function('u', `return import(u)`)
} catch (error) {
const baseURL = new URL(modulePath, location)
const cleanup = (script: any) => {
URL.revokeObjectURL(script.src)
script.remove()
}
self[importFunctionName] = (url: string) =>
new Promise((resolve, reject) => {
const absURL = new URL(url, baseURL)
// If the module has already been imported, resolve immediately.
if (self[importFunctionName].moduleMap[absURL]) {
return resolve(self[importFunctionName].moduleMap[absURL])
}
const moduleBlob = new Blob(
[
`import * as m from '${absURL}';`,
`${importFunctionName}.moduleMap['${absURL}']=m;`
],
{ type: 'text/javascript' }
)
const script = Object.assign(document.createElement('script'), {
type: 'module',
src: URL.createObjectURL(moduleBlob),
onerror() {
reject(new Error(`Failed to import: ${url}`))
cleanup(script)
},
onload() {
resolve(self[importFunctionName].moduleMap[absURL])
cleanup(script)
}
})
document.head.appendChild(script)
})
self[importFunctionName].moduleMap = {}
}
}

View File

@ -16,6 +16,7 @@ import {
import MagicString from 'magic-string'
import { checkPublicFile, assetUrlRE, urlToBuiltUrl } from './asset'
import { isCSSRequest, chunkToEmittedCssFileMap } from './css'
import { polyfillId } from './dynamicImportPolyfill'
const htmlProxyRE = /\?html-proxy&index=(\d+)\.js$/
export const isHTMLProxy = (id: string) => htmlProxyRE.test(id)
@ -208,6 +209,12 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
}
processedHtml.set(id, s.toString())
// inject dynamic import polyfill
if (config.build.polyfillDynamicImport) {
js = `import "${polyfillId}";\n${js}`
}
return js
}
},

View File

@ -11,6 +11,7 @@ import { clientInjectionsPlugin } from './clientInjections'
import { htmlPlugin } from './html'
import { wasmPlugin } from './wasm'
import { webWorkerPlugin } from './worker'
import { dynamicImportPolyfillPlugin } from './dynamicImportPolyfill'
export async function resolvePlugins(
config: ResolvedConfig,
@ -26,6 +27,9 @@ export async function resolvePlugins(
return [
aliasPlugin({ entries: config.alias }),
config.build.polyfillDynamicImport
? dynamicImportPolyfillPlugin(config)
: null,
...prePlugins,
resolvePlugin({
root: config.root,

View File

@ -445,7 +445,7 @@ export async function createPluginContainer(
if (!plugin.load) continue
ctx._activePlugin = plugin
const result = await plugin.load.call(ctx as any, id)
if (result) {
if (result != null) {
return result
}
}