release: WRNexusJS 0.8.0
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-02 23:18:51 +05:30
parent 87507edf59
commit 586a6db8ff
625 changed files with 243608 additions and 11210 deletions
+371 -19
View File
@@ -21,6 +21,7 @@ import { VOID_ELEMENTS, type Attr, type DataMode, type PageAst, type ViewNode }
import { eraseFunctionTypes, runtimeTypeOf } from "./types.ts";
import { stripRuntimeFunctionModifiers } from "@wrnexus/syntax";
import { generateStoreModule } from "./store-codegen.ts";
import { optimizeAst } from "./analysis.ts";
interface RenderBinding {
method: string;
@@ -352,6 +353,55 @@ function renderLoopBody(node: ViewNode): string {
const inner = node.children.map(renderLoopBody).join("");
if (node.tag === "Static") return inner;
if (node.tag === "Dynamic")
return (
escLit('<wrn-dynamic-region data-wrn-dynamic="true">') +
inner +
escLit("</wrn-dynamic-region>")
);
if (node.tag === "KeepAlive") {
const key = node.attrs.find((attribute) => attribute.name === "key")?.value ?? "default";
return (
escLit('<div data-wrn-keepalive="') +
bakeLoopAttr(key) +
escLit(`">`) +
inner +
escLit("</div>")
);
}
if (node.tag === "Portal") {
const target = node.attrs.find((attribute) => attribute.name === "to")?.value ?? "body";
return (
escLit('<div data-wrn-portal="') +
bakeLoopAttr(target) +
escLit('">') +
inner +
escLit("</div>")
);
}
if (node.tag === "Transition") {
const name =
node.attrs.find((attribute) => attribute.name === "name")?.value ?? "wrn-transition";
return (
escLit('<div data-wrn-transition="') +
bakeLoopAttr(name) +
escLit('">') +
inner +
escLit("</div>")
);
}
if (node.tag === "Component") {
const selected = node.attrs.find((attribute) => attribute.name === "is")?.value ?? "";
return (
escLit('<div data-wrn-dynamic-component="') +
bakeLoopAttr(selected) +
escLit('">') +
inner +
escLit("</div>")
);
}
if (componentTag) {
return (
escLit(`<div data-component="${attrEscape(node.tag)}"`) +
@@ -417,6 +467,7 @@ function compileIfExpr(node: IfNode): string {
*/
function collectControlExprs(nodes: ViewNode[], out: string[] = []): string[] {
for (const node of nodes) {
if (node.type === "text") continue;
if (node.type === "each") {
out.push(node.list);
collectControlExprs(node.body, out);
@@ -450,6 +501,73 @@ function renderNode(
return `\x00WRNEACH${loops.length - 1}\x00`;
}
if (node.tag === "Static" || node.tag === "Dynamic") {
const inner = node.children
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
.join("");
return node.tag === "Static"
? inner
: `<wrn-dynamic-region data-wrn-dynamic="true">${inner}</wrn-dynamic-region>`;
}
if (node.tag === "Portal" || node.tag === "Transition" || node.tag === "Component") {
const inner = node.children
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
.join("");
const attribute =
node.tag === "Portal"
? "data-wrn-portal"
: node.tag === "Transition"
? "data-wrn-transition"
: "data-wrn-dynamic-component";
const source = node.tag === "Portal" ? "to" : node.tag === "Transition" ? "name" : "is";
const fallback =
node.tag === "Portal" ? "body" : node.tag === "Transition" ? "wrn-transition" : "";
const original = node.attrs.find((item) => item.name === source);
const rendered = original
? renderAttrs([{ ...original, name: attribute }], undefined, reactive, loops)
: ` ${attribute}="${attrEscape(fallback)}"`;
return `<div${rendered}>${inner}</div>`;
}
if (node.tag === "Async") {
const source = attrValue(node.attrs, "source") ?? "data";
const retries = attrValue(node.attrs, "retries") ?? "2";
const serverResolved = attrValue(node.attrs, "data-wrn-async-server") === "true";
const asyncIndex = serverResolved ? loops.push("") - 1 : -1;
const branch = (name: string) => {
const element = node.children.find(
(child): child is Extract<ViewNode, { type: "element" }> =>
child.type === "element" && child.tag === name,
);
return (element?.children ?? [])
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
.join("");
};
const loading = branch("Loading");
const success = branch("Success");
const error = branch("Error");
let initial = loading;
if (serverResolved) {
const nested = (value: string) => value.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
const sourcePattern = source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const serverSuccess = success.replace(
new RegExp(`\\{\\s*(${sourcePattern}(?:\\.[A-Za-z_$][\\w$]*)*)\\s*\\}`, "g"),
(_whole, expression: string) => `\${__wrnexusEscapeHtml(${expression})}`,
);
loops[asyncIndex] =
`\${ctx[${JSON.stringify(source)}] !== undefined ? \`${nested(serverSuccess)}\` : \`${nested(loading)}\`}`;
initial = `\x00WRNEACH${asyncIndex}\x00`;
}
return `<section data-wrn-async="${attrEscape(source)}" data-wrn-async-retries="${attrEscape(retries)}"${serverResolved ? ' data-wrn-async-resolved="true"' : ""} aria-busy="${serverResolved ? "false" : "true"}"><div data-wrn-async-content>${initial}</div><template data-wrn-async-loading>${loading}</template><template data-wrn-async-success>${success}</template><template data-wrn-async-error>${error}</template></section>`;
}
if (node.tag === "KeepAlive") {
const key = attrValue(node.attrs, "key") ?? "default";
const inner = node.children
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
.join("");
return `<div data-wrn-keepalive="${attrEscape(key)}">${inner}</div>`;
}
if (isComponentTag(node.tag)) {
return renderPageComponentInvocation(
node,
@@ -848,7 +966,9 @@ function generateSsrStateAliases(stateNames: string[]): string {
}
function hydrationAttribute(ast: PageAst): string {
const strategy = ast.hydrate ?? "load";
const strategy = ["static", "server"].includes(ast.renderMode ?? "")
? "none"
: (ast.hydrate ?? "load");
const hasBrowserModule = ast.runtimeFunctions.some((fn) =>
["legacy", "client", "shared"].includes(fn.runtime),
);
@@ -879,13 +999,88 @@ function publicOutputNames(ast: PageAst): string[] {
];
}
function prepareActionForms(nodes: ViewNode[], actions: ReadonlySet<string>): void {
for (const node of nodes) {
if (node.type === "text") continue;
if (node.type === "each") {
prepareActionForms(node.body, actions);
prepareActionForms(node.empty, actions);
continue;
}
if (node.type === "if") {
node.branches.forEach((branch) => prepareActionForms(branch.body, actions));
continue;
}
prepareActionForms(node.children, actions);
if (node.tag.toLowerCase() !== "form") continue;
const submit = node.attrs.find((attr) => attr.event && attr.name === "submit");
if (!submit || !actions.has(submit.value.trim())) continue;
const name = submit.value.trim();
node.attrs = node.attrs.filter((attr) => attr !== submit);
if (!node.attrs.some((attr) => !attr.event && attr.name === "method")) {
node.attrs.push({ name: "method", value: "post", event: false });
}
node.attrs.push({ name: "data-wrn-action", value: name, event: false });
node.children.unshift({
type: "element",
tag: "input",
attrs: [
{ name: "type", value: "hidden", event: false },
{ name: "name", value: "_wrnexus_action", event: false },
{ name: "value", value: name, event: false },
],
children: [],
});
}
}
function markServerAsyncBoundaries(nodes: ViewNode[], serverLoads: ReadonlySet<string>): void {
for (const node of nodes) {
if (node.type === "text") continue;
if (node.type === "each") {
markServerAsyncBoundaries(node.body, serverLoads);
markServerAsyncBoundaries(node.empty, serverLoads);
continue;
}
if (node.type === "if") {
node.branches.forEach((branch) => markServerAsyncBoundaries(branch.body, serverLoads));
continue;
}
if (node.tag === "Async") {
const source = attrValue(node.attrs, "source") ?? "data";
if (
serverLoads.has(source) &&
!node.attrs.some((attribute) => attribute.name === "data-wrn-async-server")
) {
node.attrs.push({ name: "data-wrn-async-server", value: "true", event: false });
}
}
markServerAsyncBoundaries(node.children, serverLoads);
}
}
export function generate(ast: PageAst): string {
ast = optimizeAst(ast).ast;
if (ast.kind === "global-store" || ast.kind === "page-store") return generateStoreModule(ast);
if (ast.kind === "component" || ast.kind === "layout") {
return generateComponent(ast);
}
const out: string[] = [];
prepareActionForms(ast.view, new Set(ast.actions.map((action) => action.name)));
markServerAsyncBoundaries(
ast.view,
new Set(
ast.loads
.filter((load) => load.mode === "server" && !load.deferred && load.name)
.map((load) => load.name!),
),
);
if (ast.actions.length > 0) {
out.push(
`import { createActionClient } from "@wrnexus/csr";\nimport type { InferSchema } from "@wrnexus/validation";`,
);
}
if (ast.imports.length > 0) out.push(generatedImports(ast).join("\n"));
const ssrBindings: SsrBinding[] = [];
const csrBindings: CsrBinding[] = [];
@@ -909,11 +1104,19 @@ export function generate(ast: PageAst): string {
`export const layout = ${ast.layoutIsSymbol ? ast.layout : JSON.stringify(ast.layout)};`,
);
out.push(`export const __wrnexusRuntime = ${JSON.stringify(ast.runtime ?? "universal")};`);
out.push(`export const __wrnexusHydrate = ${JSON.stringify(ast.hydrate ?? "load")};`);
out.push(`export const __wrnexusRender = ${JSON.stringify(ast.renderMode ?? "hybrid")};`);
out.push(
`export const __wrnexusHydrate = ${JSON.stringify(["static", "server"].includes(ast.renderMode ?? "") ? "none" : (ast.hydrate ?? "load"))};`,
);
out.push(`export const __wrnexusHydrationId = ${JSON.stringify(hydrationId(ast))};`);
if (Object.keys(ast.cache ?? {}).length > 0)
out.push(`export const __wrnexusCache = ${JSON.stringify(ast.cache, null, 2)};`);
if (Object.keys(ast.security).length > 0) {
out.push(`export const __wrnexusSecurity = ${JSON.stringify(ast.security, null, 2)};`);
}
if (Object.keys(ast.navigation).length > 0) {
out.push(`export const __wrnexusNavigation = ${JSON.stringify(ast.navigation, null, 2)};`);
}
// --- View -> default page component ---
const browserStates = ast.states.filter((state) => state.runtime !== "server");
@@ -960,6 +1163,10 @@ export function generate(ast: PageAst): string {
if (pageStyleTag) {
html = `${pageStyleTag}${html}`;
}
if (ast.renderMode === "client") {
const clientRoot = hydrationId(ast);
html = `<div data-wrn-client-root="${clientRoot}" aria-busy="true"></div><template data-wrn-client-template="${clientRoot}">${html}</template>`;
}
const pageStyleExport = localStyleExport(ast, styles);
if (pageStyleExport) out.push(pageStyleExport);
if (csrBindings.length > 0) {
@@ -972,6 +1179,14 @@ export function generate(ast: PageAst): string {
// Escape the static HTML for the template literal, then swap loop sentinels for
// their real `${…}` code (which must NOT be escaped).
let body = templateEscape(html);
let staticShellBody: string | undefined;
if (ast.renderMode === "partial-static") {
const shellHtml = html.replace(
/<wrn-dynamic-region\b[^>]*>[\s\S]*?<\/wrn-dynamic-region>/gi,
'<wrn-dynamic-region data-wrn-dynamic="true"></wrn-dynamic-region>',
);
staticShellBody = templateEscape(shellHtml);
}
const dynamicStateScope = ast.states
.map(
(state) =>
@@ -994,9 +1209,16 @@ export function generate(ast: PageAst): string {
(entry) => ` const ${entry.local} = await ctx.__wrnexusUseStore(${entry.internal});`,
)
.join("\n");
const serverLoadAliases = ast.loads
.filter((load) => load.mode === "server" && !load.deferred && load.name)
.map((load) => ` const ${load.name} = ctx[${JSON.stringify(load.name)}];`)
.join("\n");
loops.forEach((code, idx) => {
body = body.replace(`\x00WRNEACH${idx}\x00`, () => code);
if (staticShellBody?.includes(`\x00WRNEACH${idx}\x00`)) {
staticShellBody = staticShellBody.replace(`\x00WRNEACH${idx}\x00`, () => code);
}
});
// Server loops iterate raw SSR data. Declare a named const for every `ssr` data
@@ -1024,6 +1246,7 @@ export function generate(ast: PageAst): string {
out.push(
`export default async function ${ast.name}(ctx: any) {
${storeDeclarations}
${serverLoadAliases}
${decls}
const __state: ${stateType} = { ${dynamicStateScope} };
${ssrStateAliases}
@@ -1055,6 +1278,7 @@ export function generate(ast: PageAst): string {
out.push(
`export default ${storeBindings.length > 0 ? "async " : ""}function ${ast.name}(ctx: any) {
${storeDeclarations}
${serverLoadAliases}
const __state: ${stateType} = { ${dynamicStateScope} };
${ssrStateAliases}
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
@@ -1081,30 +1305,122 @@ export function generate(ast: PageAst): string {
);
}
if (staticShellBody !== undefined) {
out.push(
`export async function __wrnexusBuildStaticShell(ctx: any = {}) {
${storeDeclarations}
${serverLoadAliases}
${loopConsts.length > 0 ? loopConsts.join("\n") : ""}
const __state: ${stateType} = { ${dynamicStateScope} };
${ssrStateAliases}
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
const __scopeValue = Object.entries(__hydrationState)
.map(([key, value]) => {
const encoded = typeof value === "number" || typeof value === "boolean"
? String(value)
: JSON.stringify(value == null ? "" : String(value));
return key + ": " + encoded;
})
.join(", ")
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
return \`${staticShellBody}\`.replace("__WRNEXUS_DYNAMIC_SCOPE__", __scopeValue);
}`,
);
}
if (ast.loads.length > 0) {
const serverLoads = ast.loads.filter((entry) => entry.mode === "server");
const clientLoads = ast.loads.filter((entry) => entry.mode === "client");
if (serverLoads.length > 0) {
out.push(
`export async function __wrnexusLoad(ctx: any) {\n${serverLoads.map((entry) => entry.body).join("\n")}\n}`,
);
}
if (clientLoads.length > 0) {
out.push(
`export async function __wrnexusClientLoad(ctx: any) {
${clientLoads.map((entry) => entry.body).join("\n")}
}`,
);
}
const serverLoads = ast.loads.filter((entry) => entry.mode === "server" && !entry.deferred);
const publicClientLoads = ast.loads.filter(
(entry) => entry.mode === "client" || entry.deferred,
);
const namedByName = new Map(
ast.loads.filter((entry) => entry.name).map((entry) => [entry.name!, entry]),
);
const clientNames = new Set(
publicClientLoads.flatMap((entry) => (entry.name ? [entry.name] : [])),
);
const includeDependencies = (name: string): void => {
for (const dependency of namedByName.get(name)?.dependsOn ?? []) {
if (clientNames.has(dependency)) continue;
clientNames.add(dependency);
includeDependencies(dependency);
}
};
for (const name of [...clientNames]) includeDependencies(name);
const clientLoads = ast.loads.filter((entry) => !entry.name || clientNames.has(entry.name));
const renderLoads = (
exportName: string,
execution: typeof ast.loads,
exposed: typeof ast.loads,
): string => {
const declarations = execution
.filter((entry) => entry.name)
.map((entry) => {
const dependencies = (entry.dependsOn ?? [])
.map((dependency) => `const ${dependency} = await __load_${dependency}();`)
.join("\n");
return ` let __promise_${entry.name}: Promise<unknown> | undefined;
const __load_${entry.name} = () => (__promise_${entry.name} ??= (async () => {
${dependencies}
${entry.body}
})());`;
})
.join("\n");
const visible = exposed.filter((entry) => entry.name);
return `export async function ${exportName}(ctx: any) {
${exposed
.filter((entry) => !entry.name)
.map((entry) => entry.body)
.join("\n")}
${declarations}
${
visible.length
? ` const __values = await Promise.all([${visible.map((entry) => `__load_${entry.name}()`).join(", ")}]);
return { ${visible.map((entry, index) => `${JSON.stringify(entry.name)}: __values[${index}]`).join(", ")} };`
: ""
}
}`;
};
if (serverLoads.length > 0) out.push(renderLoads("__wrnexusLoad", serverLoads, serverLoads));
if (publicClientLoads.length > 0)
out.push(renderLoads("__wrnexusClientLoad", clientLoads, publicClientLoads));
}
if (ast.actions.length > 0) {
for (const action of ast.actions) {
out.push(`export async function ${action.name}(${action.args.join(", ")}) {${action.body}}`);
if (!action.schema) {
out.push(
`export async function ${action.name}(${action.args.join(", ")}) {${action.body}}`,
);
continue;
}
out.push(`export async function ${action.name}(input: any, ctx: any) {
const invalidate = (...tags: string[]) => {
const bucket = (ctx.locals.__wrnexusInvalidatedTags ??= []);
bucket.push(...tags.flat());
};
${action.body}
}`);
}
out.push(
`export const __wrnexusActions = { ${ast.actions.map((action) => action.name).join(", ")} };`,
`export const __wrnexusActions = { ${ast.actions
.map(
(action) =>
`${action.name}: { run: ${action.name}, schema: ${action.schema ?? "undefined"} }`,
)
.join(", ")} };`,
);
out.push(`export const __wrnexusActionClients = {
${ast.actions
.map(
(action) =>
` ${action.name}: createActionClient<${action.schema ? `InferSchema<typeof ${action.schema}>` : "Record<string, unknown>"}, Awaited<ReturnType<typeof ${action.name}>>>("", ${JSON.stringify(action.name)}),`,
)
.join("\n")}
};`);
}
// --- API blocks -> method handlers ---
@@ -1545,6 +1861,34 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
return renderComponentIfNode(node, ctx);
}
if (node.tag === "Static" || node.tag === "Dynamic") {
const inner = node.children.map((child) => renderComponentNode(child, ctx)).join("");
return node.tag === "Static"
? inner
: `<wrn-dynamic-region data-wrn-dynamic="true">${inner}</wrn-dynamic-region>`;
}
if (node.tag === "KeepAlive") {
const key = node.attrs.find((attribute) => attribute.name === "key")?.value ?? "default";
const inner = node.children.map((child) => renderComponentNode(child, ctx)).join("");
return `<div data-wrn-keepalive="${compileAttrValue(key, ctx)}">${inner}</div>`;
}
if (node.tag === "Portal" || node.tag === "Transition" || node.tag === "Component") {
const inner = node.children.map((child) => renderComponentNode(child, ctx)).join("");
const attribute =
node.tag === "Portal"
? "data-wrn-portal"
: node.tag === "Transition"
? "data-wrn-transition"
: "data-wrn-dynamic-component";
const source = node.tag === "Portal" ? "to" : node.tag === "Transition" ? "name" : "is";
const fallback =
node.tag === "Portal" ? "body" : node.tag === "Transition" ? "wrn-transition" : "";
const raw = node.attrs.find((item) => item.name === source)?.value ?? fallback;
return `<div ${attribute}="${compileAttrValue(raw, ctx)}">${inner}</div>`;
}
if (isComponentTag(node.tag)) {
return renderNestedComponentInvocation(node, ctx);
}
@@ -1865,11 +2209,19 @@ function generateComponent(ast: PageAst): string {
out.push(`export const __wrnexusComponent = ${JSON.stringify(ast.name)};`);
}
out.push(`export const __wrnexusRuntime = ${JSON.stringify(ast.runtime ?? "universal")};`);
out.push(`export const __wrnexusHydrate = ${JSON.stringify(ast.hydrate ?? "load")};`);
out.push(`export const __wrnexusRender = ${JSON.stringify(ast.renderMode ?? "hybrid")};`);
out.push(
`export const __wrnexusHydrate = ${JSON.stringify(["static", "server"].includes(ast.renderMode ?? "") ? "none" : (ast.hydrate ?? "load"))};`,
);
out.push(`export const __wrnexusHydrationId = ${JSON.stringify(hydrationId(ast))};`);
if (Object.keys(ast.cache ?? {}).length > 0)
out.push(`export const __wrnexusCache = ${JSON.stringify(ast.cache, null, 2)};`);
if (Object.keys(ast.security).length > 0) {
out.push(`export const __wrnexusSecurity = ${JSON.stringify(ast.security, null, 2)};`);
}
if (Object.keys(ast.navigation).length > 0) {
out.push(`export const __wrnexusNavigation = ${JSON.stringify(ast.navigation, null, 2)};`);
}
const componentStyleExport = localStyleExport(ast, styles);
if (componentStyleExport) out.push(componentStyleExport);