Skip to content

Commit

Permalink
feat(html): Inline entry chunk when possible (#4555)
Browse files Browse the repository at this point in the history
  • Loading branch information
andylizi committed Sep 13, 2021
1 parent b61b044 commit e687d98
Show file tree
Hide file tree
Showing 13 changed files with 141 additions and 28 deletions.
34 changes: 34 additions & 0 deletions packages/playground/html/__tests__/html.spec.ts
Expand Up @@ -94,3 +94,37 @@ describe('nested w/ query', () => {

testPage(true)
})

if (isBuild) {
describe('inline entry', () => {
const _countTags = (selector) => page.$$eval(selector, (t) => t.length)
const countScriptTags = _countTags.bind(this, 'script[type=module]')
const countPreloadTags = _countTags.bind(this, 'link[rel=modulepreload]')

test('is inlined', async () => {
await page.goto(viteTestUrl + '/inline/shared-1.html?v=1')
expect(await countScriptTags()).toBeGreaterThan(1)
expect(await countPreloadTags()).toBe(0)
})

test('is not inlined', async () => {
await page.goto(viteTestUrl + '/inline/unique.html?v=1')
expect(await countScriptTags()).toBe(1)
expect(await countPreloadTags()).toBeGreaterThan(0)
})

test('execution order when inlined', async () => {
await page.goto(viteTestUrl + '/inline/shared-2.html?v=1')
expect((await page.textContent('#output')).trim()).toBe(
'dep1 common dep2 dep3 shared'
)
})

test('execution order when not inlined', async () => {
await page.goto(viteTestUrl + '/inline/unique.html?v=1')
expect((await page.textContent('#output')).trim()).toBe(
'dep1 common dep2 unique'
)
})
})
}
8 changes: 8 additions & 0 deletions packages/playground/html/inline/common.js
@@ -0,0 +1,8 @@
import './dep1'
import './dep2'

export function log(name) {
document.getElementById('output').innerHTML += name + ' '
}

log('common')
3 changes: 3 additions & 0 deletions packages/playground/html/inline/dep1.js
@@ -0,0 +1,3 @@
import { log } from './common'

log('dep1')
3 changes: 3 additions & 0 deletions packages/playground/html/inline/dep2.js
@@ -0,0 +1,3 @@
import { log } from './common'

log('dep2')
4 changes: 4 additions & 0 deletions packages/playground/html/inline/dep3.js
@@ -0,0 +1,4 @@
import './dep2'
import { log } from './common'

log('dep3')
16 changes: 16 additions & 0 deletions packages/playground/html/inline/module-graph.dot
@@ -0,0 +1,16 @@
digraph Module {
common -> { dep1, dep2 } [style=dashed,color=grey]
dep1 -> common
dep2 -> common
dep3 -> { dep2, common }

subgraph shared {
shared [style=filled]
shared -> { dep3, common }
}

subgraph unique {
unique [style=filled]
unique -> { common, dep2 }
}
}
2 changes: 2 additions & 0 deletions packages/playground/html/inline/shared-1.html
@@ -0,0 +1,2 @@
<pre id="output"></pre>
<script type="module" src="./shared.js"></script>
2 changes: 2 additions & 0 deletions packages/playground/html/inline/shared-2.html
@@ -0,0 +1,2 @@
<pre id="output"></pre>
<script type="module" src="./shared.js"></script>
4 changes: 4 additions & 0 deletions packages/playground/html/inline/shared.js
@@ -0,0 +1,4 @@
import './dep3'
import { log } from './common'

log('shared')
2 changes: 2 additions & 0 deletions packages/playground/html/inline/unique.html
@@ -0,0 +1,2 @@
<pre id="output"></pre>
<script type="module" src="./unique.js"></script>
4 changes: 4 additions & 0 deletions packages/playground/html/inline/unique.js
@@ -0,0 +1,4 @@
import { log } from './common'
import './dep2'

log('unique')
5 changes: 4 additions & 1 deletion packages/playground/html/vite.config.js
Expand Up @@ -8,7 +8,10 @@ module.exports = {
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
nested: resolve(__dirname, 'nested/index.html')
nested: resolve(__dirname, 'nested/index.html'),
inline1: resolve(__dirname, 'inline/shared-1.html'),
inline2: resolve(__dirname, 'inline/shared-2.html'),
inline3: resolve(__dirname, 'inline/unique.html'),
}
}
},
Expand Down
82 changes: 55 additions & 27 deletions packages/vite/src/node/plugins/html.ts
Expand Up @@ -273,30 +273,43 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
}
},

async generateBundle(_, bundle) {
async generateBundle(options, bundle) {
const analyzedChunk: Map<OutputChunk, number> = new Map()
const getPreloadLinksForChunk = (
const getImportedChunks = (
chunk: OutputChunk,
seen: Set<string> = new Set()
): HtmlTagDescriptor[] => {
const tags: HtmlTagDescriptor[] = []
): OutputChunk[] => {
const chunks: OutputChunk[] = []
chunk.imports.forEach((file) => {
const importee = bundle[file]
if (importee?.type === 'chunk' && !seen.has(file)) {
seen.add(file)
tags.push({
tag: 'link',
attrs: {
rel: 'modulepreload',
href: toPublicPath(file, config)
}
})
tags.push(...getPreloadLinksForChunk(importee, seen))

// post-order traversal
chunks.push(...getImportedChunks(importee, seen))
chunks.push(importee)
}
})
return tags
return chunks
}

const toScriptTag = (chunk: OutputChunk): HtmlTagDescriptor => ({
tag: 'script',
attrs: {
type: 'module',
crossorigin: true,
src: toPublicPath(chunk.fileName, config)
}
})

const toPreloadTag = (chunk: OutputChunk): HtmlTagDescriptor => ({
tag: 'link',
attrs: {
rel: 'modulepreload',
href: toPublicPath(chunk.fileName, config)
}
})

const getCssTagsForChunk = (
chunk: OutputChunk,
seen: Set<string> = new Set()
Expand Down Expand Up @@ -343,23 +356,25 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
chunk.isEntry &&
chunk.facadeModuleId === id
) as OutputChunk | undefined
let canInlineEntry = false

// inject chunk asset links
if (chunk) {
const assetTags = [
// js entry chunk for this page
{
tag: 'script',
attrs: {
type: 'module',
crossorigin: true,
src: toPublicPath(chunk.fileName, config)
}
},
// preload for imports
...getPreloadLinksForChunk(chunk),
...getCssTagsForChunk(chunk)
]
// an entry chunk can be inlined if
// - it's an ES module (e.g. not generated by the legacy plugin)
// - it contains no meaningful code other than import statments
if (options.format === 'es' && isEntirelyImport(chunk.code)) {
canInlineEntry = true
}

// when not inlined, inject <script> for entry and modulepreload its dependencies
// 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)]

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

result = injectToHead(result, assetTags)
}
Expand Down Expand Up @@ -390,6 +405,11 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
chunk
})

if (chunk && canInlineEntry) {
// all imports from entry have been inlined to html, prevent rollup from outputting it
delete bundle[chunk.fileName]
}

this.emitFile({
type: 'asset',
fileName: shortEmitName,
Expand Down Expand Up @@ -523,6 +543,14 @@ export async function applyHtmlTransforms(
return html
}

const importRE = /\bimport\s*("[^"]*[^\\]"|'[^']*[^\\]');*/g
const commentRE = /\/\*[\s\S]*?\*\/|\/\/.*$/gm
function isEntirelyImport(code: string) {
// only consider "side-effect" imports, which match <script type=module> semantics exactly
// the regexes will remove too little in some exotic cases, but false-negatives are alright
return !code.replace(importRE, '').replace(commentRE, '').trim().length
}

function toPublicPath(filename: string, config: ResolvedConfig) {
return isExternalUrl(filename) ? filename : config.base + filename
}
Expand Down

0 comments on commit e687d98

Please sign in to comment.