diff --git a/integration/single-fetch-test.ts b/integration/single-fetch-test.ts
index 38e6dd9ac3..7fa939a816 100644
--- a/integration/single-fetch-test.ts
+++ b/integration/single-fetch-test.ts
@@ -1583,6 +1583,89 @@ test.describe("single-fetch", () => {
expect(errorLogs.length).toBe(1);
});
+ test("supports nonce on streaming script tags", async ({ page }) => {
+ let fixture = await createFixture({
+ files: {
+ ...files,
+ "app/root.tsx": js`
+ import { Links, Meta, Outlet, Scripts } from "react-router";
+ export function loader() {
+ return {
+ message: "ROOT",
+ };
+ }
+ export default function Root() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+ `,
+ "app/entry.server.tsx": js`
+ import { PassThrough } from "node:stream";
+ import type { EntryContext } from "@react-router/node";
+ import { createReadableStreamFromReadable } from "@react-router/node";
+ import { RemixServer } from "react-router";
+ import { renderToPipeableStream } from "react-dom/server";
+ export default function handleRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext
+ ) {
+ return new Promise((resolve, reject) => {
+ const { pipe } = renderToPipeableStream(
+ ,
+ {
+ onShellReady() {
+ const body = new PassThrough();
+ const stream = createReadableStreamFromReadable(body);
+ responseHeaders.set("Content-Type", "text/html");
+ resolve(
+ new Response(stream, {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ })
+ );
+ pipe(body);
+ },
+ onShellError(error: unknown) {
+ reject(error);
+ },
+ onError(error: unknown) {
+ responseStatusCode = 500;
+ },
+ }
+ );
+ });
+ }
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/data", true);
+ let scripts = await page.$$("script");
+ expect(scripts.length).toBe(6);
+ let remixScriptsCount = 0;
+ for (let script of scripts) {
+ let content = await script.innerHTML();
+ if (content.includes("window.__remix")) {
+ remixScriptsCount++;
+ expect(await script.getAttribute("nonce")).toEqual("the-nonce");
+ }
+ }
+ expect(remixScriptsCount).toBe(4);
+ });
+
test.describe("client loaders", () => {
test("when no routes have client loaders", async ({ page }) => {
let fixture = await createFixture(
diff --git a/packages/react-router/lib/dom/ssr/server.tsx b/packages/react-router/lib/dom/ssr/server.tsx
index fd83438d12..57d95a12dd 100644
--- a/packages/react-router/lib/dom/ssr/server.tsx
+++ b/packages/react-router/lib/dom/ssr/server.tsx
@@ -12,6 +12,7 @@ export interface RemixServerProps {
context: EntryContext;
url: string | URL;
abortDelay?: number;
+ nonce?: string;
}
/**
@@ -23,6 +24,7 @@ export function RemixServer({
context,
url,
abortDelay,
+ nonce,
}: RemixServerProps): ReactElement {
if (typeof url === "string") {
url = new URL(url);
@@ -98,6 +100,7 @@ export function RemixServer({
identifier={0}
reader={context.serverHandoffStream.getReader()}
textDecoder={new TextDecoder()}
+ nonce={nonce}
/>
) : null}
diff --git a/packages/react-router/lib/dom/ssr/single-fetch.tsx b/packages/react-router/lib/dom/ssr/single-fetch.tsx
index c072ba4f0f..3329d3b58e 100644
--- a/packages/react-router/lib/dom/ssr/single-fetch.tsx
+++ b/packages/react-router/lib/dom/ssr/single-fetch.tsx
@@ -41,6 +41,7 @@ interface StreamTransferProps {
identifier: number;
reader: ReadableStreamDefaultReader;
textDecoder: TextDecoder;
+ nonce?: string;
}
// StreamTransfer recursively renders down chunks of the `serverHandoffStream`
@@ -50,6 +51,7 @@ export function StreamTransfer({
identifier,
reader,
textDecoder,
+ nonce,
}: StreamTransferProps) {
// If the user didn't render the component then we don't have to
// bother streaming anything in
@@ -86,6 +88,7 @@ export function StreamTransfer({
let { done, value } = promise.result;
let scriptTag = value ? (