Skip to content

Commit

Permalink
fix(webpack): escape special chars in robust way (#3570)
Browse files Browse the repository at this point in the history
  • Loading branch information
xc2 committed Mar 14, 2024
1 parent ad1bd4b commit e1a8c5e
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 14 deletions.
29 changes: 27 additions & 2 deletions packages/shared-integration/src/layers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,36 @@ export function resolveLayer(id: string) {
return match[1] || LAYER_MARK_ALL
}

export const LAYER_PLACEHOLDER_RE = /(\\?")?#--unocss--\s*{\s*layer\s*:\s*(.+?);?\s*}/g
/**
* 1 - layer
* 2 - escape-view
*/
// 111 222
export const LAYER_PLACEHOLDER_RE = /#--unocss--\s*{\s*layer\s*:\s*(.+?)\s*(?:;\s*escape-view\s*:\s*(.+?)\s*)?;?\s*}/g
export function getLayerPlaceholder(layer: string) {
return `#--unocss--{layer:${layer}}`
// escape view is to determine how many backslashes will be prepended to special symbols in this scope.
return `#--unocss--{layer:${layer};escape-view:\\"\\'\\\`\\\\}`
}

export function getCssEscaperForJsContent(view: string) {
if (!view)
return (css: string) => css

const prefix: Record<string, string> = {}
/**
* 1 - backslashes before special char
* 2 - special char
*/
// 111 2222222
const escapeViewRe = /(\\*)\\(["'`\\])/g
view.trim().replace(escapeViewRe, (_, bs, char) => {
prefix[char] = bs
return ''
})
return (css: string) => css.replace(/["'`\\]/g, (v) => {
return (prefix[v] || '') + v
})
}
export const HASH_PLACEHOLDER_RE = /#--unocss-hash--\s*{\s*content\s*:\s*\\*"(.+?)\\*";?\s*}/g
export function getHashPlaceholder(hash: string) {
return `#--unocss-hash--{content:"${hash}"}`
Expand Down
4 changes: 2 additions & 2 deletions packages/vite/src/modes/global/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ export function GlobalModeBuildPlugin(ctx: UnocssPluginContext<VitePluginConfig>
if (chunk.type === 'asset' && typeof chunk.source === 'string') {
const css = chunk.source
.replace(HASH_PLACEHOLDER_RE, '')
chunk.source = await replaceAsync(css, LAYER_PLACEHOLDER_RE, async (_, __, layer) => {
chunk.source = await replaceAsync(css, LAYER_PLACEHOLDER_RE, async (_, layer) => {
replaced = true
return getLayer(layer, css)
})
Expand All @@ -284,7 +284,7 @@ export function GlobalModeBuildPlugin(ctx: UnocssPluginContext<VitePluginConfig>
else if (chunk.type === 'chunk' && typeof chunk.code === 'string') {
const js = chunk.code
.replace(HASH_PLACEHOLDER_RE, '')
chunk.code = await replaceAsync(js, LAYER_PLACEHOLDER_RE, async (_, __, layer) => {
chunk.code = await replaceAsync(js, LAYER_PLACEHOLDER_RE, async (_, layer) => {
replaced = true
const css = getLayer(layer, js)
return css
Expand Down
24 changes: 14 additions & 10 deletions packages/webpack/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,17 @@ import WebpackSources from 'webpack-sources'
import { createContext } from '../../shared-integration/src/context'
import { setupContentExtractor } from '../../shared-integration/src/content'
import { getHash } from '../../shared-integration/src/hash'
import { HASH_PLACEHOLDER_RE, LAYER_MARK_ALL, LAYER_PLACEHOLDER_RE, RESOLVED_ID_RE, getHashPlaceholder, getLayerPlaceholder, resolveId, resolveLayer } from '../../shared-integration/src/layers'
import {
HASH_PLACEHOLDER_RE,
LAYER_MARK_ALL,
LAYER_PLACEHOLDER_RE,
RESOLVED_ID_RE,
getCssEscaperForJsContent,
getHashPlaceholder,
getLayerPlaceholder,
resolveId,
resolveLayer,
} from '../../shared-integration/src/layers'
import { applyTransformers } from '../../shared-integration/src/transformers'
import { getPath, isCssId } from '../../shared-integration/src/utils'

Expand Down Expand Up @@ -122,22 +132,16 @@ export default function WebpackPlugin<Theme extends object>(
let code = compilation.assets[file].source().toString()
let replaced = false
code = code.replace(HASH_PLACEHOLDER_RE, '')
code = code.replace(LAYER_PLACEHOLDER_RE, (_, quote, layer) => {
code = code.replace(LAYER_PLACEHOLDER_RE, (_, layer, escapeView) => {
replaced = true
const css = layer === LAYER_MARK_ALL
? result.getLayers(undefined, Array.from(entries)
.map(i => resolveLayer(i)).filter((i): i is string => !!i))
: (result.getLayer(layer) || '')

if (!quote)
return css
const escapeCss = getCssEscaperForJsContent(escapeView)

// the css is in a js file, escaping
let escaped = JSON.stringify(css).slice(1, -1)
// in `eval()`, escaping twice
if (quote === '\\"')
escaped = JSON.stringify(escaped).slice(1, -1)
return quote + escaped
return escapeCss(css)
})
if (replaced)
compilation.assets[file] = new WebpackSources.RawSource(code) as any
Expand Down

0 comments on commit e1a8c5e

Please sign in to comment.