Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: nested comments and strings, new regexp utils #7650

Merged
merged 27 commits into from Apr 11, 2022
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Expand Up @@ -79,7 +79,7 @@ Each test can be run under either dev server mode or build mode.

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

- 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.
- 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.

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

Expand Down
108 changes: 108 additions & 0 deletions packages/vite/src/node/__tests__/cleanString.spec.ts
@@ -0,0 +1,108 @@
import { emptyString, findEmptyStringRawIndex } from '../../node/cleanString'

test('comments', () => {
expect(
emptyString(`
// comment1 // comment
// comment1
/* coment2 */
/*
// coment3
*/
/* // coment3 */
/* // coment3 */ // comment
// comment 4 /* comment 5 */
`).trim()
).toBe('')
})

test('strings', () => {
const clean = emptyString(`
// comment1
const a = 'aaaa'
/* coment2 */
const b = "bbbb"
/*
// coment3
*/
/* // coment3 */
// comment 4 /* comment 5 */
`)
expect(clean).toMatch("const a = '\0\0\0\0'")
expect(clean).toMatch('const b = "\0\0\0\0"')
})

test('strings comment nested', () => {
expect(
emptyString(`
// comment 1 /* " */
const a = "a //"
// comment 2 /* " */
`)
).toMatch('const a = "\0\0\0\0"')

expect(
emptyString(`
// comment 1 /* ' */
const a = "a //"
// comment 2 /* ' */
`)
).toMatch('const a = "\0\0\0\0"')

expect(
emptyString(`
// comment 1 /* \` */
const a = "a //"
// comment 2 /* \` */
`)
).toMatch('const a = "\0\0\0\0"')

expect(
emptyString(`
const a = "a //"
console.log("console")
`)
).toMatch('const a = "\0\0\0\0"')

expect(
emptyString(`
const a = "a /*"
console.log("console")
const b = "b */"
`)
).toMatch('const a = "\0\0\0\0"')

expect(
emptyString(`
const a = "a ' "
console.log("console")
const b = "b ' "
`)
).toMatch('const a = "\0\0\0\0"')

expect(
emptyString(`
const a = "a \` "
console.log("console")
const b = "b \` "
`)
).toMatch('const a = "\0\0\0\0"')
})

test('find empty string flag in raw index', () => {
const str = `
const a = "aaaaa"
const b = "bbbbb"
`
const clean = emptyString(str)
expect(clean).toMatch('const a = "\0\0\0\0\0"')
expect(clean).toMatch('const b = "\0\0\0\0\0"')

const aIndex = str.indexOf('const a = "aaaaa"')
const a = findEmptyStringRawIndex(clean, '\0\0\0\0\0', aIndex)
expect(str.slice(a[0], a[1])).toMatch('aaaaa')

const bIndex = str.indexOf('const b = "bbbbb"')
const b = findEmptyStringRawIndex(clean, '\0\0\0\0\0', bIndex)
expect(str.slice(b[0], b[1])).toMatch('bbbbb')
})
31 changes: 31 additions & 0 deletions packages/vite/src/node/cleanString.ts
@@ -0,0 +1,31 @@
// 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 cleanerRE = /"[^"]*"|'[^']*'|\/\*(.|[\r\n])*?\*\/|\/\/.*/g

const blankReplacer = (s: string) => ' '.repeat(s.length)
const stringBlankReplacer = (s: string) =>
`${s[0]}${'\0'.repeat(s.length - 2)}${s[0]}`

export function emptyCommentsString(raw: string): string {
return raw.replace(cleanerRE, (s: string) =>
s[0] === '/' ? blankReplacer(s) : s
)
}
poyoho marked this conversation as resolved.
Show resolved Hide resolved

export function emptyString(raw: string): string {
const res = raw.replace(cleanerRE, (s: string) =>
s[0] === '/' ? blankReplacer(s) : stringBlankReplacer(s)
)
return res
}
poyoho marked this conversation as resolved.
Show resolved Hide resolved

export function findEmptyStringRawIndex(
clean: string,
emptyFlag: string,
start: number
): [number, number] {
const flagIndex = clean.indexOf(emptyFlag, start)
const flagEndIndex = flagIndex + emptyFlag.length
return [flagIndex, flagEndIndex]
}
poyoho marked this conversation as resolved.
Show resolved Hide resolved
33 changes: 14 additions & 19 deletions packages/vite/src/node/plugins/assetImportMetaUrl.ts
Expand Up @@ -3,12 +3,7 @@ import MagicString from 'magic-string'
import path from 'path'
import { fileToUrl } from './asset'
import type { ResolvedConfig } from '../config'
import {
multilineCommentsRE,
singlelineCommentsRE,
stringsRE,
blankReplacer
} from '../utils'
import { emptyString, findEmptyStringRawIndex } from '../cleanString'

/**
* Convert `new URL('./foo.png', import.meta.url)` to its resolved built URL
Expand All @@ -29,20 +24,19 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
code.includes('new URL') &&
code.includes(`import.meta.url`)
) {
const importMetaUrlRE =
/\bnew\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*,?\s*\)/g
const noCommentsCode = code
.replace(multilineCommentsRE, blankReplacer)
.replace(singlelineCommentsRE, blankReplacer)
.replace(stringsRE, (m) => `'${'\0'.repeat(m.length - 2)}'`)

let s: MagicString | null = null
let s: MagicString | undefined
let match: RegExpExecArray | null
while ((match = importMetaUrlRE.exec(noCommentsCode))) {
const assetImportMetaUrlRE =
/\bnew\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*,?\s*\)/g
const cleanString = emptyString(code)
while ((match = assetImportMetaUrlRE.exec(cleanString))) {
poyoho marked this conversation as resolved.
Show resolved Hide resolved
const { 0: exp, 1: emptyUrl, index } = match

const urlStart = exp.indexOf(emptyUrl) + index
const urlEnd = urlStart + emptyUrl.length
const [urlStart, urlEnd] = findEmptyStringRawIndex(
cleanString,
emptyUrl,
index
)
const rawUrl = code.slice(urlStart, urlEnd)

if (!s) s = new MagicString(code)
Expand Down Expand Up @@ -74,8 +68,9 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
// Get final asset URL. Catch error if the file does not exist,
// in which we can resort to the initial URL and let it resolve in runtime
const builtUrl = await fileToUrl(file, config, this).catch(() => {
const truthExp = code.slice(index, index + exp.length)
config.logger.warnOnce(
`\n${exp} doesn't exist at build time, it will remain unchanged to be resolved at runtime`
`\n${truthExp} doesn't exist at build time, it will remain unchanged to be resolved at runtime`
poyoho marked this conversation as resolved.
Show resolved Hide resolved
)
return url
})
Expand All @@ -92,8 +87,8 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
map: config.build.sourcemap ? s.generateMap({ hires: true }) : null
}
}
return null
poyoho marked this conversation as resolved.
Show resolved Hide resolved
}
return null
}
}
}
Expand Down
71 changes: 30 additions & 41 deletions packages/vite/src/node/plugins/workerImportMetaUrl.ts
Expand Up @@ -2,65 +2,56 @@ import JSON5 from 'json5'
import type { ResolvedConfig } from '../config'
import type { Plugin } from '../plugin'
import { fileToUrl } from './asset'
import {
blankReplacer,
cleanUrl,
injectQuery,
multilineCommentsRE,
singlelineCommentsRE,
stringsRE
} from '../utils'
import { cleanUrl, injectQuery } from '../utils'
import path from 'path'
import { workerFileToUrl } from './worker'
import { parseRequest } from '../utils'
import { ENV_ENTRY, ENV_PUBLIC_PATH } from '../constants'
import MagicString from 'magic-string'
import type { ViteDevServer } from '..'
import type { RollupError } from 'rollup'
import { emptyString, findEmptyStringRawIndex } from '../cleanString'

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

const WORKER_FILE_ID = 'worker_url_file'

function getWorkerType(
code: string,
noCommentsCode: string,
i: number
): WorkerType {
function getWorkerType(raw: string, clean: string, i: number): WorkerType {
function err(e: string, pos: number) {
const error = new Error(e) as RollupError
error.pos = pos
throw error
}

const commaIndex = noCommentsCode.indexOf(',', i)
const commaIndex = clean.indexOf(',', i)
if (commaIndex === -1) {
return 'classic'
}
const endIndex = noCommentsCode.indexOf(')', i)
const endIndex = clean.indexOf(')', i)

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

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

const hasViteIgnore = /\/\*\s*@vite-ignore\s*\*\//.test(workerOptsString)
const hasViteIgnore = ignoreFlagRE.test(workerOptString)
if (hasViteIgnore) {
return 'ignore'
}

// need to find in no comment code
workerOptsString = noCommentsCode.substring(commaIndex + 1, endIndex)
if (!workerOptsString.trim().length) {
const cleanworkerOptString = clean.substring(commaIndex + 1, endIndex)
if (!cleanworkerOptString.trim().length) {
poyoho marked this conversation as resolved.
Show resolved Hide resolved
return 'classic'
}

let workerOpts: { type: WorkerType } = { type: 'classic' }
try {
workerOpts = JSON5.parse(workerOptsString)
workerOpts = JSON5.parse(workerOptString)
} catch (e) {
// can't parse by JSON5, so the worker options had unexpect char.
err(
Expand Down Expand Up @@ -113,29 +104,26 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
code: injectEnv + code
}
}
let s: MagicString | undefined
if (
(code.includes('new Worker') || code.includes('new ShareWorker')) &&
code.includes('new URL') &&
code.includes(`import.meta.url`)
) {
const importMetaUrlRE =
/\bnew\s+(Worker|SharedWorker)\s*\(\s*(new\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*\))/g
const noCommentsCode = code
.replace(multilineCommentsRE, blankReplacer)
.replace(singlelineCommentsRE, blankReplacer)

const noStringCode = noCommentsCode.replace(
stringsRE,
(m) => `'${' '.repeat(m.length - 2)}'`
)
let match: RegExpExecArray | null
let s: MagicString | null = null
while ((match = importMetaUrlRE.exec(noStringCode))) {
const cleanString = emptyString(code)
const workerImportMetaUrlRE =
/\bnew\s+(Worker|SharedWorker)\s*\(\s*(new\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*\))/g

while ((match = workerImportMetaUrlRE.exec(cleanString))) {
poyoho marked this conversation as resolved.
Show resolved Hide resolved
const { 0: allExp, 2: exp, 3: emptyUrl, index } = match
const urlIndex = allExp.indexOf(exp) + index

const urlStart = allExp.indexOf(emptyUrl) + index
const urlEnd = urlStart + emptyUrl.length
const [urlStart, urlEnd] = findEmptyStringRawIndex(
cleanString,
emptyUrl,
index
)
const rawUrl = code.slice(urlStart, urlEnd)

if (options?.ssr) {
Expand All @@ -156,7 +144,7 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
s ||= new MagicString(code)
const workerType = getWorkerType(
code,
noCommentsCode,
cleanString,
index + allExp.length
)
const file = path.resolve(path.dirname(id), rawUrl.slice(1, -1))
Expand All @@ -172,14 +160,15 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
contentOnly: true
})
}
if (s) {
return {
code: s.toString(),
map: config.build.sourcemap ? s.generateMap({ hires: true }) : null
}
}

if (s) {
return {
code: s.toString(),
map: config.build.sourcemap ? s.generateMap({ hires: true }) : null
}
return null
}
return null
}
}
}
1 change: 0 additions & 1 deletion packages/vite/src/node/utils.ts
Expand Up @@ -733,4 +733,3 @@ export function parseRequest(id: string): Record<string, string> | null {
}

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