UNPKG

rwsdk

Version:

Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime

419 lines (418 loc) 19.1 kB
import { describe, expect, it } from "vitest"; import { stitchDocumentAndAppStreams } from "./stitchDocumentAndAppStreams.js"; function stringToStream(str) { const encoder = new TextEncoder(); return new ReadableStream({ start(controller) { controller.enqueue(encoder.encode(str)); controller.close(); }, }); } function streamToString(stream) { const decoder = new TextDecoder(); const reader = stream.getReader(); let result = ""; return new Promise((resolve, reject) => { function pump() { return reader .read() .then(({ done, value }) => { if (done) { resolve(result); return; } result += decoder.decode(value, { stream: true }); return pump(); }) .catch(reject); } return pump(); }); } function createChunkedStream(chunks) { const encoder = new TextEncoder(); return new ReadableStream({ start(controller) { for (let i = 0; i < chunks.length; i++) { controller.enqueue(encoder.encode(chunks[i])); } controller.close(); }, }); } describe("stitchDocumentAndAppStreams", () => { const startMarker = '<div id="rwsdk-app-start" />'; const endMarker = '<div id="rwsdk-app-end"></div>'; describe("meta tag hoisting", () => { it("places hoisted tags inside head, after existing head content", async () => { const outerHtml = `<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> </head> <body> ${startMarker} <script src="/client.js"></script> </body> </html>`; const innerHtml = `<title>Page Title</title><div>App content</div>${endMarker}`; const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), stringToStream(innerHtml), startMarker, endMarker)); expect(result).toContain(`<title>Page Title</title>`); expect(result).toMatch(/<head>[\s\S]*<meta charset="utf-8" \/>[\s\S]*<title>Page Title<\/title>[\s\S]*<\/head>/); expect(result).toContain(`<div>App content</div>`); const doctypeIndex = result.indexOf(`<!DOCTYPE html>`); const headIndex = result.indexOf(`<head>`); const charsetIndex = result.indexOf(`<meta charset="utf-8" />`); const titleIndex = result.indexOf(`<title>Page Title</title>`); const headCloseIndex = result.indexOf(`</head>`); expect(doctypeIndex).toBe(0); expect(doctypeIndex).toBeLessThan(headIndex); expect(charsetIndex).toBeLessThan(titleIndex); expect(titleIndex).toBeLessThan(headCloseIndex); }); it("places multiple hoisted tags inside head, after existing head content", async () => { const outerHtml = `<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> </head> <body> ${startMarker} <script src="/client.js"></script> </body> </html>`; const innerHtml = `<title>Page Title</title><meta name="description" content="Test" /><link rel="stylesheet" href="/styles.css" /><div>App content</div>${endMarker}`; const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), stringToStream(innerHtml), startMarker, endMarker)); expect(result).toContain(`<title>Page Title</title>`); expect(result).toContain(`<meta name="description" content="Test" />`); expect(result).toContain(`<link rel="stylesheet" href="/styles.css" />`); expect(result).toMatch(/<head>[\s\S]*<meta charset="utf-8" \/>[\s\S]*<title>Page Title<\/title>[\s\S]*<meta name="description" content="Test" \/>[\s\S]*<link rel="stylesheet" href="\/styles.css" \/>[\s\S]*<\/head>/); const doctypeIndex = result.indexOf(`<!DOCTYPE html>`); const charsetIndex = result.indexOf(`<meta charset="utf-8" />`); const titleIndex = result.indexOf(`<title>Page Title</title>`); const headCloseIndex = result.indexOf(`</head>`); expect(doctypeIndex).toBe(0); expect(charsetIndex).toBeLessThan(titleIndex); expect(titleIndex).toBeLessThan(headCloseIndex); }); it("handles app stream with no hoisted tags", async () => { const outerHtml = `<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> </head> <body> ${startMarker} <script src="/client.js"></script> </body> </html>`; const innerHtml = `<div>App content</div>${endMarker}`; const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), stringToStream(innerHtml), startMarker, endMarker)); expect(result).toContain(`<div>App content</div>`); expect(result).not.toContain(`<title>`); }); it("handles hoisted tags split across chunks", async () => { const outerHtml = `<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> </head> <body> ${startMarker} <script src="/client.js"></script> </body> </html>`; const innerHtmlChunks = [ `<title>Page `, `Title</title><meta name="description" `, `content="Test" /><div>App content</div>${endMarker}`, ]; const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), createChunkedStream(innerHtmlChunks), startMarker, endMarker)); expect(result).toContain(`<title>Page Title</title>`); expect(result).toContain(`<meta name="description" content="Test" />`); expect(result).toMatch(/<head>[\s\S]*<meta charset="utf-8" \/>[\s\S]*<title>Page Title<\/title>[\s\S]*<\/head>/); const doctypeIndex = result.indexOf(`<!DOCTYPE html>`); const charsetIndex = result.indexOf(`<meta charset="utf-8" />`); const titleIndex = result.indexOf(`<title>Page Title</title>`); const headCloseIndex = result.indexOf(`</head>`); expect(doctypeIndex).toBe(0); expect(charsetIndex).toBeLessThan(titleIndex); expect(titleIndex).toBeLessThan(headCloseIndex); }); it("ensures doctype is always first", async () => { const outerHtml = `<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> </head> <body> ${startMarker} <script src="/client.js"></script> </body> </html>`; const innerHtml = `<title>Page Title</title><div>App content</div>${endMarker}`; const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), stringToStream(innerHtml), startMarker, endMarker)); expect(result.trim().startsWith(`<!DOCTYPE html>`)).toBe(true); }); }); describe("basic stitching flow", () => { it("stitches document head, app shell, and document tail", async () => { const outerHtml = `<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> </head> <body> ${startMarker} <script src="/client.js"></script> </body> </html>`; const innerHtml = `<div>App content</div>${endMarker}`; const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), stringToStream(innerHtml), startMarker, endMarker)); expect(result).toContain(`<!DOCTYPE html>`); expect(result).toContain(`<head>`); expect(result).toContain(`<meta charset="utf-8" />`); expect(result).toContain(`<div>App content</div>`); expect(result).toContain(`<script src="/client.js"></script>`); expect(result).toContain(`</body>`); expect(result).toContain(`</html>`); const doctypeIndex = result.indexOf(`<!DOCTYPE html>`); const appContentIndex = result.indexOf(`<div>App content</div>`); const scriptIndex = result.indexOf(`<script src="/client.js"></script>`); const bodyCloseIndex = result.indexOf(`</body>`); expect(doctypeIndex).toBeLessThan(appContentIndex); expect(appContentIndex).toBeLessThan(scriptIndex); expect(scriptIndex).toBeLessThan(bodyCloseIndex); }); it("removes start marker from output", async () => { const outerHtml = `<!DOCTYPE html> <html> <body> ${startMarker} <script src="/client.js"></script> </body> </html>`; const innerHtml = `<div>App content</div>${endMarker}`; const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), stringToStream(innerHtml), startMarker, endMarker)); expect(result).not.toContain(startMarker); }); it("preserves end marker in output", async () => { const outerHtml = `<!DOCTYPE html> <html> <body> ${startMarker} <script src="/client.js"></script> </body> </html>`; const innerHtml = `<div>App content</div>${endMarker}`; const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), stringToStream(innerHtml), startMarker, endMarker)); expect(result).toContain(endMarker); }); }); describe("suspense boundaries (suspended content)", () => { it("streams suspended content after script tag", async () => { const outerHtml = `<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> </head> <body> ${startMarker} <script src="/client.js"></script> </body> </html>`; const innerHtml = `<div>Initial content</div>${endMarker}<div>Suspended content</div>`; const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), stringToStream(innerHtml), startMarker, endMarker)); expect(result).toContain(`<div>Initial content</div>`); expect(result).toContain(`<script src="/client.js"></script>`); expect(result).toContain(`<div>Suspended content</div>`); const initialIndex = result.indexOf(`<div>Initial content</div>`); const scriptIndex = result.indexOf(`<script src="/client.js"></script>`); const suspendedIndex = result.indexOf(`<div>Suspended content</div>`); const bodyCloseIndex = result.indexOf(`</body>`); expect(initialIndex).toBeLessThan(scriptIndex); expect(scriptIndex).toBeLessThan(suspendedIndex); expect(suspendedIndex).toBeLessThan(bodyCloseIndex); }); it("handles multiple suspended content chunks", async () => { const outerHtml = `<!DOCTYPE html> <html> <body> ${startMarker} <script src="/client.js"></script> </body> </html>`; const innerHtml = `<div>Initial</div>${endMarker}<div>Suspended 1</div><div>Suspended 2</div>`; const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), stringToStream(innerHtml), startMarker, endMarker)); expect(result).toContain(`<div>Initial</div>`); expect(result).toContain(`<div>Suspended 1</div>`); expect(result).toContain(`<div>Suspended 2</div>`); const scriptIndex = result.indexOf(`<script src="/client.js"></script>`); const suspended1Index = result.indexOf(`<div>Suspended 1</div>`); const suspended2Index = result.indexOf(`<div>Suspended 2</div>`); expect(scriptIndex).toBeLessThan(suspended1Index); expect(suspended1Index).toBeLessThan(suspended2Index); }); it("handles app stream with no suspended content", async () => { const outerHtml = `<!DOCTYPE html> <html> <body> ${startMarker} <script src="/client.js"></script> </body> </html>`; const innerHtml = `<div>Initial content</div>${endMarker}`; const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), stringToStream(innerHtml), startMarker, endMarker)); expect(result).toContain(`<div>Initial content</div>`); expect(result).toContain(`<script src="/client.js"></script>`); expect(result).not.toContain(`Suspended`); }); }); describe("chunked streams", () => { it("handles document stream split across chunks", async () => { const outerHtmlChunks = [ `<!DOCTYPE html>\n<html>\n<head>\n`, ` <meta charset="utf-8" />\n</head>\n<body>\n`, ` ${startMarker}\n <script src="/client.js"></script>\n</body>\n</html>`, ]; const innerHtml = `<div>App content</div>${endMarker}`; const result = await streamToString(stitchDocumentAndAppStreams(createChunkedStream(outerHtmlChunks), stringToStream(innerHtml), startMarker, endMarker)); expect(result).toContain(`<!DOCTYPE html>`); expect(result).toContain(`<div>App content</div>`); expect(result).toContain(`<script src="/client.js"></script>`); }); it("handles app stream split across chunks", async () => { const outerHtml = `<!DOCTYPE html> <html> <body> ${startMarker} <script src="/client.js"></script> </body> </html>`; const innerHtmlChunks = [ `<div>Initial `, `content</div>${endMarker}`, `<div>Suspended content</div>`, ]; const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), createChunkedStream(innerHtmlChunks), startMarker, endMarker)); expect(result).toContain(`<div>Initial content</div>`); expect(result).toContain(`<div>Suspended content</div>`); }); it("handles markers split across chunks", async () => { const outerHtml = `<!DOCTYPE html> <html> <body> ${startMarker} <script src="/client.js"></script> </body> </html>`; const innerHtmlChunks = [ `<div>App content</div><div id="rwsdk-app-end`, `"></div>`, ]; const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), createChunkedStream(innerHtmlChunks), startMarker, endMarker)); expect(result).toContain(`<div>App content</div>`); expect(result).toContain(endMarker); }); }); describe("edge cases", () => { it("handles empty app stream", async () => { const outerHtml = `<!DOCTYPE html> <html> <body> ${startMarker} <script src="/client.js"></script> </body> </html>`; const innerHtml = ``; const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), stringToStream(innerHtml), startMarker, endMarker)); expect(result).toContain(`<!DOCTYPE html>`); expect(result).toContain(`<script src="/client.js"></script>`); expect(result).toContain(`</body>`); }); it("handles empty outer stream", async () => { const outerHtml = ``; const innerHtml = `<div>App content</div>${endMarker}`; const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), stringToStream(innerHtml), startMarker, endMarker)); expect(result).toBe(`<div>App content</div>${endMarker}`); }); it("handles app stream ending before end marker", async () => { const outerHtml = `<!DOCTYPE html> <html> <body> ${startMarker} <script src="/client.js"></script> </body> </html>`; const innerHtml = `<div>App content</div>`; const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), stringToStream(innerHtml), startMarker, endMarker)); expect(result).toContain(`<div>App content</div>`); expect(result).toContain(`<script src="/client.js"></script>`); }); it("handles outer stream ending before start marker", async () => { const outerHtml = `<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> </head>`; const innerHtml = `<div>App content</div>${endMarker}`; const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), stringToStream(innerHtml), startMarker, endMarker)); expect(result).toContain(`<!DOCTYPE html>`); expect(result).toContain(`<meta charset="utf-8" />`); expect(result).toContain(`<div>App content</div>`); }); }); describe("complete flow verification", () => { it("correctly orders all phases: head -> shell -> script -> suspended -> close", async () => { const outerHtml = `<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Test Page</title> </head> <body> ${startMarker} <script src="/client.js"></script> </body> </html>`; const innerHtml = `<div>Initial content</div>${endMarker}<div>Suspended content</div>`; const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), stringToStream(innerHtml), startMarker, endMarker)); const doctypeIndex = result.indexOf(`<!DOCTYPE html>`); const headIndex = result.indexOf(`<head>`); const titleIndex = result.indexOf(`<title>Test Page</title>`); const initialIndex = result.indexOf(`<div>Initial content</div>`); const scriptIndex = result.indexOf(`<script src="/client.js"></script>`); const suspendedIndex = result.indexOf(`<div>Suspended content</div>`); const bodyCloseIndex = result.indexOf(`</body>`); const htmlCloseIndex = result.indexOf(`</html>`); expect(doctypeIndex).toBeLessThan(headIndex); expect(headIndex).toBeLessThan(titleIndex); expect(titleIndex).toBeLessThan(initialIndex); expect(initialIndex).toBeLessThan(scriptIndex); expect(scriptIndex).toBeLessThan(suspendedIndex); expect(suspendedIndex).toBeLessThan(bodyCloseIndex); expect(bodyCloseIndex).toBeLessThan(htmlCloseIndex); }); it("preserves content structure and markers", async () => { const outerHtml = `<!DOCTYPE html> <html> <body> ${startMarker} <script src="/client.js"></script> </body> </html>`; const innerHtml = `<div id="app"> <h1>Hello</h1> <p>World</p> </div>${endMarker}<div>More content</div>`; const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), stringToStream(innerHtml), startMarker, endMarker)); expect(result).toContain(`<div id="app">`); expect(result).toContain(`<h1>Hello</h1>`); expect(result).toContain(`<p>World</p>`); expect(result).toContain(endMarker); expect(result).toContain(`<div>More content</div>`); expect(result).not.toContain(startMarker); }); }); });