From cc92da906da5b8656c42a5605d8c499b14e516d9 Mon Sep 17 00:00:00 2001 From: Wei Date: Mon, 6 Mar 2023 23:47:39 +0800 Subject: [PATCH] feat: support ESM subpath imports (#7770) --- packages/vite/src/node/packages.ts | 4 +- packages/vite/src/node/plugins/resolve.ts | 48 +++++++++++++++++-- playground/resolve/__tests__/resolve.spec.ts | 20 ++++++++ .../resolve/imports-path/nested-path.js | 1 + .../imports-path/other-pkg/nest/index.js | 1 + .../imports-path/other-pkg/package.json | 4 ++ .../resolve/imports-path/slash/index.js | 1 + playground/resolve/imports-path/star/index.js | 1 + playground/resolve/imports-path/top-level.js | 1 + playground/resolve/index.html | 31 ++++++++++++ playground/resolve/package.json | 10 +++- pnpm-lock.yaml | 5 ++ 12 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 playground/resolve/imports-path/nested-path.js create mode 100644 playground/resolve/imports-path/other-pkg/nest/index.js create mode 100644 playground/resolve/imports-path/other-pkg/package.json create mode 100644 playground/resolve/imports-path/slash/index.js create mode 100644 playground/resolve/imports-path/star/index.js create mode 100644 playground/resolve/imports-path/top-level.js diff --git a/packages/vite/src/node/packages.ts b/packages/vite/src/node/packages.ts index d1a78a96d2ded6..ca44b3aaabf01c 100644 --- a/packages/vite/src/node/packages.ts +++ b/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' @@ -27,7 +28,8 @@ export interface PackageData { main: string module: string browser: string | Record - exports: string | Record | string[] + exports: Exports + imports: Imports dependencies: Record } } diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index 9fd0ad00953e79..11db96a2898179 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -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 { @@ -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', { @@ -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 @@ -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 @@ -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) @@ -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, @@ -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 { diff --git a/playground/resolve/__tests__/resolve.spec.ts b/playground/resolve/__tests__/resolve.spec.ts index 4dd7c4fd068d34..f30fc3e9338140 100644 --- a/playground/resolve/__tests__/resolve.spec.ts +++ b/playground/resolve/__tests__/resolve.spec.ts @@ -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]') +}) diff --git a/playground/resolve/imports-path/nested-path.js b/playground/resolve/imports-path/nested-path.js new file mode 100644 index 00000000000000..a191fab4e01906 --- /dev/null +++ b/playground/resolve/imports-path/nested-path.js @@ -0,0 +1 @@ +export const msg = '[success] nested path subpath imports' diff --git a/playground/resolve/imports-path/other-pkg/nest/index.js b/playground/resolve/imports-path/other-pkg/nest/index.js new file mode 100644 index 00000000000000..e333ed38cad6df --- /dev/null +++ b/playground/resolve/imports-path/other-pkg/nest/index.js @@ -0,0 +1 @@ +export const msg = '[success] subpath imports from other package' diff --git a/playground/resolve/imports-path/other-pkg/package.json b/playground/resolve/imports-path/other-pkg/package.json new file mode 100644 index 00000000000000..6ba894e8ba3e49 --- /dev/null +++ b/playground/resolve/imports-path/other-pkg/package.json @@ -0,0 +1,4 @@ +{ + "name": "@vitejs/test-resolve-imports-pkg", + "private": true +} diff --git a/playground/resolve/imports-path/slash/index.js b/playground/resolve/imports-path/slash/index.js new file mode 100644 index 00000000000000..54eb4b30252bb8 --- /dev/null +++ b/playground/resolve/imports-path/slash/index.js @@ -0,0 +1 @@ +export const msg = '[success] subpath imports with slash' diff --git a/playground/resolve/imports-path/star/index.js b/playground/resolve/imports-path/star/index.js new file mode 100644 index 00000000000000..119e154dda3fd6 --- /dev/null +++ b/playground/resolve/imports-path/star/index.js @@ -0,0 +1 @@ +export const msg = '[success] subpath imports with star' diff --git a/playground/resolve/imports-path/top-level.js b/playground/resolve/imports-path/top-level.js new file mode 100644 index 00000000000000..861f792284931d --- /dev/null +++ b/playground/resolve/imports-path/top-level.js @@ -0,0 +1 @@ +export const msg = '[success] top level subpath imports' diff --git a/playground/resolve/index.html b/playground/resolve/index.html index e419419446837d..0966f050bbd580 100644 --- a/playground/resolve/index.html +++ b/playground/resolve/index.html @@ -36,6 +36,21 @@

Exports with legacy fallback

Exports with module

fail

+

Resolving top level with imports field

+

fail

+ +

Resolving nested path with imports field

+

fail

+ +

Resolving star with imports filed

+

fail

+ +

Resolving slash with imports filed

+

fail

+ +

Resolving from other package with imports field

+

fail

+

Resolve /index.*

fail

@@ -187,6 +202,22 @@

resolve package that contains # in path

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()) diff --git a/playground/resolve/package.json b/playground/resolve/package.json index c61d1f41bbdb37..6c0fd6f16afc86 100644 --- a/playground/resolve/package.json +++ b/playground/resolve/package.json @@ -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", @@ -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" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4aa0fefaa2d789..3403499bab01a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -809,6 +809,7 @@ importers: '@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-imports-pkg': link:./imports-path/other-pkg '@vitejs/test-resolve-linked': workspace:* es5-ext: 0.10.62 normalize.css: ^8.0.1 @@ -827,6 +828,7 @@ importers: '@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-imports-pkg': link:imports-path/other-pkg '@vitejs/test-resolve-linked': link:../resolve-linked es5-ext: 0.10.62 normalize.css: 8.0.1 @@ -893,6 +895,9 @@ importers: playground/resolve/exports-with-module: specifiers: {} + playground/resolve/imports-path/other-pkg: + specifiers: {} + playground/resolve/inline-package: specifiers: {}