From e392697dd00ff25ab6bec339205e40ef4c902915 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Sun, 22 Jun 2025 15:31:59 +0900 Subject: [PATCH 01/11] fix: tolerate internal client boundary of server package --- packages/rsc/src/plugin.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/rsc/src/plugin.ts b/packages/rsc/src/plugin.ts index e13999152..651817f5a 100644 --- a/packages/rsc/src/plugin.ts +++ b/packages/rsc/src/plugin.ts @@ -764,6 +764,25 @@ function vitePluginUseClient(): Plugin[] { if (!code.includes("use client")) return; const ast = await parseAstAsync(code); + if (!hasDirective(ast.body, "use client")) return; + + // If `?v=` reached here, it means client boundary is created + // by packages imported on server environment, + // which breaks the expectation on dependency optimizer on browser. + // https://github.com/hi-ogawa/vite-plugins/pull/384 + if (id.includes("?v=")) { + assert(this.environment.mode === "dev"); + id = id.replace(/\?v=*/, "?v="); + this.warn( + `[vite-rsc] detected an internal client boundary created by a package imported on rsc environment`, + ); + // TODO: Does it help something if we copy over the hash from browser environment optimizer? + // const hash = + // server.environments.client.depsOptimizer?.metadata.browserHash; + // if (hash) { + // id += `?v=${hash}`; + // } + } let importId: string; let referenceKey: string; From b8f02768f690f3ec2f1ac16190d4053351df8b9d Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Sun, 22 Jun 2025 15:48:44 +0900 Subject: [PATCH 02/11] test: add test --- packages/rsc/examples/basic/e2e/basic.test.ts | 7 ++++++ packages/rsc/examples/basic/package.json | 1 + .../rsc/examples/basic/src/routes/root.tsx | 2 ++ .../basic/test-dep/client-in-server/README.md | 1 + .../basic/test-dep/client-in-server/client.js | 22 +++++++++++++++++++ .../test-dep/client-in-server/package.json | 9 ++++++++ .../test-dep/client-in-server/server.d.ts | 3 +++ .../basic/test-dep/client-in-server/server.js | 6 +++++ packages/rsc/src/plugin.ts | 18 +++++++-------- pnpm-lock.yaml | 12 ++++++++++ 10 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 packages/rsc/examples/basic/test-dep/client-in-server/README.md create mode 100644 packages/rsc/examples/basic/test-dep/client-in-server/client.js create mode 100644 packages/rsc/examples/basic/test-dep/client-in-server/package.json create mode 100644 packages/rsc/examples/basic/test-dep/client-in-server/server.d.ts create mode 100644 packages/rsc/examples/basic/test-dep/client-in-server/server.js diff --git a/packages/rsc/examples/basic/e2e/basic.test.ts b/packages/rsc/examples/basic/e2e/basic.test.ts index c9e46d5bc..36128d413 100644 --- a/packages/rsc/examples/basic/e2e/basic.test.ts +++ b/packages/rsc/examples/basic/e2e/basic.test.ts @@ -620,3 +620,10 @@ test("test serialization @js", async ({ page }) => { await page.getByTestId("serialization").click(); await expect(page.getByTestId("serialization")).toHaveText("ok"); }); + +test.only("client in server package", async ({ page }) => { + await page.goto("./"); + await expect(page.getByTestId("client-in-server")).toHaveText( + "[test-client-in-server: true]", + ); +}); diff --git a/packages/rsc/examples/basic/package.json b/packages/rsc/examples/basic/package.json index da076ffdb..4c5d1e576 100644 --- a/packages/rsc/examples/basic/package.json +++ b/packages/rsc/examples/basic/package.json @@ -23,6 +23,7 @@ "@types/react": "latest", "@types/react-dom": "latest", "@vitejs/plugin-react": "latest", + "@vitejs/test-dep-client-in-server": "file:./test-dep/client-in-server", "tailwindcss": "^4.1.4", "vite": "latest", "vite-plugin-inspect": "^11.2.0" diff --git a/packages/rsc/examples/basic/src/routes/root.tsx b/packages/rsc/examples/basic/src/routes/root.tsx index a5ae38a36..dda439b7a 100644 --- a/packages/rsc/examples/basic/src/routes/root.tsx +++ b/packages/rsc/examples/basic/src/routes/root.tsx @@ -19,6 +19,7 @@ import { import { TestStyleClient2 } from "./client2"; import ErrorBoundary from "./error-boundary"; import "./server.css"; +import TestClientInServerDep from "@vitejs/test-dep-client-in-server"; import { TestServerActionBindAction, TestServerActionBindClient, @@ -81,6 +82,7 @@ export function Root(props: { url: URL }) { + ); diff --git a/packages/rsc/examples/basic/test-dep/client-in-server/README.md b/packages/rsc/examples/basic/test-dep/client-in-server/README.md new file mode 100644 index 000000000..11250c52f --- /dev/null +++ b/packages/rsc/examples/basic/test-dep/client-in-server/README.md @@ -0,0 +1 @@ +test package structure similar to https://github.com/vercel/react-tweet diff --git a/packages/rsc/examples/basic/test-dep/client-in-server/client.js b/packages/rsc/examples/basic/test-dep/client-in-server/client.js new file mode 100644 index 000000000..e820ff9bc --- /dev/null +++ b/packages/rsc/examples/basic/test-dep/client-in-server/client.js @@ -0,0 +1,22 @@ +"use client"; + +import React from "react"; + +export default function TestClient() { + const hydrated = useHydrated(); + return React.createElement( + "span", + { "data-testid": "client-in-server" }, + `[test-client-in-server: ${String(hydrated)}]`, + ); +} + +const noop = () => {}; + +function useHydrated() { + return React.useSyncExternalStore( + noop, + () => true, + () => false, + ); +} diff --git a/packages/rsc/examples/basic/test-dep/client-in-server/package.json b/packages/rsc/examples/basic/test-dep/client-in-server/package.json new file mode 100644 index 000000000..e70a8103e --- /dev/null +++ b/packages/rsc/examples/basic/test-dep/client-in-server/package.json @@ -0,0 +1,9 @@ +{ + "name": "@vitejs/test-dep-client-in-server", + "private": true, + "type": "module", + "exports": "./server.js", + "peerDependencies": { + "react": "*" + } +} diff --git a/packages/rsc/examples/basic/test-dep/client-in-server/server.d.ts b/packages/rsc/examples/basic/test-dep/client-in-server/server.d.ts new file mode 100644 index 000000000..546e51b07 --- /dev/null +++ b/packages/rsc/examples/basic/test-dep/client-in-server/server.d.ts @@ -0,0 +1,3 @@ +export default function TestClientInServer(): Promise< + import("react").ReactNode +>; diff --git a/packages/rsc/examples/basic/test-dep/client-in-server/server.js b/packages/rsc/examples/basic/test-dep/client-in-server/server.js new file mode 100644 index 000000000..074088e48 --- /dev/null +++ b/packages/rsc/examples/basic/test-dep/client-in-server/server.js @@ -0,0 +1,6 @@ +import React from "react"; +import TestClient from "./client.js"; + +export default async function TestClientInServer() { + return React.createElement(TestClient); +} diff --git a/packages/rsc/src/plugin.ts b/packages/rsc/src/plugin.ts index 651817f5a..180fc6eb8 100644 --- a/packages/rsc/src/plugin.ts +++ b/packages/rsc/src/plugin.ts @@ -766,22 +766,22 @@ function vitePluginUseClient(): Plugin[] { const ast = await parseAstAsync(code); if (!hasDirective(ast.body, "use client")) return; - // If `?v=` reached here, it means client boundary is created - // by packages imported on server environment, + // If `?v=` reached here, it means this is a client boundary created + // by a package imported on server environment, // which breaks the expectation on dependency optimizer on browser. // https://github.com/hi-ogawa/vite-plugins/pull/384 if (id.includes("?v=")) { assert(this.environment.mode === "dev"); - id = id.replace(/\?v=*/, "?v="); this.warn( `[vite-rsc] detected an internal client boundary created by a package imported on rsc environment`, ); - // TODO: Does it help something if we copy over the hash from browser environment optimizer? - // const hash = - // server.environments.client.depsOptimizer?.metadata.browserHash; - // if (hash) { - // id += `?v=${hash}`; - // } + id = id.split("?v=")[0]!; + // Not sure if this helps, but for now, copy over the hash from browser environment optimizer. + const hash = + server.environments.client.depsOptimizer?.metadata.browserHash; + if (hash) { + id += `?v=${hash}`; + } } let importId: string; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea449ab56..ad4795177 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -584,6 +584,9 @@ importers: '@vitejs/plugin-react': specifier: ^4.5.0 version: 4.5.0(vite@6.3.5(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) + '@vitejs/test-dep-client-in-server': + specifier: file:./test-dep/client-in-server + version: file:packages/rsc/examples/basic/test-dep/client-in-server(react@19.1.0) tailwindcss: specifier: ^4.1.4 version: 4.1.4 @@ -2266,6 +2269,11 @@ packages: peerDependencies: vite: ^6.3.5 + '@vitejs/test-dep-client-in-server@file:packages/rsc/examples/basic/test-dep/client-in-server': + resolution: {directory: packages/rsc/examples/basic/test-dep/client-in-server, type: directory} + peerDependencies: + react: ^19.1.0 + '@vitest/expect@3.2.3': resolution: {integrity: sha512-W2RH2TPWVHA1o7UmaFKISPvdicFJH+mjykctJFoAkUw+SPTJTGjUNdKscFBrqM7IPnCVu6zihtKYa7TkZS1dkQ==} @@ -5853,6 +5861,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitejs/test-dep-client-in-server@file:packages/rsc/examples/basic/test-dep/client-in-server(react@19.1.0)': + dependencies: + react: 19.1.0 + '@vitest/expect@3.2.3': dependencies: '@types/chai': 5.2.2 From 36cd24b367af56324ccd5825381ea9ce1f2e30f1 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Sun, 22 Jun 2025 15:53:10 +0900 Subject: [PATCH 03/11] chore: cleanup --- packages/rsc/examples/basic/e2e/basic.test.ts | 2 +- packages/rsc/src/plugin.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rsc/examples/basic/e2e/basic.test.ts b/packages/rsc/examples/basic/e2e/basic.test.ts index 36128d413..763205502 100644 --- a/packages/rsc/examples/basic/e2e/basic.test.ts +++ b/packages/rsc/examples/basic/e2e/basic.test.ts @@ -621,7 +621,7 @@ test("test serialization @js", async ({ page }) => { await expect(page.getByTestId("serialization")).toHaveText("ok"); }); -test.only("client in server package", async ({ page }) => { +test("client in server package", async ({ page }) => { await page.goto("./"); await expect(page.getByTestId("client-in-server")).toHaveText( "[test-client-in-server: true]", diff --git a/packages/rsc/src/plugin.ts b/packages/rsc/src/plugin.ts index 180fc6eb8..fa0e29799 100644 --- a/packages/rsc/src/plugin.ts +++ b/packages/rsc/src/plugin.ts @@ -776,7 +776,7 @@ function vitePluginUseClient(): Plugin[] { `[vite-rsc] detected an internal client boundary created by a package imported on rsc environment`, ); id = id.split("?v=")[0]!; - // Not sure if this helps, but for now, copy over the hash from browser environment optimizer. + // Not sure if this is good or bad, but for now, copy over the hash from browser environment optimizer. const hash = server.environments.client.depsOptimizer?.metadata.browserHash; if (hash) { From aed08510558620a0d98c4d504bfe6c314ea80659 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Sun, 22 Jun 2025 16:27:17 +0900 Subject: [PATCH 04/11] test: tweak --- packages/rsc/examples/basic/e2e/basic.test.ts | 7 +++++-- .../src/routes/client-in-server/client.tsx | 6 ++++++ .../src/routes/client-in-server/server.tsx | 17 +++++++++++++++++ packages/rsc/examples/basic/src/routes/root.tsx | 4 ++-- .../basic/test-dep/client-in-server/README.md | 1 - .../basic/test-dep/client-in-server/client.js | 6 +----- .../test-dep/client-in-server/package.json | 5 ++++- .../basic/test-dep/client-in-server/server.d.ts | 3 --- packages/rsc/examples/basic/vite.config.ts | 4 ++++ packages/rsc/src/plugin.ts | 3 ++- 10 files changed, 41 insertions(+), 15 deletions(-) create mode 100644 packages/rsc/examples/basic/src/routes/client-in-server/client.tsx create mode 100644 packages/rsc/examples/basic/src/routes/client-in-server/server.tsx delete mode 100644 packages/rsc/examples/basic/test-dep/client-in-server/README.md delete mode 100644 packages/rsc/examples/basic/test-dep/client-in-server/server.d.ts diff --git a/packages/rsc/examples/basic/e2e/basic.test.ts b/packages/rsc/examples/basic/e2e/basic.test.ts index 763205502..6794832de 100644 --- a/packages/rsc/examples/basic/e2e/basic.test.ts +++ b/packages/rsc/examples/basic/e2e/basic.test.ts @@ -621,9 +621,12 @@ test("test serialization @js", async ({ page }) => { await expect(page.getByTestId("serialization")).toHaveText("ok"); }); -test("client in server package", async ({ page }) => { +test("client-in-server package", async ({ page }) => { await page.goto("./"); await expect(page.getByTestId("client-in-server")).toHaveText( - "[test-client-in-server: true]", + "[test-client-in-server-dep: true]", + ); + await expect(page.getByTestId("client-in-server-client")).toHaveText( + "[test-client-in-server-dep-direct-client: true]", ); }); diff --git a/packages/rsc/examples/basic/src/routes/client-in-server/client.tsx b/packages/rsc/examples/basic/src/routes/client-in-server/client.tsx new file mode 100644 index 000000000..5d1733d85 --- /dev/null +++ b/packages/rsc/examples/basic/src/routes/client-in-server/client.tsx @@ -0,0 +1,6 @@ +"use client"; + +// @ts-ignore +import TestClientInServerDepClient from "@vitejs/test-dep-client-in-server/client"; + +export { TestClientInServerDepClient }; diff --git a/packages/rsc/examples/basic/src/routes/client-in-server/server.tsx b/packages/rsc/examples/basic/src/routes/client-in-server/server.tsx new file mode 100644 index 000000000..39882c0be --- /dev/null +++ b/packages/rsc/examples/basic/src/routes/client-in-server/server.tsx @@ -0,0 +1,17 @@ +// @ts-ignore +import TestClientInServerDep from "@vitejs/test-dep-client-in-server/server"; +import { TestClientInServerDepClient } from "./client"; + +export function TestClientInServer() { + return ( +
+
+ [test-client-in-server-dep: ] +
+
+ [test-client-in-server-dep-direct-client:{" "} + ] +
+
+ ); +} diff --git a/packages/rsc/examples/basic/src/routes/root.tsx b/packages/rsc/examples/basic/src/routes/root.tsx index dda439b7a..fa9287f14 100644 --- a/packages/rsc/examples/basic/src/routes/root.tsx +++ b/packages/rsc/examples/basic/src/routes/root.tsx @@ -19,13 +19,13 @@ import { import { TestStyleClient2 } from "./client2"; import ErrorBoundary from "./error-boundary"; import "./server.css"; -import TestClientInServerDep from "@vitejs/test-dep-client-in-server"; import { TestServerActionBindAction, TestServerActionBindClient, TestServerActionBindReset, TestServerActionBindSimple, } from "./action-bind/server"; +import { TestClientInServer } from "./client-in-server/server"; import { TestSerializationServer } from "./serialization/server"; import styles from "./server.module.css"; @@ -82,7 +82,7 @@ export function Root(props: { url: URL }) { - + ); diff --git a/packages/rsc/examples/basic/test-dep/client-in-server/README.md b/packages/rsc/examples/basic/test-dep/client-in-server/README.md deleted file mode 100644 index 11250c52f..000000000 --- a/packages/rsc/examples/basic/test-dep/client-in-server/README.md +++ /dev/null @@ -1 +0,0 @@ -test package structure similar to https://github.com/vercel/react-tweet diff --git a/packages/rsc/examples/basic/test-dep/client-in-server/client.js b/packages/rsc/examples/basic/test-dep/client-in-server/client.js index e820ff9bc..ef60ddb96 100644 --- a/packages/rsc/examples/basic/test-dep/client-in-server/client.js +++ b/packages/rsc/examples/basic/test-dep/client-in-server/client.js @@ -4,11 +4,7 @@ import React from "react"; export default function TestClient() { const hydrated = useHydrated(); - return React.createElement( - "span", - { "data-testid": "client-in-server" }, - `[test-client-in-server: ${String(hydrated)}]`, - ); + return React.createElement("span", null, String(hydrated)); } const noop = () => {}; diff --git a/packages/rsc/examples/basic/test-dep/client-in-server/package.json b/packages/rsc/examples/basic/test-dep/client-in-server/package.json index e70a8103e..bd99518f4 100644 --- a/packages/rsc/examples/basic/test-dep/client-in-server/package.json +++ b/packages/rsc/examples/basic/test-dep/client-in-server/package.json @@ -2,7 +2,10 @@ "name": "@vitejs/test-dep-client-in-server", "private": true, "type": "module", - "exports": "./server.js", + "exports": { + "./server": "./server.js", + "./client": "./client.js" + }, "peerDependencies": { "react": "*" } diff --git a/packages/rsc/examples/basic/test-dep/client-in-server/server.d.ts b/packages/rsc/examples/basic/test-dep/client-in-server/server.d.ts deleted file mode 100644 index 546e51b07..000000000 --- a/packages/rsc/examples/basic/test-dep/client-in-server/server.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function TestClientInServer(): Promise< - import("react").ReactNode ->; diff --git a/packages/rsc/examples/basic/vite.config.ts b/packages/rsc/examples/basic/vite.config.ts index c2e6b9c41..58bc83789 100644 --- a/packages/rsc/examples/basic/vite.config.ts +++ b/packages/rsc/examples/basic/vite.config.ts @@ -117,4 +117,8 @@ export default { fetch: handler }; build: { minify: false, }, + optimizeDeps: { + // even if excluded, the module will be duplicated + exclude: ["@vitejs/test-dep-client-in-server/client"], + }, }) as any; diff --git a/packages/rsc/src/plugin.ts b/packages/rsc/src/plugin.ts index fa0e29799..8f0dd4c66 100644 --- a/packages/rsc/src/plugin.ts +++ b/packages/rsc/src/plugin.ts @@ -776,7 +776,8 @@ function vitePluginUseClient(): Plugin[] { `[vite-rsc] detected an internal client boundary created by a package imported on rsc environment`, ); id = id.split("?v=")[0]!; - // Not sure if this is good or bad, but for now, copy over the hash from browser environment optimizer. + // this doesn't seem entirely sound, but for now, + // copy over the hash from browser environment optimizer. const hash = server.environments.client.depsOptimizer?.metadata.browserHash; if (hash) { From fb3de7934563363c34c4a45e5d4d82f0fa00c592 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Sun, 22 Jun 2025 16:40:31 +0900 Subject: [PATCH 05/11] fix: avoid stale hash --- packages/rsc/examples/basic/vite.config.ts | 3 +-- packages/rsc/src/plugin.ts | 14 +++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/rsc/examples/basic/vite.config.ts b/packages/rsc/examples/basic/vite.config.ts index 58bc83789..35d65a570 100644 --- a/packages/rsc/examples/basic/vite.config.ts +++ b/packages/rsc/examples/basic/vite.config.ts @@ -118,7 +118,6 @@ export default { fetch: handler }; minify: false, }, optimizeDeps: { - // even if excluded, the module will be duplicated - exclude: ["@vitejs/test-dep-client-in-server/client"], + include: ["@vitejs/test-dep-client-in-server/client"], }, }) as any; diff --git a/packages/rsc/src/plugin.ts b/packages/rsc/src/plugin.ts index 8f0dd4c66..1d4f1dedd 100644 --- a/packages/rsc/src/plugin.ts +++ b/packages/rsc/src/plugin.ts @@ -776,13 +776,13 @@ function vitePluginUseClient(): Plugin[] { `[vite-rsc] detected an internal client boundary created by a package imported on rsc environment`, ); id = id.split("?v=")[0]!; - // this doesn't seem entirely sound, but for now, - // copy over the hash from browser environment optimizer. - const hash = - server.environments.client.depsOptimizer?.metadata.browserHash; - if (hash) { - id += `?v=${hash}`; - } + // Copying over the hash from browser environment optimizer seems sound to avoid double modules, + // but this can cause stale hash when browser optimizer reloaded, so for now this is avoided. + // const hash = + // server.environments.client.depsOptimizer?.metadata.browserHash; + // if (hash) { + // id += `?v=${hash}`; + // } } let importId: string; From a4db8f26280400da877614ceed775b79774de53c Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Sun, 22 Jun 2025 16:52:24 +0900 Subject: [PATCH 06/11] fix: fix client-package-proxy virtual --- packages/rsc/src/plugin.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/rsc/src/plugin.ts b/packages/rsc/src/plugin.ts index 1d4f1dedd..4944ef6a8 100644 --- a/packages/rsc/src/plugin.ts +++ b/packages/rsc/src/plugin.ts @@ -766,11 +766,11 @@ function vitePluginUseClient(): Plugin[] { const ast = await parseAstAsync(code); if (!hasDirective(ast.body, "use client")) return; - // If `?v=` reached here, it means this is a client boundary created - // by a package imported on server environment, - // which breaks the expectation on dependency optimizer on browser. + // If non package source `?v=` reached here, it means this is a client boundary created + // by a package imported on server environment, which breaks the expectation on dependency optimizer on browser. // https://github.com/hi-ogawa/vite-plugins/pull/384 - if (id.includes("?v=")) { + const packageSource = packageSources.get(id); + if (!packageSource && id.includes("?v=")) { assert(this.environment.mode === "dev"); this.warn( `[vite-rsc] detected an internal client boundary created by a package imported on rsc environment`, @@ -787,7 +787,6 @@ function vitePluginUseClient(): Plugin[] { let importId: string; let referenceKey: string; - const packageSource = packageSources.get(id); if (packageSource) { if (this.environment.mode === "dev") { importId = `/@id/__x00__virtual:vite-rsc/client-package-proxy/${packageSource}`; From 29d631e90b5dd197fff1a67a9f2e1b180a337c78 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Sun, 22 Jun 2025 17:59:16 +0900 Subject: [PATCH 07/11] fix: use client-in-server-package-proxy --- packages/rsc/examples/basic/vite.config.ts | 3 +- packages/rsc/src/plugin.ts | 47 ++++++++++++++-------- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/packages/rsc/examples/basic/vite.config.ts b/packages/rsc/examples/basic/vite.config.ts index 35d65a570..40bee17c4 100644 --- a/packages/rsc/examples/basic/vite.config.ts +++ b/packages/rsc/examples/basic/vite.config.ts @@ -118,6 +118,7 @@ export default { fetch: handler }; minify: false, }, optimizeDeps: { - include: ["@vitejs/test-dep-client-in-server/client"], + // TODO: test no double modules + exclude: ["@vitejs/test-dep-client-in-server/client"], }, }) as any; diff --git a/packages/rsc/src/plugin.ts b/packages/rsc/src/plugin.ts index 4944ef6a8..f67d3682f 100644 --- a/packages/rsc/src/plugin.ts +++ b/packages/rsc/src/plugin.ts @@ -766,28 +766,22 @@ function vitePluginUseClient(): Plugin[] { const ast = await parseAstAsync(code); if (!hasDirective(ast.body, "use client")) return; - // If non package source `?v=` reached here, it means this is a client boundary created - // by a package imported on server environment, which breaks the expectation on dependency optimizer on browser. - // https://github.com/hi-ogawa/vite-plugins/pull/384 + let importId: string; + let referenceKey: string; const packageSource = packageSources.get(id); if (!packageSource && id.includes("?v=")) { assert(this.environment.mode === "dev"); + // If non package source `?v=` reached here, this is a client boundary created + // by a package imported on server environment, which breaks the expectation on dependency optimizer on browser. + // Directory copying over "?v=" from client optimizer in client reference can make a hashed module stale. + // we use another virtual module wrapper to delay such process. + // TODO: suggest `optimizeDeps.exclude` and skip warning if that's already the case. this.warn( `[vite-rsc] detected an internal client boundary created by a package imported on rsc environment`, ); - id = id.split("?v=")[0]!; - // Copying over the hash from browser environment optimizer seems sound to avoid double modules, - // but this can cause stale hash when browser optimizer reloaded, so for now this is avoided. - // const hash = - // server.environments.client.depsOptimizer?.metadata.browserHash; - // if (hash) { - // id += `?v=${hash}`; - // } - } - - let importId: string; - let referenceKey: string; - if (packageSource) { + importId = `/@id/__x00__virtual:vite-rsc/client-in-server-package-proxy/${encodeURIComponent(id.split("?v=")[0]!)}`; + referenceKey = importId; + } else if (packageSource) { if (this.environment.mode === "dev") { importId = `/@id/__x00__virtual:vite-rsc/client-package-proxy/${packageSource}`; referenceKey = importId; @@ -858,6 +852,27 @@ function vitePluginUseClient(): Plugin[] { code = `export default {${code}};\n`; return { code, map: null }; }), + { + name: "rsc:virtual-client-in-server-package", + async load(id) { + if ( + id.startsWith("\0virtual:vite-rsc/client-in-server-package-proxy/") + ) { + assert.equal(this.environment.mode, "dev"); + assert.notEqual(this.environment.name, "rsc"); + id = decodeURIComponent( + id.slice( + "\0virtual:vite-rsc/client-in-server-package-proxy/".length, + ), + ); + return ` + export * from ${JSON.stringify(id)}; + import * as __all__ from ${JSON.stringify(id)}; + export default __all__.default; + `; + } + }, + }, { name: "rsc:virtual-client-package", resolveId: { From 767831bd298ca7666d53e2d6f86cd7283bad2dce Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Sun, 22 Jun 2025 18:46:22 +0900 Subject: [PATCH 08/11] test: tewak --- packages/rsc/examples/basic/e2e/basic.test.ts | 4 ++-- packages/rsc/examples/basic/package.json | 1 + .../src/routes/client-in-server/client.tsx | 6 ++++-- .../src/routes/client-in-server/server.tsx | 15 ++++++++++----- .../basic/test-dep/client-in-server/client.js | 16 +++------------- .../test-dep/client-in-server/package.json | 3 +-- .../basic/test-dep/client-in-server/server.js | 4 ++-- .../basic/test-dep/client-in-server2/client.js | 18 ++++++++++++++++++ .../test-dep/client-in-server2/package.json | 12 ++++++++++++ .../basic/test-dep/client-in-server2/server.js | 10 ++++++++++ packages/rsc/examples/basic/vite.config.ts | 6 ++++-- pnpm-lock.yaml | 12 ++++++++++++ 12 files changed, 79 insertions(+), 28 deletions(-) create mode 100644 packages/rsc/examples/basic/test-dep/client-in-server2/client.js create mode 100644 packages/rsc/examples/basic/test-dep/client-in-server2/package.json create mode 100644 packages/rsc/examples/basic/test-dep/client-in-server2/server.js diff --git a/packages/rsc/examples/basic/e2e/basic.test.ts b/packages/rsc/examples/basic/e2e/basic.test.ts index 6794832de..4dcd2c4ec 100644 --- a/packages/rsc/examples/basic/e2e/basic.test.ts +++ b/packages/rsc/examples/basic/e2e/basic.test.ts @@ -626,7 +626,7 @@ test("client-in-server package", async ({ page }) => { await expect(page.getByTestId("client-in-server")).toHaveText( "[test-client-in-server-dep: true]", ); - await expect(page.getByTestId("client-in-server-client")).toHaveText( - "[test-client-in-server-dep-direct-client: true]", + await expect(page.getByTestId("provider-in-server")).toHaveText( + "[test-provider-in-server-dep: true]", ); }); diff --git a/packages/rsc/examples/basic/package.json b/packages/rsc/examples/basic/package.json index 4c5d1e576..ab847955b 100644 --- a/packages/rsc/examples/basic/package.json +++ b/packages/rsc/examples/basic/package.json @@ -24,6 +24,7 @@ "@types/react-dom": "latest", "@vitejs/plugin-react": "latest", "@vitejs/test-dep-client-in-server": "file:./test-dep/client-in-server", + "@vitejs/test-dep-client-in-server2": "file:./test-dep/client-in-server2", "tailwindcss": "^4.1.4", "vite": "latest", "vite-plugin-inspect": "^11.2.0" diff --git a/packages/rsc/examples/basic/src/routes/client-in-server/client.tsx b/packages/rsc/examples/basic/src/routes/client-in-server/client.tsx index 5d1733d85..3acae58bc 100644 --- a/packages/rsc/examples/basic/src/routes/client-in-server/client.tsx +++ b/packages/rsc/examples/basic/src/routes/client-in-server/client.tsx @@ -1,6 +1,8 @@ "use client"; // @ts-ignore -import TestClientInServerDepClient from "@vitejs/test-dep-client-in-server/client"; +import { TestContextValue } from "@vitejs/test-dep-client-in-server2/client"; -export { TestClientInServerDepClient }; +export function TestContextValueIndirect() { + return ; +} diff --git a/packages/rsc/examples/basic/src/routes/client-in-server/server.tsx b/packages/rsc/examples/basic/src/routes/client-in-server/server.tsx index 39882c0be..904ebe7d4 100644 --- a/packages/rsc/examples/basic/src/routes/client-in-server/server.tsx +++ b/packages/rsc/examples/basic/src/routes/client-in-server/server.tsx @@ -1,6 +1,8 @@ // @ts-ignore -import TestClientInServerDep from "@vitejs/test-dep-client-in-server/server"; -import { TestClientInServerDepClient } from "./client"; +import { TestClientInServerDep } from "@vitejs/test-dep-client-in-server/server"; +// @ts-ignore +import { TestContextProviderInServer } from "@vitejs/test-dep-client-in-server2/server"; +import { TestContextValueIndirect } from "./client"; export function TestClientInServer() { return ( @@ -8,9 +10,12 @@ export function TestClientInServer() {
[test-client-in-server-dep: ]
-
- [test-client-in-server-dep-direct-client:{" "} - ] +
+ [test-provider-in-server-dep:{" "} + + + + ]
); diff --git a/packages/rsc/examples/basic/test-dep/client-in-server/client.js b/packages/rsc/examples/basic/test-dep/client-in-server/client.js index ef60ddb96..7f60db579 100644 --- a/packages/rsc/examples/basic/test-dep/client-in-server/client.js +++ b/packages/rsc/examples/basic/test-dep/client-in-server/client.js @@ -2,17 +2,7 @@ import React from "react"; -export default function TestClient() { - const hydrated = useHydrated(); - return React.createElement("span", null, String(hydrated)); -} - -const noop = () => {}; - -function useHydrated() { - return React.useSyncExternalStore( - noop, - () => true, - () => false, - ); +export function TestClient() { + const [ok] = React.useState(() => true) + return React.createElement("span", null, String(ok)); } diff --git a/packages/rsc/examples/basic/test-dep/client-in-server/package.json b/packages/rsc/examples/basic/test-dep/client-in-server/package.json index bd99518f4..68ab77952 100644 --- a/packages/rsc/examples/basic/test-dep/client-in-server/package.json +++ b/packages/rsc/examples/basic/test-dep/client-in-server/package.json @@ -3,8 +3,7 @@ "private": true, "type": "module", "exports": { - "./server": "./server.js", - "./client": "./client.js" + "./server": "./server.js" }, "peerDependencies": { "react": "*" diff --git a/packages/rsc/examples/basic/test-dep/client-in-server/server.js b/packages/rsc/examples/basic/test-dep/client-in-server/server.js index 074088e48..fda9c7ed1 100644 --- a/packages/rsc/examples/basic/test-dep/client-in-server/server.js +++ b/packages/rsc/examples/basic/test-dep/client-in-server/server.js @@ -1,6 +1,6 @@ import React from "react"; -import TestClient from "./client.js"; +import { TestClient } from "./client.js"; -export default async function TestClientInServer() { +export async function TestClientInServerDep() { return React.createElement(TestClient); } diff --git a/packages/rsc/examples/basic/test-dep/client-in-server2/client.js b/packages/rsc/examples/basic/test-dep/client-in-server2/client.js new file mode 100644 index 000000000..3854302d2 --- /dev/null +++ b/packages/rsc/examples/basic/test-dep/client-in-server2/client.js @@ -0,0 +1,18 @@ +"use client"; + +import React from "react"; + +const testContext = React.createContext(); + +export function TestContextProvider(props) { + return React.createElement( + testContext.Provider, + { value: props.value }, + props.children, + ); +} + +export function TestContextValue() { + const value = React.useContext(testContext); + return React.createElement("span", null, String(value)); +} diff --git a/packages/rsc/examples/basic/test-dep/client-in-server2/package.json b/packages/rsc/examples/basic/test-dep/client-in-server2/package.json new file mode 100644 index 000000000..fbc55fef5 --- /dev/null +++ b/packages/rsc/examples/basic/test-dep/client-in-server2/package.json @@ -0,0 +1,12 @@ +{ + "name": "@vitejs/test-dep-client-in-server2", + "private": true, + "type": "module", + "exports": { + "./server": "./server.js", + "./client": "./client.js" + }, + "peerDependencies": { + "react": "*" + } +} diff --git a/packages/rsc/examples/basic/test-dep/client-in-server2/server.js b/packages/rsc/examples/basic/test-dep/client-in-server2/server.js new file mode 100644 index 000000000..5a0f8727e --- /dev/null +++ b/packages/rsc/examples/basic/test-dep/client-in-server2/server.js @@ -0,0 +1,10 @@ +import React from "react"; +import { TestContextProvider } from "./client.js"; + +export function TestContextProviderInServer(props) { + return React.createElement( + TestContextProvider, + { value: props.value }, + props.children, + ); +} diff --git a/packages/rsc/examples/basic/vite.config.ts b/packages/rsc/examples/basic/vite.config.ts index 40bee17c4..3baf6cc5b 100644 --- a/packages/rsc/examples/basic/vite.config.ts +++ b/packages/rsc/examples/basic/vite.config.ts @@ -118,7 +118,9 @@ export default { fetch: handler }; minify: false, }, optimizeDeps: { - // TODO: test no double modules - exclude: ["@vitejs/test-dep-client-in-server/client"], + exclude: [ + "@vitejs/test-dep-client-in-server/client", + "@vitejs/test-dep-client-in-server2/client", + ], }, }) as any; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad4795177..120e25f52 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -587,6 +587,9 @@ importers: '@vitejs/test-dep-client-in-server': specifier: file:./test-dep/client-in-server version: file:packages/rsc/examples/basic/test-dep/client-in-server(react@19.1.0) + '@vitejs/test-dep-client-in-server2': + specifier: file:./test-dep/client-in-server2 + version: file:packages/rsc/examples/basic/test-dep/client-in-server2(react@19.1.0) tailwindcss: specifier: ^4.1.4 version: 4.1.4 @@ -2269,6 +2272,11 @@ packages: peerDependencies: vite: ^6.3.5 + '@vitejs/test-dep-client-in-server2@file:packages/rsc/examples/basic/test-dep/client-in-server2': + resolution: {directory: packages/rsc/examples/basic/test-dep/client-in-server2, type: directory} + peerDependencies: + react: ^19.1.0 + '@vitejs/test-dep-client-in-server@file:packages/rsc/examples/basic/test-dep/client-in-server': resolution: {directory: packages/rsc/examples/basic/test-dep/client-in-server, type: directory} peerDependencies: @@ -5861,6 +5869,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitejs/test-dep-client-in-server2@file:packages/rsc/examples/basic/test-dep/client-in-server2(react@19.1.0)': + dependencies: + react: 19.1.0 + '@vitejs/test-dep-client-in-server@file:packages/rsc/examples/basic/test-dep/client-in-server(react@19.1.0)': dependencies: react: 19.1.0 From aff7276157644b0b5a486f1411b84cb08d381157 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Sun, 22 Jun 2025 18:47:41 +0900 Subject: [PATCH 09/11] chore: lint --- packages/rsc/examples/basic/test-dep/client-in-server/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rsc/examples/basic/test-dep/client-in-server/client.js b/packages/rsc/examples/basic/test-dep/client-in-server/client.js index 7f60db579..959c1cbfd 100644 --- a/packages/rsc/examples/basic/test-dep/client-in-server/client.js +++ b/packages/rsc/examples/basic/test-dep/client-in-server/client.js @@ -3,6 +3,6 @@ import React from "react"; export function TestClient() { - const [ok] = React.useState(() => true) + const [ok] = React.useState(() => true); return React.createElement("span", null, String(ok)); } From 148d1882d1700d723d331371165cddeab5692011 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Sun, 22 Jun 2025 18:50:07 +0900 Subject: [PATCH 10/11] chore: comment --- packages/rsc/src/plugin.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/rsc/src/plugin.ts b/packages/rsc/src/plugin.ts index f67d3682f..3d1c13c44 100644 --- a/packages/rsc/src/plugin.ts +++ b/packages/rsc/src/plugin.ts @@ -771,10 +771,11 @@ function vitePluginUseClient(): Plugin[] { const packageSource = packageSources.get(id); if (!packageSource && id.includes("?v=")) { assert(this.environment.mode === "dev"); - // If non package source `?v=` reached here, this is a client boundary created - // by a package imported on server environment, which breaks the expectation on dependency optimizer on browser. - // Directory copying over "?v=" from client optimizer in client reference can make a hashed module stale. - // we use another virtual module wrapper to delay such process. + // If non package source `?v=` reached here, this is a client boundary + // created by a package imported on server environment, which breaks the + // expectation on dependency optimizer on browser. Directly copying over + // "?v=" from client optimizer in client reference can make a hashed + // module stale, so we use another virtual module wrapper to delay such process. // TODO: suggest `optimizeDeps.exclude` and skip warning if that's already the case. this.warn( `[vite-rsc] detected an internal client boundary created by a package imported on rsc environment`, From ad7212e03c32db4b1a645d030921135469d7fa22 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Sun, 22 Jun 2025 18:53:01 +0900 Subject: [PATCH 11/11] chore: comment --- packages/rsc/src/plugin.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/rsc/src/plugin.ts b/packages/rsc/src/plugin.ts index 3d1c13c44..2cb2299f8 100644 --- a/packages/rsc/src/plugin.ts +++ b/packages/rsc/src/plugin.ts @@ -866,6 +866,7 @@ function vitePluginUseClient(): Plugin[] { "\0virtual:vite-rsc/client-in-server-package-proxy/".length, ), ); + // TODO: avoid `export default undefined` return ` export * from ${JSON.stringify(id)}; import * as __all__ from ${JSON.stringify(id)};