From 1f12f766d7d8e24cd38d74f1cc2bada1cfaeade2 Mon Sep 17 00:00:00 2001 From: bluwy Date: Tue, 12 Apr 2022 14:44:29 +0800 Subject: [PATCH 1/3] perf(css): hoist at rules with regex --- .../src/node/__tests__/plugins/css.spec.ts | 30 +++++++++++- packages/vite/src/node/plugins/css.ts | 47 ++++++++----------- 2 files changed, 48 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..65a0b53183fa3f 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,31 @@ 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 @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..d64a38af81e658 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -1106,36 +1106,27 @@ 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. + s.replace(/@import.*?;/g, (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.*?;/g, (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 From d3886a427302c4f0dcce9e15b4f9407ff29c1d7b Mon Sep 17 00:00:00 2001 From: bluwy Date: Tue, 12 Apr 2022 15:00:50 +0800 Subject: [PATCH 2/3] fix: handle semicolon in quotes --- packages/vite/src/node/__tests__/plugins/css.spec.ts | 6 ++++++ packages/vite/src/node/plugins/css.ts | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/__tests__/plugins/css.spec.ts b/packages/vite/src/node/__tests__/plugins/css.spec.ts index 65a0b53183fa3f..18beb39eaf1a8b 100644 --- a/packages/vite/src/node/__tests__/plugins/css.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/css.spec.ts @@ -122,6 +122,12 @@ describe('hoist @ rules', () => { expect(result).toBe(`@import "bla";.foo{color:red;}`) }) + test('hoist @import with semicolon in rl', 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) diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index d64a38af81e658..08bdfbeed4e616 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -1111,14 +1111,15 @@ export async function hoistAtRules(css: string) { // #1845 // CSS @import can only appear at top of the file. We need to hoist all @import // to top when multiple files are concatenated. - s.replace(/@import.*?;/g, (match) => { + // 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.*?;/g, (match) => { + s.replace(/@charset\s*(?:"[^"]*"|'[^']*'|[^;]*).*?;/gm, (match) => { if (!foundCharset) { s.prepend(match) foundCharset = true From a31e1acc459c87ad77cbac7ca94d5c11a8fb17fa Mon Sep 17 00:00:00 2001 From: bluwy Date: Tue, 12 Apr 2022 15:02:57 +0800 Subject: [PATCH 3/3] chore: fix typo --- packages/vite/src/node/__tests__/plugins/css.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/__tests__/plugins/css.spec.ts b/packages/vite/src/node/__tests__/plugins/css.spec.ts index 18beb39eaf1a8b..9b652a563ccb0a 100644 --- a/packages/vite/src/node/__tests__/plugins/css.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/css.spec.ts @@ -122,7 +122,7 @@ describe('hoist @ rules', () => { expect(result).toBe(`@import "bla";.foo{color:red;}`) }) - test('hoist @import with semicolon in rl', async () => { + 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;}`)