Skip to content

Commit

Permalink
feat(legacy)!: bump modern target to support async generator (#11896)
Browse files Browse the repository at this point in the history
  • Loading branch information
sapphi-red authored Feb 2, 2023
1 parent d5b8f86 commit 55b9711
Show file tree
Hide file tree
Showing 9 changed files with 105 additions and 26 deletions.
14 changes: 10 additions & 4 deletions packages/plugin-legacy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ By default, this plugin will:

- Generate a polyfill chunk including SystemJS runtime, and any necessary polyfills determined by specified browser targets and **actual usage** in the bundle.

- Inject `<script nomodule>` tags into generated HTML to conditionally load the polyfills and legacy bundle only in browsers without native ESM support.
- Inject `<script nomodule>` tags into generated HTML to conditionally load the polyfills and legacy bundle only in browsers without widely-available features support.

- Inject the `import.meta.env.LEGACY` env variable, which will only be `true` in the legacy production build, and `false` in all other cases.

Expand Down Expand Up @@ -117,12 +117,18 @@ npm add -D terser

Defaults to `false`. Enabling this option will exclude `systemjs/dist/s.min.js` inside polyfills-legacy chunk.

## Dynamic Import
## Browsers that supports ESM but does not support widely-available features

The legacy plugin offers a way to use native `import()` in the modern build while falling back to the legacy build in browsers with native ESM but without dynamic import support (e.g. Legacy Edge). This feature works by injecting a runtime check and loading the legacy bundle with SystemJs runtime if needed. There are the following drawbacks:
The legacy plugin offers a way to use widely-available features natively in the modern build, while falling back to the legacy build in browsers with native ESM but without those features supported (e.g. Legacy Edge). This feature works by injecting a runtime check and loading the legacy bundle with SystemJs runtime if needed. There are the following drawbacks:

- Modern bundle is downloaded in all ESM browsers
- Modern bundle throws `SyntaxError` in browsers without dynamic import
- Modern bundle throws `SyntaxError` in browsers without those features support

The following syntax are considered as widely-available:

- dynamic import
- `import.meta`
- async generator

## Polyfill Specifiers

Expand Down
3 changes: 2 additions & 1 deletion packages/plugin-legacy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@
},
"homepage": "https://github.com/vitejs/vite/tree/main/packages/plugin-legacy#readme",
"dependencies": {
"core-js": "^3.27.2",
"@babel/core": "^7.20.12",
"@babel/preset-env": "^7.20.2",
"browserslist": "^4.21.4",
"core-js": "^3.27.2",
"magic-string": "^0.27.0",
"regenerator-runtime": "^0.13.11",
"systemjs": "^6.13.0"
Expand All @@ -54,6 +54,7 @@
"vite": "^4.0.0"
},
"devDependencies": {
"acorn": "^8.8.2",
"picocolors": "^1.0.0",
"vite": "workspace:*"
}
Expand Down
36 changes: 36 additions & 0 deletions packages/plugin-legacy/src/__tests__/snippets.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { expect, test } from 'vitest'
import type { ecmaVersion } from 'acorn'
import { parse } from 'acorn'
import { detectModernBrowserDetector } from '../snippets'

const shouldFailVersions: ecmaVersion[] = []
for (let v = 2015; v <= 2019; v++) {
shouldFailVersions.push(v as ecmaVersion)
}

const shouldPassVersions: acorn.ecmaVersion[] = []
for (let v = 2020; v <= 2022; v++) {
shouldPassVersions.push(v as ecmaVersion)
}

for (const version of shouldFailVersions) {
test(`detect code should not be able to be parsed with ES${version}`, () => {
expect(() => {
parse(detectModernBrowserDetector, {
ecmaVersion: version,
sourceType: 'module',
})
}).toThrow()
})
}

for (const version of shouldPassVersions) {
test(`detect code should be able to be parsed with ES${version}`, () => {
expect(() => {
parse(detectModernBrowserDetector, {
ecmaVersion: version,
sourceType: 'module',
})
}).not.toThrow()
})
}
37 changes: 16 additions & 21 deletions packages/plugin-legacy/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ import type {
import colors from 'picocolors'
import { loadConfig as browserslistLoadConfig } from 'browserslist'
import type { Options } from './types'
import {
detectModernBrowserCode,
dynamicFallbackInlineCode,
legacyEntryId,
legacyPolyfillId,
modernChunkLegacyGuard,
safari10NoModuleFix,
systemJSInlineCode,
} from './snippets'

// lazy load babel since it's not used during dev
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
Expand Down Expand Up @@ -103,20 +112,6 @@ function toAssetPathFromHtml(
)
}

// 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 detectModernBrowserVarName = '__vite_is_modern_browser'
const detectModernBrowserCode = `try{import.meta.url;import("_").catch(()=>1);}catch(e){}window.${detectModernBrowserVarName}=true;`
const dynamicFallbackInlineCode = `!function(){if(window.${detectModernBrowserVarName})return;console.warn("vite: loading legacy build because dynamic import or import.meta.url 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__`

const _require = createRequire(import.meta.url)
Expand All @@ -126,7 +121,6 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] {
let targets: Options['targets']

const genLegacy = options.renderLegacyChunks !== false
const genDynamicFallback = genLegacy

const debugFlags = (process.env.DEBUG || '').split(',')
const isDebug =
Expand Down Expand Up @@ -185,13 +179,13 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] {
// Vite's default target browsers are **not** the same.
// See https://github.com/vitejs/vite/pull/10052#issuecomment-1242076461
overriddenBuildTarget = config.build.target !== undefined
// browsers supporting ESM + dynamic import + import.meta
// browsers supporting ESM + dynamic import + import.meta + async generator
config.build.target = [
'es2020',
'edge79',
'firefox67',
'chrome64',
'safari11.1',
'safari12',
]
}
}
Expand Down Expand Up @@ -252,7 +246,7 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] {
}

// legacy bundle
if (legacyPolyfills.size || genDynamicFallback) {
if (legacyPolyfills.size) {
// check if the target needs Promise polyfill because SystemJS relies on it
// https://github.com/systemjs/systemjs#ie11-support
await detectPolyfills(
Expand Down Expand Up @@ -367,8 +361,9 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] {

const ms = new MagicString(raw)

if (genDynamicFallback && chunk.isEntry) {
ms.prepend(forceDynamicImportUsage)
if (genLegacy && chunk.isEntry) {
// append this code to avoid modern chunks running on legacy targeted browsers
ms.prepend(modernChunkLegacyGuard)
}

if (raw.includes(legacyEnvVarMarker)) {
Expand Down Expand Up @@ -557,7 +552,7 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] {
}

// 5. inject dynamic import fallback entry
if (genDynamicFallback && legacyPolyfillFilename && legacyEntryFilename) {
if (genLegacy && legacyPolyfillFilename && legacyEntryFilename) {
tags.push({
tag: 'script',
attrs: { type: 'module' },
Expand Down
15 changes: 15 additions & 0 deletions packages/plugin-legacy/src/snippets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc
// DO NOT ALTER THIS CONTENT
export 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()}}();`

export const legacyPolyfillId = 'vite-legacy-polyfill'
export const legacyEntryId = 'vite-legacy-entry'
export const systemJSInlineCode = `System.import(document.getElementById('${legacyEntryId}').getAttribute('data-src'))`

const detectModernBrowserVarName = '__vite_is_modern_browser'
export const detectModernBrowserDetector =
'import.meta.url;import("_").catch(()=>1);async function* g(){};'
export const detectModernBrowserCode = `${detectModernBrowserDetector}window.${detectModernBrowserVarName}=true;`
export const dynamicFallbackInlineCode = `!function(){if(window.${detectModernBrowserVarName})return;console.warn("vite: loading legacy chunks, syntax error above and the same error below should be ignored");var e=document.getElementById("${legacyPolyfillId}"),n=document.createElement("script");n.src=e.src,n.onload=function(){${systemJSInlineCode}},document.body.appendChild(n)}();`

export const modernChunkLegacyGuard = `export function __vite_legacy_guard(){${detectModernBrowserDetector}};`
8 changes: 8 additions & 0 deletions playground/legacy/__tests__/legacy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ test('transpiles down iterators correctly', async () => {
await untilUpdated(() => page.textContent('#iterators'), 'hello', true)
})

test('async generator', async () => {
await untilUpdated(
() => page.textContent('#async-generator'),
'[0,1,2]',
true,
)
})

test('wraps with iife', async () => {
await untilUpdated(
() => page.textContent('#babel-helpers'),
Expand Down
1 change: 1 addition & 0 deletions playground/legacy/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ <h1 id="app"></h1>
<div id="env"></div>
<div id="iterators"></div>
<div id="features-after-corejs-3"></div>
<div id="async-generator"></div>
<div id="babel-helpers"></div>
<div id="assets"></div>
<button id="dynamic-css-button">dynamic css</button>
Expand Down
15 changes: 15 additions & 0 deletions playground/legacy/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,21 @@ text(
JSON.stringify(structuredClone({ foo: 'foo' })),
)

// async generator
async function* asyncGenerator() {
for (let i = 0; i < 3; i++) {
await new Promise((resolve) => setTimeout(resolve, 10))
yield i
}
}
;(async () => {
const result = []
for await (const i of asyncGenerator()) {
result.push(i)
}
text('#async-generator', JSON.stringify(result))
})()

// babel-helpers
// Using `String.raw` to inject `@babel/plugin-transform-template-literals`
// helpers.
Expand Down
2 changes: 2 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 55b9711

Please sign in to comment.