From 62cbd53f25411ddfc7cfd5446c2f487a260da191 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 11 Jan 2021 15:18:15 -0500 Subject: [PATCH] feat(resolve): support subpath patterns + production/development conditinals in exports field Exports field resolving now delegated to https://github.com/lukeed/resolve.exports --- .../resolve/__tests__/resolve.spec.ts | 8 ++ .../resolve/exports-env/browser.prod.mjs | 1 + .../resolve/exports-env/package.json | 5 +- .../resolve/exports-path/package.json | 6 +- packages/vite/package.json | 1 + packages/vite/src/node/optimizer/index.ts | 20 +-- packages/vite/src/node/plugins/index.ts | 15 ++- packages/vite/src/node/plugins/resolve.ts | 127 +++++++----------- yarn.lock | 5 + 9 files changed, 89 insertions(+), 99 deletions(-) create mode 100644 packages/playground/resolve/exports-env/browser.prod.mjs diff --git a/packages/playground/resolve/__tests__/resolve.spec.ts b/packages/playground/resolve/__tests__/resolve.spec.ts index 9ffe4199177e4e..ebb81ba2e98c72 100644 --- a/packages/playground/resolve/__tests__/resolve.spec.ts +++ b/packages/playground/resolve/__tests__/resolve.spec.ts @@ -1,3 +1,5 @@ +import { isBuild } from '../../testUtils' + test('deep import', async () => { expect(await page.textContent('.deep-import')).toMatch('[2,4]') }) @@ -26,6 +28,12 @@ test('Respect exports field env key priority', async () => { expect(await page.textContent('.exports-env')).toMatch('[success]') }) +test('Respect production/development conditionals', async () => { + expect(await page.textContent('.exports-env')).toMatch( + isBuild ? `browser.prod.mjs` : `browser.mjs` + ) +}) + test('omitted index/*', async () => { expect(await page.textContent('.index')).toMatch('[success]') }) diff --git a/packages/playground/resolve/exports-env/browser.prod.mjs b/packages/playground/resolve/exports-env/browser.prod.mjs new file mode 100644 index 00000000000000..8265343ed6220f --- /dev/null +++ b/packages/playground/resolve/exports-env/browser.prod.mjs @@ -0,0 +1 @@ +export const msg = '[success] exports env (browser.prod.mjs)' \ No newline at end of file diff --git a/packages/playground/resolve/exports-env/package.json b/packages/playground/resolve/exports-env/package.json index 4a61c1d8aec53e..766a6c7d40481d 100644 --- a/packages/playground/resolve/exports-env/package.json +++ b/packages/playground/resolve/exports-env/package.json @@ -3,7 +3,10 @@ "version": "1.0.0", "exports": { "import": { - "browser": "./browser.mjs" + "browser": { + "production": "./browser.prod.mjs", + "development": "./browser.mjs" + } }, "browser": "./browser.js", "default": "./fallback.umd.js" diff --git a/packages/playground/resolve/exports-path/package.json b/packages/playground/resolve/exports-path/package.json index 88370f08129fdc..603a18e67e75d4 100644 --- a/packages/playground/resolve/exports-path/package.json +++ b/packages/playground/resolve/exports-path/package.json @@ -8,9 +8,9 @@ }, "./deep.js": "./deep.js", "./dir/": "./dir/", - "./dir-mapped/": { - "import": "./dir/", - "require": "./dir-cjs/" + "./dir-mapped/*": { + "import": "./dir/*", + "require": "./dir-cjs/*" } } } diff --git a/packages/vite/package.json b/packages/vite/package.json index 60953ff3174df3..4fa8bbedf3cb3a 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -101,6 +101,7 @@ "postcss-import": "^13.0.0", "postcss-load-config": "^3.0.0", "postcss-modules": "^4.0.0", + "resolve.exports": "^1.0.1", "rollup-plugin-dynamic-import-variables": "^1.1.0", "rollup-plugin-license": "^2.2.0", "selfsigned": "^1.10.8", diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index e5a5b931ab2b5c..ee5e7932ded1d5 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -139,7 +139,7 @@ export async function optimizeDeps( // Force included deps - these can also be deep paths if (options.include) { options.include.forEach((id) => { - const filePath = tryNodeResolve(id, root) + const filePath = tryNodeResolve(id, root, config.isProduction) if (filePath) { qualified[id] = filePath.id } @@ -173,7 +173,6 @@ export async function optimizeDeps( try { const rollup = require('rollup') as typeof Rollup - const bundle = await rollup.rollup({ input: qualified, external, @@ -184,12 +183,15 @@ export async function optimizeDeps( aliasPlugin({ entries: config.alias }), ...pre, depAssetExternalPlugin(config), - resolvePlugin({ - root: config.root, - dedupe: config.dedupe, - isBuild: true, - asSrc: false - }), + resolvePlugin( + { + root: config.root, + dedupe: config.dedupe, + isBuild: true, + asSrc: false + }, + config + ), jsonPlugin({ preferConst: true, namedExports: true @@ -289,7 +291,7 @@ async function resolveQualifiedDeps( } let filePath try { - const resolved = tryNodeResolve(id, root) + const resolved = tryNodeResolve(id, root, config.isProduction) filePath = resolved && resolved.id } catch (e) {} if (!filePath) { diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index f528aa9122e258..db514023bf099a 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -31,12 +31,15 @@ export async function resolvePlugins( config.build.polyfillDynamicImport ? dynamicImportPolyfillPlugin(config) : null, - resolvePlugin({ - root: config.root, - dedupe: config.dedupe, - isBuild, - asSrc: true - }), + resolvePlugin( + { + root: config.root, + dedupe: config.dedupe, + isBuild, + asSrc: true + }, + config + ), htmlPlugin(), cssPlugin(config), config.esbuild !== false ? esbuildPlugin(config.esbuild) : null, diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index 549026e50fa190..e2ad5b1cf7bac5 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -21,9 +21,21 @@ import { createFilter } from '@rollup/pluginutils' import { PartialResolvedId } from 'rollup' import isBuiltin from 'isbuiltin' import { isCSSRequest } from './css' +import { resolve as _resolveExports } from 'resolve.exports' const mainFields = ['module', 'main'] +function resolveExports( + pkg: PackageData['data'], + key: string, + isProduction: boolean +) { + return _resolveExports(pkg, key, { + browser: true, + conditions: isProduction ? ['production'] : ['development'] + }) +} + // special id for paths marked with browser: false // https://github.com/defunctzombie/package-browser-field-spec#ignore-a-module const browserExternalId = '__browser-external' @@ -45,13 +57,11 @@ interface ResolveOptions { dedupe?: string[] } -export function resolvePlugin({ - root, - isBuild, - asSrc, - dedupe -}: ResolveOptions): Plugin { - let config: ResolvedConfig | undefined +export function resolvePlugin( + { root, isBuild, asSrc, dedupe }: ResolveOptions, + config?: ResolvedConfig +): Plugin { + const isProduction = !!config?.isProduction let server: ViteDevServer | undefined return { @@ -91,7 +101,7 @@ export function resolvePlugin({ // /foo -> /fs-root/foo if (asSrc && id.startsWith('/')) { const fsPath = path.resolve(root, id.slice(1)) - if ((res = tryFsResolve(fsPath))) { + if ((res = tryFsResolve(fsPath, isProduction))) { isDebug && debug(`[url] ${chalk.cyan(id)} -> ${chalk.dim(res)}`) return res } @@ -115,7 +125,7 @@ export function resolvePlugin({ return browserExternalId } } - if ((res = tryFsResolve(fsPath))) { + if ((res = tryFsResolve(fsPath, isProduction))) { isDebug && debug(`[relative] ${chalk.cyan(id)} -> ${chalk.dim(res)}`) if (pkg) { idToPkgMap.set(res, pkg) @@ -129,7 +139,7 @@ export function resolvePlugin({ } // absolute fs paths - if (path.isAbsolute(id) && (res = tryFsResolve(id))) { + if (path.isAbsolute(id) && (res = tryFsResolve(id, isProduction))) { isDebug && debug(`[fs] ${chalk.cyan(id)} -> ${chalk.dim(res)}`) return res } @@ -158,6 +168,7 @@ export function resolvePlugin({ (res = tryNodeResolve( id, importer ? path.dirname(importer) : root, + isProduction, isBuild, dedupe, root, @@ -196,15 +207,19 @@ export function resolvePlugin({ } } -function tryFsResolve(fsPath: string, tryIndex = true): string | undefined { +function tryFsResolve( + fsPath: string, + isProduction: boolean, + tryIndex = true +): string | undefined { const [file, q] = fsPath.split(`?`, 2) const query = q ? `?${q}` : `` let res: string | undefined - if ((res = tryResolveFile(file, query, tryIndex))) { + if ((res = tryResolveFile(file, query, isProduction, tryIndex))) { return res } for (const ext of SUPPORTED_EXTS) { - if ((res = tryResolveFile(file + ext, query, tryIndex))) { + if ((res = tryResolveFile(file + ext, query, isProduction, tryIndex))) { return res } } @@ -213,20 +228,21 @@ function tryFsResolve(fsPath: string, tryIndex = true): string | undefined { function tryResolveFile( file: string, query: string, + isProduction: boolean, tryIndex: boolean ): string | undefined { if (fs.existsSync(file)) { const isDir = fs.statSync(file).isDirectory() if (isDir) { if (tryIndex) { - const index = tryFsResolve(file + '/index', false) + const index = tryFsResolve(file + '/index', isProduction, false) if (index) return normalizePath(index) + query } const pkgPath = file + '/package.json' if (fs.existsSync(pkgPath)) { // path points to a node package const pkg = loadPackageData(pkgPath) - return resolvePackageEntry(file, pkg) + return resolvePackageEntry(file, pkg, isProduction) } } else { return normalizePath(file) + query @@ -239,6 +255,7 @@ export const idToPkgMap = new Map() export function tryNodeResolve( id: string, basedir: string, + isProduction: boolean, isBuild = true, dedupe?: string[], dedupeRoot?: string, @@ -282,8 +299,8 @@ export function tryNodeResolve( } let resolved = deepMatch - ? resolveDeepImport(id, pkg) - : resolvePackageEntry(id, pkg) + ? resolveDeepImport(id, pkg, isProduction) + : resolvePackageEntry(id, pkg, isProduction) if (!resolved) { return } @@ -385,19 +402,19 @@ function loadPackageData(pkgPath: string, cacheKey = pkgPath) { export function resolvePackageEntry( id: string, - { resolvedImports, dir, data }: PackageData + { resolvedImports, dir, data }: PackageData, + isProduction = false ): string | undefined { if (resolvedImports['.']) { return resolvedImports['.'] } - let entryPoint: string | undefined + let entryPoint: string | undefined | void // resolve exports field with highest priority - // https://nodejs.org/api/packages.html#packages_package_entry_points - const { exports: exportsField } = data - if (exportsField) { - entryPoint = resolveConditionalExports(exportsField, '.') + // using https://github.com/lukeed/resolve.exports + if (data.exports) { + entryPoint = resolveExports(data, '.', isProduction) } if (!entryPoint) { @@ -417,7 +434,8 @@ export function resolvePackageEntry( // possible and check for hints of UMD. If it is UMD, prefer "module" // instead; Otherwise, assume it's ESM and use it. const resolvedBrowserEntry = tryFsResolve( - path.resolve(dir, browserEntry) + path.resolve(dir, browserEntry), + isProduction ) if (resolvedBrowserEntry) { const content = fs.readFileSync(resolvedBrowserEntry, 'utf-8') @@ -454,7 +472,7 @@ export function resolvePackageEntry( } entryPoint = path.resolve(dir, entryPoint) - const resolvedEntryPont = tryFsResolve(entryPoint) + const resolvedEntryPont = tryFsResolve(entryPoint, isProduction) if (resolvedEntryPont) { isDebug && @@ -473,20 +491,21 @@ export function resolvePackageEntry( function resolveDeepImport( id: string, - { resolvedImports, dir, data }: PackageData + { resolvedImports, dir, data }: PackageData, + isProduction: boolean ): string | undefined { id = '.' + id.slice(data.name.length) if (resolvedImports[id]) { return resolvedImports[id] } - let relativeId: string | undefined = id + let relativeId: string | undefined | void = id const { exports: exportsField, browser: browserField } = data // map relative based on exports data if (exportsField) { if (isObject(exportsField) && !Array.isArray(exportsField)) { - relativeId = resolveConditionalExports(exportsField, relativeId) + relativeId = resolveExports(data, relativeId, isProduction) } else { // not exposed relativeId = undefined @@ -516,58 +535,6 @@ function resolveDeepImport( } } -const ENV_KEYS = [ - 'esmodules', - 'import', - 'module', - 'require', - 'browser', - 'node', - 'default' -] - -// https://nodejs.org/api/packages.html -// TODO: subpath imports & subpath patterns -function resolveConditionalExports(exp: any, id: string): string | undefined { - if (typeof exp === 'string') { - return exp - } else if (isObject(exp)) { - let isFileListing: boolean | undefined - let fallback: string | undefined - for (const key in exp) { - if (isFileListing === undefined) { - isFileListing = key[0] === '.' - } - if (isFileListing) { - if (key === id) { - return resolveConditionalExports(exp[key], id) - } else if (key.endsWith('/') && id.startsWith(key)) { - // mapped directory - const replacement = resolveConditionalExports(exp[key], id) - return replacement && id.replace(key, replacement) - } - } else if (ENV_KEYS.includes(key)) { - // https://github.com/vitejs/vite/issues/1418 - // respect env key order - // but intentionally de-prioritize "require" and "default" keys - if (key === 'require' || key === 'default') { - if (!fallback) fallback = key - } else { - return resolveConditionalExports(exp[key], id) - } - } - if (fallback) { - return resolveConditionalExports(exp[key], id) - } - } - } else if (Array.isArray(exp)) { - for (let i = 0; i < exp.length; i++) { - const res = resolveConditionalExports(exp[i], id) - if (res) return res - } - } -} - /** * given a relative path in pkg dir, * return a relative path in pkg dir, diff --git a/yarn.lock b/yarn.lock index 37bfcd6b84f3e9..47da774e0fccc1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6468,6 +6468,11 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= +resolve.exports@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.0.1.tgz#9aff14e2ed43ea40bbdc2f83b9a2465b1f093325" + integrity sha512-rV7NC8yDnV2wSg3nB7Faoje+oQk7/7cuxEq4cgOR81mULtAWvHGODowzxojq4FIftNA0FRtnn5v5a+F4aTiSzA== + resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.15.1, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.19.0: version "1.19.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c"