// @ts-check const path = require('path') const { createHash } = require('crypto') const { build } = require('vite') const MagicString = require('magic-string').default // lazy load babel since it's not used during dev let babel /** * @return {import('@babel/standalone')} */ const loadBabel = () => babel || (babel = require('@babel/standalone')) // https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc // DO NOT ALTER THIS CONTENT const safari10NoModuleFix = `!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",(function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()}),!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();` const legacyPolyfillId = 'vite-legacy-polyfill' const legacyEntryId = 'vite-legacy-entry' const systemJSInlineCode = `System.import(document.getElementById('${legacyEntryId}').getAttribute('data-src'))` const detectDynamicImportVarName = '__vite_is_dynamic_import_support' const detectDynamicImportCode = `try{import("_").catch(()=>1);}catch(e){}window.${detectDynamicImportVarName}=true;` const dynamicFallbackInlineCode = `!function(){if(window.${detectDynamicImportVarName})return;console.warn("vite: loading legacy build because dynamic import is unsupported, syntax error above should be ignored");var e=document.getElementById("${legacyPolyfillId}"),n=document.createElement("script");n.src=e.src,n.onload=function(){${systemJSInlineCode}},document.body.appendChild(n)}();` const forceDynamicImportUsage = `export function __vite_legacy_guard(){import('data:text/javascript,')};` const legacyEnvVarMarker = `__VITE_IS_LEGACY__` /** * @param {import('.').Options} options * @returns {import('vite').Plugin[]} */ function viteLegacyPlugin(options = {}) { /** * @type {import('vite').ResolvedConfig} */ let config const targets = options.targets || 'defaults' const genLegacy = options.renderLegacyChunks !== false const genDynamicFallback = genLegacy const debugFlags = (process.env.DEBUG || '').split(',') const isDebug = debugFlags.includes('vite:*') || debugFlags.includes('vite:legacy') const facadeToLegacyChunkMap = new Map() const facadeToLegacyPolyfillMap = new Map() const facadeToModernPolyfillMap = new Map() const modernPolyfills = new Set() // System JS relies on the Promise interface. It needs to be polyfilled for IE 11. (array.iterator is mandatory for supporting Promise.all) const DEFAULT_LEGACY_POLYFILL = [ 'core-js/modules/es.promise', 'core-js/modules/es.array.iterator' ] const legacyPolyfills = new Set(DEFAULT_LEGACY_POLYFILL) if (Array.isArray(options.modernPolyfills)) { options.modernPolyfills.forEach((i) => { modernPolyfills.add( i.includes('/') ? `core-js/${i}` : `core-js/modules/${i}.js` ) }) } if (Array.isArray(options.polyfills)) { options.polyfills.forEach((i) => { if (i.startsWith(`regenerator`)) { legacyPolyfills.add(`regenerator-runtime/runtime.js`) } else { legacyPolyfills.add( i.includes('/') ? `core-js/${i}` : `core-js/modules/${i}.js` ) } }) } if (Array.isArray(options.additionalLegacyPolyfills)) { options.additionalLegacyPolyfills.forEach((i) => { legacyPolyfills.add(i) }) } /** * @type {import('vite').Plugin} */ const legacyConfigPlugin = { name: 'vite:legacy-config', apply: 'build', config(config) { if (!config.build) { config.build = {} } if (!config.build.cssTarget) { // Hint for esbuild that we are targeting legacy browsers when minifying CSS. // Full CSS compat table available at https://github.com/evanw/esbuild/blob/78e04680228cf989bdd7d471e02bbc2c8d345dc9/internal/compat/css_table.go // But note that only the `HexRGBA` feature affects the minify outcome. // HSL & rebeccapurple values will be minified away regardless the target. // So targeting `chrome61` suffices to fix the compatiblity issue. config.build.cssTarget = 'chrome61' } } } /** * @type {import('vite').Plugin} */ const legacyGenerateBundlePlugin = { name: 'vite:legacy-generate-polyfill-chunk', apply: 'build', async generateBundle(opts, bundle) { if (config.build.ssr) { return } if (!isLegacyBundle(bundle, opts)) { if (!modernPolyfills.size) { return } isDebug && console.log( `[@vitejs/plugin-legacy] modern polyfills:`, modernPolyfills ) await buildPolyfillChunk( 'polyfills-modern', modernPolyfills, bundle, facadeToModernPolyfillMap, config.build, options.externalSystemJS ) return } if (!genLegacy) { return } // legacy bundle if (legacyPolyfills.size || genDynamicFallback) { if (!legacyPolyfills.has('es.promise')) { // check if the target needs Promise polyfill because SystemJS relies // on it detectPolyfills(`Promise.resolve()`, targets, legacyPolyfills) } isDebug && console.log( `[@vitejs/plugin-legacy] legacy polyfills:`, legacyPolyfills ) await buildPolyfillChunk( 'polyfills-legacy', legacyPolyfills, bundle, facadeToLegacyPolyfillMap, // force using terser for legacy polyfill minification, since esbuild // isn't legacy-safe config.build, options.externalSystemJS ) } } } /** * @type {import('vite').Plugin} */ const legacyPostPlugin = { name: 'vite:legacy-post-process', enforce: 'post', apply: 'build', configResolved(_config) { if (_config.build.lib) { throw new Error('@vitejs/plugin-legacy does not support library mode.') } config = _config if (!genLegacy || config.build.ssr) { return } /** * @param {string | ((chunkInfo: import('rollup').PreRenderedChunk) => string)} fileNames * @param {string?} defaultFileName * @returns {string | ((chunkInfo: import('rollup').PreRenderedChunk) => string)} */ const getLegacyOutputFileName = ( fileNames, defaultFileName = '[name]-legacy.[hash].js' ) => { if (!fileNames) { return path.posix.join(config.build.assetsDir, defaultFileName) } return (chunkInfo) => { let fileName = typeof fileNames === 'function' ? fileNames(chunkInfo) : fileNames if (fileName.includes('[name]')) { // [name]-[hash].[format] -> [name]-legacy-[hash].[format] fileName = fileName.replace('[name]', '[name]-legacy') } else { // entry.js -> entry-legacy.js fileName = fileName.replace(/(.+)\.(.+)/, '$1-legacy.$2') } return fileName } } /** * @param {import('rollup').OutputOptions} options * @returns {import('rollup').OutputOptions} */ const createLegacyOutput = (options = {}) => { return { ...options, format: 'system', entryFileNames: getLegacyOutputFileName(options.entryFileNames), chunkFileNames: getLegacyOutputFileName(options.chunkFileNames) } } const { rollupOptions } = config.build const { output } = rollupOptions if (Array.isArray(output)) { rollupOptions.output = [...output.map(createLegacyOutput), ...output] } else { rollupOptions.output = [createLegacyOutput(output), output || {}] } }, renderChunk(raw, chunk, opts) { if (config.build.ssr) { return } if (!isLegacyChunk(chunk, opts)) { if ( options.modernPolyfills && !Array.isArray(options.modernPolyfills) ) { // analyze and record modern polyfills detectPolyfills(raw, { esmodules: true }, modernPolyfills) } const ms = new MagicString(raw) if (genDynamicFallback && chunk.isEntry) { ms.prepend(forceDynamicImportUsage) } if (raw.includes(legacyEnvVarMarker)) { const re = new RegExp(legacyEnvVarMarker, 'g') let match while ((match = re.exec(raw))) { ms.overwrite( match.index, match.index + legacyEnvVarMarker.length, `false` ) } } if (config.build.sourcemap) { return { code: ms.toString(), map: ms.generateMap({ hires: true }) } } return ms.toString() } if (!genLegacy) { return } // @ts-ignore avoid esbuild transform on legacy chunks since it produces // legacy-unsafe code - e.g. rewriting object properties into shorthands opts.__vite_skip_esbuild__ = true // @ts-ignore force terser for legacy chunks. This only takes effect if // minification isn't disabled, because that leaves out the terser plugin // entirely. opts.__vite_force_terser__ = true // @ts-ignore // In the `generateBundle` hook, // we'll delete the assets from the legacy bundle to avoid emitting duplicate assets. // But that's still a waste of computing resource. // So we add this flag to avoid emitting the asset in the first place whenever possible. opts.__vite_skip_asset_emit__ = true // @ts-ignore avoid emitting assets for legacy bundle const needPolyfills = options.polyfills !== false && !Array.isArray(options.polyfills) // transform the legacy chunk with @babel/preset-env const sourceMaps = !!config.build.sourcemap const { code, map } = loadBabel().transform(raw, { babelrc: false, configFile: false, compact: true, sourceMaps, inputSourceMap: sourceMaps && chunk.map, presets: [ // forcing our plugin to run before preset-env by wrapping it in a // preset so we can catch the injected import statements... [ () => ({ plugins: [ recordAndRemovePolyfillBabelPlugin(legacyPolyfills), replaceLegacyEnvBabelPlugin(), wrapIIFEBabelPlugin() ] }) ], [ 'env', { targets, modules: false, bugfixes: true, loose: false, useBuiltIns: needPolyfills ? 'usage' : false, corejs: needPolyfills ? { version: require('core-js/package.json').version, proposals: false } : undefined, shippedProposals: true, ignoreBrowserslistConfig: options.ignoreBrowserslistConfig } ] ] }) return { code, map } }, transformIndexHtml(html, { chunk }) { if (config.build.ssr) return if (!chunk) return if (chunk.fileName.includes('-legacy')) { // The legacy bundle is built first, and its index.html isn't actually // emitted. Here we simply record its corresponding legacy chunk. facadeToLegacyChunkMap.set(chunk.facadeModuleId, chunk.fileName) return } /** * @type {import('vite').HtmlTagDescriptor[]} */ const tags = [] const htmlFilename = chunk.facadeModuleId.replace(/\?.*$/, '') // 1. inject modern polyfills const modernPolyfillFilename = facadeToModernPolyfillMap.get( chunk.facadeModuleId ) if (modernPolyfillFilename) { tags.push({ tag: 'script', attrs: { type: 'module', src: `${config.base}${modernPolyfillFilename}` } }) } else if (modernPolyfills.size) { throw new Error( `No corresponding modern polyfill chunk found for ${htmlFilename}` ) } if (!genLegacy) { return { html, tags } } // 2. inject Safari 10 nomodule fix tags.push({ tag: 'script', attrs: { nomodule: true }, children: safari10NoModuleFix, injectTo: 'body' }) // 3. inject legacy polyfills const legacyPolyfillFilename = facadeToLegacyPolyfillMap.get( chunk.facadeModuleId ) if (legacyPolyfillFilename) { tags.push({ tag: 'script', attrs: { nomodule: true, id: legacyPolyfillId, src: `${config.base}${legacyPolyfillFilename}` }, injectTo: 'body' }) } else if (legacyPolyfills.size) { throw new Error( `No corresponding legacy polyfill chunk found for ${htmlFilename}` ) } // 4. inject legacy entry const legacyEntryFilename = facadeToLegacyChunkMap.get( chunk.facadeModuleId ) if (legacyEntryFilename) { tags.push({ tag: 'script', attrs: { nomodule: true, // we set the entry path on the element as an attribute so that the // script content will stay consistent - which allows using a constant // hash value for CSP. id: legacyEntryId, 'data-src': config.base + legacyEntryFilename }, children: systemJSInlineCode, injectTo: 'body' }) } else { throw new Error( `No corresponding legacy entry chunk found for ${htmlFilename}` ) } // 5. inject dynamic import fallback entry if (genDynamicFallback && legacyPolyfillFilename && legacyEntryFilename) { tags.push({ tag: 'script', attrs: { type: 'module' }, children: detectDynamicImportCode, injectTo: 'head' }) tags.push({ tag: 'script', attrs: { type: 'module' }, children: dynamicFallbackInlineCode, injectTo: 'head' }) } return { html, tags } }, generateBundle(opts, bundle) { if (config.build.ssr) { return } if (isLegacyBundle(bundle, opts)) { // avoid emitting duplicate assets for (const name in bundle) { if (bundle[name].type === 'asset') { delete bundle[name] } } } } } let envInjectionFailed = false /** * @type {import('vite').Plugin} */ const legacyEnvPlugin = { name: 'vite:legacy-env', config(config, env) { if (env) { return { define: { 'import.meta.env.LEGACY': env.command === 'serve' || config.build.ssr ? false : legacyEnvVarMarker } } } else { envInjectionFailed = true } }, configResolved(config) { if (envInjectionFailed) { config.logger.warn( `[@vitejs/plugin-legacy] import.meta.env.LEGACY was not injected due ` + `to incompatible vite version (requires vite@^2.0.0-beta.69).` ) } } } return [ legacyConfigPlugin, legacyGenerateBundlePlugin, legacyPostPlugin, legacyEnvPlugin ] } /** * @param {string} code * @param {any} targets * @param {Set} list */ function detectPolyfills(code, targets, list) { const { ast } = loadBabel().transform(code, { ast: true, babelrc: false, configFile: false, presets: [ [ 'env', { targets, modules: false, useBuiltIns: 'usage', corejs: { version: 3, proposals: false }, shippedProposals: true, ignoreBrowserslistConfig: true } ] ] }) for (const node of ast.program.body) { if (node.type === 'ImportDeclaration') { const source = node.source.value if ( source.startsWith('core-js/') || source.startsWith('regenerator-runtime/') ) { list.add(source) } } } } /** * @param {string} name * @param {Set} imports * @param {import('rollup').OutputBundle} bundle * @param {Map} facadeToChunkMap * @param {import('vite').BuildOptions} buildOptions */ async function buildPolyfillChunk( name, imports, bundle, facadeToChunkMap, buildOptions, externalSystemJS ) { let { minify, assetsDir } = buildOptions minify = minify ? 'terser' : false const res = await build({ // so that everything is resolved from here root: __dirname, configFile: false, logLevel: 'error', plugins: [polyfillsPlugin(imports, externalSystemJS)], build: { write: false, target: false, minify, assetsDir, rollupOptions: { input: { [name]: polyfillId }, output: { format: name.includes('legacy') ? 'iife' : 'es', manualChunks: undefined } } } }) const _polyfillChunk = Array.isArray(res) ? res[0] : res if (!('output' in _polyfillChunk)) return const polyfillChunk = _polyfillChunk.output[0] // associate the polyfill chunk to every entry chunk so that we can retrieve // the polyfill filename in index html transform for (const key in bundle) { const chunk = bundle[key] if (chunk.type === 'chunk' && chunk.facadeModuleId) { facadeToChunkMap.set(chunk.facadeModuleId, polyfillChunk.fileName) } } // add the chunk to the bundle bundle[polyfillChunk.name] = polyfillChunk } const polyfillId = '\0vite/legacy-polyfills' /** * @param {Set} imports * @return {import('rollup').Plugin} */ function polyfillsPlugin(imports, externalSystemJS) { return { name: 'vite:legacy-polyfills', resolveId(id) { if (id === polyfillId) { return id } }, load(id) { if (id === polyfillId) { return ( [...imports].map((i) => `import "${i}";`).join('') + (externalSystemJS ? '' : `import "systemjs/dist/s.min.js";`) ) } } } } /** * @param {import('rollup').RenderedChunk} chunk * @param {import('rollup').NormalizedOutputOptions} options */ function isLegacyChunk(chunk, options) { return options.format === 'system' && chunk.fileName.includes('-legacy') } /** * @param {import('rollup').OutputBundle} bundle * @param {import('rollup').NormalizedOutputOptions} options */ function isLegacyBundle(bundle, options) { if (options.format === 'system') { const entryChunk = Object.values(bundle).find( (output) => output.type === 'chunk' && output.isEntry ) return !!entryChunk && entryChunk.fileName.includes('-legacy') } return false } /** * @param {Set} polyfills */ function recordAndRemovePolyfillBabelPlugin(polyfills) { return ({ types: t }) => ({ name: 'vite-remove-polyfill-import', post({ path }) { path.get('body').forEach((p) => { if (t.isImportDeclaration(p)) { polyfills.add(p.node.source.value) p.remove() } }) } }) } function replaceLegacyEnvBabelPlugin() { return ({ types: t }) => ({ name: 'vite-replace-env-legacy', visitor: { Identifier(path) { if (path.node.name === legacyEnvVarMarker) { path.replaceWith(t.booleanLiteral(true)) } } } }) } function wrapIIFEBabelPlugin() { return ({ types: t, template }) => { const buildIIFE = template(';(function(){%%body%%})();') return { name: 'vite-wrap-iife', post({ path }) { if (!this.isWrapped) { this.isWrapped = true path.replaceWith(t.program(buildIIFE({ body: path.node.body }))) } } } } } module.exports = viteLegacyPlugin viteLegacyPlugin.default = viteLegacyPlugin viteLegacyPlugin.cspHashes = [ createHash('sha256').update(safari10NoModuleFix).digest('base64'), createHash('sha256').update(systemJSInlineCode).digest('base64'), createHash('sha256').update(detectDynamicImportCode).digest('base64'), createHash('sha256').update(dynamicFallbackInlineCode).digest('base64') ]