From 71731f92bbd45fb05449fb28e35612ba8a70c086 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 1 Dec 2025 18:31:25 +0900 Subject: [PATCH 1/4] fix: allow inlining fully dynamic import --- .../src/node/environments/fetchModule.ts | 9 ++++++++ pnpm-lock.yaml | 8 +++++++ test/config/deps/test-dep-virtual/index.js | 1 + .../config/deps/test-dep-virtual/package.json | 6 +++++ .../vite-ssr-resolve/inline-dep/package.json | 1 + .../vite-ssr-resolve/other-dep/package.json | 1 + .../ssr-no-external-dep/package.json | 1 + .../fixtures/external/dynamic/basic.test.ts | 7 ++++++ .../external/dynamic/vitest.config.ts | 22 +++++++++++++++++++ test/config/package.json | 1 + test/config/test/external.test.ts | 15 +++++++++++++ 11 files changed, 72 insertions(+) create mode 100644 test/config/deps/test-dep-virtual/index.js create mode 100644 test/config/deps/test-dep-virtual/package.json create mode 100644 test/config/fixtures/external/dynamic/basic.test.ts create mode 100644 test/config/fixtures/external/dynamic/vitest.config.ts create mode 100644 test/config/test/external.test.ts diff --git a/packages/vitest/src/node/environments/fetchModule.ts b/packages/vitest/src/node/environments/fetchModule.ts index 5da42d3a65f6..127f994beef9 100644 --- a/packages/vitest/src/node/environments/fetchModule.ts +++ b/packages/vitest/src/node/environments/fetchModule.ts @@ -12,6 +12,7 @@ import { isExternalUrl, unwrapId } from '@vitest/utils/helpers' import { join } from 'pathe' import { fetchModule } from 'vite' import { hash } from '../hash' +import { normalizeResolvedIdToUrl } from './normalizeUrl' const saveCachePromises = new Map>() const readFilePromises = new Map>() @@ -55,6 +56,14 @@ class ModuleFetcher { return { externalize: url, type: 'network' } } + // handle unresolved id of dynamic import skipped by Vite import analysis + if (url[0] !== '/') { + const resolved = await environment.pluginContainer.resolveId(url, importer) + if (resolved) { + url = normalizeResolvedIdToUrl(environment, resolved.id) + } + } + const moduleGraphModule = await environment.moduleGraph.ensureEntryFromUrl(unwrapId(url)) const cached = !!moduleGraphModule.transformResult diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 996c999e3e5a..f9bcd0e36150 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1214,6 +1214,9 @@ importers: '@test/test-dep-config': specifier: link:./deps/test-dep-config version: link:deps/test-dep-config + '@vitejs/test-dep-virtual': + specifier: file:./deps/test-dep-virtual + version: file:test/config/deps/test-dep-virtual '@vitest/browser-playwright': specifier: workspace:* version: link:../../packages/browser-playwright @@ -4679,6 +4682,9 @@ packages: vite: ^7.1.5 vue: ^3.2.25 + '@vitejs/test-dep-virtual@file:test/config/deps/test-dep-virtual': + resolution: {directory: test/config/deps/test-dep-virtual, type: directory} + '@vitest/cjs-lib@file:test/browser/cjs-lib': resolution: {directory: test/browser/cjs-lib, type: directory} @@ -12837,6 +12843,8 @@ snapshots: vite: 7.1.5(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vue: 3.5.25(typescript@5.9.3) + '@vitejs/test-dep-virtual@file:test/config/deps/test-dep-virtual': {} + '@vitest/cjs-lib@file:test/browser/cjs-lib': {} '@vitest/eslint-plugin@1.4.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@packages+vitest)': diff --git a/test/config/deps/test-dep-virtual/index.js b/test/config/deps/test-dep-virtual/index.js new file mode 100644 index 000000000000..5d93e91ca740 --- /dev/null +++ b/test/config/deps/test-dep-virtual/index.js @@ -0,0 +1 @@ +export * from 'virtual:test' diff --git a/test/config/deps/test-dep-virtual/package.json b/test/config/deps/test-dep-virtual/package.json new file mode 100644 index 000000000000..1cb608531663 --- /dev/null +++ b/test/config/deps/test-dep-virtual/package.json @@ -0,0 +1,6 @@ +{ + "name": "@vitejs/test-dep-virtual", + "type": "module", + "private": true, + "exports": "./index.js" +} diff --git a/test/config/deps/vite-ssr-resolve/inline-dep/package.json b/test/config/deps/vite-ssr-resolve/inline-dep/package.json index e60fe29aab1d..e3b343b74bf9 100644 --- a/test/config/deps/vite-ssr-resolve/inline-dep/package.json +++ b/test/config/deps/vite-ssr-resolve/inline-dep/package.json @@ -1,5 +1,6 @@ { "name": "inline-dep", "type": "module", + "private": true, "exports": "./index.js" } diff --git a/test/config/deps/vite-ssr-resolve/other-dep/package.json b/test/config/deps/vite-ssr-resolve/other-dep/package.json index b41039228d90..c4f06a9a9a69 100644 --- a/test/config/deps/vite-ssr-resolve/other-dep/package.json +++ b/test/config/deps/vite-ssr-resolve/other-dep/package.json @@ -1,5 +1,6 @@ { "name": "other-dep", "type": "module", + "private": true, "exports": "./index.js" } diff --git a/test/config/deps/vite-ssr-resolve/ssr-no-external-dep/package.json b/test/config/deps/vite-ssr-resolve/ssr-no-external-dep/package.json index d1b7fd723c74..68a203442474 100644 --- a/test/config/deps/vite-ssr-resolve/ssr-no-external-dep/package.json +++ b/test/config/deps/vite-ssr-resolve/ssr-no-external-dep/package.json @@ -1,5 +1,6 @@ { "name": "ssr-no-external-dep", "type": "module", + "private": true, "exports": "./index.js" } diff --git a/test/config/fixtures/external/dynamic/basic.test.ts b/test/config/fixtures/external/dynamic/basic.test.ts new file mode 100644 index 000000000000..e4044fb92fa4 --- /dev/null +++ b/test/config/fixtures/external/dynamic/basic.test.ts @@ -0,0 +1,7 @@ +import { expect, it } from "vitest"; + +it("basic", async () => { + const getId = () => "@vitejs/test-dep-virtual"; + const mod = await import(getId()); + expect(mod.test).toBe("ok"); +}); diff --git a/test/config/fixtures/external/dynamic/vitest.config.ts b/test/config/fixtures/external/dynamic/vitest.config.ts new file mode 100644 index 000000000000..2eeb1fa2789f --- /dev/null +++ b/test/config/fixtures/external/dynamic/vitest.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + ssr: { + noExternal: ["@vitejs/test-dep-virtual"], + }, + plugins: [ + { + name: 'test-virtual', + resolveId(source) { + if (source === 'virtual:test') { + return '\0' + source; + } + }, + load(id) { + if (id === '\0virtual:test') { + return `export const test = "ok";`; + } + } + } + ], +}); diff --git a/test/config/package.json b/test/config/package.json index 984aed97c50f..b99cea7333ad 100644 --- a/test/config/package.json +++ b/test/config/package.json @@ -7,6 +7,7 @@ }, "devDependencies": { "@test/test-dep-config": "link:./deps/test-dep-config", + "@vitejs/test-dep-virtual": "file:./deps/test-dep-virtual", "@vitest/browser-playwright": "workspace:*", "@vitest/browser-preview": "workspace:*", "@vitest/browser-webdriverio": "workspace:*", diff --git a/test/config/test/external.test.ts b/test/config/test/external.test.ts new file mode 100644 index 000000000000..26850e286332 --- /dev/null +++ b/test/config/test/external.test.ts @@ -0,0 +1,15 @@ +import { expect, it } from 'vitest' +import { runVitest } from '../../test-utils' + +it('can inline fully dynamic import', async () => { + const { errorTree } = await runVitest({ + root: 'fixtures/external/dynamic', + }) + expect(errorTree()).toMatchInlineSnapshot(` + { + "basic.test.ts": { + "basic": "passed", + }, + } + `) +}) From 2d4ddece670bf596bb1c70e198a91f1b87ee733c Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 1 Dec 2025 18:33:06 +0900 Subject: [PATCH 2/4] chore: cleanup --- test/config/deps/vite-ssr-resolve/inline-dep/package.json | 1 - test/config/deps/vite-ssr-resolve/other-dep/package.json | 1 - .../deps/vite-ssr-resolve/ssr-no-external-dep/package.json | 1 - 3 files changed, 3 deletions(-) diff --git a/test/config/deps/vite-ssr-resolve/inline-dep/package.json b/test/config/deps/vite-ssr-resolve/inline-dep/package.json index e3b343b74bf9..e60fe29aab1d 100644 --- a/test/config/deps/vite-ssr-resolve/inline-dep/package.json +++ b/test/config/deps/vite-ssr-resolve/inline-dep/package.json @@ -1,6 +1,5 @@ { "name": "inline-dep", "type": "module", - "private": true, "exports": "./index.js" } diff --git a/test/config/deps/vite-ssr-resolve/other-dep/package.json b/test/config/deps/vite-ssr-resolve/other-dep/package.json index c4f06a9a9a69..b41039228d90 100644 --- a/test/config/deps/vite-ssr-resolve/other-dep/package.json +++ b/test/config/deps/vite-ssr-resolve/other-dep/package.json @@ -1,6 +1,5 @@ { "name": "other-dep", "type": "module", - "private": true, "exports": "./index.js" } diff --git a/test/config/deps/vite-ssr-resolve/ssr-no-external-dep/package.json b/test/config/deps/vite-ssr-resolve/ssr-no-external-dep/package.json index 68a203442474..d1b7fd723c74 100644 --- a/test/config/deps/vite-ssr-resolve/ssr-no-external-dep/package.json +++ b/test/config/deps/vite-ssr-resolve/ssr-no-external-dep/package.json @@ -1,6 +1,5 @@ { "name": "ssr-no-external-dep", "type": "module", - "private": true, "exports": "./index.js" } From 7d73f431fe3cfa72e7775764a00a5042364f3c91 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 3 Dec 2025 10:49:27 +0900 Subject: [PATCH 3/4] refactor: remove old edge case --- .../runtime/moduleRunner/moduleEvaluator.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/vitest/src/runtime/moduleRunner/moduleEvaluator.ts b/packages/vitest/src/runtime/moduleRunner/moduleEvaluator.ts index 9b8c87f15979..7b5dfa7f69f1 100644 --- a/packages/vitest/src/runtime/moduleRunner/moduleEvaluator.ts +++ b/packages/vitest/src/runtime/moduleRunner/moduleEvaluator.ts @@ -322,21 +322,22 @@ export class VitestModuleEvaluator implements ModuleEvaluator { ? vm.runInContext(wrappedCode, this.vm.context, options) : vm.runInThisContext(wrappedCode, options) - const dynamicRequest = async (dep: string, options: ImportCallOptions) => { - dep = String(dep) - // TODO: support more edge cases? - // vite doesn't support dynamic modules by design, but we have to - if (dep[0] === '#') { - return context[ssrDynamicImportKey](wrapId(dep), options) - } - return context[ssrDynamicImportKey](dep, options) - } + // const dynamicRequest = async (dep: string, options: ImportCallOptions) => { + // dep = String(dep) + // // TODO: support more edge cases? + // // vite doesn't support dynamic modules by design, but we have to + // if (dep[0] === '#') { + // return context[ssrDynamicImportKey](wrapId(dep), options) + // } + // return context[ssrDynamicImportKey](dep, options) + // } await initModule( context[ssrModuleExportsKey], context[ssrImportMetaKey], context[ssrImportKey], - dynamicRequest, + // dynamicRequest, + context[ssrDynamicImportKey], context[ssrExportAllKey], // vite 7 support, remove when vite 7+ is supported (context as any).__vite_ssr_exportName__ From 043220c00b4eee7bfcd6ff39121d2e2d922510ba Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 3 Dec 2025 11:02:39 +0900 Subject: [PATCH 4/4] cleanup --- .../src/runtime/moduleRunner/moduleEvaluator.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/vitest/src/runtime/moduleRunner/moduleEvaluator.ts b/packages/vitest/src/runtime/moduleRunner/moduleEvaluator.ts index 7b5dfa7f69f1..a6be9421a218 100644 --- a/packages/vitest/src/runtime/moduleRunner/moduleEvaluator.ts +++ b/packages/vitest/src/runtime/moduleRunner/moduleEvaluator.ts @@ -322,21 +322,10 @@ export class VitestModuleEvaluator implements ModuleEvaluator { ? vm.runInContext(wrappedCode, this.vm.context, options) : vm.runInThisContext(wrappedCode, options) - // const dynamicRequest = async (dep: string, options: ImportCallOptions) => { - // dep = String(dep) - // // TODO: support more edge cases? - // // vite doesn't support dynamic modules by design, but we have to - // if (dep[0] === '#') { - // return context[ssrDynamicImportKey](wrapId(dep), options) - // } - // return context[ssrDynamicImportKey](dep, options) - // } - await initModule( context[ssrModuleExportsKey], context[ssrImportMetaKey], context[ssrImportKey], - // dynamicRequest, context[ssrDynamicImportKey], context[ssrExportAllKey], // vite 7 support, remove when vite 7+ is supported