feat: add typed WRN declarations
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user