release: WRNexusJS 0.6.0

This commit is contained in:
2026-08-01 01:09:58 +05:30
parent 3e565e8d03
commit 687d345882
502 changed files with 33038 additions and 11358 deletions
+116 -33
View File
@@ -19,6 +19,8 @@ import { Buffer } from "node:buffer";
import { VOID_ELEMENTS, type Attr, type DataMode, type PageAst, type ViewNode } from "./parser.ts";
import { eraseFunctionTypes, runtimeTypeOf } from "./types.ts";
import { stripRuntimeFunctionModifiers } from "@wrnexus/syntax";
import { generateStoreModule } from "./store-codegen.ts";
interface RenderBinding {
method: string;
@@ -765,6 +767,38 @@ function localStyleExport(ast: PageAst, styles: string[]): string | null {
)};`;
}
function isStoreImportSource(source: string): boolean {
return /(?:^|\/)stores?\//.test(source) || source.startsWith("@wrnexus/store");
}
function importedStoreBindings(ast: PageAst): Array<{ local: string; internal: string }> {
return ast.structuredImports
.filter(
(entry) =>
entry.defaultImport && entry.source.endsWith(".wrn") && isStoreImportSource(entry.source),
)
.map((entry) => ({
local: entry.defaultImport!,
internal: `__wrnexusStoreDefinition_${entry.defaultImport}`,
}));
}
function generatedImports(ast: PageAst): string[] {
const stores = new Map(importedStoreBindings(ast).map((entry) => [entry.local, entry.internal]));
return ast.structuredImports.map((entry) => {
if (!entry.defaultImport) return entry.raw;
const internal = stores.get(entry.defaultImport);
return internal
? entry.raw.replace(
new RegExp(
`^(\\s*import\\s+(?:type\\s+)?)(?:${entry.defaultImport.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")})(\\s+from\\s+)`,
),
`$1${internal}$2`,
)
: entry.raw;
});
}
function isSafeGeneratedIdentifier(name: string): boolean {
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
}
@@ -781,22 +815,47 @@ function generateSsrStateAliases(stateNames: string[]): string {
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")}"`;
const hasBrowserModule = ast.runtimeFunctions.some((fn) =>
["legacy", "client", "shared"].includes(fn.runtime),
);
const moduleAttribute = hasBrowserModule
? ' data-wrn-client-module="__WRNEXUS_CLIENT_MODULE__"'
: "";
return ` data-wrn-hydration="${attrEscape(hydrationId(ast))}" data-wrn-hydrate="${attrEscape(strategy)}" data-wrn-runtime="${attrEscape(ast.runtime ?? "universal")}"${moduleAttribute}`;
}
function targetFunctions(ast: PageAst, target: "browser" | "server"): string {
const runtimes =
target === "browser"
? (["legacy", "client", "shared"] as const)
: (["legacy", "server", "shared"] as const);
return ast.functions
.map((body) => stripRuntimeFunctionModifiers(body, [...runtimes]))
.map((body) => body.trim())
.filter(Boolean)
.join("\n\n");
}
function publicOutputNames(ast: PageAst): string[] {
return [
...new Set([
...ast.outputs.map((output) => output.name),
...ast.events.map((event) => event.name),
]),
];
}
export function generate(ast: PageAst): string {
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[] = [];
if (ast.imports.length > 0) out.push(ast.imports.join("\n"));
if (ast.imports.length > 0) out.push(generatedImports(ast).join("\n"));
const ssrBindings: SsrBinding[] = [];
const csrBindings: CsrBinding[] = [];
const helpers = ast.functions
.map((body) => body.trim())
.filter(Boolean)
.join("\n\n");
const helpers = targetFunctions(ast, "server");
const apiBindings = apiBindingMap(ast, helpers);
const typeSource = ast.types
@@ -811,7 +870,10 @@ 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)};`);
if (ast.layout)
out.push(
`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 __wrnexusHydrationId = ${JSON.stringify(hydrationId(ast))};`);
@@ -820,6 +882,7 @@ export function generate(ast: PageAst): string {
}
// --- View -> default page component ---
const browserStates = ast.states.filter((state) => state.runtime !== "server");
const seedScope = evalStateSeeds(ast.states);
for (const entry of ast.computed) {
try {
@@ -831,7 +894,7 @@ export function generate(ast: PageAst): string {
}
}
const reactiveNames = [
...ast.states.map((entry) => entry.name),
...browserStates.map((entry) => entry.name),
...ast.computed.map((entry) => entry.name),
];
const runtimeStateNames = new Set(
@@ -849,7 +912,7 @@ export function generate(ast: PageAst): string {
const pageBehavior = ast.runtime === "server" ? null : componentBehavior(ast);
const needsClientRuntime =
ast.runtime !== "server" &&
(ast.states.length > 0 ||
(browserStates.length > 0 ||
ast.computed.length > 0 ||
hasClientBehavior(ast.view) ||
pageBehavior !== null);
@@ -887,9 +950,17 @@ export function generate(ast: PageAst): string {
.map((state) => `${JSON.stringify(state.name)}: ${state.valueType ?? "unknown"}`)
.join("; ")} }`
: "Record<string, never>";
const hydrationStateNames = JSON.stringify(browserStates.map((state) => state.name));
const ssrStateAliases = generateSsrStateAliases(ast.states.map((state) => state.name));
const storeBindings = importedStoreBindings(ast);
const storeDeclarations = storeBindings
.map(
(entry) => ` const ${entry.local} = await ctx.__wrnexusUseStore(${entry.internal});`,
)
.join("\n");
loops.forEach((code, idx) => {
body = body.replace(`\x00WRNEACH${idx}\x00`, () => code);
});
@@ -918,10 +989,12 @@ export function generate(ast: PageAst): string {
const decls = loopConsts.length > 0 ? loopConsts.join("\n") + "\n" : "";
out.push(
`export default async function ${ast.name}(ctx: any) {
${storeDeclarations}
${decls}
const __state: ${stateType} = { ${dynamicStateScope} };
${ssrStateAliases}
const __scopeValue = Object.entries(__state)
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"
@@ -946,10 +1019,12 @@ export function generate(ast: PageAst): string {
);
} else {
out.push(
`export default function ${ast.name}(ctx: any) {
`export default ${storeBindings.length > 0 ? "async " : ""}function ${ast.name}(ctx: any) {
${storeDeclarations}
const __state: ${stateType} = { ${dynamicStateScope} };
${ssrStateAliases}
const __scopeValue = Object.entries(__state)
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"
@@ -1062,6 +1137,10 @@ interface CompCtx {
interface ComponentBehavior {
functions: string;
outputs: Array<{
name: string;
payload?: { name: string; valueType: string; optional: boolean };
}>;
computed: Array<{ name: string; expr: string }>;
effects: string[];
lifecycle: {
@@ -1150,12 +1229,7 @@ function escLit(s: string): string {
}
function componentBehavior(ast: PageAst): ComponentBehavior | null {
const functions = eraseFunctionTypes(
ast.functions
.map((body) => body.trim())
.filter(Boolean)
.join("\n\n"),
);
const functions = eraseFunctionTypes(targetFunctions(ast, "browser"));
const computed = ast.computed.map((entry) => ({ name: entry.name, expr: entry.expr.trim() }));
const effects = ast.effects.map((entry) => entry.body.trim()).filter(Boolean);
@@ -1175,6 +1249,7 @@ function componentBehavior(ast: PageAst): ComponentBehavior | null {
if (
!functions &&
ast.outputs.length === 0 &&
computed.length === 0 &&
effects.length === 0 &&
Object.keys(lifecycle).length === 0 &&
@@ -1185,6 +1260,7 @@ function componentBehavior(ast: PageAst): ComponentBehavior | null {
return {
functions,
outputs: ast.outputs,
computed,
effects,
lifecycle,
@@ -1613,7 +1689,7 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
function generateComponent(ast: PageAst): string {
const out: string[] = [];
if (ast.imports.length > 0) out.push(ast.imports.join("\n"));
if (ast.imports.length > 0) out.push(generatedImports(ast).join("\n"));
const hasServerEach = viewHasServerEach(ast.view);
const effectiveProps =
@@ -1629,8 +1705,9 @@ function generateComponent(ast: PageAst): string {
]
: ast.props;
const browserStates = ast.states.filter((state) => state.runtime !== "server");
const stateNames = new Set([
...ast.states.map((entry) => entry.name),
...browserStates.map((entry) => entry.name),
...ast.computed.map((entry) => entry.name),
]);
const nameRefs = new Map<string, string>();
@@ -1652,21 +1729,13 @@ function generateComponent(ast: PageAst): string {
const ctx: CompCtx = {
stateNames,
functionNames: new Set(
ast.functions.flatMap((body) => {
return Array.from(
body.matchAll(/(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/g),
(match) => match[1]!,
);
}),
ast.runtimeFunctions.filter((fn) => fn.runtime !== "server").map((fn) => fn.name),
),
resolveExpr,
eventNames: ast.events.map((event) => event.name),
eventNames: publicOutputNames(ast),
};
const serverFunctions = ast.functions
.map((body) => body.trim())
.filter(Boolean)
.join("\n\n");
const serverFunctions = targetFunctions(ast, "server");
const hasExplicitRestSpread = viewHasRestAttributeSpread(ast.view);
const rootElementIndex = ast.view.findIndex((node) => node.type === "element");
@@ -1694,7 +1763,7 @@ function generateComponent(ast: PageAst): string {
const needsScope =
ast.runtime !== "server" &&
(ast.states.length > 0 ||
(browserStates.length > 0 ||
ast.computed.length > 0 ||
viewHasEvents(ast.view) ||
behavior !== null);
@@ -1705,7 +1774,7 @@ function generateComponent(ast: PageAst): string {
const scopeKeys = [
...effectiveProps.map((prop) => prop.name),
...ast.states.map((state) => state.name),
...browserStates.map((state) => state.name),
];
const behaviorAttr = behaviorAttribute(behavior);
@@ -1791,6 +1860,17 @@ function generateComponent(ast: PageAst): string {
);
}
if (ast.outputs.length > 0) {
out.push(
`export interface ${ast.name}Outputs {\n${ast.outputs
.map(
(output) =>
` ${JSON.stringify(output.name)}(${output.payload ? `${output.payload.name}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}): void;`,
)
.join("\n")}\n}`,
);
}
out.push(`function __coerce(v: any, def: any, declared: string = "unknown"): any {
if (v === undefined || v === null) {
return def;
@@ -2023,6 +2103,9 @@ function __wireRaw(v: any): string {
`}`,
);
out.push(
`export default { name: ${JSON.stringify(ast.name)}, kind: ${JSON.stringify(ast.kind)}, render };`,
);
return out.join("\n\n") + "\n";
}