Skip to content

Commit 93900f0

Browse files
authored
fix: nested comments and strings, new regexp utils (#7650)
1 parent eb57627 commit 93900f0

File tree

6 files changed

+151
-50
lines changed

6 files changed

+151
-50
lines changed

CONTRIBUTING.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ Each test can be run under either dev server mode or build mode.
7979

8080
- `pnpm run test-build` runs tests only under build mode.
8181

82-
- You can also use `pnpm run test-serve -- [match]` or `pnpm run test-build -- [match]` to run tests in a specific playground package, e.g. `pnpm run test-serve -- css` will run tests for both `playground/css` and `playground/css-codesplit` under serve mode.
82+
- You can also use `pnpm run test-serve -- [match]` or `pnpm run test-build -- [match]` to run tests in a specific playground package, e.g. `pnpm run test-serve -- asset` will run tests for both `playground/asset` and `vite/src/node/__tests__/asset` under serve mode and `vite/src/node/__tests__/**/*` just run in serve mode.
8383

8484
Note package matching is not available for the `pnpm test` script, which always runs all tests.
8585

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { emptyString } from '../../node/cleanString'
2+
3+
test('comments', () => {
4+
expect(
5+
emptyString(`
6+
// comment1 // comment
7+
// comment1
8+
/* coment2 */
9+
/*
10+
// coment3
11+
*/
12+
/* // coment3 */
13+
/* // coment3 */ // comment
14+
// comment 4 /* comment 5 */
15+
`).trim()
16+
).toBe('')
17+
})
18+
19+
test('strings', () => {
20+
const clean = emptyString(`
21+
// comment1
22+
const a = 'aaaa'
23+
/* coment2 */
24+
const b = "bbbb"
25+
/*
26+
// coment3
27+
*/
28+
/* // coment3 */
29+
// comment 4 /* comment 5 */
30+
`)
31+
expect(clean).toMatch("const a = '\0\0\0\0'")
32+
expect(clean).toMatch('const b = "\0\0\0\0"')
33+
})
34+
35+
test('strings comment nested', () => {
36+
expect(
37+
emptyString(`
38+
// comment 1 /* " */
39+
const a = "a //"
40+
// comment 2 /* " */
41+
`)
42+
).toMatch('const a = "\0\0\0\0"')
43+
44+
expect(
45+
emptyString(`
46+
// comment 1 /* ' */
47+
const a = "a //"
48+
// comment 2 /* ' */
49+
`)
50+
).toMatch('const a = "\0\0\0\0"')
51+
52+
expect(
53+
emptyString(`
54+
// comment 1 /* \` */
55+
const a = "a //"
56+
// comment 2 /* \` */
57+
`)
58+
).toMatch('const a = "\0\0\0\0"')
59+
60+
expect(
61+
emptyString(`
62+
const a = "a //"
63+
console.log("console")
64+
`)
65+
).toMatch('const a = "\0\0\0\0"')
66+
67+
expect(
68+
emptyString(`
69+
const a = "a /*"
70+
console.log("console")
71+
const b = "b */"
72+
`)
73+
).toMatch('const a = "\0\0\0\0"')
74+
75+
expect(
76+
emptyString(`
77+
const a = "a ' "
78+
console.log("console")
79+
const b = "b ' "
80+
`)
81+
).toMatch('const a = "\0\0\0\0"')
82+
83+
expect(
84+
emptyString(`
85+
const a = "a \` "
86+
console.log("console")
87+
const b = "b \` "
88+
`)
89+
).toMatch('const a = "\0\0\0\0"')
90+
})
91+
92+
test('find empty string flag in raw index', () => {
93+
const str = `
94+
const a = "aaaaa"
95+
const b = "bbbbb"
96+
`
97+
const clean = emptyString(str)
98+
expect(clean).toMatch('const a = "\0\0\0\0\0"')
99+
expect(clean).toMatch('const b = "\0\0\0\0\0"')
100+
101+
const aIndex = str.indexOf('const a = "aaaaa"')
102+
const aStart = clean.indexOf('\0\0\0\0\0', aIndex)
103+
expect(str.slice(aStart, aStart + 5)).toMatch('aaaaa')
104+
105+
const bIndex = str.indexOf('const b = "bbbbb"')
106+
const bStart = clean.indexOf('\0\0\0\0\0', bIndex)
107+
expect(str.slice(bStart, bStart + 5)).toMatch('bbbbb')
108+
})

packages/vite/src/node/cleanString.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// bank on the non-overlapping nature of regex matches and combine all filters into one giant regex
2+
// /`([^`\$\{\}]|\$\{(`|\g<1>)*\})*`/g can match nested string template
3+
// but js not support match expression(\g<0>). so clean string template(`...`) in other ways.
4+
const cleanerRE = /"[^"]*"|'[^']*'|\/\*(.|[\r\n])*?\*\/|\/\/.*/g
5+
6+
const blankReplacer = (s: string) => ' '.repeat(s.length)
7+
const stringBlankReplacer = (s: string) =>
8+
`${s[0]}${'\0'.repeat(s.length - 2)}${s[0]}`
9+
10+
export function emptyString(raw: string): string {
11+
return raw.replace(cleanerRE, (s: string) =>
12+
s[0] === '/' ? blankReplacer(s) : stringBlankReplacer(s)
13+
)
14+
}

packages/vite/src/node/plugins/assetImportMetaUrl.ts

+8-15
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,7 @@ import MagicString from 'magic-string'
33
import path from 'path'
44
import { fileToUrl } from './asset'
55
import type { ResolvedConfig } from '../config'
6-
import {
7-
multilineCommentsRE,
8-
singlelineCommentsRE,
9-
stringsRE,
10-
blankReplacer
11-
} from '../utils'
6+
import { emptyString } from '../cleanString'
127

138
/**
149
* Convert `new URL('./foo.png', import.meta.url)` to its resolved built URL
@@ -29,19 +24,16 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
2924
code.includes('new URL') &&
3025
code.includes(`import.meta.url`)
3126
) {
32-
const importMetaUrlRE =
27+
let s: MagicString | undefined
28+
const assetImportMetaUrlRE =
3329
/\bnew\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*,?\s*\)/g
34-
const noCommentsCode = code
35-
.replace(multilineCommentsRE, blankReplacer)
36-
.replace(singlelineCommentsRE, blankReplacer)
37-
.replace(stringsRE, (m) => `'${'\0'.repeat(m.length - 2)}'`)
30+
const cleanString = emptyString(code)
3831

39-
let s: MagicString | null = null
4032
let match: RegExpExecArray | null
41-
while ((match = importMetaUrlRE.exec(noCommentsCode))) {
33+
while ((match = assetImportMetaUrlRE.exec(cleanString))) {
4234
const { 0: exp, 1: emptyUrl, index } = match
4335

44-
const urlStart = exp.indexOf(emptyUrl) + index
36+
const urlStart = cleanString.indexOf(emptyUrl, index)
4537
const urlEnd = urlStart + emptyUrl.length
4638
const rawUrl = code.slice(urlStart, urlEnd)
4739

@@ -74,8 +66,9 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
7466
// Get final asset URL. Catch error if the file does not exist,
7567
// in which we can resort to the initial URL and let it resolve in runtime
7668
const builtUrl = await fileToUrl(file, config, this).catch(() => {
69+
const rawExp = code.slice(index, index + exp.length)
7770
config.logger.warnOnce(
78-
`\n${exp} doesn't exist at build time, it will remain unchanged to be resolved at runtime`
71+
`\n${rawExp} doesn't exist at build time, it will remain unchanged to be resolved at runtime`
7972
)
8073
return url
8174
})

packages/vite/src/node/plugins/workerImportMetaUrl.ts

+20-33
Original file line numberDiff line numberDiff line change
@@ -2,65 +2,56 @@ import JSON5 from 'json5'
22
import type { ResolvedConfig } from '../config'
33
import type { Plugin } from '../plugin'
44
import { fileToUrl } from './asset'
5-
import {
6-
blankReplacer,
7-
cleanUrl,
8-
injectQuery,
9-
multilineCommentsRE,
10-
singlelineCommentsRE,
11-
stringsRE
12-
} from '../utils'
5+
import { cleanUrl, injectQuery } from '../utils'
136
import path from 'path'
147
import { workerFileToUrl } from './worker'
158
import { parseRequest } from '../utils'
169
import { ENV_ENTRY, ENV_PUBLIC_PATH } from '../constants'
1710
import MagicString from 'magic-string'
1811
import type { ViteDevServer } from '..'
1912
import type { RollupError } from 'rollup'
13+
import { emptyString } from '../cleanString'
2014

2115
type WorkerType = 'classic' | 'module' | 'ignore'
16+
const ignoreFlagRE = /\/\*\s*@vite-ignore\s*\*\//
2217

2318
const WORKER_FILE_ID = 'worker_url_file'
2419

25-
function getWorkerType(
26-
code: string,
27-
noCommentsCode: string,
28-
i: number
29-
): WorkerType {
20+
function getWorkerType(raw: string, clean: string, i: number): WorkerType {
3021
function err(e: string, pos: number) {
3122
const error = new Error(e) as RollupError
3223
error.pos = pos
3324
throw error
3425
}
3526

36-
const commaIndex = noCommentsCode.indexOf(',', i)
27+
const commaIndex = clean.indexOf(',', i)
3728
if (commaIndex === -1) {
3829
return 'classic'
3930
}
40-
const endIndex = noCommentsCode.indexOf(')', i)
31+
const endIndex = clean.indexOf(')', i)
4132

4233
// case: ') ... ,' mean no worker options params
4334
if (commaIndex > endIndex) {
4435
return 'classic'
4536
}
4637

4738
// need to find in comment code
48-
let workerOptsString = code.substring(commaIndex + 1, endIndex)
39+
const workerOptString = raw.substring(commaIndex + 1, endIndex)
4940

50-
const hasViteIgnore = /\/\*\s*@vite-ignore\s*\*\//.test(workerOptsString)
41+
const hasViteIgnore = ignoreFlagRE.test(workerOptString)
5142
if (hasViteIgnore) {
5243
return 'ignore'
5344
}
5445

5546
// need to find in no comment code
56-
workerOptsString = noCommentsCode.substring(commaIndex + 1, endIndex)
57-
if (!workerOptsString.trim().length) {
47+
const cleanWorkerOptString = clean.substring(commaIndex + 1, endIndex)
48+
if (!cleanWorkerOptString.trim().length) {
5849
return 'classic'
5950
}
6051

6152
let workerOpts: { type: WorkerType } = { type: 'classic' }
6253
try {
63-
workerOpts = JSON5.parse(workerOptsString)
54+
workerOpts = JSON5.parse(workerOptString)
6455
} catch (e) {
6556
// can't parse by JSON5, so the worker options had unexpect char.
6657
err(
@@ -113,28 +104,22 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
113104
code: injectEnv + code
114105
}
115106
}
107+
let s: MagicString | undefined
116108
if (
117109
(code.includes('new Worker') || code.includes('new ShareWorker')) &&
118110
code.includes('new URL') &&
119111
code.includes(`import.meta.url`)
120112
) {
121-
const importMetaUrlRE =
113+
const cleanString = emptyString(code)
114+
const workerImportMetaUrlRE =
122115
/\bnew\s+(Worker|SharedWorker)\s*\(\s*(new\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*\))/g
123-
const noCommentsCode = code
124-
.replace(multilineCommentsRE, blankReplacer)
125-
.replace(singlelineCommentsRE, blankReplacer)
126-
127-
const noStringCode = noCommentsCode.replace(
128-
stringsRE,
129-
(m) => `'${' '.repeat(m.length - 2)}'`
130-
)
116+
131117
let match: RegExpExecArray | null
132-
let s: MagicString | null = null
133-
while ((match = importMetaUrlRE.exec(noStringCode))) {
118+
while ((match = workerImportMetaUrlRE.exec(cleanString))) {
134119
const { 0: allExp, 2: exp, 3: emptyUrl, index } = match
135120
const urlIndex = allExp.indexOf(exp) + index
136121

137-
const urlStart = allExp.indexOf(emptyUrl) + index
122+
const urlStart = cleanString.indexOf(emptyUrl, index)
138123
const urlEnd = urlStart + emptyUrl.length
139124
const rawUrl = code.slice(urlStart, urlEnd)
140125

@@ -156,7 +141,7 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
156141
s ||= new MagicString(code)
157142
const workerType = getWorkerType(
158143
code,
159-
noCommentsCode,
144+
cleanString,
160145
index + allExp.length
161146
)
162147
const file = path.resolve(path.dirname(id), rawUrl.slice(1, -1))
@@ -172,12 +157,14 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
172157
contentOnly: true
173158
})
174159
}
160+
175161
if (s) {
176162
return {
177163
code: s.toString(),
178164
map: config.build.sourcemap ? s.generateMap({ hires: true }) : null
179165
}
180166
}
167+
181168
return null
182169
}
183170
}

packages/vite/src/node/utils.ts

-1
Original file line numberDiff line numberDiff line change
@@ -733,4 +733,3 @@ export function parseRequest(id: string): Record<string, string> | null {
733733
}
734734

735735
export const blankReplacer = (match: string) => ' '.repeat(match.length)
736-
export const stringsRE = /"[^"]*"|'[^']*'|`[^`]*`/g

0 commit comments

Comments
 (0)