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
+132 -15
View File
@@ -654,6 +654,32 @@ async function __wrnexusRenderSsrBindings(html: string, ctx: any): Promise<strin
}`;
}
function stableHash(value: string): string {
let hash = 0x811c9dc5;
for (let index = 0; index < value.length; index++) {
hash ^= value.charCodeAt(index);
hash = Math.imul(hash, 0x01000193);
}
return (hash >>> 0).toString(36);
}
function hydrationId(ast: PageAst): string {
const shape = JSON.stringify({
kind: ast.kind,
name: ast.name,
props: ast.props.map((entry) => entry.name),
states: ast.states.map((entry) => entry.name),
computed: ast.computed.map((entry) => entry.name),
view: ast.view,
});
return `${ast.name}:${stableHash(shape)}`;
}
function hydrationAttribute(ast: PageAst): string {
const strategy = ast.hydrate ?? "load";
return ` data-wrn-hydration="${attrEscape(hydrationId(ast))}" data-wrn-hydrate="${attrEscape(strategy)}" data-wrn-runtime="${attrEscape(ast.runtime ?? "universal")}"`;
}
export function generate(ast: PageAst): string {
if (ast.kind === "component" || ast.kind === "layout") {
return generateComponent(ast);
@@ -682,22 +708,46 @@ export function generate(ast: PageAst): string {
// --- Page metadata / SEO ---
out.push(`export const meta = ${JSON.stringify({ title: ast.name, ...ast.seo }, null, 2)};`);
if (ast.layout) out.push(`export const 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 __wrnexusHydrationId = ${JSON.stringify(hydrationId(ast))};`);
if (Object.keys(ast.security).length > 0) {
out.push(`export const __wrnexusSecurity = ${JSON.stringify(ast.security, null, 2)};`);
}
// --- View -> default page component ---
const seedScope = evalStateSeeds(ast.states);
for (const entry of ast.computed) {
try {
seedScope[entry.name] = new Function("with(this){return (" + entry.expr + ");}").call(
seedScope,
);
} catch {
seedScope[entry.name] = undefined;
}
}
const reactiveNames = [
...ast.states.map((entry) => entry.name),
...ast.computed.map((entry) => entry.name),
];
const reactive: PageReactive | null =
ast.states.length > 0
? { stateNames: new Set(ast.states.map((s) => s.name)), scope: evalStateSeeds(ast.states) }
: null;
reactiveNames.length > 0 ? { stateNames: new Set(reactiveNames), scope: seedScope } : null;
const loops: string[] = [];
let html = ast.view
.map((node) => renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive))
.join("");
const styles = ast.styles.map((body) => body.trim()).filter(Boolean);
const needsClientRuntime = ast.states.length > 0 || hasClientBehavior(ast.view);
const pageBehavior = ast.runtime === "server" ? null : componentBehavior(ast);
const needsClientRuntime =
ast.runtime !== "server" &&
(ast.states.length > 0 ||
ast.computed.length > 0 ||
hasClientBehavior(ast.view) ||
pageBehavior !== null);
if (needsClientRuntime) {
const scopePlaceholder = "__WRNEXUS_DYNAMIC_SCOPE__";
html = `<div data-scope="${scopePlaceholder}">${html}</div>`;
html = `<div data-scope="${scopePlaceholder}"${behaviorAttribute(pageBehavior)}${hydrationAttribute(ast)}>${html}</div>`;
}
if (styles.length > 0) {
@@ -707,6 +757,9 @@ export function generate(ast: PageAst): string {
if (csrBindings.length > 0) {
out.push(`export const __wrnexusCsr = ${JSON.stringify(csrBindings, null, 2)};`);
}
if (pageBehavior) {
out.push(`export const __wrnexusBehavior = ${JSON.stringify(pageBehavior, null, 2)};`);
}
// Escape the static HTML for the template literal, then swap loop sentinels for
// their real `${…}` code (which must NOT be escaped).
@@ -805,6 +858,32 @@ export function generate(ast: PageAst): string {
);
}
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")}
}`,
);
}
}
if (ast.actions.length > 0) {
for (const action of ast.actions) {
out.push(`export async function ${action.name}(${action.args.join(", ")}) {${action.body}}`);
}
out.push(
`export const __wrnexusActions = { ${ast.actions.map((action) => action.name).join(", ")} };`,
);
}
// --- API blocks -> method handlers ---
if (ast.apis.length > 0) {
ast.apis.forEach((api, index) => {
@@ -863,6 +942,8 @@ interface CompCtx {
interface ComponentBehavior {
functions: string;
computed: Array<{ name: string; expr: string }>;
effects: string[];
lifecycle: {
mount?: string;
update?: string;
@@ -874,13 +955,16 @@ interface ComponentBehavior {
}>;
}
/** Parse a `data-for="item in list"` / `"item, i in list"` directive value. */
export function parseForExpr(value: string): { item: string; index?: string; list: string } | null {
const m = /^\s*([A-Za-z_$][\w$]*)\s*(?:,\s*([A-Za-z_$][\w$]*)\s*)?\s+in\s+([\s\S]+?)\s*$/.exec(
value,
);
/** Parse a `data-for="item in list [key expr]"` / `"item, i in list [key expr]"` directive. */
export function parseForExpr(
value: string,
): { item: string; index?: string; list: string; key?: string } | null {
const m =
/^\s*([A-Za-z_$][\w$]*)\s*(?:,\s*([A-Za-z_$][\w$]*)\s*)?\s+in\s+([\s\S]+?)(?:\s+key\s+([\s\S]+?))?\s*$/.exec(
value,
);
if (!m) return null;
return { item: m[1]!, index: m[2], list: m[3]! };
return { item: m[1]!, index: m[2], list: m[3]!.trim(), key: m[4]?.trim() };
}
/** The loop variables a node introduces via `data-for`, if any. */
@@ -953,6 +1037,9 @@ function componentBehavior(ast: PageAst): ComponentBehavior | null {
.join("\n\n"),
);
const computed = ast.computed.map((entry) => ({ name: entry.name, expr: entry.expr.trim() }));
const effects = ast.effects.map((entry) => entry.body.trim()).filter(Boolean);
const lifecycle = {
...(ast.lifecycle.mount?.trim() ? { mount: ast.lifecycle.mount.trim() } : {}),
@@ -966,12 +1053,20 @@ function componentBehavior(ast: PageAst): ComponentBehavior | null {
body: watch.body.trim(),
}));
if (!functions && Object.keys(lifecycle).length === 0 && watches.length === 0) {
if (
!functions &&
computed.length === 0 &&
effects.length === 0 &&
Object.keys(lifecycle).length === 0 &&
watches.length === 0
) {
return null;
}
return {
functions,
computed,
effects,
lifecycle,
watches,
};
@@ -1357,12 +1452,16 @@ function generateComponent(ast: PageAst): string {
]
: ast.props;
const stateNames = new Set(ast.states.map((s) => s.name));
const stateNames = new Set([
...ast.states.map((entry) => entry.name),
...ast.computed.map((entry) => entry.name),
]);
const nameRefs = new Map<string, string>();
for (const p of effectiveProps) {
nameRefs.set(p.name, safeRef(p.name));
}
for (const s of ast.states) nameRefs.set(s.name, safeRef(s.name));
for (const entry of ast.computed) nameRefs.set(entry.name, safeRef(entry.name));
const resolveExpr = (expr: string): string => {
let result = expr;
for (const [name, ref] of nameRefs) {
@@ -1391,7 +1490,12 @@ function generateComponent(ast: PageAst): string {
// no JavaScript at all.
const behavior = componentBehavior(ast);
const needsScope = ast.states.length > 0 || viewHasEvents(ast.view) || behavior !== null;
const needsScope =
ast.runtime !== "server" &&
(ast.states.length > 0 ||
ast.computed.length > 0 ||
viewHasEvents(ast.view) ||
behavior !== null);
const scopeKeys = [
...effectiveProps.map((prop) => prop.name),
@@ -1418,9 +1522,16 @@ function generateComponent(ast: PageAst): string {
` let ${nameRefs.get(state.name)}${state.valueType ? `: ${state.valueType}` : ""} = (${resolveExpr(state.expr)});`,
);
}
for (const entry of ast.computed) {
decls.push(` const ${nameRefs.get(entry.name)} = (${resolveExpr(entry.expr)});`);
}
const returnExpr = needsScope
? "`" + styleTag + `<div data-scope="\${__scope}"${behaviorAttr}>` + viewCode + "</div>`"
? "`" +
styleTag +
`<div data-scope="\${__scope}"${behaviorAttr}${hydrationAttribute(ast)}>` +
viewCode +
"</div>`"
: "`" + styleTag + viewCode + "`";
const scopeLine =
@@ -1437,6 +1548,12 @@ function generateComponent(ast: PageAst): string {
} else {
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 __wrnexusHydrationId = ${JSON.stringify(hydrationId(ast))};`);
if (Object.keys(ast.security).length > 0) {
out.push(`export const __wrnexusSecurity = ${JSON.stringify(ast.security, null, 2)};`);
}
if (behavior) {
out.push(`export const __wrnexusBehavior = ${JSON.stringify(behavior, null, 2)};`);