Skip to content

Commit 165cdef

Browse files
committed
feat(resolve): support "fallback array" in package exports field
Closes #4439 More context: #10504
1 parent 0a69985 commit 165cdef

File tree

3 files changed

+60
-106
lines changed

3 files changed

+60
-106
lines changed

packages/vite/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@
110110
"postcss-import": "^15.0.0",
111111
"postcss-load-config": "^4.0.1",
112112
"postcss-modules": "^5.0.0",
113-
"resolve.exports": "^1.1.0",
113+
"resolve.exports": "npm:@alloc/resolve.exports@^1.1.0",
114114
"sirv": "^2.0.2",
115115
"source-map-js": "^1.0.2",
116116
"source-map-support": "^0.5.21",

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

+52-98
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import fs from 'node:fs'
22
import path from 'node:path'
33
import colors from 'picocolors'
44
import type { PartialResolvedId } from 'rollup'
5-
import { resolve as _resolveExports } from 'resolve.exports'
5+
import { resolveExports } from 'resolve.exports'
66
import { hasESMSyntax } from 'mlly'
77
import type { Plugin } from '../plugin'
88
import {
@@ -923,29 +923,28 @@ export function resolvePackageEntry(
923923
return cached
924924
}
925925
try {
926-
let entryPoint: string | undefined | void
926+
let entryPoints: string[] = []
927927

928-
// resolve exports field with highest priority
929-
// using https://github.com/lukeed/resolve.exports
928+
// the exports field takes highest priority as described in
929+
// https://nodejs.org/api/packages.html#package-entry-points
930930
if (data.exports) {
931-
entryPoint = resolveExports(data, '.', options, targetWeb)
932-
}
933-
934-
// if exports resolved to .mjs, still resolve other fields.
935-
// This is because .mjs files can technically import .cjs files which would
936-
// make them invalid for pure ESM environments - so if other module/browser
937-
// fields are present, prioritize those instead.
938-
if (
939-
targetWeb &&
940-
options.browserField &&
941-
(!entryPoint || entryPoint.endsWith('.mjs'))
942-
) {
931+
entryPoints = resolveExports(
932+
data,
933+
'.',
934+
options,
935+
getInlineConditions(options.conditions, targetWeb)
936+
)
937+
if (!entryPoints.length) {
938+
packageEntryFailure(id)
939+
}
940+
} else if (targetWeb && options.browserField) {
943941
// check browser field
944942
// https://github.com/defunctzombie/package-browser-field-spec
945943
const browserEntry =
946944
typeof data.browser === 'string'
947945
? data.browser
948946
: isObject(data.browser) && data.browser['.']
947+
949948
if (browserEntry) {
950949
// check if the package also has a "module" field.
951950
if (
@@ -968,34 +967,34 @@ export function resolvePackageEntry(
968967
const content = fs.readFileSync(resolvedBrowserEntry, 'utf-8')
969968
if (hasESMSyntax(content)) {
970969
// likely ESM, prefer browser
971-
entryPoint = browserEntry
970+
entryPoints[0] = browserEntry
972971
} else {
973972
// non-ESM, UMD or IIFE or CJS(!!! e.g. firebase 7.x), prefer module
974-
entryPoint = data.module
973+
entryPoints[0] = data.module
975974
}
976975
}
977976
} else {
978-
entryPoint = browserEntry
977+
entryPoints[0] = browserEntry
979978
}
980979
}
981980
}
982981

983-
if (!entryPoint || entryPoint.endsWith('.mjs')) {
982+
if (!entryPoints[0]) {
984983
for (const field of options.mainFields) {
985984
if (field === 'browser') continue // already checked above
986985
if (typeof data[field] === 'string') {
987-
entryPoint = data[field]
986+
entryPoints[0] = data[field]
988987
break
989988
}
990989
}
990+
entryPoints[0] ||= data.main
991991
}
992-
entryPoint ||= data.main
993992

994993
// try default entry when entry is not define
995994
// https://nodejs.org/api/modules.html#all-together
996-
const entryPoints = entryPoint
997-
? [entryPoint]
998-
: ['index.js', 'index.json', 'index.node']
995+
if (!entryPoints[0]) {
996+
entryPoints = ['index.js', 'index.json', 'index.node']
997+
}
999998

1000999
for (let entry of entryPoints) {
10011000
// make sure we don't get scripts when looking for sass
@@ -1040,52 +1039,8 @@ function packageEntryFailure(id: string, details?: string) {
10401039
)
10411040
}
10421041

1043-
const conditionalConditions = new Set(['production', 'development', 'module'])
1044-
1045-
function resolveExports(
1046-
pkg: PackageData['data'],
1047-
key: string,
1048-
options: InternalResolveOptionsWithOverrideConditions,
1049-
targetWeb: boolean
1050-
) {
1051-
const overrideConditions = options.overrideConditions
1052-
? new Set(options.overrideConditions)
1053-
: undefined
1054-
1055-
const conditions = []
1056-
if (
1057-
(!overrideConditions || overrideConditions.has('production')) &&
1058-
options.isProduction
1059-
) {
1060-
conditions.push('production')
1061-
}
1062-
if (
1063-
(!overrideConditions || overrideConditions.has('development')) &&
1064-
!options.isProduction
1065-
) {
1066-
conditions.push('development')
1067-
}
1068-
if (
1069-
(!overrideConditions || overrideConditions.has('module')) &&
1070-
!options.isRequire
1071-
) {
1072-
conditions.push('module')
1073-
}
1074-
if (options.overrideConditions) {
1075-
conditions.push(
1076-
...options.overrideConditions.filter((condition) =>
1077-
conditionalConditions.has(condition)
1078-
)
1079-
)
1080-
} else if (options.conditions.length > 0) {
1081-
conditions.push(...options.conditions)
1082-
}
1083-
1084-
return _resolveExports(pkg, key, {
1085-
browser: targetWeb && !conditions.includes('node'),
1086-
require: options.isRequire && !conditions.includes('import'),
1087-
conditions
1088-
})
1042+
function getInlineConditions(conditions: string[], targetWeb: boolean) {
1043+
return targetWeb && !conditions.includes('node') ? ['browser'] : ['node']
10891044
}
10901045

10911046
function resolveDeepImport(
@@ -1098,56 +1053,55 @@ function resolveDeepImport(
10981053
data
10991054
}: PackageData,
11001055
targetWeb: boolean,
1101-
options: InternalResolveOptions
1056+
options: InternalResolveOptionsWithOverrideConditions
11021057
): string | undefined {
11031058
const cache = getResolvedCache(id, targetWeb)
11041059
if (cache) {
11051060
return cache
11061061
}
11071062

1108-
let relativeId: string | undefined | void = id
11091063
const { exports: exportsField, browser: browserField } = data
1064+
const { file, postfix } = splitFileAndPostfix(id)
11101065

1111-
// map relative based on exports data
1066+
let possibleFiles: string[] | undefined
11121067
if (exportsField) {
1113-
if (isObject(exportsField) && !Array.isArray(exportsField)) {
1114-
// resolve without postfix (see #7098)
1115-
const { file, postfix } = splitFileAndPostfix(relativeId)
1116-
const exportsId = resolveExports(data, file, options, targetWeb)
1117-
if (exportsId !== undefined) {
1118-
relativeId = exportsId + postfix
1119-
} else {
1120-
relativeId = undefined
1121-
}
1122-
} else {
1123-
// not exposed
1124-
relativeId = undefined
1125-
}
1126-
if (!relativeId) {
1068+
// map relative based on exports data
1069+
possibleFiles = resolveExports(
1070+
data,
1071+
file,
1072+
options,
1073+
getInlineConditions(options.conditions, targetWeb),
1074+
options.overrideConditions
1075+
)
1076+
if (!possibleFiles.length) {
11271077
throw new Error(
1128-
`Package subpath '${relativeId}' is not defined by "exports" in ` +
1078+
`Package subpath '${file}' is not defined by "exports" in ` +
11291079
`${path.join(dir, 'package.json')}.`
11301080
)
11311081
}
11321082
} else if (targetWeb && options.browserField && isObject(browserField)) {
1133-
// resolve without postfix (see #7098)
1134-
const { file, postfix } = splitFileAndPostfix(relativeId)
11351083
const mapped = mapWithBrowserField(file, browserField)
11361084
if (mapped) {
1137-
relativeId = mapped + postfix
1085+
possibleFiles = [mapped]
11381086
} else if (mapped === false) {
11391087
return (webResolvedImports[id] = browserExternalId)
11401088
}
11411089
}
11421090

1143-
if (relativeId) {
1144-
const resolved = tryFsResolve(
1145-
path.join(dir, relativeId),
1146-
options,
1147-
!exportsField, // try index only if no exports field
1148-
targetWeb
1091+
possibleFiles ||= [id]
1092+
if (possibleFiles[0]) {
1093+
let resolved: string | undefined
1094+
possibleFiles.some(
1095+
(file) =>
1096+
(resolved = tryFsResolve(
1097+
path.join(dir, file),
1098+
options,
1099+
!exportsField, // try index only if no exports field
1100+
targetWeb
1101+
))
11491102
)
11501103
if (resolved) {
1104+
resolved += postfix
11511105
isDebug &&
11521106
debug(
11531107
`[node/deep-import] ${colors.cyan(id)} -> ${colors.dim(resolved)}`

pnpm-lock.yaml

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

0 commit comments

Comments
 (0)