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

perf: more regex improvements #12520

Merged
merged 3 commits into from Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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
4 changes: 2 additions & 2 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 {
escapeForRegex,
flattenId,
isBuiltin,
isExternalUrl,
Expand Down Expand Up @@ -290,8 +291,7 @@ export function esbuildCjsExternalPlugin(
return {
name: 'cjs-external',
setup(build) {
const escape = (text: string) =>
`^${text.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')}$`
const escape = (text: string) => `^${escapeForRegex(text)}$`
const filter = new RegExp(externals.map(escape).join('|'))

build.onResolve({ filter: new RegExp(`^${nonFacadePrefix}`) }, (args) => {
Expand Down
6 changes: 4 additions & 2 deletions packages/vite/src/node/optimizer/index.ts
Expand Up @@ -16,6 +16,7 @@ import {
emptyDir,
flattenId,
getHash,
initialSlashRemoved,
isOptimizable,
lookupFile,
normalizeId,
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/${initialSlashRemoved(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,
initialSlashRemoved,
joinUrlSegments,
normalizePath,
} 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 @@ -167,7 +174,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 @@ -253,7 +260,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, initialSlashRemoved(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 { escapeForRegex, 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(escapeForRegex).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 @@ -16,6 +16,7 @@ import {
cleanUrl,
generateCodeFrame,
getHash,
initialSlashRemoved,
isDataUrl,
isExternalUrl,
normalizePath,
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(initialSlashRemoved(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)) {
patak-dev marked this conversation as resolved.
Show resolved Hide resolved
continue
} else {
if (state === LexerState.inCall) {
Expand Down
7 changes: 5 additions & 2 deletions packages/vite/src/node/server/middlewares/static.ts
Expand Up @@ -9,6 +9,7 @@ import {
cleanUrl,
fsPathFromId,
fsPathFromUrl,
initialSlashRemoved,
isFileReadable,
isImportRequest,
isInternalRequest,
Expand All @@ -18,6 +19,8 @@ import {
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, initialSlashRemoved(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 initialSlashRemoved(str: string): string {
patak-dev marked this conversation as resolved.
Show resolved Hide resolved
return str.startsWith('/') ? str.slice(1) : str
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

str[0] === '/' ?

image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's interesting, I think we used startsWith in quite a few other places too (and I usually use that). Do you have a link to the benchmark?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quite a big diff... changed here. @sun0day maybe you could change this in other places in another PR if you would like.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://perf.link/

I saw sapphi-red mentioned this link in another pr.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL changes when you do a test in perf.link so you can share that directly instead (or with) the screenshot so others can continue playing and running it on their machines too.

Copy link
Member

@sun0day sun0day Mar 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

=== should be faster according to startsWith source in theory

perf test data[0] === 'x' wins most of the time

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOVE the site! Let's go with [0] === '/' then. It would be good to change this in other places like patak mentioned.

}

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 escapeForRegexRE = /[-/\\^$*+?.()|[\]{}]/g
export function escapeForRegex(str: string): string {
patak-dev marked this conversation as resolved.
Show resolved Hide resolved
return str.replace(escapeForRegexRE, '\\$&')
}