Skip to content

Commit 1fc8c65

Browse files
authored
perf(resolve): improve package.json resolve speed (#12441)
1 parent 8d2931b commit 1fc8c65

File tree

7 files changed

+95
-71
lines changed

7 files changed

+95
-71
lines changed

packages/vite/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
"@rollup/plugin-node-resolve": "15.0.1",
8787
"@rollup/plugin-typescript": "^11.0.0",
8888
"@rollup/pluginutils": "^5.0.2",
89+
"@types/pnpapi": "^0.0.2",
8990
"acorn": "^8.8.2",
9091
"acorn-walk": "^8.2.0",
9192
"cac": "^6.7.14",

packages/vite/src/node/optimizer/index.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {
1818
getHash,
1919
isOptimizable,
2020
lookupFile,
21-
nestedResolveFrom,
2221
normalizeId,
2322
normalizePath,
2423
removeDir,
@@ -27,6 +26,7 @@ import {
2726
} from '../utils'
2827
import { transformWithEsbuild } from '../plugins/esbuild'
2928
import { ESBUILD_MODULES_TARGET } from '../constants'
29+
import { resolvePkgJsonPath } from '../packages'
3030
import { esbuildCjsExternalPlugin, esbuildDepPlugin } from './esbuildDepPlugin'
3131
import { scanImports } from './scan'
3232
export {
@@ -855,16 +855,30 @@ function createOptimizeDepsIncludeResolver(
855855
// 'foo > bar > baz' => 'foo > bar' & 'baz'
856856
const nestedRoot = id.substring(0, lastArrowIndex).trim()
857857
const nestedPath = id.substring(lastArrowIndex + 1).trim()
858-
const basedir = nestedResolveFrom(
858+
const basedir = nestedResolvePkgJsonPath(
859859
nestedRoot,
860860
config.root,
861861
config.resolve.preserveSymlinks,
862-
ssr,
863862
)
864863
return await resolve(nestedPath, basedir, undefined, ssr)
865864
}
866865
}
867866

867+
/**
868+
* Like `resolvePkgJsonPath`, but supports resolving nested package names with '>'
869+
*/
870+
function nestedResolvePkgJsonPath(
871+
id: string,
872+
basedir: string,
873+
preserveSymlinks = false,
874+
) {
875+
const pkgs = id.split('>').map((pkg) => pkg.trim())
876+
for (const pkg of pkgs) {
877+
basedir = resolvePkgJsonPath(pkg, basedir, preserveSymlinks) || basedir
878+
}
879+
return basedir
880+
}
881+
868882
export function newDepOptimizationProcessing(): DepOptimizationProcessing {
869883
let resolve: () => void
870884
const promise = new Promise((_resolve) => {

packages/vite/src/node/packages.ts

+41-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
import fs from 'node:fs'
22
import path from 'node:path'
3-
import { createDebugger, createFilter, resolveFrom } from './utils'
3+
import { createRequire } from 'node:module'
4+
import { createDebugger, createFilter, safeRealpathSync } from './utils'
45
import type { ResolvedConfig } from './config'
56
import type { Plugin } from './plugin'
67

8+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
9+
let pnp: typeof import('pnpapi') | undefined
10+
if (process.versions.pnp) {
11+
try {
12+
pnp = createRequire(import.meta.url)('pnpapi')
13+
} catch {}
14+
}
15+
716
const isDebug = process.env.DEBUG
817
const debug = createDebugger('vite:resolve-details', {
918
onlyWhenFocused: true,
@@ -60,9 +69,9 @@ export function resolvePackageData(
6069
return pkg
6170
}
6271
}
63-
let pkgPath: string | undefined
72+
const pkgPath = resolvePkgJsonPath(id, basedir, preserveSymlinks)
73+
if (!pkgPath) return null
6474
try {
65-
pkgPath = resolveFrom(`${id}/package.json`, basedir, preserveSymlinks)
6675
pkg = loadPackageData(pkgPath, true, packageCache)
6776
if (packageCache) {
6877
packageCache.set(cacheKey!, pkg)
@@ -72,12 +81,8 @@ export function resolvePackageData(
7281
if (e instanceof SyntaxError) {
7382
isDebug && debug(`Parsing failed: ${pkgPath}`)
7483
}
75-
// Ignore error for missing package.json
76-
else if (e.code !== 'MODULE_NOT_FOUND') {
77-
throw e
78-
}
84+
throw e
7985
}
80-
return null
8186
}
8287

8388
export function loadPackageData(
@@ -86,7 +91,7 @@ export function loadPackageData(
8691
packageCache?: PackageCache,
8792
): PackageData {
8893
if (!preserveSymlinks) {
89-
pkgPath = fs.realpathSync.native(pkgPath)
94+
pkgPath = safeRealpathSync(pkgPath)
9095
}
9196

9297
let cached: PackageData | undefined
@@ -178,3 +183,30 @@ export function watchPackageDataPlugin(config: ResolvedConfig): Plugin {
178183
},
179184
}
180185
}
186+
187+
export function resolvePkgJsonPath(
188+
pkgName: string,
189+
basedir: string,
190+
preserveSymlinks = false,
191+
): string | undefined {
192+
if (pnp) {
193+
const pkg = pnp.resolveToUnqualified(pkgName, basedir)
194+
if (!pkg) return undefined
195+
return path.join(pkg, 'package.json')
196+
}
197+
198+
let root = basedir
199+
while (root) {
200+
const pkg = path.join(root, 'node_modules', pkgName, 'package.json')
201+
try {
202+
if (fs.existsSync(pkg)) {
203+
return preserveSymlinks ? pkg : safeRealpathSync(pkg)
204+
}
205+
} catch {}
206+
const nextRoot = path.dirname(root)
207+
if (nextRoot === root) break
208+
root = nextRoot
209+
}
210+
211+
return undefined
212+
}

packages/vite/src/node/plugins/resolve.ts

+8-25
Original file line numberDiff line numberDiff line change
@@ -1252,38 +1252,21 @@ function getRealPath(resolved: string, preserveSymlinks?: boolean): string {
12521252
}
12531253

12541254
/**
1255-
* if importer was not resolved by vite's resolver previously
1256-
* (when esbuild resolved it)
1257-
* resolve importer's pkg and add to idToPkgMap
1255+
* Load closest `package.json` to `importer`
12581256
*/
12591257
function resolvePkg(importer: string, options: InternalResolveOptions) {
1260-
const { root, preserveSymlinks, packageCache } = options
1258+
const { preserveSymlinks, packageCache } = options
12611259

12621260
if (importer.includes('\x00')) {
12631261
return null
12641262
}
12651263

1266-
const possiblePkgIds: string[] = []
1267-
for (let prevSlashIndex = -1; ; ) {
1268-
const slashIndex = importer.indexOf(isWindows ? '\\' : '/', prevSlashIndex)
1269-
if (slashIndex < 0) {
1270-
break
1271-
}
1272-
1273-
prevSlashIndex = slashIndex + 1
1274-
1275-
const possiblePkgId = importer.slice(0, slashIndex)
1276-
possiblePkgIds.push(possiblePkgId)
1277-
}
1278-
1279-
let pkg: PackageData | undefined
1280-
possiblePkgIds.reverse().find((pkgId) => {
1281-
pkg = resolvePackageData(pkgId, root, preserveSymlinks, packageCache)!
1282-
return pkg
1283-
})!
1284-
1285-
if (pkg) {
1264+
const pkgPath = lookupFile(importer, ['package.json'], { pathOnly: true })
1265+
if (pkgPath) {
1266+
const pkg = loadPackageData(pkgPath, preserveSymlinks, packageCache)
12861267
idToPkgMap.set(importer, pkg)
1268+
return pkg
12871269
}
1288-
return pkg
1270+
1271+
return undefined
12891272
}

packages/vite/src/node/ssr/ssrExternal.ts

+14-8
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import {
1111
isDefined,
1212
lookupFile,
1313
normalizePath,
14-
resolveFrom,
1514
} from '../utils'
1615
import type { Logger, ResolvedConfig } from '..'
16+
import { resolvePkgJsonPath } from '../packages'
1717

1818
const debug = createDebugger('vite:ssr-external')
1919

@@ -256,16 +256,16 @@ function cjsSsrCollectExternals(
256256
// which returns with '/', require.resolve returns with '\\'
257257
requireEntry = normalizePath(_require.resolve(id, { paths: [root] }))
258258
} catch (e) {
259-
try {
260-
// no main entry, but deep imports may be allowed
261-
const pkgPath = resolveFrom(`${id}/package.json`, root)
259+
// no main entry, but deep imports may be allowed
260+
const pkgPath = resolvePkgJsonPath(id, root)
261+
if (pkgPath) {
262262
if (pkgPath.includes('node_modules')) {
263263
ssrExternals.add(id)
264264
} else {
265265
depsToTrace.add(path.dirname(pkgPath))
266266
}
267267
continue
268-
} catch {}
268+
}
269269

270270
// resolve failed, assume include
271271
debug(`Failed to resolve entries for package "${id}"\n`, e)
@@ -277,8 +277,10 @@ function cjsSsrCollectExternals(
277277
}
278278
// trace the dependencies of linked packages
279279
else if (!esmEntry.includes('node_modules')) {
280-
const pkgPath = resolveFrom(`${id}/package.json`, root)
281-
depsToTrace.add(path.dirname(pkgPath))
280+
const pkgPath = resolvePkgJsonPath(id, root)
281+
if (pkgPath) {
282+
depsToTrace.add(path.dirname(pkgPath))
283+
}
282284
}
283285
// has separate esm/require entry, assume require entry is cjs
284286
else if (esmEntry !== requireEntry) {
@@ -288,7 +290,11 @@ function cjsSsrCollectExternals(
288290
// or are there others like SystemJS / AMD that we'd need to handle?
289291
// for now, we'll just leave this as is
290292
else if (/\.m?js$/.test(esmEntry)) {
291-
const pkgPath = resolveFrom(`${id}/package.json`, root)
293+
const pkgPath = resolvePkgJsonPath(id, root)
294+
if (!pkgPath) {
295+
continue
296+
}
297+
292298
const pkgContent = fs.readFileSync(pkgPath, 'utf-8')
293299

294300
if (!pkgContent) {

packages/vite/src/node/utils.ts

+8-26
Original file line numberDiff line numberDiff line change
@@ -127,15 +127,9 @@ export function isOptimizable(
127127
export const bareImportRE = /^[\w@](?!.*:\/\/)/
128128
export const deepImportRE = /^([^@][^/]*)\/|^(@[^/]+\/[^/]+)\//
129129

130-
export let isRunningWithYarnPnp: boolean
131-
132130
// TODO: use import()
133131
const _require = createRequire(import.meta.url)
134132

135-
try {
136-
isRunningWithYarnPnp = Boolean(_require('pnpapi'))
137-
} catch {}
138-
139133
const ssrExtensions = ['.js', '.cjs', '.json', '.node']
140134

141135
export function resolveFrom(
@@ -149,29 +143,10 @@ export function resolveFrom(
149143
paths: [],
150144
extensions: ssr ? ssrExtensions : DEFAULT_EXTENSIONS,
151145
// necessary to work with pnpm
152-
preserveSymlinks: preserveSymlinks || isRunningWithYarnPnp || false,
146+
preserveSymlinks: preserveSymlinks || !!process.versions.pnp || false,
153147
})
154148
}
155149

156-
/**
157-
* like `resolveFrom` but supports resolving `>` path in `id`,
158-
* for example: `foo > bar > baz`
159-
*/
160-
export function nestedResolveFrom(
161-
id: string,
162-
basedir: string,
163-
preserveSymlinks = false,
164-
ssr = false,
165-
): string {
166-
const pkgs = id.split('>').map((pkg) => pkg.trim())
167-
try {
168-
for (const pkg of pkgs) {
169-
basedir = resolveFrom(pkg, basedir, preserveSymlinks, ssr)
170-
}
171-
} catch {}
172-
return basedir
173-
}
174-
175150
// set in bin/vite.js
176151
const filter = process.env.VITE_DEBUG_FILTER
177152

@@ -607,6 +582,13 @@ export const removeDir = isWindows
607582
}
608583
export const renameDir = isWindows ? promisify(gracefulRename) : fs.renameSync
609584

585+
// `fs.realpathSync.native` resolves differently in Windows network drive,
586+
// causing file read errors. skip for now.
587+
// https://github.com/nodejs/node/issues/37737
588+
export const safeRealpathSync = isWindows
589+
? fs.realpathSync
590+
: fs.realpathSync.native
591+
610592
export function ensureWatchedFile(
611593
watcher: FSWatcher,
612594
file: string | null,

pnpm-lock.yaml

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)