Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(resolve): improve package.json resolve speed #12441

Merged
merged 7 commits into from Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to refactor this function, since it's incorrectly using resolvePackageData and looks unnecessarily complex 🤔 What happens is that this function gets the nearest package.json, and loads the PackageData. And the new changes does the same too.

In the old changes, it manually walks /User/foo/project, create a possiblePkgIds of ['/User/foo/project', '/User/foo', '/User'], and then try to resolve the package.json in those directories. So resolvePackageData would receive absolute paths instead of bare imports which doesn't work with this refactor.

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.