Skip to content

Commit

Permalink
feat: support ESM subpath imports
Browse files Browse the repository at this point in the history
  • Loading branch information
fi3ework committed Apr 17, 2022
1 parent 9a93233 commit 7acab6a
Show file tree
Hide file tree
Showing 12 changed files with 258 additions and 10 deletions.
20 changes: 20 additions & 0 deletions packages/playground/resolve/__tests__/resolve.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,26 @@ test('Respect production/development conditionals', async () => {
)
})

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]')
})

test('implicit dir/index.js', async () => {
expect(await page.textContent('.index')).toMatch('[success]')
})
Expand Down
1 change: 1 addition & 0 deletions packages/playground/resolve/imports-path/a.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const msg = '[success] ./a.js'
1 change: 1 addition & 0 deletions packages/playground/resolve/imports-path/b.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const msg = '[success] ./b.js'
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "resolve-imports-pkg",
"private": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const msg = '[success] some-pkg/some-slash/d.js'
1 change: 1 addition & 0 deletions packages/playground/resolve/imports-path/some-slash/d.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const msg = '[success] ./some-slash/d.js'
1 change: 1 addition & 0 deletions packages/playground/resolve/imports-path/some-star/c.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const msg = '[success] ./some-star/c.js'
31 changes: 31 additions & 0 deletions packages/playground/resolve/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ <h2>Deep import with exports field + mapped directory</h2>
<h2>Exports field env priority</h2>
<p class="exports-env">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 @@ -132,6 +147,22 @@ <h2>resolve package that contains # in path</h2>
import { msg as exportsEnvMsg } from 'resolve-exports-env'
text('.exports-env', exportsEnvMsg)

// 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/c.js'
text('.imports-star', importsStar)

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

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

// implicit index resolving
import { foo } from './util'
text('.index', foo())
Expand Down
10 changes: 9 additions & 1 deletion packages/playground/resolve/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
"debug": "node --inspect-brk ../../vite/bin/vite",
"preview": "vite preview"
},
"imports": {
"#top-level": "./imports-path/a.js",
"#nested/path.js": "./imports-path/b.js",
"#star/*": "./imports-path/some-star/*",
"#slash/": "./imports-path/some-slash/",
"#other-pkg-slash/": "resolve-imports-pkg/some-slash/"
},
"dependencies": {
"@babel/runtime": "^7.16.0",
"es5-ext": "0.10.53",
Expand All @@ -18,6 +25,7 @@
"resolve-custom-main-field": "link:./custom-main-field",
"resolve-exports-env": "link:./exports-env",
"resolve-exports-path": "link:./exports-path",
"resolve-linked": "workspace:*"
"resolve-linked": "workspace:*",
"resolve-imports-pkg": "link:./imports-path/other-pkg"
}
}
1 change: 1 addition & 0 deletions packages/vite/src/node/packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface PackageData {
module: string
browser: string | Record<string, string | false>
exports: string | Record<string, any> | string[]
imports: Record<string, any>
dependencies: Record<string, string>
}
}
Expand Down
192 changes: 183 additions & 9 deletions packages/vite/src/node/plugins/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ import {
isFileReadable,
isTsRequest,
isPossibleTsOutput,
getPotentialTsSrcPaths
getPotentialTsSrcPaths,
lookupFile
} from '../utils'
import {
createIsOptimizedDepUrl,
Expand Down Expand Up @@ -100,6 +101,10 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
let isOptimizedDepUrl: (url: string) => boolean

const { target: ssrTarget, noExternal: ssrNoExternal } = ssrConfig ?? {}
const rootPkgContent = lookupFile(root, [`package.json`])
const packageJson = rootPkgContent
? (JSON.parse(rootPkgContent) as PackageData['data'])
: null

return {
name: 'vite:resolve',
Expand Down Expand Up @@ -134,6 +139,21 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
scan: resolveOpts?.scan ?? baseOptions.scan
}

// imports field
if (packageJson?.imports) {
const entryPoint = resolveExportsOrImports({
pkgJson: packageJson,
key: id,
options,
targetWeb,
type: 'imports'
})

if (entryPoint) {
id = entryPoint
}
}

let res: string | PartialResolvedId | undefined

// resolve pre-bundled deps requests, these could be resolved by
Expand Down Expand Up @@ -166,6 +186,21 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
}
}

// imports field
if (packageJson?.imports) {
const entryPoint = resolveExportsOrImports({
pkgJson: packageJson,
key: id,
options,
targetWeb,
type: 'imports'
})

if (entryPoint) {
id = entryPoint
}
}

// relative
if (id.startsWith('.') || (preferRelative && /^\w/.test(id))) {
const basedir = importer ? path.dirname(importer) : process.cwd()
Expand Down Expand Up @@ -727,7 +762,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({
pkgJson: data,
key: '.',
options,
targetWeb,
type: 'exports'
})
}

// if exports resolved to .mjs, still resolve other fields.
Expand Down Expand Up @@ -834,12 +875,19 @@ function packageEntryFailure(id: string, details?: string) {
)
}

function resolveExports(
pkg: PackageData['data'],
key: string,
options: InternalResolveOptions,
function resolveExportsOrImports({
pkgJson,
key,
options,
targetWeb,
type
}: {
pkgJson: PackageData['data']
key: string
options: InternalResolveOptions
targetWeb: boolean
) {
type: 'imports' | 'exports'
}) {
const conditions = [options.isProduction ? 'production' : 'development']
if (!options.isRequire) {
conditions.push('module')
Expand All @@ -848,7 +896,9 @@ function resolveExports(
conditions.push(...options.conditions)
}

return _resolveExports(pkg, key, {
const method = type === 'exports' ? _resolveExports : _resolveImports

return method(pkgJson, key, {
browser: targetWeb,
require: options.isRequire,
conditions
Expand Down Expand Up @@ -880,7 +930,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({
pkgJson: data,
key: file,
options,
targetWeb,
type: 'exports'
})
if (exportsId !== undefined) {
relativeId = exportsId + postfix
} else {
Expand Down Expand Up @@ -990,3 +1046,121 @@ function getRealPath(resolved: string, preserveSymlinks?: boolean): string {
}
return normalizePath(resolved)
}

// #region Simple implementation of resolve.imports by forking resolve.exports
// we can remove this region after resolve.exports supports imports algorithm
// https://github.com/lukeed/resolve.exports/issues/14
function loop(
imports: string | Record<string, string>,
keys: Set<string>
): string | void {
if (typeof imports === 'string') {
return imports
}

if (imports) {
let idx, tmp
if (Array.isArray(imports)) {
for (idx = 0; idx < imports.length; idx++) {
if ((tmp = loop(imports[idx], keys))) return tmp
}
} else {
for (idx in imports) {
if (keys.has(idx)) {
return loop(imports[idx], keys)
}
}
}
}
}

function bail(name: string, entry: string, condition?: number) {
throw new Error(
condition
? `No known conditions for "${entry}" entry in "${name}" package`
: `Missing "${entry}" import in "${name}" package`
)
}

function toName(name: string, entry: string) {
return entry === name
? '.'
: entry[0] === '.'
? entry
: entry.replace(new RegExp('^' + name + '/'), './')
}

function _resolveImports(
pkg: PackageData['data'],
entry: string,
options:
| {
browser?: boolean
conditions?: readonly string[]
require?: boolean
unsafe?: false
}
| {
conditions?: readonly string[]
unsafe?: true
} = {}
): string | void {
const { name, imports } = pkg

if (imports) {
const { unsafe, conditions = [] } = options

const target = toName(name, entry)
// if (target[0] !== '.') target = './' + target;

if (typeof imports === 'string') {
return target === '#' ? imports : bail(name, target)
}

const allows = new Set(['default', ...conditions])
unsafe || allows.add((options as any).require ? 'require' : 'import')
unsafe || allows.add((options as any).browser ? 'browser' : 'node')

let key,
tmp,
isSingle = false

for (key in imports) {
isSingle = key[0] !== '#'
break
}

if (isSingle) {
return target === '#'
? loop(imports, allows) || bail(name, target, 1)
: bail(name, target)
}

if ((tmp = imports[target])) {
return loop(tmp, allows) || bail(name, target, 1)
}

for (key in imports) {
tmp = key[key.length - 1]
if (tmp === '/' && target.startsWith(key)) {
const _tmp = loop(imports[key], allows)
return _tmp
? _tmp + target.substring(key.length)
: bail(name, target, 1)
}
if (tmp === '*' && target.startsWith(key.slice(0, -1))) {
// do not trigger if no *content* to inject
if (target.substring(key.length - 1).length > 0) {
const _tmp = loop(imports[key], allows)
return _tmp
? _tmp.replace('*', target.substring(key.length - 1))
: bail(name, target, 1)
}
}
}

// return bail(name, target)
return undefined
}
}
// #endregion
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 7acab6a

Please sign in to comment.