Skip to content

Commit

Permalink
fix(css): clean comments before hoist at rules (#7924)
Browse files Browse the repository at this point in the history
  • Loading branch information
bluwy committed Apr 27, 2022
1 parent 5c1ee5a commit e48827f
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 21 deletions.
48 changes: 46 additions & 2 deletions packages/vite/src/node/__tests__/plugins/css.spec.ts
Expand Up @@ -155,10 +155,54 @@ describe('hoist @ rules', () => {
})

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

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

test('dont hoist @charset in comments', 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;}/* @charset "utf-8"; */`
)
})

test('dont hoist @import and @charset in comments', async () => {
const css = `
.foo{color:red;}
/*
@import "bla";
*/
@charset "utf-8";
/*
@charset "utf-8";
@import "bar";
*/
@import "baz";
`
const result = await hoistAtRules(css)
expect(result).toBe(
`@charset "utf-8";@import "baz";
.foo{color:red;}
/*
@import "bla";
*/
/*
@charset "utf-8";
@import "bar";
*/
`
)
})
})
14 changes: 11 additions & 3 deletions packages/vite/src/node/cleanString.ts
@@ -1,10 +1,14 @@
import type { RollupError } from 'rollup'
import { multilineCommentsRE, singlelineCommentsRE } from './utils'

// bank on the non-overlapping nature of regex matches and combine all filters into one giant regex
// /`([^`\$\{\}]|\$\{(`|\g<1>)*\})*`/g can match nested string template
// but js not support match expression(\g<0>). so clean string template(`...`) in other ways.
const stringsRE = /"([^"\r\n]|(?<=\\)")*"|'([^'\r\n]|(?<=\\)')*'/.source
const commentsRE = /\/\*(.|[\r\n])*?\*\/|\/\/.*/.source
const cleanerRE = new RegExp(`${stringsRE}|${commentsRE}`, 'g')
const stringsRE = /"([^"\r\n]|(?<=\\)")*"|'([^'\r\n]|(?<=\\)')*'/g
const cleanerRE = new RegExp(
`${stringsRE.source}|${multilineCommentsRE.source}|${singlelineCommentsRE.source}`,
'g'
)

const blankReplacer = (s: string) => ' '.repeat(s.length)
const stringBlankReplacer = (s: string) =>
Expand All @@ -26,6 +30,10 @@ export function emptyString(raw: string): string {
return res
}

export function emptyCssComments(raw: string) {
return raw.replace(multilineCommentsRE, blankReplacer)
}

const enum LexerState {
// template string
inTemplateString,
Expand Down
37 changes: 21 additions & 16 deletions packages/vite/src/node/plugins/css.ts
Expand Up @@ -48,6 +48,7 @@ import { transform, formatMessages } from 'esbuild'
import { addToHTMLProxyTransformResult } from './html'
import { injectSourcesContent, getCodeWithSourcemap } from '../server/sourcemap'
import type { RawSourceMap } from '@ampproject/remapping'
import { emptyCssComments } from '../cleanString'

// const debug = createDebugger('vite:css')

Expand Down Expand Up @@ -1117,30 +1118,34 @@ async function minifyCSS(css: string, config: ResolvedConfig) {

export async function hoistAtRules(css: string) {
const s = new MagicString(css)
const cleanCss = emptyCssComments(css)
let match: RegExpExecArray | null

// #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*(?:url\([^\)]*\)|"([^"]|(?<=\\)")*"|'([^']|(?<=\\)')*'|[^;]*).*?;/gm,
(match) => {
s.appendLeft(0, match)
return ''
}
)
const atImportRE =
/@import\s*(?:url\([^\)]*\)|"([^"]|(?<=\\)")*"|'([^']|(?<=\\)')*'|[^;]*).*?;/gm
while ((match = atImportRE.exec(cleanCss))) {
s.remove(match.index, match.index + match[0].length)
// Use `appendLeft` instead of `prepend` to preserve original @import order
s.appendLeft(0, match[0])
}

// #6333
// CSS @charset must be the top-first in the file, hoist the first to top
const atCharsetRE =
/@charset\s*(?:"([^"]|(?<=\\)")*"|'([^']|(?<=\\)')*'|[^;]*).*?;/gm
let foundCharset = false
s.replace(
/@charset\s*(?:"([^"]|(?<=\\)")*"|'([^']|(?<=\\)')*'|[^;]*).*?;/gm,
(match) => {
if (!foundCharset) {
s.prepend(match)
foundCharset = true
}
return ''
while ((match = atCharsetRE.exec(cleanCss))) {
s.remove(match.index, match.index + match[0].length)
if (!foundCharset) {
s.prepend(match[0])
foundCharset = true
}
)
}

return s.toString()
}

Expand Down

0 comments on commit e48827f

Please sign in to comment.