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
+74
View File
@@ -66,6 +66,80 @@ test("parses a component with props and state", () => {
expect(ast.states.map((s) => s.name)).toEqual(["count"]);
});
test("typed props, required props, state, custom types, and function parameters compile", () => {
const source = `component TypedPicker {
types {
interface DateOption { value: string; label: string }
type ChangeHandler = (value: string) => void
}
props {
value: string = ""
options: DateOption[] = []
required: boolean = false
onChange: ChangeHandler
}
state open: boolean = false
functions {
function choose(option: DateOption, index: number): void {
open = false
onChange(option.value)
}
}
view { <button @click="choose(options[0], 0)">{value}</button> }
}`;
const ast = parse(source);
expect(ast.props.map(({ name, valueType, required }) => ({ name, valueType, required }))).toEqual(
[
{ name: "value", valueType: "string", required: false },
{ name: "options", valueType: "DateOption[]", required: false },
{ name: "required", valueType: "boolean", required: false },
{ name: "onChange", valueType: "ChangeHandler", required: true },
],
);
expect(ast.states[0]).toMatchObject({ name: "open", valueType: "boolean", expr: "false" });
const output = generate(ast);
expect(output).toContain("interface DateOption");
expect(output).toContain("export interface TypedPickerProps");
expect(output).toContain('"onChange": ChangeHandler;');
expect(output).toContain("let open: boolean = (false)");
expect(output).toContain("function choose(option: DateOption, index: number): void");
const behavior = extractCompiledBehavior(output);
expect(behavior.functions).toContain("function choose(option, index)");
expect(behavior.functions).not.toContain("option: DateOption");
});
test("typed declarations reject obvious initializer mismatches", () => {
expect(() =>
parse(`component Invalid {
props {
required: boolean = "yes"
}
view { <div></div> }
}`),
).toThrow("Prop 'required' is declared as boolean, but its initializer is string");
expect(() => parse(`page Invalid { state count: number = false\n view { <p></p> } }`)).toThrow(
"State 'count' is declared as number, but its initializer is boolean",
);
});
test("typed props enforce required values and runtime-compatible input", async () => {
const component = await compileAndImport(`component TypedInput {
props {
label: string
count: number = 0
enabled: boolean = false
}
view { <span>{label}:{count}:{enabled}</span> }
}`);
const render = component.render as (props?: Record<string, unknown>) => string;
expect(() => render()).toThrow("TypedInput requires prop 'label' (string)");
expect(render({ label: "Total", count: "4", enabled: "true" })).toContain(
"<span>Total:4:true</span>",
);
expect(() => render({ label: "Total", count: "many" })).toThrow("Expected a finite number prop");
expect(() => render({ label: "Total", enabled: "sometimes" })).toThrow("Expected a boolean prop");
});
test("HTML view: void elements, boolean attrs, comments, lone <", () => {
const ast = parse(
`component T {\n view {\n <input type="text" disabled>\n <br/>\n <!-- comment -->\n <p>a < b</p>\n }\n}`,