Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support ESM subpath imports #7770

Merged
merged 2 commits into from
Mar 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/vite/src/node/packages.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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,
})
Comment on lines +159 to +161
Copy link
Member

Choose a reason for hiding this comment

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

Note to self. exports only work for root package.json, but node decided that imports doesn't need to and works for the closest package.json, so this is fine. Stackblitz

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const msg = '[success] subpath imports with slash'
1 change: 1 addition & 0 deletions playground/resolve/imports-path/star/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const msg = '[success] subpath imports with star'
1 change: 1 addition & 0 deletions playground/resolve/imports-path/top-level.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const msg = '[success] top level subpath imports'
31 changes: 31 additions & 0 deletions playground/resolve/index.html
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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.20.13",
"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.