Skip to content

Commit

Permalink
perf: more regex improvements (#12520)
Browse files Browse the repository at this point in the history
  • Loading branch information
patak-dev committed Mar 22, 2023
1 parent 45549e4 commit abf536f
Show file tree
Hide file tree
Showing 11 changed files with 73 additions and 36 deletions.
3 changes: 2 additions & 1 deletion packages/vite/scripts/util.ts
Expand Up @@ -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 {
Expand Down
7 changes: 4 additions & 3 deletions packages/vite/src/node/optimizer/esbuildDepPlugin.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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 {
Expand Down
6 changes: 4 additions & 2 deletions packages/vite/src/node/optimizer/index.ts
Expand Up @@ -21,6 +21,7 @@ import {
normalizeId,
normalizePath,
removeDir,
removeLeadingSlash,
renameDir,
writeFile,
} from '../utils'
Expand All @@ -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
Expand Down Expand Up @@ -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}`
Expand Down Expand Up @@ -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,
}
Expand Down
13 changes: 10 additions & 3 deletions packages/vite/src/node/plugins/asset.ts
Expand Up @@ -16,14 +16,21 @@ 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

const rawRE = /(?:\?|&)raw(?:&|$)/
const urlRE = /(\?|&)url(?:&|$)/
const jsSourceMapRE = /\.[cm]?js\.map$/
const unnededFinalQueryCharRE = /[?&]$/

const assetCache = new WeakMap<ResolvedConfig, Map<string, string>>()

Expand Down Expand Up @@ -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)}`
},
Expand Down Expand Up @@ -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(
Expand Down
5 changes: 4 additions & 1 deletion packages/vite/src/node/plugins/clientInjections.ts
Expand Up @@ -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)
Expand Down Expand Up @@ -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),
)
Expand Down
8 changes: 2 additions & 6 deletions 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'

Expand Down Expand Up @@ -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
'(?<![\\p{L}\\p{N}_$]|(?<!\\.\\.)\\.)(' +
replacementsKeys
.map((str) => {
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*?=[^=]))',
Expand Down
3 changes: 2 additions & 1 deletion packages/vite/src/node/plugins/html.ts
Expand Up @@ -20,6 +20,7 @@ import {
isExternalUrl,
normalizePath,
processSrcSet,
removeLeadingSlash,
} from '../utils'
import type { ResolvedConfig } from '../config'
import { toOutputFilePathInHtml } from '../build'
Expand Down Expand Up @@ -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 =
Expand Down
Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion packages/vite/src/node/server/hmr.ts
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
7 changes: 5 additions & 2 deletions packages/vite/src/node/server/middlewares/static.ts
Expand Up @@ -14,10 +14,13 @@ import {
isInternalRequest,
isParentDirectory,
isWindows,
removeLeadingSlash,
shouldServeFile,
slash,
} from '../../utils'

const knownJavascriptExtensionRE = /\.[tj]sx?$/

const sirvOptions = ({
headers,
shouldServe,
Expand All @@ -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) {
Expand Down Expand Up @@ -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 + '/'
}
Expand Down
51 changes: 36 additions & 15 deletions packages/vite/src/node/utils.ts
Expand Up @@ -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, '/')
}

/**
Expand All @@ -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([
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down Expand Up @@ -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 '/'
Expand All @@ -1246,3 +1262,8 @@ export function evalValue<T = any>(rawValue: string): T {
`)
return fn()
}

const escapeRegexRE = /[-/\\^$*+?.()|[\]{}]/g
export function escapeRegex(str: string): string {
return str.replace(escapeRegexRE, '\\$&')
}

0 comments on commit abf536f

Please sign in to comment.