Skip to content

Commit

Permalink
Allow a nonce to be set on single fetch stream transfer inline scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 committed May 10, 2024
1 parent 9c23e0c commit 4bd6378
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 0 deletions.
83 changes: 83 additions & 0 deletions integration/single-fetch-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Outlet />
<Scripts nonce="the-nonce" />
</body>
</html>
);
}
`,
"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(
<RemixServer context={remixContext} url={request.url} nonce="the-nonce" />,
{
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(
Expand Down
3 changes: 3 additions & 0 deletions packages/react-router/lib/dom/ssr/server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface RemixServerProps {
context: EntryContext;
url: string | URL;
abortDelay?: number;
nonce?: string;
}

/**
Expand All @@ -23,6 +24,7 @@ export function RemixServer({
context,
url,
abortDelay,
nonce,
}: RemixServerProps): ReactElement {
if (typeof url === "string") {
url = new URL(url);
Expand Down Expand Up @@ -98,6 +100,7 @@ export function RemixServer({
identifier={0}
reader={context.serverHandoffStream.getReader()}
textDecoder={new TextDecoder()}
nonce={nonce}
/>
</React.Suspense>
) : null}
Expand Down
5 changes: 5 additions & 0 deletions packages/react-router/lib/dom/ssr/single-fetch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ interface StreamTransferProps {
identifier: number;
reader: ReadableStreamDefaultReader<Uint8Array>;
textDecoder: TextDecoder;
nonce?: string;
}

// StreamTransfer recursively renders down chunks of the `serverHandoffStream`
Expand All @@ -50,6 +51,7 @@ export function StreamTransfer({
identifier,
reader,
textDecoder,
nonce,
}: StreamTransferProps) {
// If the user didn't render the <Scripts> component then we don't have to
// bother streaming anything in
Expand Down Expand Up @@ -86,6 +88,7 @@ export function StreamTransfer({
let { done, value } = promise.result;
let scriptTag = value ? (
<script
nonce={nonce}
dangerouslySetInnerHTML={{
__html: `window.__remixContext.streamController.enqueue(${escapeHtml(
JSON.stringify(value)
Expand All @@ -99,6 +102,7 @@ export function StreamTransfer({
<>
{scriptTag}
<script
nonce={nonce}
dangerouslySetInnerHTML={{
__html: `window.__remixContext.streamController.close();`,
}}
Expand All @@ -115,6 +119,7 @@ export function StreamTransfer({
identifier={identifier + 1}
reader={reader}
textDecoder={textDecoder}
nonce={nonce}
/>
</React.Suspense>
</>
Expand Down

0 comments on commit 4bd6378

Please sign in to comment.