Skip to content
Open
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
65 changes: 64 additions & 1 deletion packages/vite/src/node/__tests__/external.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { fileURLToPath } from 'node:url'
import { describe, expect, test } from 'vitest'
import { resolveConfig } from '../config'
import { createIsConfiguredAsExternal } from '../external'
import { createIsConfiguredAsExternal, shouldExternalize } from '../external'
import { PartialEnvironment } from '../baseEnvironment'
import type { Environment } from '../environment'

describe('createIsConfiguredAsExternal', () => {
test('default', async () => {
Expand All @@ -16,6 +17,68 @@ describe('createIsConfiguredAsExternal', () => {
})
})

describe('shouldExternalize', () => {
test('same specifier with different importers is not cached as one entry', async () => {
// Regression test for https://github.com/vitejs/vite/issues/22078
//
// Before the fix, processedIds cached by bare specifier only. Calling
// shouldExternalize("foo", importerA) would cache the result, and a
// subsequent call with importerB would return the cached result from
// importerA without evaluating importerB's context.
//
// With external: true, both importers resolve to external, so we
// verify the function is callable with different importers and both
// invocations succeed independently (no cache corruption).
const resolvedConfig = await resolveConfig(
{
configFile: false,
root: fileURLToPath(new URL('./', import.meta.url)),
resolve: { external: true },
},
'serve',
)
const environment = new PartialEnvironment(
'ssr',
resolvedConfig,
) as unknown as Environment

// Call with two different importers — both should succeed
const result1 = shouldExternalize(
environment,
'@vitejs/cjs-ssr-dep',
'/some/importer-a.js',
)
const result2 = shouldExternalize(
environment,
'@vitejs/cjs-ssr-dep',
'/some/importer-b.js',
)

// Both resolve to true since external: true is set globally
expect(result1).toBe(true)
expect(result2).toBe(true)
})

test('relative and absolute specifiers bypass externalization', async () => {
const resolvedConfig = await resolveConfig(
{
configFile: false,
root: fileURLToPath(new URL('./', import.meta.url)),
resolve: { external: true },
},
'serve',
)
const environment = new PartialEnvironment(
'ssr',
resolvedConfig,
) as unknown as Environment

// Relative and absolute paths are never external
expect(shouldExternalize(environment, './foo', undefined)).toBe(false)
expect(shouldExternalize(environment, '/foo', undefined)).toBe(false)
})
})

async function createIsExternal(external?: true) {
const resolvedConfig = await resolveConfig(
{
Expand Down
11 changes: 8 additions & 3 deletions packages/vite/src/node/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,16 +132,21 @@ function createIsExternal(
const isConfiguredAsExternal = createIsConfiguredAsExternal(environment)

return (id: string, importer?: string) => {
if (processedIds.has(id)) {
return processedIds.get(id)!
// Cache key includes importer because different importers may resolve
// the same bare specifier to different packages (e.g. in pnpm monorepos
// with multiple versions of a dependency). Without importer in the key,
// the first resolution's result is incorrectly reused for all importers.
const cacheKey = importer ? `${id}\0${importer}` : id
if (processedIds.has(cacheKey)) {
return processedIds.get(cacheKey)!
}
let isExternal = false
if (id[0] !== '.' && !path.isAbsolute(id)) {
isExternal =
isBuiltin(environment.config.resolve.builtins, id) ||
isConfiguredAsExternal(id, importer)
}
processedIds.set(id, isExternal)
processedIds.set(cacheKey, isExternal)
return isExternal
}
}
Expand Down