From 88581807cce75bbb104ef4c1cb3dd483b748d3fd Mon Sep 17 00:00:00 2001 From: Bjorn Lu Date: Tue, 12 Apr 2022 21:52:50 +0800 Subject: [PATCH] perf(css): hoist at rules with regex (#7691) --- .../src/node/__tests__/plugins/css.spec.ts | 36 +++++++++++++- packages/vite/src/node/plugins/css.ts | 48 ++++++++----------- 2 files changed, 55 insertions(+), 29 deletions(-) diff --git a/packages/vite/src/node/__tests__/plugins/css.spec.ts b/packages/vite/src/node/__tests__/plugins/css.spec.ts index 539ec2f1af1810..9b652a563ccb0a 100644 --- a/packages/vite/src/node/__tests__/plugins/css.spec.ts +++ b/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' @@ -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;}` + ) + }) +}) diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index adef254950c5e5..08bdfbeed4e616 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -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 = () => { - 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