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..fc3a3bd2e3acf2 --- /dev/null +++ b/packages/vite/src/node/__tests__/SymlinkResolver.spec.ts @@ -0,0 +1,159 @@ +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 = vi.fn() +const readlinkMock = vi.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/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, diff --git a/packages/vite/src/node/packages.ts b/packages/vite/src/node/packages.ts index e1a85bff441212..c093b3660970df 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,49 @@ export function resolvePackageData( return null } +export type LoadPackageOptions = { + preserveSymlinks?: boolean + symlinkResolver?: SymlinkResolver + packageCache?: PackageCache +} + +/** + * 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 || {} + + pkgPath = getRealPath( + pkgPath, + options.symlinkResolver, + options.preserveSymlinks + ) let cached: PackageData | undefined - if ((cached = packageCache?.get(pkgPath))) { + if ((cached = options.packageCache?.get(pkgPath))) { return cached } @@ -127,7 +185,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..54b51f48a97e4f --- /dev/null +++ b/packages/vite/src/node/symlinks.ts @@ -0,0 +1,171 @@ +import path from 'node:path' +import nodeFs from 'node:fs' +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 = nodeFs +): 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) +}