From 6e73bdaebbd687e7094f46dbc478ad3ded57bf05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20L=C3=A1z=C3=A1r?= Date: Thu, 26 Mar 2026 16:56:45 +0100 Subject: [PATCH] fix: rsc react-server conditions and vite cors --- .../react-server/lib/dev/create-server.mjs | 74 +++++++++++++++++++ pnpm-lock.yaml | 8 ++ test/__test__/basic.spec.mjs | 9 +++ .../react-server-condition-pkg/default.mjs | 2 + .../react-server-condition-pkg/package.json | 10 +++ .../react-server-condition-pkg/server.mjs | 2 + test/fixtures/react-server-condition.jsx | 10 +++ test/package.json | 1 + 8 files changed, 116 insertions(+) create mode 100644 test/fixtures/react-server-condition-pkg/default.mjs create mode 100644 test/fixtures/react-server-condition-pkg/package.json create mode 100644 test/fixtures/react-server-condition-pkg/server.mjs create mode 100644 test/fixtures/react-server-condition.jsx diff --git a/packages/react-server/lib/dev/create-server.mjs b/packages/react-server/lib/dev/create-server.mjs index 3f49c76e..46636007 100644 --- a/packages/react-server/lib/dev/create-server.mjs +++ b/packages/react-server/lib/dev/create-server.mjs @@ -451,6 +451,9 @@ export default async function createServer(root, options) { }, }, rsc: { + resolve: { + conditions: ["react-server"], + }, dev: { createEnvironment: (name, config) => createRunnableDevEnvironment(name, config, { @@ -541,6 +544,77 @@ export default async function createServer(root, options) { const viteDevServer = await createViteDevServer(viteConfig); viteCreateSpan.end(); + // Inject a Connect-level CORS middleware at the very front of the stack so + // that Vite-handled requests (module transforms, static assets, HMR) also + // receive proper CORS headers. The react-server CORS middleware in the + // composed handler chain only covers requests that reach the SSR handler, + // but Vite's internal middlewares respond earlier and would otherwise send + // responses without any Access-Control-* headers. + if (corsEnabled) { + const _serverCors = serverCors || {}; + const _originFn = + typeof _serverCors.origin === "function" ? _serverCors.origin : null; + const _staticOrigin = _originFn ? null : (_serverCors.origin ?? "*"); + const _credentials = _serverCors.credentials ?? false; + + // unshift onto Connect's stack so this runs before all Vite-internal + // middlewares (which are already registered by createViteDevServer). + viteDevServer.middlewares.stack.unshift({ + route: "", + handle: function viteCorsShim(req, res, next) { + const requestOrigin = req.headers.origin; + if (!requestOrigin) return next(); + + let allowed; + if (_originFn) { + // The origin function expects a context-like object; build a minimal + // shim that matches what the react-server CORS middleware receives. + allowed = _originFn({ + request: { + headers: { + get: (name) => req.headers[name.toLowerCase()], + }, + }, + }); + } else { + allowed = _staticOrigin === true ? requestOrigin : _staticOrigin; + } + + // allowed may be a promise when using the default dynamic origin + Promise.resolve(allowed).then((origin) => { + const effectiveOrigin = + origin === true ? requestOrigin : origin || requestOrigin; + res.setHeader("access-control-allow-origin", effectiveOrigin); + if (_credentials) { + res.setHeader("access-control-allow-credentials", "true"); + } + if (req.method === "OPTIONS") { + res.setHeader( + "access-control-allow-methods", + _serverCors.allowMethods || "GET,HEAD,PUT,PATCH,POST,DELETE" + ); + const allowHeaders = + _serverCors.allowHeaders || + req.headers["access-control-request-headers"]; + if (allowHeaders) { + res.setHeader("access-control-allow-headers", allowHeaders); + } + if (_serverCors.maxAge) { + res.setHeader( + "access-control-max-age", + String(_serverCors.maxAge) + ); + } + res.statusCode = 204; + res.end(); + return; + } + next(); + }); + }, + }); + } + if (config.envDir !== false) { if (globalThis.__react_server_prev_env_keys__) { for (const key of globalThis.__react_server_prev_env_keys__) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6499f95f..b4ecc434 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1249,6 +1249,9 @@ importers: picomatch: specifier: ^4.0.2 version: 4.0.2 + react-server-condition-pkg: + specifier: file:fixtures/react-server-condition-pkg + version: file:test/fixtures/react-server-condition-pkg rolldown: specifier: 1.0.0-rc.12 version: 1.0.0-rc.12 @@ -10093,6 +10096,9 @@ packages: peerDependencies: react: 0.0.0-experimental-46103596-20260305 + react-server-condition-pkg@file:test/fixtures/react-server-condition-pkg: + resolution: {directory: test/fixtures/react-server-condition-pkg, type: directory} + react-server-dom-webpack@0.0.0-experimental-46103596-20260305: resolution: {integrity: sha512-2SgiuhasLeadCbu5Ddaezp2epqnqqNa+/a0mYs4lZzp4nE6Po5TDmNIwb80pOuKVNKt2qaF1AKimgX/OB9auyA==} engines: {node: '>=0.10.0'} @@ -22311,6 +22317,8 @@ snapshots: '@remix-run/router': 1.17.0 react: 0.0.0-experimental-46103596-20260305 + react-server-condition-pkg@file:test/fixtures/react-server-condition-pkg: {} + react-server-dom-webpack@0.0.0-experimental-46103596-20260305(react-dom@0.0.0-experimental-46103596-20260305(react@0.0.0-experimental-46103596-20260305))(react@0.0.0-experimental-46103596-20260305)(webpack@5.97.1(@swc/core@1.11.21)): dependencies: acorn-loose: 8.4.0 diff --git a/test/__test__/basic.spec.mjs b/test/__test__/basic.spec.mjs index 1d67fb85..32a6f325 100644 --- a/test/__test__/basic.spec.mjs +++ b/test/__test__/basic.spec.mjs @@ -202,6 +202,15 @@ test.skipIf(process.env.EDGE_ENTRY)( } ); +test("react-server export condition", async () => { + await server("fixtures/react-server-condition.jsx"); + await page.goto(hostname); + expect(await page.textContent("#message")).toBe( + "from react-server condition" + ); + expect(await page.textContent("#source")).toBe("server"); +}); + test("navigation location", async () => { await server("fixtures/navigation-location.jsx"); await page.goto(`${hostname}/pathname?foo=bar`); diff --git a/test/fixtures/react-server-condition-pkg/default.mjs b/test/fixtures/react-server-condition-pkg/default.mjs new file mode 100644 index 00000000..fb5dd1d9 --- /dev/null +++ b/test/fixtures/react-server-condition-pkg/default.mjs @@ -0,0 +1,2 @@ +export const message = "from default condition"; +export const source = "client"; diff --git a/test/fixtures/react-server-condition-pkg/package.json b/test/fixtures/react-server-condition-pkg/package.json new file mode 100644 index 00000000..b1c9a50e --- /dev/null +++ b/test/fixtures/react-server-condition-pkg/package.json @@ -0,0 +1,10 @@ +{ + "name": "react-server-condition-pkg", + "type": "module", + "exports": { + ".": { + "react-server": "./server.mjs", + "default": "./default.mjs" + } + } +} diff --git a/test/fixtures/react-server-condition-pkg/server.mjs b/test/fixtures/react-server-condition-pkg/server.mjs new file mode 100644 index 00000000..564a3394 --- /dev/null +++ b/test/fixtures/react-server-condition-pkg/server.mjs @@ -0,0 +1,2 @@ +export const message = "from react-server condition"; +export const source = "server"; diff --git a/test/fixtures/react-server-condition.jsx b/test/fixtures/react-server-condition.jsx new file mode 100644 index 00000000..851849b9 --- /dev/null +++ b/test/fixtures/react-server-condition.jsx @@ -0,0 +1,10 @@ +import { message, source } from "react-server-condition-pkg"; + +export default function ReactServerCondition() { + return ( +
+ {message} + {source} +
+ ); +} diff --git a/test/package.json b/test/package.json index 1b1f5028..b3348fb0 100644 --- a/test/package.json +++ b/test/package.json @@ -16,6 +16,7 @@ "@lazarv/react-server": "workspace:*", "idb-keyval": "^6.2.2", "picomatch": "^4.0.2", + "react-server-condition-pkg": "file:fixtures/react-server-condition-pkg", "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.13", "unstorage": "^1.16.0",