diff --git a/packages/vite/src/node/__tests__/cleanString.spec.ts b/packages/vite/src/node/__tests__/cleanString.spec.ts
index 883dc67abfe2f9..1065a2d4985ceb 100644
--- a/packages/vite/src/node/__tests__/cleanString.spec.ts
+++ b/packages/vite/src/node/__tests__/cleanString.spec.ts
@@ -106,3 +106,55 @@ test('find empty string flag in raw index', () => {
const bStart = clean.indexOf('\0\0\0\0\0', bIndex)
expect(str.slice(bStart, bStart + 5)).toMatch('bbbbb')
})
+
+test('template string nested', () => {
+ let str = '`aaaa`'
+ let res = '`\0\0\0\0`'
+ let clean = emptyString(str)
+ expect(clean).toMatch(res)
+
+ str = '`aaaa` `aaaa`'
+ res = '`\0\0\0\0` `\0\0\0\0`'
+ clean = emptyString(str)
+ expect(clean).toMatch(res)
+
+ str = '`aa${a}aa`'
+ res = '`\0\0${a}\0\0`'
+ clean = emptyString(str)
+ expect(clean).toMatch(res)
+
+ str = '`aa${a + `a` + a}aa`'
+ res = '`\0\0${a + `\0` + a}\0\0`'
+ clean = emptyString(str)
+ expect(clean).toMatch(res)
+
+ str = '`aa${a + `a` + a}aa` `aa${a + `a` + a}aa`'
+ res = '`\0\0${a + `\0` + a}\0\0` `\0\0${a + `\0` + a}\0\0`'
+ clean = emptyString(str)
+ expect(clean).toMatch(res)
+
+ str = '`aa${a + `aaaa${c + (a = {b: 1}) + d}` + a}aa`'
+ res = '`\0\0${a + `\0\0\0\0${c + (a = {b: 1}) + d}` + a}\0\0`'
+ clean = emptyString(str)
+ expect(clean).toMatch(res)
+
+ str =
+ '`aa${a + `aaaa${c + (a = {b: 1}) + d}` + a}aa` `aa${a + `aaaa${c + (a = {b: 1}) + d}` + a}aa`'
+ res =
+ '`\0\0${a + `\0\0\0\0${c + (a = {b: 1}) + d}` + a}\0\0` `\0\0${a + `\0\0\0\0${c + (a = {b: 1}) + d}` + a}\0\0`'
+ clean = emptyString(str)
+ expect(clean).toMatch(res)
+
+ str = '`aaaa'
+ res = ''
+ try {
+ clean = emptyString(str)
+ } catch {}
+ expect(clean).toMatch(res)
+
+ str =
+ ""
+ res = ``
+ clean = emptyString(str)
+ expect(clean).toMatch(res)
+})
diff --git a/packages/vite/src/node/cleanString.ts b/packages/vite/src/node/cleanString.ts
index d26274397124ff..05163ea055b631 100644
--- a/packages/vite/src/node/cleanString.ts
+++ b/packages/vite/src/node/cleanString.ts
@@ -1,3 +1,4 @@
+import type { RollupError } from 'rollup'
// 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.
@@ -8,7 +9,117 @@ const stringBlankReplacer = (s: string) =>
`${s[0]}${'\0'.repeat(s.length - 2)}${s[0]}`
export function emptyString(raw: string): string {
- return raw.replace(cleanerRE, (s: string) =>
+ let res = raw.replace(cleanerRE, (s: string) =>
s[0] === '/' ? blankReplacer(s) : stringBlankReplacer(s)
)
+
+ let lastEnd = 0
+ let start = 0
+ while ((start = res.indexOf('`', lastEnd)) >= 0) {
+ let clean
+ ;[clean, lastEnd] = lexStringTemplateExpression(res, start)
+ res = replaceAt(res, start, lastEnd, clean)
+ }
+
+ return res
+}
+
+const enum LexerState {
+ inTemplateString,
+ inInterpolationExpression,
+ inObjectExpression
+}
+
+function replaceAt(
+ string: string,
+ start: number,
+ end: number,
+ replacement: string
+): string {
+ return string.slice(0, start) + replacement + string.slice(end)
+}
+
+/**
+ * lex string template and clean it.
+ */
+function lexStringTemplateExpression(
+ code: string,
+ start: number
+): [string, number] {
+ let state = LexerState.inTemplateString as LexerState
+ let clean = '`'
+ const opStack: LexerState[] = [state]
+
+ function pushStack(newState: LexerState) {
+ state = newState
+ opStack.push(state)
+ }
+
+ function popStack() {
+ opStack.pop()
+ state = opStack[opStack.length - 1]
+ }
+
+ let i = start + 1
+ outer: for (; i < code.length; i++) {
+ const char = code.charAt(i)
+ switch (state) {
+ case LexerState.inTemplateString:
+ if (char === '$' && code.charAt(i + 1) === '{') {
+ pushStack(LexerState.inInterpolationExpression)
+ clean += '${'
+ i++ // jump next
+ } else if (char === '`') {
+ popStack()
+ clean += char
+ if (opStack.length === 0) {
+ break outer
+ }
+ } else {
+ clean += '\0'
+ }
+ break
+ case LexerState.inInterpolationExpression:
+ if (char === '{') {
+ pushStack(LexerState.inObjectExpression)
+ clean += char
+ } else if (char === '}') {
+ popStack()
+ clean += char
+ } else if (char === '`') {
+ pushStack(LexerState.inTemplateString)
+ clean += char
+ } else {
+ clean += char
+ }
+ break
+ case LexerState.inObjectExpression:
+ if (char === '}') {
+ popStack()
+ clean += char
+ } else if (char === '`') {
+ pushStack(LexerState.inTemplateString)
+ clean += char
+ } else {
+ clean += char
+ }
+ break
+ default:
+ throw new Error('unknown string template lexer state')
+ }
+ }
+
+ if (opStack.length !== 0) {
+ error(start)
+ }
+
+ return [clean, i + 1]
+}
+
+function error(pos: number) {
+ const err = new Error(
+ `can not match string template expression.`
+ ) as RollupError
+ err.pos = pos
+ throw err
}