From abf536f920c32db1b96f6b4cabb510281e5170f5 Mon Sep 17 00:00:00 2001 From: patak Date: Wed, 22 Mar 2023 09:56:23 +0100 Subject: [PATCH] perf: more regex improvements (#12520) --- packages/vite/scripts/util.ts | 3 +- .../src/node/optimizer/esbuildDepPlugin.ts | 7 +-- packages/vite/src/node/optimizer/index.ts | 6 ++- packages/vite/src/node/plugins/asset.ts | 13 +++-- .../vite/src/node/plugins/clientInjections.ts | 5 +- packages/vite/src/node/plugins/define.ts | 8 +-- packages/vite/src/node/plugins/html.ts | 3 +- .../server/__tests__/pluginContainer.spec.ts | 2 +- packages/vite/src/node/server/hmr.ts | 4 +- .../src/node/server/middlewares/static.ts | 7 ++- packages/vite/src/node/utils.ts | 51 +++++++++++++------ 11 files changed, 73 insertions(+), 36 deletions(-) diff --git a/packages/vite/scripts/util.ts b/packages/vite/scripts/util.ts index 135c701098fc57..c742b3338e8717 100644 --- a/packages/vite/scripts/util.ts +++ b/packages/vite/scripts/util.ts @@ -17,8 +17,9 @@ export function rewriteImports( }) } +const windowsSlashRE = /\\/g export function slash(p: string): string { - return p.replace(/\\/g, '/') + return p.replace(windowsSlashRE, '/') } export function walkDir(dir: string, handleFile: (file: string) => void): void { diff --git a/packages/vite/src/node/optimizer/esbuildDepPlugin.ts b/packages/vite/src/node/optimizer/esbuildDepPlugin.ts index a01f48eb5a556b..e3bb4730eef1d7 100644 --- a/packages/vite/src/node/optimizer/esbuildDepPlugin.ts +++ b/packages/vite/src/node/optimizer/esbuildDepPlugin.ts @@ -4,6 +4,7 @@ import { CSS_LANGS_RE, KNOWN_ASSET_TYPES } from '../constants' import { getDepOptimizationConfig } from '..' import type { PackageCache, ResolvedConfig } from '..' import { + escapeRegex, flattenId, isBuiltin, isExternalUrl, @@ -281,6 +282,8 @@ module.exports = Object.create(new Proxy({}, { } } +const matchesEntireLine = (text: string) => `^${escapeRegex(text)}$` + // esbuild doesn't transpile `require('foo')` into `import` statements if 'foo' is externalized // https://github.com/evanw/esbuild/issues/566#issuecomment-735551834 export function esbuildCjsExternalPlugin( @@ -290,9 +293,7 @@ export function esbuildCjsExternalPlugin( return { name: 'cjs-external', setup(build) { - const escape = (text: string) => - `^${text.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')}$` - const filter = new RegExp(externals.map(escape).join('|')) + const filter = new RegExp(externals.map(matchesEntireLine).join('|')) build.onResolve({ filter: new RegExp(`^${nonFacadePrefix}`) }, (args) => { return { diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 04ba88cb38eb08..c9e189a5da18e6 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -21,6 +21,7 @@ import { normalizeId, normalizePath, removeDir, + removeLeadingSlash, renameDir, writeFile, } from '../utils' @@ -41,6 +42,7 @@ const isDebugEnabled = _debug('vite:deps').enabled const jsExtensionRE = /\.js$/i const jsMapExtensionRE = /\.js\.map$/i +const reExportRE = /export\s+\*\s+from/ export type ExportsData = { hasImports: boolean @@ -956,7 +958,7 @@ export function createIsOptimizedDepUrl( const depsCacheDirPrefix = depsCacheDirRelative.startsWith('../') ? // if the cache directory is outside root, the url prefix would be something // like '/@fs/absolute/path/to/node_modules/.vite' - `/@fs/${normalizePath(depsCacheDir).replace(/^\//, '')}` + `/@fs/${removeLeadingSlash(normalizePath(depsCacheDir))}` : // if the cache directory is inside root, the url prefix would be something // like '/node_modules/.vite' `/${depsCacheDirRelative}` @@ -1140,7 +1142,7 @@ export async function extractExportsData( facade, hasReExports: imports.some(({ ss, se }) => { const exp = entryContent.slice(ss, se) - return /export\s+\*\s+from/.test(exp) + return reExportRE.test(exp) }), jsxLoader: usedJsxLoader, } diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 21f8a4d5109be7..c0fe474ddb3f15 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -16,7 +16,13 @@ import { } from '../build' import type { Plugin } from '../plugin' import type { ResolvedConfig } from '../config' -import { cleanUrl, getHash, joinUrlSegments, normalizePath } from '../utils' +import { + cleanUrl, + getHash, + joinUrlSegments, + normalizePath, + removeLeadingSlash, +} from '../utils' import { FS_PREFIX } from '../constants' export const assetUrlRE = /__VITE_ASSET__([a-z\d]+)__(?:\$_(.*?)__)?/g @@ -24,6 +30,7 @@ export const assetUrlRE = /__VITE_ASSET__([a-z\d]+)__(?:\$_(.*?)__)?/g const rawRE = /(?:\?|&)raw(?:&|$)/ const urlRE = /(\?|&)url(?:&|$)/ const jsSourceMapRE = /\.[cm]?js\.map$/ +const unnededFinalQueryCharRE = /[?&]$/ const assetCache = new WeakMap>() @@ -169,7 +176,7 @@ export function assetPlugin(config: ResolvedConfig): Plugin { return } - id = id.replace(urlRE, '$1').replace(/[?&]$/, '') + id = id.replace(urlRE, '$1').replace(unnededFinalQueryCharRE, '') const url = await fileToUrl(id, config, this) return `export default ${JSON.stringify(url)}` }, @@ -255,7 +262,7 @@ function fileToDevUrl(id: string, config: ResolvedConfig) { rtn = path.posix.join(FS_PREFIX, id) } const base = joinUrlSegments(config.server?.origin ?? '', config.base) - return joinUrlSegments(base, rtn.replace(/^\//, '')) + return joinUrlSegments(base, removeLeadingSlash(rtn)) } export function getPublicAssetFilename( diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index dfd3b878b6f459..5ac79c8d14ef8e 100644 --- a/packages/vite/src/node/plugins/clientInjections.ts +++ b/packages/vite/src/node/plugins/clientInjections.ts @@ -4,6 +4,9 @@ import type { ResolvedConfig } from '../config' import { CLIENT_ENTRY, ENV_ENTRY } from '../constants' import { isObject, normalizePath, resolveHostname } from '../utils' +const process_env_NODE_ENV_RE = + /(\bglobal(This)?\.)?\bprocess\.env\.NODE_ENV\b/g + // ids in transform are normalized to unix style const normalizedClientEntry = normalizePath(CLIENT_ENTRY) const normalizedEnvEntry = normalizePath(ENV_ENTRY) @@ -86,7 +89,7 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin { // for it to avoid shimming a `process` object during dev, // avoiding inconsistencies between dev and build return code.replace( - /(\bglobal(This)?\.)?\bprocess\.env\.NODE_ENV\b/g, + process_env_NODE_ENV_RE, config.define?.['process.env.NODE_ENV'] || JSON.stringify(process.env.NODE_ENV || config.mode), ) diff --git a/packages/vite/src/node/plugins/define.ts b/packages/vite/src/node/plugins/define.ts index f47a3ff97fed64..c0c37183b81aaf 100644 --- a/packages/vite/src/node/plugins/define.ts +++ b/packages/vite/src/node/plugins/define.ts @@ -1,7 +1,7 @@ import MagicString from 'magic-string' import type { ResolvedConfig } from '../config' import type { Plugin } from '../plugin' -import { transformStableResult } from '../utils' +import { escapeRegex, transformStableResult } from '../utils' import { isCSSRequest } from './css' import { isHTMLRequest } from './html' @@ -113,11 +113,7 @@ export function definePlugin(config: ResolvedConfig): Plugin { // Mustn't be preceded by a char that can be part of an identifier // or a '.' that isn't part of a spread operator '(? { - return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&') - }) - .join('|') + + replacementsKeys.map(escapeRegex).join('|') + // Mustn't be followed by a char that can be part of an identifier // or an assignment (but allow equality operators) ')(?:(?<=\\.)|(?![\\p{L}\\p{N}_$]|\\s*?=[^=]))', diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 9737dcb3a9e9b7..18dc2405ba988f 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -20,6 +20,7 @@ import { isExternalUrl, normalizePath, processSrcSet, + removeLeadingSlash, } from '../utils' import type { ResolvedConfig } from '../config' import { toOutputFilePathInHtml } from '../build' @@ -537,7 +538,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { if ( content !== '' && // Empty attribute !namedOutput.includes(content) && // Direct reference to named output - !namedOutput.includes(content.replace(/^\//, '')) // Allow for absolute references as named output can't be an absolute path + !namedOutput.includes(removeLeadingSlash(content)) // Allow for absolute references as named output can't be an absolute path ) { try { const url = diff --git a/packages/vite/src/node/server/__tests__/pluginContainer.spec.ts b/packages/vite/src/node/server/__tests__/pluginContainer.spec.ts index 089a45497a593e..a20fc919f7fa10 100644 --- a/packages/vite/src/node/server/__tests__/pluginContainer.spec.ts +++ b/packages/vite/src/node/server/__tests__/pluginContainer.spec.ts @@ -196,7 +196,7 @@ async function getPluginContainer( ) // @ts-expect-error This plugin requires a ViteDevServer instance. - config.plugins = config.plugins.filter((p) => !/pre-alias/.test(p.name)) + config.plugins = config.plugins.filter((p) => !p.name.includes('pre-alias')) resolveId = (id) => container.resolveId(id) const container = await createPluginContainer(config, moduleGraph) diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index c0accc63e5bc71..fd956a8477b8d1 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -14,6 +14,8 @@ import type { ModuleNode } from './moduleGraph' export const debugHmr = createDebugger('vite:hmr') +const whitespaceRE = /\s/ + const normalizedClientDir = normalizePath(CLIENT_DIR) export interface HmrOptions { @@ -388,7 +390,7 @@ export function lexAcceptedHmrDeps( } else if (char === '`') { prevState = state state = LexerState.inTemplateString - } else if (/\s/.test(char)) { + } else if (whitespaceRE.test(char)) { continue } else { if (state === LexerState.inCall) { diff --git a/packages/vite/src/node/server/middlewares/static.ts b/packages/vite/src/node/server/middlewares/static.ts index 229b860dbcec56..27ac7cbceecab5 100644 --- a/packages/vite/src/node/server/middlewares/static.ts +++ b/packages/vite/src/node/server/middlewares/static.ts @@ -14,10 +14,13 @@ import { isInternalRequest, isParentDirectory, isWindows, + removeLeadingSlash, shouldServeFile, slash, } from '../../utils' +const knownJavascriptExtensionRE = /\.[tj]sx?$/ + const sirvOptions = ({ headers, shouldServe, @@ -35,7 +38,7 @@ const sirvOptions = ({ // for the MIME type video/mp2t. In almost all cases, we can expect // these files to be TypeScript files, and for Vite to serve them with // this Content-Type. - if (/\.[tj]sx?$/.test(pathname)) { + if (knownJavascriptExtensionRE.test(pathname)) { res.setHeader('Content-Type', 'application/javascript') } if (headers) { @@ -119,7 +122,7 @@ export function serveStaticMiddleware( } const resolvedPathname = redirectedPathname || pathname - let fileUrl = path.resolve(dir, resolvedPathname.replace(/^\//, '')) + let fileUrl = path.resolve(dir, removeLeadingSlash(resolvedPathname)) if (resolvedPathname.endsWith('/') && !fileUrl.endsWith('/')) { fileUrl = fileUrl + '/' } diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 02b7f6d48c80fe..1b385c19bb6736 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -50,8 +50,9 @@ export const createFilter = _createFilter as ( options?: { resolve?: string | false | null }, ) => (id: string | unknown) => boolean +const windowsSlashRE = /\\/g export function slash(p: string): string { - return p.replace(/\\/g, '/') + return p.replace(windowsSlashRE, '/') } /** @@ -74,15 +75,19 @@ export function unwrapId(id: string): string { : id } +const replaceSlashOrColonRE = /[/:]/g +const replaceDotRE = /\./g +const replaceNestedIdRE = /(\s*>\s*)/g +const replaceHashRE = /#/g export const flattenId = (id: string): string => id - .replace(/[/:]/g, '_') - .replace(/\./g, '__') - .replace(/(\s*>\s*)/g, '___') - .replace(/#/g, '____') + .replace(replaceSlashOrColonRE, '_') + .replace(replaceDotRE, '__') + .replace(replaceNestedIdRE, '___') + .replace(replaceHashRE, '____') export const normalizeId = (id: string): string => - id.replace(/(\s*>\s*)/g, ' > ') + id.replace(replaceNestedIdRE, ' > ') //TODO: revisit later to see if the edge case that "compiling using node v12 code to be run in node v16 in the server" is what we intend to support. const builtins = new Set([ @@ -300,10 +305,14 @@ export function removeDirectQuery(url: string): string { return url.replace(directRequestRE, '$1').replace(trailingSeparatorRE, '') } +const replacePercentageRE = /%/g export function injectQuery(url: string, queryToInject: string): string { // encode percents for consistent behavior with pathToFileURL // see #2614 for details - const resolvedUrl = new URL(url.replace(/%/g, '%25'), 'relative:///') + const resolvedUrl = new URL( + url.replace(replacePercentageRE, '%25'), + 'relative:///', + ) const { search, hash } = resolvedUrl let pathname = cleanUrl(url) pathname = isWindows ? slash(pathname) : pathname @@ -659,13 +668,12 @@ export function processSrcSetSync( ) } +const cleanSrcSetRE = + /(?:url|image|gradient|cross-fade)\([^)]*\)|"([^"]|(?<=\\)")*"|'([^']|(?<=\\)')*'/g function splitSrcSet(srcs: string) { const parts: string[] = [] // There could be a ',' inside of url(data:...), linear-gradient(...) or "data:..." - const cleanedSrcs = srcs.replace( - /(?:url|image|gradient|cross-fade)\([^)]*\)|"([^"]|(?<=\\)")*"|'([^']|(?<=\\)')*'/g, - blankReplacer, - ) + const cleanedSrcs = srcs.replace(cleanSrcSetRE, blankReplacer) let startIndex = 0 let splitIndex: number do { @@ -678,22 +686,26 @@ function splitSrcSet(srcs: string) { return parts } +const windowsDriveRE = /^[A-Z]:/ +const replaceWindowsDriveRE = /^([A-Z]):\// +const linuxAbsolutePathRE = /^\/[^/]/ function escapeToLinuxLikePath(path: string) { - if (/^[A-Z]:/.test(path)) { - return path.replace(/^([A-Z]):\//, '/windows/$1/') + if (windowsDriveRE.test(path)) { + return path.replace(replaceWindowsDriveRE, '/windows/$1/') } - if (/^\/[^/]/.test(path)) { + if (linuxAbsolutePathRE.test(path)) { return `/linux${path}` } return path } +const revertWindowsDriveRE = /^\/windows\/([A-Z])\// function unescapeToLinuxLikePath(path: string) { if (path.startsWith('/linux/')) { return path.slice('/linux'.length) } if (path.startsWith('/windows/')) { - return path.replace(/^\/windows\/([A-Z])\//, '$1:/') + return path.replace(revertWindowsDriveRE, '$1:/') } return path } @@ -1222,6 +1234,10 @@ export function joinUrlSegments(a: string, b: string): string { return a + b } +export function removeLeadingSlash(str: string): string { + return str[0] === '/' ? str.slice(1) : str +} + export function stripBase(path: string, base: string): string { if (path === base) { return '/' @@ -1246,3 +1262,8 @@ export function evalValue(rawValue: string): T { `) return fn() } + +const escapeRegexRE = /[-/\\^$*+?.()|[\]{}]/g +export function escapeRegex(str: string): string { + return str.replace(escapeRegexRE, '\\$&') +}