diff --git a/docs/config/index.md b/docs/config/index.md index 41b0bfe04a85b7..2faf013789714e 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -480,6 +480,23 @@ export default async ({ command, mode }) => { Note the build will fail if the code contains features that cannot be safely transpiled by esbuild. See [esbuild docs](https://esbuild.github.io/content-types/#javascript) for more details. +### build.polyfillDynamicImport + +- **Type:** `boolean` +- **Default:** `false` + + Whether to automatically inject [dynamic import polyfill](https://github.com/GoogleChromeLabs/dynamic-import-polyfill). + + If set to true, 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' + ``` + + When using [`@vitejs/plugin-legacy`](https://github.com/vitejs/vite/tree/main/packages/plugin-legacy), the plugin sets this option to `true` automatically. + + 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 - **Type:** `string` diff --git a/docs/guide/backend-integration.md b/docs/guide/backend-integration.md index fad9fff75c61d6..b9f853d90d4cbf 100644 --- a/docs/guide/backend-integration.md +++ b/docs/guide/backend-integration.md @@ -20,6 +20,13 @@ Or you can follow these steps to configure it manually: } ``` + If you use [`@vitejs/plugin-legacy`](https://github.com/vitejs/vite/tree/main/packages/plugin-legacy) or manually enable the [`build.dynamicImportPolyfill` option](/config/#build-polyfilldynamicimport), remember to add the [dynamic import polyfill](/config/#build-polyfilldynamicimport) 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 diff --git a/packages/plugin-legacy/index.js b/packages/plugin-legacy/index.js index 60dee775aaa5d2..1b449c963dda4f 100644 --- a/packages/plugin-legacy/index.js +++ b/packages/plugin-legacy/index.js @@ -65,6 +65,21 @@ function viteLegacyPlugin(options = {}) { }) } + /** + * @type {import('vite').Plugin} + */ + const legacyConfigPlugin = { + name: 'legacy-config', + + apply: 'build', + config(config) { + if (!config.build) { + config.build = {} + } + config.build.polyfillDynamicImport = true + } + } + /** * @type {import('vite').Plugin} */ @@ -398,7 +413,12 @@ function viteLegacyPlugin(options = {}) { } } - return [legacyGenerateBundlePlugin, legacyPostPlugin, legacyEnvPlugin] + return [ + legacyConfigPlugin, + legacyGenerateBundlePlugin, + legacyPostPlugin, + legacyEnvPlugin + ] } /** diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 14125aae3bd56e..276f47f2fa0301 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -64,7 +64,7 @@ export interface BuildOptions { /** * whether to inject dynamic import polyfill. * Note: does not apply to library mode. - * @deprecated the dynamic import polyfill has been removed + * @default false */ polyfillDynamicImport?: boolean /** @@ -194,13 +194,12 @@ export interface LibraryOptions { export type LibraryFormats = 'es' | 'cjs' | 'umd' | 'iife' -export type ResolvedBuildOptions = Required< - Omit -> +export type ResolvedBuildOptions = Required> export function resolveBuildOptions(raw?: BuildOptions): ResolvedBuildOptions { const resolved: ResolvedBuildOptions = { target: 'modules', + polyfillDynamicImport: false, outDir: 'dist', assetsDir: 'assets', assetsInlineLimit: 4096, diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index c5813512f3910b..849b7372e50132 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -470,25 +470,6 @@ export async function resolveConfig( } }) - if (config.build?.polyfillDynamicImport) { - logDeprecationWarning( - 'build.polyfillDynamicImport', - '"polyfillDynamicImport" has been removed. Please use @vitejs/plugin-legacy if your target browsers do not support dynamic imports.' - ) - } - - Object.defineProperty(resolvedBuildOptions, 'polyfillDynamicImport', { - enumerable: false, - get() { - logDeprecationWarning( - 'build.polyfillDynamicImport', - '"polyfillDynamicImport" has been removed. Please use @vitejs/plugin-legacy if your target browsers do not support dynamic imports.', - new Error() - ) - return false - } - }) - if (config.alias) { logDeprecationWarning('alias', 'Use "resolve.alias" instead.') } diff --git a/packages/vite/src/node/plugins/dynamicImportPolyfill.ts b/packages/vite/src/node/plugins/dynamicImportPolyfill.ts index 8a0e126f4fec01..9c6fdfa7a85e2b 100644 --- a/packages/vite/src/node/plugins/dynamicImportPolyfill.ts +++ b/packages/vite/src/node/plugins/dynamicImportPolyfill.ts @@ -1,12 +1,27 @@ import { ResolvedConfig } from '..' import { Plugin } from '../plugin' +import { isModernFlag } from './importAnalysisBuild' +import path from 'path' export const polyfillId = 'vite/dynamic-import-polyfill' -/** - * @deprecated - */ +function resolveModulePath(config: ResolvedConfig) { + const { + base, + build: { assetsDir } + } = config + // #2918 path.posix.join returns a wrong path when config.base is a URL + if (/^(https?:)?(\/\/)/i.test(base)) { + return `${base.replace(/\/$/, '')}/${assetsDir}/` + } + return path.posix.join(base, assetsDir, '/') +} + export function dynamicImportPolyfillPlugin(config: ResolvedConfig): Plugin { + const enabled = config.build.polyfillDynamicImport + const skip = !enabled || config.command === 'serve' || config.build.ssr + let polyfillString: string | undefined + return { name: 'vite:dynamic-import-polyfill', resolveId(id) { @@ -16,11 +31,114 @@ export function dynamicImportPolyfillPlugin(config: ResolvedConfig): Plugin { }, load(id) { if (id === polyfillId) { - config.logger.warn( - `\n'vite/dynamic-import-polyfill' is no longer needed, refer to https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md#230-2021-05-10` + if (!enabled) { + config.logger.warnOnce( + `\n'vite/dynamic-import-polyfill' is no longer needed if you target modern browsers` + ) + } + if (skip) { + return '' + } + // return a placeholder here and defer the injection to renderChunk + // so that we can selectively skip the injection based on output format + if (!polyfillString) { + polyfillString = + `const p = ${polyfill.toString()};` + + `${isModernFlag}&&p(${JSON.stringify(resolveModulePath(config))});` + } + return polyfillString + } + }, + + renderDynamicImport({ format }) { + if (skip || format !== 'es') { + return null + } + if (!polyfillString) { + 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 '' } + // we do not actually return anything here because rewriting here would + // make it impossible to use es-module-lexer on the rendered chunks, which + // we need for import graph optimization in ./importAnalysisBuild. } } } + +/** +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 = {} + } +} diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 4fd86ff8169575..8e316b6709f0d4 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -20,6 +20,7 @@ import { getAssetFilename } from './asset' import { isCSSRequest, chunkToEmittedCssFileMap } from './css' +import { polyfillId } from './dynamicImportPolyfill' import { AttributeNode, NodeTransform, @@ -262,6 +263,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 } }, diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index 9099c663bced65..52faaa095ba120 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -204,6 +204,7 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { return } + const isPolyfillEnabled = config.build.polyfillDynamicImport for (const file in bundle) { const chunk = bundle[file] // can't use chunk.dynamicImports.length here since some modules e.g. @@ -220,7 +221,12 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { if (imports.length) { const s = new MagicString(code) for (let index = 0; index < imports.length; index++) { - const { s: start, e: end } = imports[index] + const { s: start, e: end, d: dynamicIndex } = imports[index] + // if dynamic import polyfill is used, rewrite the import to + // use the polyfilled function. + if (isPolyfillEnabled) { + s.overwrite(dynamicIndex, dynamicIndex + 6, `__import__`) + } // check the chunk being imported const url = code.slice(start, end) const deps: Set = new Set()