Studying all day long)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

713 lines
20 KiB

2 years ago
// @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<string>} 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<string>} imports
* @param {import('rollup').OutputBundle} bundle
* @param {Map<string, string>} 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<string>} 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<string>} 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')
]