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

feat(resolve): support "fallback array" in package exports field #10504

Closed
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion packages/vite/package.json
Expand Up @@ -110,7 +110,7 @@
"postcss-import": "^15.0.0",
"postcss-load-config": "^4.0.1",
"postcss-modules": "^5.0.0",
"resolve.exports": "^1.1.0",
"resolve.exports": "npm:@alloc/resolve.exports@^1.1.0",
"sirv": "^2.0.2",
"source-map-js": "^1.0.2",
"source-map-support": "^0.5.21",
Expand Down
150 changes: 52 additions & 98 deletions packages/vite/src/node/plugins/resolve.ts
Expand Up @@ -2,7 +2,7 @@ import fs from 'node:fs'
import path from 'node:path'
import colors from 'picocolors'
import type { PartialResolvedId } from 'rollup'
import { resolve as _resolveExports } from 'resolve.exports'
import { resolveExports } from 'resolve.exports'
import { hasESMSyntax } from 'mlly'
import type { Plugin } from '../plugin'
import {
Expand Down Expand Up @@ -923,29 +923,28 @@ export function resolvePackageEntry(
return cached
}
try {
let entryPoint: string | undefined | void
let entryPoints: string[] = []
Copy link
Member Author

Choose a reason for hiding this comment

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

The new resolveExports returns an array of possible module paths. It prefers to return an empty array, rather than throw an error. Vite should only throw if none of the paths in the array are found.


// resolve exports field with highest priority
// using https://github.com/lukeed/resolve.exports
// the exports field takes highest priority as described in
// https://nodejs.org/api/packages.html#package-entry-points
if (data.exports) {
entryPoint = resolveExports(data, '.', options, targetWeb)
}

// if exports resolved to .mjs, still resolve other fields.
// This is because .mjs files can technically import .cjs files which would
// make them invalid for pure ESM environments - so if other module/browser
// fields are present, prioritize those instead.
if (
targetWeb &&
options.browserField &&
(!entryPoint || entryPoint.endsWith('.mjs'))
Copy link
Member Author

Choose a reason for hiding this comment

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

No need for .mjs check anymore, since exports resolution is now strict.

) {
entryPoints = resolveExports(
data,
'.',
options,
getInlineConditions(options.conditions, targetWeb)
Copy link
Member Author

Choose a reason for hiding this comment

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

Conditions that can't be inferred from Vite's resolve options are generated with getInlineConditions and passed as their own argument.

)
if (!entryPoints.length) {
packageEntryFailure(id)
Copy link
Member Author

Choose a reason for hiding this comment

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

This comes from #8484, and it makes resolution more strict when an exports field is found. If the imported module isn't specified in exports map, avoid checking other fields (e.g. main or browser).

}
} else if (targetWeb && options.browserField) {
// check browser field
// https://github.com/defunctzombie/package-browser-field-spec
const browserEntry =
typeof data.browser === 'string'
? data.browser
: isObject(data.browser) && data.browser['.']

if (browserEntry) {
// check if the package also has a "module" field.
if (
Expand All @@ -968,34 +967,34 @@ export function resolvePackageEntry(
const content = fs.readFileSync(resolvedBrowserEntry, 'utf-8')
if (hasESMSyntax(content)) {
// likely ESM, prefer browser
entryPoint = browserEntry
entryPoints[0] = browserEntry
} else {
// non-ESM, UMD or IIFE or CJS(!!! e.g. firebase 7.x), prefer module
entryPoint = data.module
entryPoints[0] = data.module
}
}
} else {
entryPoint = browserEntry
entryPoints[0] = browserEntry
}
}
}

if (!entryPoint || entryPoint.endsWith('.mjs')) {
if (!entryPoints[0]) {
for (const field of options.mainFields) {
if (field === 'browser') continue // already checked above
if (typeof data[field] === 'string') {
entryPoint = data[field]
entryPoints[0] = data[field]
break
}
}
entryPoints[0] ||= data.main
}
entryPoint ||= data.main

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

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

const conditionalConditions = new Set(['production', 'development', 'module'])

function resolveExports(
pkg: PackageData['data'],
key: string,
options: InternalResolveOptionsWithOverrideConditions,
targetWeb: boolean
) {
const overrideConditions = options.overrideConditions
? new Set(options.overrideConditions)
: undefined

const conditions = []
if (
(!overrideConditions || overrideConditions.has('production')) &&
options.isProduction
) {
conditions.push('production')
}
if (
(!overrideConditions || overrideConditions.has('development')) &&
!options.isProduction
) {
conditions.push('development')
}
if (
(!overrideConditions || overrideConditions.has('module')) &&
!options.isRequire
) {
conditions.push('module')
}
if (options.overrideConditions) {
conditions.push(
...options.overrideConditions.filter((condition) =>
conditionalConditions.has(condition)
)
)
} else if (options.conditions.length > 0) {
conditions.push(...options.conditions)
}

return _resolveExports(pkg, key, {
browser: targetWeb && !conditions.includes('node'),
require: options.isRequire && !conditions.includes('import'),
conditions
})
function getInlineConditions(conditions: string[], targetWeb: boolean) {
return targetWeb && !conditions.includes('node') ? ['browser'] : ['node']
}

function resolveDeepImport(
Expand All @@ -1098,56 +1053,55 @@ function resolveDeepImport(
data
}: PackageData,
targetWeb: boolean,
options: InternalResolveOptions
options: InternalResolveOptionsWithOverrideConditions
): string | undefined {
const cache = getResolvedCache(id, targetWeb)
if (cache) {
return cache
}

let relativeId: string | undefined | void = id
const { exports: exportsField, browser: browserField } = data
const { file, postfix } = splitFileAndPostfix(id)

// map relative based on exports data
let possibleFiles: string[] | undefined
if (exportsField) {
if (isObject(exportsField) && !Array.isArray(exportsField)) {
// resolve without postfix (see #7098)
const { file, postfix } = splitFileAndPostfix(relativeId)
const exportsId = resolveExports(data, file, options, targetWeb)
if (exportsId !== undefined) {
relativeId = exportsId + postfix
} else {
relativeId = undefined
}
} else {
// not exposed
relativeId = undefined
}
if (!relativeId) {
// map relative based on exports data
possibleFiles = resolveExports(
data,
file,
options,
getInlineConditions(options.conditions, targetWeb),
options.overrideConditions
)
if (!possibleFiles.length) {
throw new Error(
`Package subpath '${relativeId}' is not defined by "exports" in ` +
`Package subpath '${file}' is not defined by "exports" in ` +
`${path.join(dir, 'package.json')}.`
)
}
} else if (targetWeb && options.browserField && isObject(browserField)) {
// resolve without postfix (see #7098)
const { file, postfix } = splitFileAndPostfix(relativeId)
const mapped = mapWithBrowserField(file, browserField)
if (mapped) {
relativeId = mapped + postfix
possibleFiles = [mapped]
} else if (mapped === false) {
return (webResolvedImports[id] = browserExternalId)
}
}

if (relativeId) {
const resolved = tryFsResolve(
path.join(dir, relativeId),
options,
!exportsField, // try index only if no exports field
targetWeb
possibleFiles ||= [id]
if (possibleFiles[0]) {
let resolved: string | undefined
possibleFiles.some(
(file) =>
(resolved = tryFsResolve(
path.join(dir, file),
options,
!exportsField, // try index only if no exports field
targetWeb
))
)
if (resolved) {
resolved += postfix
isDebug &&
debug(
`[node/deep-import] ${colors.cyan(id)} -> ${colors.dim(resolved)}`
Expand Down
14 changes: 7 additions & 7 deletions pnpm-lock.yaml

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