feat: close application architecture gaps
Quality / quality (ubuntu-latest) (push) Failing after 9m56s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-23 11:45:57 +05:30
parent d87b197224
commit 2e12656060
20 changed files with 548 additions and 23 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/typecheck",
"version": "0.8.11",
"version": "0.8.13",
"type": "module",
"main": "src/index.ts",
"exports": {
+61 -2
View File
@@ -307,6 +307,9 @@ export function virtualTypeScriptModule(
if (ast.dataApis.length) append(`declare const api: { ${apiType} };`);
append(`declare const props: Readonly<${ast.name}Props>;`);
append("declare const refs: Record<string, Element | null>;");
append(
"declare function useFetch(path: string, method?: string | { method?: string; query?: unknown; params?: unknown; body?: unknown; data?: unknown }, input?: unknown): Promise<any>;",
);
append(
ast.kind === "global-store" || ast.kind === "page-store"
? storeContract(ast)
@@ -332,6 +335,59 @@ export function virtualTypeScriptModule(
for (const fn of functions) append(functionDeclaration(fn), fn.source);
append("}");
}
const viewFunctions = new Map<string, RuntimeFunctionDecl["runtime"]>();
for (const fn of ast.runtimeFunctions) {
if (fn.runtime !== "server") viewFunctions.set(fn.name, fn.runtime);
}
for (const [name, runtime] of viewFunctions) {
append(`declare const ${name}: typeof ${runtimeNamespace(runtime)}.${name};`);
}
// View expressions execute in the client scope, but historically were not
// represented in the virtual TypeScript module. Validate them here so a
// misspelled state/function name cannot silently become `undefined` at
// runtime. Each/handler locals mirror the browser evaluator's bindings.
let bindingIndex = 0;
const appendViewBindings = (nodes: ViewNode[], locals: Set<string>): void => {
for (const node of nodes) {
if (node.type === "element") {
for (const attr of node.attrs) {
const expressions = attr.event
? [attr.value]
: [...attr.value.matchAll(/\{([^{}]+)\}/g)].map((match) => match[1]!.trim());
for (const expression of expressions) {
if (!expression.trim()) continue;
const declarations = [
...[...locals].map((name) => `declare const ${name}: any;`),
...(attr.event ? ["declare const payload: any;", "declare const event: Event;"] : []),
].join(" ");
append(
`namespace __wrn_view_${bindingIndex++} { ${declarations} void (${expression}); }`,
attr.value,
);
}
}
appendViewBindings(node.children, locals);
} else if (node.type === "if") {
for (const branch of node.branches) {
if (branch.cond)
append(
`namespace __wrn_view_${bindingIndex++} { void (${branch.cond}); }`,
branch.cond,
);
appendViewBindings(branch.body, locals);
}
} else if (node.type === "each") {
append(`namespace __wrn_view_${bindingIndex++} { void (${node.list}); }`, node.list);
const nested = new Set(locals);
nested.add(node.item);
if (node.index) nested.add(node.index);
appendViewBindings(node.body, nested);
appendViewBindings(node.empty, locals);
}
}
};
appendViewBindings(ast.view, new Set());
return {
ast,
@@ -464,7 +520,9 @@ function componentUsageDiagnostics(
const shape = shapes.get(node.tag);
if (!shape) return;
const attributes = new Map(
node.attrs.filter((attr) => !attr.event).map((attr) => [attr.name, attr]),
node.attrs
.filter((attr) => !attr.event)
.map((attr) => [attr.name.startsWith("bind:") ? attr.name.slice(5) : attr.name, attr]),
);
const outputNames = new Set(shape.outputs.map((output) => output.name));
const position = lineAt(source, `<${node.tag}`);
@@ -501,7 +559,8 @@ function componentUsageDiagnostics(
continue;
}
if (/^(?:class|id|style|slot|data-|aria-)/.test(attr.name) || attr.name === "attrs") continue;
const prop = known.get(attr.name);
const propName = attr.name.startsWith("bind:") ? attr.name.slice(5) : attr.name;
const prop = known.get(propName);
if (!prop) {
diagnostics.push({
code: "WRN-COMPONENT-UNKNOWN-PROP",
+27
View File
@@ -85,3 +85,30 @@ test("provides the framework context to page state expressions", () => {
);
expect(diagnostics.some((diagnostic) => diagnostic.code === "WRN-TYPE-2304")).toBe(false);
});
test("rejects an out-of-scope name in a view event handler", () => {
const root = app();
const diagnostics = checkWrnSource(
`page AccountPage {
functions { client function save() {} }
view { <button @click="savve(missingValue)"></button> }
}`,
{ appRoot: root, filePath: join(root, "app", "pages", "account.wrn") },
);
expect(diagnostics.some((diagnostic) => diagnostic.code === "WRN-TYPE-2304")).toBe(true);
});
test("accepts handler payload, loop locals, and the public useFetch helper", () => {
const root = app();
const diagnostics = checkWrnSource(
`page AccountPage {
state rows = [{ id: "1" }]
functions {
client async function save(id: string) { await useFetch("/api/rows/" + id, "POST", { id }) }
}
view { {#each rows as row}<button @click="save(row.id)"></button>{/each} }
}`,
{ appRoot: root, filePath: join(root, "app", "pages", "account.wrn") },
);
expect(diagnostics.filter((diagnostic) => diagnostic.code === "WRN-TYPE-2304")).toEqual([]);
});