Skip to content

Commit

Permalink
feat: async script module support, close #3163 (#4864)
Browse files Browse the repository at this point in the history
  • Loading branch information
patak-dev authored Sep 16, 2021
1 parent 4b90e0f commit 3984569
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 5 deletions.
22 changes: 22 additions & 0 deletions packages/playground/html/__tests__/html.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,28 @@ describe('nested w/ query', () => {
})

if (isBuild) {
describe('scriptAsync', () => {
beforeAll(async () => {
// viteTestUrl is globally injected in scripts/jestPerTestSetup.ts
await page.goto(viteTestUrl + '/scriptAsync.html')
})

test('script is async', async () => {
expect(await page.$('head script[type=module][async]')).toBeTruthy()
})
})

describe('scriptMixed', () => {
beforeAll(async () => {
// viteTestUrl is globally injected in scripts/jestPerTestSetup.ts
await page.goto(viteTestUrl + '/scriptMixed.html')
})

test('script is mixed', async () => {
expect(await page.$('head script[type=module][async]')).toBeNull()
})
})

describe('inline entry', () => {
const _countTags = (selector) => page.$$eval(selector, (t) => t.length)
const countScriptTags = _countTags.bind(this, 'script[type=module]')
Expand Down
14 changes: 14 additions & 0 deletions packages/playground/html/scriptAsync.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>scriptAsync</title>
<script async type="module" src="/main.js"></script>
<script async type="module" src="/nested/nested.js"></script>
</head>
<body>
<h1>scriptAsync.html</h1>
</body>
</html>
14 changes: 14 additions & 0 deletions packages/playground/html/scriptMixed.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>scriptMixed</title>
<script async type="module" src="/main.js"></script>
<script type="module" src="/nested/nested.js"></script>
</head>
<body>
<h1>scriptMixed.html</h1>
</body>
</html>
2 changes: 2 additions & 0 deletions packages/playground/html/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ module.exports = {
input: {
main: resolve(__dirname, 'index.html'),
nested: resolve(__dirname, 'nested/index.html'),
scriptAsync: resolve(__dirname, 'scriptAsync.html'),
scriptMixed: resolve(__dirname, 'scriptMixed.html'),
inline1: resolve(__dirname, 'inline/shared-1.html'),
inline2: resolve(__dirname, 'inline/shared-2.html'),
inline3: resolve(__dirname, 'inline/unique.html')
Expand Down
45 changes: 40 additions & 5 deletions packages/vite/src/node/plugins/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ export const assetAttrsConfig: Record<string, string[]> = {
use: ['xlink:href', 'href']
}

export const isAsyncScriptMap = new WeakMap<
ResolvedConfig,
Map<string, boolean>
>()

export async function traverseHtml(
html: string,
filePath: string,
Expand Down Expand Up @@ -105,20 +110,24 @@ export async function traverseHtml(
export function getScriptInfo(node: ElementNode): {
src: AttributeNode | undefined
isModule: boolean
isAsync: boolean
} {
let src: AttributeNode | undefined
let isModule = false
let isAsync = false
for (let i = 0; i < node.props.length; i++) {
const p = node.props[i]
if (p.type === NodeTypes.ATTRIBUTE) {
if (p.name === 'src') {
src = p
} else if (p.name === 'type' && p.value && p.value.content === 'module') {
isModule = true
} else if (p.name === 'async') {
isAsync = true
}
}
}
return { src, isModule }
return { src, isModule, isAsync }
}

function formatParseError(e: any, id: string, html: string): Error {
Expand Down Expand Up @@ -149,6 +158,10 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
return {
name: 'vite:build-html',

buildStart() {
isAsyncScriptMap.set(config, new Map())
},

async transform(html, id) {
if (id.endsWith('.html')) {
const publicPath = `/${slash(path.relative(config.root, id))}`
Expand All @@ -163,6 +176,10 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
const assetUrls: AttributeNode[] = []
let inlineModuleIndex = -1

let everyScriptIsAsync = true
let someScriptsAreAsync = false
let someScriptsAreDefer = false

await traverseHtml(html, id, (node) => {
if (node.type !== NodeTypes.ELEMENT) {
return
Expand All @@ -172,7 +189,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {

// script tags
if (node.tag === 'script') {
const { src, isModule } = getScriptInfo(node)
const { src, isModule, isAsync } = getScriptInfo(node)

const url = src && src.value && src.value.content
if (url && checkPublicFile(url, config)) {
Expand All @@ -196,6 +213,10 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
js += `\nimport "${id}?html-proxy&index=${inlineModuleIndex}.js"`
shouldRemove = true
}

everyScriptIsAsync &&= isAsync
someScriptsAreAsync ||= isAsync
someScriptsAreDefer ||= !isAsync
}
}

Expand Down Expand Up @@ -236,6 +257,14 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
}
})

isAsyncScriptMap.get(config)!.set(id, everyScriptIsAsync)

if (someScriptsAreAsync && someScriptsAreDefer) {
config.logger.warn(
`\nMixed async and defer script modules in ${id}, output script will fallback to defer. Every script, including inline ones, need to be marked as async for your output script to be async.`
)
}

// for each encountered asset url, rewrite original html so that it
// references the post-build location.
for (const attr of assetUrls) {
Expand Down Expand Up @@ -293,9 +322,13 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
return chunks
}

const toScriptTag = (chunk: OutputChunk): HtmlTagDescriptor => ({
const toScriptTag = (
chunk: OutputChunk,
isAsync: boolean
): HtmlTagDescriptor => ({
tag: 'script',
attrs: {
...(isAsync ? { async: true } : {}),
type: 'module',
crossorigin: true,
src: toPublicPath(chunk.fileName, config)
Expand Down Expand Up @@ -344,6 +377,8 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
}

for (const [id, html] of processedHtml) {
const isAsync = isAsyncScriptMap.get(config)!.get(id)!

// resolve asset url references
let result = html.replace(assetUrlRE, (_, fileHash, postfix = '') => {
return config.base + getAssetFilename(fileHash, config) + postfix
Expand Down Expand Up @@ -371,8 +406,8 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
// when inlined, discard entry chunk and inject <script> for everything in post-order
const imports = getImportedChunks(chunk)
const assetTags = canInlineEntry
? imports.map(toScriptTag)
: [toScriptTag(chunk), ...imports.map(toPreloadTag)]
? imports.map((chunk) => toScriptTag(chunk, isAsync))
: [toScriptTag(chunk, isAsync), ...imports.map(toPreloadTag)]

assetTags.push(...getCssTagsForChunk(chunk))

Expand Down

0 comments on commit 3984569

Please sign in to comment.