-
-
Notifications
You must be signed in to change notification settings - Fork 6.1k
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
Changes from all commits
165cdef
606f126
a94200f
296fe46
4d3c6ac
bb4fbd0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
|
@@ -107,6 +107,7 @@ export interface InternalResolveOptions extends Required<ResolveOptions> { | |
shouldExternalize?: (id: string) => boolean | undefined | ||
// Check this resolve is called from `hookNodeResolve` in SSR | ||
isHookNodeResolve?: boolean | ||
overrideConditions?: string[] | ||
} | ||
|
||
export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { | ||
|
@@ -579,21 +580,12 @@ function tryResolveFile( | |
} | ||
} | ||
|
||
export type InternalResolveOptionsWithOverrideConditions = | ||
InternalResolveOptions & { | ||
/** | ||
* @deprecated In future, `conditions` will work like this. | ||
* @internal | ||
*/ | ||
overrideConditions?: string[] | ||
} | ||
|
||
export const idToPkgMap = new Map<string, PackageData>() | ||
|
||
export function tryNodeResolve( | ||
id: string, | ||
importer: string | null | undefined, | ||
options: InternalResolveOptionsWithOverrideConditions, | ||
options: InternalResolveOptions, | ||
targetWeb: boolean, | ||
depsOptimizer?: DepsOptimizer, | ||
ssr?: boolean, | ||
|
@@ -923,29 +915,29 @@ export function resolvePackageEntry( | |
return cached | ||
} | ||
try { | ||
let entryPoint: string | undefined | void | ||
let entryPoints: string[] = [] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The new |
||
|
||
// 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')) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need for |
||
) { | ||
entryPoints = resolveExports( | ||
data, | ||
'.', | ||
options, | ||
getInlineConditions(options, targetWeb), | ||
options.overrideConditions | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When |
||
) | ||
if (!entryPoints.length) { | ||
packageEntryFailure(id) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comes from #8484, and it makes resolution more strict when an |
||
} | ||
} 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 ( | ||
|
@@ -968,34 +960,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 | ||
|
@@ -1040,52 +1032,41 @@ function packageEntryFailure(id: string, details?: string) { | |
) | ||
} | ||
|
||
const conditionalConditions = new Set(['production', 'development', 'module']) | ||
|
||
function resolveExports( | ||
pkg: PackageData['data'], | ||
key: string, | ||
options: InternalResolveOptionsWithOverrideConditions, | ||
/** | ||
* This generates conditions that aren't inferred by `resolveExports` | ||
* from the `options` object. | ||
*/ | ||
function getInlineConditions( | ||
options: InternalResolveOptions, | ||
targetWeb: boolean | ||
) { | ||
const overrideConditions = options.overrideConditions | ||
? new Set(options.overrideConditions) | ||
: undefined | ||
const inlineConditions: string[] = [] | ||
|
||
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') | ||
const conditions: readonly string[] = | ||
options.overrideConditions || options.conditions | ||
|
||
if (targetWeb) { | ||
if (!conditions.includes('node')) { | ||
inlineConditions.push('browser') | ||
} | ||
} else if (!conditions.includes('browser')) { | ||
inlineConditions.push('node') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This mirrors the |
||
} | ||
if (options.overrideConditions) { | ||
conditions.push( | ||
...options.overrideConditions.filter((condition) => | ||
conditionalConditions.has(condition) | ||
) | ||
) | ||
} else if (options.conditions.length > 0) { | ||
conditions.push(...options.conditions) | ||
|
||
// The "module" condition is no longer recommended, but some older | ||
// packages may still use it. | ||
if (!options.isRequire && !conditions.includes('require')) { | ||
inlineConditions.push('module') | ||
} | ||
|
||
return _resolveExports(pkg, key, { | ||
browser: targetWeb && !conditions.includes('node'), | ||
require: options.isRequire && !conditions.includes('import'), | ||
conditions | ||
// The "overrideConditions" array can add arbitrary conditions. | ||
options.overrideConditions?.forEach((condition) => { | ||
if (!inlineConditions.includes(condition)) { | ||
inlineConditions.push(condition) | ||
} | ||
}) | ||
|
||
return inlineConditions | ||
} | ||
|
||
function resolveDeepImport( | ||
|
@@ -1105,47 +1086,58 @@ function resolveDeepImport( | |
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 | ||
// map relative based on exports data | ||
possibleFiles = resolveExports( | ||
data, | ||
file, | ||
options, | ||
getInlineConditions(options, targetWeb), | ||
options.overrideConditions | ||
) | ||
if (postfix) { | ||
if (possibleFiles.length) { | ||
possibleFiles = possibleFiles.map((f) => f + postfix) | ||
} else { | ||
relativeId = undefined | ||
possibleFiles = resolveExports( | ||
data, | ||
file + postfix, | ||
options, | ||
getInlineConditions(options, targetWeb), | ||
options.overrideConditions | ||
) | ||
} | ||
} else { | ||
// not exposed | ||
relativeId = undefined | ||
} | ||
if (!relativeId) { | ||
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 + postfix] | ||
} 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) { | ||
isDebug && | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This fixes a bug introduced in #10683
Some packages use "require" instead of "default" for CJS entry (eg:
vitest/config
)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, this bug was introduced (and now fixed) by this PR :)
The
overrideConditions
array is respected by@alloc/resolve.exports
for all conditions (includingimport
andrequire
) except fordefault
of course. So we have to defineimport
orrequire
explicitly in theoverrideConditions
option.