From ba3e478648148b9201ed7cc62921a325cf17e736 Mon Sep 17 00:00:00 2001
From: fi3ework <fi3ework@gmail.com>
Date: Mon, 30 Jan 2023 01:46:24 +0800
Subject: [PATCH 1/2] feat: support subpath imports

---
 packages/vite/src/node/packages.ts            |  4 +-
 packages/vite/src/node/plugins/resolve.ts     | 47 +++++++++++++++++--
 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, 119 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<string, string | false>
-    exports: string | Record<string, any> | string[]
+    exports: Exports
+    imports: Imports
     dependencies: Record<string, string>
   }
 }
diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts
index 1976de78f64258..9e358d666e4a9d 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,28 @@ 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 importerPkgJson = lookupFile(basedir, ['package.json'], {
+          predicate: (content) => !!JSON.parse(content).name,
+        })
+        if (!importerPkgJson) return
+
+        return resolveExportsOrImports(
+          JSON.parse(importerPkgJson),
+          id,
+          options,
+          targetWeb,
+          'imports',
+        )
+      }
+
+      const resolvedImports = resolveSubpathImports(id, importer)
+      if (resolvedImports) {
+        id = resolvedImports
+      }
+
       if (importer) {
         const _importer = isWorkerRequest(importer)
           ? splitFileAndPostfix(importer).file
@@ -958,7 +981,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 +1105,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 +1145,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 +1180,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 d08947c398980f..03af89c8008dde 100644
--- a/playground/resolve/index.html
+++ b/playground/resolve/index.html
@@ -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>
 
@@ -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())
diff --git a/playground/resolve/package.json b/playground/resolve/package.json
index 63ac4e0358fb3e..07c0733db269b5 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.20.13",
     "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 d7c1c4e2cc4dd2..5bcf9344d2cc15 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
@@ -882,6 +884,9 @@ importers:
   playground/resolve/exports-with-module:
     specifiers: {}
 
+  playground/resolve/imports-path/other-pkg:
+    specifiers: {}
+
   playground/resolve/inline-package:
     specifiers: {}
 

From 3a91f90521b1883b36c1d42988a4877796401475 Mon Sep 17 00:00:00 2001
From: fi3ework <fi3ework@gmail.com>
Date: Thu, 23 Feb 2023 02:17:33 +0800
Subject: [PATCH 2/2] refactor: use loadPackageData

---
 packages/vite/src/node/plugins/resolve.ts | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts
index 9e358d666e4a9d..78e18185c6c494 100644
--- a/packages/vite/src/node/plugins/resolve.ts
+++ b/packages/vite/src/node/plugins/resolve.ts
@@ -156,13 +156,14 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin {
       const resolveSubpathImports = (id: string, importer?: string) => {
         if (!importer || !id.startsWith(subpathImportsPrefix)) return
         const basedir = path.dirname(importer)
-        const importerPkgJson = lookupFile(basedir, ['package.json'], {
-          predicate: (content) => !!JSON.parse(content).name,
+        const pkgJsonPath = lookupFile(basedir, ['package.json'], {
+          pathOnly: true,
         })
-        if (!importerPkgJson) return
+        if (!pkgJsonPath) return
 
+        const pkgData = loadPackageData(pkgJsonPath, options.preserveSymlinks)
         return resolveExportsOrImports(
-          JSON.parse(importerPkgJson),
+          pkgData.data,
           id,
           options,
           targetWeb,