From 5b75cad78c5189862d24c33ad636de854d86922c Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Tue, 21 Dec 2021 15:33:47 -0500 Subject: [PATCH 1/5] fix: reduce number of `fs.realpath` calls --- .../node/__tests__/SymlinkResolver.spec.ts | 158 ++++++++++++++++ packages/vite/src/node/config.ts | 6 + packages/vite/src/node/packages.ts | 106 +++++++++-- packages/vite/src/node/plugins/css.ts | 7 +- packages/vite/src/node/plugins/index.ts | 1 + packages/vite/src/node/plugins/resolve.ts | 45 ++--- packages/vite/src/node/server/sourcemap.ts | 6 +- .../vite/src/node/server/transformRequest.ts | 2 +- packages/vite/src/node/symlinks.ts | 170 ++++++++++++++++++ packages/vite/src/node/utils.ts | 36 +++- 10 files changed, 489 insertions(+), 48 deletions(-) create mode 100644 packages/vite/src/node/__tests__/SymlinkResolver.spec.ts create mode 100644 packages/vite/src/node/symlinks.ts diff --git a/packages/vite/src/node/__tests__/SymlinkResolver.spec.ts b/packages/vite/src/node/__tests__/SymlinkResolver.spec.ts new file mode 100644 index 00000000000000..370096f605072e --- /dev/null +++ b/packages/vite/src/node/__tests__/SymlinkResolver.spec.ts @@ -0,0 +1,158 @@ +import { join, relative, resolve } from 'path' +import type { SymlinkResolver } from '../symlinks' +import { createSymlinkResolver } from '../symlinks' + +let resolver: SymlinkResolver + +const realpathMock = jest.fn() +const readlinkMock = jest.fn() + +const root = '/dev/root' +const realpathSync = (p: string) => { + return resolver.realpathSync(resolve(root, p)) +} + +describe('SymlinkResolver', () => { + beforeEach(() => { + mockRealPath({}) + mockReadLink({}) + resolver = createSymlinkResolver(root, { + realpathSync: { native: realpathMock }, + readlinkSync: readlinkMock + }) + }) + + describe('inside project root', () => { + test('no symlinks present', () => { + const result = realpathSync('foo/bar') + expect(result).toMatchInlineSnapshot(`"/dev/root/foo/bar"`) + expect(resolver.fsCalls).toMatchInlineSnapshot(`2`) + expect(resolver.cacheHits).toMatchInlineSnapshot(`0`) + + // …is cached? + expect(realpathSync('foo/bar')).toBe(result) + expect(resolver.fsCalls).toMatchInlineSnapshot(`2`) + expect(resolver.cacheHits).toMatchInlineSnapshot(`1`) + }) + + test('given path is a symlink', () => { + mockReadLink({ 'foo/bar': './baz' }) + + const result = realpathSync('foo/bar') + expect(result).toMatchInlineSnapshot(`"/dev/root/foo/baz"`) + expect(resolver.fsCalls).toMatchInlineSnapshot(`3`) + expect(resolver.cacheHits).toMatchInlineSnapshot(`0`) + + // …is cached? + expect(realpathSync('foo/bar')).toBe(result) + expect(resolver.fsCalls).toMatchInlineSnapshot(`3`) + expect(resolver.cacheHits).toMatchInlineSnapshot(`1`) + }) + + test('given path is a symlink pointing out of root', () => { + mockReadLink({ foo: '/dev/foo' }) + + const result = realpathSync('foo') + expect(result).toMatchInlineSnapshot(`"/dev/foo"`) + expect(resolver.fsCalls).toMatchInlineSnapshot(`4`) + expect(resolver.cacheHits).toMatchInlineSnapshot(`0`) + + // …is cached? + expect(realpathSync('foo')).toBe(result) + expect(resolver.fsCalls).toMatchInlineSnapshot(`4`) + expect(resolver.cacheHits).toMatchInlineSnapshot(`1`) + }) + + test('given path is a symlink within a symlink', () => { + mockRealPath({ foo: 'red' }) + mockReadLink({ 'red/bar': './baz' }) + + const result = realpathSync('foo/bar') + expect(result).toMatchInlineSnapshot(`"/dev/root/red/baz"`) + expect(resolver.fsCalls).toMatchInlineSnapshot(`3`) + expect(resolver.cacheHits).toMatchInlineSnapshot(`0`) + + // …is cached? + expect(realpathSync('foo/bar')).toBe(result) + expect(resolver.fsCalls).toMatchInlineSnapshot(`3`) + expect(resolver.cacheHits).toMatchInlineSnapshot(`1`) + }) + + test('given path has symlink grand parent', () => { + mockRealPath({ 'foo/bar': 'red/bar' }) + + const result = realpathSync('foo/bar/main.js') + expect(result).toMatchInlineSnapshot(`"/dev/root/red/bar/main.js"`) + expect(resolver.fsCalls).toMatchInlineSnapshot(`2`) + expect(resolver.cacheHits).toMatchInlineSnapshot(`0`) + + // …is cached? + expect(realpathSync('foo/bar/main.js')).toBe(result) + expect(realpathSync('foo/bar')).toMatchInlineSnapshot( + `"/dev/root/red/bar"` + ) + expect(resolver.fsCalls).toMatchInlineSnapshot(`2`) + expect(resolver.cacheHits).toMatchInlineSnapshot(`2`) + }) + + test('given path has two symlink parents', () => { + mockRealPath({ 'foo/bar': 'red/blue' }) + + const result = realpathSync('foo/bar/main.js') + expect(result).toMatchInlineSnapshot(`"/dev/root/red/blue/main.js"`) + expect(resolver.fsCalls).toMatchInlineSnapshot(`2`) + expect(resolver.cacheHits).toMatchInlineSnapshot(`0`) + + // …is cached? + expect(realpathSync('foo/bar/main.js')).toBe(result) + expect(realpathSync('foo/bar')).toMatchInlineSnapshot( + `"/dev/root/red/blue"` + ) + expect(realpathSync('foo')).toMatchInlineSnapshot(`"/dev/root/red"`) + expect(resolver.fsCalls).toMatchInlineSnapshot(`2`) + expect(resolver.cacheHits).toMatchInlineSnapshot(`3`) + }) + }) + + test('symlink outside project root', () => { + // Mock a symlink that points to another symlink. + mockReadLink({ '../foo': './bar', '../bar': './baz' }) + + const result = realpathSync('../foo') + expect(result).toMatchInlineSnapshot(`"/dev/baz"`) + expect(resolver.fsCalls).toMatchInlineSnapshot(`4`) + expect(resolver.cacheHits).toMatchInlineSnapshot(`0`) + + // …is cached? + expect(realpathSync('../foo')).toBe(result) + expect(realpathSync('../bar')).toMatchInlineSnapshot(`"/dev/baz"`) + expect(realpathSync('../baz')).toMatchInlineSnapshot(`"/dev/baz"`) + expect(resolver.fsCalls).toMatchInlineSnapshot(`4`) + expect(resolver.cacheHits).toMatchInlineSnapshot(`3`) + }) +}) + +function mockRealPath(pathMap: Record) { + realpathMock.mockReset() + realpathMock.mockImplementation((arg) => { + return resolve(root, pathMap[relative(root, arg)] || arg) + }) +} + +// Thrown by fs.readlinkSync if given a path that's not a symlink. +const throwInvalid = throwError(-22) + +function mockReadLink(linkMap: Record) { + readlinkMock.mockReset() + readlinkMock.mockImplementation((arg) => { + return linkMap[relative(root, arg)] || throwInvalid() + }) +} + +function throwError(errno: number) { + return () => { + const e: any = new Error() + e.errno = errno + throw e + } +} diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index e90bdd7150bced..132272a47c7c8a 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -64,6 +64,8 @@ import type { PackageCache } from './packages' import { loadEnv, resolveEnvPrefix } from './env' import type { ResolvedSSROptions, SSROptions } from './ssr' import { resolveSSROptions } from './ssr' +import { createSymlinkResolver } from './symlinks' +import type { SymlinkResolver } from './symlinks' const debug = createDebugger('vite:config') @@ -348,6 +350,8 @@ export type ResolvedConfig = Readonly< createResolver: (options?: Partial) => ResolveFn optimizeDeps: DepOptimizationOptions /** @internal */ + symlinkResolver: SymlinkResolver + /** @internal */ packageCache: PackageCache worker: ResolveWorkerOptions appType: AppType @@ -558,6 +562,7 @@ export async function resolveConfig( aliasPlugin({ entries: resolved.resolve.alias }), resolvePlugin({ ...resolved.resolve, + symlinkResolver: resolved.symlinkResolver, root: resolvedRoot, isProduction, isBuild: command === 'build', @@ -655,6 +660,7 @@ export async function resolveConfig( }, logger, packageCache: new Map(), + symlinkResolver: createSymlinkResolver(resolvedRoot), createResolver, optimizeDeps: { disabled: 'build', diff --git a/packages/vite/src/node/packages.ts b/packages/vite/src/node/packages.ts index e1a85bff441212..eb3bb147f9ed97 100644 --- a/packages/vite/src/node/packages.ts +++ b/packages/vite/src/node/packages.ts @@ -1,8 +1,9 @@ import fs from 'node:fs' import path from 'node:path' -import { createDebugger, createFilter, resolveFrom } from './utils' +import { createDebugger, createFilter, getRealPath, resolveFrom } from './utils' import type { ResolvedConfig } from './config' import type { Plugin } from './plugin' +import type { SymlinkResolver } from './symlinks' const isDebug = process.env.DEBUG const debug = createDebugger('vite:resolve-details', { @@ -45,27 +46,52 @@ export function invalidatePackageData( }) } +/** + * Find and load the `package.json` associated with a module id. + * + * Using the `options.packageCache` argument is highly recommended for + * performance. The easiest way is setting it to the `packageCache` + * property of the `vite.ResolvedConfig` object. + */ export function resolvePackageData( id: string, basedir: string, - preserveSymlinks = false, + options?: LoadPackageOptions +): PackageData | null + +/** @deprecated Use `options` object argument instead */ +export function resolvePackageData( + id: string, + basedir: string, + preserveSymlinks: boolean | undefined, packageCache?: PackageCache +): PackageData | null + +export function resolvePackageData( + id: string, + basedir: string, + arg3?: boolean | LoadPackageOptions, + arg4?: PackageCache ): PackageData | null { + const options = + typeof arg3 === 'boolean' + ? { preserveSymlinks: arg3, packageCache: arg4 } + : arg3 || {} + let pkg: PackageData | undefined let cacheKey: string | undefined - if (packageCache) { - cacheKey = `${id}&${basedir}&${preserveSymlinks}` - if ((pkg = packageCache.get(cacheKey))) { + if (options.packageCache) { + cacheKey = `${id}&${basedir}&${options.preserveSymlinks || false}` + if ((pkg = options.packageCache.get(cacheKey))) { return pkg } } + let pkgPath: string | undefined try { - pkgPath = resolveFrom(`${id}/package.json`, basedir, preserveSymlinks) - pkg = loadPackageData(pkgPath, true, packageCache) - if (packageCache) { - packageCache.set(cacheKey!, pkg) - } + pkgPath = resolveFrom(`${id}/package.json`, basedir, true) + pkg = loadPackageData(pkgPath, options) + options.packageCache?.set(cacheKey!, pkg) return pkg } catch (e) { if (e instanceof SyntaxError) { @@ -79,17 +105,67 @@ export function resolvePackageData( return null } +export type LoadPackageOptions = { + preserveSymlinks?: boolean + symlinkResolver?: SymlinkResolver + packageCache?: PackageCache + cjsInclude?: (string | RegExp)[] +} + +/** + * Load a `package.json` file into memory. + * + * Using the `options.packageCache` argument is highly recommended for + * performance. The easiest way is setting it to the `packageCache` + * property of the `vite.ResolvedConfig` object. + */ +export function loadPackageData( + pkgPath: string, + options?: LoadPackageOptions +): PackageData + +/** @deprecated Use `options` object argument instead */ export function loadPackageData( pkgPath: string, - preserveSymlinks?: boolean, + preserveSymlinks: boolean | undefined, packageCache?: PackageCache +): PackageData + +export function loadPackageData( + pkgPath: string, + arg2?: boolean | LoadPackageOptions, + arg3?: PackageCache ): PackageData { - if (!preserveSymlinks) { - pkgPath = fs.realpathSync.native(pkgPath) + const options = + typeof arg2 === 'boolean' + ? { preserveSymlinks: arg2, packageCache: arg3 } + : arg2 || {} + + if (options.preserveSymlinks !== true) { + const originalPkgPath = pkgPath + + // Support uncached realpath calls for backwards compatibility. + pkgPath = getRealPath( + pkgPath, + options.symlinkResolver, + options.preserveSymlinks + ) + + // In case a linked package is a local clone of a CommonJS dependency, + // we need to ensure @rollup/plugin-commonjs analyzes the package even + // after it's been resolved to its actual file location. + if (options.cjsInclude && pkgPath !== originalPkgPath) { + const filter = createFilter(options.cjsInclude, undefined, { + resolve: false + }) + if (!filter(pkgPath) && filter(originalPkgPath)) { + options.cjsInclude.push(path.dirname(pkgPath) + '/**') + } + } } let cached: PackageData | undefined - if ((cached = packageCache?.get(pkgPath))) { + if ((cached = options.packageCache?.get(pkgPath))) { return cached } @@ -127,7 +203,7 @@ export function loadPackageData( } } - packageCache?.set(pkgPath, pkg) + options.packageCache?.set(pkgPath, pkg) return pkg } diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 35f2e01a6701ad..95ce0e4cd843ce 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -364,7 +364,12 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { const getContentWithSourcemap = async (content: string) => { if (config.css?.devSourcemap) { const sourcemap = this.getCombinedSourcemap() - await injectSourcesContent(sourcemap, cleanUrl(id), config.logger) + await injectSourcesContent( + sourcemap, + cleanUrl(id), + config.logger, + config.symlinkResolver + ) return getCodeWithSourcemap('css', content, sourcemap) } return content diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index 057f4f1faa5271..fd3adcf4d2ab69 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -63,6 +63,7 @@ export async function resolvePlugins( isProduction: config.isProduction, isBuild, packageCache: config.packageCache, + symlinkResolver: config.symlinkResolver, ssrConfig: config.ssr, asSrc: true, getDepsOptimizer: (ssr: boolean) => getDepsOptimizer(config, ssr), diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index 1c1c80f573e84d..4066aa9d5ced4b 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -22,6 +22,7 @@ import { ensureVolumeInPath, fsPathFromId, getPotentialTsSrcPaths, + getRealPath, injectQuery, isBuiltin, isDataUrl, @@ -44,6 +45,7 @@ import type { DepsOptimizer } from '../optimizer' import type { SSROptions } from '..' import type { PackageCache, PackageData } from '../packages' import { loadPackageData, resolvePackageData } from '../packages' +import type { SymlinkResolver } from '../symlinks' import { isWorkerRequest } from './worker' const normalizedClientEntry = normalizePath(CLIENT_ENTRY) @@ -81,6 +83,7 @@ export interface InternalResolveOptions extends Required { isProduction: boolean ssrConfig?: SSROptions packageCache?: PackageCache + symlinkResolver?: SymlinkResolver /** * src code mode also attempts the following: * - resolving /xxx as URLs @@ -535,13 +538,21 @@ function tryResolveFile( ): string | undefined { if (isFileReadable(file)) { if (!fs.statSync(file).isDirectory()) { - return getRealPath(file, options.preserveSymlinks) + postfix + file = ensureVolumeInPath(file) + if (file !== browserExternalId) { + file = getRealPath( + file, + options.symlinkResolver, + options.preserveSymlinks + ) + } + return normalizePath(file) + postfix } else if (tryIndex) { if (!skipPackageJson) { const pkgPath = file + '/package.json' try { // path points to a node package - const pkg = loadPackageData(pkgPath, options.preserveSymlinks) + const pkg = loadPackageData(pkgPath, options) const resolved = resolvePackageEntry(file, pkg, targetWeb, options) return resolved } catch (e) { @@ -600,7 +611,7 @@ export function tryNodeResolve( externalize?: boolean, allowLinkedExternal: boolean = true ): PartialResolvedId | undefined { - const { root, dedupe, isBuild, preserveSymlinks, packageCache } = options + const { root, dedupe, isBuild } = options ssr ??= false @@ -653,7 +664,7 @@ export function tryNodeResolve( // nested node module, step-by-step resolve to the basedir of the nestedPath if (nestedRoot) { - basedir = nestedResolveFrom(nestedRoot, basedir, preserveSymlinks) + basedir = nestedResolveFrom(nestedRoot, basedir, options) } // nearest package.json @@ -662,22 +673,12 @@ export function tryNodeResolve( let pkg: PackageData | undefined let pkgId = possiblePkgIds.reverse().find((pkgId) => { - nearestPkg = resolvePackageData( - pkgId, - basedir, - preserveSymlinks, - packageCache - )! + nearestPkg = resolvePackageData(pkgId, basedir, options)! return nearestPkg })! const rootPkgId = possiblePkgIds[0] - const rootPkg = resolvePackageData( - rootPkgId, - basedir, - preserveSymlinks, - packageCache - )! + const rootPkg = resolvePackageData(rootPkgId, basedir, options)! if (rootPkg?.data?.exports) { pkg = rootPkg pkgId = rootPkgId @@ -1219,22 +1220,12 @@ 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 { - resolved = ensureVolumeInPath(resolved) - if (!preserveSymlinks && browserExternalId !== resolved) { - resolved = fs.realpathSync(resolved) - } - return normalizePath(resolved) -} - /** * if importer was not resolved by vite's resolver previously * (when esbuild resolved it) * resolve importer's pkg and add to idToPkgMap */ function resolvePkg(importer: string, options: InternalResolveOptions) { - const { root, preserveSymlinks, packageCache } = options - if (importer.includes('\x00')) { return null } @@ -1254,7 +1245,7 @@ function resolvePkg(importer: string, options: InternalResolveOptions) { let pkg: PackageData | undefined possiblePkgIds.reverse().find((pkgId) => { - pkg = resolvePackageData(pkgId, root, preserveSymlinks, packageCache)! + pkg = resolvePackageData(pkgId, options.root, options)! return pkg })! diff --git a/packages/vite/src/node/server/sourcemap.ts b/packages/vite/src/node/server/sourcemap.ts index 774d74dc8bb398..40e9aadf081c16 100644 --- a/packages/vite/src/node/server/sourcemap.ts +++ b/packages/vite/src/node/server/sourcemap.ts @@ -3,6 +3,7 @@ import { promises as fs } from 'node:fs' import type { SourceMap } from 'rollup' import type { Logger } from '../logger' import { createDebugger } from '../utils' +import type { SymlinkResolver } from '../symlinks' const isDebug = !!process.env.DEBUG const debug = createDebugger('vite:sourcemap', { @@ -23,12 +24,13 @@ interface SourceMapLike { export async function injectSourcesContent( map: SourceMapLike, file: string, - logger: Logger + logger: Logger, + symlinkResolver: SymlinkResolver ): Promise { let sourceRoot: string | undefined try { // The source root is undefined for virtual modules and permission errors. - sourceRoot = await fs.realpath( + sourceRoot = symlinkResolver.realpathSync( path.resolve(path.dirname(file), map.sourceRoot || '') ) } catch {} diff --git a/packages/vite/src/node/server/transformRequest.ts b/packages/vite/src/node/server/transformRequest.ts index b7bed216348504..7557aa34d1c19e 100644 --- a/packages/vite/src/node/server/transformRequest.ts +++ b/packages/vite/src/node/server/transformRequest.ts @@ -259,7 +259,7 @@ async function loadAndTransform( if (map && mod.file) { map = (typeof map === 'string' ? JSON.parse(map) : map) as SourceMap if (map.mappings && !map.sourcesContent) { - await injectSourcesContent(map, mod.file, logger) + await injectSourcesContent(map, mod.file, logger, config.symlinkResolver) } } diff --git a/packages/vite/src/node/symlinks.ts b/packages/vite/src/node/symlinks.ts new file mode 100644 index 00000000000000..6c857d3c60da1d --- /dev/null +++ b/packages/vite/src/node/symlinks.ts @@ -0,0 +1,170 @@ +import path from 'path' +import { createDebugger } from './utils' + +const isDebug = !!process.env.DEBUG +const isVerbose = isDebug && false +const debug = createDebugger('vite:symlinks') + +export interface SymlinkResolver { + fsCalls: number + cacheHits: number + cacheSize: number + realpathSync(path: string, seen?: Set): string + invalidate(path: string): void +} + +export interface FileSystem { + realpathSync: { native(path: string): string } + readlinkSync(path: string): string +} + +/** + * Create a symlink resolver that uses a cache to reduce the + * number of I/O calls. See #6030 for more information. + */ +export function createSymlinkResolver( + root: string, + fs: FileSystem = require('fs') +): SymlinkResolver { + const cache: Record = Object.create(null) + + // Recursively check the cache. + const resolveWithCache = (unresolvedPath: string) => { + let resolvedPath: string | undefined + while ( + (resolvedPath = cache[unresolvedPath]) && + resolvedPath !== unresolvedPath + ) { + unresolvedPath = resolvedPath + } + return resolvedPath + } + + const rootParent = path.dirname(root) + const cacheRecursively = (resolvedPath: string) => { + while (resolvedPath !== rootParent) { + cache[resolvedPath] = resolvedPath + resolvedPath = path.dirname(resolvedPath) + } + } + + return { + // Increment "fsCalls" whenever fs.realpath or fs.readlink are called. + fsCalls: 0, + // Increment "cacheHits" when a call to our `realpathSync` method + // is short-circuited by the cache. + cacheHits: 0, + get cacheSize() { + return Object.keys(cache).length + }, + // This method assumes `unresolvedPath` is normalized. + realpathSync(unresolvedPath, seen) { + if (isVerbose && !seen) { + debug(`Called realpathSync on "${unresolvedPath}"`) + } + + let resolvedPath = resolveWithCache(unresolvedPath) + if (resolvedPath) { + this.cacheHits++ + if (isVerbose) { + debug(`Resolution of "${unresolvedPath}" was cached`) + } + return resolvedPath + } + + let parentPath = path.dirname(unresolvedPath) + + // Check all parent directories within the project root. + // If our unresolved path is outside the project root, we only + // check the immediate parent directory (for optimal performance). + const isInRoot = unresolvedPath.startsWith(root + '/') + if (isInRoot) + while (parentPath !== root && !cache[parentPath]) { + parentPath = path.dirname(parentPath) + } + + // Use the nearest parent with a cached resolution. + const cachedParent = (resolvedPath = cache[parentPath]) + if (!cachedParent) { + if (isInRoot) { + // Always use the immediate parent when calling fs.realpath + parentPath = path.dirname(unresolvedPath) + } + + this.fsCalls++ + resolvedPath = fs.realpathSync.native(parentPath) + cache[parentPath] = resolvedPath + if (isDebug && parentPath !== resolvedPath) { + debug(`Resolved "${parentPath}" to "${resolvedPath}"`) + } + // Since fs.realpath resolves all directories in a given path, + // we can safely cache every directory in the resolved path. + if (resolvedPath.startsWith(root + '/')) { + cacheRecursively(resolvedPath) + } + } + + // Append the unresolved subpath. + resolvedPath += unresolvedPath.slice(parentPath.length) + + if (resolvedPath !== unresolvedPath) { + cache[unresolvedPath] = resolvedPath + if (isDebug) { + debug(`Resolved "${unresolvedPath}" to "${resolvedPath}"`) + } + + // Check the cache again now that our parent directories are resolved. + unresolvedPath = resolvedPath + resolvedPath = resolveWithCache(unresolvedPath) || unresolvedPath + if (resolvedPath !== unresolvedPath) { + if (isVerbose) { + debug(`Found "${unresolvedPath}" in cache`) + } + if (cachedParent) { + this.cacheHits++ + } + return resolvedPath + } + } + + // When the `unresolvedPath` is itself a symlink, we must follow it + // *after* resolving parent directories, in case its target path is + // pointing to a location outside a symlinked parent directory. + try { + this.fsCalls++ + const targetPath = fs.readlinkSync(resolvedPath) + if (targetPath) { + resolvedPath = path.resolve(path.dirname(resolvedPath), targetPath) + + // Avoid deadlocks from circular symlinks. + if (seen?.has(resolvedPath)) { + return resolvedPath + } + seen ??= new Set() + seen.add(resolvedPath) + + // The resolved path may be a file within a symlinked directory + // and/or a symlink itself. + resolvedPath = this.realpathSync(resolvedPath, seen) + } + } catch (e: any) { + if (e.errno !== -22) { + // Non-existent path or forbidden access + return unresolvedPath + } + } + + cache[unresolvedPath] = resolvedPath + if (isDebug && resolvedPath !== unresolvedPath) { + debug(`Resolved "${unresolvedPath}" to "${resolvedPath}"`) + } else if (isVerbose && !seen) { + debug(`Nothing to resolve for "${unresolvedPath}"`) + } + + return resolvedPath + }, + invalidate(unresolvedPath) { + delete cache[unresolvedPath] + } + } +} diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 8123ec08c68dea..9b3a859709f24a 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -34,6 +34,7 @@ import { import type { DepOptimizationConfig } from './optimizer' import type { ResolvedConfig } from './config' import type { ResolvedServerUrls } from './server' +import type { SymlinkResolver } from './symlinks' import type { CommonServerOptions } from '.' /** @@ -159,12 +160,17 @@ export function resolveFrom( export function nestedResolveFrom( id: string, basedir: string, - preserveSymlinks = false + options?: { symlinkResolver?: SymlinkResolver; preserveSymlinks?: boolean } ): string { const pkgs = id.split('>').map((pkg) => pkg.trim()) try { for (const pkg of pkgs) { - basedir = resolveFrom(pkg, basedir, preserveSymlinks) + basedir = resolveFrom(pkg, basedir, true) + basedir = getRealPath( + basedir, + options?.symlinkResolver, + options?.preserveSymlinks + ) } } catch {} return basedir @@ -1267,3 +1273,29 @@ export function arrayEqual(a: any[], b: any[]): boolean { } return true } + +export function getRealPath(file: string, preserveSymlinks?: boolean): string +export function getRealPath( + file: string, + resolver: SymlinkResolver | undefined, + preserveSymlinks?: boolean +): string +export function getRealPath( + file: string, + arg2?: boolean | SymlinkResolver, + preserveSymlinks?: boolean +): string { + let symlinkResolver: SymlinkResolver | undefined + if (typeof arg2 === 'boolean') { + preserveSymlinks = arg2 + } else { + symlinkResolver = arg2 + } + if (preserveSymlinks) { + return file + } + if (symlinkResolver) { + return symlinkResolver.realpathSync(file) + } + return fs.realpathSync.native(file) +} From faae6982ea7a7f074943af8560afb40d3e4f3dec Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Mon, 14 Nov 2022 18:34:12 -0500 Subject: [PATCH 2/5] fix: export missing types --- packages/vite/src/node/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index f43c460a58e184..535a4527d50182 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -51,7 +51,8 @@ export type { SSRTarget } from './ssr' export type { Plugin, HookHandler } from './plugin' -export type { PackageCache, PackageData } from './packages' +export type { PackageCache, PackageData, LoadPackageOptions } from './packages' +export type { SymlinkResolver } from './symlinks' export type { Logger, LogOptions, From 178dd9c0a543497e39d3a0e51e9fd54b93ebc91c Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Tue, 15 Nov 2022 10:46:32 -0500 Subject: [PATCH 3/5] fix: remove cjsInclude from another PR --- packages/vite/src/node/packages.ts | 28 +++++----------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/packages/vite/src/node/packages.ts b/packages/vite/src/node/packages.ts index eb3bb147f9ed97..c093b3660970df 100644 --- a/packages/vite/src/node/packages.ts +++ b/packages/vite/src/node/packages.ts @@ -109,7 +109,6 @@ export type LoadPackageOptions = { preserveSymlinks?: boolean symlinkResolver?: SymlinkResolver packageCache?: PackageCache - cjsInclude?: (string | RegExp)[] } /** @@ -141,28 +140,11 @@ export function loadPackageData( ? { preserveSymlinks: arg2, packageCache: arg3 } : arg2 || {} - if (options.preserveSymlinks !== true) { - const originalPkgPath = pkgPath - - // Support uncached realpath calls for backwards compatibility. - pkgPath = getRealPath( - pkgPath, - options.symlinkResolver, - options.preserveSymlinks - ) - - // In case a linked package is a local clone of a CommonJS dependency, - // we need to ensure @rollup/plugin-commonjs analyzes the package even - // after it's been resolved to its actual file location. - if (options.cjsInclude && pkgPath !== originalPkgPath) { - const filter = createFilter(options.cjsInclude, undefined, { - resolve: false - }) - if (!filter(pkgPath) && filter(originalPkgPath)) { - options.cjsInclude.push(path.dirname(pkgPath) + '/**') - } - } - } + pkgPath = getRealPath( + pkgPath, + options.symlinkResolver, + options.preserveSymlinks + ) let cached: PackageData | undefined if ((cached = options.packageCache?.get(pkgPath))) { From f8092f1ac05b1e398dc146e834a0cf97e2f84ddf Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Tue, 15 Nov 2022 10:57:16 -0500 Subject: [PATCH 4/5] chore: fix lint --- packages/vite/src/node/__tests__/SymlinkResolver.spec.ts | 2 +- packages/vite/src/node/symlinks.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/vite/src/node/__tests__/SymlinkResolver.spec.ts b/packages/vite/src/node/__tests__/SymlinkResolver.spec.ts index 370096f605072e..65359e28f43c39 100644 --- a/packages/vite/src/node/__tests__/SymlinkResolver.spec.ts +++ b/packages/vite/src/node/__tests__/SymlinkResolver.spec.ts @@ -1,4 +1,4 @@ -import { join, relative, resolve } from 'path' +import { relative, resolve } from 'node:path' import type { SymlinkResolver } from '../symlinks' import { createSymlinkResolver } from '../symlinks' diff --git a/packages/vite/src/node/symlinks.ts b/packages/vite/src/node/symlinks.ts index 6c857d3c60da1d..54b51f48a97e4f 100644 --- a/packages/vite/src/node/symlinks.ts +++ b/packages/vite/src/node/symlinks.ts @@ -1,4 +1,5 @@ -import path from 'path' +import path from 'node:path' +import nodeFs from 'node:fs' import { createDebugger } from './utils' const isDebug = !!process.env.DEBUG @@ -24,7 +25,7 @@ export interface FileSystem { */ export function createSymlinkResolver( root: string, - fs: FileSystem = require('fs') + fs: FileSystem = nodeFs ): SymlinkResolver { const cache: Record = Object.create(null) From 5ee39774f1b480e09fbf0d2200ef0f23ffa0c5f2 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Tue, 15 Nov 2022 10:58:48 -0500 Subject: [PATCH 5/5] chore: move tests to vitest --- packages/vite/src/node/__tests__/SymlinkResolver.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/__tests__/SymlinkResolver.spec.ts b/packages/vite/src/node/__tests__/SymlinkResolver.spec.ts index 65359e28f43c39..fc3a3bd2e3acf2 100644 --- a/packages/vite/src/node/__tests__/SymlinkResolver.spec.ts +++ b/packages/vite/src/node/__tests__/SymlinkResolver.spec.ts @@ -1,11 +1,12 @@ import { relative, resolve } from 'node:path' +import { beforeEach, describe, expect, test, vi } from 'vitest' import type { SymlinkResolver } from '../symlinks' import { createSymlinkResolver } from '../symlinks' let resolver: SymlinkResolver -const realpathMock = jest.fn() -const readlinkMock = jest.fn() +const realpathMock = vi.fn() +const readlinkMock = vi.fn() const root = '/dev/root' const realpathSync = (p: string) => {