From cb75dbd4a2b1c56e2a7aeaf1625818a4d1865f00 Mon Sep 17 00:00:00 2001 From: patak Date: Thu, 29 Jul 2021 07:17:13 +0200 Subject: [PATCH] feat: modulepreload polyfill (#4058) --- docs/config/index.md | 15 +++ docs/guide/backend-integration.md | 7 ++ packages/vite/src/node/build.ts | 7 ++ packages/vite/src/node/plugins/html.ts | 6 ++ .../src/node/plugins/importAnalysisBuild.ts | 28 ++--- packages/vite/src/node/plugins/index.ts | 4 + .../src/node/plugins/modulePreloadPolyfill.ts | 100 ++++++++++++++++++ 7 files changed, 155 insertions(+), 12 deletions(-) create mode 100644 packages/vite/src/node/plugins/modulePreloadPolyfill.ts diff --git a/docs/config/index.md b/docs/config/index.md index f19887a758ae75..edf90d4986661c 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -543,6 +543,21 @@ createServer() 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.polyfillModulePreload + +- **Type:** `boolean` +- **Default:** `true` + + Whether to automatically inject [module preload polyfill](https://guybedford.com/es-module-preloading-integrity#modulepreload-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/modulepreload-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 - **Type:** `string` diff --git a/docs/guide/backend-integration.md b/docs/guide/backend-integration.md index 0e03b9a7c2f0e4..fa35b51f257400 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 haven't disabled the [module preload polyfill](/config/#polyfillmodulepreload), you also need to import the polyfill in your entry + + ```js + // add the beginning of your app entry + import 'vite/modulepreload-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/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 6d227a8ff0bc33..5d086f98269169 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -64,6 +64,12 @@ export interface BuildOptions { * https://esbuild.github.io/content-types/#javascript for more details. */ target?: 'modules' | TransformOptions['target'] | false + /** + * whether to inject module preload polyfill. + * Note: does not apply to library mode. + * @default true + */ + polyfillModulePreload?: boolean /** * whether to inject dynamic import polyfill. * Note: does not apply to library mode. @@ -211,6 +217,7 @@ export type ResolvedBuildOptions = Required< export function resolveBuildOptions(raw?: BuildOptions): ResolvedBuildOptions { const resolved: ResolvedBuildOptions = { target: 'modules', + polyfillModulePreload: true, outDir: 'dist', assetsDir: 'assets', assetsInlineLimit: 4096, diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 703dcd9c010f85..c10026ad276999 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 { modulePreloadPolyfillId } from './modulePreloadPolyfill' import { AttributeNode, NodeTransform, @@ -263,6 +264,11 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { processedHtml.set(id, s.toString()) + // inject module preload polyfill + if (config.build.polyfillModulePreload) { + js = `import "${modulePreloadPolyfillId}";\n${js}` + } + return js } }, diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index 4e9a12f07148c3..bf619c46712b70 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -18,30 +18,29 @@ export const preloadMarker = `__VITE_PRELOAD__` export const preloadBaseMarker = `__VITE_PRELOAD_BASE__` const preloadHelperId = 'vite/preload-helper' -const preloadCode = `let scriptRel;const seen = {};const base = '${preloadBaseMarker}';export const ${preloadMethod} = ${preload.toString()}` const preloadMarkerRE = new RegExp(`"${preloadMarker}"`, 'g') /** * Helper for preloading CSS and direct imports of async chunks in parallel to * the async chunk itself. */ + +function detectScriptRel() { + // @ts-ignore + const relList = document.createElement('link').relList + // @ts-ignore + return relList && relList.supports && relList.supports('modulepreload') + ? 'modulepreload' + : 'preload' +} + +declare const scriptRel: string function preload(baseModule: () => Promise<{}>, deps?: string[]) { // @ts-ignore if (!__VITE_IS_MODERN__ || !deps || deps.length === 0) { return baseModule() } - // @ts-ignore - if (scriptRel === undefined) { - // @ts-ignore - const relList = document.createElement('link').relList - // @ts-ignore - scriptRel = - relList && relList.supports && relList.supports('modulepreload') - ? 'modulepreload' - : 'preload' - } - return Promise.all( deps.map((dep) => { // @ts-ignore @@ -84,6 +83,11 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { const ssr = !!config.build.ssr const insertPreload = !(ssr || !!config.build.lib) + const scriptRel = config.build.polyfillModulePreload + ? `'modulepreload'` + : `(${detectScriptRel.toString()})()` + const preloadCode = `const scriptRel = ${scriptRel};const seen = {};const base = '${preloadBaseMarker}';export const ${preloadMethod} = ${preload.toString()}` + return { name: 'vite:import-analysis', diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index 39f672256c2699..a83ddae2b2bf2a 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -10,6 +10,7 @@ import { assetPlugin } from './asset' import { clientInjectionsPlugin } from './clientInjections' import { htmlInlineScriptProxyPlugin } from './html' import { wasmPlugin } from './wasm' +import { modulePreloadPolyfillPlugin } from './modulePreloadPolyfill' import { webWorkerPlugin } from './worker' import { preAliasPlugin } from './preAlias' import { definePlugin } from './define' @@ -30,6 +31,9 @@ export async function resolvePlugins( isBuild ? null : preAliasPlugin(), aliasPlugin({ entries: config.resolve.alias }), ...prePlugins, + config.build.polyfillModulePreload + ? modulePreloadPolyfillPlugin(config) + : null, resolvePlugin({ ...config.resolve, root: config.root, diff --git a/packages/vite/src/node/plugins/modulePreloadPolyfill.ts b/packages/vite/src/node/plugins/modulePreloadPolyfill.ts new file mode 100644 index 00000000000000..f5bfe9a36a39a9 --- /dev/null +++ b/packages/vite/src/node/plugins/modulePreloadPolyfill.ts @@ -0,0 +1,100 @@ +import { ResolvedConfig } from '..' +import { Plugin } from '../plugin' +import { isModernFlag } from './importAnalysisBuild' + +export const modulePreloadPolyfillId = 'vite/modulepreload-polyfill' + +export function modulePreloadPolyfillPlugin(config: ResolvedConfig): Plugin { + const skip = config.build.ssr + let polyfillString: string | undefined + + return { + name: 'vite:modulepreload-polyfill', + resolveId(id) { + if (id === modulePreloadPolyfillId) { + return id + } + }, + load(id) { + if (id === modulePreloadPolyfillId) { + if (skip) { + return '' + } + if (!polyfillString) { + polyfillString = + `const p = ${polyfill.toString()};` + `${isModernFlag}&&p();` + } + return polyfillString + } + } + } +} + +/** +The following polyfill function is meant to run in the browser and adapted from +https://github.com/guybedford/es-module-shims +MIT License +Copyright (C) 2018-2021 Guy Bedford +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 document: any +declare const MutationObserver: any +declare const fetch: any + +function polyfill() { + const relList = document.createElement('link').relList + if (relList && relList.supports && relList.supports('modulepreload')) { + return + } + + for (const link of document.querySelectorAll('link[rel="modulepreload"]')) { + processPreload(link) + } + + new MutationObserver((mutations: any) => { + for (const mutation of mutations) { + if (mutation.type !== 'childList') { + continue + } + for (const node of mutation.addedNodes) { + if (node.tagName === 'LINK' && node.rel === 'modulepreload') + processPreload(node) + } + } + }).observe(document, { childList: true, subtree: true }) + + function getFetchOpts(script: any) { + const fetchOpts = {} as any + if (script.integrity) fetchOpts.integrity = script.integrity + if (script.referrerpolicy) fetchOpts.referrerPolicy = script.referrerpolicy + if (script.crossorigin === 'use-credentials') + fetchOpts.credentials = 'include' + else if (script.crossorigin === 'anonymous') fetchOpts.credentials = 'omit' + else fetchOpts.credentials = 'same-origin' + return fetchOpts + } + + function processPreload(link: any) { + if (link.ep) + // ep marker = processed + return + link.ep = true + // prepopulate the load record + const fetchOpts = getFetchOpts(link) + fetch(link.href, fetchOpts) + } +}