Skip to content

Commit

Permalink
feat: css code splitting in async chunks
Browse files Browse the repository at this point in the history
close #190
  • Loading branch information
yyx990803 committed May 20, 2020
1 parent 161fe64 commit 09879b3
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 11 deletions.
8 changes: 7 additions & 1 deletion playground/TestAsync.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div class="async">This should show up...</div>
<div class="async">This should show up... and be brown</div>
<div>Loaded in {{ time }}ms.</div>
</template>

Expand All @@ -10,3 +10,9 @@ export default {
}
}
</script>

<style scoped>
.async {
color: #8B4513;
}
</style>
63 changes: 55 additions & 8 deletions src/node/build/buildPluginCss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@ import path from 'path'
import { Plugin } from 'rollup'
import { resolveAsset, registerAssets } from './buildPluginAsset'
import { loadPostcssConfig, parseWithQuery } from '../utils'
import { Transform } from '../config'
import { Transform, BuildConfig } from '../config'
import hash_sum from 'hash-sum'
import { rewriteCssUrls } from '../utils/cssUtils'

const debug = require('debug')('vite:build:css')

const urlRE = /(url\(\s*['"]?)([^"')]+)(["']?\s*\))/

const cssInjectionMarker = `__VITE_CSS__`
const cssInjectionRE = /__VITE_CSS__\(\)/g

export const createBuildCssPlugin = (
root: string,
publicBase: string,
assetsDir: string,
minify = false,
minify: BuildConfig['minify'] = false,
inlineLimit = 0,
transforms: Transform[] = []
): Plugin => {
Expand Down Expand Up @@ -98,12 +101,51 @@ export const createBuildCssPlugin = (
return {
code: modules
? `export default ${JSON.stringify(modules)}`
: '/* css extracted by vite */',
: // a fake marker to avoid the module from being tree-shaken.
// this preserves the .css file as a module in the bundle metadata
// so that we can perform chunk-based css code splitting.
// this is removed by terser during minification.
`${cssInjectionMarker}()\n`,
map: null
}
}
},

async renderChunk(code, chunk) {
// for each dynamic entry chunk, collect its css and inline it as JS
// strings.
if (chunk.isDynamicEntry) {
let chunkCSS = ''
for (const id in chunk.modules) {
if (styles.has(id)) {
chunkCSS += styles.get(id)
styles.delete(id) // remove inlined css
}
}
chunkCSS = await minifyCSS(chunkCSS)
let isFirst = true
code = code.replace(cssInjectionRE, () => {
if (isFirst) {
isFirst = false
// make sure the code is in one line so that source map is preserved.
return (
`let ${cssInjectionMarker} = document.createElement('style');` +
`${cssInjectionMarker}.innerHTML = ${JSON.stringify(chunkCSS)};` +
`document.head.appendChild(${cssInjectionMarker});`
)
} else {
return ''
}
})
} else {
code = code.replace(cssInjectionRE, '')
}
return {
code,
map: null
}
},

async generateBundle(_options, bundle) {
let css = ''
// finalize extracted css
Expand All @@ -112,11 +154,7 @@ export const createBuildCssPlugin = (
})
// minify with cssnano
if (minify) {
css = (
await require('postcss')([require('cssnano')]).process(css, {
from: undefined
})
).css
css = await minifyCSS(css)
}

const cssFileName = `style.${hash_sum(css)}.css`
Expand All @@ -132,3 +170,12 @@ export const createBuildCssPlugin = (
}
}
}

let postcss: any
let cssnano: any

async function minifyCSS(css: string) {
postcss = postcss || require('postcss')
cssnano = cssnano || require('cssnano')
return (await postcss(cssnano).process(css, { from: undefined })).css
}
4 changes: 2 additions & 2 deletions src/node/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ export async function build(options: BuildConfig = {}): Promise<BuildResult> {
root,
publicBasePath,
assetsDir,
!!minify,
minify,
assetsInlineLimit,
transforms
),
Expand All @@ -249,7 +249,7 @@ export async function build(options: BuildConfig = {}): Promise<BuildResult> {
format: 'es',
sourcemap,
entryFileNames: `[name].[hash].js`,
chunkFileNames: `common.[hash].js`,
chunkFileNames: `[name].[hash].js`,
...rollupOutputOptions
})

Expand Down
7 changes: 7 additions & 0 deletions src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@ export interface BuildConfig extends SharedConfig {
* @default 4096
*/
assetsInlineLimit?: number
/**
* Whether to code-split CSS. When enabled, CSS in async chunks will be
* inlined as strings in the chunk and inserted via dynamically created
* style tags when the chunk is loaded.
* @default true
*/
cssCodeSplit?: boolean
/**
* Whether to generate sourcemap
* @default false
Expand Down
24 changes: 24 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ describe('vite', () => {

test('async component', async () => {
await expectByPolling(() => getText('.async'), 'should show up')
expect(await getComputedColor('.async')).toBe('rgb(139, 69, 19)')
})
}

Expand Down Expand Up @@ -379,6 +380,29 @@ describe('vite', () => {

declareTests(true)
})

test('css codesplit in async chunks', async () => {
const colorToMatch = /#8B4513/i // from TestAsync.vue

const files = await fs.readdir(path.join(tempDir, 'dist/_assets'))
const cssFile = files.find((f) => f.endsWith('.css'))
const css = await fs.readFile(
path.join(tempDir, 'dist/_assets', cssFile),
'utf-8'
)
// should be extracted from the main css file
expect(css).not.toMatch(colorToMatch)
// should be inside the split chunk file
const asyncChunk = files.find(
(f) => f.startsWith('TestAsync') && f.endsWith('.js')
)
const code = await fs.readFile(
path.join(tempDir, 'dist/_assets', asyncChunk),
'utf-8'
)
// should be inside the async chunk
expect(code).toMatch(colorToMatch)
})
})

describe('dev', () => {
Expand Down

0 comments on commit 09879b3

Please sign in to comment.