Skip to content

Commit

Permalink
feat: support ESM subpath imports (#7770)
Browse files Browse the repository at this point in the history
  • Loading branch information
fi3ework committed Mar 6, 2023
1 parent cde9191 commit cc92da9
Show file tree
Hide file tree
Showing 12 changed files with 120 additions and 7 deletions.
4 changes: 3 additions & 1 deletion packages/vite/src/node/packages.ts
@@ -1,5 +1,6 @@
import fs from 'node:fs'
import path from 'node:path'
import type { Exports, Imports } from 'resolve.exports'
import { createDebugger, createFilter, resolveFrom } from './utils'
import type { ResolvedConfig } from './config'
import type { Plugin } from './plugin'
Expand Down Expand Up @@ -27,7 +28,8 @@ export interface PackageData {
main: string
module: string
browser: string | Record<string, string | false>
exports: string | Record<string, any> | string[]
exports: Exports
imports: Imports
dependencies: Record<string, string>
}
}
Expand Down
48 changes: 43 additions & 5 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 { exports } from 'resolve.exports'
import { exports, imports } from 'resolve.exports'
import { hasESMSyntax } from 'mlly'
import type { Plugin } from '../plugin'
import {
Expand Down Expand Up @@ -55,6 +55,7 @@ export const browserExternalId = '__vite-browser-external'
export const optionalPeerDepId = '__vite-optional-peer-dep'

const nodeModulesInPathRE = /(?:^|\/)node_modules\//
const subpathImportsPrefix = '#'

const isDebug = process.env.DEBUG
const debug = createDebugger('vite:resolve-details', {
Expand Down Expand Up @@ -152,6 +153,29 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin {
scan: resolveOpts?.scan ?? resolveOptions.scan,
}

const resolveSubpathImports = (id: string, importer?: string) => {
if (!importer || !id.startsWith(subpathImportsPrefix)) return
const basedir = path.dirname(importer)
const pkgJsonPath = lookupFile(basedir, ['package.json'], {
pathOnly: true,
})
if (!pkgJsonPath) return

const pkgData = loadPackageData(pkgJsonPath, options.preserveSymlinks)
return resolveExportsOrImports(
pkgData.data,
id,
options,
targetWeb,
'imports',
)
}

const resolvedImports = resolveSubpathImports(id, importer)
if (resolvedImports) {
id = resolvedImports
}

if (importer) {
const _importer = isWorkerRequest(importer)
? splitFileAndPostfix(importer).file
Expand Down Expand Up @@ -958,7 +982,13 @@ export function resolvePackageEntry(
// resolve exports field with highest priority
// using https://github.com/lukeed/resolve.exports
if (data.exports) {
entryPoint = resolveExports(data, '.', options, targetWeb)
entryPoint = resolveExportsOrImports(
data,
'.',
options,
targetWeb,
'exports',
)
}

const resolvedFromExports = !!entryPoint
Expand Down Expand Up @@ -1076,11 +1106,12 @@ function packageEntryFailure(id: string, details?: string) {

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

function resolveExports(
function resolveExportsOrImports(
pkg: PackageData['data'],
key: string,
options: InternalResolveOptionsWithOverrideConditions,
targetWeb: boolean,
type: 'imports' | 'exports',
) {
const overrideConditions = options.overrideConditions
? new Set(options.overrideConditions)
Expand Down Expand Up @@ -1115,7 +1146,8 @@ function resolveExports(
conditions.push(...options.conditions)
}

const result = exports(pkg, key, {
const fn = type === 'imports' ? imports : exports
const result = fn(pkg, key, {
browser: targetWeb && !conditions.includes('node'),
require: options.isRequire && !conditions.includes('import'),
conditions,
Expand Down Expand Up @@ -1149,7 +1181,13 @@ function resolveDeepImport(
if (isObject(exportsField) && !Array.isArray(exportsField)) {
// resolve without postfix (see #7098)
const { file, postfix } = splitFileAndPostfix(relativeId)
const exportsId = resolveExports(data, file, options, targetWeb)
const exportsId = resolveExportsOrImports(
data,
file,
options,
targetWeb,
'exports',
)
if (exportsId !== undefined) {
relativeId = exportsId + postfix
} else {
Expand Down
20 changes: 20 additions & 0 deletions playground/resolve/__tests__/resolve.spec.ts
Expand Up @@ -158,3 +158,23 @@ test('resolve package that contains # in path', async () => {
'[success]',
)
})

test('Resolving top level with imports field', async () => {
expect(await page.textContent('.imports-top-level')).toMatch('[success]')
})

test('Resolving nested path with imports field', async () => {
expect(await page.textContent('.imports-nested')).toMatch('[success]')
})

test('Resolving star with imports filed', async () => {
expect(await page.textContent('.imports-star')).toMatch('[success]')
})

test('Resolving slash with imports filed', async () => {
expect(await page.textContent('.imports-slash')).toMatch('[success]')
})

test('Resolving from other package with imports field', async () => {
expect(await page.textContent('.imports-pkg-slash')).toMatch('[success]')
})
1 change: 1 addition & 0 deletions playground/resolve/imports-path/nested-path.js
@@ -0,0 +1 @@
export const msg = '[success] nested path subpath imports'
1 change: 1 addition & 0 deletions playground/resolve/imports-path/other-pkg/nest/index.js
@@ -0,0 +1 @@
export const msg = '[success] subpath imports from other package'
4 changes: 4 additions & 0 deletions playground/resolve/imports-path/other-pkg/package.json
@@ -0,0 +1,4 @@
{
"name": "@vitejs/test-resolve-imports-pkg",
"private": true
}
1 change: 1 addition & 0 deletions playground/resolve/imports-path/slash/index.js
@@ -0,0 +1 @@
export const msg = '[success] subpath imports with slash'
1 change: 1 addition & 0 deletions playground/resolve/imports-path/star/index.js
@@ -0,0 +1 @@
export const msg = '[success] subpath imports with star'
1 change: 1 addition & 0 deletions playground/resolve/imports-path/top-level.js
@@ -0,0 +1 @@
export const msg = '[success] top level subpath imports'
31 changes: 31 additions & 0 deletions playground/resolve/index.html
Expand Up @@ -36,6 +36,21 @@ <h2>Exports with legacy fallback</h2>
<h2>Exports with module</h2>
<p class="exports-with-module">fail</p>

<h2>Resolving top level with imports field</h2>
<p class="imports-top-level">fail</p>

<h2>Resolving nested path with imports field</h2>
<p class="imports-nested">fail</p>

<h2>Resolving star with imports filed</h2>
<p class="imports-star">fail</p>

<h2>Resolving slash with imports filed</h2>
<p class="imports-slash">fail</p>

<h2>Resolving from other package with imports field</h2>
<p class="imports-pkg-slash">fail</p>

<h2>Resolve /index.*</h2>
<p class="index">fail</p>

Expand Down Expand Up @@ -187,6 +202,22 @@ <h2>resolve package that contains # in path</h2>
import { msg as exportsWithModule } from '@vitejs/test-resolve-exports-with-module'
text('.exports-with-module', exportsWithModule)

// imports field
import { msg as importsTopLevel } from '#top-level'
text('.imports-top-level', importsTopLevel)

import { msg as importsNested } from '#nested/path.js'
text('.imports-nested', importsNested)

import { msg as importsStar } from '#star/index.js'
text('.imports-star', importsStar)

import { msg as importsSlash } from '#slash/index.js'
text('.imports-slash', importsSlash)

import { msg as importsPkgSlash } from '#other-pkg-slash/index.js'
text('.imports-pkg-slash', importsPkgSlash)

// implicit index resolving
import { foo } from './util'
text('.index', foo())
Expand Down
10 changes: 9 additions & 1 deletion playground/resolve/package.json
Expand Up @@ -8,6 +8,13 @@
"debug": "node --inspect-brk ../../packages/vite/bin/vite",
"preview": "vite preview"
},
"imports": {
"#top-level": "./imports-path/top-level.js",
"#nested/path.js": "./imports-path/nested-path.js",
"#star/*": "./imports-path/star/*",
"#slash/": "./imports-path/slash/",
"#other-pkg-slash/": "@vitejs/test-resolve-imports-pkg/nest/"
},
"dependencies": {
"@babel/runtime": "^7.21.0",
"es5-ext": "0.10.62",
Expand All @@ -25,6 +32,7 @@
"@vitejs/test-resolve-exports-legacy-fallback": "link:./exports-legacy-fallback",
"@vitejs/test-resolve-exports-path": "link:./exports-path",
"@vitejs/test-resolve-exports-with-module": "link:./exports-with-module",
"@vitejs/test-resolve-linked": "workspace:*"
"@vitejs/test-resolve-linked": "workspace:*",
"@vitejs/test-resolve-imports-pkg": "link:./imports-path/other-pkg"
}
}
5 changes: 5 additions & 0 deletions pnpm-lock.yaml

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

0 comments on commit cc92da9

Please sign in to comment.