diff --git a/packages/vite/package.json b/packages/vite/package.json index d3daf7938255da..1f837b12a3017b 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -86,6 +86,7 @@ "@rollup/plugin-node-resolve": "15.0.1", "@rollup/plugin-typescript": "^11.0.0", "@rollup/pluginutils": "^5.0.2", + "@types/pnpapi": "^0.0.2", "acorn": "^8.8.2", "acorn-walk": "^8.2.0", "cac": "^6.7.14", diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 62b3a9f14d7494..42620fc2ff7ab2 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -18,7 +18,6 @@ import { getHash, isOptimizable, lookupFile, - nestedResolveFrom, normalizeId, normalizePath, removeDir, @@ -27,6 +26,7 @@ import { } from '../utils' import { transformWithEsbuild } from '../plugins/esbuild' import { ESBUILD_MODULES_TARGET } from '../constants' +import { resolvePkgJsonPath } from '../packages' import { esbuildCjsExternalPlugin, esbuildDepPlugin } from './esbuildDepPlugin' import { scanImports } from './scan' export { @@ -855,16 +855,30 @@ function createOptimizeDepsIncludeResolver( // 'foo > bar > baz' => 'foo > bar' & 'baz' const nestedRoot = id.substring(0, lastArrowIndex).trim() const nestedPath = id.substring(lastArrowIndex + 1).trim() - const basedir = nestedResolveFrom( + const basedir = nestedResolvePkgJsonPath( nestedRoot, config.root, config.resolve.preserveSymlinks, - ssr, ) return await resolve(nestedPath, basedir, undefined, ssr) } } +/** + * Like `resolvePkgJsonPath`, but supports resolving nested package names with '>' + */ +function nestedResolvePkgJsonPath( + id: string, + basedir: string, + preserveSymlinks = false, +) { + const pkgs = id.split('>').map((pkg) => pkg.trim()) + for (const pkg of pkgs) { + basedir = resolvePkgJsonPath(pkg, basedir, preserveSymlinks) || basedir + } + return basedir +} + export function newDepOptimizationProcessing(): DepOptimizationProcessing { let resolve: () => void const promise = new Promise((_resolve) => { diff --git a/packages/vite/src/node/packages.ts b/packages/vite/src/node/packages.ts index 1451d18853729a..a26c6e50a04350 100644 --- a/packages/vite/src/node/packages.ts +++ b/packages/vite/src/node/packages.ts @@ -1,9 +1,18 @@ import fs from 'node:fs' import path from 'node:path' -import { createDebugger, createFilter, resolveFrom } from './utils' +import { createRequire } from 'node:module' +import { createDebugger, createFilter, safeRealpathSync } from './utils' import type { ResolvedConfig } from './config' import type { Plugin } from './plugin' +// eslint-disable-next-line @typescript-eslint/consistent-type-imports +let pnp: typeof import('pnpapi') | undefined +if (process.versions.pnp) { + try { + pnp = createRequire(import.meta.url)('pnpapi') + } catch {} +} + const isDebug = process.env.DEBUG const debug = createDebugger('vite:resolve-details', { onlyWhenFocused: true, @@ -60,9 +69,9 @@ export function resolvePackageData( return pkg } } - let pkgPath: string | undefined + const pkgPath = resolvePkgJsonPath(id, basedir, preserveSymlinks) + if (!pkgPath) return null try { - pkgPath = resolveFrom(`${id}/package.json`, basedir, preserveSymlinks) pkg = loadPackageData(pkgPath, true, packageCache) if (packageCache) { packageCache.set(cacheKey!, pkg) @@ -72,12 +81,8 @@ export function resolvePackageData( if (e instanceof SyntaxError) { isDebug && debug(`Parsing failed: ${pkgPath}`) } - // Ignore error for missing package.json - else if (e.code !== 'MODULE_NOT_FOUND') { - throw e - } + throw e } - return null } export function loadPackageData( @@ -86,7 +91,7 @@ export function loadPackageData( packageCache?: PackageCache, ): PackageData { if (!preserveSymlinks) { - pkgPath = fs.realpathSync.native(pkgPath) + pkgPath = safeRealpathSync(pkgPath) } let cached: PackageData | undefined @@ -178,3 +183,30 @@ export function watchPackageDataPlugin(config: ResolvedConfig): Plugin { }, } } + +export function resolvePkgJsonPath( + pkgName: string, + basedir: string, + preserveSymlinks = false, +): string | undefined { + if (pnp) { + const pkg = pnp.resolveToUnqualified(pkgName, basedir) + if (!pkg) return undefined + return path.join(pkg, 'package.json') + } + + let root = basedir + while (root) { + const pkg = path.join(root, 'node_modules', pkgName, 'package.json') + try { + if (fs.existsSync(pkg)) { + return preserveSymlinks ? pkg : safeRealpathSync(pkg) + } + } catch {} + const nextRoot = path.dirname(root) + if (nextRoot === root) break + root = nextRoot + } + + return undefined +} diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index df75ced4f6da94..d1b43ce08e407f 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -1252,38 +1252,21 @@ function getRealPath(resolved: string, preserveSymlinks?: boolean): string { } /** - * if importer was not resolved by vite's resolver previously - * (when esbuild resolved it) - * resolve importer's pkg and add to idToPkgMap + * Load closest `package.json` to `importer` */ function resolvePkg(importer: string, options: InternalResolveOptions) { - const { root, preserveSymlinks, packageCache } = options + const { preserveSymlinks, packageCache } = options if (importer.includes('\x00')) { return null } - const possiblePkgIds: string[] = [] - for (let prevSlashIndex = -1; ; ) { - const slashIndex = importer.indexOf(isWindows ? '\\' : '/', prevSlashIndex) - if (slashIndex < 0) { - break - } - - prevSlashIndex = slashIndex + 1 - - const possiblePkgId = importer.slice(0, slashIndex) - possiblePkgIds.push(possiblePkgId) - } - - let pkg: PackageData | undefined - possiblePkgIds.reverse().find((pkgId) => { - pkg = resolvePackageData(pkgId, root, preserveSymlinks, packageCache)! - return pkg - })! - - if (pkg) { + const pkgPath = lookupFile(importer, ['package.json'], { pathOnly: true }) + if (pkgPath) { + const pkg = loadPackageData(pkgPath, preserveSymlinks, packageCache) idToPkgMap.set(importer, pkg) + return pkg } - return pkg + + return undefined } diff --git a/packages/vite/src/node/ssr/ssrExternal.ts b/packages/vite/src/node/ssr/ssrExternal.ts index ce08b020d17fbf..f3ecb56f2a7312 100644 --- a/packages/vite/src/node/ssr/ssrExternal.ts +++ b/packages/vite/src/node/ssr/ssrExternal.ts @@ -11,9 +11,9 @@ import { isDefined, lookupFile, normalizePath, - resolveFrom, } from '../utils' import type { Logger, ResolvedConfig } from '..' +import { resolvePkgJsonPath } from '../packages' const debug = createDebugger('vite:ssr-external') @@ -256,16 +256,16 @@ function cjsSsrCollectExternals( // which returns with '/', require.resolve returns with '\\' requireEntry = normalizePath(_require.resolve(id, { paths: [root] })) } catch (e) { - try { - // no main entry, but deep imports may be allowed - const pkgPath = resolveFrom(`${id}/package.json`, root) + // no main entry, but deep imports may be allowed + const pkgPath = resolvePkgJsonPath(id, root) + if (pkgPath) { if (pkgPath.includes('node_modules')) { ssrExternals.add(id) } else { depsToTrace.add(path.dirname(pkgPath)) } continue - } catch {} + } // resolve failed, assume include debug(`Failed to resolve entries for package "${id}"\n`, e) @@ -277,8 +277,10 @@ function cjsSsrCollectExternals( } // trace the dependencies of linked packages else if (!esmEntry.includes('node_modules')) { - const pkgPath = resolveFrom(`${id}/package.json`, root) - depsToTrace.add(path.dirname(pkgPath)) + const pkgPath = resolvePkgJsonPath(id, root) + if (pkgPath) { + depsToTrace.add(path.dirname(pkgPath)) + } } // has separate esm/require entry, assume require entry is cjs else if (esmEntry !== requireEntry) { @@ -288,7 +290,11 @@ function cjsSsrCollectExternals( // or are there others like SystemJS / AMD that we'd need to handle? // for now, we'll just leave this as is else if (/\.m?js$/.test(esmEntry)) { - const pkgPath = resolveFrom(`${id}/package.json`, root) + const pkgPath = resolvePkgJsonPath(id, root) + if (!pkgPath) { + continue + } + const pkgContent = fs.readFileSync(pkgPath, 'utf-8') if (!pkgContent) { diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 1ff7291f84a319..02b7f6d48c80fe 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -127,15 +127,9 @@ export function isOptimizable( export const bareImportRE = /^[\w@](?!.*:\/\/)/ export const deepImportRE = /^([^@][^/]*)\/|^(@[^/]+\/[^/]+)\// -export let isRunningWithYarnPnp: boolean - // TODO: use import() const _require = createRequire(import.meta.url) -try { - isRunningWithYarnPnp = Boolean(_require('pnpapi')) -} catch {} - const ssrExtensions = ['.js', '.cjs', '.json', '.node'] export function resolveFrom( @@ -149,29 +143,10 @@ export function resolveFrom( paths: [], extensions: ssr ? ssrExtensions : DEFAULT_EXTENSIONS, // necessary to work with pnpm - preserveSymlinks: preserveSymlinks || isRunningWithYarnPnp || false, + preserveSymlinks: preserveSymlinks || !!process.versions.pnp || false, }) } -/** - * like `resolveFrom` but supports resolving `>` path in `id`, - * for example: `foo > bar > baz` - */ -export function nestedResolveFrom( - id: string, - basedir: string, - preserveSymlinks = false, - ssr = false, -): string { - const pkgs = id.split('>').map((pkg) => pkg.trim()) - try { - for (const pkg of pkgs) { - basedir = resolveFrom(pkg, basedir, preserveSymlinks, ssr) - } - } catch {} - return basedir -} - // set in bin/vite.js const filter = process.env.VITE_DEBUG_FILTER @@ -607,6 +582,13 @@ export const removeDir = isWindows } export const renameDir = isWindows ? promisify(gracefulRename) : fs.renameSync +// `fs.realpathSync.native` resolves differently in Windows network drive, +// causing file read errors. skip for now. +// https://github.com/nodejs/node/issues/37737 +export const safeRealpathSync = isWindows + ? fs.realpathSync + : fs.realpathSync.native + export function ensureWatchedFile( watcher: FSWatcher, file: string | null, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97c07b405e12ce..1fd4807dde3821 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -177,6 +177,7 @@ importers: '@rollup/plugin-node-resolve': 15.0.1 '@rollup/plugin-typescript': ^11.0.0 '@rollup/pluginutils': ^5.0.2 + '@types/pnpapi': ^0.0.2 acorn: ^8.8.2 acorn-walk: ^8.2.0 cac: ^6.7.14 @@ -246,6 +247,7 @@ importers: '@rollup/plugin-node-resolve': 15.0.1_rollup@3.20.0 '@rollup/plugin-typescript': 11.0.0_rollup@3.20.0+tslib@2.5.0 '@rollup/pluginutils': 5.0.2_rollup@3.20.0 + '@types/pnpapi': 0.0.2 acorn: 8.8.2 acorn-walk: 8.2.0_acorn@8.8.2 cac: 6.7.14 @@ -3491,6 +3493,10 @@ packages: resolution: {integrity: sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g==} dev: true + /@types/pnpapi/0.0.2: + resolution: {integrity: sha512-2lqsQt1iXkiTqwuzw2SS50iduoPZNpV/ou4/vJD443C0weF63Gqd3ErGS811CBNMzLO64zVJ+tiyh+4yimdYkg==} + dev: true + /@types/prompts/2.4.2: resolution: {integrity: sha512-TwNx7qsjvRIUv/BCx583tqF5IINEVjCNqg9ofKHRlSoUHE62WBHrem4B1HGXcIrG511v29d1kJ9a/t2Esz7MIg==} dependencies: