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
+3 -2
View File
@@ -1,12 +1,13 @@
{
"name": "@wrnexus/compiler",
"version": "0.5.14",
"version": "0.6.0",
"type": "module",
"main": "src/index.ts",
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"@wrnexus/syntax": "workspace:*"
"@wrnexus/syntax": "workspace:*",
"@wrnexus/store": "workspace:*"
}
}
+200
View File
@@ -0,0 +1,200 @@
import { eraseFunctionTypes, type PageAst, type RuntimeFunctionDecl } from "@wrnexus/syntax";
const RESERVED_BINDINGS = new Set([
"await",
"break",
"case",
"catch",
"class",
"const",
"continue",
"debugger",
"default",
"delete",
"do",
"else",
"enum",
"export",
"extends",
"false",
"finally",
"for",
"function",
"if",
"implements",
"import",
"in",
"instanceof",
"interface",
"let",
"new",
"null",
"package",
"private",
"protected",
"public",
"return",
"static",
"super",
"switch",
"this",
"throw",
"true",
"try",
"typeof",
"var",
"void",
"while",
"with",
"yield",
]);
const RUNTIME_BINDINGS = new Set([
"context",
"state",
"output",
"server",
"props",
"refs",
"event",
"payload",
]);
function safeIdentifier(name: string): boolean {
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && !RESERVED_BINDINGS.has(name);
}
function functionEntry(
ast: PageAst,
fn: RuntimeFunctionDecl,
availableFunctions: string[],
): string {
const parameterNames = new Set(fn.parameters.map((parameter) => parameter.name));
const stateNames = ast.states
.filter(
(state) =>
state.runtime !== "server" &&
safeIdentifier(state.name) &&
!RUNTIME_BINDINGS.has(state.name) &&
!parameterNames.has(state.name),
)
.map((state) => state.name);
const stateSet = new Set(stateNames);
const propNames = ast.props
.filter(
(prop) =>
safeIdentifier(prop.name) &&
!RUNTIME_BINDINGS.has(prop.name) &&
!parameterNames.has(prop.name) &&
!stateSet.has(prop.name),
)
.map((prop) => prop.name);
const functionAliases = availableFunctions.filter(
(name) =>
safeIdentifier(name) &&
!RUNTIME_BINDINGS.has(name) &&
!parameterNames.has(name) &&
!stateSet.has(name) &&
!propNames.includes(name),
);
const parameters = fn.parameters.map((parameter) => parameter.name).join(", ");
const initialStateSnapshot = stateNames.length
? `const __wrnexusInitialState = { ${stateNames.map((name) => `${JSON.stringify(name)}: context.state.${name}`).join(", ")} };`
: "";
const stateAliases = stateNames.length ? `let { ${stateNames.join(", ")} } = context.state;` : "";
const propAliases = propNames.length ? `const { ${propNames.join(", ")} } = context.props;` : "";
const syncStateToContext = stateNames.map((name) => `context.state.${name} = ${name};`).join(" ");
const syncStateFromContext = stateNames
.map((name) => `${name} = context.state.${name};`)
.join(" ");
const peerAliases = functionAliases
.map((name) => {
const call = `context.functions[${JSON.stringify(name)}](...__wrnexusPeerArgs)`;
if (!stateNames.length) {
return `const ${name} = (...__wrnexusPeerArgs) => ${call};`;
}
return `const ${name} = (...__wrnexusPeerArgs) => {
${syncStateToContext}
let __wrnexusPeerResult;
try {
__wrnexusPeerResult = ${call};
} catch (__wrnexusPeerError) {
${syncStateFromContext}
throw __wrnexusPeerError;
}
if (__wrnexusPeerResult && typeof __wrnexusPeerResult.then === "function") {
return Promise.resolve(__wrnexusPeerResult).finally(() => { ${syncStateFromContext} });
}
${syncStateFromContext}
return __wrnexusPeerResult;
};`;
})
.join("\n");
const copyBack = stateNames
.map(
(name) =>
`if (!Object.is(${name}, __wrnexusInitialState[${JSON.stringify(name)}])) context.state.${name} = ${name};`,
)
.join("\n");
const body = eraseFunctionTypes(fn.body);
const runtimeBindings = [
!parameterNames.has("output") ? "const output = context.output;" : "",
!parameterNames.has("server") ? "const server = context.server;" : "",
!parameterNames.has("props") ? "const props = context.props;" : "",
!parameterNames.has("refs") ? "const refs = context.refs;" : "",
]
.filter(Boolean)
.join("\n ");
return `${JSON.stringify(fn.name)}: ${fn.async ? "async " : ""}function(context${parameters ? `, ${parameters}` : ""}) {
${initialStateSnapshot}
${stateAliases}
${propAliases}
${peerAliases}
${runtimeBindings}
try {
${body}
} finally {
${copyBack}
}
}`;
}
export function generateBrowserModule(ast: PageAst): string {
const functions = ast.runtimeFunctions.filter((fn) =>
["legacy", "client", "shared"].includes(fn.runtime),
);
const functionNames = functions.map((fn) => fn.name);
const state = ast.states.filter((entry) => entry.runtime !== "server").map((entry) => entry.name);
const storeImports = ast.structuredImports.filter(
(entry) =>
!entry.typeOnly && entry.source.endsWith(".wrn") && /(?:^|\/)stores?\//.test(entry.source),
);
const imports = ast.structuredImports
.filter((entry) => !entry.typeOnly)
.filter((entry) => !entry.source.endsWith(".wrn") || /(?:^|\/)stores?\//.test(entry.source))
.map((entry) => entry.raw)
.join("\n");
const importedBindings = storeImports
.flatMap((entry) => [
...(entry.defaultImport ? [entry.defaultImport] : []),
...(entry.namespaceImport ? [entry.namespaceImport] : []),
...entry.namedImports.map((item) => item.local),
])
.filter(safeIdentifier);
return `// generated WRNexusJS browser module for ${ast.name}
${imports}
export const __wrnexusClientFunctions = {
${functions.map((fn) => ` ${functionEntry(ast, fn, functionNames)}`).join(",\n")}
};
export const __wrnexusClientState = ${JSON.stringify(state)};
export const __wrnexusOutputs = ${JSON.stringify(ast.outputs)};
export const __wrnexusImportedBindings = { ${importedBindings.join(", ")} };
export function bindClientScope(context) {
const functions = {};
const scopedContext = { ...context, functions };
for (const [name, handler] of Object.entries(__wrnexusClientFunctions)) {
functions[name] = (...args) => handler(scopedContext, ...args);
}
return functions;
}
`;
}
+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";
}
@@ -0,0 +1,87 @@
import type { PageAst } from "@wrnexus/syntax";
export interface ComponentContractMetadata {
name: string;
kind: PageAst["kind"];
props: Array<{
name: string;
type: string;
required: boolean;
default?: string;
options?: string[];
}>;
outputs: Array<{ name: string; payloadName?: string; payloadType?: string }>;
functions: Array<{
name: string;
runtime: string;
async: boolean;
parameters: Array<{ name: string; type: string; optional: boolean }>;
returnType: string;
}>;
states: Array<{ name: string; runtime: string; type: string; initializer: string }>;
computed: Array<{ name: string; type: string; expression: string }>;
imports: Array<{
source: string;
typeOnly: boolean;
defaultImport?: string;
namedImports: string[];
}>;
}
function unionOptions(type: string | undefined): string[] | undefined {
if (!type || !type.includes("|")) return undefined;
const values = type
.split("|")
.map((part) => part.trim())
.filter((part) => /^(?:"[^"]*"|'[^']*')$/.test(part))
.map((part) => part.slice(1, -1));
return values.length ? values : undefined;
}
export function createComponentContract(ast: PageAst): ComponentContractMetadata {
return {
name: ast.name,
kind: ast.kind,
props: ast.props.map((prop) => ({
name: prop.name,
type: prop.valueType ?? "unknown",
required: prop.required,
...(prop.default !== "undefined" ? { default: prop.default } : {}),
...(unionOptions(prop.valueType) ? { options: unionOptions(prop.valueType) } : {}),
})),
outputs: ast.outputs.map((output) => ({
name: output.name,
...(output.payload
? { payloadName: output.payload.name, payloadType: output.payload.valueType }
: {}),
})),
functions: ast.runtimeFunctions.map((fn) => ({
name: fn.name,
runtime: fn.runtime,
async: fn.async,
parameters: fn.parameters.map((param) => ({
name: param.name,
type: param.valueType ?? "unknown",
optional: param.optional,
})),
returnType: fn.returnType ?? (fn.async ? "Promise<unknown>" : "unknown"),
})),
states: ast.states.map((state) => ({
name: state.name,
runtime: state.runtime,
type: state.valueType ?? "unknown",
initializer: state.expr,
})),
computed: ast.computed.map((entry) => ({
name: entry.name,
type: entry.valueType ?? "unknown",
expression: entry.expr,
})),
imports: ast.structuredImports.map((entry) => ({
source: entry.source,
typeOnly: entry.typeOnly,
...(entry.defaultImport ? { defaultImport: entry.defaultImport } : {}),
namedImports: entry.namedImports.map((named) => named.local),
})),
};
}
+60
View File
@@ -0,0 +1,60 @@
import { existsSync, realpathSync } from "node:fs";
import { dirname, extname, join, resolve } from "node:path";
import type { StructuredImportDecl } from "@wrnexus/syntax";
export type ImportMode = "legacy" | "compatible" | "explicit";
export interface ImportResolverOptions {
appRoot: string;
mode?: ImportMode;
aliases?: Record<string, string>;
}
export interface ResolvedImport {
declaration: StructuredImportDecl;
resolved?: string;
diagnostic?: { code: string; message: string; severity: "error" | "warning" };
}
function candidates(path: string): string[] {
return extname(path)
? [path]
: [
path,
`${path}.wrn`,
`${path}.ts`,
`${path}.d.ts`,
join(path, "index.wrn"),
join(path, "index.ts"),
];
}
export function resolveWrnImport(
declaration: StructuredImportDecl,
importer: string,
options: ImportResolverOptions,
): ResolvedImport {
const source = declaration.source;
if (!source.startsWith(".") && !source.startsWith("@/")) return { declaration, resolved: source };
const aliasRoot = options.aliases?.["@"] ?? "./app";
const base = source.startsWith("@/")
? resolve(options.appRoot, aliasRoot, source.slice(2))
: resolve(dirname(importer), source);
const found = candidates(base).find(existsSync);
if (found) return { declaration, resolved: realpathSync(found) };
const severity = (options.mode ?? "compatible") === "explicit" ? "error" : "warning";
return {
declaration,
diagnostic: {
code: "WRN-IMPORT-NOT-FOUND",
message: `Cannot resolve import '${source}' from ${importer}`,
severity,
},
};
}
export function resolveWrnImports(
declarations: StructuredImportDecl[],
importer: string,
options: ImportResolverOptions,
): ResolvedImport[] {
return declarations.map((declaration) => resolveWrnImport(declaration, importer, options));
}
+13
View File
@@ -27,6 +27,14 @@ export {
ParseError,
} from "@wrnexus/syntax";
export { generate } from "./codegen.ts";
export { generateTargets } from "./targets.ts";
export { generateBrowserModule } from "./client-codegen.ts";
export { generateServerFunctionsModule, rpcManifest } from "./server-codegen.ts";
export { generateDeclarations } from "./type-codegen.ts";
export { generateStoreBrowserModule, generateStoreModule } from "./store-codegen.ts";
export { createComponentContract } from "./component-contract.ts";
export { resolveWrnImport, resolveWrnImports } from "./import-resolver.ts";
export { createWrnSourceMap } from "./source-map.ts";
export { generateNative, NativeCompileError } from "./native-codegen.ts";
export { Lexer, LexError } from "@wrnexus/syntax";
export { eraseFunctionTypes, inferredRuntimeType, runtimeTypeOf } from "@wrnexus/syntax";
@@ -39,6 +47,11 @@ export type {
DataMode,
EffectBlock,
EventDecl,
OutputDecl,
RuntimeFunctionDecl,
StateRuntime,
StoreKind,
StructuredImportDecl,
LoadBlock,
ModeFunctionsBlock,
PageAst,
+68
View File
@@ -0,0 +1,68 @@
import { stripRuntimeFunctionModifiers, type PageAst } from "@wrnexus/syntax";
export interface RpcManifestEntry {
id: string;
component: string;
function: string;
parameters: Array<{ name: string; type: string; optional: boolean }>;
returnType: string;
}
function stableId(value: string): string {
let hash = 0x811c9dc5;
for (let index = 0; index < value.length; index++) {
hash ^= value.charCodeAt(index);
hash = Math.imul(hash, 0x01000193);
}
return `wrn_${(hash >>> 0).toString(36)}`;
}
/**
* Remote exposure is reference based in v0.6. A server function is included in
* the RPC manifest only when browser-capable code calls `server.<name>(...)`.
* Server functions remain available to SSR/server modules without becoming
* remotely callable by default.
*/
export function remotelyReferencedServerFunctions(ast: PageAst): Set<string> {
const browserSources = ast.runtimeFunctions
.filter((fn) => ["legacy", "client", "shared"].includes(fn.runtime))
.map((fn) => fn.body);
for (const [hook, body] of Object.entries(ast.storeLifecycle)) {
if (hook !== "serverInit" && body) browserSources.push(body);
}
const names = new Set<string>();
const call = /\bserver\.([A-Za-z_$][\w$]*)\s*\(/g;
for (const source of browserSources) {
for (const match of source.matchAll(call)) names.add(match[1]!);
}
return names;
}
export function rpcManifest(ast: PageAst): RpcManifestEntry[] {
const exposed = remotelyReferencedServerFunctions(ast);
return ast.runtimeFunctions
.filter((fn) => fn.runtime === "server" && exposed.has(fn.name))
.map((fn) => ({
id: stableId(`${ast.name}:${fn.name}`),
component: ast.name,
function: fn.name,
parameters: fn.parameters.map((param) => ({
name: param.name,
type: param.valueType ?? "unknown",
optional: param.optional,
})),
returnType: fn.returnType ?? (fn.async ? "Promise<unknown>" : "unknown"),
}));
}
export function generateServerFunctionsModule(ast: PageAst): string {
const source = ast.functions
.map((body) => stripRuntimeFunctionModifiers(body, ["legacy", "server", "shared"]))
.filter(Boolean)
.join("\n\n");
const names = ast.runtimeFunctions
.filter((fn) => ["legacy", "server", "shared"].includes(fn.runtime))
.map((fn) => fn.name);
const manifest = rpcManifest(ast);
return `// generated WRNexusJS server module for ${ast.name}\n${source}\n\nexport const __wrnexusServerFunctions = { ${[...new Set(names)].join(", ")} };\nexport const __wrnexusRpcManifest = ${JSON.stringify(manifest, null, 2)};\n`;
}
+23
View File
@@ -0,0 +1,23 @@
export interface WrnSourceMapEntry {
generatedLine: number;
sourceLine: number;
sourceColumn: number;
kind: string;
}
export interface WrnSourceMap {
version: 1;
source: string;
generated: string;
mappings: WrnSourceMapEntry[];
}
export function createWrnSourceMap(source: string, generated: string): WrnSourceMap {
const sourceLines = source.split(/\r?\n/).length;
const generatedLines = generated.split(/\r?\n/).length;
const mappings = Array.from({ length: Math.min(sourceLines, generatedLines) }, (_, index) => ({
generatedLine: index + 1,
sourceLine: index + 1,
sourceColumn: 1,
kind: "line",
}));
return { version: 1, source, generated, mappings };
}
+406
View File
@@ -0,0 +1,406 @@
import { eraseFunctionTypes, type PageAst, type RuntimeFunctionDecl } from "@wrnexus/syntax";
import { generateDeclarations } from "./type-codegen.ts";
import { rpcManifest } from "./server-codegen.ts";
const RESERVED_BINDINGS = new Set([
"await",
"break",
"case",
"catch",
"class",
"const",
"continue",
"debugger",
"default",
"delete",
"do",
"else",
"enum",
"export",
"extends",
"false",
"finally",
"for",
"function",
"if",
"implements",
"import",
"in",
"instanceof",
"interface",
"let",
"new",
"null",
"package",
"private",
"protected",
"public",
"return",
"static",
"super",
"switch",
"this",
"throw",
"true",
"try",
"typeof",
"var",
"void",
"while",
"with",
"yield",
]);
function safeBinding(name: string): boolean {
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && !RESERVED_BINDINGS.has(name);
}
function stateObject(ast: PageAst, runtime: "shared" | "client" | "server"): string {
const entries = ast.states
.filter((state) => state.runtime === runtime)
.map((state) => `${JSON.stringify(state.name)}: (${state.expr})`);
return `{ ${entries.join(", ")} }`;
}
function actionSource(fn: RuntimeFunctionDecl, stateNames: string[], eraseTypes = false): string {
const parameterNames = new Set(fn.parameters.map((param) => param.name));
const params = fn.parameters.map((param) => param.name).join(", ");
const aliases = stateNames.filter((name) => safeBinding(name) && !parameterNames.has(name));
const aliasSource = aliases.length ? `let { ${aliases.join(", ")} } = context.state;` : "";
const runtimeAliases = ["server"]
.filter((name) => !parameterNames.has(name))
.map((name) => `const ${name} = context.${name};`)
.join("\n");
const copyBack = aliases.map((name) => `context.state.${name} = ${name};`).join("\n");
const body = eraseTypes ? eraseFunctionTypes(fn.body) : fn.body;
return `{ runtime: ${JSON.stringify(fn.runtime)}, handler: ${fn.async ? "async " : ""}function(context${params ? `, ${params}` : ""}) { ${runtimeAliases}\n${aliasSource}\ntry { ${body} } finally { ${copyBack} } } }`;
}
function persistedCallback(
source: string | undefined,
functionName: "migrate" | "validate",
): string | undefined {
if (!source?.trim()) return undefined;
const body = eraseFunctionTypes(source);
if (functionName === "migrate") {
return `(value, fromVersion, toVersion) => {\n${body}\nif (typeof migrate === "function") return migrate(value, fromVersion, toVersion);\nreturn value;\n}`;
}
return `(value) => {\n${body}\nif (typeof validate === "function") return validate(value);\nreturn value && typeof value === "object" && !Array.isArray(value) ? value : null;\n}`;
}
function persistenceSource(ast: PageAst): string {
if (!ast.persist) return "undefined";
const migrate = persistedCallback(ast.persist.migrations, "migrate");
const validate = persistedCallback(ast.persist.validation, "validate");
return `{ storage: ${JSON.stringify(ast.persist.storage)}, include: ${JSON.stringify(ast.persist.include)}, version: ${ast.persist.version}${migrate ? `, migrate: ${migrate}` : ""}${validate ? `, validate: ${validate}` : ""} }`;
}
function lifecycleSource(ast: PageAst, stateNames: string[], browser: boolean): string {
return Object.entries(ast.storeLifecycle)
.filter(([name]) => !browser || name !== "serverInit")
.map(([name, body]) => {
const aliases = stateNames.filter(safeBinding);
const aliasSource = aliases.length ? `let { ${aliases.join(", ")} } = context.state;` : "";
const runtimeAliases = browser ? "const server = context.server;" : "";
const copyBack = aliases.map((state) => `context.state.${state} = ${state};`).join("\n");
const emittedBody = browser ? eraseFunctionTypes(body) : body;
return `${name}: async (context) => { ${runtimeAliases} ${aliasSource} try { ${emittedBody} } finally { ${copyBack} } }`;
})
.join(",\n");
}
export function generateStoreModule(ast: PageAst): string {
if (ast.kind !== "global-store" && ast.kind !== "page-store") {
throw new Error("generateStoreModule requires a store AST");
}
const stateNames = ast.states.map((state) => state.name);
const safeStateNames = stateNames.filter(safeBinding);
const computed = ast.computed
.map(
(entry) =>
`${JSON.stringify(entry.name)}: (state) => { ${safeStateNames.length ? `const { ${safeStateNames.join(", ")} } = state;` : ""} return (${entry.expr}); }`,
)
.join(",\n");
const actionGroups = new Map<string, RuntimeFunctionDecl[]>();
for (const fn of ast.runtimeFunctions) {
const group = actionGroups.get(fn.name) ?? [];
group.push(fn);
actionGroups.set(fn.name, group);
}
const actions = Array.from(
actionGroups,
([name, functions]) =>
`${JSON.stringify(name)}: [${functions.map((fn) => actionSource(fn, stateNames)).join(", ")}]`,
).join(",\n");
const persistence = persistenceSource(ast);
const lifecycle = lifecycleSource(ast, stateNames, false);
const manifest = rpcManifest(ast);
const remoteFunctions = manifest.map((entry) => entry.function);
const rpcWrappers = remoteFunctions
.map(
(name) => `${JSON.stringify(name)}: async (...received) => {
const rpcContext = received.pop();
if (!rpcContext || !rpcContext.request) throw new Error("WRN-RPC-CONTEXT: request context is required");
let container = __wrnexusRpcContainers.get(rpcContext.request);
if (!container) {
const url = new URL(rpcContext.request.url);
container = createRequestStoreContainer(rpcContext.request, url.pathname + url.search);
__wrnexusRpcContainers.set(rpcContext.request, container);
}
const store = await container.use(${ast.name});
const action = store.actions[${JSON.stringify(name)}];
if (typeof action !== "function") throw new Error(${JSON.stringify(`WRN-RPC-FUNCTION: store action '${name}' is unavailable on the server`)});
return action(...received);
}`,
)
.join(",\n");
return `${ast.imports.join("\n")}\nimport { defineStore } from "@wrnexus/store";\nimport { createRequestStoreContainer } from "@wrnexus/store/server";\n\n${ast.types.join("\n\n")}\n\nexport const ${ast.name} = defineStore({\n name: ${JSON.stringify(ast.name)},\n kind: ${JSON.stringify(ast.storeKind)},\n createSharedState: () => (${stateObject(ast, "shared")}),\n createClientState: () => (${stateObject(ast, "client")}),\n createServerState: () => (${stateObject(ast, "server")}),\n computed: { ${computed} },\n actions: { ${actions} },\n persist: ${persistence},\n lifecycle: { ${lifecycle} },\n});\n\nexport default ${ast.name};\n\nconst __wrnexusRpcContainers = new WeakMap();\nexport const __wrnexusServerFunctions = {\n${rpcWrappers}\n};\nexport const __wrnexusRpcManifest = ${JSON.stringify(manifest, null, 2)};\n\n${generateDeclarations(ast)}\n`;
}
/** Standalone browser artifact for an imported `.wrn` store. */
export function generateStoreBrowserModule(ast: PageAst): string {
if (ast.kind !== "global-store" && ast.kind !== "page-store") {
throw new Error("generateStoreBrowserModule requires a store AST");
}
const browserStates = ast.states.filter((state) => state.runtime !== "server");
const stateNames = browserStates.map((state) => state.name);
const safeStateNames = stateNames.filter(safeBinding);
const initialState = `{ ${browserStates.map((state) => `${JSON.stringify(state.name)}: (${state.expr})`).join(", ")} }`;
const computed = ast.computed
.map(
(entry) =>
`${JSON.stringify(entry.name)}: (state) => { ${safeStateNames.length ? `const { ${safeStateNames.join(", ")} } = state;` : ""} return (${entry.expr}); }`,
)
.join(",\n");
const groups = new Map<string, RuntimeFunctionDecl[]>();
for (const fn of ast.runtimeFunctions.filter((entry) =>
["client", "shared", "legacy"].includes(entry.runtime),
)) {
const group = groups.get(fn.name) ?? [];
group.push(fn);
groups.set(fn.name, group);
}
const actions = Array.from(
groups,
([name, functions]) =>
`${JSON.stringify(name)}: [${functions.map((fn) => actionSource(fn, stateNames, true)).join(", ")}]`,
).join(",\n");
const persistence = persistenceSource(ast);
const lifecycleEntries = lifecycleSource(ast, stateNames, true);
return `// generated WRNexusJS browser store module for ${ast.name}
const __root = globalThis;
const __registry = __root.__wrnexusStoreRegistry || (__root.__wrnexusStoreRegistry = new Map());
const __hydrationNode = typeof document !== "undefined" ? document.querySelector("script[data-wrnexus-store-hydration]") : null;
let __hydration = {};
try { __hydration = __hydrationNode ? JSON.parse(__hydrationNode.textContent || "{}") : {}; } catch (_) {}
function __clone(value) { try { return structuredClone(value); } catch (_) { return JSON.parse(JSON.stringify(value)); } }
function __storage(kind) { if (typeof window === "undefined") return null; return kind === "local" ? window.localStorage : kind === "session" ? window.sessionStorage : null; }
function __diagnostic(code, message, details) {
const detail = { code, message, store: ${JSON.stringify(ast.name)}, details };
try { __root.dispatchEvent && __root.dispatchEvent(new CustomEvent("wrnexus:store-diagnostic", { detail })); } catch (_) {}
if (typeof console !== "undefined" && console.warn) console.warn("[wrnexus:store] " + code + ": " + message, details || "");
}
function __csrfToken() {
if (typeof document === "undefined") return undefined;
const match = /(?:^|;\\s*)wrnexus_csrf=([^;]+)/.exec(document.cookie || "");
return match ? decodeURIComponent(match[1]) : undefined;
}
async function __callServerFunction(storeName, functionName, args, options) {
options = options || {};
const csrf = options.csrfToken || __csrfToken();
const traceId = options.traceId || (globalThis.crypto && crypto.randomUUID ? crypto.randomUUID() : String(Date.now()));
const response = await fetch(options.endpoint || "/__wrnexus/rpc", {
method: "POST",
credentials: "same-origin",
signal: options.signal,
headers: Object.assign({ "content-type": "application/json", "x-request-id": traceId }, csrf ? { "x-wrnexus-csrf": csrf } : {}, options.headers || {}),
body: JSON.stringify({ component: storeName, function: functionName, args: args }),
});
const payload = await response.json().catch(function () { return null; });
if (!response.ok || !payload || !payload.ok) {
const error = new Error(payload && payload.error && payload.error.message || "Server call failed (" + response.status + ")");
error.code = payload && payload.error && payload.error.code || "WRN-RPC-FAILED";
error.status = response.status;
error.details = payload && payload.error && payload.error.details;
error.traceId = payload && payload.error && payload.error.traceId || traceId;
throw error;
}
return payload.value;
}
function __compatible(expected, value) {
if (expected === null || value === null) return expected === value || expected === null;
if (Array.isArray(expected)) return Array.isArray(value);
return typeof expected === typeof value;
}
function __create(definition) {
const routeId = typeof location !== "undefined" ? location.pathname + location.search : "default";
const key = definition.kind === "page" ? definition.name + "@" + routeId : definition.name;
if (__registry.has(key)) return __registry.get(key);
let currentDefinition = definition;
const listeners = new Set();
const initial = currentDefinition.createState();
let restored = null;
if (currentDefinition.persist) {
try {
const storage = __storage(currentDefinition.persist.storage);
const rawValue = storage && storage.getItem("wrnexus:store:" + currentDefinition.name);
const parsed = rawValue ? JSON.parse(rawValue) : null;
if (parsed) {
let candidate = parsed.state;
const fromVersion = Number(parsed.version || 0);
if (fromVersion !== currentDefinition.persist.version) {
if (typeof currentDefinition.persist.migrate === "function") candidate = currentDefinition.persist.migrate(candidate, fromVersion, currentDefinition.persist.version);
else { __diagnostic("WRN-PERSIST-VERSION", "Persisted state version cannot be restored without a migration.", { fromVersion, toVersion: currentDefinition.persist.version }); candidate = null; }
}
if (candidate && typeof currentDefinition.persist.validate === "function") candidate = currentDefinition.persist.validate(candidate);
if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
restored = {};
currentDefinition.persist.include.forEach(function (name) { if (Object.prototype.hasOwnProperty.call(candidate, name)) restored[name] = candidate[name]; });
} else if (candidate != null) {
__diagnostic("WRN-PERSIST-INVALID", "Persisted state failed validation and was reset.", candidate);
}
}
} catch (error) { __diagnostic("WRN-PERSIST-RESTORE", "Persisted state could not be restored and was reset.", error); }
}
const raw = Object.assign({}, initial, restored || {}, __hydration[currentDefinition.name] || {});
let mutable = false;
let actionName = "direct";
function persistState() {
if (!currentDefinition.persist) return;
try {
const picked = {};
currentDefinition.persist.include.forEach(function (name) { picked[name] = raw[name]; });
const storage = __storage(currentDefinition.persist.storage);
if (storage) storage.setItem("wrnexus:store:" + currentDefinition.name, JSON.stringify({ version: currentDefinition.persist.version, state: picked }));
} catch (error) { __diagnostic("WRN-PERSIST-WRITE", "Persisted state could not be written.", error); }
}
const state = new Proxy(raw, {
set(target, property, value) {
if (!mutable) throw new TypeError("WRN-STORE-READONLY: " + currentDefinition.name + "." + String(property) + " must be changed by a store action.");
if (Object.is(target[property], value)) return true;
target[property] = value;
persistState();
listeners.forEach(function (listener) { listener(instance.snapshot(), { store: currentDefinition.name, action: actionName, changed: [String(property)] }); });
return true;
},
deleteProperty(target, property) {
if (!mutable) throw new TypeError("WRN-STORE-READONLY: store state is readonly outside actions");
return Reflect.deleteProperty(target, property);
},
});
const actions = {};
const server = new Proxy({}, { get: function (_target, property) { return function () { return __callServerFunction(currentDefinition.name, String(property), Array.prototype.slice.call(arguments)); }; } });
function installActions() {
Object.keys(actions).forEach(function (name) { delete actions[name]; });
Object.entries(currentDefinition.actions || {}).forEach(function (pair) {
const name = pair[0], candidates = pair[1];
const selected = candidates.find(function (entry) { return entry.runtime === "client"; }) || candidates.find(function (entry) { return entry.runtime === "shared"; }) || candidates.find(function (entry) { return entry.runtime === "legacy"; });
if (!selected) return;
actions[name] = async function () {
const args = Array.prototype.slice.call(arguments);
const previousMutable = mutable, previousAction = actionName;
mutable = true; actionName = name;
try { return await selected.handler({ state, snapshot: function () { return __clone(state); }, reset: function () { return instance.reset(); }, runtime: "client", routeId, server }, ...args); }
finally { mutable = previousMutable; actionName = previousAction; }
};
});
}
installActions();
const core = {
name: currentDefinition.name,
kind: currentDefinition.kind,
state,
actions,
whenReady: Promise.resolve(),
reset() {
mutable = true; actionName = "$reset";
try {
const next = currentDefinition.createState();
Object.keys(raw).forEach(function (name) { if (!(name in next)) delete raw[name]; });
Object.assign(raw, next); persistState();
listeners.forEach(function (listener) { listener(instance.snapshot(), { store: currentDefinition.name, action: "$reset", changed: Object.keys(next) }); });
} finally { mutable = false; actionName = "direct"; }
},
snapshot() { return Object.freeze(__clone(raw)); },
subscribe(listener) { listeners.add(listener); return function () { listeners.delete(listener); }; },
async dispose() {
mutable = true; actionName = "$dispose";
try { await currentDefinition.lifecycle && currentDefinition.lifecycle.dispose && currentDefinition.lifecycle.dispose({ state, runtime: "client", routeId, server }); }
finally { mutable = false; actionName = "direct"; listeners.clear(); __registry.delete(key); }
},
async __hotUpdate(nextDefinition) {
const previous = __clone(raw);
const nextShape = nextDefinition.createState();
const preserved = [], reset = [], added = [], removed = [];
Object.keys(previous).forEach(function (name) {
if (!(name in nextShape)) { removed.push(name); return; }
if (__compatible(nextShape[name], previous[name])) { nextShape[name] = previous[name]; preserved.push(name); }
else reset.push(name);
});
Object.keys(nextShape).forEach(function (name) { if (!(name in previous)) added.push(name); });
currentDefinition = nextDefinition;
mutable = true; actionName = "$hmr";
try {
Object.keys(raw).forEach(function (name) { delete raw[name]; });
Object.assign(raw, nextShape);
installActions(); persistState();
} finally { mutable = false; actionName = "direct"; }
const result = { store: currentDefinition.name, preserved, reset, added, removed };
listeners.forEach(function (listener) { listener(instance.snapshot(), { store: currentDefinition.name, action: "$hmr", changed: added.concat(reset, removed) }); });
try { __root.dispatchEvent && __root.dispatchEvent(new CustomEvent("wrnexus:store-hmr", { detail: result })); } catch (_) {}
return result;
},
};
const instance = new Proxy(core, {
get(target, property, receiver) {
if (Reflect.has(target, property)) return Reflect.get(target, property, receiver);
if (property in actions) return actions[property];
if (property in (currentDefinition.computed || {})) return currentDefinition.computed[property](state);
return state[property];
},
set() { throw new TypeError("WRN-STORE-READONLY: store state is readonly outside actions"); },
});
__registry.set(key, instance);
const hydrationSource = __hydration[currentDefinition.name];
const init = async function () {
const run = async function (name, hook) {
if (!hook) return;
mutable = true; actionName = name;
try { await hook({ state, runtime: "client", routeId, server }); }
finally { mutable = false; actionName = "direct"; }
};
await run("$clientInit", currentDefinition.lifecycle && currentDefinition.lifecycle.clientInit);
if (hydrationSource) await run("$hydrate", currentDefinition.lifecycle && currentDefinition.lifecycle.hydrate);
};
core.whenReady = init();
return instance;
}
if (!__root.__wrnexusApplyStoreHotUpdate) {
__root.__wrnexusApplyStoreHotUpdate = async function (name, definition) {
const results = [];
for (const item of Array.from(__registry.values())) if (item.name === name && typeof item.__hotUpdate === "function") results.push(await item.__hotUpdate(definition));
return results;
};
}
if (!__root.__wrnexusStoreContainer) {
__root.__wrnexusStoreContainer = {
async disposePageStores() { for (const item of Array.from(__registry.values())) if (item.kind === "page") await item.dispose(); },
async hotUpdate(name, definition) { return __root.__wrnexusApplyStoreHotUpdate(name, definition); },
inspect() { return Array.from(__registry.values()).map(function (item) { return { name: item.name, kind: item.kind, state: item.snapshot() }; }); },
};
}
export const ${ast.name}Definition = {
name: ${JSON.stringify(ast.name)},
kind: ${JSON.stringify(ast.storeKind)},
createState: () => (${initialState}),
computed: { ${computed} },
actions: { ${actions} },
persist: ${persistence},
lifecycle: { ${lifecycleEntries} },
};
export const ${ast.name} = __create(${ast.name}Definition);
export default ${ast.name};
`;
}
+30
View File
@@ -0,0 +1,30 @@
import type { PageAst } from "@wrnexus/syntax";
import { createComponentContract } from "./component-contract.ts";
import { generateBrowserModule } from "./client-codegen.ts";
import { generateServerFunctionsModule, rpcManifest } from "./server-codegen.ts";
import { generateDeclarations } from "./type-codegen.ts";
import { generateStoreBrowserModule, generateStoreModule } from "./store-codegen.ts";
export interface CompileTargets {
server: string;
browser: string;
declarations: string;
contract: ReturnType<typeof createComponentContract>;
rpc: ReturnType<typeof rpcManifest>;
}
export function generateTargets(ast: PageAst): CompileTargets {
return {
server:
ast.kind === "global-store" || ast.kind === "page-store"
? generateStoreModule(ast)
: generateServerFunctionsModule(ast),
browser:
ast.kind === "global-store" || ast.kind === "page-store"
? generateStoreBrowserModule(ast)
: generateBrowserModule(ast),
declarations: generateDeclarations(ast),
contract: createComponentContract(ast),
rpc: rpcManifest(ast),
};
}
+64
View File
@@ -0,0 +1,64 @@
import type { PageAst } from "@wrnexus/syntax";
function member(name: string): string {
return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
}
function params(astParams: PageAst["runtimeFunctions"][number]["parameters"]): string {
return astParams
.map(
(param) =>
`${member(param.name)}${param.optional ? "?" : ""}: ${param.valueType ?? "unknown"}`,
)
.join(", ");
}
export function generateDeclarations(ast: PageAst): string {
const inline = ast.types
.map((body) => body.trim())
.filter(Boolean)
.join("\n\n");
if (ast.kind === "global-store" || ast.kind === "page-store") {
const state = ast.states
.filter((entry) => entry.runtime !== "server")
.map((entry) => ` readonly ${member(entry.name)}: ${entry.valueType ?? "unknown"};`)
.join("\n");
const computed = ast.computed
.map((entry) => ` readonly ${member(entry.name)}: ${entry.valueType ?? "unknown"};`)
.join("\n");
const actions = ast.runtimeFunctions
.filter((fn) => fn.runtime !== "server")
.map(
(fn) =>
` ${member(fn.name)}(${params(fn.parameters)}): ${fn.returnType ?? (fn.async ? "Promise<unknown>" : "unknown")};`,
)
.join("\n");
return `${inline ? `${inline}\n\n` : ""}export interface ${ast.name}State {\n${state}\n}\n\nexport interface ${ast.name}Computed {\n${computed}\n}\n\nexport interface ${ast.name}Actions {\n${actions}\n}\n\nexport interface ${ast.name}Instance extends ${ast.name}State, ${ast.name}Computed, ${ast.name}Actions {\n reset(): void;\n snapshot(): Readonly<${ast.name}State>;\n}\n\ndeclare const store: ${ast.name}Instance;\nexport default store;\n`;
}
const props = ast.props
.map(
(prop) =>
` readonly ${member(prop.name)}${prop.required ? "" : "?"}: ${prop.valueType ?? "unknown"};`,
)
.join("\n");
const outputs = ast.outputs
.map(
(output) =>
` ${member(output.name)}(${output.payload ? `${member(output.payload.name)}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}): void;`,
)
.join("\n");
const clientFunctions = ast.runtimeFunctions
.filter((fn) => fn.runtime === "client" || fn.runtime === "shared" || fn.runtime === "legacy")
.map(
(fn) =>
` ${member(fn.name)}(${params(fn.parameters)}): ${fn.returnType ?? (fn.async ? "Promise<unknown>" : "unknown")};`,
)
.join("\n");
const serverFunctions = ast.runtimeFunctions
.filter((fn) => fn.runtime === "server")
.map(
(fn) =>
` ${member(fn.name)}(${params(fn.parameters)}): Promise<Awaited<${fn.returnType ?? "unknown"}>>;`,
)
.join("\n");
return `${inline ? `${inline}\n\n` : ""}export interface ${ast.name}Props {\n${props}\n}\n\nexport interface ${ast.name}Outputs {\n${outputs}\n}\n\nexport interface ${ast.name}ClientFunctions {\n${clientFunctions}\n}\n\nexport interface ${ast.name}ServerCalls {\n${serverFunctions}\n}\n`;
}
+147
View File
@@ -0,0 +1,147 @@
import { expect, test } from "bun:test";
import { generateTargets, parse } from "../src/index.ts";
const ast = parse(`component ConfirmDialog {
props { title: string open: boolean = false }
state { loading: boolean = false }
server state { internalId: string = "secret" }
outputs { confirm(payload: string) close() }
functions {
client async function confirm(value: string): Promise<void> {
const saved = await server.confirm(value)
output.confirm(saved)
}
server async function confirm(value: string): Promise<string> { return value }
shared function normalize(value: string): string { return value.trim() }
}
view { <button>{title}</button> }
}`);
test("emits separate browser and server function targets", () => {
const targets = generateTargets(ast);
expect(targets.browser).toContain('"confirm": async function');
expect(targets.browser).toContain('"normalize": function');
expect(targets.server).toContain("async function confirm");
expect(targets.rpc).toEqual([
expect.objectContaining({ component: "ConfirmDialog", function: "confirm" }),
]);
expect(targets.declarations).toContain("interface ConfirmDialogOutputs");
expect(targets.declarations).toContain("interface ConfirmDialogServerCalls");
});
test("component contracts retain declared union options", () => {
const targets = generateTargets(
parse(`component SizeBox {
props { size: "small" | "medium" | "large" = "medium" }
view { <div></div> }
}`),
);
expect(targets.contract.props[0]?.options).toEqual(["small", "medium", "large"]);
});
test("generates a standalone browser module for imported stores", () => {
const targets = generateTargets(
parse(`page store SearchStore {
state { query: string = "" }
client state { focused: boolean = false }
server state { secret: string = "hidden" }
computed { empty: boolean = query.length === 0 }
functions { client function setQuery(value: string): void { query = value } }
persist { storage = "session" include = ["query"] version = 1 }
}`),
);
expect(targets.browser).toContain("__wrnexusStoreRegistry");
expect(targets.browser).toContain('name: "SearchStore"');
expect(targets.browser).toContain('"query"');
expect(targets.browser).not.toContain('"secret": ("hidden")');
expect(targets.declarations).toContain("interface SearchStoreInstance");
});
test("browser codegen avoids reserved prop bindings and parameter collisions", () => {
const targets = generateTargets(
parse(`component ReservedBindings {
props { class: string = "" output: string = "" }
state { value: string = "" }
outputs { change(payload: { value: string }) }
functions {
client function update(output: string): void {
value = output
}
client function notify(): void {
output.change({ value: value })
}
}
view { <button>{class}</button> }
}`),
);
expect(targets.browser).not.toContain("const { class }");
expect(targets.browser).not.toContain("const output = context.output;\n const output");
expect(() => new Function(targets.browser.replace(/^export\s+/gm, ""))).not.toThrow();
});
test("RPC manifests expose only server functions referenced through server.name", () => {
const targets = generateTargets(
parse(`component SecureActions {
functions {
client async function saveClient(): Promise<void> { await server.save("ok") }
server async function save(value: string): Promise<string> { return value }
server function internalSecret(): string { return "secret" }
}
view { <button @click='saveClient()'>Save</button> }
}`),
);
expect(targets.rpc.map((entry) => entry.function)).toEqual(["save"]);
expect(targets.server).toContain("internalSecret");
});
test("generated browser stores bind typed RPC, persistence validation, and HMR", () => {
const targets = generateTargets(
parse(`global store UserStore {
state { user: string | null = null count: number = 0 }
functions {
client async function refresh(): Promise<void> { user = await server.loadCurrentUser() }
server async function loadCurrentUser(): Promise<string | null> { return "Ajay" }
server function internalOnly(): string { return "secret" }
}
persist {
storage = "local"
include = ["count"]
version = 2
migrations { function migrate(value, fromVersion, toVersion) { return value } }
validate { function validate(value) { return value && typeof value === "object" ? value : null } }
}
}`),
);
expect(targets.browser).toContain("const server = context.server");
expect(targets.browser).toContain("currentDefinition.persist.migrate");
expect(targets.browser).toContain("currentDefinition.persist.validate");
expect(targets.browser).toContain("__wrnexusApplyStoreHotUpdate");
expect(targets.rpc.map((entry) => entry.function)).toEqual(["loadCurrentUser"]);
});
test("browser codegen binds peer client functions through the scoped function table", () => {
const targets = generateTargets(
parse(`component PeerCalls {
state { value: number = 0 }
functions {
client function increment(): void { value += 1 }
client function run(): void { increment() }
}
view { <button @click='run()'>{value}</button> }
}`),
);
expect(targets.browser).toContain("context.functions");
const executable = new Function(
`${targets.browser.replace(/^export\s+/gm, "")}\nreturn { bindClientScope };`,
)() as { bindClientScope: (context: Record<string, unknown>) => Record<string, () => void> };
const state = { value: 0 };
const functions = executable.bindClientScope({
state,
props: {},
output: {},
server: {},
refs: {},
});
functions.run?.();
expect(state.value).toBe(1);
});