diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 7c5491694b27db..18c6893478ec88 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -53,7 +53,7 @@ import type { InternalResolveOptionsWithOverrideConditions, ResolveOptions, } from './plugins/resolve' -import { resolvePlugin, tryNodeResolve } from './plugins/resolve' +import { resolvePlugin, tryNodeResolveCore } from './plugins/resolve' import type { LogLevel, Logger } from './logger' import { createLogger } from './logger' import type { DepOptimizationConfig, DepOptimizationOptions } from './optimizer' @@ -1005,12 +1005,18 @@ async function bundleConfigFile( } const isIdESM = isESM || kind === 'dynamic-import' - let idFsPath = tryNodeResolve( + let idFsPath: undefined | string + const resolveResult = tryNodeResolveCore( id, importer, { ...options, isRequire: !isIdESM }, false, - )?.id + ) + if ( + resolveResult.resultType === 'success' || + resolveResult.resultType === 'fail-as-optional-peer-dep' + ) + idFsPath = resolveResult.resolved if (idFsPath && isIdESM) { idFsPath = pathToFileURL(idFsPath).href } diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index 1dbd8bf90b95fc..df0cd1a35b6402 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -6,6 +6,7 @@ import { getDepsOptimizer } from '../optimizer' import { shouldExternalizeForSSR } from '../ssr/ssrExternal' import { jsonPlugin } from './json' import { resolvePlugin } from './resolve' +import type { InternalResolveOptions } from './resolve' import { optimizedDepsBuildPlugin, optimizedDepsPlugin } from './optimizedDeps' import { esbuildPlugin } from './esbuild' import { importAnalysisPlugin } from './importAnalysis' @@ -38,10 +39,25 @@ export async function resolvePlugins( : { pre: [], post: [] } const { modulePreload } = config.build + const resolveOptions: InternalResolveOptions = { + ...config.resolve, + root: config.root, + isProduction: config.isProduction, + isBuild, + packageCache: config.packageCache, + ssrConfig: config.ssr, + asSrc: true, + getDepsOptimizer: (ssr: boolean) => getDepsOptimizer(config, ssr), + shouldExternalize: + isBuild && config.build.ssr && config.ssr?.format !== 'cjs' + ? (id) => shouldExternalizeForSSR(id, config) + : undefined, + } + return [ isWatch ? ensureWatchPlugin() : null, isBuild ? metadataPlugin() : null, - preAliasPlugin(config), + preAliasPlugin(config, resolveOptions), aliasPlugin({ entries: config.resolve.alias }), ...prePlugins, modulePreload === true || @@ -56,20 +72,7 @@ export async function resolvePlugins( : optimizedDepsPlugin(config), ] : []), - resolvePlugin({ - ...config.resolve, - root: config.root, - isProduction: config.isProduction, - isBuild, - packageCache: config.packageCache, - ssrConfig: config.ssr, - asSrc: true, - getDepsOptimizer: (ssr: boolean) => getDepsOptimizer(config, ssr), - shouldExternalize: - isBuild && config.build.ssr && config.ssr?.format !== 'cjs' - ? (id) => shouldExternalizeForSSR(id, config) - : undefined, - }), + resolvePlugin(resolveOptions), htmlInlineProxyPlugin(config), cssPlugin(config), config.esbuild !== false ? esbuildPlugin(config.esbuild) : null, diff --git a/packages/vite/src/node/plugins/preAlias.ts b/packages/vite/src/node/plugins/preAlias.ts index 2b285723d1d80a..a1a06c5c5cbc68 100644 --- a/packages/vite/src/node/plugins/preAlias.ts +++ b/packages/vite/src/node/plugins/preAlias.ts @@ -16,14 +16,20 @@ import { } from '../utils' import { getDepsOptimizer } from '../optimizer' import { tryOptimizedResolve } from './resolve' +import type { InternalResolveOptions } from './resolve' /** * A plugin to avoid an aliased AND optimized dep from being aliased in src */ -export function preAliasPlugin(config: ResolvedConfig): Plugin { +export function preAliasPlugin( + config: ResolvedConfig, + resolveOptions: InternalResolveOptions, +): Plugin { const findPatterns = getAliasPatterns(config.resolve.alias) const isConfiguredAsExternal = createIsConfiguredAsSsrExternal(config) const isBuild = config.command === 'build' + const ssrTarget = resolveOptions.ssrConfig?.target + return { name: 'vite:pre-alias', async resolveId(id, importer, options) { @@ -38,10 +44,13 @@ export function preAliasPlugin(config: ResolvedConfig): Plugin { id !== '@vite/env' ) { if (findPatterns.find((pattern) => matches(pattern, id))) { + const targetWeb = !ssr || ssrTarget === 'webworker' const optimizedId = await tryOptimizedResolve( - depsOptimizer, id, importer, + resolveOptions, + targetWeb, + depsOptimizer, ) if (optimizedId) { return optimizedId // aliased dep already optimized diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index e0df31fc055598..4e3a33451034f5 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -16,6 +16,7 @@ import { SPECIAL_QUERY_RE, } from '../constants' import { + assertUnreachable, bareImportRE, cleanUrl, createDebugger, @@ -35,7 +36,6 @@ import { lookupFile, nestedResolveFrom, normalizePath, - resolveFrom, slash, } from '../utils' import { optimizedDepInfoFromFile, optimizedDepInfoFromId } from '../optimizer' @@ -309,7 +309,13 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { asSrc && depsOptimizer && !options.scan && - (res = await tryOptimizedResolve(depsOptimizer, id, importer)) + (res = await tryOptimizedResolve( + id, + importer, + options, + targetWeb, + depsOptimizer, + )) ) { return res } @@ -599,26 +605,27 @@ export type InternalResolveOptionsWithOverrideConditions = export const idToPkgMap = new Map() -export function tryNodeResolve( +export type TryNodeResolveCoreResult = + | { + resultType: 'success' + resolved: string + pkg: PackageData + pkgId: string + nearestPkg: PackageData + isDeepImport: boolean + } + | { resultType: 'fail-as-optional-peer-dep'; resolved: string } + | { resultType: 'fail' } + +export function tryNodeResolveCore( id: string, importer: string | null | undefined, options: InternalResolveOptionsWithOverrideConditions, targetWeb: boolean, - depsOptimizer?: DepsOptimizer, - ssr?: boolean, - externalize?: boolean, - allowLinkedExternal: boolean = true, -): PartialResolvedId | undefined { - const { root, dedupe, isBuild, preserveSymlinks, packageCache } = options - - ssr ??= false +): TryNodeResolveCoreResult { + const { root, dedupe, preserveSymlinks, packageCache } = options - // split id by last '>' for nested selected packages, for example: - // 'foo > bar > baz' => 'foo > bar' & 'baz' - // 'foo' => '' & 'foo' - const lastArrowIndex = id.lastIndexOf('>') - const nestedRoot = id.substring(0, lastArrowIndex).trim() - const nestedPath = id.substring(lastArrowIndex + 1).trim() + const { nestedRoot, nestedPath } = parseNestedId(id) const possiblePkgIds: string[] = [] for (let prevSlashIndex = -1; ; ) { @@ -717,12 +724,13 @@ export function tryNodeResolve( mainPkg.peerDependenciesMeta?.[nestedPath]?.optional ) { return { - id: `${optionalPeerDepId}:${nestedPath}:${mainPkg.name}`, + resultType: 'fail-as-optional-peer-dep', + resolved: `${optionalPeerDepId}:${nestedPath}:${mainPkg.name}`, } } } } - return + return { resultType: 'fail' } } let resolveId = resolvePackageEntry @@ -750,8 +758,45 @@ export function tryNodeResolve( }) } if (!resolved) { - return + return { resultType: 'fail' } + } + + // link id to pkg for browser field mapping check + idToPkgMap.set(resolved, pkg) + + return { + resultType: 'success', + resolved, + pkg, + pkgId, + nearestPkg, + isDeepImport, } +} + +export function tryNodeResolve( + id: string, + importer: string | null | undefined, + options: InternalResolveOptionsWithOverrideConditions, + targetWeb: boolean, + depsOptimizer?: DepsOptimizer, + ssr?: boolean, + externalize?: boolean, + allowLinkedExternal: boolean = true, +): PartialResolvedId | undefined { + const coreResult = tryNodeResolveCore(id, importer, options, targetWeb) + if (coreResult.resultType === 'fail') return + if (coreResult.resultType === 'fail-as-optional-peer-dep') + return { + id: coreResult.resolved, + } + if (coreResult.resultType !== 'success') return assertUnreachable(coreResult) + + const { pkg, pkgId, nearestPkg, isDeepImport } = coreResult + let { resolved } = coreResult + const { isBuild } = options + ssr ??= false + const { nestedPath } = parseNestedId(id) const processResult = (resolved: PartialResolvedId) => { if (!externalize) { @@ -784,8 +829,6 @@ export function tryNodeResolve( return { ...resolved, id: resolvedId, external: true } } - // link id to pkg for browser field mapping check - idToPkgMap.set(resolved, pkg) if ((isBuild && !depsOptimizer) || externalize) { // Resolve package side effects for build so that rollup can better // perform tree-shaking @@ -875,10 +918,24 @@ export function tryNodeResolve( } } +/** + * split id by last '>' for nested selected packages, for example: + * 'foo > bar > baz' => 'foo > bar' & 'baz' + * 'foo' => '' & 'foo' + */ +function parseNestedId(id: string) { + const lastArrowIndex = id.lastIndexOf('>') + const nestedRoot = id.substring(0, lastArrowIndex).trim() + const nestedPath = id.substring(lastArrowIndex + 1).trim() + return { nestedRoot, nestedPath } +} + export async function tryOptimizedResolve( - depsOptimizer: DepsOptimizer, id: string, - importer?: string, + importer: string | null | undefined, + resolveOptions: InternalResolveOptions, + targetWeb: boolean, + depsOptimizer: DepsOptimizer, ): Promise { // TODO: we need to wait until scanning is done here as this function // is used in the preAliasPlugin to decide if an aliased dep is optimized, @@ -888,13 +945,15 @@ export async function tryOptimizedResolve( const metadata = depsOptimizer.metadata - const depInfo = optimizedDepInfoFromId(metadata, id) - if (depInfo) { - return depsOptimizer.getOptimizedDepId(depInfo) + if (!importer) { + // no importer. try our best to find an optimized dep + const depInfo = optimizedDepInfoFromId(metadata, id) + if (depInfo) { + return depsOptimizer.getOptimizedDepId(depInfo) + } + return } - if (!importer) return - // further check if id is imported by nested dependency let resolvedSrc: string | undefined @@ -911,8 +970,18 @@ export async function tryOptimizedResolve( // lazily initialize resolvedSrc if (resolvedSrc == null) { try { - // this may throw errors if unable to resolve, e.g. aliased id - resolvedSrc = normalizePath(resolveFrom(id, path.dirname(importer))) + const resolveResult = tryNodeResolveCore( + id, + importer, + resolveOptions, + targetWeb, + ) + if (resolveResult.resultType !== 'success') { + // no resolvedSrc, no need to continue + break + } else { + resolvedSrc = normalizePath(resolveResult.resolved) + } } catch { // this is best-effort only so swallow errors break diff --git a/packages/vite/src/node/ssr/ssrModuleLoader.ts b/packages/vite/src/node/ssr/ssrModuleLoader.ts index 4cb226de83944e..e6a91481e88dde 100644 --- a/packages/vite/src/node/ssr/ssrModuleLoader.ts +++ b/packages/vite/src/node/ssr/ssrModuleLoader.ts @@ -2,6 +2,7 @@ import path from 'node:path' import { pathToFileURL } from 'node:url' import type { ViteDevServer } from '../server' import { + assertUnreachable, dynamicImport, isBuiltin, unwrapId, @@ -9,7 +10,7 @@ import { } from '../utils' import { transformRequest } from '../server/transformRequest' import type { InternalResolveOptionsWithOverrideConditions } from '../plugins/resolve' -import { tryNodeResolve } from '../plugins/resolve' +import { tryNodeResolveCore } from '../plugins/resolve' import { ssrDynamicImportKey, ssrExportAllKey, @@ -230,7 +231,7 @@ async function nodeImport( if (id.startsWith('node:') || isBuiltin(id)) { url = id } else { - const resolved = tryNodeResolve( + const resolveResult = tryNodeResolveCore( id, importer, // Non-external modules can import ESM-only modules, but only outside @@ -241,14 +242,19 @@ async function nodeImport( : resolveOptions, false, ) - if (!resolved) { + if ( + resolveResult.resultType === 'success' || + resolveResult.resultType === 'fail-as-optional-peer-dep' + ) { + url = resolveResult.resolved + } else { + if (resolveResult.resultType !== 'fail') assertUnreachable(resolveResult) const err: any = new Error( `Cannot find module '${id}' imported from '${importer}'`, ) err.code = 'ERR_MODULE_NOT_FOUND' throw err } - url = resolved.id if (usingDynamicImport) { url = pathToFileURL(url).toString() } diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 13ef1c8c91326e..537ee7ab981c0b 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -1263,3 +1263,11 @@ export function evalValue(rawValue: string): T { `) return fn() } + +/** + * let typescript do exhaustive check to ensure we have handled all cases + * https://stackoverflow.com/a/39419171 + */ +export function assertUnreachable(x: never): never { + throw new Error("Didn't expect to get here") +} diff --git a/playground/resolve-optimized-dup-deps/__tests__/resolve-optimized-dup-deps.spec.ts b/playground/resolve-optimized-dup-deps/__tests__/resolve-optimized-dup-deps.spec.ts new file mode 100644 index 00000000000000..e2c1507ecdb2ff --- /dev/null +++ b/playground/resolve-optimized-dup-deps/__tests__/resolve-optimized-dup-deps.spec.ts @@ -0,0 +1,7 @@ +import { expect, test } from 'vitest' +import { page } from '~utils' + +test('resolve-optimized-dup-deps', async () => { + expect(await page.textContent('.a')).toBe('test-package-a:test-package-b-v2') + expect(await page.textContent('.b')).toBe('test-package-b-v1') +}) diff --git a/playground/resolve-optimized-dup-deps/index.html b/playground/resolve-optimized-dup-deps/index.html new file mode 100644 index 00000000000000..89b7bca36300e7 --- /dev/null +++ b/playground/resolve-optimized-dup-deps/index.html @@ -0,0 +1,17 @@ +

direct dependency A

+

+
+

direct dependency B

+

+
+
diff --git a/playground/resolve-optimized-dup-deps/package-a/index.js b/playground/resolve-optimized-dup-deps/package-a/index.js
new file mode 100644
index 00000000000000..52c82187d5ee47
--- /dev/null
+++ b/playground/resolve-optimized-dup-deps/package-a/index.js
@@ -0,0 +1,6 @@
+import b from '@vitejs/test-resolve-optimized-dup-deps-package-b'
+
+// should get test-package-a:test-package-b-v2
+const result = 'test-package-a:' + b
+
+export default result
diff --git a/playground/resolve-optimized-dup-deps/package-a/package.json b/playground/resolve-optimized-dup-deps/package-a/package.json
new file mode 100644
index 00000000000000..a2c0067d1573cb
--- /dev/null
+++ b/playground/resolve-optimized-dup-deps/package-a/package.json
@@ -0,0 +1,9 @@
+{
+  "name": "@vitejs/test-resolve-optimized-dup-deps-package-a",
+  "private": true,
+  "version": "1.0.0",
+  "main": "index.js",
+  "dependencies": {
+    "@vitejs/test-resolve-optimized-dup-deps-package-b": "file:../package-b-v2"
+  }
+}
diff --git a/playground/resolve-optimized-dup-deps/package-b-v1/index.js b/playground/resolve-optimized-dup-deps/package-b-v1/index.js
new file mode 100644
index 00000000000000..7d6c1ef0a992f1
--- /dev/null
+++ b/playground/resolve-optimized-dup-deps/package-b-v1/index.js
@@ -0,0 +1,3 @@
+// test-package-b-v1 is install and imported by user
+// it is written in cjs and should be optimized
+module.exports = 'test-package-b-v1'
diff --git a/playground/resolve-optimized-dup-deps/package-b-v1/package.json b/playground/resolve-optimized-dup-deps/package-b-v1/package.json
new file mode 100644
index 00000000000000..68b1c74f57f8e0
--- /dev/null
+++ b/playground/resolve-optimized-dup-deps/package-b-v1/package.json
@@ -0,0 +1,6 @@
+{
+  "name": "@vitejs/test-resolve-optimized-dup-deps-package-b",
+  "private": true,
+  "version": "1.0.0",
+  "main": "index.js"
+}
diff --git a/playground/resolve-optimized-dup-deps/package-b-v2/index.js b/playground/resolve-optimized-dup-deps/package-b-v2/index.js
new file mode 100644
index 00000000000000..204404e745db09
--- /dev/null
+++ b/playground/resolve-optimized-dup-deps/package-b-v2/index.js
@@ -0,0 +1,3 @@
+// test-package-b-v2 is install and imported by test-package-a
+// it is written in esm. it is not optimized
+export default 'test-package-b-v2'
diff --git a/playground/resolve-optimized-dup-deps/package-b-v2/package.json b/playground/resolve-optimized-dup-deps/package-b-v2/package.json
new file mode 100644
index 00000000000000..ba9c84ea4538d6
--- /dev/null
+++ b/playground/resolve-optimized-dup-deps/package-b-v2/package.json
@@ -0,0 +1,6 @@
+{
+  "name": "@vitejs/test-resolve-optimized-dup-deps-package-b",
+  "private": true,
+  "version": "2.0.0",
+  "main": "index.js"
+}
diff --git a/playground/resolve-optimized-dup-deps/package.json b/playground/resolve-optimized-dup-deps/package.json
new file mode 100644
index 00000000000000..71267db3985ddd
--- /dev/null
+++ b/playground/resolve-optimized-dup-deps/package.json
@@ -0,0 +1,15 @@
+{
+  "name": "@vitejs/test-resolve-optimized-dup-deps",
+  "private": true,
+  "version": "0.0.0",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@vitejs/test-resolve-optimized-dup-deps-package-a": "file:./package-a",
+    "@vitejs/test-resolve-optimized-dup-deps-package-b": "file:./package-b-v1"
+  }
+}
diff --git a/playground/resolve-optimized-dup-deps/vite.config.js b/playground/resolve-optimized-dup-deps/vite.config.js
new file mode 100644
index 00000000000000..2d6290f12b9098
--- /dev/null
+++ b/playground/resolve-optimized-dup-deps/vite.config.js
@@ -0,0 +1,8 @@
+/**
+ * @type {import('vite').UserConfig}
+ */
+module.exports = {
+  optimizeDeps: {
+    exclude: ['@vitejs/test-resolve-optimized-dup-deps-package-a'],
+  },
+}
diff --git a/playground/resolve-optimized/__tests__/resolve-optimized.spec.ts b/playground/resolve-optimized/__tests__/resolve-optimized.spec.ts
new file mode 100644
index 00000000000000..628f91f3a9b802
--- /dev/null
+++ b/playground/resolve-optimized/__tests__/resolve-optimized.spec.ts
@@ -0,0 +1,19 @@
+import { expect, test } from 'vitest'
+import { page } from '~utils'
+
+test('resolve-optimized', async () => {
+  expect(await page.textContent('.a')).toBe('package-a-module')
+  expect(await page.textContent('.b')).toBe(
+    'package-b-module:package-a-module:package-custom-main-field:package-custom-condition',
+  )
+  expect(await page.textContent('.custom-main-field')).toBe(
+    'package-custom-main-field',
+  )
+  expect(await page.textContent('.custom-condition')).toBe(
+    'package-custom-condition',
+  )
+
+  expect(await page.textContent('.a-count')).toBe('success')
+  expect(await page.textContent('.custom-main-field-count')).toBe('success')
+  expect(await page.textContent('.custom-condition-count')).toBe('success')
+})
diff --git a/playground/resolve-optimized/index.html b/playground/resolve-optimized/index.html
new file mode 100644
index 00000000000000..dd4544ca828f9b
--- /dev/null
+++ b/playground/resolve-optimized/index.html
@@ -0,0 +1,46 @@
+

direct dependency A

+

+

+
+

direct dependency B

+

+
+

direct dependency custom-main-field

+

+

+
+

direct dependency custom-condition

+

+

+
+
diff --git a/playground/resolve-optimized/package-a/index.main.js b/playground/resolve-optimized/package-a/index.main.js
new file mode 100644
index 00000000000000..064a081489e136
--- /dev/null
+++ b/playground/resolve-optimized/package-a/index.main.js
@@ -0,0 +1,3 @@
+export default 'package-a-main'
+
+throw new Error('should resolve to package-a/index.module.js instead of this')
diff --git a/playground/resolve-optimized/package-a/index.module.js b/playground/resolve-optimized/package-a/index.module.js
new file mode 100644
index 00000000000000..53c7756645146c
--- /dev/null
+++ b/playground/resolve-optimized/package-a/index.module.js
@@ -0,0 +1,6 @@
+const key = 'package-a-module'
+
+export default key
+
+globalThis[key] ||= 0
+globalThis[key]++
diff --git a/playground/resolve-optimized/package-a/package.json b/playground/resolve-optimized/package-a/package.json
new file mode 100644
index 00000000000000..acb5911de21a08
--- /dev/null
+++ b/playground/resolve-optimized/package-a/package.json
@@ -0,0 +1,8 @@
+{
+  "name": "@vitejs/test-resolve-optimized-package-a",
+  "private": true,
+  "version": "1.0.0",
+  "main": "index.main.js",
+  "module": "index.module.js",
+  "dependencies": {}
+}
diff --git a/playground/resolve-optimized/package-b/index.module.js b/playground/resolve-optimized/package-b/index.module.js
new file mode 100644
index 00000000000000..64bbc86a2bb605
--- /dev/null
+++ b/playground/resolve-optimized/package-b/index.module.js
@@ -0,0 +1,5 @@
+import a from '@vitejs/test-resolve-optimized-package-a'
+import customMain from '@vitejs/test-resolve-optimized-package-custom-main-field'
+import customCondition from '@vitejs/test-resolve-optimized-package-custom-condition'
+
+export default `package-b-module:${a}:${customMain}:${customCondition}`
diff --git a/playground/resolve-optimized/package-b/package.json b/playground/resolve-optimized/package-b/package.json
new file mode 100644
index 00000000000000..1636a87a1b9fa6
--- /dev/null
+++ b/playground/resolve-optimized/package-b/package.json
@@ -0,0 +1,12 @@
+{
+  "name": "@vitejs/test-resolve-optimized-package-b",
+  "private": true,
+  "version": "1.0.0",
+  "module": "index.module.js",
+  "dependencies": {},
+  "peerDependencies": {
+    "@vitejs/test-resolve-optimized-package-a": "*",
+    "@vitejs/test-resolve-optimized-package-custom-condition": "*",
+    "@vitejs/test-resolve-optimized-package-custom-main-field": "*"
+  }
+}
diff --git a/playground/resolve-optimized/package-custom-condition/index.custom.js b/playground/resolve-optimized/package-custom-condition/index.custom.js
new file mode 100644
index 00000000000000..4e21f0120f24fe
--- /dev/null
+++ b/playground/resolve-optimized/package-custom-condition/index.custom.js
@@ -0,0 +1,6 @@
+const key = 'package-custom-condition'
+
+export default key
+
+globalThis[key] ||= 0
+globalThis[key]++
diff --git a/playground/resolve-optimized/package-custom-condition/index.js b/playground/resolve-optimized/package-custom-condition/index.js
new file mode 100644
index 00000000000000..2acbd887fce5ff
--- /dev/null
+++ b/playground/resolve-optimized/package-custom-condition/index.js
@@ -0,0 +1,3 @@
+export default '[fail]'
+
+throw new Error('should resolve to custom condition instead of this')
diff --git a/playground/resolve-optimized/package-custom-condition/package.json b/playground/resolve-optimized/package-custom-condition/package.json
new file mode 100644
index 00000000000000..5bb2f63cdb5be6
--- /dev/null
+++ b/playground/resolve-optimized/package-custom-condition/package.json
@@ -0,0 +1,13 @@
+{
+  "name": "@vitejs/test-resolve-optimized-package-custom-condition",
+  "private": true,
+  "version": "1.0.0",
+  "main": "index.js",
+  "exports": {
+    ".": {
+      "custom": "./index.custom.js",
+      "import": "./index.js",
+      "require": "./index.js"
+    }
+  }
+}
diff --git a/playground/resolve-optimized/package-custom-main-field/index.custom.js b/playground/resolve-optimized/package-custom-main-field/index.custom.js
new file mode 100644
index 00000000000000..2ea5db9ba8aae3
--- /dev/null
+++ b/playground/resolve-optimized/package-custom-main-field/index.custom.js
@@ -0,0 +1,6 @@
+const key = 'package-custom-main-field'
+
+export default key
+
+globalThis[key] ||= 0
+globalThis[key]++
diff --git a/playground/resolve-optimized/package-custom-main-field/index.js b/playground/resolve-optimized/package-custom-main-field/index.js
new file mode 100644
index 00000000000000..4dd83909cacc48
--- /dev/null
+++ b/playground/resolve-optimized/package-custom-main-field/index.js
@@ -0,0 +1,3 @@
+export default '[fail]'
+
+throw new Error('should resolve to custom main field instead of this')
diff --git a/playground/resolve-optimized/package-custom-main-field/package.json b/playground/resolve-optimized/package-custom-main-field/package.json
new file mode 100644
index 00000000000000..d0d973ccfaf5b1
--- /dev/null
+++ b/playground/resolve-optimized/package-custom-main-field/package.json
@@ -0,0 +1,7 @@
+{
+  "name": "@vitejs/test-resolve-optimized-package-custom-main-field",
+  "private": true,
+  "version": "1.0.0",
+  "main": "index.js",
+  "custom": "index.custom.js"
+}
diff --git a/playground/resolve-optimized/package.json b/playground/resolve-optimized/package.json
new file mode 100644
index 00000000000000..6494761b54c4d7
--- /dev/null
+++ b/playground/resolve-optimized/package.json
@@ -0,0 +1,17 @@
+{
+  "name": "@vitejs/test-resolve-optimized-dup-deps",
+  "private": true,
+  "version": "0.0.0",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@vitejs/test-resolve-optimized-package-a": "file:./package-a",
+    "@vitejs/test-resolve-optimized-package-b": "file:./package-b",
+    "@vitejs/test-resolve-optimized-package-custom-condition": "file:./package-custom-condition",
+    "@vitejs/test-resolve-optimized-package-custom-main-field": "file:./package-custom-main-field"
+  }
+}
diff --git a/playground/resolve-optimized/vite.config.js b/playground/resolve-optimized/vite.config.js
new file mode 100644
index 00000000000000..93581bb0844b19
--- /dev/null
+++ b/playground/resolve-optimized/vite.config.js
@@ -0,0 +1,13 @@
+/**
+ * @type {import('vite').UserConfig}
+ */
+module.exports = {
+  optimizeDeps: {
+    exclude: ['@vitejs/test-resolve-optimized-package-b'],
+  },
+  resolve: {
+    // test if tryOptimizedResolve respect resolve options
+    mainFields: ['custom', 'module'],
+    conditions: ['custom'],
+  },
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6490355ece55fd..643d456254dd77 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -827,6 +827,50 @@ importers:
   playground/resolve-linked:
     specifiers: {}
 
+  playground/resolve-optimized:
+    specifiers:
+      '@vitejs/test-resolve-optimized-package-a': file:./package-a
+      '@vitejs/test-resolve-optimized-package-b': file:./package-b
+      '@vitejs/test-resolve-optimized-package-custom-condition': file:./package-custom-condition
+      '@vitejs/test-resolve-optimized-package-custom-main-field': file:./package-custom-main-field
+    dependencies:
+      '@vitejs/test-resolve-optimized-package-a': file:playground/resolve-optimized/package-a
+      '@vitejs/test-resolve-optimized-package-b': file:playground/resolve-optimized/package-b_j3czbr56p6y2ktoozljpzmhxu4
+      '@vitejs/test-resolve-optimized-package-custom-condition': file:playground/resolve-optimized/package-custom-condition
+      '@vitejs/test-resolve-optimized-package-custom-main-field': file:playground/resolve-optimized/package-custom-main-field
+
+  playground/resolve-optimized-dup-deps:
+    specifiers:
+      '@vitejs/test-resolve-optimized-dup-deps-package-a': file:./package-a
+      '@vitejs/test-resolve-optimized-dup-deps-package-b': file:./package-b-v1
+    dependencies:
+      '@vitejs/test-resolve-optimized-dup-deps-package-a': file:playground/resolve-optimized-dup-deps/package-a
+      '@vitejs/test-resolve-optimized-dup-deps-package-b': file:playground/resolve-optimized-dup-deps/package-b-v1
+
+  playground/resolve-optimized-dup-deps/package-a:
+    specifiers:
+      '@vitejs/test-resolve-optimized-dup-deps-package-b': file:../package-b-v2
+    dependencies:
+      '@vitejs/test-resolve-optimized-dup-deps-package-b': file:playground/resolve-optimized-dup-deps/package-b-v2
+
+  playground/resolve-optimized-dup-deps/package-b-v1:
+    specifiers: {}
+
+  playground/resolve-optimized-dup-deps/package-b-v2:
+    specifiers: {}
+
+  playground/resolve-optimized/package-a:
+    specifiers: {}
+
+  playground/resolve-optimized/package-b:
+    specifiers: {}
+
+  playground/resolve-optimized/package-custom-condition:
+    specifiers: {}
+
+  playground/resolve-optimized/package-custom-main-field:
+    specifiers: {}
+
   playground/resolve/browser-field:
     specifiers: {}
 
@@ -9015,6 +9059,59 @@ packages:
       dep-a: file:playground/preload/dep-a
     dev: true
 
+  file:playground/resolve-optimized-dup-deps/package-a:
+    resolution: {directory: playground/resolve-optimized-dup-deps/package-a, type: directory}
+    name: '@vitejs/test-resolve-optimized-dup-deps-package-a'
+    version: 1.0.0
+    dependencies:
+      '@vitejs/test-resolve-optimized-dup-deps-package-b': file:playground/resolve-optimized-dup-deps/package-b-v2
+    dev: false
+
+  file:playground/resolve-optimized-dup-deps/package-b-v1:
+    resolution: {directory: playground/resolve-optimized-dup-deps/package-b-v1, type: directory}
+    name: '@vitejs/test-resolve-optimized-dup-deps-package-b'
+    version: 1.0.0
+    dev: false
+
+  file:playground/resolve-optimized-dup-deps/package-b-v2:
+    resolution: {directory: playground/resolve-optimized-dup-deps/package-b-v2, type: directory}
+    name: '@vitejs/test-resolve-optimized-dup-deps-package-b'
+    version: 2.0.0
+    dev: false
+
+  file:playground/resolve-optimized/package-a:
+    resolution: {directory: playground/resolve-optimized/package-a, type: directory}
+    name: '@vitejs/test-resolve-optimized-package-a'
+    version: 1.0.0
+    dev: false
+
+  file:playground/resolve-optimized/package-b_j3czbr56p6y2ktoozljpzmhxu4:
+    resolution: {directory: playground/resolve-optimized/package-b, type: directory}
+    id: file:playground/resolve-optimized/package-b
+    name: '@vitejs/test-resolve-optimized-package-b'
+    version: 1.0.0
+    peerDependencies:
+      '@vitejs/test-resolve-optimized-package-a': '*'
+      '@vitejs/test-resolve-optimized-package-custom-condition': '*'
+      '@vitejs/test-resolve-optimized-package-custom-main-field': '*'
+    dependencies:
+      '@vitejs/test-resolve-optimized-package-a': file:playground/resolve-optimized/package-a
+      '@vitejs/test-resolve-optimized-package-custom-condition': file:playground/resolve-optimized/package-custom-condition
+      '@vitejs/test-resolve-optimized-package-custom-main-field': file:playground/resolve-optimized/package-custom-main-field
+    dev: false
+
+  file:playground/resolve-optimized/package-custom-condition:
+    resolution: {directory: playground/resolve-optimized/package-custom-condition, type: directory}
+    name: '@vitejs/test-resolve-optimized-package-custom-condition'
+    version: 1.0.0
+    dev: false
+
+  file:playground/resolve-optimized/package-custom-main-field:
+    resolution: {directory: playground/resolve-optimized/package-custom-main-field, type: directory}
+    name: '@vitejs/test-resolve-optimized-package-custom-main-field'
+    version: 1.0.0
+    dev: false
+
   file:playground/ssr-deps/css-lib:
     resolution: {directory: playground/ssr-deps/css-lib, type: directory}
     name: '@vitejs/test-css-lib'