feat: add typed WRN declarations
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
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";
|
||||
|
||||
interface RenderBinding {
|
||||
method: string;
|
||||
@@ -667,6 +668,12 @@ export function generate(ast: PageAst): string {
|
||||
.join("\n\n");
|
||||
const apiBindings = apiBindingMap(ast, helpers);
|
||||
|
||||
const typeSource = ast.types
|
||||
.map((body) => body.trim())
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
if (typeSource) out.push(typeSource);
|
||||
|
||||
if (helpers) {
|
||||
out.push(`// --- .wrn functions ---\n${helpers}`);
|
||||
}
|
||||
@@ -709,6 +716,12 @@ export function generate(ast: PageAst): string {
|
||||
`${JSON.stringify(state.name)}: (() => { try { return (${state.expr}); } catch { return undefined; } })()`,
|
||||
)
|
||||
.join(", ");
|
||||
const stateType =
|
||||
ast.states.length > 0
|
||||
? `{ ${ast.states
|
||||
.map((state) => `${JSON.stringify(state.name)}: ${state.valueType ?? "unknown"}`)
|
||||
.join("; ")} }`
|
||||
: "Record<string, never>";
|
||||
loops.forEach((code, idx) => {
|
||||
body = body.replace(`\x00WRNEACH${idx}\x00`, () => code);
|
||||
});
|
||||
@@ -738,7 +751,7 @@ export function generate(ast: PageAst): string {
|
||||
out.push(
|
||||
`export default async function ${ast.name}(ctx: any) {
|
||||
${decls}
|
||||
const __state = { ${dynamicStateScope} };
|
||||
const __state: ${stateType} = { ${dynamicStateScope} };
|
||||
|
||||
const __scopeValue = Object.entries(__state)
|
||||
.map(([key, value]) => {
|
||||
@@ -766,7 +779,7 @@ export function generate(ast: PageAst): string {
|
||||
} else {
|
||||
out.push(
|
||||
`export default function ${ast.name}(ctx: any) {
|
||||
const __state = { ${dynamicStateScope} };
|
||||
const __state: ${stateType} = { ${dynamicStateScope} };
|
||||
|
||||
const __scopeValue = Object.entries(__state)
|
||||
.map(([key, value]) => {
|
||||
@@ -932,10 +945,12 @@ function escLit(s: string): string {
|
||||
}
|
||||
|
||||
function componentBehavior(ast: PageAst): ComponentBehavior | null {
|
||||
const functions = ast.functions
|
||||
.map((body) => body.trim())
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
const functions = eraseFunctionTypes(
|
||||
ast.functions
|
||||
.map((body) => body.trim())
|
||||
.filter(Boolean)
|
||||
.join("\n\n"),
|
||||
);
|
||||
|
||||
const lifecycle = {
|
||||
...(ast.lifecycle.mount?.trim() ? { mount: ast.lifecycle.mount.trim() } : {}),
|
||||
@@ -1289,6 +1304,8 @@ function generateComponent(ast: PageAst): string {
|
||||
{
|
||||
name: "content",
|
||||
default: '""',
|
||||
valueType: "string",
|
||||
required: false,
|
||||
},
|
||||
...ast.props,
|
||||
]
|
||||
@@ -1339,12 +1356,21 @@ function generateComponent(ast: PageAst): string {
|
||||
|
||||
const decls: string[] = [];
|
||||
for (const prop of effectiveProps) {
|
||||
if (prop.required) {
|
||||
decls.push(
|
||||
` if (__p[${JSON.stringify(prop.name)}] === undefined) throw new TypeError(${JSON.stringify(
|
||||
`${ast.name} requires prop '${prop.name}' (${prop.valueType ?? "unknown"})`,
|
||||
)});`,
|
||||
);
|
||||
}
|
||||
decls.push(
|
||||
` const ${nameRefs.get(prop.name)} = __coerce(__p[${JSON.stringify(prop.name)}], (${resolveExpr(prop.default)}));`,
|
||||
` const ${nameRefs.get(prop.name)}: ${prop.valueType ?? "any"} = __coerce(__p[${JSON.stringify(prop.name)}], (${resolveExpr(prop.default)}), ${JSON.stringify(runtimeTypeOf(prop.valueType))});`,
|
||||
);
|
||||
}
|
||||
for (const state of ast.states) {
|
||||
decls.push(` let ${nameRefs.get(state.name)} = (${resolveExpr(state.expr)});`);
|
||||
decls.push(
|
||||
` let ${nameRefs.get(state.name)}${state.valueType ? `: ${state.valueType}` : ""} = (${resolveExpr(state.expr)});`,
|
||||
);
|
||||
}
|
||||
|
||||
const returnExpr = needsScope
|
||||
@@ -1370,20 +1396,41 @@ function generateComponent(ast: PageAst): string {
|
||||
out.push(`export const __wrnexusBehavior = ${JSON.stringify(behavior, null, 2)};`);
|
||||
}
|
||||
|
||||
out.push(`function __coerce(v: any, def: any): any {
|
||||
const typeSource = ast.types
|
||||
.map((body) => body.trim())
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
if (typeSource) out.push(typeSource);
|
||||
|
||||
if (effectiveProps.length > 0) {
|
||||
out.push(
|
||||
`export interface ${ast.name}Props {\n${effectiveProps
|
||||
.map(
|
||||
(prop) =>
|
||||
` ${JSON.stringify(prop.name)}${prop.required ? "" : "?"}: ${prop.valueType ?? "unknown"};`,
|
||||
)
|
||||
.join("\n")}\n}`,
|
||||
);
|
||||
}
|
||||
|
||||
out.push(`function __coerce(v: any, def: any, declared: string = "unknown"): any {
|
||||
if (v === undefined || v === null) {
|
||||
return def;
|
||||
}
|
||||
|
||||
if (typeof def === "number") {
|
||||
return Number(v);
|
||||
if (declared === "number" || typeof def === "number") {
|
||||
const parsed = Number(v);
|
||||
if (!Number.isFinite(parsed)) throw new TypeError("Expected a finite number prop");
|
||||
return parsed;
|
||||
}
|
||||
|
||||
if (typeof def === "boolean") {
|
||||
return v === true || v === "" || v === "true";
|
||||
if (declared === "boolean" || typeof def === "boolean") {
|
||||
if (v === true || v === "" || v === "true" || v === 1 || v === "1") return true;
|
||||
if (v === false || v === "false" || v === 0 || v === "0") return false;
|
||||
throw new TypeError("Expected a boolean prop");
|
||||
}
|
||||
|
||||
if (Array.isArray(def)) {
|
||||
if (declared === "array" || Array.isArray(def)) {
|
||||
if (Array.isArray(v)) {
|
||||
return v;
|
||||
}
|
||||
@@ -1393,6 +1440,7 @@ function generateComponent(ast: PageAst): string {
|
||||
const parsed = JSON.parse(v);
|
||||
return Array.isArray(parsed) ? parsed : def;
|
||||
} catch {
|
||||
if (declared === "array") throw new TypeError("Expected an array prop");
|
||||
return def;
|
||||
}
|
||||
}
|
||||
@@ -1400,7 +1448,7 @@ function generateComponent(ast: PageAst): string {
|
||||
return def;
|
||||
}
|
||||
|
||||
if (def !== null && typeof def === "object") {
|
||||
if (declared === "object" || (def !== null && typeof def === "object")) {
|
||||
if (
|
||||
v !== null &&
|
||||
typeof v === "object" &&
|
||||
@@ -1421,6 +1469,7 @@ function generateComponent(ast: PageAst): string {
|
||||
? parsed
|
||||
: def;
|
||||
} catch {
|
||||
if (declared === "object") throw new TypeError("Expected an object prop");
|
||||
return def;
|
||||
}
|
||||
}
|
||||
@@ -1428,7 +1477,11 @@ function generateComponent(ast: PageAst): string {
|
||||
return def;
|
||||
}
|
||||
|
||||
return String(v);
|
||||
if (declared === "bigint") return BigInt(v);
|
||||
if (declared === "function" && typeof v !== "function") {
|
||||
throw new TypeError("Expected a function prop");
|
||||
}
|
||||
return declared === "unknown" && def === undefined ? v : String(v);
|
||||
}
|
||||
|
||||
function __wireHtml(v: any): string {
|
||||
@@ -1532,7 +1585,7 @@ function __wireRaw(v: any): string {
|
||||
const serverFunctionSource = serverFunctions ? `${serverFunctions}\n` : "";
|
||||
|
||||
out.push(
|
||||
`export function render(props: Record<string, any> = {}): string {\n` +
|
||||
`export function render(props: ${effectiveProps.length > 0 ? `${ast.name}Props` : "Record<string, any>"} = {} as ${effectiveProps.length > 0 ? `${ast.name}Props` : "Record<string, any>"}): string {\n` +
|
||||
` const __p = props || {};\n` +
|
||||
(decls.length > 0 ? decls.join("\n") + "\n" : "") +
|
||||
serverFunctionSource +
|
||||
|
||||
Reference in New Issue
Block a user