├── .gitignore ├── .npmrc ├── .release-it.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.config.ts ├── eslint.config.js ├── package.json ├── playground ├── index.html ├── main.js ├── package.json └── vite.config.ts ├── pnpm-lock.yaml ├── src ├── index.ts ├── snippets.ts └── types.ts ├── tests ├── playground.spec.ts ├── readme.spec.ts └── snippets.spec.ts ├── tsconfig.json ├── tsconfig.tools.json └── vitest.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | 5 | # Log files 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Editor directories and files 11 | .idea 12 | .vscode 13 | *.suo 14 | *.ntvs* 15 | *.njsproj 16 | *.sln 17 | *.sw* 18 | *.cpuprofile 19 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | message=chore: release v%s 2 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "commitMessage": "chore: release v${version}", 4 | "tagName": "v${version}" 5 | }, 6 | "github": { 7 | "release": true, 8 | "releaseName": "v${version}" 9 | }, 10 | "plugins": { 11 | "@release-it/conventional-changelog": { 12 | "preset": "conventionalcommits", 13 | "infile": "CHANGELOG.md" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## [1.2.3](https://github.com/CyanSalt/vite-plugin-legacy-swc/compare/v1.2.2...v1.2.3) (2024-12-20) 4 | 5 | ### Bug Fixes 6 | 7 | * tranpile when modern polyfills is auto detected ([15c330a](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/15c330aaae3c48d3b5a146808e58d1c3355c82f9)) 8 | 9 | ## [1.2.2](https://github.com/CyanSalt/vite-plugin-legacy-swc/compare/v1.2.1...v1.2.2) (2024-12-20) 10 | 11 | ### Bug Fixes 12 | 13 | * generate sourcemap for polyfill chunks, https://github.com/vitejs/vite/commit/f311ff3c2b19636457c3023095ef32ab9a96b84a[#diff](https://github.com/CyanSalt/vite-plugin-legacy-swc/issues/diff)-84204d8a3f75f1603eb03a8aac4a229855fecee87636307619b9f499041466f8L144 ([a78574a](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/a78574a5556de7da3e0454f20d8142c8298a1c3e)), closes [/github.com/vitejs/vite/commit/f311ff3c2b19636457c3023095ef32ab9a96b84a#diff-84204d8a3f75f1603eb03a8aac4a229855fecee87636307619b9f499041466f8L144](https://github.com/CyanSalt//github.com/vitejs/vite/commit/f311ff3c2b19636457c3023095ef32ab9a96b84a/issues/diff-84204d8a3f75f1603eb03a8aac4a229855fecee87636307619b9f499041466f8L144) 14 | * log label ([4e0687e](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/4e0687ef12dc9519dcb826daa466e1923094d444)) 15 | 16 | ### Performance Improvements 17 | 18 | * use crypto.hash when available, https://github.com/vitejs/vite/commit/2a148844cf2382a5377b75066351f00207843352[#diff](https://github.com/CyanSalt/vite-plugin-legacy-swc/issues/diff)-ab75c34fa418085884af97e74c9166830b9f0a3456f9d3336e0c075d6ae9b05aR974 ([be5b068](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/be5b068b39b16a3fdd55a0ce10754445e5e3c165)), closes [/github.com/vitejs/vite/commit/2a148844cf2382a5377b75066351f00207843352#diff-ab75c34fa418085884af97e74c9166830b9f0a3456f9d3336e0c075d6ae9b05aR974](https://github.com/CyanSalt//github.com/vitejs/vite/commit/2a148844cf2382a5377b75066351f00207843352/issues/diff-ab75c34fa418085884af97e74c9166830b9f0a3456f9d3336e0c075d6ae9b05aR974) 19 | 20 | ## [1.2.1](https://github.com/CyanSalt/vite-plugin-legacy-swc/compare/v1.2.0...v1.2.1) (2024-08-14) 21 | 22 | 23 | ### Bug Fixes 24 | 25 | * add ctxt on nodes ([bd21942](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/bd2194294a7daa81e58dae1cd99458527717928f)) 26 | 27 | ## [1.2.0](https://github.com/CyanSalt/vite-plugin-legacy-swc/compare/v1.1.0...v1.2.0) (2024-05-31) 28 | 29 | 30 | ### Features 31 | 32 | * support `additionalModernPolyfills` ([4e880b3](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/4e880b3d7819a36f045019350345fb9b6ef3e6af)) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * group discovered polyfills by output ([98cd27f](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/98cd27f82038d7859eee9fb6a41c4f33367a9258)) 38 | * improve deterministic polyfills discovery ([f28f633](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/f28f633173b6c006055f8aa0484b3ae49157f8b6)) 39 | 40 | ## [1.1.0](https://github.com/CyanSalt/vite-plugin-legacy-swc/compare/v1.0.0...v1.1.0) (2024-03-19) 41 | 42 | 43 | ### Features 44 | 45 | * align with @vitejs/plugin-legacy ([95579ef](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/95579ef5305f793b140014680989ccc61c3b14b2)) 46 | 47 | ## [1.0.0](https://github.com/CyanSalt/vite-plugin-legacy-swc/compare/v0.5.1...v1.0.0) (2023-12-08) 48 | 49 | 50 | ### Features 51 | 52 | * build file name optimization ([8e96295](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/8e96295c8a7fc4f96d6979790765012f7850f7d9)) 53 | 54 | 55 | ### Bug Fixes 56 | 57 | * perserve async generator function invocation ([fb61e33](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/fb61e337971c7dac2ee963cff7fb7aeaabbf3759)) 58 | 59 | ## [0.5.1](https://github.com/CyanSalt/vite-plugin-legacy-swc/compare/v0.5.0...v0.5.1) (2023-10-18) 60 | 61 | 62 | ### Bug Fixes 63 | 64 | * lower targets for swc ([29f3008](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/29f30080258a19f7840175f21efb816176fd770f)) 65 | 66 | ## [0.5.0](https://github.com/CyanSalt/vite-plugin-legacy-swc/compare/v0.4.2...v0.5.0) (2023-10-12) 67 | 68 | 69 | ### Features 70 | 71 | * sync implementations from @vitejs/plugin-legacy ([879d384](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/879d384f896a98b4e0f834f3f9fc3a636d42f472)) 72 | 73 | ### [0.4.2](https://github.com/CyanSalt/vite-plugin-legacy-swc/compare/v0.4.1...v0.4.2) (2023-10-07) 74 | 75 | 76 | ### Bug Fixes 77 | 78 | * broken build when renderModernChunks=false & polyfills=false ([f32154f](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/f32154f317e3157a0f0e92f336679c586345ce99)) 79 | 80 | ### [0.4.1](https://github.com/CyanSalt/vite-plugin-legacy-swc/compare/v0.4.0...v0.4.1) (2023-09-20) 81 | 82 | 83 | ### Bug Fixes 84 | 85 | * type exports ([11520ca](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/11520ca71b912fc91794f14342f599f2ea26951c)) 86 | 87 | ## [0.4.0](https://github.com/CyanSalt/vite-plugin-legacy-swc/compare/v0.3.1...v0.4.0) (2023-09-10) 88 | 89 | 90 | ### Features 91 | 92 | * add option to output only legacy builds ([1b346d5](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/1b346d54ad69cee298dde76be7241038cb94594e)) 93 | 94 | ### [0.3.1](https://github.com/CyanSalt/vite-plugin-legacy-swc/compare/v0.3.0...v0.3.1) (2023-05-08) 95 | 96 | 97 | ### Bug Fixes 98 | 99 | * minify polyfill chunks ([7e5e74d](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/7e5e74dc3c32cd8fcda266d8098cf80d3c5ee3ec)) 100 | * severity vulnerability ([08420af](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/08420af503d06a2eda0f450ad89143b2dda8e5dd)) 101 | 102 | ## [0.3.0](https://github.com/CyanSalt/vite-plugin-legacy-swc/compare/v0.2.3...v0.3.0) (2023-02-17) 103 | 104 | 105 | ### Features 106 | 107 | * support native esm ([05732ef](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/05732ef8e41d0dc70b0c87efb703304c7bf31462)) 108 | 109 | ### [0.2.3](https://github.com/CyanSalt/vite-plugin-legacy-swc/compare/v0.2.2...v0.2.3) (2023-02-15) 110 | 111 | 112 | ### Bug Fixes 113 | 114 | * disable minification if minify is false ([c7bca76](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/c7bca76adf04ca64fe5908952aea84179b3bf660)) 115 | 116 | ### [0.2.2](https://github.com/CyanSalt/vite-plugin-legacy-swc/compare/v0.2.1...v0.2.2) (2023-02-09) 117 | 118 | 119 | ### Bug Fixes 120 | 121 | * remove terser as peer deps ([ca0bd7d](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/ca0bd7dbd9e0851de9d81206989eac3830478e77)) 122 | 123 | ### [0.2.1](https://github.com/CyanSalt/vite-plugin-legacy-swc/compare/v0.2.0...v0.2.1) (2023-02-09) 124 | 125 | 126 | ### Bug Fixes 127 | 128 | * export options type ([5908dbf](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/5908dbf7988d105c78d34b34e1fabb74bf2b6070)) 129 | 130 | ## [0.2.0](https://github.com/CyanSalt/vite-plugin-legacy-swc/compare/v0.1.2...v0.2.0) (2023-02-08) 131 | 132 | 133 | ### Features 134 | 135 | * minify with swc instead of terser ([72c3791](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/72c3791fa4d05e189e660842d8ab0f3898766231)) 136 | 137 | ### [0.1.2](https://github.com/CyanSalt/vite-plugin-legacy-swc/compare/v0.1.1...v0.1.2) (2023-02-08) 138 | 139 | 140 | ### Bug Fixes 141 | 142 | * apply swc plugins after transforming ([2e4e138](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/2e4e13851f1ceb3672125c09502750988fe30033)) 143 | * browserslist-rs compat ([d8aff61](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/d8aff618cf162f5b95562a1169b748fad14a6470)) 144 | * missing deps ([a600f7c](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/a600f7c3f1b7e8711d330d32cab55bff8d9d2c01)) 145 | 146 | ### [0.1.1](https://github.com/CyanSalt/vite-plugin-legacy-swc/compare/v0.1.0...v0.1.1) (2023-02-08) 147 | 148 | 149 | ### Bug Fixes 150 | 151 | * core-js version ([e6fed36](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/e6fed36b25f95b88f7fa84493854eac98ef89b88)) 152 | * support `ignoreBrowserslistConfig` ([747d4cf](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/747d4cfa0a90aab1c1590e0ca0d843bb2adfb06b)) 153 | * swc error ([3422f17](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/3422f173fa64dbb4d15a60d89710086a68f82f47)) 154 | 155 | ## 0.1.0 (2023-02-08) 156 | 157 | 158 | ### Features 159 | 160 | * init ([a4e3cda](https://github.com/CyanSalt/vite-plugin-legacy-swc/commit/a4e3cdace03d082f006c589ad758018aa05d29d1)) 161 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2020-present CyanSalt 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vite-plugin-legacy-swc 2 | 3 | [![npm](https://img.shields.io/npm/v/vite-plugin-legacy-swc.svg)](https://www.npmjs.com/package/vite-plugin-legacy-swc) 4 | 5 | Provides legacy browsers support for the production build with [SWC](https://github.com/swc-project/swc). 6 | 7 | This package is intended to replace [`@vitejs/plugin-legacy`](https://www.npmjs.com/package/@vitejs/plugin-legacy) in performance-sensitive situations. It is basically an implementation of [vitejs/vite#4105](https://github.com/vitejs/vite/pull/4105). 8 | 9 | As for performance, for reference, the results of my tests on a huge private project (with `{ modernPolyfills: true }`) are as follows: 10 | 11 | | | Without legacy browsers support | With `@vitejs/plugin-legacy` | With `vite-plugin-legacy-swc` | 12 | | --- | --- | --- | --- | 13 | | CPU Time | 146.45s | 697.82s | 295.04s | 14 | | JS Asset Size* | 9.5M | 22M | 21M | 15 | | JS Asset Size Without Legacy Chunks | 9.5M | 9.6M | 9.6M | 16 | 17 | Compared to `@vitejs/plugin-legacy`, `vite-plugin-legacy-swc` **saves 58% of time and 4% of JS asset size**. 18 | 19 | *\* In my current tests, `@vitejs/plugin-legacy` does not generate source maps for legacy chunks correctly, so the asset size statistics exclude the source maps.* 20 | 21 | --- 22 | 23 | Vite's default browser support baseline is [Native ESM](https://caniuse.com/es6-module), [native ESM dynamic import](https://caniuse.com/es6-module-dynamic-import), and [`import.meta`](https://caniuse.com/mdn-javascript_operators_import_meta). This plugin provides support for legacy browsers that do not support those features when building for production. 24 | 25 | By default, this plugin will: 26 | 27 | - Generate a corresponding legacy chunk for every chunk in the final bundle, transformed with [@swc/core](https://swc.rs/docs/configuration/supported-browsers) and emitted as [SystemJS modules](https://github.com/systemjs/systemjs) (code splitting is still supported!). 28 | 29 | - Generate a polyfill chunk including SystemJS runtime, and any necessary polyfills determined by specified browser targets and **actual usage** in the bundle. 30 | 31 | - Inject ` 5 | 6 | 7 | -------------------------------------------------------------------------------- /playground/main.js: -------------------------------------------------------------------------------- 1 | alert(import.meta.env.PROD ? 'PROD' : 'DEV') 2 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-legacy-swc-playground", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module" 6 | } 7 | -------------------------------------------------------------------------------- /playground/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | import { defineConfig } from 'vite' 4 | import legacy from '../src' 5 | 6 | export default defineConfig({ 7 | root: path.dirname(fileURLToPath(import.meta.url)), 8 | plugins: [ 9 | legacy({ 10 | targets: 'IE 11', 11 | modernPolyfills: true, 12 | }), 13 | ], 14 | }) 15 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | import { createRequire } from 'module' 3 | import path from 'path' 4 | import { fileURLToPath } from 'url' 5 | import type { 6 | EnvConfig, 7 | JsMinifyOptions, 8 | Statement, 9 | Options as SwcOptions, 10 | Plugin as SwcPlugin, 11 | } from '@swc/core' 12 | import browserslist from 'browserslist' 13 | import MagicString from 'magic-string' 14 | import colors from 'picocolors' 15 | import type { 16 | NormalizedOutputOptions, 17 | OutputAsset, 18 | OutputBundle, 19 | OutputChunk, 20 | OutputOptions, 21 | PreRenderedChunk, 22 | RenderedChunk, 23 | } from 'rollup' 24 | import type { 25 | BuildOptions, 26 | HtmlTagDescriptor, 27 | Plugin, 28 | ResolvedConfig, 29 | } from 'vite' 30 | import { build, normalizePath } from 'vite' 31 | import { 32 | detectModernBrowserCode, 33 | dynamicFallbackInlineCode, 34 | legacyEntryId, 35 | legacyPolyfillId, 36 | modernChunkLegacyGuard, 37 | safari10NoModuleFix, 38 | systemJSInlineCode, 39 | } from './snippets' 40 | import type { Options } from './types' 41 | 42 | // lazy load swc since it's not used during dev 43 | // eslint-disable-next-line @typescript-eslint/consistent-type-imports 44 | let loadedSwc: typeof import('@swc/core') | undefined 45 | async function loadSwc() { 46 | if (!loadedSwc) { 47 | loadedSwc = await import('@swc/core') 48 | } 49 | return loadedSwc 50 | } 51 | 52 | // The requested module 'browserslist' is a CommonJS module 53 | // which may not support all module.exports as named exports 54 | const { loadConfig: browserslistLoadConfig } = browserslist 55 | 56 | // Duplicated from build.ts in Vite Core, at least while the feature is experimental 57 | // We should later expose this helper for other plugins to use 58 | function toOutputFilePathInHtml({ 59 | filename, 60 | type, 61 | hostId, 62 | hostType, 63 | config, 64 | toRelative, 65 | }: { 66 | filename: string, 67 | type: 'asset' | 'public', 68 | hostId: string, 69 | hostType: 'js' | 'css' | 'html', 70 | config: ResolvedConfig, 71 | toRelative: (filename: string, importer: string) => string, 72 | }): string { 73 | const { renderBuiltUrl } = config.experimental 74 | let relative = config.base === '' || config.base === './' 75 | if (renderBuiltUrl) { 76 | const result = renderBuiltUrl(filename, { 77 | hostId, 78 | hostType, 79 | type, 80 | ssr: Boolean(config.build.ssr), 81 | }) 82 | if (typeof result === 'object') { 83 | if (result.runtime) { 84 | throw new Error( 85 | `{ runtime: "${result.runtime}" } is not supported for assets in ${hostType} files: ${filename}`, 86 | ) 87 | } 88 | if (typeof result.relative === 'boolean') { 89 | relative = result.relative 90 | } 91 | } else if (result) { 92 | return result 93 | } 94 | } 95 | if (relative && !config.build.ssr) { 96 | return toRelative(filename, hostId) 97 | } else { 98 | return joinUrlSegments(config['decodedBase'] ?? config.base, filename) 99 | } 100 | } 101 | function getBaseInHTML(urlRelativePath: string, config: ResolvedConfig) { 102 | // Prefer explicit URL if defined for linking to assets and public files from HTML, 103 | // even when base relative is specified 104 | return config.base === './' || config.base === '' 105 | ? path.posix.join( 106 | path.posix.relative(urlRelativePath, '').slice(0, -2), 107 | './', 108 | ) 109 | : config.base 110 | } 111 | function joinUrlSegments(a: string, b: string): string { 112 | if (!a || !b) { 113 | return a || b || '' 114 | } 115 | if (a[a.length - 1] === '/') { 116 | a = a.slice(0, Math.max(0, a.length - 1)) 117 | } 118 | if (b[0] !== '/') { 119 | b = '/' + b 120 | } 121 | return a + b 122 | } 123 | 124 | function toAssetPathFromHtml( 125 | filename: string, 126 | htmlPath: string, 127 | config: ResolvedConfig, 128 | ): string { 129 | const relativeUrlPath = normalizePath(path.relative(config.root, htmlPath)) 130 | const toRelative = (filePath: string, hostId: string) => 131 | getBaseInHTML(relativeUrlPath, config) + filePath 132 | return toOutputFilePathInHtml({ 133 | filename, 134 | type: 'asset', 135 | hostId: htmlPath, 136 | hostType: 'html', 137 | config, 138 | toRelative, 139 | }) 140 | } 141 | 142 | const legacyEnvVarMarker = `__VITE_IS_LEGACY__` 143 | 144 | const $require = createRequire(import.meta.url) 145 | 146 | const nonLeadingHashInFileNameRE = /[^/]+\[hash(?::\d+)?\]/ 147 | const prefixedHashInFileNameRE = /\W?\[hash(?::\d+)?\]/ 148 | 149 | function viteLegacyPlugin(options: Options = {}): Plugin[] { 150 | let resolvedConfig: ResolvedConfig 151 | let targets: Options['targets'] 152 | let modernTargets: Options['modernTargets'] 153 | 154 | // browsers supporting ESM + dynamic import + import.meta + async generator 155 | const modernTargetsEsbuild = [ 156 | 'es2020', 157 | 'edge79', 158 | 'firefox67', 159 | 'chrome64', 160 | 'safari12', 161 | ] 162 | // same with above but by browserslist syntax 163 | // es2020 = chrome 80+, safari 13.1+, firefox 72+, edge 80+ 164 | // https://github.com/evanw/esbuild/issues/121#issuecomment-646956379 165 | const modernTargetsSwc 166 | = 'edge>=79, firefox>=67, chrome>=64, safari>=12, chromeAndroid>=64, iOS>=12' 167 | 168 | const genLegacy = options.renderLegacyChunks !== false 169 | const genModern = options.renderModernChunks !== false 170 | if (!genLegacy && !genModern) { 171 | throw new Error( 172 | '`renderLegacyChunks` and `renderModernChunks` cannot be both false', 173 | ) 174 | } 175 | 176 | const debugFlags = (process.env.DEBUG || '').split(',') 177 | const isDebug 178 | = debugFlags.includes('vite:*') || debugFlags.includes('vite:legacy') 179 | 180 | const facadeToLegacyChunkMap = new Map() 181 | const facadeToLegacyPolyfillMap = new Map() 182 | const facadeToModernPolyfillMap = new Map() 183 | const modernPolyfills = new Set() 184 | const legacyPolyfills = new Set() 185 | // When discovering polyfills in `renderChunk`, the hook may be non-deterministic, so we group the 186 | // modern and legacy polyfills in a sorted chunks map for each rendered outputs before merging them. 187 | const outputToChunkFileNameToPolyfills = new WeakMap< 188 | NormalizedOutputOptions, 189 | Map, legacy: Set }> | null 190 | >() 191 | 192 | if (Array.isArray(options.modernPolyfills) && genModern) { 193 | options.modernPolyfills.forEach((i) => { 194 | modernPolyfills.add( 195 | i.includes('/') ? `core-js/${i}` : `core-js/modules/${i}.js`, 196 | ) 197 | }) 198 | } 199 | if (Array.isArray(options.additionalModernPolyfills)) { 200 | options.additionalModernPolyfills.forEach((i) => { 201 | modernPolyfills.add(i) 202 | }) 203 | } 204 | if (Array.isArray(options.polyfills)) { 205 | options.polyfills.forEach((i) => { 206 | if (i.startsWith(`regenerator`)) { 207 | legacyPolyfills.add(`regenerator-runtime/runtime.js`) 208 | } else { 209 | legacyPolyfills.add( 210 | i.includes('/') ? `core-js/${i}` : `core-js/modules/${i}.js`, 211 | ) 212 | } 213 | }) 214 | } 215 | if (Array.isArray(options.additionalLegacyPolyfills)) { 216 | options.additionalLegacyPolyfills.forEach((i) => { 217 | legacyPolyfills.add(i) 218 | }) 219 | } 220 | 221 | let overriddenBuildTarget = false 222 | let overriddenDefaultModernTargets = false 223 | const legacyConfigPlugin: Plugin = { 224 | name: 'vite:legacy-config', 225 | 226 | async config(config, env) { 227 | if (env.command === 'build' && !config.build?.ssr) { 228 | if (!config.build) { 229 | config.build = {} 230 | } 231 | 232 | if (!config.build.cssTarget) { 233 | // Hint for esbuild that we are targeting legacy browsers when minifying CSS. 234 | // Full CSS compat table available at https://github.com/evanw/esbuild/blob/78e04680228cf989bdd7d471e02bbc2c8d345dc9/internal/compat/css_table.go 235 | // But note that only the `HexRGBA` feature affects the minify outcome. 236 | // HSL & rebeccapurple values will be minified away regardless the target. 237 | // So targeting `chrome61` suffices to fix the compatibility issue. 238 | config.build.cssTarget = 'chrome61' 239 | } 240 | 241 | // Babel-based legacy plugin will only override esbuild's target when rendering legacy chunks. 242 | // This will cause syntax transformation to be inconsistent with polyfill behavior when rendering modern chunks only. 243 | // For example, when using `Promise.allSettled` and nullish coalescing operator (both from ES2020), 244 | // the latter will be retained while the former will be polyfilled. 245 | // Here is a simple heuristic strategy: `target` will be overridden when modern polyfill detection is turned on 246 | // if (genLegacy) { ... } 247 | if (genLegacy || options.modernPolyfills && !Array.isArray(options.modernPolyfills)) { 248 | // Vite's default target browsers are **not** the same. 249 | // See https://github.com/vitejs/vite/pull/10052#issuecomment-1242076461 250 | overriddenBuildTarget = config.build.target !== undefined 251 | overriddenDefaultModernTargets = options.modernTargets !== undefined 252 | 253 | if (options.modernTargets) { 254 | // Package is ESM only 255 | const { default: browserslistToEsbuild } = await import( 256 | 'browserslist-to-esbuild' 257 | ) 258 | config.build.target = browserslistToEsbuild(options.modernTargets) 259 | } else { 260 | config.build.target = modernTargetsEsbuild 261 | } 262 | } 263 | } 264 | 265 | return { 266 | define: { 267 | 'import.meta.env.LEGACY': 268 | env.command === 'serve' || config.build?.ssr 269 | ? false 270 | : legacyEnvVarMarker, 271 | }, 272 | } 273 | }, 274 | configResolved(config) { 275 | if (overriddenBuildTarget) { 276 | config.logger.warn( 277 | colors.yellow( 278 | `vite-plugin-legacy-swc overrode 'build.target'. You should pass 'targets' as an option to this plugin with the list of legacy browsers to support instead.`, 279 | ), 280 | ) 281 | } 282 | if (overriddenDefaultModernTargets) { 283 | config.logger.warn( 284 | colors.yellow( 285 | `vite-plugin-legacy-swc 'modernTargets' option overrode the builtin targets of modern chunks. Some versions of browsers between legacy and modern may not be supported.`, 286 | ), 287 | ) 288 | } 289 | }, 290 | } 291 | 292 | const legacyGenerateBundlePlugin: Plugin = { 293 | name: 'vite:legacy-generate-polyfill-chunk', 294 | apply: 'build', 295 | 296 | async generateBundle(opts, bundle) { 297 | if (resolvedConfig.build.ssr) { 298 | return 299 | } 300 | 301 | const chunkFileNameToPolyfills 302 | = outputToChunkFileNameToPolyfills.get(opts) 303 | if (!chunkFileNameToPolyfills) { 304 | throw new Error( 305 | 'Internal vite-plugin-legacy-swc error: discovered polyfills should exist', 306 | ) 307 | } 308 | 309 | if (!isLegacyBundle(bundle, opts)) { 310 | // Merge discovered modern polyfills to `modernPolyfills` 311 | for (const { modern } of chunkFileNameToPolyfills.values()) { 312 | modern.forEach((p) => modernPolyfills.add(p)) 313 | } 314 | if (!modernPolyfills.size) { 315 | return 316 | } 317 | if (isDebug) { 318 | console.log( 319 | `[vite-plugin-legacy-swc] modern polyfills:`, 320 | modernPolyfills, 321 | ) 322 | } 323 | await buildPolyfillChunk({ 324 | mode: resolvedConfig.mode, 325 | imports: modernPolyfills, 326 | bundle, 327 | facadeToChunkMap: facadeToModernPolyfillMap, 328 | buildOptions: resolvedConfig.build, 329 | format: 'es', 330 | rollupOutputOptions: opts, 331 | excludeSystemJS: true, 332 | prependModernChunkLegacyGuard: genLegacy, 333 | }) 334 | return 335 | } 336 | 337 | if (!genLegacy) { 338 | return 339 | } 340 | 341 | // Merge discovered legacy polyfills to `legacyPolyfills` 342 | for (const { legacy } of chunkFileNameToPolyfills.values()) { 343 | legacy.forEach((p) => legacyPolyfills.add(p)) 344 | } 345 | 346 | // legacy bundle 347 | if (options.polyfills !== false) { 348 | // check if the target needs Promise polyfill because SystemJS relies on it 349 | // https://github.com/systemjs/systemjs#ie11-support 350 | await detectPolyfills( 351 | `Promise.resolve(); Promise.all();`, 352 | targets, 353 | legacyPolyfills, 354 | ) 355 | } 356 | 357 | if (legacyPolyfills.size || !options.externalSystemJS) { 358 | if (isDebug) { 359 | console.log( 360 | `[vite-plugin-legacy-swc] legacy polyfills:`, 361 | legacyPolyfills, 362 | ) 363 | } 364 | 365 | await buildPolyfillChunk({ 366 | mode: resolvedConfig.mode, 367 | imports: legacyPolyfills, 368 | bundle, 369 | facadeToChunkMap: facadeToLegacyPolyfillMap, 370 | // force using swc for legacy polyfill minification, since esbuild 371 | // isn't legacy-safe 372 | buildOptions: resolvedConfig.build, 373 | format: 'iife', 374 | rollupOutputOptions: opts, 375 | excludeSystemJS: options.externalSystemJS, 376 | }) 377 | } 378 | }, 379 | } 380 | 381 | const legacyPostPlugin: Plugin = { 382 | name: 'vite:legacy-post-process', 383 | enforce: 'post', 384 | apply: 'build', 385 | 386 | renderStart(opts) { 387 | // Empty the nested map for this output 388 | outputToChunkFileNameToPolyfills.set(opts, null) 389 | }, 390 | 391 | configResolved(config) { 392 | if (config.build.lib) { 393 | throw new Error('vite-plugin-legacy-swc does not support library mode.') 394 | } 395 | resolvedConfig = config 396 | 397 | modernTargets = options.modernTargets || modernTargetsSwc 398 | if (isDebug) { 399 | console.log(`[vite-plugin-legacy-swc] modernTargets:`, modernTargets) 400 | } 401 | 402 | if (!genLegacy || resolvedConfig.build.ssr) { 403 | return 404 | } 405 | 406 | targets 407 | = options.targets 408 | || browserslistLoadConfig({ path: resolvedConfig.root }) 409 | || 'last 2 versions and not dead, > 0.3%, Firefox ESR' 410 | if (isDebug) { 411 | console.log(`[vite-plugin-legacy-swc] targets:`, targets) 412 | } 413 | 414 | const getLegacyOutputFileName = ( 415 | fileNames: 416 | | string 417 | | ((chunkInfo: PreRenderedChunk) => string) 418 | | undefined, 419 | defaultFileName = '[name]-legacy-[hash].js', 420 | ): string | ((chunkInfo: PreRenderedChunk) => string) => { 421 | if (!fileNames) { 422 | return path.posix.join(resolvedConfig.build.assetsDir, defaultFileName) 423 | } 424 | 425 | return (chunkInfo) => { 426 | let fileName 427 | = typeof fileNames === 'function' ? fileNames(chunkInfo) : fileNames 428 | 429 | if (fileName.includes('[name]')) { 430 | // [name]-[hash].[format] -> [name]-legacy-[hash].[format] 431 | fileName = fileName.replace('[name]', '[name]-legacy') 432 | } else if (nonLeadingHashInFileNameRE.test(fileName)) { 433 | // custom[hash].[format] -> [name]-legacy[hash].[format] 434 | // custom-[hash].[format] -> [name]-legacy-[hash].[format] 435 | // custom.[hash].[format] -> [name]-legacy.[hash].[format] 436 | // custom.[hash:10].[format] -> custom-legacy.[hash:10].[format] 437 | fileName = fileName.replace(prefixedHashInFileNameRE, '-legacy$&') 438 | } else { 439 | // entry.js -> entry-legacy.js 440 | // entry.min.js -> entry-legacy.min.js 441 | fileName = fileName.replace(/(.+?)\.(.+)/, '$1-legacy.$2') 442 | } 443 | 444 | return fileName 445 | } 446 | } 447 | 448 | const createLegacyOutput = ( 449 | outputOptions: OutputOptions = {}, 450 | ): OutputOptions => { 451 | return { 452 | ...outputOptions, 453 | format: 'system', 454 | entryFileNames: getLegacyOutputFileName(outputOptions.entryFileNames), 455 | chunkFileNames: getLegacyOutputFileName(outputOptions.chunkFileNames), 456 | } 457 | } 458 | 459 | const { rollupOptions } = resolvedConfig.build 460 | const { output } = rollupOptions 461 | if (Array.isArray(output)) { 462 | rollupOptions.output = [ 463 | ...output.map(createLegacyOutput), 464 | ...(genModern ? output : []), 465 | ] 466 | } else { 467 | rollupOptions.output = [ 468 | createLegacyOutput(output), 469 | ...(genModern ? [output ?? {}] : []), 470 | ] 471 | } 472 | }, 473 | 474 | async renderChunk(raw, chunk, opts, { chunks }) { 475 | if (resolvedConfig.build.ssr) { 476 | return null 477 | } 478 | 479 | // On first run, initialize the map with sorted chunk file names 480 | let chunkFileNameToPolyfills = outputToChunkFileNameToPolyfills.get(opts) 481 | if (!chunkFileNameToPolyfills) { 482 | chunkFileNameToPolyfills = new Map() 483 | Object.keys(chunks).forEach((fileName) => { 484 | chunkFileNameToPolyfills!.set(fileName, { 485 | modern: new Set(), 486 | legacy: new Set(), 487 | }) 488 | }) 489 | outputToChunkFileNameToPolyfills.set(opts, chunkFileNameToPolyfills) 490 | } 491 | const polyfillsDiscovered = chunkFileNameToPolyfills.get(chunk.fileName) 492 | if (!polyfillsDiscovered) { 493 | throw new Error( 494 | `Internal vite-plugin-legacy-swc error: discovered polyfills for ${chunk.fileName} should exist`, 495 | ) 496 | } 497 | 498 | if (!isLegacyChunk(chunk, opts)) { 499 | if ( 500 | options.modernPolyfills 501 | && !Array.isArray(options.modernPolyfills) 502 | && genModern 503 | ) { 504 | // analyze and record modern polyfills 505 | await detectPolyfills(raw, modernTargets, polyfillsDiscovered.modern) 506 | } 507 | 508 | const ms = new MagicString(raw) 509 | 510 | if (genLegacy && chunk.isEntry) { 511 | // append this code to avoid modern chunks running on legacy targeted browsers 512 | ms.prepend(modernChunkLegacyGuard) 513 | } 514 | 515 | if (raw.includes(legacyEnvVarMarker)) { 516 | const re = new RegExp(legacyEnvVarMarker, 'g') 517 | let match 518 | while ((match = re.exec(raw))) { 519 | ms.overwrite( 520 | match.index, 521 | match.index + legacyEnvVarMarker.length, 522 | `false`, 523 | ) 524 | } 525 | } 526 | 527 | if (resolvedConfig.build.sourcemap) { 528 | return { 529 | code: ms.toString(), 530 | map: ms.generateMap({ hires: 'boundary' }), 531 | } 532 | } 533 | return { 534 | code: ms.toString(), 535 | } 536 | } 537 | 538 | if (!genLegacy) { 539 | return null 540 | } 541 | 542 | // @ts-expect-error avoid esbuild transform on legacy chunks since it produces 543 | // legacy-unsafe code - e.g. rewriting object properties into shorthands 544 | opts.__vite_skip_esbuild__ = true 545 | 546 | // @ts-expect-error force terser for legacy chunks. This only takes effect if 547 | // minification isn't disabled, because that leaves out the terser plugin 548 | // entirely. 549 | opts.__vite_force_terser__ = true 550 | 551 | // @ts-expect-error In the `generateBundle` hook, 552 | // we'll delete the assets from the legacy bundle to avoid emitting duplicate assets. 553 | // But that's still a waste of computing resource. 554 | // So we add this flag to avoid emitting the asset in the first place whenever possible. 555 | opts.__vite_skip_asset_emit__ = true 556 | 557 | // avoid emitting assets for legacy bundle 558 | const needPolyfills 559 | = options.polyfills !== false && !Array.isArray(options.polyfills) 560 | 561 | // transform the legacy chunk with @swc/core 562 | const sourceMaps = Boolean(resolvedConfig.build.sourcemap) 563 | const swc = await loadSwc() 564 | const swcOptions: SwcOptions = { 565 | swcrc: false, 566 | configFile: false, 567 | sourceMaps, 568 | env: createSwcEnvOptions(targets, { 569 | needPolyfills, 570 | }), 571 | } 572 | const minifyOptions: JsMinifyOptions = { 573 | compress: { 574 | // Different defaults between terser and swc 575 | dead_code: true, 576 | keep_fargs: true, 577 | passes: 1, 578 | }, 579 | mangle: true, 580 | safari10: true, 581 | ...resolvedConfig.build.terserOptions as JsMinifyOptions, 582 | sourceMap: Boolean(opts.sourcemap), 583 | module: opts.format.startsWith('es'), 584 | toplevel: opts.format === 'cjs', 585 | } 586 | const transformResult = await swc.transform(raw, { 587 | ...swcOptions, 588 | inputSourceMap: undefined, 589 | minify: Boolean(resolvedConfig.build.minify && minifyOptions.mangle), 590 | jsc: { 591 | // mangle only 592 | minify: { 593 | ...minifyOptions, 594 | compress: false, 595 | }, 596 | transform: { 597 | optimizer: { 598 | globals: { 599 | vars: { [legacyEnvVarMarker]: 'true' }, 600 | }, 601 | }, 602 | }, 603 | }, 604 | }) 605 | const plugin = swc.plugins([ 606 | recordAndRemovePolyfillSwcPlugin(polyfillsDiscovered.legacy), 607 | wrapIIFESwcPlugin(), 608 | ]) 609 | const ast = await swc.parse(transformResult.code) 610 | const result = await swc.print(plugin(ast), { 611 | ...swcOptions, 612 | inputSourceMap: transformResult.map, 613 | minify: Boolean(resolvedConfig.build.minify && minifyOptions.compress), 614 | jsc: { 615 | // compress only 616 | minify: { 617 | ...minifyOptions, 618 | mangle: false, 619 | }, 620 | }, 621 | }) 622 | 623 | return result 624 | }, 625 | 626 | transformIndexHtml(html, { chunk }) { 627 | if (resolvedConfig.build.ssr) return 628 | if (!chunk) return 629 | if (chunk.fileName.includes('-legacy')) { 630 | // The legacy bundle is built first, and its index.html isn't actually emitted if 631 | // modern bundle will be generated. Here we simply record its corresponding legacy chunk. 632 | facadeToLegacyChunkMap.set(chunk.facadeModuleId, chunk.fileName) 633 | if (genModern) { 634 | return 635 | } 636 | } 637 | if (!genModern) { 638 | html = html.replace(/