Skip to content

Commit

Permalink
perf(css): hoist at rules with regex (#7691)
Browse files Browse the repository at this point in the history
  • Loading branch information
bluwy committed Apr 12, 2022
1 parent 7e6a2c8 commit 8858180
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 29 deletions.
36 changes: 35 additions & 1 deletion packages/vite/src/node/__tests__/plugins/css.spec.ts
@@ -1,4 +1,4 @@
import { cssUrlRE, cssPlugin } from '../../plugins/css'
import { cssUrlRE, cssPlugin, hoistAtRules } from '../../plugins/css'
import { resolveConfig } from '../../config'
import fs from 'fs'
import path from 'path'
Expand Down Expand Up @@ -114,3 +114,37 @@ describe('css path resolutions', () => {
mockFs.mockReset()
})
})

describe('hoist @ rules', () => {
test('hoist @import', async () => {
const css = `.foo{color:red;}@import "bla";`
const result = await hoistAtRules(css)
expect(result).toBe(`@import "bla";.foo{color:red;}`)
})

test('hoist @import with semicolon in quotes', async () => {
const css = `.foo{color:red;}@import "bla;bar";`
const result = await hoistAtRules(css)
expect(result).toBe(`@import "bla;bar";.foo{color:red;}`)
})

test('hoist @charset', async () => {
const css = `.foo{color:red;}@charset "utf-8";`
const result = await hoistAtRules(css)
expect(result).toBe(`@charset "utf-8";.foo{color:red;}`)
})

test('hoist one @charset only', async () => {
const css = `.foo{color:red;}@charset "utf-8";@charset "utf-8";`
const result = await hoistAtRules(css)
expect(result).toBe(`@charset "utf-8";.foo{color:red;}`)
})

test('hoist @import and @charset', async () => {
const css = `.foo{color:red;}@import "bla";@charset "utf-8";.bar{color:grren;}@import "baz";`
const result = await hoistAtRules(css)
expect(result).toBe(
`@charset "utf-8";@import "bla";@import "baz";.foo{color:red;}.bar{color:grren;}`
)
})
})
48 changes: 20 additions & 28 deletions packages/vite/src/node/plugins/css.ts
Expand Up @@ -1106,36 +1106,28 @@ async function minifyCSS(css: string, config: ResolvedConfig) {
return code
}

// #1845
// CSS @import can only appear at top of the file. We need to hoist all @import
// to top when multiple files are concatenated.
// #6333
// CSS @charset must be the top-first in the file, hoist to top too
async function hoistAtRules(css: string) {
const postcss = await import('postcss')
return (await postcss.default([AtRuleHoistPlugin]).process(css)).css
}

const AtRuleHoistPlugin: PostCSS.PluginCreator<any> = () => {
return {
postcssPlugin: 'vite-hoist-at-rules',
Once(root) {
const imports: PostCSS.AtRule[] = []
let charset: PostCSS.AtRule | undefined
root.walkAtRules((rule) => {
if (rule.name === 'import') {
// record in reverse so that can simply prepend to preserve order
imports.unshift(rule)
} else if (!charset && rule.name === 'charset') {
charset = rule
}
})
imports.forEach((i) => root.prepend(i))
if (charset) root.prepend(charset)
export async function hoistAtRules(css: string) {
const s = new MagicString(css)
// #1845
// CSS @import can only appear at top of the file. We need to hoist all @import
// to top when multiple files are concatenated.
// match until semicolon that's not in quotes
s.replace(/@import\s*(?:"[^"]*"|'[^']*'|[^;]*).*?;/gm, (match) => {
s.appendLeft(0, match)
return ''
})
// #6333
// CSS @charset must be the top-first in the file, hoist the first to top
let foundCharset = false
s.replace(/@charset\s*(?:"[^"]*"|'[^']*'|[^;]*).*?;/gm, (match) => {
if (!foundCharset) {
s.prepend(match)
foundCharset = true
}
}
return ''
})
return s.toString()
}
AtRuleHoistPlugin.postcss = true

// Preprocessor support. This logic is largely replicated from @vue/compiler-sfc

Expand Down

0 comments on commit 8858180

Please sign in to comment.