From 1095a0edb22ed67718b4f201f7c9ad31cd62b468 Mon Sep 17 00:00:00 2001 From: ygj6 Date: Tue, 24 Aug 2021 15:32:43 +0800 Subject: [PATCH] feat: add resolve.preserveSymlinks option --- docs/config/index.md | 10 ++ packages/vite/src/node/config.ts | 1 + packages/vite/src/node/plugins/resolve.ts | 99 +++++++++++++++---- packages/vite/src/node/ssr/ssrExternal.ts | 4 +- packages/vite/src/node/ssr/ssrModuleLoader.ts | 25 ++++- packages/vite/src/node/utils.ts | 9 +- 6 files changed, 120 insertions(+), 28 deletions(-) diff --git a/docs/config/index.md b/docs/config/index.md index e7043a5145cc30..840d5740062169 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -206,6 +206,16 @@ export default defineConfig(async ({ command, mode }) => { List of file extensions to try for imports that omit extensions. Note it is **NOT** recommended to omit extensions for custom import types (e.g. `.vue`) since it can interfere with IDE and type support. +### resolve.preserveSymlinks + +- **Type:** `boolean` +- **Default:** `false` + + Enabling this setting causes vite to determine file identity by the original file path (i.e. the path without following symlinks) instead of the real file path (i.e. the path after following symlinks). + +- **Related:** [esbuild#preserve-symlinks](https://esbuild.github.io/api/#preserve-symlinks), [webpack#resolve.symlinks + ](https://webpack.js.org/configuration/resolve/#resolvesymlinks) + ### css.modules - **Type:** diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index cd1d801fb8388d..2e31f5eb54f865 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -437,6 +437,7 @@ export async function resolveConfig( ...config.optimizeDeps, esbuildOptions: { keepNames: config.optimizeDeps?.keepNames, + preserveSymlinks: config.resolve?.preserveSymlinks, ...config.optimizeDeps?.esbuildOptions } } diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index dfaa270e79050f..136e889b4d33db 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -44,6 +44,7 @@ export interface ResolveOptions { conditions?: string[] extensions?: string[] dedupe?: string[] + preserveSymlinks?: boolean } export interface InternalResolveOptions extends ResolveOptions { @@ -108,12 +109,14 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin { const options = isRequire ? requireOptions : baseOptions + const preserveSymlinks = !!server?.config.resolve.preserveSymlinks + let res: string | PartialResolvedId | undefined // explicit fs paths that starts with /@fs/* if (asSrc && id.startsWith(FS_PREFIX)) { const fsPath = fsPathFromId(id) - res = tryFsResolve(fsPath, options) + res = tryFsResolve(fsPath, options, preserveSymlinks) isDebug && debug(`[@fs] ${chalk.cyan(id)} -> ${chalk.dim(res)}`) // always return here even if res doesn't exist since /@fs/ is explicit // if the file doesn't exist it should be a 404 @@ -124,7 +127,7 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin { // /foo -> /fs-root/foo if (asSrc && id.startsWith('/')) { const fsPath = path.resolve(root, id.slice(1)) - if ((res = tryFsResolve(fsPath, options))) { + if ((res = tryFsResolve(fsPath, options, preserveSymlinks))) { isDebug && debug(`[url] ${chalk.cyan(id)} -> ${chalk.dim(res)}`) return res } @@ -159,12 +162,18 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin { if ( targetWeb && - (res = tryResolveBrowserMapping(fsPath, importer, options, true)) + (res = tryResolveBrowserMapping( + fsPath, + importer, + options, + true, + preserveSymlinks + )) ) { return res } - if ((res = tryFsResolve(fsPath, options))) { + if ((res = tryFsResolve(fsPath, options, preserveSymlinks))) { isDebug && debug(`[relative] ${chalk.cyan(id)} -> ${chalk.dim(res)}`) const pkg = importer != null && idToPkgMap.get(importer) if (pkg) { @@ -179,7 +188,10 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin { } // absolute fs paths - if (path.isAbsolute(id) && (res = tryFsResolve(id, options))) { + if ( + path.isAbsolute(id) && + (res = tryFsResolve(id, options, preserveSymlinks)) + ) { isDebug && debug(`[fs] ${chalk.cyan(id)} -> ${chalk.dim(res)}`) return res } @@ -211,7 +223,13 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin { if ( targetWeb && - (res = tryResolveBrowserMapping(id, importer, options, false)) + (res = tryResolveBrowserMapping( + id, + importer, + options, + false, + preserveSymlinks + )) ) { return res } @@ -278,6 +296,7 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin { function tryFsResolve( fsPath: string, options: InternalResolveOptions, + preserveSymlinks: boolean, tryIndex = true, targetWeb = true ): string | undefined { @@ -301,6 +320,7 @@ function tryFsResolve( options, false, targetWeb, + preserveSymlinks, options.tryPrefix, options.skipPackageJson )) @@ -316,6 +336,7 @@ function tryFsResolve( options, false, targetWeb, + preserveSymlinks, options.tryPrefix, options.skipPackageJson )) @@ -331,6 +352,7 @@ function tryFsResolve( options, tryIndex, targetWeb, + preserveSymlinks, options.tryPrefix, options.skipPackageJson )) @@ -345,6 +367,7 @@ function tryResolveFile( options: InternalResolveOptions, tryIndex: boolean, targetWeb: boolean, + preserveSymlinks: boolean, tryPrefix?: string, skipPackageJson?: boolean ): string | undefined { @@ -365,16 +388,30 @@ function tryResolveFile( if (fs.existsSync(pkgPath)) { // path points to a node package const pkg = loadPackageData(pkgPath) - return resolvePackageEntry(file, pkg, options, targetWeb) + const resolved = resolvePackageEntry( + file, + pkg, + options, + targetWeb, + preserveSymlinks + ) + return resolved ? getRealPath(resolved, preserveSymlinks) : resolved } } - const index = tryFsResolve(file + '/index', options) + const index = tryFsResolve(file + '/index', options, preserveSymlinks) if (index) return index + postfix } } if (tryPrefix) { const prefixed = `${path.dirname(file)}/${tryPrefix}${path.basename(file)}` - return tryResolveFile(prefixed, postfix, options, tryIndex, targetWeb) + return tryResolveFile( + prefixed, + postfix, + options, + tryIndex, + targetWeb, + preserveSymlinks + ) } } @@ -405,18 +442,23 @@ export function tryNodeResolve( basedir = root } - const pkg = resolvePackageData(pkgId, basedir) + const preserveSymlinks = !!server?.config.resolve.preserveSymlinks + + const pkg = resolvePackageData(pkgId, basedir, preserveSymlinks) if (!pkg) { return } let resolved = deepMatch - ? resolveDeepImport(id, pkg, options, targetWeb) - : resolvePackageEntry(id, pkg, options, targetWeb) + ? resolveDeepImport(id, pkg, options, targetWeb, preserveSymlinks) + : resolvePackageEntry(id, pkg, options, targetWeb, preserveSymlinks) if (!resolved) { return } + + resolved = getRealPath(resolved, preserveSymlinks) + // link id to pkg for browser field mapping check idToPkgMap.set(resolved, pkg) if (isBuild) { @@ -504,14 +546,15 @@ const packageCache = new Map() export function resolvePackageData( id: string, - basedir: string + basedir: string, + preserveSymlinks: boolean ): PackageData | undefined { const cacheKey = id + basedir if (packageCache.has(cacheKey)) { return packageCache.get(cacheKey) } try { - const pkgPath = resolveFrom(`${id}/package.json`, basedir) + const pkgPath = resolveFrom(`${id}/package.json`, basedir, preserveSymlinks) return loadPackageData(pkgPath, cacheKey) } catch (e) { isDebug && debug(`${chalk.red(`[failed loading package.json]`)} ${id}`) @@ -560,7 +603,8 @@ export function resolvePackageEntry( id: string, { dir, data, setResolvedCache, getResolvedCache }: PackageData, options: InternalResolveOptions, - targetWeb: boolean + targetWeb: boolean, + preserveSymlinks = false ): string | undefined { const cached = getResolvedCache('.', targetWeb) if (cached) { @@ -597,7 +641,8 @@ export function resolvePackageEntry( // instead; Otherwise, assume it's ESM and use it. const resolvedBrowserEntry = tryFsResolve( path.join(dir, browserEntry), - options + options, + preserveSymlinks ) if (resolvedBrowserEntry) { const content = fs.readFileSync(resolvedBrowserEntry, 'utf-8') @@ -643,7 +688,11 @@ export function resolvePackageEntry( } entryPoint = path.join(dir, entryPoint) - const resolvedEntryPoint = tryFsResolve(entryPoint, options) + const resolvedEntryPoint = tryFsResolve( + entryPoint, + options, + preserveSymlinks + ) if (resolvedEntryPoint) { isDebug && @@ -700,7 +749,8 @@ function resolveDeepImport( data }: PackageData, options: InternalResolveOptions, - targetWeb: boolean + targetWeb: boolean, + preserveSymlinks: boolean ): string | undefined { id = '.' + id.slice(data.name.length) const cache = getResolvedCache(id, targetWeb) @@ -738,6 +788,7 @@ function resolveDeepImport( const resolved = tryFsResolve( path.join(dir, relativeId), options, + preserveSymlinks, !exportsField, // try index only if no exports field targetWeb ) @@ -754,7 +805,8 @@ function tryResolveBrowserMapping( id: string, importer: string | undefined, options: InternalResolveOptions, - isFilePath: boolean + isFilePath: boolean, + preserveSymlinks: boolean ) { let res: string | undefined const pkg = importer && idToPkgMap.get(importer) @@ -763,7 +815,7 @@ function tryResolveBrowserMapping( const browserMappedPath = mapWithBrowserField(mapId, pkg.data.browser) if (browserMappedPath) { const fsPath = path.join(pkg.dir, browserMappedPath) - if ((res = tryFsResolve(fsPath, options))) { + if ((res = tryFsResolve(fsPath, options, preserveSymlinks))) { isDebug && debug(`[browser mapped] ${chalk.cyan(id)} -> ${chalk.dim(res)}`) idToPkgMap.set(res, pkg) @@ -807,3 +859,10 @@ 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) { + return (resolved = normalizePath(fs.realpathSync(resolved))) + } + return resolved +} diff --git a/packages/vite/src/node/ssr/ssrExternal.ts b/packages/vite/src/node/ssr/ssrExternal.ts index 4c3b40e1915a9a..2c4214ad9e751b 100644 --- a/packages/vite/src/node/ssr/ssrExternal.ts +++ b/packages/vite/src/node/ssr/ssrExternal.ts @@ -96,7 +96,9 @@ export function resolveSSRExternal( } for (const id of depsToTrace) { - const depRoot = path.dirname(resolveFrom(`${id}/package.json`, root)) + const depRoot = path.dirname( + resolveFrom(`${id}/package.json`, root, !!config.resolve.preserveSymlinks) + ) resolveSSRExternal( { ...config, diff --git a/packages/vite/src/node/ssr/ssrModuleLoader.ts b/packages/vite/src/node/ssr/ssrModuleLoader.ts index 6a64d6a0fdaca9..24f005bd22aac8 100644 --- a/packages/vite/src/node/ssr/ssrModuleLoader.ts +++ b/packages/vite/src/node/ssr/ssrModuleLoader.ts @@ -91,7 +91,12 @@ async function instantiateModule( const ssrImport = async (dep: string) => { if (dep[0] !== '.' && dep[0] !== '/') { - return nodeRequire(dep, mod.file, server.config.root) + return nodeRequire( + dep, + mod.file, + server.config.root, + !!server.config.resolve.preserveSymlinks + ) } dep = unwrapId(dep) if (!isCircular(dep) && !pendingImports.get(dep)?.some(isCircular)) { @@ -169,8 +174,13 @@ async function instantiateModule( return Object.freeze(ssrModule) } -function nodeRequire(id: string, importer: string | null, root: string) { - const mod = require(resolve(id, importer, root)) +function nodeRequire( + id: string, + importer: string | null, + root: string, + preserveSymlinks: boolean +) { + const mod = require(resolve(id, importer, root, preserveSymlinks)) const defaultExport = mod.__esModule ? mod.default : mod // rollup-style default import interop for cjs return new Proxy(mod, { @@ -183,7 +193,12 @@ function nodeRequire(id: string, importer: string | null, root: string) { const resolveCache = new Map() -function resolve(id: string, importer: string | null, root: string) { +function resolve( + id: string, + importer: string | null, + root: string, + preserveSymlinks: boolean +) { const key = id + importer + root const cached = resolveCache.get(key) if (cached) { @@ -193,7 +208,7 @@ function resolve(id: string, importer: string | null, root: string) { importer && fs.existsSync(cleanUrl(importer)) ? path.dirname(importer) : root - const resolved = resolveFrom(id, resolveDir, true) + const resolved = resolveFrom(id, resolveDir, preserveSymlinks, true) resolveCache.set(key, resolved) return resolved } diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 305e08ce37796a..bd82a72233aa10 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -40,12 +40,17 @@ try { const ssrExtensions = ['.js', '.json', '.node'] -export function resolveFrom(id: string, basedir: string, ssr = false): string { +export function resolveFrom( + id: string, + basedir: string, + preserveSymlinks = false, + ssr = false +): string { return resolve.sync(id, { basedir, extensions: ssr ? ssrExtensions : DEFAULT_EXTENSIONS, // necessary to work with pnpm - preserveSymlinks: isRunningWithYarnPnp || false + preserveSymlinks: preserveSymlinks || isRunningWithYarnPnp || false }) }