diff --git a/packages/vite/package.json b/packages/vite/package.json index 4cc39e7f156009..dd8dd089cd1267 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -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", diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index e90bdd7150bced..23f4b8c40371e4 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -48,11 +48,7 @@ import { DEFAULT_MAIN_FIELDS, ENV_ENTRY } from './constants' -import type { - InternalResolveOptions, - InternalResolveOptionsWithOverrideConditions, - ResolveOptions -} from './plugins/resolve' +import type { InternalResolveOptions, ResolveOptions } from './plugins/resolve' import { resolvePlugin, tryNodeResolve } from './plugins/resolve' import type { LogLevel, Logger } from './logger' import { createLogger } from './logger' @@ -958,7 +954,7 @@ async function bundleConfigFile( { name: 'externalize-deps', setup(build) { - const options: InternalResolveOptionsWithOverrideConditions = { + const options: InternalResolveOptions = { root: path.dirname(fileName), isBuild: true, isProduction: true, @@ -968,7 +964,7 @@ async function bundleConfigFile( mainFields: [], browserField: false, conditions: [], - overrideConditions: ['node'], + overrideConditions: ['node', 'require'], dedupe: [], extensions: DEFAULT_EXTENSIONS, preserveSymlinks: false diff --git a/packages/vite/src/node/packages.ts b/packages/vite/src/node/packages.ts index e1a85bff441212..a9ab32948c631a 100644 --- a/packages/vite/src/node/packages.ts +++ b/packages/vite/src/node/packages.ts @@ -1,6 +1,6 @@ import fs from 'node:fs' import path from 'node:path' -import { createDebugger, createFilter, resolveFrom } from './utils' +import { createDebugger, createFilter, lookupFile, resolveFrom } from './utils' import type { ResolvedConfig } from './config' import type { Plugin } from './plugin' @@ -27,6 +27,7 @@ export interface PackageData { main: string module: string browser: string | Record + imports: Record exports: string | Record | string[] dependencies: Record } @@ -131,6 +132,26 @@ export function loadPackageData( return pkg } +export function loadNearestPackageData( + startDir: string, + options?: { preserveSymlinks?: boolean }, + predicate?: (pkg: PackageData) => boolean +): PackageData | null { + let importerPkg: PackageData | undefined + lookupFile(startDir, ['package.json'], { + pathOnly: true, + predicate(pkgPath) { + importerPkg = loadPackageData(pkgPath, options?.preserveSymlinks) + return !predicate || predicate(importerPkg) + } + }) + return importerPkg || null +} + +export function isNamedPackage(pkg: PackageData): boolean { + return !!pkg.data.name +} + export function watchPackageDataPlugin(config: ResolvedConfig): Plugin { const watchQueue = new Set() let watchFile = (id: string) => { @@ -163,3 +184,40 @@ export function watchPackageDataPlugin(config: ResolvedConfig): Plugin { } } } + +export function findPackageJson(dir: string): string | null { + // Stop looking at node_modules directory. + if (path.basename(dir) === 'node_modules') { + return null + } + const pkgPath = path.join(dir, 'package.json') + if (fs.existsSync(pkgPath)) { + return pkgPath + } + const parentDir = path.dirname(dir) + return parentDir !== dir ? findPackageJson(parentDir) : null +} + +const workspaceRootFiles = ['lerna.json', 'pnpm-workspace.yaml', '.git'] + +export function isWorkspaceRoot( + dir: string, + preserveSymlinks?: boolean, + packageCache?: PackageCache +): boolean { + const files = fs.readdirSync(dir) + if (files.some((file) => workspaceRootFiles.includes(file))) { + return true // Found a lerna/pnpm workspace or git repository. + } + if (files.includes('package.json')) { + const workspacePkg = loadPackageData( + path.join(dir, 'package.json'), + preserveSymlinks, + packageCache + ) + if (workspacePkg?.data.workspaces) { + return true // Found a npm/yarn workspace. + } + } + return false +} diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index 1c1c80f573e84d..5f59761671388f 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -1,8 +1,9 @@ import fs from 'node:fs' import path from 'node:path' +import { Module } from 'node:module' 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 { @@ -42,7 +43,14 @@ import { import { optimizedDepInfoFromFile, optimizedDepInfoFromId } from '../optimizer' import type { DepsOptimizer } from '../optimizer' import type { SSROptions } from '..' -import type { PackageCache, PackageData } from '../packages' +import { + findPackageJson, + isNamedPackage, + isWorkspaceRoot, + loadNearestPackageData, + PackageCache, + PackageData +} from '../packages' import { loadPackageData, resolvePackageData } from '../packages' import { isWorkerRequest } from './worker' @@ -107,6 +115,7 @@ export interface InternalResolveOptions extends Required { shouldExternalize?: (id: string) => boolean | undefined // Check this resolve is called from `hookNodeResolve` in SSR isHookNodeResolve?: boolean + overrideConditions?: string[] } export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { @@ -270,6 +279,58 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { } } + // handle subpath imports + // https://nodejs.org/api/packages.html#subpath-imports + if (id[0] === '#') { + if (!importer) { + return // Module not found. + } + const importerPkg = loadNearestPackageData( + path.dirname(importer), + options, + isNamedPackage + ) + if (!importerPkg || !importerPkg.data.imports) { + return // Module not found. + } + // Rewrite the `imports` object to be compatible with the + // `resolve.exports` package. + const { name, imports } = importerPkg.data + const exports = Object.fromEntries( + Object.entries(imports).map((entry) => { + entry[0] = entry[0].replace(/^#/, './') + return entry + }) + ) + const possiblePaths = resolveExports( + { name, exports }, + id.replace(/^#/, './'), + options, + getInlineConditions(options, targetWeb), + options.overrideConditions + ) + if (!possiblePaths.length) { + throw new Error( + `Package subpath '${id}' is not defined by "imports" in ` + + `${path.join(importerPkg.dir, 'package.json')}.` + ) + } + for (const possiblePath of possiblePaths) { + if (possiblePath[0] === '#') { + continue // Subpath imports cannot be recursive. + } + const resolved = await this.resolve( + possiblePath, + importer, + resolveOpts + ) + if (resolved) { + return resolved + } + } + return // Module not found. + } + // drive relative fs paths (only windows) if (isWindows && id.startsWith('/')) { const basedir = importer ? path.dirname(importer) : process.cwd() @@ -494,13 +555,17 @@ function tryFsResolve( } } + if (!tryIndex) { + return + } + if ( postfix && (res = tryResolveFile( fsPath, '', options, - tryIndex, + true, targetWeb, options.tryPrefix, options.skipPackageJson @@ -514,7 +579,7 @@ function tryFsResolve( file, postfix, options, - tryIndex, + true, targetWeb, options.tryPrefix, options.skipPackageJson @@ -550,8 +615,10 @@ function tryResolveFile( } } } - const index = tryFsResolve(file + '/index', options) - if (index) return index + postfix + const indexFile = tryIndexFile(file, targetWeb, options) + if (indexFile) { + return indexFile + postfix + } } } @@ -579,21 +646,27 @@ function tryResolveFile( } } -export type InternalResolveOptionsWithOverrideConditions = - InternalResolveOptions & { - /** - * @deprecated In future, `conditions` will work like this. - * @internal - */ - overrideConditions?: string[] +function tryIndexFile( + dir: string, + targetWeb: boolean, + options: InternalResolveOptions +) { + if (!options.skipPackageJson) { + options = { ...options, skipPackageJson: true } } + return tryFsResolve(dir + '/index', options, false, targetWeb) +} export const idToPkgMap = new Map() +const lookupNodeModules = (Module as any)._nodeModulePaths as { + (cwd: string): string[] +} + export function tryNodeResolve( id: string, importer: string | null | undefined, - options: InternalResolveOptionsWithOverrideConditions, + options: InternalResolveOptions, targetWeb: boolean, depsOptimizer?: DepsOptimizer, ssr?: boolean, @@ -609,37 +682,15 @@ export function tryNodeResolve( // 'foo' => '' & 'foo' const lastArrowIndex = id.lastIndexOf('>') const nestedRoot = id.substring(0, lastArrowIndex).trim() - const nestedPath = id.substring(lastArrowIndex + 1).trim() - - const possiblePkgIds: string[] = [] - for (let prevSlashIndex = -1; ; ) { - let slashIndex = nestedPath.indexOf('/', prevSlashIndex + 1) - if (slashIndex < 0) { - slashIndex = nestedPath.length - } - const part = nestedPath.slice( - prevSlashIndex + 1, - (prevSlashIndex = slashIndex) - ) - if (!part) { - break - } - - // Assume path parts with an extension are not package roots, except for the - // first path part (since periods are sadly allowed in package names). - // At the same time, skip the first path part if it begins with "@" - // (since "@foo/bar" should be treated as the top-level path). - if (possiblePkgIds.length ? path.extname(part) : part[0] === '@') { - continue - } - - const possiblePkgId = nestedPath.slice(0, slashIndex) - possiblePkgIds.push(possiblePkgId) + if (lastArrowIndex !== -1) { + id = id.substring(lastArrowIndex + 1).trim() } + const basePkgId = id.split('/', id[0] === '@' ? 2 : 1).join('/') + let basedir: string - if (dedupe?.some((id) => possiblePkgIds.includes(id))) { + if (dedupe?.includes(basePkgId)) { basedir = root } else if ( importer && @@ -651,95 +702,202 @@ export function tryNodeResolve( basedir = root } - // nested node module, step-by-step resolve to the basedir of the nestedPath + // resolve a chain of packages notated by '>' in the id if (nestedRoot) { basedir = nestedResolveFrom(nestedRoot, basedir, preserveSymlinks) } - // nearest package.json - let nearestPkg: PackageData | undefined - // nearest package.json that may have the `exports` field - let pkg: PackageData | undefined + let resolvedPkg: PackageData | undefined + let resolvedPkgId: string | undefined + let resolvedPkgType: string | undefined + let resolvedId: string | undefined + let resolver: typeof resolvePackageEntry - let pkgId = possiblePkgIds.reverse().find((pkgId) => { - nearestPkg = resolvePackageData( - pkgId, - basedir, - preserveSymlinks, - packageCache - )! - return nearestPkg - })! + const nodeModules = lookupNodeModules(basedir) + for (const nodeModulesDir of nodeModules) { + if (!fs.existsSync(nodeModulesDir)) { + continue + } - const rootPkgId = possiblePkgIds[0] - const rootPkg = resolvePackageData( - rootPkgId, - basedir, - preserveSymlinks, - packageCache - )! - if (rootPkg?.data?.exports) { - pkg = rootPkg - pkgId = rootPkgId - } else { - pkg = nearestPkg - } + const entryPath = path.join(nodeModulesDir, id) + const nearestPkgPath = findPackageJson(entryPath) + if (nearestPkgPath) { + resolvedPkg = loadPackageData( + nearestPkgPath, + preserveSymlinks, + packageCache + ) + resolvedPkgId = path.dirname( + path.relative(nodeModulesDir, nearestPkgPath) + ) - if (!pkg || !nearestPkg) { - // if import can't be found, check if it's an optional peer dep. - // if so, we can resolve to a special id that errors only when imported. - if ( - !options.isHookNodeResolve && - basedir !== root && // root has no peer dep - !isBuiltin(nestedPath) && - !nestedPath.includes('\0') && - bareImportRE.test(nestedPath) - ) { - // find package.json with `name` as main - const mainPackageJson = lookupFile(basedir, ['package.json'], { - predicate: (content) => !!JSON.parse(content).name - }) - if (mainPackageJson) { - const mainPkg = JSON.parse(mainPackageJson) - if ( - mainPkg.peerDependencies?.[nestedPath] && - mainPkg.peerDependenciesMeta?.[nestedPath]?.optional - ) { - return { - id: `${optionalPeerDepId}:${nestedPath}:${mainPkg.name}` + // Always use the nearest package.json to determine whether a + // ".js" module is ESM or CJS. + resolvedPkgType = resolvedPkg.data.type + + // If the nearest package.json has no "exports" field, then we + // need to check the dependency's root directory for an exports + // field, since that should take precedence (see #10371). + if (resolvedPkgId !== basePkgId) { + try { + const basePkgPath = path.join( + nodeModulesDir, + basePkgId, + 'package.json' + ) + const basePkg = loadPackageData( + basePkgPath, + preserveSymlinks, + packageCache + ) + if (basePkg.data.exports) { + resolvedPkg = basePkg + resolvedPkgId = path.dirname( + path.relative(nodeModulesDir, basePkgPath) + ) } + } catch (e) { + if (e.code !== 'ENOENT') { + throw e + } + } + } + + let usedId: string + if (resolvedPkgId === id) { + // Use the main entry point + resolver = resolvePackageEntry + usedId = id + } else { + // Use a deep entry point + resolver = resolveDeepImport + usedId = '.' + id.slice(resolvedPkgId.length) + } + + try { + resolvedId = resolver(usedId, resolvedPkg, targetWeb, options) + if (resolvedId) { + break + } + } catch (err) { + if (!options.tryEsmOnly) { + throw err + } + } + if (options.tryEsmOnly) { + resolvedId = resolver(usedId, resolvedPkg, targetWeb, { + ...options, + isRequire: false, + mainFields: DEFAULT_MAIN_FIELDS, + extensions: DEFAULT_EXTENSIONS + }) + if (resolvedId) { + break } } + + // Reset the resolvedPkg variables to avoid false positives as we + // continue our search. + resolvedPkg = undefined + resolvedPkgId = undefined + resolvedPkgType = undefined + continue + } + + // No package.json was found, but there could still be a module + // here. To match Node's behavior, we must be able to resolve a + // module without a package.json file helping us out. + try { + const stat = fs.statSync(entryPath) + if (stat.isFile()) { + resolvedId = entryPath + break + } + resolvedId = tryIndexFile(entryPath, targetWeb, options) + if (resolvedId) { + break + } + } catch {} + + // In case a file extension is missing, we need to try calling the + // `tryFsResolve` function. + let entryDir = path.dirname(entryPath) + let entryDirExists = false + if (entryDir === nodeModulesDir) { + entryDirExists = true + } else { + try { + const stat = fs.statSync(entryDir) + entryDirExists = stat.isDirectory() + } catch {} + } + + if (entryDirExists) { + resolvedId = tryFsResolve( + entryPath, + { ...options, skipPackageJson: true }, + false, + targetWeb + ) + if (resolvedId) { + break + } } - return - } - let resolveId = resolvePackageEntry - let unresolvedId = pkgId - const isDeepImport = unresolvedId !== nestedPath - if (isDeepImport) { - resolveId = resolveDeepImport - unresolvedId = '.' + nestedPath.slice(pkgId.length) + // Stop looking if we're at the workspace root directory. + if ( + isWorkspaceRoot( + path.dirname(nodeModulesDir), + preserveSymlinks, + packageCache + ) + ) + break } - let resolved: string | undefined - try { - resolved = resolveId(unresolvedId, pkg, targetWeb, options) - } catch (err) { - if (!options.tryEsmOnly) { - throw err + if (!resolvedId) { + const mayBeOptionalPeerDep = + !options.isHookNodeResolve && + basedir !== root && + !isBuiltin(basePkgId) && + !basePkgId.includes('\0') && + bareImportRE.test(basePkgId) + + if (!mayBeOptionalPeerDep) { + return // Module not found. + } + + // Find the importer's nearest package.json with a "name" field. + // Some projects (like Svelte) have nameless package.json files to + // appease older Node.js versions and they don't have the list of + // optional peer dependencies like the root package.json does. + const basePkg = loadNearestPackageData(basedir, options, isNamedPackage) + if (!basePkg) { + return // Module not found. + } + + const { peerDependencies, peerDependenciesMeta } = basePkg.data + const optionalPeerDep = + peerDependenciesMeta?.[basePkgId]?.optional && + peerDependencies?.[basePkgId] + + if (!optionalPeerDep) { + return // Module not found. + } + + return { + id: `${optionalPeerDepId}:${basePkgId}:${basePkg.data.name}` } } - if (!resolved && options.tryEsmOnly) { - resolved = resolveId(unresolvedId, pkg, targetWeb, { - ...options, - isRequire: false, - mainFields: DEFAULT_MAIN_FIELDS, - extensions: DEFAULT_EXTENSIONS + + if (!resolvedPkg) { + const pkgPath = lookupFile(path.dirname(resolvedId), ['package.json'], { + pathOnly: true }) - } - if (!resolved) { - return + if (!pkgPath) { + return { id: resolvedId } + } + resolvedPkg = loadPackageData(pkgPath, preserveSymlinks, packageCache) } const processResult = (resolved: PartialResolvedId) => { @@ -761,8 +919,8 @@ export function tryNodeResolve( return resolved } let resolvedId = id - if (isDeepImport) { - if (!pkg?.data.exports && path.extname(id) !== resolvedExt) { + if (resolver === resolveDeepImport) { + if (!resolvedPkg?.data.exports && path.extname(id) !== resolvedExt) { resolvedId = resolved.id.slice(resolved.id.indexOf(id)) isDebug && debug( @@ -774,33 +932,33 @@ export function tryNodeResolve( } // link id to pkg for browser field mapping check - idToPkgMap.set(resolved, pkg) + idToPkgMap.set(resolvedId, resolvedPkg) if ((isBuild && !depsOptimizer) || externalize) { // Resolve package side effects for build so that rollup can better // perform tree-shaking return processResult({ - id: resolved, - moduleSideEffects: pkg.hasSideEffects(resolved) + id: resolvedId, + moduleSideEffects: resolvedPkg.hasSideEffects(resolvedId) }) } - const ext = path.extname(resolved) + const ext = path.extname(resolvedId) const isCJS = - ext === '.cjs' || (ext === '.js' && nearestPkg.data.type !== 'module') + ext === '.cjs' || (ext === '.js' && resolvedPkgType !== 'module') if ( !options.ssrOptimizeCheck && - (!resolved.includes('node_modules') || // linked + (!resolvedId.includes('node_modules') || // linked !depsOptimizer || // resolving before listening to the server options.scan) // initial esbuild scan phase ) { - return { id: resolved } + return { id: resolvedId } } // if we reach here, it's a valid dep import that hasn't been optimized. const isJsType = depsOptimizer - ? isOptimizable(resolved, depsOptimizer.options) - : OPTIMIZABLE_ENTRY_RE.test(resolved) + ? isOptimizable(resolvedId, depsOptimizer.options) + : OPTIMIZABLE_ENTRY_RE.test(resolvedId) let exclude = depsOptimizer?.options.exclude let include = depsOptimizer?.options.exclude @@ -811,22 +969,28 @@ export function tryNodeResolve( } const skipOptimization = + !resolvedPkgId || !isJsType || importer?.includes('node_modules') || - exclude?.includes(pkgId) || - exclude?.includes(nestedPath) || - SPECIAL_QUERY_RE.test(resolved) || + exclude?.includes(resolvedPkgId) || + exclude?.includes(basePkgId) || + exclude?.includes(id) || + SPECIAL_QUERY_RE.test(resolvedId) || (!isBuild && ssr) || // Only optimize non-external CJS deps during SSR by default (ssr && !isCJS && - !(include?.includes(pkgId) || include?.includes(nestedPath))) + !( + include?.includes(resolvedPkgId) || + include?.includes(basePkgId) || + include?.includes(id) + )) if (options.ssrOptimizeCheck) { return { id: skipOptimization - ? injectQuery(resolved, `__vite_skip_optimization`) - : resolved + ? injectQuery(resolvedId, `__vite_skip_optimization`) + : resolvedId } } @@ -839,25 +1003,25 @@ export function tryNodeResolve( if (!isBuild) { const versionHash = depsOptimizer!.metadata.browserHash if (versionHash && isJsType) { - resolved = injectQuery(resolved, `v=${versionHash}`) + resolvedId = injectQuery(resolvedId, `v=${versionHash}`) } } } else { // this is a missing import, queue optimize-deps re-run and // get a resolved its optimized info - const optimizedInfo = depsOptimizer!.registerMissingImport(id, resolved) - resolved = depsOptimizer!.getOptimizedDepId(optimizedInfo) + const optimizedInfo = depsOptimizer!.registerMissingImport(id, resolvedId) + resolvedId = depsOptimizer!.getOptimizedDepId(optimizedInfo) } if (isBuild) { // Resolve package side effects for build so that rollup can better // perform tree-shaking return { - id: resolved, - moduleSideEffects: pkg.hasSideEffects(resolved) + id: resolvedId, + moduleSideEffects: resolvedPkg.hasSideEffects(resolvedId) } } else { - return { id: resolved! } + return { id: resolvedId } } } @@ -923,29 +1087,29 @@ export function resolvePackageEntry( return cached } try { - let entryPoint: string | undefined | void + let entryPoints: string[] = [] - // 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')) - ) { + entryPoints = resolveExports( + data, + '.', + options, + getInlineConditions(options, targetWeb), + options.overrideConditions + ) + if (!entryPoints.length) { + packageEntryFailure(id) + } + } 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 +1132,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 +1204,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') } - 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 +1258,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 && diff --git a/playground/resolve/__tests__/resolve.spec.ts b/playground/resolve/__tests__/resolve.spec.ts index 8f910f4989bfaa..0ddcc465adf7bf 100644 --- a/playground/resolve/__tests__/resolve.spec.ts +++ b/playground/resolve/__tests__/resolve.spec.ts @@ -144,3 +144,12 @@ test('resolve package that contains # in path', async () => { '[success]' ) }) + +// Support this so we can add symlinks to local directories without +// creating a package.json file (and because Node.js also supports +// this). +test('unpackaged modules in node_modules', async () => { + expect(await page.textContent('.unpackaged-file')).toMatch('[success]') + expect(await page.textContent('.unpackaged-index-file')).toMatch('[success]') + expect(await page.textContent('.unpackaged-deep-import')).toMatch('[success]') +}) diff --git a/playground/resolve/index.html b/playground/resolve/index.html index 6150dc86dbb1ef..89f507de13873e 100644 --- a/playground/resolve/index.html +++ b/playground/resolve/index.html @@ -121,6 +121,15 @@

resolve.conditions

resolve package that contains # in path

+

unpackaged file in node_modules

+

+ +

index file from unpackaged directory in node_modules

+

+ +

deep import of unpackaged directory in node_modules

+

+