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: in-memory public files check #15195

Merged
merged 12 commits into from
Dec 5, 2023
27 changes: 1 addition & 26 deletions packages/vite/src/node/plugins/asset.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import path from 'node:path'
import { parse as parseUrl } from 'node:url'
import fs from 'node:fs'
import fsp from 'node:fs/promises'
import { Buffer } from 'node:buffer'
import * as mrmime from 'mrmime'
Expand All @@ -17,6 +16,7 @@ import {
} from '../build'
import type { Plugin } from '../plugin'
import type { ResolvedConfig } from '../config'
import { checkPublicFile } from '../publicDir'
import {
cleanUrl,
getHash,
Expand Down Expand Up @@ -249,31 +249,6 @@ export function assetPlugin(config: ResolvedConfig): Plugin {
}
}

export function checkPublicFile(
url: string,
{ publicDir }: ResolvedConfig,
): string | undefined {
// note if the file is in /public, the resolver would have returned it
// as-is so it's not going to be a fully resolved path.
if (!publicDir || url[0] !== '/') {
return
}
const publicFile = path.join(publicDir, cleanUrl(url))
if (
!normalizePath(publicFile).startsWith(
withTrailingSlash(normalizePath(publicDir)),
)
) {
// can happen if URL starts with '../'
return
}
if (fs.existsSync(publicFile)) {
return publicFile
} else {
return
}
}

export async function fileToUrl(
id: string,
config: ResolvedConfig,
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
} from '../constants'
import type { ResolvedConfig } from '../config'
import type { Plugin } from '../plugin'
import { checkPublicFile } from '../publicDir'
import {
arraify,
asyncReplace,
Expand All @@ -62,7 +63,6 @@ import type { Logger } from '../logger'
import { addToHTMLProxyTransformResult } from './html'
import {
assetUrlRE,
checkPublicFile,
fileToUrl,
generatedAssets,
publicAssetUrlCache,
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/plugins/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ import {
urlCanParse,
} from '../utils'
import type { ResolvedConfig } from '../config'
import { checkPublicFile } from '../publicDir'
import { toOutputFilePathInHtml } from '../build'
import { resolveEnvPrefix } from '../env'
import type { Logger } from '../logger'
import {
assetUrlRE,
checkPublicFile,
getPublicAssetFilename,
publicAssetUrlRE,
urlToBuiltUrl,
Expand Down
3 changes: 2 additions & 1 deletion packages/vite/src/node/plugins/importAnalysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,13 @@ import {
withTrailingSlash,
wrapId,
} from '../utils'
import { checkPublicFile } from '../publicDir'
import { getDepOptimizationConfig } from '../config'
import type { ResolvedConfig } from '../config'
import type { Plugin } from '../plugin'
import { shouldExternalizeForSSR } from '../ssr/ssrExternal'
import { getDepsOptimizer, optimizedDepNeedsInterop } from '../optimizer'
import { checkPublicFile, urlRE } from './asset'
import { urlRE } from './asset'
import { throwOutdatedRequest } from './optimizedDeps'
import { isCSSRequest, isDirectCSSRequest } from './css'
import { browserExternalId } from './resolve'
Expand Down
56 changes: 56 additions & 0 deletions packages/vite/src/node/publicDir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import fs from 'node:fs'
import path from 'node:path'
import type { ResolvedConfig } from './config'
import {
cleanUrl,
normalizePath,
recursiveReaddir,
withTrailingSlash,
} from './utils'

const publicFilesMap = new WeakMap<ResolvedConfig, Set<string>>()

export async function initPublicFiles(
config: ResolvedConfig,
): Promise<Set<string>> {
const fileNames = await recursiveReaddir(config.publicDir)
const publicFiles = new Set(
fileNames.map((fileName) => fileName.slice(config.publicDir.length)),
)
publicFilesMap.set(config, publicFiles)
return publicFiles
}

function getPublicFiles(config: ResolvedConfig): Set<string> | undefined {
return publicFilesMap.get(config)
}

export function checkPublicFile(
url: string,
config: ResolvedConfig,
): string | undefined {
// note if the file is in /public, the resolver would have returned it
// as-is so it's not going to be a fully resolved path.
const { publicDir } = config
if (!publicDir || url[0] !== '/') {
return
}

const fileName = cleanUrl(url)

// short-circuit if we have an in-memory publicFiles cache
const publicFiles = getPublicFiles(config)
if (publicFiles) {
return publicFiles.has(fileName)
? normalizePath(path.join(publicDir, fileName))
: undefined
}

const publicFile = normalizePath(path.join(publicDir, fileName))
if (!publicFile.startsWith(withTrailingSlash(normalizePath(publicDir)))) {
// can happen if URL starts with '../'
return
}

return fs.existsSync(publicFile) ? publicFile : undefined
bluwy marked this conversation as resolved.
Show resolved Hide resolved
}
27 changes: 20 additions & 7 deletions packages/vite/src/node/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { CLIENT_DIR, DEFAULT_DEV_PORT } from '../constants'
import type { Logger } from '../logger'
import { printServerUrls } from '../logger'
import { createNoopWatcher, resolveChokidarOptions } from '../watch'
import { initPublicFiles } from '../publicDir'
import type { PluginContainer } from './pluginContainer'
import { ERR_CLOSED_SERVER, createPluginContainer } from './pluginContainer'
import type { WebSocketServer } from './ws'
Expand Down Expand Up @@ -378,6 +379,8 @@ export async function _createServer(
): Promise<ViteDevServer> {
const config = await resolveConfig(inlineConfig, 'serve')

const initPublicFilesPromise = initPublicFiles(config)

const { root, server: serverConfig } = config
const httpsOptions = await resolveHttpsConfig(config.server.https)
const { middlewareMode } = serverConfig
Expand Down Expand Up @@ -623,6 +626,8 @@ export async function _createServer(
}
}

const publicFiles = await initPublicFilesPromise

const onHMRUpdate = async (file: string, configOnly: boolean) => {
if (serverConfig.hmr !== false) {
try {
Expand All @@ -639,17 +644,25 @@ export async function _createServer(
const onFileAddUnlink = async (file: string, isUnlink: boolean) => {
file = normalizePath(file)
await container.watchChange(file, { event: isUnlink ? 'delete' : 'create' })
await handleFileAddUnlink(file, server, isUnlink)
await onHMRUpdate(file, true)

if (config.publicDir && file.startsWith(config.publicDir)) {
publicFiles[isUnlink ? 'delete' : 'add'](
file.slice(config.publicDir.length),
)
} else {
await handleFileAddUnlink(file, server, isUnlink)
await onHMRUpdate(file, true)
}
}

watcher.on('change', async (file) => {
file = normalizePath(file)
await container.watchChange(file, { event: 'update' })
// invalidate module graph cache on file change
moduleGraph.onFileChange(file)

await onHMRUpdate(file, false)
if (!(config.publicDir && file.startsWith(config.publicDir))) {
// invalidate module graph cache on file change
moduleGraph.onFileChange(file)
await onHMRUpdate(file, false)
}
patak-dev marked this conversation as resolved.
Show resolved Hide resolved
})

watcher.on('add', (file) => onFileAddUnlink(file, false))
Expand Down Expand Up @@ -733,7 +746,7 @@ export async function _createServer(
// this applies before the transform middleware so that these files are served
// as-is without transforms.
if (config.publicDir) {
middlewares.use(servePublicMiddleware(server))
middlewares.use(servePublicMiddleware(server, publicFiles))
}

// main transform middleware
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/server/middlewares/indexHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ import {
unwrapId,
wrapId,
} from '../../utils'
import { checkPublicFile } from '../../publicDir'
import { isCSSRequest } from '../../plugins/css'
import { checkPublicFile } from '../../plugins/asset'
import { getCodeWithSourcemap, injectSourcesContent } from '../sourcemap'

interface AssetNode {
Expand Down
29 changes: 22 additions & 7 deletions packages/vite/src/node/server/middlewares/static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import {
isParentDirectory,
isSameFileUri,
isWindows,
normalizePath,
removeLeadingSlash,
shouldServeFile,
slash,
withTrailingSlash,
} from '../../utils'
Expand All @@ -26,10 +26,8 @@ const knownJavascriptExtensionRE = /\.[tj]sx?$/

const sirvOptions = ({
getHeaders,
shouldServe,
}: {
getHeaders: () => OutgoingHttpHeaders | undefined
shouldServe?: (p: string) => void
}): Options => {
return {
dev: true,
Expand All @@ -51,26 +49,43 @@ const sirvOptions = ({
}
}
},
shouldServe,
}
}

export function servePublicMiddleware(
server: ViteDevServer,
publicFiles: Set<string>,
): Connect.NextHandleFunction {
const dir = server.config.publicDir
const serve = sirv(
dir,
sirvOptions({
getHeaders: () => server.config.server.headers,
shouldServe: (filePath) => shouldServeFile(filePath, dir),
}),
)

const toFilePath = (url: string) => {
let filePath = cleanUrl(url)
if (filePath.indexOf('%') !== -1) {
try {
filePath = decodeURI(filePath)
} catch (err) {
/* malform uri */
}
}
return normalizePath(filePath)
}

// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
return function viteServePublicMiddleware(req, res, next) {
// skip import request and internal requests `/@fs/ /@vite-client` etc...
if (isImportRequest(req.url!) || isInternalRequest(req.url!)) {
// To avoid the performance impact of `existsSync` on every request, we check against an
// in-memory set of known public files. This set is updated on restarts.
// also skip import request and internal requests `/@fs/ /@vite-client` etc...
if (
!publicFiles.has(toFilePath(req.url!)) ||
isImportRequest(req.url!) ||
isInternalRequest(req.url!)
) {
return next()
}
serve(req, res, next)
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/server/transformRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
timeFrom,
unwrapId,
} from '../utils'
import { checkPublicFile } from '../plugins/asset'
import { checkPublicFile } from '../publicDir'
import { getDepsOptimizer } from '../optimizer'
import { applySourcemapIgnoreList, injectSourcesContent } from './sourcemap'
import { isFileServingAllowed } from './middlewares/static'
Expand Down
24 changes: 24 additions & 0 deletions packages/vite/src/node/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { builtinModules, createRequire } from 'node:module'
import { promises as dns } from 'node:dns'
import { performance } from 'node:perf_hooks'
import type { AddressInfo, Server } from 'node:net'
import fsp from 'node:fs/promises'
import type { FSWatcher } from 'chokidar'
import remapping from '@ampproject/remapping'
import type { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping'
Expand Down Expand Up @@ -622,6 +623,29 @@ export function copyDir(srcDir: string, destDir: string): void {
}
}

export async function recursiveReaddir(dir: string): Promise<string[]> {
if (!fs.existsSync(dir)) {
return []
}
let dirents: fs.Dirent[]
try {
dirents = await fsp.readdir(dir, { withFileTypes: true })
} catch (e) {
if (e.code === 'EACCES') {
// Ignore permission errors
return []
}
throw e
}
const files = await Promise.all(
dirents.map((dirent) => {
const res = path.resolve(dir, dirent.name)
return dirent.isDirectory() ? recursiveReaddir(res) : normalizePath(res)
}),
)
return files.flat(1)
}

// `fs.realpathSync.native` resolves differently in Windows network drive,
// causing file read errors. skip for now.
// https://github.com/nodejs/node/issues/37737
Expand Down