Skip to content

Commit

Permalink
perf(resolve): improve package.json resolve speed (#12441)
Browse files Browse the repository at this point in the history
  • Loading branch information
bluwy committed Mar 21, 2023
1 parent 8d2931b commit 1fc8c65
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 71 deletions.
1 change: 1 addition & 0 deletions packages/vite/package.json
Expand Up @@ -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",
Expand Down
20 changes: 17 additions & 3 deletions packages/vite/src/node/optimizer/index.ts
Expand Up @@ -18,7 +18,6 @@ import {
getHash,
isOptimizable,
lookupFile,
nestedResolveFrom,
normalizeId,
normalizePath,
removeDir,
Expand All @@ -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 {
Expand Down Expand Up @@ -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) => {
Expand Down
50 changes: 41 additions & 9 deletions 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,
Expand Down Expand Up @@ -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)
Expand All @@ -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(
Expand All @@ -86,7 +91,7 @@ export function loadPackageData(
packageCache?: PackageCache,
): PackageData {
if (!preserveSymlinks) {
pkgPath = fs.realpathSync.native(pkgPath)
pkgPath = safeRealpathSync(pkgPath)
}

let cached: PackageData | undefined
Expand Down Expand Up @@ -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
}
33 changes: 8 additions & 25 deletions packages/vite/src/node/plugins/resolve.ts
Expand Up @@ -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
}
22 changes: 14 additions & 8 deletions packages/vite/src/node/ssr/ssrExternal.ts
Expand Up @@ -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')

Expand Down Expand Up @@ -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)
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down
34 changes: 8 additions & 26 deletions packages/vite/src/node/utils.ts
Expand Up @@ -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(
Expand All @@ -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

Expand Down Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 1fc8c65

Please sign in to comment.