diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b105499159bebb..f086627de5cfe0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. diff --git a/packages/vite/src/node/__tests__/cleanString.spec.ts b/packages/vite/src/node/__tests__/cleanString.spec.ts new file mode 100644 index 00000000000000..883dc67abfe2f9 --- /dev/null +++ b/packages/vite/src/node/__tests__/cleanString.spec.ts @@ -0,0 +1,108 @@ +import { emptyString } 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 aStart = clean.indexOf('\0\0\0\0\0', aIndex) + expect(str.slice(aStart, aStart + 5)).toMatch('aaaaa') + + const bIndex = str.indexOf('const b = "bbbbb"') + const bStart = clean.indexOf('\0\0\0\0\0', bIndex) + expect(str.slice(bStart, bStart + 5)).toMatch('bbbbb') +}) diff --git a/packages/vite/src/node/cleanString.ts b/packages/vite/src/node/cleanString.ts new file mode 100644 index 00000000000000..d26274397124ff --- /dev/null +++ b/packages/vite/src/node/cleanString.ts @@ -0,0 +1,14 @@ +// 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 emptyString(raw: string): string { + return raw.replace(cleanerRE, (s: string) => + s[0] === '/' ? blankReplacer(s) : stringBlankReplacer(s) + ) +} diff --git a/packages/vite/src/node/plugins/assetImportMetaUrl.ts b/packages/vite/src/node/plugins/assetImportMetaUrl.ts index a3f8e441b0f933..b8c16f76d2b93f 100644 --- a/packages/vite/src/node/plugins/assetImportMetaUrl.ts +++ b/packages/vite/src/node/plugins/assetImportMetaUrl.ts @@ -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 } from '../cleanString' /** * Convert `new URL('./foo.png', import.meta.url)` to its resolved built URL @@ -29,19 +24,16 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin { code.includes('new URL') && code.includes(`import.meta.url`) ) { - const importMetaUrlRE = + let s: MagicString | undefined + const assetImportMetaUrlRE = /\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)}'`) + const cleanString = emptyString(code) - let s: MagicString | null = null let match: RegExpExecArray | null - while ((match = importMetaUrlRE.exec(noCommentsCode))) { + while ((match = assetImportMetaUrlRE.exec(cleanString))) { const { 0: exp, 1: emptyUrl, index } = match - const urlStart = exp.indexOf(emptyUrl) + index + const urlStart = cleanString.indexOf(emptyUrl, index) const urlEnd = urlStart + emptyUrl.length const rawUrl = code.slice(urlStart, urlEnd) @@ -74,8 +66,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 rawExp = 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${rawExp} doesn't exist at build time, it will remain unchanged to be resolved at runtime` ) return url }) diff --git a/packages/vite/src/node/plugins/workerImportMetaUrl.ts b/packages/vite/src/node/plugins/workerImportMetaUrl.ts index c9a2903d9a4142..c8ab20fe21694c 100644 --- a/packages/vite/src/node/plugins/workerImportMetaUrl.ts +++ b/packages/vite/src/node/plugins/workerImportMetaUrl.ts @@ -2,14 +2,7 @@ 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' @@ -17,27 +10,25 @@ import { ENV_ENTRY, ENV_PUBLIC_PATH } from '../constants' import MagicString from 'magic-string' import type { ViteDevServer } from '..' import type { RollupError } from 'rollup' +import { emptyString } 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) { @@ -45,22 +36,22 @@ function getWorkerType( } // 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) { 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( @@ -113,28 +104,22 @@ 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 = + const cleanString = emptyString(code) + const workerImportMetaUrlRE = /\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))) { + while ((match = workerImportMetaUrlRE.exec(cleanString))) { const { 0: allExp, 2: exp, 3: emptyUrl, index } = match const urlIndex = allExp.indexOf(exp) + index - const urlStart = allExp.indexOf(emptyUrl) + index + const urlStart = cleanString.indexOf(emptyUrl, index) const urlEnd = urlStart + emptyUrl.length const rawUrl = code.slice(urlStart, urlEnd) @@ -156,7 +141,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)) @@ -172,12 +157,14 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin { contentOnly: true }) } + if (s) { return { code: s.toString(), map: config.build.sourcemap ? s.generateMap({ hires: true }) : null } } + return null } } diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 8c7859e3850454..16391df8c73df3 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -733,4 +733,3 @@ export function parseRequest(id: string): Record | null { } export const blankReplacer = (match: string) => ' '.repeat(match.length) -export const stringsRE = /"[^"]*"|'[^']*'|`[^`]*`/g