Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "resolve-subpath-imports",
"imports": {
"#hash": "./public.js",
"#hash#blocked": null,
"#hash?blocked": null
}
}
18 changes: 18 additions & 0 deletions packages/vite/src/node/__tests__/resolve.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,27 @@ import { join } from 'node:path'
import { describe, expect, onTestFinished, test, vi } from 'vitest'
import { build } from '../build'
import type { EnvironmentOptions, InlineConfig } from '../config'
import { resolveSubpathImports } from '../plugins/resolve'
import { createServer } from '../server'
import { createServerModuleRunner } from '../ssr/runtime/serverModuleRunner'

for (const id of ['#hash#blocked', '#hash?blocked']) {
test(`does not fall back from explicitly blocked imports key ${id}`, () => {
expect(() =>
resolveSubpathImports(
id,
join(import.meta.dirname, 'fixtures/resolve-subpath-imports/index.js'),
{
conditions: [],
externalConditions: [],
isProduction: false,
isRequire: false,
},
),
).toThrow(`Missing "${id}" specifier`)
})
}

describe('import and resolveId', () => {
async function createTestServer() {
const server = await createServer({
Expand Down
45 changes: 35 additions & 10 deletions packages/vite/src/node/plugins/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,16 +505,41 @@ export function resolveSubpathImports(
const basedir = path.dirname(importer)
const pkgData = findNearestPackageData(basedir, options.packageCache)
if (!pkgData) return

let { file: idWithoutPostfix, postfix } = splitFileAndPostfix(id.slice(1))
idWithoutPostfix = '#' + idWithoutPostfix

let importsPath = resolveExportsOrImports(
pkgData.data,
idWithoutPostfix,
options,
'imports',
)
// Imports keys may themselves contain `#` or `?` (e.g. `##/*`, `#a?b`).
// Try the full id first, then each query or hash boundary from right to left
// so that the longest matching key wins (#16085, #23622).
let importsPath: string | undefined
let postfix = ''
let fullIdError: unknown
for (
let postfixIndex = id.length;
postfixIndex > 0;
postfixIndex = Math.max(
id.lastIndexOf('?', postfixIndex - 1),
id.lastIndexOf('#', postfixIndex - 1),
)
) {
const key = id.slice(0, postfixIndex)
try {
importsPath = resolveExportsOrImports(
pkgData.data,
key,
options,
'imports',
)
if (importsPath !== undefined) {
postfix = id.slice(postfixIndex)
break
}
} catch (e) {
if (Object.hasOwn(pkgData.data.imports, key)) throw e
// resolve.exports throws when no key matches; keep the full id error.
fullIdError ??= e
}
Comment thread
btea marked this conversation as resolved.
}
if (importsPath === undefined && fullIdError) {
throw fullIdError
}

if (importsPath?.[0] === '.') {
importsPath = path.relative(basedir, path.join(pkgData.dir, importsPath))
Expand Down
40 changes: 40 additions & 0 deletions playground/resolve/__tests__/resolve.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,46 @@ test('Resolving #/ root alias pattern with imports field', async () => {
expect(await page.textContent('.imports-root-slash')).toMatch('[success]')
})

test('Resolving ## prefixed pattern with imports field', async () => {
// subpath imports keys may contain more than one leading `#` (e.g. `##/*`)
expect(await page.textContent('.imports-double-hash')).toMatch('[success]')
})

test('Resolving key containing # with imports field', async () => {
// subpath imports keys may contain `#` outside the leading position
expect(await page.textContent('.imports-mid-hash')).toMatch('[success]')
})

test('Resolving key containing ? with imports field', async () => {
// subpath imports keys may contain `?` and are matched literally,
// taking precedence over stripping it as a query postfix
expect(await page.textContent('.imports-question-mark')).toMatch('[success]')
})

test('Resolving with hash postfix with imports field', async () => {
expect(await page.textContent('.imports-hash-hash')).toBe(
'[success] subpath imports #hash',
)
})

test('Resolving #hash?query with imports field', async () => {
expect(await page.textContent('.imports-hash-query')).toBe(
'[success] subpath imports #hash',
)
})

test('Resolving #mid#hash#fragment?query with imports field', async () => {
expect(await page.textContent('.imports-mid-hash-postfix')).toBe(
'[success] subpath imports with # inside key',
)
})

test('Resolving #question?mark?query#fragment with imports field', async () => {
expect(await page.textContent('.imports-question-mark-postfix')).toBe(
'[success] subpath imports with ? inside key',
)
})

test("Resolve doesn't interrupt page request with trailing query and .css", async () => {
await page.goto(viteTestUrl + '/?test.css')
expect(await page.locator('vite-error-overlay').count()).toBe(0)
Expand Down
1 change: 1 addition & 0 deletions playground/resolve/imports-path/double-hash/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const msg = '[success] subpath imports with ## prefixed pattern'
1 change: 1 addition & 0 deletions playground/resolve/imports-path/hash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const msg = '[success] subpath imports #hash'
3 changes: 3 additions & 0 deletions playground/resolve/imports-path/importer-hash-hash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { msg } from '#hash#hash'

export { msg }
1 change: 1 addition & 0 deletions playground/resolve/imports-path/mid-hash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const msg = '[success] subpath imports with # inside key'
1 change: 1 addition & 0 deletions playground/resolve/imports-path/question-mark.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const msg = '[success] subpath imports with ? inside key'
42 changes: 42 additions & 0 deletions playground/resolve/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,27 @@ <h2>Resolving dot-prefixed directory with imports field</h2>
<h2>Resolving #/ root alias pattern with imports field</h2>
<p class="imports-root-slash">fail</p>

<h2>Resolving ## prefixed pattern with imports field</h2>
<p class="imports-double-hash">fail</p>

<h2>Resolving key containing # with imports field</h2>
<p class="imports-mid-hash">fail</p>

<h2>Resolving key containing ? with imports field</h2>
<p class="imports-question-mark">fail</p>

<h2>Resolving with hash postfix with imports field</h2>
<p class="imports-hash-hash">fail</p>

<h2>Resolving #hash?query with imports field</h2>
<p class="imports-hash-query">fail</p>

<h2>Resolving #mid#hash#fragment?query with imports field</h2>
<p class="imports-mid-hash-postfix">fail</p>

<h2>Resolving #question?mark?query#fragment with imports field</h2>
<p class="imports-question-mark-postfix">fail</p>

<h2>Resolve /index.*</h2>
<p class="index">fail</p>

Expand Down Expand Up @@ -279,6 +300,27 @@ <h2>utf8-bom-package</h2>
import { msg as importsRootSlash } from '#/index.js'
text('.imports-root-slash', importsRootSlash)

import { msg as importsDoubleHash } from '##double/index.js'
text('.imports-double-hash', importsDoubleHash)

import { msg as importsMidHash } from '#mid#hash'
text('.imports-mid-hash', importsMidHash)

import { msg as importsQuestionMark } from '#question?mark'
text('.imports-question-mark', importsQuestionMark)

import { msg as importsHashHash } from './imports-path/importer-hash-hash.js'
text('.imports-hash-hash', importsHashHash)

import { msg as importsHashQuery } from '#hash?query'
text('.imports-hash-query', importsHashQuery)

import { msg as importsMidHashPostfix } from '#mid#hash#fragment?query'
text('.imports-mid-hash-postfix', importsMidHashPostfix)

import { msg as importsQuestionMarkPostfix } from '#question?mark?query#fragment'
text('.imports-question-mark-postfix', importsQuestionMarkPostfix)

// implicit index resolving
import { foo } from './util'
text('.index', foo())
Expand Down
6 changes: 5 additions & 1 deletion playground/resolve/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
"#other-pkg-slash/": "@vitejs/test-resolve-imports-pkg/nest/",
"#query": "./imports-path/query.json",
"#dot-prefixed": "./imports-path/.dot-prefixed/index.js",
"#/*": "./imports-path/root-slash/*"
"#/*": "./imports-path/root-slash/*",
"##double/*": "./imports-path/double-hash/*",
"#mid#hash": "./imports-path/mid-hash.js",
"#question?mark": "./imports-path/question-mark.js",
"#hash": "./imports-path/hash.js"
},
"dependencies": {
"@babel/runtime": "^7.29.7",
Expand Down
Loading