plan/react-rewrite #1

Merged
gnezim merged 138 commits from plan/react-rewrite into main 2026-04-15 12:21:16 +03:00
2 changed files with 145 additions and 0 deletions
Showing only changes of commit 4f93d0a9bf - Show all commits
@@ -0,0 +1,77 @@
import { describe, expect, it } from "vitest";
import { Readable } from "node:stream";
import { wrapSsrStreamWithNonce } from "./nonce-stream-transform.js";
function streamToString(stream: NodeJS.ReadableStream): Promise<string> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
stream.on("data", (chunk: Buffer) => chunks.push(Buffer.from(chunk)));
stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
stream.on("error", reject);
});
}
function createStream(chunks: string[]): NodeJS.ReadableStream {
return new Readable({
read() {
for (const chunk of chunks) {
this.push(chunk);
}
this.push(null);
},
});
}
const NONCE = "test-nonce-abc123";
describe("wrapSsrStreamWithNonce", () => {
it("injects nonce on bare <script> tags", async () => {
const input = createStream(["<html><head><script>console.log('hi')</script></head></html>"]);
const output = wrapSsrStreamWithNonce(input, NONCE);
const result = await streamToString(output);
expect(result).toContain(`<script nonce="${NONCE}">`);
});
it("injects nonce on <script src> tags", async () => {
const input = createStream([`<script src="/app.js"></script>`]);
const output = wrapSsrStreamWithNonce(input, NONCE);
const result = await streamToString(output);
expect(result).toContain(`<script nonce="${NONCE}" src="/app.js">`);
});
it("injects nonce on <script type='module'> tags", async () => {
const input = createStream([`<script type="module" src="/app.js"></script>`]);
const output = wrapSsrStreamWithNonce(input, NONCE);
const result = await streamToString(output);
expect(result).toContain(`<script nonce="${NONCE}" type="module" src="/app.js">`);
});
it("does not double-inject on scripts that already have nonce", async () => {
const input = createStream([`<script nonce="existing">code</script>`]);
const output = wrapSsrStreamWithNonce(input, NONCE);
const result = await streamToString(output);
expect(result).toBe(`<script nonce="existing">code</script>`);
expect(result).not.toContain(NONCE);
});
it("handles multiple script tags in one chunk", async () => {
const input = createStream([
`<script>a</script><script nonce="x">b</script><script src="/c.js"></script>`,
]);
const output = wrapSsrStreamWithNonce(input, NONCE);
const result = await streamToString(output);
expect(result).toContain(`<script nonce="${NONCE}">a</script>`);
expect(result).toContain(`<script nonce="x">b</script>`);
expect(result).toContain(`<script nonce="${NONCE}" src="/c.js">`);
});
it("handles chunks split mid-tag", async () => {
const input = createStream([
`<html><scr`,
`ipt src="/app.js"></script></html>`,
]);
const output = wrapSsrStreamWithNonce(input, NONCE);
const result = await streamToString(output);
expect(result).toContain(`<script nonce="${NONCE}" src="/app.js">`);
});
});
@@ -0,0 +1,68 @@
import { Transform } from "node:stream";
/**
* Regex matching `<script` opening tags.
* Captures everything between `<script` and `>` to check for existing nonce.
*/
const SCRIPT_OPEN_RE = /<script(\s[^>]*)?\s*>/gi;
/**
* Wraps an SSR HTML stream to inject `nonce="..."` on every `<script>` tag
* that doesn't already have a nonce attribute.
*
* Workaround for React issue #24883: renderToPipeableStream({ nonce }) only
* applies the nonce to inline bootstrapScriptContent, not to external
* bootstrapScripts src URLs.
*/
export function wrapSsrStreamWithNonce(
stream: NodeJS.ReadableStream,
nonce: string,
): NodeJS.ReadableStream {
let buffer = "";
const transform = new Transform({
decodeStrings: false,
transform(chunk: Buffer | string, _encoding, callback) {
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
// Keep a trailing partial `<script...` that might be split across chunks.
// Find the last `<scr` that might be an incomplete tag.
const lastOpenBracket = buffer.lastIndexOf("<scr");
if (lastOpenBracket !== -1 && !buffer.includes(">", lastOpenBracket)) {
// Incomplete tag — hold the partial in the buffer
const ready = buffer.slice(0, lastOpenBracket);
buffer = buffer.slice(lastOpenBracket);
this.push(injectNonce(ready, nonce));
} else {
// No incomplete tag — flush everything
this.push(injectNonce(buffer, nonce));
buffer = "";
}
callback();
},
flush(callback) {
if (buffer.length > 0) {
this.push(injectNonce(buffer, nonce));
buffer = "";
}
callback();
},
});
stream.pipe(transform);
return transform;
}
function injectNonce(html: string, nonce: string): string {
return html.replace(SCRIPT_OPEN_RE, (match, attrs: string | undefined) => {
const attrStr = attrs ?? "";
// Don't inject if nonce already present
if (/\bnonce\s*=/i.test(attrStr)) {
return match;
}
return `<script nonce="${nonce}"${attrStr}>`;
});
}