Skip to content
6 changes: 6 additions & 0 deletions packages/vite/src/node/__tests__/plugins/css.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ describe('search css url function', () => {
),
).toBe(true)
})

test('should capture the full url with escaped parentheses', () => {
const css = 'background-image: url(public/awkward-name\\)2.png);'
const match = cssUrlRE.exec(css)
expect(match?.[1].trim()).toBe('public/awkward-name\\)2.png')
})
})

describe('css modules', () => {
Expand Down
9 changes: 6 additions & 3 deletions packages/vite/src/node/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1896,7 +1896,7 @@ type CssUrlReplacer = (
) => string | false | Promise<string | false>
// https://drafts.csswg.org/css-syntax-3/#identifier-code-point
export const cssUrlRE =
/(?<!@import\s+)(?<=^|[^\w\-\u0080-\uffff])url\((\s*('[^']+'|"[^"]+")\s*|[^'")]+)\)/
/(?<!@import\s+)(?<=^|[^\w\-\u0080-\uffff])url\((\s*('[^']+'|"[^"]+")\s*|(?:\\.|[^'")\\])+)\)/
export const cssDataUriRE =
/(?<=^|[^\w\-\u0080-\uffff])data-uri\((\s*('[^']+'|"[^"]+")\s*|[^'")]+)\)/
export const importCssRE =
Expand Down Expand Up @@ -2049,14 +2049,17 @@ async function doUrlReplace(
if (skipUrlReplacer(unquotedUrl)) {
return matched
}
// Remove escape sequences to get the actual file name before resolving.
unquotedUrl = unquotedUrl.replace(/\\(\W)/g, '$1')

let newUrl = await replacer(unquotedUrl, rawUrl)
if (newUrl === false) {
return matched
}

// The new url might need wrapping even if the original did not have it, e.g. if a space was added during replacement
if (wrap === '' && newUrl !== encodeURI(newUrl)) {
// The new url might need wrapping even if the original did not have it, e.g.
// if a space was added during replacement or the URL contains ")"
if (wrap === '' && (newUrl !== encodeURI(newUrl) || newUrl.includes(')'))) {
wrap = '"'
}
// If wrapping in single quotes and newUrl also contains single quotes, switch to double quotes.
Expand Down