Skip to content

Commit cc92da9

Browse files
authoredMar 6, 2023
feat: support ESM subpath imports (#7770)
1 parent cde9191 commit cc92da9

File tree

12 files changed

+120
-7
lines changed

12 files changed

+120
-7
lines changed
 

‎packages/vite/src/node/packages.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs from 'node:fs'
22
import path from 'node:path'
3+
import type { Exports, Imports } from 'resolve.exports'
34
import { createDebugger, createFilter, resolveFrom } from './utils'
45
import type { ResolvedConfig } from './config'
56
import type { Plugin } from './plugin'
@@ -27,7 +28,8 @@ export interface PackageData {
2728
main: string
2829
module: string
2930
browser: string | Record<string, string | false>
30-
exports: string | Record<string, any> | string[]
31+
exports: Exports
32+
imports: Imports
3133
dependencies: Record<string, string>
3234
}
3335
}

‎packages/vite/src/node/plugins/resolve.ts

+43-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import fs from 'node:fs'
22
import path from 'node:path'
33
import colors from 'picocolors'
44
import type { PartialResolvedId } from 'rollup'
5-
import { exports } from 'resolve.exports'
5+
import { exports, imports } from 'resolve.exports'
66
import { hasESMSyntax } from 'mlly'
77
import type { Plugin } from '../plugin'
88
import {
@@ -55,6 +55,7 @@ export const browserExternalId = '__vite-browser-external'
5555
export const optionalPeerDepId = '__vite-optional-peer-dep'
5656

5757
const nodeModulesInPathRE = /(?:^|\/)node_modules\//
58+
const subpathImportsPrefix = '#'
5859

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

156+
const resolveSubpathImports = (id: string, importer?: string) => {
157+
if (!importer || !id.startsWith(subpathImportsPrefix)) return
158+
const basedir = path.dirname(importer)
159+
const pkgJsonPath = lookupFile(basedir, ['package.json'], {
160+
pathOnly: true,
161+
})
162+
if (!pkgJsonPath) return
163+
164+
const pkgData = loadPackageData(pkgJsonPath, options.preserveSymlinks)
165+
return resolveExportsOrImports(
166+
pkgData.data,
167+
id,
168+
options,
169+
targetWeb,
170+
'imports',
171+
)
172+
}
173+
174+
const resolvedImports = resolveSubpathImports(id, importer)
175+
if (resolvedImports) {
176+
id = resolvedImports
177+
}
178+
155179
if (importer) {
156180
const _importer = isWorkerRequest(importer)
157181
? splitFileAndPostfix(importer).file
@@ -958,7 +982,13 @@ export function resolvePackageEntry(
958982
// resolve exports field with highest priority
959983
// using https://github.com/lukeed/resolve.exports
960984
if (data.exports) {
961-
entryPoint = resolveExports(data, '.', options, targetWeb)
985+
entryPoint = resolveExportsOrImports(
986+
data,
987+
'.',
988+
options,
989+
targetWeb,
990+
'exports',
991+
)
962992
}
963993

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

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

1079-
function resolveExports(
1109+
function resolveExportsOrImports(
10801110
pkg: PackageData['data'],
10811111
key: string,
10821112
options: InternalResolveOptionsWithOverrideConditions,
10831113
targetWeb: boolean,
1114+
type: 'imports' | 'exports',
10841115
) {
10851116
const overrideConditions = options.overrideConditions
10861117
? new Set(options.overrideConditions)
@@ -1115,7 +1146,8 @@ function resolveExports(
11151146
conditions.push(...options.conditions)
11161147
}
11171148

1118-
const result = exports(pkg, key, {
1149+
const fn = type === 'imports' ? imports : exports
1150+
const result = fn(pkg, key, {
11191151
browser: targetWeb && !conditions.includes('node'),
11201152
require: options.isRequire && !conditions.includes('import'),
11211153
conditions,
@@ -1149,7 +1181,13 @@ function resolveDeepImport(
11491181
if (isObject(exportsField) && !Array.isArray(exportsField)) {
11501182
// resolve without postfix (see #7098)
11511183
const { file, postfix } = splitFileAndPostfix(relativeId)
1152-
const exportsId = resolveExports(data, file, options, targetWeb)
1184+
const exportsId = resolveExportsOrImports(
1185+
data,
1186+
file,
1187+
options,
1188+
targetWeb,
1189+
'exports',
1190+
)
11531191
if (exportsId !== undefined) {
11541192
relativeId = exportsId + postfix
11551193
} else {

‎playground/resolve/__tests__/resolve.spec.ts

+20
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,23 @@ test('resolve package that contains # in path', async () => {
158158
'[success]',
159159
)
160160
})
161+
162+
test('Resolving top level with imports field', async () => {
163+
expect(await page.textContent('.imports-top-level')).toMatch('[success]')
164+
})
165+
166+
test('Resolving nested path with imports field', async () => {
167+
expect(await page.textContent('.imports-nested')).toMatch('[success]')
168+
})
169+
170+
test('Resolving star with imports filed', async () => {
171+
expect(await page.textContent('.imports-star')).toMatch('[success]')
172+
})
173+
174+
test('Resolving slash with imports filed', async () => {
175+
expect(await page.textContent('.imports-slash')).toMatch('[success]')
176+
})
177+
178+
test('Resolving from other package with imports field', async () => {
179+
expect(await page.textContent('.imports-pkg-slash')).toMatch('[success]')
180+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const msg = '[success] nested path subpath imports'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const msg = '[success] subpath imports from other package'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "@vitejs/test-resolve-imports-pkg",
3+
"private": true
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const msg = '[success] subpath imports with slash'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const msg = '[success] subpath imports with star'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const msg = '[success] top level subpath imports'

‎playground/resolve/index.html

+31
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,21 @@ <h2>Exports with legacy fallback</h2>
3636
<h2>Exports with module</h2>
3737
<p class="exports-with-module">fail</p>
3838

39+
<h2>Resolving top level with imports field</h2>
40+
<p class="imports-top-level">fail</p>
41+
42+
<h2>Resolving nested path with imports field</h2>
43+
<p class="imports-nested">fail</p>
44+
45+
<h2>Resolving star with imports filed</h2>
46+
<p class="imports-star">fail</p>
47+
48+
<h2>Resolving slash with imports filed</h2>
49+
<p class="imports-slash">fail</p>
50+
51+
<h2>Resolving from other package with imports field</h2>
52+
<p class="imports-pkg-slash">fail</p>
53+
3954
<h2>Resolve /index.*</h2>
4055
<p class="index">fail</p>
4156

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

205+
// imports field
206+
import { msg as importsTopLevel } from '#top-level'
207+
text('.imports-top-level', importsTopLevel)
208+
209+
import { msg as importsNested } from '#nested/path.js'
210+
text('.imports-nested', importsNested)
211+
212+
import { msg as importsStar } from '#star/index.js'
213+
text('.imports-star', importsStar)
214+
215+
import { msg as importsSlash } from '#slash/index.js'
216+
text('.imports-slash', importsSlash)
217+
218+
import { msg as importsPkgSlash } from '#other-pkg-slash/index.js'
219+
text('.imports-pkg-slash', importsPkgSlash)
220+
190221
// implicit index resolving
191222
import { foo } from './util'
192223
text('.index', foo())

‎playground/resolve/package.json

+9-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88
"debug": "node --inspect-brk ../../packages/vite/bin/vite",
99
"preview": "vite preview"
1010
},
11+
"imports": {
12+
"#top-level": "./imports-path/top-level.js",
13+
"#nested/path.js": "./imports-path/nested-path.js",
14+
"#star/*": "./imports-path/star/*",
15+
"#slash/": "./imports-path/slash/",
16+
"#other-pkg-slash/": "@vitejs/test-resolve-imports-pkg/nest/"
17+
},
1118
"dependencies": {
1219
"@babel/runtime": "^7.21.0",
1320
"es5-ext": "0.10.62",
@@ -25,6 +32,7 @@
2532
"@vitejs/test-resolve-exports-legacy-fallback": "link:./exports-legacy-fallback",
2633
"@vitejs/test-resolve-exports-path": "link:./exports-path",
2734
"@vitejs/test-resolve-exports-with-module": "link:./exports-with-module",
28-
"@vitejs/test-resolve-linked": "workspace:*"
35+
"@vitejs/test-resolve-linked": "workspace:*",
36+
"@vitejs/test-resolve-imports-pkg": "link:./imports-path/other-pkg"
2937
}
3038
}

‎pnpm-lock.yaml

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.