Skip to content

Commit

Permalink
perf: in-memory public files check (#15195)
Browse files Browse the repository at this point in the history
Co-authored-by: 翠 / green <green@sapphi.red>
  • Loading branch information
patak-dev and sapphi-red committed Dec 5, 2023
1 parent 35a5bcf commit 0f9e1bf
Show file tree
Hide file tree
Showing 10 changed files with 121 additions and 40 deletions.
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
}
14 changes: 12 additions & 2 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,6 +644,12 @@ export async function _createServer(
const onFileAddUnlink = async (file: string, isUnlink: boolean) => {
file = normalizePath(file)
await container.watchChange(file, { event: isUnlink ? 'delete' : 'create' })

if (config.publicDir && file.startsWith(config.publicDir)) {
publicFiles[isUnlink ? 'delete' : 'add'](
file.slice(config.publicDir.length),
)
}
await handleFileAddUnlink(file, server, isUnlink)
await onHMRUpdate(file, true)
}
Expand All @@ -648,7 +659,6 @@ export async function _createServer(
await container.watchChange(file, { event: 'update' })
// invalidate module graph cache on file change
moduleGraph.onFileChange(file)

await onHMRUpdate(file, false)
})

Expand Down Expand Up @@ -733,7 +743,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

0 comments on commit 0f9e1bf

Please sign in to comment.