feat: release design system engine, studio and component contracts
This commit is contained in:
1 parent
ff1065fb27
commit
3943b946d6
693 files changed
+175546
-3894
No files matched your search
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ssr",
|
||||
"version": "0.8.12",
|
||||
"version": "0.8.20",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -7,7 +7,14 @@
|
||||
* the browser, which keeps "server-only code" genuinely server-only.
|
||||
*/
|
||||
|
||||
import { escapeHtml, renderControlTemplates, type PageMeta, type SeoConfig } from "@wrnexus/core";
|
||||
import {
|
||||
escapeHtml,
|
||||
encodeControlTemplate,
|
||||
getRequestContext,
|
||||
renderControlTemplates,
|
||||
type PageMeta,
|
||||
type SeoConfig,
|
||||
} from "@wrnexus/core";
|
||||
|
||||
export interface ScriptAsset {
|
||||
src: string;
|
||||
@@ -64,6 +71,8 @@ export function streamPartialDocument(
|
||||
}
|
||||
|
||||
export interface RenderOptions {
|
||||
/** Trusted compiler output. Applied after all extracted component styles. */
|
||||
componentRecipeCss?: string;
|
||||
/** Page metadata for the document head. */
|
||||
meta: PageMeta;
|
||||
/** Global SEO defaults from `wrnexus.config.ts`. */
|
||||
@@ -106,9 +115,62 @@ export function renderDocument(opts: RenderOptions): string {
|
||||
const title = escapeHtml(seo.title);
|
||||
const scripts = normalizeScripts(opts.scripts ?? []);
|
||||
|
||||
const sourceDocument = opts.documentTemplate?.trim();
|
||||
const extracted = extractWrnexusStyles(sourceDocument ?? opts.body, opts.styleNonce);
|
||||
const renderedBody = sourceDocument ? opts.body : extracted.html;
|
||||
// Repeated component instances use identical behavior descriptors. Share one
|
||||
// request-owned JSON value instead of repeating base64 in every branch/row.
|
||||
const request = getRequestContext()?.req;
|
||||
// Existing tabs keep the inline transport until their runtime advertises
|
||||
// registry support. Full documents load the versioned matching runtime.
|
||||
const shareRegistry =
|
||||
request?.headers.get("x-wrnexus-nav") !== "1" ||
|
||||
["registry-v1", "registry-v2"].includes(request?.headers.get("x-wrnexus-behaviors") || "");
|
||||
const shareScopes =
|
||||
request?.headers.get("x-wrnexus-nav") !== "1" ||
|
||||
request?.headers.get("x-wrnexus-behaviors") === "registry-v2";
|
||||
const behaviors = new Map<string, string>();
|
||||
const shareBehaviors = (markup: string) => {
|
||||
if (!shareRegistry) return markup;
|
||||
const shared = markup.replace(
|
||||
/data-wrn-(behavior|scope|loop-locals)="([A-Za-z0-9+/=]+)"/g,
|
||||
(attribute, kind: string, encoded: string) => {
|
||||
if (kind !== "behavior" && !shareScopes) return attribute;
|
||||
const metadataKey = (kind === "behavior" ? "behavior:" : "scope:") + encoded;
|
||||
let reference = behaviors.get(metadataKey);
|
||||
if (!reference) {
|
||||
try {
|
||||
const value = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return attribute;
|
||||
reference = encodeControlTemplate(
|
||||
kind === "behavior" ? value : { __wrnMetadata: true, value },
|
||||
);
|
||||
behaviors.set(metadataKey, reference);
|
||||
} catch {
|
||||
return attribute;
|
||||
}
|
||||
}
|
||||
return `data-wrn-${kind}="${reference}"`;
|
||||
},
|
||||
);
|
||||
// The encoded scope is authoritative; retain only its root marker instead
|
||||
// of repeating the same state in the fallback declaration as well.
|
||||
return !shareScopes
|
||||
? shared
|
||||
: shared.replace(/<[^>]+\bdata-wrn-scope="@[^"]+"[^>]*>/g, (tag) =>
|
||||
tag.replace(/\bdata-scope="[^"]*"/g, 'data-scope=""'),
|
||||
);
|
||||
};
|
||||
const hydrationBody = shareBehaviors(opts.body);
|
||||
const sourceDocument = opts.documentTemplate?.trim()
|
||||
? shareBehaviors(opts.documentTemplate.trim())
|
||||
: undefined;
|
||||
let controlStyles = "";
|
||||
const controls = renderControlTemplates((markup) => {
|
||||
const prepared = extractWrnexusStyles(shareBehaviors(markup), opts.styleNonce);
|
||||
controlStyles += prepared.styles;
|
||||
return prepared.html;
|
||||
});
|
||||
const extracted = extractWrnexusStyles(sourceDocument ?? hydrationBody, opts.styleNonce);
|
||||
extracted.styles = extractWrnexusStyles(controlStyles + extracted.styles, opts.styleNonce).styles;
|
||||
const renderedBody = sourceDocument ? hydrationBody : extracted.html;
|
||||
const renderedDocument = sourceDocument ? extracted.html : undefined;
|
||||
|
||||
const seoTags = renderSeoTags(seo);
|
||||
@@ -119,8 +181,11 @@ export function renderDocument(opts: RenderOptions): string {
|
||||
const scriptTags = scripts.map((script) => `\n ${renderScriptTag(script)}`).join("");
|
||||
|
||||
const globalHead = opts.extraHead ? `\n ${opts.extraHead}` : "";
|
||||
const localStyles = extracted.styles ? `\n ${extracted.styles}` : "";
|
||||
const extraBody = renderControlTemplates() + (opts.extraBody ? `\n ${opts.extraBody}` : "");
|
||||
const recipeStyles = opts.componentRecipeCss
|
||||
? `\n <style data-wrn-design-recipes${opts.styleNonce ? ` nonce="${escapeHtml(opts.styleNonce)}"` : ""}>${opts.componentRecipeCss.replace(/</g, "\\3c ")}</style>`
|
||||
: "";
|
||||
const localStyles = (extracted.styles ? `\n ${extracted.styles}` : "") + recipeStyles;
|
||||
const extraBody = controls + (opts.extraBody ? `\n ${opts.extraBody}` : "");
|
||||
|
||||
const htmlAttrs = /(?:^|\s)lang\s*=/.test(opts.htmlAttrs ?? "")
|
||||
? (opts.htmlAttrs ?? "")
|
||||
@@ -446,7 +511,10 @@ export function renderDocumentStream(opts: StreamRenderOptions): ReadableStream<
|
||||
controller.enqueue(encoder.encode(prefix ?? ""));
|
||||
for await (const chunk of bodyChunks(opts.body)) controller.enqueue(encoder.encode(chunk));
|
||||
// Async body rendering can register templates after the shell is built.
|
||||
const tail = (suffix ?? "").replace(/<script type="application\/json" data-wrn-controls[^>]*>[\s\S]*?<\/script>/g, "");
|
||||
const tail = (suffix ?? "").replace(
|
||||
/<script type="application\/json" data-wrn-controls[^>]*>[\s\S]*?<\/script>/g,
|
||||
"",
|
||||
);
|
||||
controller.enqueue(encoder.encode(renderControlTemplates() + tail));
|
||||
controller.close();
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,6 +3,35 @@ import { renderDocument, renderDocumentStream, renderStoreHydration } from "../s
|
||||
import type { StoreContainer } from "@wrnexus/store";
|
||||
import { runWithRequestContext, encodeControlTemplate } from "@wrnexus/core";
|
||||
|
||||
test("inactive hydration branches share nonce-tagged styles without changing registry source", () => {
|
||||
runWithRequestContext({} as never, () => {
|
||||
const css = ".review{color:blue}".repeat(500);
|
||||
const style = `<style data-wrnexus-style-id="review" data-wrnexus-style-owner="Review" data-wrnexus-style-kind="component">${css}</style>`;
|
||||
const reference = encodeControlTemplate([
|
||||
{ cond: "open", body: `${style.repeat(30)}<button>Review</button>` },
|
||||
]);
|
||||
const options = {
|
||||
meta: {},
|
||||
body: `<template data-wrn-if="${reference}"></template>${style}`,
|
||||
styleNonce: "review-nonce",
|
||||
};
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
const html = renderDocument(options);
|
||||
expect(html.match(/data-wrnexus-style-id="review"/g)).toHaveLength(1);
|
||||
expect(html).toContain('nonce="review-nonce"');
|
||||
expect(html.indexOf(css)).toBeLessThan(html.indexOf("</head>"));
|
||||
const definitions = JSON.parse(/data-wrn-controls[^>]*>(.*?)<\/script>/s.exec(html)![1]!);
|
||||
expect(definitions[0][0].body).toBe("<button>Review</button>");
|
||||
expect(html.length).toBeLessThan(css.length + 2000);
|
||||
}
|
||||
const inactiveOnly = renderDocument({
|
||||
meta: {},
|
||||
body: `<template data-wrn-if="${reference}"></template>`,
|
||||
});
|
||||
expect(inactiveOnly).toContain(css);
|
||||
});
|
||||
});
|
||||
|
||||
test("streamed documents include templates registered by late body chunks", async () => {
|
||||
await runWithRequestContext({} as never, async () => {
|
||||
async function* body() {
|
||||
@@ -11,7 +40,7 @@ test("streamed documents include templates registered by late body chunks", asyn
|
||||
yield `<template data-wrn-if="${encodeControlTemplate([{ cond: null, body: "Late" }])}"></template>`;
|
||||
}
|
||||
const html = await new Response(renderDocumentStream({ meta: {}, body: body() })).text();
|
||||
expect(html).toContain('data-wrn-controls');
|
||||
expect(html).toContain("data-wrn-controls");
|
||||
expect(html).toContain('"body":"Late"');
|
||||
expect(html.match(/data-wrn-controls/g)).toHaveLength(1);
|
||||
});
|
||||
@@ -185,3 +214,98 @@ test("places document-layout WRN styles at the end of head", () => {
|
||||
expect(styleIndex).toBeLessThan(headEnd);
|
||||
expect(html).not.toContain("<body><style");
|
||||
});
|
||||
|
||||
test("repeated behavior descriptors are shared across body and inactive templates", () => {
|
||||
runWithRequestContext({} as never, () => {
|
||||
const behavior = {
|
||||
functions: "function increment(){count++}",
|
||||
outputs: [],
|
||||
computed: [],
|
||||
effects: [],
|
||||
lifecycle: {},
|
||||
watches: [],
|
||||
};
|
||||
const encoded = Buffer.from(JSON.stringify(behavior)).toString("base64");
|
||||
const instance = `<div data-scope="count: 0" data-wrn-behavior="${encoded}"><button>Increment</button></div>`;
|
||||
const reference = encodeControlTemplate([{ cond: "open", body: instance.repeat(100) }]);
|
||||
const document = renderDocument({
|
||||
meta: {},
|
||||
body: instance.repeat(100) + `<template data-wrn-if="${reference}"></template>`,
|
||||
});
|
||||
const definitions = JSON.parse(/data-wrn-controls[^>]*>(.*?)<\/script>/s.exec(document)![1]!);
|
||||
expect(definitions.filter((entry: any) => entry.functions)).toHaveLength(1);
|
||||
expect(document).not.toContain(`data-wrn-behavior="${encoded}"`);
|
||||
expect(document.match(/data-wrn-behavior="@/g)?.length).toBe(100);
|
||||
expect(definitions[0][0].body.match(/data-wrn-behavior="@/g)).toHaveLength(100);
|
||||
expect(document.length).toBeLessThan(instance.length * 200);
|
||||
});
|
||||
});
|
||||
|
||||
test("old client navigation keeps inline behavior while capable clients use references", () => {
|
||||
const encoded = Buffer.from(
|
||||
JSON.stringify({ functions: "function increment(){count++}" }),
|
||||
).toString("base64");
|
||||
for (const capable of [false, true])
|
||||
runWithRequestContext(
|
||||
{
|
||||
req: new Request("https://example.test/next", {
|
||||
headers: {
|
||||
"x-wrnexus-nav": "1",
|
||||
...(capable ? { "x-wrnexus-behaviors": "registry-v1" } : {}),
|
||||
},
|
||||
}),
|
||||
} as never,
|
||||
() => {
|
||||
const html = renderDocument({
|
||||
meta: {},
|
||||
body: `<div data-scope="count: 0" data-wrn-behavior="${encoded}"></div>`,
|
||||
});
|
||||
expect(html.includes(`data-wrn-behavior="${encoded}"`)).toBe(!capable);
|
||||
expect(html.includes('data-wrn-behavior="@')).toBe(capable);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("scope and loop metadata share a request value without duplicating fallback declarations", () => {
|
||||
const encoded = Buffer.from(
|
||||
JSON.stringify({ items: Array.from({ length: 30 }, (_, i) => ({ label: "Menu " + i })) }),
|
||||
).toString("base64");
|
||||
for (const mode of ["registry-v1", "registry-v2"])
|
||||
runWithRequestContext(
|
||||
{
|
||||
req: new Request("https://example.test/", {
|
||||
headers: { "x-wrnexus-nav": "1", "x-wrnexus-behaviors": mode },
|
||||
}),
|
||||
} as never,
|
||||
() => {
|
||||
const node = `<div data-scope="items: []" data-wrn-scope="${encoded}"><button data-wrn-loop-locals="${encoded}">Menu</button></div>`;
|
||||
const html = renderDocument({ meta: {}, body: node.repeat(50) });
|
||||
if (mode === "registry-v1") {
|
||||
expect(html).toContain(`data-wrn-scope="${encoded}"`);
|
||||
expect(html).toContain('data-scope="items: []"');
|
||||
} else {
|
||||
expect(html).not.toContain(encoded);
|
||||
expect(html).toContain('data-scope=""');
|
||||
expect(html).toContain('data-wrn-loop-locals="@');
|
||||
expect(html.length).toBeLessThan(node.length * 10);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("scope state named body remains data rather than a control template", () => {
|
||||
runWithRequestContext({} as never, () => {
|
||||
const value = {
|
||||
body: '<style data-wrnexus-style-id="user">literal user text</style>',
|
||||
label: "keep",
|
||||
};
|
||||
const encoded = Buffer.from(JSON.stringify(value)).toString("base64");
|
||||
const html = renderDocument({
|
||||
meta: {},
|
||||
body: `<div data-scope="" data-wrn-scope="${encoded}"></div>`,
|
||||
});
|
||||
const entries = JSON.parse(/data-wrn-controls[^>]*>(.*?)<\/script>/s.exec(html)![1]!);
|
||||
expect(entries.find((entry: any) => entry.__wrnMetadata)?.value).toEqual(value);
|
||||
expect(html.split("</head>")[0]).not.toContain("literal user text");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user