Skip to content

Commit

Permalink
feat(resolve): support subpath patterns + production/development cond…
Browse files Browse the repository at this point in the history
…itinals in exports field

Exports field resolving now delegated to https://github.com/lukeed/resolve.exports
  • Loading branch information
yyx990803 committed Jan 11, 2021
1 parent 0501084 commit 62cbd53
Show file tree
Hide file tree
Showing 9 changed files with 89 additions and 99 deletions.
8 changes: 8 additions & 0 deletions packages/playground/resolve/__tests__/resolve.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isBuild } from '../../testUtils'

test('deep import', async () => {
expect(await page.textContent('.deep-import')).toMatch('[2,4]')
})
Expand Down Expand Up @@ -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]')
})
Expand Down
1 change: 1 addition & 0 deletions packages/playground/resolve/exports-env/browser.prod.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const msg = '[success] exports env (browser.prod.mjs)'
5 changes: 4 additions & 1 deletion packages/playground/resolve/exports-env/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 3 additions & 3 deletions packages/playground/resolve/exports-path/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
},
"./deep.js": "./deep.js",
"./dir/": "./dir/",
"./dir-mapped/": {
"import": "./dir/",
"require": "./dir-cjs/"
"./dir-mapped/*": {
"import": "./dir/*",
"require": "./dir-cjs/*"
}
}
}
1 change: 1 addition & 0 deletions packages/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 11 additions & 9 deletions packages/vite/src/node/optimizer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -173,7 +173,6 @@ export async function optimizeDeps(

try {
const rollup = require('rollup') as typeof Rollup

const bundle = await rollup.rollup({
input: qualified,
external,
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
15 changes: 9 additions & 6 deletions packages/vite/src/node/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
127 changes: 47 additions & 80 deletions packages/vite/src/node/plugins/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
Expand All @@ -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)
Expand All @@ -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
}
Expand Down Expand Up @@ -158,6 +168,7 @@ export function resolvePlugin({
(res = tryNodeResolve(
id,
importer ? path.dirname(importer) : root,
isProduction,
isBuild,
dedupe,
root,
Expand Down Expand Up @@ -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
}
}
Expand All @@ -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
Expand All @@ -239,6 +255,7 @@ export const idToPkgMap = new Map<string, PackageData>()
export function tryNodeResolve(
id: string,
basedir: string,
isProduction: boolean,
isBuild = true,
dedupe?: string[],
dedupeRoot?: string,
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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')
Expand Down Expand Up @@ -454,7 +472,7 @@ export function resolvePackageEntry(
}

entryPoint = path.resolve(dir, entryPoint)
const resolvedEntryPont = tryFsResolve(entryPoint)
const resolvedEntryPont = tryFsResolve(entryPoint, isProduction)

if (resolvedEntryPont) {
isDebug &&
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Loading

3 comments on commit 62cbd53

@lukeed
Copy link

@lukeed lukeed commented on 62cbd53 Jan 11, 2021

Choose a reason for hiding this comment

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

Glad to see it was pretty straightforward! By the looks of it, you'll be dropping esmodules and module conditions. If you care about these, you should be able to add them easily:

// the order here doesn't matter
conditions: ['esmodules', 'module', isProduction ? 'production' : 'development']

@yyx990803
Copy link
Member Author

@yyx990803 yyx990803 commented on 62cbd53 Jan 11, 2021

Choose a reason for hiding this comment

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

@lukeed Thanks for reviewing this - I'm ok with dropping those, since they are not actually specified. One should use import anyway. This also dropped support for require inside exports.

@lukeed
Copy link

@lukeed lukeed commented on 62cbd53 Jan 11, 2021

Choose a reason for hiding this comment

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

No problem – agreed. And yeah, but I assumed you intentionally dropped that by not using the option :)

Please sign in to comment.