Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions docs/src/pages/en/(pages)/features/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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).

<Link name="dev-open">
### --open
</Link>

Open browser on server start. Default is `false`. Opens your app in the default browser.

<Link name="dev-cors">
### --cors
</Link>
Expand All @@ -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).

<Link name="dev-origin">
### --origin
</Link>

Specify the origin part of the URL to a constant value. Same as using the `ORIGIN` environment variable.

<Link name="dev-trust-proxy">
### --trust-proxy
</Link>

Trust `X-Forwarded-*` headers.

<Link name="dev-open">
### --open
</Link>

Open browser on server start. Default is `false`. Opens your app in the default browser.

<Link name="dev-force">
### --force
</Link>
Expand Down
24 changes: 18 additions & 6 deletions docs/src/pages/ja/(pages)/features/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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)を参照してください。

<Link name="dev-open">
### --open
</Link>

サーバー起動時にデフォルトブラウザを開くか指定します。デフォルトは`false`です。デフォルトブラウザでアプリケーションにアクセスします。

<Link name="dev-cors">
### --cors
</Link>
Expand All @@ -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)を参照してください。

<Link name="dev-origin">
### --origin
</Link>

URLのオリジン部分を定数に指定します。環境変数`ORIGIN`を使っても同じ結果になります。

<Link name="dev-trust-proxy">
### --trust-proxy
</Link>

`X-Forwarded-*`ヘッダーを信頼します。

<Link name="dev-open">
### --open
</Link>

サーバー起動時にデフォルトブラウザを開くか指定します。デフォルトは`false`です。デフォルトブラウザでアプリケーションにアクセスします。

<Link name="dev-force">
### --force
</Link>
Expand Down
4 changes: 3 additions & 1 deletion packages/react-server/bin/commands/dev.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ export default (cli) =>
})
.option("--port <port>", "[number] port to listen on", { default: 3000 })
.option("--https", "[boolean] use HTTPS protocol", { default: false })
.option("--cors", "enable CORS", { default: false })
.option("--origin <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", {
Expand Down
5 changes: 4 additions & 1 deletion packages/react-server/cache/rsc-browser.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 = {}) {
Expand Down
4 changes: 4 additions & 0 deletions packages/react-server/cache/rsc.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
77 changes: 60 additions & 17 deletions packages/react-server/client/ReactServerComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,26 @@ import {
useClient,
} from "./context.mjs";

// Execute scripts stored as <template data-script-attrs> 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,
Expand All @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
15 changes: 14 additions & 1 deletion packages/react-server/lib/dev/create-server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
19 changes: 18 additions & 1 deletion packages/react-server/server/dom-flight.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
32 changes: 28 additions & 4 deletions packages/rsc/server/shared.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down