feat: add typed WRN declarations

This commit is contained in:
2026-07-19 18:44:27 +05:30
parent 0d3ec79ee4
commit 94ae40d8bd
81 changed files with 942 additions and 241 deletions
+58 -6
View File
@@ -4,7 +4,9 @@
* Grammar (subset of the vision, but real):
*
* page <Name> {
* state <ident> = <expr> // zero or more
* types { <TypeScript declarations> }
* props { <ident>: <type> [= <expr>] } // no default means required
* state <ident>: <type> = <expr> // type annotation is optional
* view { <html> } // plain HTML (see parseHtmlView)
* seo { title = "Home" description = "..." }
* ssr { api <name> <METHOD> <path> { <render js> } functions { <raw js> } }
@@ -21,9 +23,12 @@
*/
import { Lexer, LexError, type Token } from "./tokenizer.ts";
import { validateTypedInitializer } from "./types.ts";
export interface StateDecl {
name: string;
/** Explicit TypeScript-style type annotation, when supplied. */
valueType?: string;
/** Raw JS initializer expression, e.g. `0` or `'x'`. */
expr: string;
}
@@ -131,6 +136,10 @@ export interface RealtimeBlock {
export interface PropDecl {
name: string;
/** Explicit TypeScript-style type annotation, when supplied. */
valueType?: string;
/** Props without a default are required. */
required: boolean;
/** Raw JS default expression, e.g. `0` or `'Count'`. Its type drives coercion. */
default: string;
}
@@ -147,6 +156,8 @@ export interface PageAst {
layout?: string;
/** Declared component props (empty for pages). */
props: PropDecl[];
/** Raw declarations from `types { ... }`, emitted as TypeScript. */
types: string[];
states: StateDecl[];
seo: SeoBlock;
view: ViewNode[];
@@ -218,6 +229,7 @@ export function parse(source: string): PageAst {
let layout: string | undefined;
const props: PropDecl[] = [];
const types: string[] = [];
const states: StateDecl[] = [];
const seo: SeoBlock = {};
const view: ViewNode[] = [];
@@ -245,7 +257,7 @@ export function parse(source: string): PageAst {
break;
}
case "props": {
// props { name = <default> ... } — one declaration per line.
// props { name: Type = <default> } — omit the default for required props.
lx.next();
expect("lbrace");
while (lx.peek().type !== "rbrace") {
@@ -255,8 +267,19 @@ export function parse(source: string): PageAst {
throw new ParseError(`Expected a prop name at offset ${t.pos}`);
}
const pName = expect("ident").value;
expect("eq");
props.push({ name: pName, default: lx.readToLineEnd() });
let valueType: string | undefined;
let hasDefault = false;
if (lx.peek().type === "colon") {
lx.next();
const annotation = lx.readTypeAnnotation();
valueType = annotation.type;
hasDefault = annotation.hasDefault;
} else {
expect("eq");
hasDefault = true;
}
const defaultValue = hasDefault ? lx.readToLineEnd() : "undefined";
props.push({ name: pName, valueType, required: !hasDefault, default: defaultValue });
}
expect("rbrace");
break;
@@ -264,8 +287,23 @@ export function parse(source: string): PageAst {
case "state": {
lx.next();
const sName = expect("ident").value;
expect("eq");
states.push({ name: sName, expr: lx.readToLineEnd() });
let valueType: string | undefined;
if (lx.peek().type === "colon") {
lx.next();
const annotation = lx.readTypeAnnotation();
valueType = annotation.type;
if (!annotation.hasDefault) {
throw new ParseError(`State '${sName}' requires an initializer`);
}
} else {
expect("eq");
}
states.push({ name: sName, valueType, expr: lx.readToLineEnd() });
break;
}
case "types": {
lx.next();
types.push(lx.readBalancedBraces());
break;
}
case "view": {
@@ -421,12 +459,26 @@ export function parse(source: string): PageAst {
}
}
for (const prop of props) {
const problem = validateTypedInitializer(`Prop '${prop.name}'`, prop.valueType, prop.default);
if (problem) throw new ParseError(problem);
}
for (const state of states) {
const problem = validateTypedInitializer(
`State '${state.name}'`,
state.valueType,
state.expr,
);
if (problem) throw new ParseError(problem);
}
return {
type: "page",
kind,
name,
layout,
props,
types,
states,
seo,
view,