fix: rsc react-server conditions and vite cors (#369)
Fixes react-server based conditional import in the RSC layer.
Fixes Vite CORS handling.
8be293dcde58Fixes react-server based conditional import in the RSC layer.
Fixes Vite CORS handling.
packages/react-server/lib/dev/create-server.mjs+74 -0pnpm-lock.yaml+8 -0test/__test__/basic.spec.mjs+9 -0test/fixtures/react-server-condition-pkg/default.mjs+2 -0test/fixtures/react-server-condition-pkg/package.json+10 -0test/fixtures/react-server-condition-pkg/server.mjs+2 -0test/fixtures/react-server-condition.jsx+10 -0test/package.json+1 -0packages/react-server/lib/dev/create-server.mjs+74 -0pnpm-lock.yaml+8 -0test/__test__/basic.spec.mjs+9 -0| 451 | 451 | }, | |
| 452 | 452 | }, | |
| 453 | 453 | rsc: { | |
| 454 | + | resolve: { | |
| 455 | + | conditions: ["react-server"], | |
| 456 | + | }, | |
| 454 | 457 | dev: { | |
| 455 | 458 | createEnvironment: (name, config) => | |
| 456 | 459 | createRunnableDevEnvironment(name, config, { |
| 541 | 544 | const viteDevServer = await createViteDevServer(viteConfig); | |
| 542 | 545 | viteCreateSpan.end(); | |
| 543 | 546 | | |
| 547 | + | // Inject a Connect-level CORS middleware at the very front of the stack so | |
| 548 | + | // that Vite-handled requests (module transforms, static assets, HMR) also | |
| 549 | + | // receive proper CORS headers. The react-server CORS middleware in the | |
| 550 | + | // composed handler chain only covers requests that reach the SSR handler, | |
| 551 | + | // but Vite's internal middlewares respond earlier and would otherwise send | |
| 552 | + | // responses without any Access-Control-* headers. | |
| 553 | + | if (corsEnabled) { | |
| 554 | + | const _serverCors = serverCors || {}; | |
| 555 | + | const _originFn = | |
| 556 | + | typeof _serverCors.origin === "function" ? _serverCors.origin : null; | |
| 557 | + | const _staticOrigin = _originFn ? null : (_serverCors.origin ?? "*"); | |
| 558 | + | const _credentials = _serverCors.credentials ?? false; | |
| 559 | + | | |
| 560 | + | // unshift onto Connect's stack so this runs before all Vite-internal | |
| 561 | + | // middlewares (which are already registered by createViteDevServer). | |
| 562 | + | viteDevServer.middlewares.stack.unshift({ | |
| 563 | + | route: "", | |
| 564 | + | handle: function viteCorsShim(req, res, next) { | |
| 565 | + | const requestOrigin = req.headers.origin; | |
| 566 | + | if (!requestOrigin) return next(); | |
| 567 | + | | |
| 568 | + | let allowed; | |
| 569 | + | if (_originFn) { | |
| 570 | + | // The origin function expects a context-like object; build a minimal | |
| 571 | + | // shim that matches what the react-server CORS middleware receives. | |
| 572 | + | allowed = _originFn({ | |
| 573 | + | request: { | |
| 574 | + | headers: { | |
| 575 | + | get: (name) => req.headers[name.toLowerCase()], | |
| 576 | + | }, | |
| 577 | + | }, | |
| 578 | + | }); | |
| 579 | + | } else { | |
| 580 | + | allowed = _staticOrigin === true ? requestOrigin : _staticOrigin; | |
| 581 | + | } | |
| 582 | + | | |
| 583 | + | // allowed may be a promise when using the default dynamic origin | |
| 584 | + | Promise.resolve(allowed).then((origin) => { | |
| 585 | + | const effectiveOrigin = | |
| 586 | + | origin === true ? requestOrigin : origin || requestOrigin; | |
| 587 | + | res.setHeader("access-control-allow-origin", effectiveOrigin); | |
| 588 | + | if (_credentials) { | |
| 589 | + | res.setHeader("access-control-allow-credentials", "true"); | |
| 590 | + | } | |
| 591 | + | if (req.method === "OPTIONS") { | |
| 592 | + | res.setHeader( | |
| 593 | + | "access-control-allow-methods", | |
| 594 | + | _serverCors.allowMethods || "GET,HEAD,PUT,PATCH,POST,DELETE" | |
| 595 | + | ); | |
| 596 | + | const allowHeaders = | |
| 597 | + | _serverCors.allowHeaders || | |
| 598 | + | req.headers["access-control-request-headers"]; | |
| 599 | + | if (allowHeaders) { | |
| 600 | + | res.setHeader("access-control-allow-headers", allowHeaders); | |
| 601 | + | } | |
| 602 | + | if (_serverCors.maxAge) { | |
| 603 | + | res.setHeader( | |
| 604 | + | "access-control-max-age", | |
| 605 | + | String(_serverCors.maxAge) | |
| 606 | + | ); | |
| 607 | + | } | |
| 608 | + | res.statusCode = 204; | |
| 609 | + | res.end(); | |
| 610 | + | return; | |
| 611 | + | } | |
| 612 | + | next(); | |
| 613 | + | }); | |
| 614 | + | }, | |
| 615 | + | }); | |
| 616 | + | } | |
| 617 | + | | |
| 544 | 618 | if (config.envDir !== false) { | |
| 545 | 619 | if (globalThis.__react_server_prev_env_keys__) { | |
| 546 | 620 | for (const key of globalThis.__react_server_prev_env_keys__) { |
| 1249 | 1249 | picomatch: | |
| 1250 | 1250 | specifier: ^4.0.2 | |
| 1251 | 1251 | version: 4.0.2 | |
| 1252 | + | react-server-condition-pkg: | |
| 1253 | + | specifier: file:fixtures/react-server-condition-pkg | |
| 1254 | + | version: file:test/fixtures/react-server-condition-pkg | |
| 1252 | 1255 | rolldown: | |
| 1253 | 1256 | specifier: 1.0.0-rc.12 | |
| 1254 | 1257 | version: 1.0.0-rc.12 |
| 10093 | 10096 | peerDependencies: | |
| 10094 | 10097 | react: 0.0.0-experimental-46103596-20260305 | |
| 10095 | 10098 | | |
| 10099 | + | react-server-condition-pkg@file:test/fixtures/react-server-condition-pkg: | |
| 10100 | + | resolution: {directory: test/fixtures/react-server-condition-pkg, type: directory} | |
| 10101 | + | | |
| 10096 | 10102 | react-server-dom-webpack@0.0.0-experimental-46103596-20260305: | |
| 10097 | 10103 | resolution: {integrity: sha512-2SgiuhasLeadCbu5Ddaezp2epqnqqNa+/a0mYs4lZzp4nE6Po5TDmNIwb80pOuKVNKt2qaF1AKimgX/OB9auyA==} | |
| 10098 | 10104 | engines: {node: '>=0.10.0'} |
| 22311 | 22317 | '@remix-run/router': 1.17.0 | |
| 22312 | 22318 | react: 0.0.0-experimental-46103596-20260305 | |
| 22313 | 22319 | | |
| 22320 | + | react-server-condition-pkg@file:test/fixtures/react-server-condition-pkg: {} | |
| 22321 | + | | |
| 22314 | 22322 | 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)): | |
| 22315 | 22323 | dependencies: | |
| 22316 | 22324 | acorn-loose: 8.4.0 |
| 202 | 202 | } | |
| 203 | 203 | ); | |
| 204 | 204 | | |
| 205 | + | test("react-server export condition", async () => { | |
| 206 | + | await server("fixtures/react-server-condition.jsx"); | |
| 207 | + | await page.goto(hostname); | |
| 208 | + | expect(await page.textContent("#message")).toBe( | |
| 209 | + | "from react-server condition" | |
| 210 | + | ); | |
| 211 | + | expect(await page.textContent("#source")).toBe("server"); | |
| 212 | + | }); | |
| 213 | + | | |
| 205 | 214 | test("navigation location", async () => { | |
| 206 | 215 | await server("fixtures/navigation-location.jsx"); | |
| 207 | 216 | await page.goto(`${hostname}/pathname?foo=bar`); |
test/fixtures/react-server-condition-pkg/default.mjs+2 -0| 1 | + | export const message = "from default condition"; | |
| 2 | + | export const source = "client"; |
test/fixtures/react-server-condition-pkg/package.json+10 -0| 1 | + | { | |
| 2 | + | "name": "react-server-condition-pkg", | |
| 3 | + | "type": "module", | |
| 4 | + | "exports": { | |
| 5 | + | ".": { | |
| 6 | + | "react-server": "./server.mjs", | |
| 7 | + | "default": "./default.mjs" | |
| 8 | + | } | |
| 9 | + | } | |
| 10 | + | } |
test/fixtures/react-server-condition-pkg/server.mjs+2 -0| 1 | + | export const message = "from react-server condition"; | |
| 2 | + | export const source = "server"; |
test/fixtures/react-server-condition.jsx+10 -0| 1 | + | import { message, source } from "react-server-condition-pkg"; | |
| 2 | + | | |
| 3 | + | export default function ReactServerCondition() { | |
| 4 | + | return ( | |
| 5 | + | <div> | |
| 6 | + | <span id="message">{message}</span> | |
| 7 | + | <span id="source">{source}</span> | |
| 8 | + | </div> | |
| 9 | + | ); | |
| 10 | + | } |
test/package.json+1 -0| 16 | 16 | "@lazarv/react-server": "workspace:*", | |
| 17 | 17 | "idb-keyval": "^6.2.2", | |
| 18 | 18 | "picomatch": "^4.0.2", | |
| 19 | + | "react-server-condition-pkg": "file:fixtures/react-server-condition-pkg", | |
| 19 | 20 | "rolldown": "1.0.0-rc.12", | |
| 20 | 21 | "tinyglobby": "^0.2.13", | |
| 21 | 22 | "unstorage": "^1.16.0", |