From 4d666abdd1157cdd700b01962ef28229ee4307d4 Mon Sep 17 00:00:00 2001 From: patak Date: Thu, 7 Dec 2023 12:21:44 +0100 Subject: [PATCH 01/22] perf: cached fs utils --- packages/vite/src/node/fsUtils.ts | 268 ++++++++++++++++++ .../vite/src/node/plugins/importAnalysis.ts | 5 +- packages/vite/src/node/plugins/index.ts | 2 + packages/vite/src/node/plugins/preAlias.ts | 5 +- packages/vite/src/node/plugins/resolve.ts | 76 ++--- packages/vite/src/node/server/index.ts | 27 +- .../node/server/middlewares/htmlFallback.ts | 10 +- .../src/node/server/middlewares/indexHtml.ts | 7 +- 8 files changed, 337 insertions(+), 63 deletions(-) create mode 100644 packages/vite/src/node/fsUtils.ts diff --git a/packages/vite/src/node/fsUtils.ts b/packages/vite/src/node/fsUtils.ts new file mode 100644 index 00000000000000..6ed7ab7cdcd9ae --- /dev/null +++ b/packages/vite/src/node/fsUtils.ts @@ -0,0 +1,268 @@ +import fs from 'node:fs' +// import fsp from 'node:fs/promises' +import path from 'node:path' +import type { ResolvedConfig } from './config' +import { normalizePath, safeRealpathSync, tryStatSync } from './utils' + +export interface FsUtils { + existsSync: (path: string) => boolean + tryResolveRealFile: ( + path: string, + preserveSymlinks?: boolean, + ) => string | undefined + tryResolveRealFileOrType: ( + path: string, + preserveSymlinks?: boolean, + ) => { path?: string; type: 'directory' | 'file' } | undefined + isDirectory: (path: string) => boolean + + onFileAdd?: (file: string) => void + onFileUnlink?: (file: string) => void + onDirectoryAdd?: (file: string) => void + onDirectoryUnlink?: (file: string) => void +} + +// An implementation of fsUtils without caching +export const commonFsUtils: FsUtils = { + existsSync: fs.existsSync, + tryResolveRealFile, + tryResolveRealFileOrType, + isDirectory, +} + +const cachedFsUtilsMap = new WeakMap() +export function getFsUtils(config: ResolvedConfig): FsUtils { + if (config.command !== 'serve') { + // cached fsUtils is only used in the dev server for now + return commonFsUtils + } + let fsUtils = cachedFsUtilsMap.get(config) + if (!fsUtils) { + fsUtils = createCachedFsUtils(config) + cachedFsUtilsMap.set(config, fsUtils) + } + return fsUtils +} + +type DirentsMap = Map + +type DirentCacheType = 'directory' | 'file' | 'symlink' | 'error' + +interface DirentCache { + dirents?: DirentsMap | Promise + type: DirentCacheType +} + +function readDirCacheSync(file: string): undefined | DirentsMap { + let dirents: fs.Dirent[] + try { + dirents = fs.readdirSync(file, { withFileTypes: true }) + } catch { + return + } + return direntsToDirentMap(dirents) +} + +function direntsToDirentMap(fsDirents: fs.Dirent[]): DirentsMap { + const dirents = new Map() + for (const dirent of fsDirents) { + // We ignore non directory, file, and symlink entries + const type = dirent.isDirectory() + ? 'directory' + : dirent.isSymbolicLink() + ? 'symlink' + : dirent.isFile() + ? 'file' + : undefined + if (type) { + dirents.set(dirent.name, { type }) + } + } + return dirents +} + +export function createCachedFsUtils(config: ResolvedConfig): FsUtils { + const { root } = config + const rootDirPath = `${root}/` + const rootCache = { type: 'directory' as DirentCacheType } // dirents will be computed lazily + if (!rootCache) { + return commonFsUtils + } + + const getDirentCacheSync = (parts: string[]): DirentCache | undefined => { + let direntCache: DirentCache = rootCache + for (let i = 0; i < parts.length; i++) { + if (direntCache.type === 'directory') { + if (!direntCache.dirents || direntCache.dirents instanceof Promise) { + const dirPath = path.posix.join(root, ...parts.slice(0, i)) + const dirents = readDirCacheSync(dirPath) + if (!dirents) { + direntCache.type = 'error' + return + } + direntCache.dirents = dirents + } + const nextDirentCache = direntCache.dirents.get(parts[i]) + if (!nextDirentCache) { + return + } + direntCache = nextDirentCache + } else if (direntCache.type === 'symlink') { + // early return if we encounter a symlink + return direntCache + } else if (direntCache.type === 'error') { + return direntCache + } else if (direntCache.type === 'file') { + return i === parts.length - 1 ? direntCache : undefined + } + } + return direntCache + } + + function getDirentCacheFromPath( + file: string, + ): DirentCache | false | undefined { + const normalizedFile = normalizePath(file) + if (normalizedFile === root) { + return rootCache + } + if ( + !normalizedFile.startsWith(rootDirPath) || + normalizedFile.includes('/node_modules/') + ) { + return undefined + } + const pathFromRoot = normalizedFile.slice(rootDirPath.length) + const parts = pathFromRoot.split('/') + const direntCache = getDirentCacheSync(parts) + if (!direntCache || direntCache.type === 'error') { + return false + } + return direntCache + } + + function onPathAdd(file: string): void { + const direntCache = getDirentCacheFromPath(path.dirname(file)) + if (direntCache && direntCache.type === 'directory') { + // We don't know if the file is a symlink or not for the stats + // in the chokidar callback, so we delete the direntCache for the + // parent directory and let the next call to fsUtils recreate it + direntCache.dirents = undefined + } + } + + async function onPathUnlink(file: string): Promise { + const direntCache = getDirentCacheFromPath(path.dirname(file)) + if (direntCache && direntCache.type === 'directory') { + if (direntCache.dirents) { + const dirents = await direntCache.dirents + dirents.delete(path.basename(file)) + } + } + } + + return { + existsSync(file: string) { + const direntCache = getDirentCacheFromPath(file) + if ( + direntCache === undefined || + (direntCache && direntCache.type === 'symlink') + ) { + // fallback to built-in fs for out-of-root and symlinked files + return fs.existsSync(file) + } + return !!direntCache + }, + tryResolveRealFile( + file: string, + preserveSymlinks?: boolean, + ): string | undefined { + const direntCache = getDirentCacheFromPath(file) + if ( + direntCache === undefined || + (direntCache && direntCache.type === 'symlink') + ) { + // fallback to built-in fs for out-of-root and symlinked files + return tryResolveRealFile(file, preserveSymlinks) + } + if (!direntCache || direntCache.type === 'directory') { + return + } + // We can avoid getRealPath even if preserveSymlinks is false because we know it's + // a file without symlinks in its path + return normalizePath(file) + }, + tryResolveRealFileOrType( + file: string, + preserveSymlinks?: boolean, + ): { path?: string; type: 'directory' | 'file' } | undefined { + const direntCache = getDirentCacheFromPath(file) + if ( + direntCache === undefined || + (direntCache && direntCache.type === 'symlink') + ) { + // fallback to built-in fs for out-of-root and symlinked files + return tryResolveRealFileOrType(file, preserveSymlinks) + } + if (!direntCache) { + return + } + if (direntCache.type === 'directory') { + return { type: 'directory' } + } + // We can avoid getRealPath even if preserveSymlinks is false because we know it's + // a file without symlinks in its path + return { path: normalizePath(file), type: 'file' } + }, + isDirectory(path: string) { + const direntCache = getDirentCacheFromPath(path) + if ( + direntCache === undefined || + (direntCache && direntCache.type === 'symlink') + ) { + // fallback to built-in fs for out-of-root and symlinked files + return isDirectory(path) + } + return direntCache && direntCache.type === 'directory' + }, + + onFileAdd: onPathAdd, + onFileUnlink: onPathUnlink, + onDirectoryAdd: onPathAdd, + onDirectoryUnlink: onPathUnlink, + } +} + +function tryResolveRealFile( + file: string, + preserveSymlinks?: boolean, +): string | undefined { + const stat = tryStatSync(file) + if (stat?.isFile()) return getRealPath(file, preserveSymlinks) +} + +function tryResolveRealFileOrType( + file: string, + preserveSymlinks?: boolean, +): { path?: string; type: 'directory' | 'file' } | undefined { + const fileStat = tryStatSync(file) + if (fileStat?.isFile()) { + return { path: getRealPath(file, preserveSymlinks), type: 'file' } + } + if (fileStat?.isDirectory()) { + return { type: 'directory' } + } + return +} + +function getRealPath(resolved: string, preserveSymlinks?: boolean): string { + if (!preserveSymlinks) { + resolved = safeRealpathSync(resolved) + } + return normalizePath(resolved) +} + +function isDirectory(path: string): boolean { + const stat = tryStatSync(path) + return stat?.isDirectory() ?? false +} diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index 89b7238ddf197a..575eeeb35646ae 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -1,4 +1,3 @@ -import fs from 'node:fs' import path from 'node:path' import { performance } from 'node:perf_hooks' import colors from 'picocolors' @@ -52,6 +51,7 @@ import { withTrailingSlash, wrapId, } from '../utils' +import { getFsUtils } from '../fsUtils' import { checkPublicFile } from '../publicDir' import { getDepOptimizationConfig } from '../config' import type { ResolvedConfig } from '../config' @@ -174,6 +174,7 @@ function extractImportedBindings( */ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { const { root, base } = config + const fsUtils = getFsUtils(config) const clientPublicPath = path.posix.join(base, CLIENT_PUBLIC_PATH) const enablePartialAccept = config.experimental?.hmrPartialAccept let server: ViteDevServer @@ -338,7 +339,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { } else if ( depsOptimizer?.isOptimizedDepFile(resolved.id) || (path.isAbsolute(cleanUrl(resolved.id)) && - fs.existsSync(cleanUrl(resolved.id))) + fsUtils.existsSync(cleanUrl(resolved.id))) ) { // an optimized deps may not yet exists in the filesystem, or // a regular file exists but is out of root: rewrite to absolute /@fs/ paths diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index cefc11bb147ebb..87f8aea70d630c 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -6,6 +6,7 @@ import type { HookHandler, Plugin, PluginWithRequiredHook } from '../plugin' import { getDepsOptimizer } from '../optimizer' import { shouldExternalizeForSSR } from '../ssr/ssrExternal' import { watchPackageDataPlugin } from '../packages' +import { getFsUtils } from '../fsUtils' import { jsonPlugin } from './json' import { resolvePlugin } from './resolve' import { optimizedDepsBuildPlugin, optimizedDepsPlugin } from './optimizedDeps' @@ -67,6 +68,7 @@ export async function resolvePlugins( packageCache: config.packageCache, ssrConfig: config.ssr, asSrc: true, + fsUtils: getFsUtils(config), getDepsOptimizer: (ssr: boolean) => getDepsOptimizer(config, ssr), shouldExternalize: isBuild && config.build.ssr diff --git a/packages/vite/src/node/plugins/preAlias.ts b/packages/vite/src/node/plugins/preAlias.ts index 029bca0ac9f067..33df60b2d58b2e 100644 --- a/packages/vite/src/node/plugins/preAlias.ts +++ b/packages/vite/src/node/plugins/preAlias.ts @@ -1,4 +1,3 @@ -import fs from 'node:fs' import path from 'node:path' import type { Alias, @@ -16,6 +15,7 @@ import { moduleListContains, withTrailingSlash, } from '../utils' +import { getFsUtils } from '../fsUtils' import { getDepsOptimizer } from '../optimizer' import { tryOptimizedResolve } from './resolve' @@ -26,6 +26,7 @@ export function preAliasPlugin(config: ResolvedConfig): Plugin { const findPatterns = getAliasPatterns(config.resolve.alias) const isConfiguredAsExternal = createIsConfiguredAsSsrExternal(config) const isBuild = config.command === 'build' + const fsUtils = getFsUtils(config) return { name: 'vite:pre-alias', async resolveId(id, importer, options) { @@ -63,7 +64,7 @@ export function preAliasPlugin(config: ResolvedConfig): Plugin { const isVirtual = resolvedId === id || resolvedId.includes('\0') if ( !isVirtual && - fs.existsSync(resolvedId) && + fsUtils.existsSync(resolvedId) && !moduleListContains(optimizeDeps.exclude, id) && path.isAbsolute(resolvedId) && (isInNodeModules(resolvedId) || diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index 4131f08f2cb7de..79356e2db087e8 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -43,6 +43,8 @@ import { optimizedDepInfoFromFile, optimizedDepInfoFromId } from '../optimizer' import type { DepsOptimizer } from '../optimizer' import type { SSROptions } from '..' import type { PackageCache, PackageData } from '../packages' +import type { FsUtils } from '../fsUtils' +import { commonFsUtils } from '../fsUtils' import { findNearestMainPackageData, findNearestPackageData, @@ -92,6 +94,7 @@ export interface InternalResolveOptions extends Required { isProduction: boolean ssrConfig?: SSROptions packageCache?: PackageCache + fsUtils?: FsUtils /** * src code mode also attempts the following: * - resolving /xxx as URLs @@ -564,21 +567,22 @@ function tryCleanFsResolve( targetWeb = true, skipPackageJson = false, ): string | undefined { - const { tryPrefix, extensions, preserveSymlinks } = options + const { tryPrefix } = options - const fileStat = tryStatSync(file) + // Optimization to get the real type or file type (directory, file, other) + const fileResult = ( + options.fsUtils ?? commonFsUtils + ).tryResolveRealFileOrType(file, options.preserveSymlinks) - // Try direct match first - if (fileStat?.isFile()) return getRealPath(file, options.preserveSymlinks) + if (fileResult?.path) return fileResult.path let res: string | undefined // If path.dirname is a valid directory, try extensions and ts resolution logic const possibleJsToTs = options.isFromTsImporter && isPossibleTsOutput(file) - if (possibleJsToTs || extensions.length || tryPrefix) { + if (possibleJsToTs || options.extensions.length || tryPrefix) { const dirPath = path.dirname(file) - const dirStat = tryStatSync(dirPath) - if (dirStat?.isDirectory()) { + if ((options.fsUtils ?? commonFsUtils).isDirectory(dirPath)) { if (possibleJsToTs) { // try resolve .js, .mjs, .cjs or .jsx import to typescript file const fileExt = path.extname(file) @@ -586,45 +590,32 @@ function tryCleanFsResolve( if ( (res = tryResolveRealFile( fileName + fileExt.replace('js', 'ts'), - preserveSymlinks, + options, )) ) return res // for .js, also try .tsx if ( fileExt === '.js' && - (res = tryResolveRealFile(fileName + '.tsx', preserveSymlinks)) + (res = tryResolveRealFile(fileName + '.tsx', options)) ) return res } - if ( - (res = tryResolveRealFileWithExtensions( - file, - extensions, - preserveSymlinks, - )) - ) - return res + if ((res = tryResolveRealFileWithExtensions(file, options))) return res if (tryPrefix) { const prefixed = `${dirPath}/${options.tryPrefix}${path.basename(file)}` - if ((res = tryResolveRealFile(prefixed, preserveSymlinks))) return res + if ((res = tryResolveRealFile(prefixed, options))) return res - if ( - (res = tryResolveRealFileWithExtensions( - prefixed, - extensions, - preserveSymlinks, - )) - ) + if ((res = tryResolveRealFileWithExtensions(prefixed, options))) return res } } } - if (tryIndex && fileStat) { + if (tryIndex && fileResult?.type === 'directory') { // Path points to a directory, check for package.json and entry and /index file const dirPath = file @@ -646,21 +637,14 @@ function tryCleanFsResolve( } } - if ( - (res = tryResolveRealFileWithExtensions( - `${dirPath}/index`, - extensions, - preserveSymlinks, - )) - ) + if ((res = tryResolveRealFileWithExtensions(`${dirPath}/index`, options))) return res if (tryPrefix) { if ( (res = tryResolveRealFileWithExtensions( `${dirPath}/${options.tryPrefix}index`, - extensions, - preserveSymlinks, + options, )) ) return res @@ -670,19 +654,20 @@ function tryCleanFsResolve( function tryResolveRealFile( file: string, - preserveSymlinks: boolean, + options: InternalResolveOptions, ): string | undefined { - const stat = tryStatSync(file) - if (stat?.isFile()) return getRealPath(file, preserveSymlinks) + return (options.fsUtils ?? commonFsUtils).tryResolveRealFile( + file, + options.preserveSymlinks, + ) } function tryResolveRealFileWithExtensions( filePath: string, - extensions: string[], - preserveSymlinks: boolean, + options: InternalResolveOptions, ): string | undefined { - for (const ext of extensions) { - const res = tryResolveRealFile(filePath + ext, preserveSymlinks) + for (const ext of options.extensions) { + const res = tryResolveRealFile(filePath + ext, options) if (res) return res } } @@ -1297,10 +1282,3 @@ function mapWithBrowserField( function equalWithoutSuffix(path: string, key: string, suffix: string) { return key.endsWith(suffix) && key.slice(0, -suffix.length) === path } - -function getRealPath(resolved: string, preserveSymlinks?: boolean): string { - if (!preserveSymlinks && browserExternalId !== resolved) { - resolved = safeRealpathSync(resolved) - } - return normalizePath(resolved) -} diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 2b799dbfd2b8e7..6d3c4636e24c57 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -35,6 +35,7 @@ import { resolveHostname, resolveServerUrls, } from '../utils' +import { getFsUtils } from '../fsUtils' import { ssrLoadModule } from '../ssr/ssrModuleLoader' import { ssrFixStacktrace, ssrRewriteStacktrace } from '../ssr/ssrStacktrace' import { ssrTransform } from '../ssr/ssrTransform' @@ -662,8 +663,22 @@ export async function _createServer( await onHMRUpdate(file, false) }) - watcher.on('add', (file) => onFileAddUnlink(file, false)) - watcher.on('unlink', (file) => onFileAddUnlink(file, true)) + const fsUtils = getFsUtils(config) + + watcher.on('add', (file) => { + onFileAddUnlink(file, false) + fsUtils.onFileAdd?.(file) + }) + watcher.on('unlink', (file) => { + onFileAddUnlink(file, true) + fsUtils.onFileUnlink?.(file) + }) + watcher.on('addDir', (dir) => { + fsUtils.onDirectoryAdd?.(dir) + }) + watcher.on('unlinkDir', (dir) => { + fsUtils.onDirectoryUnlink?.(dir) + }) ws.on('vite:invalidate', async ({ path, message }: InvalidatePayload) => { const mod = moduleGraph.urlToModuleMap.get(path) @@ -755,7 +770,13 @@ export async function _createServer( // html fallback if (config.appType === 'spa' || config.appType === 'mpa') { - middlewares.use(htmlFallbackMiddleware(root, config.appType === 'spa')) + middlewares.use( + htmlFallbackMiddleware( + root, + config.appType === 'spa', + getFsUtils(config), + ), + ) } // run post config hooks diff --git a/packages/vite/src/node/server/middlewares/htmlFallback.ts b/packages/vite/src/node/server/middlewares/htmlFallback.ts index 5af9a34b2df2dd..0662c413062077 100644 --- a/packages/vite/src/node/server/middlewares/htmlFallback.ts +++ b/packages/vite/src/node/server/middlewares/htmlFallback.ts @@ -1,13 +1,15 @@ -import fs from 'node:fs' import path from 'node:path' import type { Connect } from 'dep-types/connect' import { cleanUrl, createDebugger } from '../../utils' +import type { FsUtils } from '../../fsUtils' +import { commonFsUtils } from '../../fsUtils' const debug = createDebugger('vite:html-fallback') export function htmlFallbackMiddleware( root: string, spaFallback: boolean, + fsUtils: FsUtils = commonFsUtils, ): Connect.NextHandleFunction { // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...` return function viteHtmlFallbackMiddleware(req, res, next) { @@ -32,7 +34,7 @@ export function htmlFallbackMiddleware( // so we need to check if the file exists if (pathname.endsWith('.html')) { const filePath = path.join(root, pathname) - if (fs.existsSync(filePath)) { + if (fsUtils.existsSync(filePath)) { debug?.(`Rewriting ${req.method} ${req.url} to ${url}`) req.url = url return next() @@ -41,7 +43,7 @@ export function htmlFallbackMiddleware( // trailing slash should check for fallback index.html else if (pathname[pathname.length - 1] === '/') { const filePath = path.join(root, pathname, 'index.html') - if (fs.existsSync(filePath)) { + if (fsUtils.existsSync(filePath)) { const newUrl = url + 'index.html' debug?.(`Rewriting ${req.method} ${req.url} to ${newUrl}`) req.url = newUrl @@ -51,7 +53,7 @@ export function htmlFallbackMiddleware( // non-trailing slash should check for fallback .html else { const filePath = path.join(root, pathname + '.html') - if (fs.existsSync(filePath)) { + if (fsUtils.existsSync(filePath)) { const newUrl = url + '.html' debug?.(`Rewriting ${req.method} ${req.url} to ${newUrl}`) req.url = newUrl diff --git a/packages/vite/src/node/server/middlewares/indexHtml.ts b/packages/vite/src/node/server/middlewares/indexHtml.ts index 54351a937c8e1f..b56f8c3cf94bb2 100644 --- a/packages/vite/src/node/server/middlewares/indexHtml.ts +++ b/packages/vite/src/node/server/middlewares/indexHtml.ts @@ -1,4 +1,3 @@ -import fs from 'node:fs' import fsp from 'node:fs/promises' import path from 'node:path' import MagicString from 'magic-string' @@ -41,6 +40,7 @@ import { unwrapId, wrapId, } from '../../utils' +import { getFsUtils } from '../../fsUtils' import { checkPublicFile } from '../../publicDir' import { isCSSRequest } from '../../plugins/css' import { getCodeWithSourcemap, injectSourcesContent } from '../sourcemap' @@ -175,7 +175,7 @@ const devHtmlHook: IndexHtmlTransformHook = async ( let proxyModuleUrl: string const trailingSlash = htmlPath.endsWith('/') - if (!trailingSlash && fs.existsSync(filename)) { + if (!trailingSlash && getFsUtils(config).existsSync(filename)) { proxyModulePath = htmlPath proxyModuleUrl = joinUrlSegments(base, htmlPath) } else { @@ -393,6 +393,7 @@ export function indexHtmlMiddleware( server: ViteDevServer | PreviewServer, ): Connect.NextHandleFunction { const isDev = isDevServer(server) + const fsUtils = getFsUtils(server.config) // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...` return async function viteIndexHtmlMiddleware(req, res, next) { @@ -410,7 +411,7 @@ export function indexHtmlMiddleware( filePath = path.join(root, decodeURIComponent(url)) } - if (fs.existsSync(filePath)) { + if (fsUtils.existsSync(filePath)) { const headers = isDev ? server.config.server.headers : server.config.preview.headers From 524a52af4364130d78eecb7381f00a853ac62eb5 Mon Sep 17 00:00:00 2001 From: patak Date: Thu, 7 Dec 2023 15:18:17 +0100 Subject: [PATCH 02/22] fix: temporal guard for custom watchers --- packages/vite/src/node/fsUtils.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/fsUtils.ts b/packages/vite/src/node/fsUtils.ts index 6ed7ab7cdcd9ae..e4583beaaab125 100644 --- a/packages/vite/src/node/fsUtils.ts +++ b/packages/vite/src/node/fsUtils.ts @@ -32,8 +32,13 @@ export const commonFsUtils: FsUtils = { const cachedFsUtilsMap = new WeakMap() export function getFsUtils(config: ResolvedConfig): FsUtils { - if (config.command !== 'serve') { - // cached fsUtils is only used in the dev server for now + if ( + config.command !== 'serve' || + config.server.watch === null || + config.server.watch?.ignored + ) { + // cached fsUtils is only used in the dev server for now, and only when the watcher isn't configured + // we can support custom ignored patterns later return commonFsUtils } let fsUtils = cachedFsUtilsMap.get(config) From c929f967bbc7757a8833f8060a72f41bf7b56f96 Mon Sep 17 00:00:00 2001 From: patak Date: Thu, 7 Dec 2023 18:16:42 +0100 Subject: [PATCH 03/22] feat: add experimental server.fs.cacheChecks --- packages/vite/src/node/fsUtils.ts | 27 ++++++++++++++++---------- packages/vite/src/node/server/index.ts | 9 +++++++++ playground/html/vite.config.js | 3 +++ 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/packages/vite/src/node/fsUtils.ts b/packages/vite/src/node/fsUtils.ts index e4583beaaab125..77239a03cafc67 100644 --- a/packages/vite/src/node/fsUtils.ts +++ b/packages/vite/src/node/fsUtils.ts @@ -1,6 +1,7 @@ import fs from 'node:fs' // import fsp from 'node:fs/promises' import path from 'node:path' +import colors from 'picocolors' import type { ResolvedConfig } from './config' import { normalizePath, safeRealpathSync, tryStatSync } from './utils' @@ -32,18 +33,24 @@ export const commonFsUtils: FsUtils = { const cachedFsUtilsMap = new WeakMap() export function getFsUtils(config: ResolvedConfig): FsUtils { - if ( - config.command !== 'serve' || - config.server.watch === null || - config.server.watch?.ignored - ) { - // cached fsUtils is only used in the dev server for now, and only when the watcher isn't configured - // we can support custom ignored patterns later - return commonFsUtils - } let fsUtils = cachedFsUtilsMap.get(config) if (!fsUtils) { - fsUtils = createCachedFsUtils(config) + if (config.command !== 'serve' || !config.server.fs.cachedChecks) { + // cached fsUtils is only used in the dev server for now, and only when the watcher isn't configured + // we can support custom ignored patterns later + fsUtils = commonFsUtils + } else if (config.server.watch === null || config.server.watch?.ignored) { + config.logger.warn( + colors.yellow( + `${colors.bold( + `(!)`, + )} server.fs.cachedChecks isn't supported if server.watch is null or a custom server.watch.ignored is configured\n`, + ), + ) + fsUtils = commonFsUtils + } else { + fsUtils = createCachedFsUtils(config) + } cachedFsUtilsMap.set(config, fsUtils) } return fsUtils diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 5c00ae514df67d..905dfe5ceb8b47 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -189,6 +189,14 @@ export interface FileSystemServeOptions { * @default ['.env', '.env.*', '*.crt', '*.pem'] */ deny?: string[] + + /** + * Enable caching of fs calls. + * + * @experimental + * @default false + */ + cachedChecks?: boolean } export type ServerHook = ( @@ -944,6 +952,7 @@ export function resolveServerOptions( strict: server.fs?.strict ?? true, allow: allowDirs, deny, + cachedChecks: server.fs?.cachedChecks ?? false, } if (server.origin?.endsWith('/')) { diff --git a/playground/html/vite.config.js b/playground/html/vite.config.js index 625a990e531891..a7f3964cb165bc 100644 --- a/playground/html/vite.config.js +++ b/playground/html/vite.config.js @@ -44,6 +44,9 @@ export default defineConfig({ }, server: { + fs: { + cachedChecks: true, + }, warmup: { clientFiles: ['./warmup/*'], }, From 2fbf00aa90fe927d8a0a8a5b789a0fb8f007ae9b Mon Sep 17 00:00:00 2001 From: patak Date: Thu, 7 Dec 2023 20:42:35 +0100 Subject: [PATCH 04/22] feat: also enable fsUtils in createResolver --- packages/vite/src/node/config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 13db8fdab595fe..a8d61f03a5a090 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -41,6 +41,7 @@ import { normalizePath, withTrailingSlash, } from './utils' +import { getFsUtils } from './fsUtils' import { createPluginHookUtils, getHookHandler, @@ -633,6 +634,7 @@ export async function resolveConfig( tryIndex: true, ...options, idOnly: true, + fsUtils: getFsUtils(resolved), }), ], })) From 2f25f9eba3ea1708c0f2a0bf4847e259619dcda9 Mon Sep 17 00:00:00 2001 From: patak Date: Thu, 7 Dec 2023 20:49:51 +0100 Subject: [PATCH 05/22] chore: update types --- packages/vite/src/node/fsUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/fsUtils.ts b/packages/vite/src/node/fsUtils.ts index 77239a03cafc67..5209b6f6b33e62 100644 --- a/packages/vite/src/node/fsUtils.ts +++ b/packages/vite/src/node/fsUtils.ts @@ -56,7 +56,7 @@ export function getFsUtils(config: ResolvedConfig): FsUtils { return fsUtils } -type DirentsMap = Map +type DirentsMap = Map type DirentCacheType = 'directory' | 'file' | 'symlink' | 'error' From a343c66979465d8a3e562e869e7cdf9bcc32781a Mon Sep 17 00:00:00 2001 From: patak Date: Thu, 7 Dec 2023 22:46:08 +0100 Subject: [PATCH 06/22] feat: optimize on add file or directory --- packages/vite/src/node/fsUtils.ts | 55 +++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/packages/vite/src/node/fsUtils.ts b/packages/vite/src/node/fsUtils.ts index 5209b6f6b33e62..be75d7006d64aa 100644 --- a/packages/vite/src/node/fsUtils.ts +++ b/packages/vite/src/node/fsUtils.ts @@ -58,7 +58,13 @@ export function getFsUtils(config: ResolvedConfig): FsUtils { type DirentsMap = Map -type DirentCacheType = 'directory' | 'file' | 'symlink' | 'error' +type DirentCacheType = + | 'directory' + | 'file' + | 'symlink' + | 'error' + | 'directory_maybe_symlink' + | 'file_maybe_symlink' interface DirentCache { dirents?: DirentsMap | Promise @@ -105,8 +111,9 @@ export function createCachedFsUtils(config: ResolvedConfig): FsUtils { let direntCache: DirentCache = rootCache for (let i = 0; i < parts.length; i++) { if (direntCache.type === 'directory') { + let dirPath if (!direntCache.dirents || direntCache.dirents instanceof Promise) { - const dirPath = path.posix.join(root, ...parts.slice(0, i)) + dirPath = path.posix.join(root, ...parts.slice(0, i)) const dirents = readDirCacheSync(dirPath) if (!dirents) { direntCache.type = 'error' @@ -118,14 +125,33 @@ export function createCachedFsUtils(config: ResolvedConfig): FsUtils { if (!nextDirentCache) { return } + if (nextDirentCache.type === 'directory_maybe_symlink') { + dirPath ??= path.posix.join(root, ...parts.slice(0, i)) + const isSymlink = fs + .lstatSync(dirPath, { throwIfNoEntry: false }) + ?.isSymbolicLink() + direntCache.type = isSymlink ? 'symlink' : 'directory' + } direntCache = nextDirentCache } else if (direntCache.type === 'symlink') { // early return if we encounter a symlink return direntCache } else if (direntCache.type === 'error') { return direntCache - } else if (direntCache.type === 'file') { - return i === parts.length - 1 ? direntCache : undefined + } else { + if (i !== parts.length - 1) { + return + } + if (direntCache.type === 'file_maybe_symlink') { + const filePath = path.posix.join(root, ...parts.slice(0, i)) + const isSymlink = fs + .lstatSync(filePath, { throwIfNoEntry: false }) + ?.isSymbolicLink() + direntCache.type = isSymlink ? 'symlink' : 'file' + return direntCache + } else if (direntCache.type === 'file') { + return direntCache + } } } return direntCache @@ -153,13 +179,18 @@ export function createCachedFsUtils(config: ResolvedConfig): FsUtils { return direntCache } - function onPathAdd(file: string): void { + async function onPathAdd( + file: string, + type: 'directory_maybe_symlink' | 'file_maybe_symlink', + ): Promise { const direntCache = getDirentCacheFromPath(path.dirname(file)) if (direntCache && direntCache.type === 'directory') { // We don't know if the file is a symlink or not for the stats - // in the chokidar callback, so we delete the direntCache for the - // parent directory and let the next call to fsUtils recreate it - direntCache.dirents = undefined + // in the chokidar callback, so we add it with type unknown. + // If it is accessed, we'll do a new fs call to get the real type. + if (direntCache.dirents) { + ;(await direntCache.dirents).set(path.basename(file), { type }) + } } } @@ -238,9 +269,13 @@ export function createCachedFsUtils(config: ResolvedConfig): FsUtils { return direntCache && direntCache.type === 'directory' }, - onFileAdd: onPathAdd, + onFileAdd(file) { + onPathAdd(file, 'file_maybe_symlink') + }, onFileUnlink: onPathUnlink, - onDirectoryAdd: onPathAdd, + onDirectoryAdd(file) { + onPathAdd(file, 'directory_maybe_symlink') + }, onDirectoryUnlink: onPathUnlink, } } From 89df6d5b7e69a883e2a57474b0f1c2fe090eeb31 Mon Sep 17 00:00:00 2001 From: patak Date: Fri, 8 Dec 2023 11:59:43 +0100 Subject: [PATCH 07/22] feat: optimize tryResolveRealFileWithExtensions --- packages/vite/src/node/fsUtils.ts | 98 +++++++++++++++++++++-- packages/vite/src/node/plugins/resolve.ts | 77 ++++++++++-------- 2 files changed, 132 insertions(+), 43 deletions(-) diff --git a/packages/vite/src/node/fsUtils.ts b/packages/vite/src/node/fsUtils.ts index be75d7006d64aa..458fab13958249 100644 --- a/packages/vite/src/node/fsUtils.ts +++ b/packages/vite/src/node/fsUtils.ts @@ -7,15 +7,21 @@ import { normalizePath, safeRealpathSync, tryStatSync } from './utils' export interface FsUtils { existsSync: (path: string) => boolean + isDirectory: (path: string) => boolean + tryResolveRealFile: ( path: string, preserveSymlinks?: boolean, ) => string | undefined + tryResolveRealFileWithExtensions: ( + path: string, + extensions: string[], + preserveSymlinks?: boolean, + ) => string | undefined tryResolveRealFileOrType: ( path: string, preserveSymlinks?: boolean, ) => { path?: string; type: 'directory' | 'file' } | undefined - isDirectory: (path: string) => boolean onFileAdd?: (file: string) => void onFileUnlink?: (file: string) => void @@ -26,9 +32,11 @@ export interface FsUtils { // An implementation of fsUtils without caching export const commonFsUtils: FsUtils = { existsSync: fs.existsSync, + isDirectory, + tryResolveRealFile, + tryResolveRealFileWithExtensions, tryResolveRealFileOrType, - isDirectory, } const cachedFsUtilsMap = new WeakMap() @@ -99,6 +107,19 @@ function direntsToDirentMap(fsDirents: fs.Dirent[]): DirentsMap { return dirents } +function ensureFileMaybeSymlinkIsResolved( + direntCache: DirentCache, + filePath: string, +) { + if (direntCache.type !== 'file_maybe_symlink') return + + const isSymlink = fs + .lstatSync(filePath, { throwIfNoEntry: false }) + ?.isSymbolicLink() + direntCache.type = + isSymlink === undefined ? 'error' : isSymlink ? 'symlink' : 'file' +} + export function createCachedFsUtils(config: ResolvedConfig): FsUtils { const { root } = config const rootDirPath = `${root}/` @@ -121,7 +142,7 @@ export function createCachedFsUtils(config: ResolvedConfig): FsUtils { } direntCache.dirents = dirents } - const nextDirentCache = direntCache.dirents.get(parts[i]) + const nextDirentCache = direntCache.dirents!.get(parts[i]) if (!nextDirentCache) { return } @@ -143,14 +164,15 @@ export function createCachedFsUtils(config: ResolvedConfig): FsUtils { return } if (direntCache.type === 'file_maybe_symlink') { - const filePath = path.posix.join(root, ...parts.slice(0, i)) - const isSymlink = fs - .lstatSync(filePath, { throwIfNoEntry: false }) - ?.isSymbolicLink() - direntCache.type = isSymlink ? 'symlink' : 'file' + ensureFileMaybeSymlinkIsResolved( + direntCache, + path.posix.join(root, ...parts.slice(0, i)), + ) return direntCache } else if (direntCache.type === 'file') { return direntCache + } else { + return } } } @@ -235,6 +257,55 @@ export function createCachedFsUtils(config: ResolvedConfig): FsUtils { // a file without symlinks in its path return normalizePath(file) }, + tryResolveRealFileWithExtensions( + file: string, + extensions: string[], + preserveSymlinks?: boolean, + ): string | undefined { + file = normalizePath(file) + const dirPath = path.dirname(file) + const direntCache = getDirentCacheFromPath(dirPath) + if ( + direntCache === undefined || + (direntCache && direntCache.type === 'symlink') + ) { + // fallback to built-in fs for out-of-root and symlinked files + return tryResolveRealFileWithExtensions( + file, + extensions, + preserveSymlinks, + ) + } + if (!direntCache || direntCache.type !== 'directory') { + return + } + + if (!direntCache.dirents || direntCache.dirents instanceof Promise) { + const dirents = readDirCacheSync(dirPath) + if (!dirents) { + direntCache.type = 'error' + return + } + direntCache.dirents = dirents + } + + const base = path.basename(file) + for (const ext of extensions) { + const fileName = base + ext + const fileDirentCache = direntCache.dirents.get(fileName) + if (fileDirentCache) { + const filePath = path.posix.join(dirPath, fileName) + ensureFileMaybeSymlinkIsResolved(fileDirentCache, filePath) + if (fileDirentCache.type === 'symlink') { + // fallback to built-in fs for symlinked files + return tryResolveRealFile(filePath, preserveSymlinks) + } + if (fileDirentCache.type === 'file') { + return normalizePath(filePath) + } + } + } + }, tryResolveRealFileOrType( file: string, preserveSymlinks?: boolean, @@ -288,6 +359,17 @@ function tryResolveRealFile( if (stat?.isFile()) return getRealPath(file, preserveSymlinks) } +function tryResolveRealFileWithExtensions( + filePath: string, + extensions: string[], + preserveSymlinks?: boolean, +): string | undefined { + for (const ext of extensions) { + const res = tryResolveRealFile(filePath + ext, preserveSymlinks) + if (res) return res + } +} + function tryResolveRealFileOrType( file: string, preserveSymlinks?: boolean, diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index 79356e2db087e8..71577872e5bed8 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -567,12 +567,15 @@ function tryCleanFsResolve( targetWeb = true, skipPackageJson = false, ): string | undefined { - const { tryPrefix } = options + const { tryPrefix, extensions, preserveSymlinks } = options + + const fsUtils = options.fsUtils ?? commonFsUtils // Optimization to get the real type or file type (directory, file, other) - const fileResult = ( - options.fsUtils ?? commonFsUtils - ).tryResolveRealFileOrType(file, options.preserveSymlinks) + const fileResult = fsUtils.tryResolveRealFileOrType( + file, + options.preserveSymlinks, + ) if (fileResult?.path) return fileResult.path @@ -582,34 +585,51 @@ function tryCleanFsResolve( const possibleJsToTs = options.isFromTsImporter && isPossibleTsOutput(file) if (possibleJsToTs || options.extensions.length || tryPrefix) { const dirPath = path.dirname(file) - if ((options.fsUtils ?? commonFsUtils).isDirectory(dirPath)) { + if (fsUtils.isDirectory(dirPath)) { if (possibleJsToTs) { // try resolve .js, .mjs, .cjs or .jsx import to typescript file const fileExt = path.extname(file) const fileName = file.slice(0, -fileExt.length) if ( - (res = tryResolveRealFile( + (res = fsUtils.tryResolveRealFile( fileName + fileExt.replace('js', 'ts'), - options, + preserveSymlinks, )) ) return res // for .js, also try .tsx if ( fileExt === '.js' && - (res = tryResolveRealFile(fileName + '.tsx', options)) + (res = fsUtils.tryResolveRealFile( + fileName + '.tsx', + preserveSymlinks, + )) ) return res } - if ((res = tryResolveRealFileWithExtensions(file, options))) return res + if ( + (res = fsUtils.tryResolveRealFileWithExtensions( + file, + extensions, + preserveSymlinks, + )) + ) + return res if (tryPrefix) { const prefixed = `${dirPath}/${options.tryPrefix}${path.basename(file)}` - if ((res = tryResolveRealFile(prefixed, options))) return res + if ((res = fsUtils.tryResolveRealFile(prefixed, preserveSymlinks))) + return res - if ((res = tryResolveRealFileWithExtensions(prefixed, options))) + if ( + (res = fsUtils.tryResolveRealFileWithExtensions( + prefixed, + extensions, + preserveSymlinks, + )) + ) return res } } @@ -622,7 +642,7 @@ function tryCleanFsResolve( if (!skipPackageJson) { let pkgPath = `${dirPath}/package.json` try { - if (fs.existsSync(pkgPath)) { + if (fsUtils.existsSync(pkgPath)) { if (!options.preserveSymlinks) { pkgPath = safeRealpathSync(pkgPath) } @@ -637,14 +657,21 @@ function tryCleanFsResolve( } } - if ((res = tryResolveRealFileWithExtensions(`${dirPath}/index`, options))) + if ( + (res = fsUtils.tryResolveRealFileWithExtensions( + `${dirPath}/index`, + extensions, + preserveSymlinks, + )) + ) return res if (tryPrefix) { if ( - (res = tryResolveRealFileWithExtensions( + (res = fsUtils.tryResolveRealFileWithExtensions( `${dirPath}/${options.tryPrefix}index`, - options, + extensions, + preserveSymlinks, )) ) return res @@ -652,26 +679,6 @@ function tryCleanFsResolve( } } -function tryResolveRealFile( - file: string, - options: InternalResolveOptions, -): string | undefined { - return (options.fsUtils ?? commonFsUtils).tryResolveRealFile( - file, - options.preserveSymlinks, - ) -} - -function tryResolveRealFileWithExtensions( - filePath: string, - options: InternalResolveOptions, -): string | undefined { - for (const ext of options.extensions) { - const res = tryResolveRealFile(filePath + ext, options) - if (res) return res - } -} - export type InternalResolveOptionsWithOverrideConditions = InternalResolveOptions & { /** From d3d45fcf5ab24bd66cf644a6ee786dad2598e490 Mon Sep 17 00:00:00 2001 From: patak Date: Fri, 8 Dec 2023 12:46:07 +0100 Subject: [PATCH 08/22] feat: avoid double normalizePath calls --- packages/vite/src/node/fsUtils.ts | 61 +++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/packages/vite/src/node/fsUtils.ts b/packages/vite/src/node/fsUtils.ts index 458fab13958249..cc30aa56fb77dd 100644 --- a/packages/vite/src/node/fsUtils.ts +++ b/packages/vite/src/node/fsUtils.ts @@ -3,7 +3,12 @@ import fs from 'node:fs' import path from 'node:path' import colors from 'picocolors' import type { ResolvedConfig } from './config' -import { normalizePath, safeRealpathSync, tryStatSync } from './utils' +import { + isInNodeModules, + normalizePath, + safeRealpathSync, + tryStatSync, +} from './utils' export interface FsUtils { existsSync: (path: string) => boolean @@ -180,16 +185,12 @@ export function createCachedFsUtils(config: ResolvedConfig): FsUtils { } function getDirentCacheFromPath( - file: string, + normalizedFile: string, ): DirentCache | false | undefined { - const normalizedFile = normalizePath(file) if (normalizedFile === root) { return rootCache } - if ( - !normalizedFile.startsWith(rootDirPath) || - normalizedFile.includes('/node_modules/') - ) { + if (!normalizedFile.startsWith(rootDirPath)) { return undefined } const pathFromRoot = normalizedFile.slice(rootDirPath.length) @@ -228,7 +229,11 @@ export function createCachedFsUtils(config: ResolvedConfig): FsUtils { return { existsSync(file: string) { - const direntCache = getDirentCacheFromPath(file) + if (isInNodeModules(file)) { + return fs.existsSync(file) + } + const normalizedFile = normalizePath(file) + const direntCache = getDirentCacheFromPath(normalizedFile) if ( direntCache === undefined || (direntCache && direntCache.type === 'symlink') @@ -242,7 +247,11 @@ export function createCachedFsUtils(config: ResolvedConfig): FsUtils { file: string, preserveSymlinks?: boolean, ): string | undefined { - const direntCache = getDirentCacheFromPath(file) + if (isInNodeModules(file)) { + return tryResolveRealFile(file, preserveSymlinks) + } + const normalizedFile = normalizePath(file) + const direntCache = getDirentCacheFromPath(normalizedFile) if ( direntCache === undefined || (direntCache && direntCache.type === 'symlink') @@ -255,15 +264,22 @@ export function createCachedFsUtils(config: ResolvedConfig): FsUtils { } // We can avoid getRealPath even if preserveSymlinks is false because we know it's // a file without symlinks in its path - return normalizePath(file) + return normalizedFile }, tryResolveRealFileWithExtensions( file: string, extensions: string[], preserveSymlinks?: boolean, ): string | undefined { - file = normalizePath(file) - const dirPath = path.dirname(file) + if (isInNodeModules(file)) { + return tryResolveRealFileWithExtensions( + file, + extensions, + preserveSymlinks, + ) + } + const normalizedFile = normalizePath(file) + const dirPath = path.posix.dirname(normalizedFile) const direntCache = getDirentCacheFromPath(dirPath) if ( direntCache === undefined || @@ -289,7 +305,7 @@ export function createCachedFsUtils(config: ResolvedConfig): FsUtils { direntCache.dirents = dirents } - const base = path.basename(file) + const base = path.posix.basename(normalizedFile) for (const ext of extensions) { const fileName = base + ext const fileDirentCache = direntCache.dirents.get(fileName) @@ -301,7 +317,7 @@ export function createCachedFsUtils(config: ResolvedConfig): FsUtils { return tryResolveRealFile(filePath, preserveSymlinks) } if (fileDirentCache.type === 'file') { - return normalizePath(filePath) + return filePath } } } @@ -310,7 +326,11 @@ export function createCachedFsUtils(config: ResolvedConfig): FsUtils { file: string, preserveSymlinks?: boolean, ): { path?: string; type: 'directory' | 'file' } | undefined { - const direntCache = getDirentCacheFromPath(file) + if (isInNodeModules(file)) { + return tryResolveRealFileOrType(file, preserveSymlinks) + } + const normalizedFile = normalizePath(file) + const direntCache = getDirentCacheFromPath(normalizedFile) if ( direntCache === undefined || (direntCache && direntCache.type === 'symlink') @@ -326,16 +346,19 @@ export function createCachedFsUtils(config: ResolvedConfig): FsUtils { } // We can avoid getRealPath even if preserveSymlinks is false because we know it's // a file without symlinks in its path - return { path: normalizePath(file), type: 'file' } + return { path: normalizedFile, type: 'file' } }, - isDirectory(path: string) { - const direntCache = getDirentCacheFromPath(path) + isDirectory(dirPath: string) { + if (isInNodeModules(dirPath)) { + return isDirectory(dirPath) + } + const direntCache = getDirentCacheFromPath(normalizePath(dirPath)) if ( direntCache === undefined || (direntCache && direntCache.type === 'symlink') ) { // fallback to built-in fs for out-of-root and symlinked files - return isDirectory(path) + return isDirectory(dirPath) } return direntCache && direntCache.type === 'directory' }, From 8ea355e29cb81fe73515d9e79b34e085e64e5692 Mon Sep 17 00:00:00 2001 From: patak Date: Fri, 8 Dec 2023 18:12:21 +0100 Subject: [PATCH 09/22] chore: update --- packages/vite/src/node/fsUtils.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/vite/src/node/fsUtils.ts b/packages/vite/src/node/fsUtils.ts index cc30aa56fb77dd..b0852ba593e977 100644 --- a/packages/vite/src/node/fsUtils.ts +++ b/packages/vite/src/node/fsUtils.ts @@ -129,9 +129,6 @@ export function createCachedFsUtils(config: ResolvedConfig): FsUtils { const { root } = config const rootDirPath = `${root}/` const rootCache = { type: 'directory' as DirentCacheType } // dirents will be computed lazily - if (!rootCache) { - return commonFsUtils - } const getDirentCacheSync = (parts: string[]): DirentCache | undefined => { let direntCache: DirentCache = rootCache From df42d68f17e771d55428c44f49bcbf291efa3389 Mon Sep 17 00:00:00 2001 From: patak Date: Fri, 8 Dec 2023 22:24:26 +0100 Subject: [PATCH 10/22] chore: remove watch null and custom ignored guard --- packages/vite/src/node/fsUtils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/fsUtils.ts b/packages/vite/src/node/fsUtils.ts index b0852ba593e977..86b099c904789b 100644 --- a/packages/vite/src/node/fsUtils.ts +++ b/packages/vite/src/node/fsUtils.ts @@ -52,7 +52,8 @@ export function getFsUtils(config: ResolvedConfig): FsUtils { // cached fsUtils is only used in the dev server for now, and only when the watcher isn't configured // we can support custom ignored patterns later fsUtils = commonFsUtils - } else if (config.server.watch === null || config.server.watch?.ignored) { + } /* TODO: Enabling for testing, we need to review if this guard is needed + else if (config.server.watch === null || config.server.watch?.ignored) { config.logger.warn( colors.yellow( `${colors.bold( @@ -61,7 +62,7 @@ export function getFsUtils(config: ResolvedConfig): FsUtils { ), ) fsUtils = commonFsUtils - } else { + } */ else { fsUtils = createCachedFsUtils(config) } cachedFsUtilsMap.set(config, fsUtils) From 93572c86f3d0b041485d1069daf9ad8486db0164 Mon Sep 17 00:00:00 2001 From: patak Date: Fri, 8 Dec 2023 22:27:39 +0100 Subject: [PATCH 11/22] chore: lint --- packages/vite/src/node/fsUtils.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/vite/src/node/fsUtils.ts b/packages/vite/src/node/fsUtils.ts index 86b099c904789b..24853c005281c2 100644 --- a/packages/vite/src/node/fsUtils.ts +++ b/packages/vite/src/node/fsUtils.ts @@ -1,7 +1,5 @@ import fs from 'node:fs' -// import fsp from 'node:fs/promises' import path from 'node:path' -import colors from 'picocolors' import type { ResolvedConfig } from './config' import { isInNodeModules, From b35aefa4694fba53090795e4ed517dba32f22ab5 Mon Sep 17 00:00:00 2001 From: patak Date: Sat, 9 Dec 2023 08:56:56 +0100 Subject: [PATCH 12/22] chore: remove async wip code --- packages/vite/src/node/fsUtils.ts | 36 +++++++++++++++---------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/vite/src/node/fsUtils.ts b/packages/vite/src/node/fsUtils.ts index 24853c005281c2..a1cbcad6fcf385 100644 --- a/packages/vite/src/node/fsUtils.ts +++ b/packages/vite/src/node/fsUtils.ts @@ -79,7 +79,7 @@ type DirentCacheType = | 'file_maybe_symlink' interface DirentCache { - dirents?: DirentsMap | Promise + dirents?: DirentsMap type: DirentCacheType } @@ -134,7 +134,7 @@ export function createCachedFsUtils(config: ResolvedConfig): FsUtils { for (let i = 0; i < parts.length; i++) { if (direntCache.type === 'directory') { let dirPath - if (!direntCache.dirents || direntCache.dirents instanceof Promise) { + if (!direntCache.dirents) { dirPath = path.posix.join(root, ...parts.slice(0, i)) const dirents = readDirCacheSync(dirPath) if (!dirents) { @@ -198,28 +198,28 @@ export function createCachedFsUtils(config: ResolvedConfig): FsUtils { return direntCache } - async function onPathAdd( + function onPathAdd( file: string, type: 'directory_maybe_symlink' | 'file_maybe_symlink', - ): Promise { + ) { const direntCache = getDirentCacheFromPath(path.dirname(file)) - if (direntCache && direntCache.type === 'directory') { - // We don't know if the file is a symlink or not for the stats - // in the chokidar callback, so we add it with type unknown. - // If it is accessed, we'll do a new fs call to get the real type. - if (direntCache.dirents) { - ;(await direntCache.dirents).set(path.basename(file), { type }) - } + if ( + direntCache && + direntCache.type === 'directory' && + direntCache.dirents + ) { + direntCache.dirents.set(path.basename(file), { type }) } } - async function onPathUnlink(file: string): Promise { + function onPathUnlink(file: string) { const direntCache = getDirentCacheFromPath(path.dirname(file)) - if (direntCache && direntCache.type === 'directory') { - if (direntCache.dirents) { - const dirents = await direntCache.dirents - dirents.delete(path.basename(file)) - } + if ( + direntCache && + direntCache.type === 'directory' && + direntCache.dirents + ) { + direntCache.dirents.delete(path.basename(file)) } } @@ -292,7 +292,7 @@ export function createCachedFsUtils(config: ResolvedConfig): FsUtils { return } - if (!direntCache.dirents || direntCache.dirents instanceof Promise) { + if (!direntCache.dirents) { const dirents = readDirCacheSync(dirPath) if (!dirents) { direntCache.type = 'error' From a98c9b40ac260478f0b02e81f4ef93e7e10d4d4f Mon Sep 17 00:00:00 2001 From: patak Date: Sat, 9 Dec 2023 15:23:25 +0100 Subject: [PATCH 13/22] perf: optimize joins --- packages/vite/src/node/fsUtils.ts | 14 ++++++++++---- packages/vite/src/node/server/index.ts | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/vite/src/node/fsUtils.ts b/packages/vite/src/node/fsUtils.ts index a1cbcad6fcf385..977c6a8ad01c63 100644 --- a/packages/vite/src/node/fsUtils.ts +++ b/packages/vite/src/node/fsUtils.ts @@ -124,6 +124,12 @@ function ensureFileMaybeSymlinkIsResolved( isSymlink === undefined ? 'error' : isSymlink ? 'symlink' : 'file' } +function pathUntilPart(root: string, parts: string[], i: number): string { + let p = root + for (let k = 0; k < i; k++) p += '/' + parts[k] + return p +} + export function createCachedFsUtils(config: ResolvedConfig): FsUtils { const { root } = config const rootDirPath = `${root}/` @@ -135,7 +141,7 @@ export function createCachedFsUtils(config: ResolvedConfig): FsUtils { if (direntCache.type === 'directory') { let dirPath if (!direntCache.dirents) { - dirPath = path.posix.join(root, ...parts.slice(0, i)) + dirPath = pathUntilPart(root, parts, i) const dirents = readDirCacheSync(dirPath) if (!dirents) { direntCache.type = 'error' @@ -148,7 +154,7 @@ export function createCachedFsUtils(config: ResolvedConfig): FsUtils { return } if (nextDirentCache.type === 'directory_maybe_symlink') { - dirPath ??= path.posix.join(root, ...parts.slice(0, i)) + dirPath ??= pathUntilPart(root, parts, i) const isSymlink = fs .lstatSync(dirPath, { throwIfNoEntry: false }) ?.isSymbolicLink() @@ -167,7 +173,7 @@ export function createCachedFsUtils(config: ResolvedConfig): FsUtils { if (direntCache.type === 'file_maybe_symlink') { ensureFileMaybeSymlinkIsResolved( direntCache, - path.posix.join(root, ...parts.slice(0, i)), + pathUntilPart(root, parts, i), ) return direntCache } else if (direntCache.type === 'file') { @@ -306,7 +312,7 @@ export function createCachedFsUtils(config: ResolvedConfig): FsUtils { const fileName = base + ext const fileDirentCache = direntCache.dirents.get(fileName) if (fileDirentCache) { - const filePath = path.posix.join(dirPath, fileName) + const filePath = dirPath + '/' + fileName ensureFileMaybeSymlinkIsResolved(fileDirentCache, filePath) if (fileDirentCache.type === 'symlink') { // fallback to built-in fs for symlinked files diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 905dfe5ceb8b47..5bea0604fa0d17 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -952,7 +952,7 @@ export function resolveServerOptions( strict: server.fs?.strict ?? true, allow: allowDirs, deny, - cachedChecks: server.fs?.cachedChecks ?? false, + cachedChecks: server.fs?.cachedChecks ?? true, } if (server.origin?.endsWith('/')) { From 6fd956c5bd580d5089d750e20ecf0b83f95637a8 Mon Sep 17 00:00:00 2001 From: patak Date: Sat, 9 Dec 2023 16:02:45 +0100 Subject: [PATCH 14/22] chore: set default to false --- packages/vite/src/node/server/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 5bea0604fa0d17..905dfe5ceb8b47 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -952,7 +952,7 @@ export function resolveServerOptions( strict: server.fs?.strict ?? true, allow: allowDirs, deny, - cachedChecks: server.fs?.cachedChecks ?? true, + cachedChecks: server.fs?.cachedChecks ?? false, } if (server.origin?.endsWith('/')) { From 01742a547fa9a7a9de6825d96fbe93aab051f065 Mon Sep 17 00:00:00 2001 From: patak Date: Sat, 9 Dec 2023 16:10:00 +0100 Subject: [PATCH 15/22] refactor: fsUtils.initWatcher --- packages/vite/src/node/fsUtils.ts | 22 +++++++++++----------- packages/vite/src/node/server/index.ts | 10 +--------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/packages/vite/src/node/fsUtils.ts b/packages/vite/src/node/fsUtils.ts index 977c6a8ad01c63..15afa0895f64ad 100644 --- a/packages/vite/src/node/fsUtils.ts +++ b/packages/vite/src/node/fsUtils.ts @@ -1,5 +1,6 @@ import fs from 'node:fs' import path from 'node:path' +import type { FSWatcher } from 'dep-types/chokidar' import type { ResolvedConfig } from './config' import { isInNodeModules, @@ -26,10 +27,7 @@ export interface FsUtils { preserveSymlinks?: boolean, ) => { path?: string; type: 'directory' | 'file' } | undefined - onFileAdd?: (file: string) => void - onFileUnlink?: (file: string) => void - onDirectoryAdd?: (file: string) => void - onDirectoryUnlink?: (file: string) => void + initWatcher?: (watcher: FSWatcher) => void } // An implementation of fsUtils without caching @@ -365,14 +363,16 @@ export function createCachedFsUtils(config: ResolvedConfig): FsUtils { return direntCache && direntCache.type === 'directory' }, - onFileAdd(file) { - onPathAdd(file, 'file_maybe_symlink') + initWatcher(watcher: FSWatcher) { + watcher.on('add', (file) => { + onPathAdd(file, 'file_maybe_symlink') + }) + watcher.on('addDir', (dir) => { + onPathAdd(dir, 'directory_maybe_symlink') + }) + watcher.on('unlink', onPathUnlink) + watcher.on('unlinkDir', onPathUnlink) }, - onFileUnlink: onPathUnlink, - onDirectoryAdd(file) { - onPathAdd(file, 'directory_maybe_symlink') - }, - onDirectoryUnlink: onPathUnlink, } } diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 905dfe5ceb8b47..4e0ececd61b0b3 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -671,21 +671,13 @@ export async function _createServer( await onHMRUpdate(file, false) }) - const fsUtils = getFsUtils(config) + getFsUtils(config).initWatcher?.(watcher) watcher.on('add', (file) => { onFileAddUnlink(file, false) - fsUtils.onFileAdd?.(file) }) watcher.on('unlink', (file) => { onFileAddUnlink(file, true) - fsUtils.onFileUnlink?.(file) - }) - watcher.on('addDir', (dir) => { - fsUtils.onDirectoryAdd?.(dir) - }) - watcher.on('unlinkDir', (dir) => { - fsUtils.onDirectoryUnlink?.(dir) }) ws.on('vite:invalidate', async ({ path, message }: InvalidatePayload) => { From ab9e7d8d118cc21bb43d4432fbb079fb72cb4e4a Mon Sep 17 00:00:00 2001 From: patak Date: Sat, 9 Dec 2023 20:30:51 +0100 Subject: [PATCH 16/22] feat: add VITE_SERVE_FS_CACHED_CHECKS --- packages/vite/src/node/server/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 4e0ececd61b0b3..6f372ffbf345f9 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -944,7 +944,8 @@ export function resolveServerOptions( strict: server.fs?.strict ?? true, allow: allowDirs, deny, - cachedChecks: server.fs?.cachedChecks ?? false, + cachedChecks: + server.fs?.cachedChecks ?? !!process.env.VITE_SERVE_FS_CACHED_CHECKS, } if (server.origin?.endsWith('/')) { From d25375e17d8a2461d8e5a87b6ea57ee848e4ee9a Mon Sep 17 00:00:00 2001 From: patak Date: Sat, 9 Dec 2023 20:32:15 +0100 Subject: [PATCH 17/22] chore: fix typo --- packages/vite/src/node/server/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 6f372ffbf345f9..cc7e66b299ab97 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -945,7 +945,7 @@ export function resolveServerOptions( allow: allowDirs, deny, cachedChecks: - server.fs?.cachedChecks ?? !!process.env.VITE_SERVE_FS_CACHED_CHECKS, + server.fs?.cachedChecks ?? !!process.env.VITE_SERVER_FS_CACHED_CHECKS, } if (server.origin?.endsWith('/')) { From 7c562d236008a851c067f679da94497189752879 Mon Sep 17 00:00:00 2001 From: patak Date: Sat, 9 Dec 2023 20:39:33 +0100 Subject: [PATCH 18/22] fix: add guard for symlinked root --- packages/vite/src/node/fsUtils.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/node/fsUtils.ts b/packages/vite/src/node/fsUtils.ts index 15afa0895f64ad..35380d084d5b9c 100644 --- a/packages/vite/src/node/fsUtils.ts +++ b/packages/vite/src/node/fsUtils.ts @@ -1,5 +1,6 @@ import fs from 'node:fs' import path from 'node:path' +import colors from 'picocolors' import type { FSWatcher } from 'dep-types/chokidar' import type { ResolvedConfig } from './config' import { @@ -58,7 +59,19 @@ export function getFsUtils(config: ResolvedConfig): FsUtils { ), ) fsUtils = commonFsUtils - } */ else { + } */ else if ( + !config.resolve.preserveSymlinks && + config.root !== getRealPath(config.root) + ) { + config.logger.warn( + colors.yellow( + `${colors.bold( + `(!)`, + )} server.fs.cachedChecks isn't supported resolve.preserveSymlinks is false and root is symlinked\n`, + ), + ) + fsUtils = commonFsUtils + } else { fsUtils = createCachedFsUtils(config) } cachedFsUtilsMap.set(config, fsUtils) From 6c1b741bfbf5cedca69fbded04e202136ca0b6fb Mon Sep 17 00:00:00 2001 From: patak Date: Sun, 10 Dec 2023 09:55:54 +0100 Subject: [PATCH 19/22] chore: ensure root without trailing slash --- packages/vite/src/node/fsUtils.ts | 3 ++- packages/vite/src/node/utils.ts | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/node/fsUtils.ts b/packages/vite/src/node/fsUtils.ts index 35380d084d5b9c..69454bdd0fe671 100644 --- a/packages/vite/src/node/fsUtils.ts +++ b/packages/vite/src/node/fsUtils.ts @@ -8,6 +8,7 @@ import { normalizePath, safeRealpathSync, tryStatSync, + withoutTrailingSlash, } from './utils' export interface FsUtils { @@ -142,7 +143,7 @@ function pathUntilPart(root: string, parts: string[], i: number): string { } export function createCachedFsUtils(config: ResolvedConfig): FsUtils { - const { root } = config + const root = withoutTrailingSlash(config.root) const rootDirPath = `${root}/` const rootCache = { type: 'directory' as DirentCacheType } // dirents will be computed lazily diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index ff23df8a6d3e30..96874a172d8c31 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -250,6 +250,13 @@ export function withTrailingSlash(path: string): string { return path } +export function withoutTrailingSlash(path: string): string { + if (path[path.length - 1] === '/') { + return path.slice(0, -1) + } + return path +} + /** * Check if dir is a parent of file * From 5ede827c7af6c1cab45ab3e1b8ed3cc340fe872b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arnaud=20Barr=C3=A9?= Date: Sun, 10 Dec 2023 22:50:07 +0100 Subject: [PATCH 20/22] nits --- packages/vite/src/node/fsUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/fsUtils.ts b/packages/vite/src/node/fsUtils.ts index 69454bdd0fe671..93a1ff46afdaa3 100644 --- a/packages/vite/src/node/fsUtils.ts +++ b/packages/vite/src/node/fsUtils.ts @@ -106,7 +106,7 @@ function readDirCacheSync(file: string): undefined | DirentsMap { } function direntsToDirentMap(fsDirents: fs.Dirent[]): DirentsMap { - const dirents = new Map() + const dirents: DirentsMap = new Map() for (const dirent of fsDirents) { // We ignore non directory, file, and symlink entries const type = dirent.isDirectory() @@ -145,7 +145,7 @@ function pathUntilPart(root: string, parts: string[], i: number): string { export function createCachedFsUtils(config: ResolvedConfig): FsUtils { const root = withoutTrailingSlash(config.root) const rootDirPath = `${root}/` - const rootCache = { type: 'directory' as DirentCacheType } // dirents will be computed lazily + const rootCache: DirentCache = { type: 'directory' } // dirents will be computed lazily const getDirentCacheSync = (parts: string[]): DirentCache | undefined => { let direntCache: DirentCache = rootCache From 476dc3bd62dae09ffdd4f601af13e7a28fd53592 Mon Sep 17 00:00:00 2001 From: patak Date: Mon, 11 Dec 2023 09:53:11 +0100 Subject: [PATCH 21/22] chore: typo Co-authored-by: Bjorn Lu --- packages/vite/src/node/fsUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/fsUtils.ts b/packages/vite/src/node/fsUtils.ts index 93a1ff46afdaa3..d1e5f0ca785e45 100644 --- a/packages/vite/src/node/fsUtils.ts +++ b/packages/vite/src/node/fsUtils.ts @@ -68,7 +68,7 @@ export function getFsUtils(config: ResolvedConfig): FsUtils { colors.yellow( `${colors.bold( `(!)`, - )} server.fs.cachedChecks isn't supported resolve.preserveSymlinks is false and root is symlinked\n`, + )} server.fs.cachedChecks isn't supported when resolve.preserveSymlinks is false and root is symlinked\n`, ), ) fsUtils = commonFsUtils From d6599208ab2d51a8658b3a3e4a610b7ffc9d41f9 Mon Sep 17 00:00:00 2001 From: patak Date: Tue, 12 Dec 2023 09:29:12 +0100 Subject: [PATCH 22/22] chore: remove unneeded withoutTrailingSlash util --- packages/vite/src/node/fsUtils.ts | 3 +-- packages/vite/src/node/utils.ts | 7 ------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/vite/src/node/fsUtils.ts b/packages/vite/src/node/fsUtils.ts index d1e5f0ca785e45..ec9c6431bfa645 100644 --- a/packages/vite/src/node/fsUtils.ts +++ b/packages/vite/src/node/fsUtils.ts @@ -8,7 +8,6 @@ import { normalizePath, safeRealpathSync, tryStatSync, - withoutTrailingSlash, } from './utils' export interface FsUtils { @@ -143,7 +142,7 @@ function pathUntilPart(root: string, parts: string[], i: number): string { } export function createCachedFsUtils(config: ResolvedConfig): FsUtils { - const root = withoutTrailingSlash(config.root) + const root = config.root // root is resolved and normalized, so it doesn't have a trailing slash const rootDirPath = `${root}/` const rootCache: DirentCache = { type: 'directory' } // dirents will be computed lazily diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 96874a172d8c31..ff23df8a6d3e30 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -250,13 +250,6 @@ export function withTrailingSlash(path: string): string { return path } -export function withoutTrailingSlash(path: string): string { - if (path[path.length - 1] === '/') { - return path.slice(0, -1) - } - return path -} - /** * Check if dir is a parent of file *