fix: defer reactive props in branch pre-rendering
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/dev-server",
|
"name": "@wrnexus/dev-server",
|
||||||
"version": "0.8.52",
|
"version": "0.8.53",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1739,6 +1739,15 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
|||||||
// Props may carry `{t:key}` i18n markers — resolve them for the active
|
// Props may carry `{t:key}` i18n markers — resolve them for the active
|
||||||
// language before handing them to the component.
|
// language before handing them to the component.
|
||||||
const props = resolveTProps(parseComponentProps(attrStr!), translate);
|
const props = resolveTProps(parseComponentProps(attrStr!), translate);
|
||||||
|
// Client control templates retain reactive expressions (for example
|
||||||
|
// `metrics="{metrics}"`) because their parent state is not available
|
||||||
|
// while we pre-render the inert branch metadata. Do not feed those
|
||||||
|
// expression strings into typed component coercion. Rendering with the
|
||||||
|
// component defaults creates the real child boundary; the bridged prop
|
||||||
|
// binding synchronizes the live value when the branch is instantiated.
|
||||||
|
for (const [key, value] of Object.entries(props)) {
|
||||||
|
if (/^\{[\s\S]*\}$/.test(value) && !/^\{t:/.test(value)) delete props[key];
|
||||||
|
}
|
||||||
if (normalizedName === "languageswitcher" && deps.i18n) {
|
if (normalizedName === "languageswitcher" && deps.i18n) {
|
||||||
props.locales ??= JSON.stringify(
|
props.locales ??= JSON.stringify(
|
||||||
deps.i18n.langs.map((locale) => ({
|
deps.i18n.langs.map((locale) => ({
|
||||||
@@ -2682,7 +2691,12 @@ export function parseComponentProps(attrStr: string): Record<string, string> {
|
|||||||
const props: Record<string, string> = {};
|
const props: Record<string, string> = {};
|
||||||
for (const m of attrStr.matchAll(/([A-Za-z_][\w-]*)(?:="([^"]*)")?/g)) {
|
for (const m of attrStr.matchAll(/([A-Za-z_][\w-]*)(?:="([^"]*)")?/g)) {
|
||||||
const key = m[1]!;
|
const key = m[1]!;
|
||||||
if (key === "data-component") continue;
|
if (
|
||||||
|
key === "data-component" ||
|
||||||
|
key.startsWith("data-wrn-bind-") ||
|
||||||
|
key.startsWith("data-wrn-prop-bind-")
|
||||||
|
)
|
||||||
|
continue;
|
||||||
props[key] = decodeHtmlEntities(m[2] ?? "");
|
props[key] = decodeHtmlEntities(m[2] ?? "");
|
||||||
}
|
}
|
||||||
return props;
|
return props;
|
||||||
|
|||||||
@@ -90,6 +90,58 @@ test("a component in an initially hidden client branch is server-prepared for in
|
|||||||
expect(branches[0].body).not.toContain("data-component=");
|
expect(branches[0].body).not.toContain("data-component=");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("typed components in client branches use defaults until reactive props are available", async () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "wrnexus-component-reactive-branch-"));
|
||||||
|
roots.push(root);
|
||||||
|
const app = join(root, "app");
|
||||||
|
mkdirSync(join(app, "pages"), { recursive: true });
|
||||||
|
mkdirSync(join(app, "components"), { recursive: true });
|
||||||
|
writeFileSync(join(app, "pages/index.ts"), "export default () => '';\n");
|
||||||
|
writeFileSync(join(app, "components/Panel.wrn"), "component Panel { view { <section /> } }\n");
|
||||||
|
const marker = "["items","{items}"]";
|
||||||
|
const definition = Buffer.from(
|
||||||
|
JSON.stringify([
|
||||||
|
{
|
||||||
|
cond: "open",
|
||||||
|
body: `<div data-component="Panel" items="{items}" visible="{true}" data-wrn-prop-bind-0="${marker}"></div>`,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
"utf8",
|
||||||
|
).toString("base64");
|
||||||
|
let observed: Record<string, string> | undefined;
|
||||||
|
|
||||||
|
const handlers = createHandlers({
|
||||||
|
mode: "development",
|
||||||
|
hmr: false,
|
||||||
|
router: buildRouter(app),
|
||||||
|
loadModule: async (file) =>
|
||||||
|
basename(file) === "Panel.wrn"
|
||||||
|
? {
|
||||||
|
render: (props: Record<string, string>) => {
|
||||||
|
observed = props;
|
||||||
|
return '<section data-scope="items: [], visible: false" data-wrn-scope="e30="></section>';
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
default: () =>
|
||||||
|
`<template data-wrn-if="${definition}"></template><template data-wrn-control-end></template>`,
|
||||||
|
},
|
||||||
|
getMiddleware: async () => [],
|
||||||
|
assets: { serve: async () => null },
|
||||||
|
} satisfies RuntimeDeps);
|
||||||
|
|
||||||
|
const response = await handlers.fetch(new Request("https://example.test/"), {
|
||||||
|
upgrade: () => false,
|
||||||
|
});
|
||||||
|
const html = await response!.text();
|
||||||
|
expect(observed).toEqual({});
|
||||||
|
const encoded = /data-wrn-if="([A-Za-z0-9+/=]+)"/.exec(html)?.[1];
|
||||||
|
const branches = JSON.parse(Buffer.from(encoded!, "base64").toString("utf8"));
|
||||||
|
expect(branches[0].body).toContain("<section");
|
||||||
|
expect(branches[0].body).toContain("data-wrn-prop-bind-0");
|
||||||
|
expect(branches[0].body).not.toContain("data-component=");
|
||||||
|
});
|
||||||
|
|
||||||
test("SSR carries a parent prop binding onto the rendered child scope", async () => {
|
test("SSR carries a parent prop binding onto the rendered child scope", async () => {
|
||||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-component-prop-"));
|
const root = mkdtempSync(join(tmpdir(), "wrnexus-component-prop-"));
|
||||||
roots.push(root);
|
roots.push(root);
|
||||||
|
|||||||
Reference in New Issue
Block a user