diff --git a/docs/src/pages/en/(pages)/features/cli.mdx b/docs/src/pages/en/(pages)/features/cli.mdx
index 599648c2..ecd99ec1 100644
--- a/docs/src/pages/en/(pages)/features/cli.mdx
+++ b/docs/src/pages/en/(pages)/features/cli.mdx
@@ -62,12 +62,6 @@ Use HTTPS protocol. Default is `false`.
When you want to use HTTPS for your development server. You need to specify your HTTPS configuration in your `react-server.config.mjs` or `vite.config.mjs` file. See details in the Vite documentation at [server.https](https://vitejs.dev/config/server-options.html#server-https).
-
-### --open
-
-
-Open browser on server start. Default is `false`. Opens your app in the default browser.
-
### --cors
@@ -76,6 +70,24 @@ Enable CORS. Default is `false`.
This is useful when you want to allow cross-origin requests. If you need more detailed CORS configuration, you can define the CORS configuration in your `react-server.config.mjs` or `vite.config.mjs` file. See details in the Vite documentation at [server.cors](https://vitejs.dev/config/server-options.html#server-cors).
+
+### --origin
+
+
+Specify the origin part of the URL to a constant value. Same as using the `ORIGIN` environment variable.
+
+
+### --trust-proxy
+
+
+Trust `X-Forwarded-*` headers.
+
+
+### --open
+
+
+Open browser on server start. Default is `false`. Opens your app in the default browser.
+
### --force
diff --git a/docs/src/pages/ja/(pages)/features/cli.mdx b/docs/src/pages/ja/(pages)/features/cli.mdx
index 02d4cadd..1b4726d6 100644
--- a/docs/src/pages/ja/(pages)/features/cli.mdx
+++ b/docs/src/pages/ja/(pages)/features/cli.mdx
@@ -62,12 +62,6 @@ HTTPSプロトコルを有効にする場合に指定します。デフォルト
もし開発サーバーでHTTPSプロトコルを使う場合、`react-server.config.mjs`または`vite.config.mjs`でHTTPSの設定をする必要があります。詳しくはViteドキュメントの[server.https](https://ja.vite.dev/config/server-options#server-https)を参照してください。
-
-### --open
-
-
-サーバー起動時にデフォルトブラウザを開くか指定します。デフォルトは`false`です。デフォルトブラウザでアプリケーションにアクセスします。
-
### --cors
@@ -76,6 +70,24 @@ CORSを有効にします。デフォルトは`false`です。
もしオリジン間リソース共有を有効化したい場合は便利です。より詳細なCORS設定が必要な場合は`react-server.config.mjs`または`vite.config.mjs`で設定することが出来ます。詳しくはViteドキュメントの[server.cors](https://ja.vite.dev/config/server-options#server-cors)を参照してください。
+
+### --origin
+
+
+URLのオリジン部分を定数に指定します。環境変数`ORIGIN`を使っても同じ結果になります。
+
+
+### --trust-proxy
+
+
+`X-Forwarded-*`ヘッダーを信頼します。
+
+
+### --open
+
+
+サーバー起動時にデフォルトブラウザを開くか指定します。デフォルトは`false`です。デフォルトブラウザでアプリケーションにアクセスします。
+
### --force
diff --git a/packages/react-server/bin/commands/dev.mjs b/packages/react-server/bin/commands/dev.mjs
index 93159776..e0d2092c 100644
--- a/packages/react-server/bin/commands/dev.mjs
+++ b/packages/react-server/bin/commands/dev.mjs
@@ -10,10 +10,12 @@ export default (cli) =>
})
.option("--port ", "[number] port to listen on", { default: 3000 })
.option("--https", "[boolean] use HTTPS protocol", { default: false })
+ .option("--cors", "enable CORS", { default: false })
+ .option("--origin ", "[string] origin", { default: "" })
+ .option("--trust-proxy", "[boolean] trust proxy", { default: false })
.option("--open [url]", "[boolean|string] open browser on server start", {
default: false,
})
- .option("--cors", "enable CORS", { default: false })
.option("--force", "force optimize deps", { default: false })
.option("--watch", "watch for config changes", { default: true })
.option("--clear-screen", "clear screen on server start", {
diff --git a/packages/react-server/cache/rsc-browser.mjs b/packages/react-server/cache/rsc-browser.mjs
index 4a73939e..240b8ddc 100644
--- a/packages/react-server/cache/rsc-browser.mjs
+++ b/packages/react-server/cache/rsc-browser.mjs
@@ -16,9 +16,12 @@ function concat(buffers) {
return result;
}
+const isDev = typeof import.meta.env !== "undefined" && !!import.meta.env.DEV;
+
export function toBuffer(model, options = {}) {
return new Promise(async (resolve, reject) => {
const stream = renderToReadableStream(model, {
+ debug: isDev,
...options,
onError(error) {
reject(error);
@@ -35,7 +38,7 @@ export function toBuffer(model, options = {}) {
}
export async function toStream(model, options = {}) {
- return renderToReadableStream(model, options);
+ return renderToReadableStream(model, { debug: isDev, ...options });
}
export function fromBuffer(payload, options = {}) {
diff --git a/packages/react-server/cache/rsc.mjs b/packages/react-server/cache/rsc.mjs
index 67bf8259..f893e962 100644
--- a/packages/react-server/cache/rsc.mjs
+++ b/packages/react-server/cache/rsc.mjs
@@ -3,12 +3,15 @@ import { renderToReadableStream } from "@lazarv/rsc/server";
import { concat, copyBytesFrom } from "../lib/sys.mjs";
+const isDev = typeof import.meta.env !== "undefined" && !!import.meta.env.DEV;
+
export function toBuffer(model, options = {}) {
return new Promise(async (resolve, reject) => {
const { clientReferenceMap } =
await import("@lazarv/react-server/dist/server/client-reference-map");
const map = clientReferenceMap();
const stream = renderToReadableStream(model, {
+ debug: isDev,
...options,
moduleResolver: {
resolveClientReference(value) {
@@ -34,6 +37,7 @@ export async function toStream(model, options = {}) {
await import("@lazarv/react-server/dist/server/client-reference-map");
const map = clientReferenceMap();
return renderToReadableStream(model, {
+ debug: isDev,
...options,
moduleResolver: {
resolveClientReference(value) {
diff --git a/packages/react-server/client/ReactServerComponent.jsx b/packages/react-server/client/ReactServerComponent.jsx
index 27bf375f..5a196a40 100644
--- a/packages/react-server/client/ReactServerComponent.jsx
+++ b/packages/react-server/client/ReactServerComponent.jsx
@@ -19,6 +19,26 @@ import {
useClient,
} from "./context.mjs";
+// Execute scripts stored as by dom-flight.mjs
+// to avoid React's "Encountered a script tag" warning during SSR/RSC rendering.
+// We leave the template in the DOM so React can still reconcile its fiber tree.
+function activateScriptTemplates(root) {
+ if (typeof document === "undefined") return;
+ root.querySelectorAll("template[data-script-attrs]").forEach((template) => {
+ if (template.dataset.activated) return;
+ template.dataset.activated = "";
+ const attrs = JSON.parse(template.dataset.scriptAttrs);
+ const script = document.createElement("script");
+ for (const [key, value] of Object.entries(attrs)) {
+ script.setAttribute(key, value);
+ }
+ script.textContent = template.content.textContent;
+ // Append to execute, then remove the script (not the template).
+ document.head.appendChild(script);
+ script.remove();
+ });
+}
+
function FlightComponent({
remote = false,
defer = false,
@@ -39,23 +59,36 @@ function FlightComponent({
createTemporaryReferenceSet,
encodeReply,
} = client;
- const [{ resourceKey, error, Component }, setComponent] = useState({
- resourceKey: 0,
- error: null,
- Component:
- children ||
- (outlet === PAGE_ROOT || remote
- ? getFlightResponse?.(url, {
- outlet,
- remote,
- remoteProps,
- temporaryReferences: remoteProps
- ? createRemoteTemporaryReferenceSet(remoteProps)
- : null,
- defer,
- request,
- })
- : null),
+ const [{ resourceKey, error, Component }, setComponent] = useState(() => {
+ // Activate script templates before first getFlightResponse so the
+ // __flightStream__ globals are available for hydration.
+ if (typeof document !== "undefined") {
+ if (isolate) {
+ const host = document.getElementById(`shadowroot_${outlet}`);
+ if (host?.shadowRoot) {
+ activateScriptTemplates(host.shadowRoot);
+ }
+ }
+ activateScriptTemplates(document);
+ }
+ return {
+ resourceKey: 0,
+ error: null,
+ Component:
+ children ||
+ (outlet === PAGE_ROOT || remote
+ ? getFlightResponse?.(url, {
+ outlet,
+ remote,
+ remoteProps,
+ temporaryReferences: remoteProps
+ ? createRemoteTemporaryReferenceSet(remoteProps)
+ : null,
+ defer,
+ request,
+ })
+ : null),
+ };
});
const errorRef = useRef(null);
const componentPromiseRef = useRef(null);
@@ -184,6 +217,16 @@ function FlightComponent({
})()
: Promise.resolve({})
).then(({ temporaryReferences, body }) => {
+ // Activate any new script templates before reading flight stream
+ if (typeof document !== "undefined") {
+ if (isolate) {
+ const host = document.getElementById(`shadowroot_${outlet}`);
+ if (host?.shadowRoot) {
+ activateScriptTemplates(host.shadowRoot);
+ }
+ }
+ activateScriptTemplates(document);
+ }
getFlightResponse(url, {
outlet,
remote,
diff --git a/packages/react-server/lib/dev/create-server.mjs b/packages/react-server/lib/dev/create-server.mjs
index b0099b64..aef773a5 100644
--- a/packages/react-server/lib/dev/create-server.mjs
+++ b/packages/react-server/lib/dev/create-server.mjs
@@ -987,7 +987,20 @@ export default async function createServer(root, options) {
: [...initialHandlers, ...(config.handlers ?? [])]
);
- viteDevServer.middlewares.use(createMiddleware(composedHandlers));
+ viteDevServer.middlewares.use(
+ createMiddleware(composedHandlers, {
+ origin:
+ options.origin ??
+ sys.getEnv("ORIGIN") ??
+ config.server?.origin ??
+ `${
+ config.server?.https || options.https ? "https" : "http"
+ }://${options.host ?? sys.getEnv("HOST") ?? config.server?.host ?? "localhost"}:$${
+ options.port ?? sys.getEnv("PORT") ?? config.server?.port ?? 3000
+ }`,
+ trustProxy: config.server?.trustProxy ?? options.trustProxy,
+ })
+ );
const localHostnames = new Set([
"localhost",
diff --git a/packages/react-server/server/dom-flight.mjs b/packages/react-server/server/dom-flight.mjs
index c78930a8..2a61efe8 100644
--- a/packages/react-server/server/dom-flight.mjs
+++ b/packages/react-server/server/dom-flight.mjs
@@ -10,8 +10,25 @@ export default function visit(node, context) {
return node.value;
case "#comment":
return null;
+ case "script":
+ node.nodeName = "template";
+ node.attrs = [
+ {
+ name: "data-script-attrs",
+ value: JSON.stringify(
+ node.attrs.reduce((attrs, attr) => {
+ attrs[attr.name] = attr.value;
+ return attrs;
+ }, {})
+ ),
+ },
+ ];
default: {
- if (node.nodeName === "template" && context.defer) {
+ if (
+ node.nodeName === "template" &&
+ context.defer &&
+ !node.attrs?.some((attr) => attr.name === "data-script")
+ ) {
return null;
}
const childNodes =
diff --git a/packages/rsc/server/shared.mjs b/packages/rsc/server/shared.mjs
index f20186fd..77671aee 100644
--- a/packages/rsc/server/shared.mjs
+++ b/packages/rsc/server/shared.mjs
@@ -1320,6 +1320,23 @@ function serializeElement(request, element) {
// Keyless Fragment - output children as plain array
const children = props?.children;
if (Array.isArray(children)) {
+ // Mark keyless element children as needing validation (validated=2)
+ // to match React's renderFragment behavior. This ensures the Flight
+ // client-side reconciler correctly warns about missing keys.
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i];
+ if (
+ child !== null &&
+ typeof child === "object" &&
+ isReactElement(child) &&
+ child.key === null &&
+ !child._store?.validated
+ ) {
+ if (child._store) {
+ child._store.validated = 2;
+ }
+ }
+ }
return children.map((child, i) =>
serializeValue(request, child, props, i)
);
@@ -1492,7 +1509,7 @@ function serializeElement(request, element) {
// Build the element tuple
// Production format: ["$", type, key, props]
- // Dev format: ["$", type, key, props, owner?, debugStack?, debugCounter?]
+ // Dev format: ["$", type, key, props, owner?, debugStack?, validated?]
const tuple = [
"$",
serializedType,
@@ -1573,9 +1590,16 @@ function serializeElement(request, element) {
// Add debug stack reference (6th element)
tuple.push(debugStackRef);
- // Add debug counter (7th element) - increments for each element
- request.debugCounter++;
- tuple.push(request.debugCounter);
+ // Add validated flag (7th element) - matches React's Flight protocol
+ // where position 6 carries `element._store.validated`:
+ // 0 = not yet validated
+ // 1 = already validated (key check passed or set by parent)
+ // 2 = needs validation (element is in an array without a key)
+ // The Flight client reads this into `_store.validated` on deserialized
+ // elements, and react-dom's reconciler uses it to decide whether to
+ // warn about missing keys.
+ const validated = element._store?.validated ?? 0;
+ tuple.push(validated);
}
return tuple;