release: WRNexusJS 0.3.0

This commit is contained in:
2026-07-22 17:29:08 +05:30
parent 13dfa31d19
commit 07d8fb59d6
145 changed files with 9664 additions and 3881 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ssr",
"version": "0.2.79",
"version": "0.3.0",
"type": "module",
"main": "src/index.ts",
"exports": {
+59
View File
@@ -177,3 +177,62 @@ function resolveSeoUrl(
return value;
}
}
export interface StreamRenderOptions extends Omit<RenderOptions, "body"> {
body: string | Promise<string> | AsyncIterable<string>;
}
function isAsyncIterable(value: unknown): value is AsyncIterable<string> {
return (
typeof value === "object" &&
value !== null &&
Symbol.asyncIterator in value &&
typeof (value as AsyncIterable<string>)[Symbol.asyncIterator] === "function"
);
}
async function* bodyChunks(body: StreamRenderOptions["body"]): AsyncIterable<string> {
if (typeof body === "string") {
yield body;
return;
}
if (isAsyncIterable(body)) {
yield* body;
return;
}
yield await body;
}
/**
* Stream a complete document while preserving the exact head/body contract of
* `renderDocument`. Async iterables can flush a shell, primary content, and
* slower fragments without buffering the entire route.
*/
export function renderDocumentStream(opts: StreamRenderOptions): ReadableStream<Uint8Array> {
const marker = "<!--__WRNEXUS_STREAM_BODY__-->";
const document = renderDocument({ ...opts, body: marker });
const [prefix, suffix] = document.split(marker);
const encoder = new TextEncoder();
return new ReadableStream<Uint8Array>({
async start(controller) {
try {
controller.enqueue(encoder.encode(prefix ?? ""));
for await (const chunk of bodyChunks(opts.body)) controller.enqueue(encoder.encode(chunk));
controller.enqueue(encoder.encode(suffix ?? ""));
controller.close();
} catch (error) {
controller.error(error);
}
},
});
}
export function streamDocumentResponse(
opts: StreamRenderOptions,
init: ResponseInit = {},
): Response {
const headers = new Headers(init.headers);
if (!headers.has("content-type")) headers.set("content-type", "text/html; charset=utf-8");
return new Response(renderDocumentStream(opts), { ...init, headers });
}
+12 -1
View File
@@ -1,5 +1,5 @@
import { expect, test } from "bun:test";
import { renderDocument } from "../src/index.ts";
import { renderDocument, renderDocumentStream } from "../src/index.ts";
test("escapes metadata and script URLs while preserving trusted rendered body", () => {
const html = renderDocument({
@@ -35,3 +35,14 @@ test("always emits a document language and preserves an explicit language", () =
expect(explicit).toContain('<html data-theme="dark" lang="fr">');
expect(explicit).not.toContain('lang="en"');
});
test("streams async body chunks inside the document shell", async () => {
async function* body() {
yield "<h1>Shell</h1>";
yield "<p>Later</p>";
}
const html = await new Response(
renderDocumentStream({ meta: { title: "Stream" }, body: body() }),
).text();
expect(html).toContain('<div id="app"><h1>Shell</h1><p>Later</p></div>');
});