From 4f122fce77fb4a0d81e85eba3d6af37c915b80b6 Mon Sep 17 00:00:00 2001 From: Neo Nie Date: Fri, 24 Jul 2026 02:59:22 +0100 Subject: [PATCH 1/3] fix(rsc): handle root package resolution errors --- .../e2e/client-package-resolution.test.ts | 121 ++++++++++++++++++ packages/plugin-rsc/src/plugin.ts | 16 ++- 2 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 packages/plugin-rsc/e2e/client-package-resolution.test.ts diff --git a/packages/plugin-rsc/e2e/client-package-resolution.test.ts b/packages/plugin-rsc/e2e/client-package-resolution.test.ts new file mode 100644 index 000000000..26a5084a9 --- /dev/null +++ b/packages/plugin-rsc/e2e/client-package-resolution.test.ts @@ -0,0 +1,121 @@ +import { expect, test } from '@playwright/test' +import { setupInlineFixture, useFixture } from './fixture' +import { waitForHydration } from './helper' + +test.describe(() => { + const root = 'examples/e2e/temp/client-package-resolution' + + test.beforeAll(async () => { + await setupInlineFixture({ + src: 'examples/starter-extra', + dest: root, + files: { + 'package.json': { + edit: (source) => { + const packageJson = JSON.parse(source) + packageJson.dependencies = { + ...packageJson.dependencies, + '@vitejs/test-dep-root-conflict': '1.0.0', + '@vitejs/test-dep-server-with-root-conflict': '1.0.0', + } + return JSON.stringify(packageJson, null, 2) + '\n' + }, + }, + 'src/root.tsx': /* tsx */ ` + import { TestServer } from '@vitejs/test-dep-server-with-root-conflict/server' + + export function Root() { + return ( + + + + + + ) + } + `, + 'node_modules/@vitejs/test-dep-root-conflict/package.json': /* json */ ` + { + "name": "@vitejs/test-dep-root-conflict", + "version": "1.0.0", + "type": "module", + "exports": { + ".": "./index.js" + } + } + `, + 'node_modules/@vitejs/test-dep-root-conflict/index.js': /* js */ ` + export const root = true + `, + 'node_modules/@vitejs/test-dep-server-with-root-conflict/package.json': /* json */ ` + { + "name": "@vitejs/test-dep-server-with-root-conflict", + "version": "1.0.0", + "type": "module", + "exports": { + "./server": "./server.js" + }, + "dependencies": { + "@vitejs/test-dep-root-conflict": "2.0.0" + }, + "peerDependencies": { + "react": "*" + } + } + `, + 'node_modules/@vitejs/test-dep-server-with-root-conflict/server.js': /* js */ ` + import { TestClient } from '@vitejs/test-dep-root-conflict/client' + import React from 'react' + + export function TestServer() { + return React.createElement(TestClient) + } + `, + 'node_modules/@vitejs/test-dep-server-with-root-conflict/node_modules/@vitejs/test-dep-root-conflict/package.json': /* json */ ` + { + "name": "@vitejs/test-dep-root-conflict", + "version": "2.0.0", + "type": "module", + "exports": { + "./client": "./client.js" + }, + "peerDependencies": { + "react": "*" + } + } + `, + 'node_modules/@vitejs/test-dep-server-with-root-conflict/node_modules/@vitejs/test-dep-root-conflict/client.js': /* js */ ` + 'use client' + + import React from 'react' + + export function TestClient() { + const [count, setCount] = React.useState(0) + return React.createElement( + 'button', + { onClick: () => setCount((value) => value + 1) }, + 'Nested client: ' + count, + ) + } + `, + }, + }) + }) + + for (const mode of ['dev', 'build'] as const) { + test.describe(mode, () => { + const f = useFixture({ root, mode }) + + test('uses the client package resolved from its importer', async ({ + page, + }) => { + await page.goto(f.url()) + await waitForHydration(page) + await page.getByRole('button', { name: 'Nested client: 0' }).click() + await expect( + page.getByRole('button', { name: 'Nested client: 1' }), + ).toBeVisible() + }) + }) + } +}) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 9253de54f..198eb3515 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -1738,11 +1738,17 @@ function vitePluginUseClient( this.environment.config.root, 'index.html', ) - const resolvedAtRoot = await this.resolve( - source, - rootImporter, - options, - ) + let resolvedAtRoot + try { + resolvedAtRoot = await this.resolve( + source, + rootImporter, + options, + ) + } catch { + // A different version at the root may not export this subpath. + return + } if (!resolvedAtRoot || resolvedAtRoot.id !== resolved.id) { return } From 7485119e0fc3316f432aa32119413629e40a6255 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Mon, 27 Jul 2026 12:04:45 +0900 Subject: [PATCH 2/3] test(rsc): clarify client package resolution fixture Co-authored-by: OpenCode --- .../e2e/client-package-resolution.test.ts | 99 ++++++++++--------- 1 file changed, 52 insertions(+), 47 deletions(-) diff --git a/packages/plugin-rsc/e2e/client-package-resolution.test.ts b/packages/plugin-rsc/e2e/client-package-resolution.test.ts index 26a5084a9..710a9bbd7 100644 --- a/packages/plugin-rsc/e2e/client-package-resolution.test.ts +++ b/packages/plugin-rsc/e2e/client-package-resolution.test.ts @@ -4,8 +4,22 @@ import { waitForHydration } from './helper' test.describe(() => { const root = 'examples/e2e/temp/client-package-resolution' + const json = (value: unknown) => JSON.stringify(value, null, 2) test.beforeAll(async () => { + // Dependency and import graph: + // + // app + // |-- @vitejs/test-client-dep@1 (exports only its root) + // `-- @vitejs/test-server-dep + // `-- @vitejs/test-client-dep@2 (exports "./client") + // + // root.tsx -> test-server-dep -> test-client-dep@2/client + // ^ resolves from the actual importer + // + // The plugin also probes v2's "./client" specifier from the app root. That + // finds v1 and throws because v1 does not export "./client", so the plugin + // must keep the fully resolved nested v2 module ID instead. await setupInlineFixture({ src: 'examples/starter-extra', dest: root, @@ -15,14 +29,14 @@ test.describe(() => { const packageJson = JSON.parse(source) packageJson.dependencies = { ...packageJson.dependencies, - '@vitejs/test-dep-root-conflict': '1.0.0', - '@vitejs/test-dep-server-with-root-conflict': '1.0.0', + '@vitejs/test-client-dep': '1.0.0', + '@vitejs/test-server-dep': '1.0.0', } return JSON.stringify(packageJson, null, 2) + '\n' }, }, 'src/root.tsx': /* tsx */ ` - import { TestServer } from '@vitejs/test-dep-server-with-root-conflict/server' + import { TestServer } from '@vitejs/test-server-dep/server' export function Root() { return ( @@ -34,57 +48,48 @@ test.describe(() => { ) } `, - 'node_modules/@vitejs/test-dep-root-conflict/package.json': /* json */ ` - { - "name": "@vitejs/test-dep-root-conflict", - "version": "1.0.0", - "type": "module", - "exports": { - ".": "./index.js" - } - } - `, - 'node_modules/@vitejs/test-dep-root-conflict/index.js': /* js */ ` - export const root = true - `, - 'node_modules/@vitejs/test-dep-server-with-root-conflict/package.json': /* json */ ` - { - "name": "@vitejs/test-dep-server-with-root-conflict", - "version": "1.0.0", - "type": "module", - "exports": { - "./server": "./server.js" - }, - "dependencies": { - "@vitejs/test-dep-root-conflict": "2.0.0" - }, - "peerDependencies": { - "react": "*" - } - } - `, - 'node_modules/@vitejs/test-dep-server-with-root-conflict/server.js': /* js */ ` - import { TestClient } from '@vitejs/test-dep-root-conflict/client' + 'node_modules/@vitejs/test-server-dep/package.json': json({ + name: '@vitejs/test-server-dep', + version: '1.0.0', + type: 'module', + exports: { + './server': './server.js', + }, + dependencies: { + '@vitejs/test-client-dep': '2.0.0', + }, + peerDependencies: { + react: '*', + }, + }), + 'node_modules/@vitejs/test-server-dep/server.js': /* js */ ` + import { TestClient } from '@vitejs/test-client-dep/client' import React from 'react' export function TestServer() { return React.createElement(TestClient) } `, - 'node_modules/@vitejs/test-dep-server-with-root-conflict/node_modules/@vitejs/test-dep-root-conflict/package.json': /* json */ ` - { - "name": "@vitejs/test-dep-root-conflict", - "version": "2.0.0", - "type": "module", - "exports": { - "./client": "./client.js" + 'node_modules/@vitejs/test-client-dep/package.json': json({ + name: '@vitejs/test-client-dep', + version: '1.0.0', + type: 'module', + exports: './index.js', + }), + 'node_modules/@vitejs/test-client-dep/index.js': /* js */ ``, + 'node_modules/@vitejs/test-server-dep/node_modules/@vitejs/test-client-dep/package.json': + json({ + name: '@vitejs/test-client-dep', + version: '2.0.0', + type: 'module', + exports: { + './client': './client.js', }, - "peerDependencies": { - "react": "*" - } - } - `, - 'node_modules/@vitejs/test-dep-server-with-root-conflict/node_modules/@vitejs/test-dep-root-conflict/client.js': /* js */ ` + peerDependencies: { + react: '*', + }, + }), + 'node_modules/@vitejs/test-server-dep/node_modules/@vitejs/test-client-dep/client.js': /* js */ ` 'use client' import React from 'react' From d6da759a14ded5a1f425be60ec9667b325855d87 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Mon, 27 Jul 2026 12:05:19 +0900 Subject: [PATCH 3/3] nit --- packages/plugin-rsc/e2e/client-package-resolution.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-rsc/e2e/client-package-resolution.test.ts b/packages/plugin-rsc/e2e/client-package-resolution.test.ts index 710a9bbd7..ba8909dc7 100644 --- a/packages/plugin-rsc/e2e/client-package-resolution.test.ts +++ b/packages/plugin-rsc/e2e/client-package-resolution.test.ts @@ -4,7 +4,6 @@ import { waitForHydration } from './helper' test.describe(() => { const root = 'examples/e2e/temp/client-package-resolution' - const json = (value: unknown) => JSON.stringify(value, null, 2) test.beforeAll(async () => { // Dependency and import graph: @@ -20,6 +19,7 @@ test.describe(() => { // The plugin also probes v2's "./client" specifier from the app root. That // finds v1 and throws because v1 does not export "./client", so the plugin // must keep the fully resolved nested v2 module ID instead. + const json = (value: unknown) => JSON.stringify(value, null, 2) await setupInlineFixture({ src: 'examples/starter-extra', dest: root,