fix(tooling): reconcile framework fixes in canonical WireJS checkout

This commit is contained in:
Clintchiz committed 2026-08-26 18:10:58 +05:30
1 parent a077515336
commit 65d63dfbe7
19 files changed
+312 -156

No files matched your search

+2
View File
@@ -3,3 +3,5 @@
Static type checking for `.wrn` declarations, props, state, outputs, functions, stores, and generated virtual TypeScript files.
The package is compiler tooling rather than a browser UI package, so its public kit consists of programmatic typecheck helpers and diagnostics.
View attribute `{t:key}` segments are translation markers, not TypeScript expressions. Ordinary expressions in the same attribute remain typechecked. Inline event bindings accept statement lists, with `event.currentTarget` typed as the bound HTML/SVG element. Nullable DOM queries still require normal guards.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/typecheck",
"version": "0.8.16",
"version": "0.8.17",
"type": "module",
"main": "src/index.ts",
"exports": {
+11 -3
View File
@@ -377,6 +377,9 @@ export function virtualTypeScriptModule(
// misspelled state/function name cannot silently become `undefined` at
// runtime. Each/handler locals mirror the browser evaluator's bindings.
let bindingIndex = 0;
append(
"type __WrnEventElement<Tag extends string> = Tag extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[Tag] : Tag extends keyof SVGElementTagNameMap ? SVGElementTagNameMap[Tag] : HTMLElement;",
);
const reservedProps = new Set(
ast.props.map((prop) => prop.name).filter((name) => !safeBindingName(name)),
);
@@ -386,7 +389,12 @@ export function virtualTypeScriptModule(
for (const attr of node.attrs) {
const expressions = attr.event
? [attr.value]
: extractInterpolations(attr.value).map((segment) => segment.expression);
: extractInterpolations(attr.value)
// Translation markers are resolved by i18n, not evaluated as JavaScript.
// Filter individual segments so mixed translated/dynamic attributes
// still typecheck their ordinary expressions.
.filter((segment) => !/^t:[^{}]+$/.test(segment.expression))
.map((segment) => segment.expression);
for (const expression of expressions) {
if (!expression.trim()) continue;
const declarations = [
@@ -394,12 +402,12 @@ export function virtualTypeScriptModule(
...(attr.event
? [
"declare const payload: any;",
"declare const event: Event & { target: HTMLElement };",
`declare const event: Event & { target: HTMLElement; currentTarget: __WrnEventElement<${JSON.stringify(node.tag)}> };`,
]
: []),
].join(" ");
append(
`namespace __wrn_view_${bindingIndex++} { ${declarations} void (${safeViewExpression(expression, reservedProps)}); }`,
`namespace __wrn_view_${bindingIndex++} { ${declarations} ${attr.event ? `void (() => { ${safeViewExpression(expression, reservedProps)} });` : `void (${safeViewExpression(expression, reservedProps)});`} }`,
attr.value,
);
}
@@ -0,0 +1,59 @@
import { expect, test } from "bun:test";
import { checkWrnSource, virtualTypeScriptModule } from "../src/index.ts";
test("translation markers in native and component attributes are not TypeScript expressions", () => {
const source = `page Contact {
state count = 1
view {
<div data-component="PageHeader" eyebrow="{t:contact.eyebrow}" title="{t:contact.title}" description="{t:contact.description}"></div>
<input aria-label="{t:contact.form.name}" placeholder='{t:contact.form.namePlaceholder}' />
<div data-success-message="{t:contact.form.successMessage}" title="{t:contact.title} — {count}">{t:contact.title}</div>
<span t:aria-label="contact.form.label"></span>
}
}`;
expect(checkWrnSource(source)).toEqual([]);
const virtual = virtualTypeScriptModule(source).code;
expect(virtual).not.toContain("void (t:");
expect(virtual).toContain("void (count)");
});
test("mixed translation bindings retain genuine expression diagnostics", () => {
const source = `page Contact {
view {
<div title="{t:contact.title} — {missingValue}"></div>
<div title="{true ? missingBranch : 'fallback'}"></div>
<div data-value="{{ t: missingProperty }}"></div>
}
}`;
const diagnostics = checkWrnSource(source);
for (const name of ["missingValue", "missingBranch", "missingProperty"]) {
expect(
diagnostics.some((item) => item.code === "WRN-TYPE-2304" && item.message.includes(name)),
).toBe(true);
}
expect(diagnostics.some((item) => item.message.includes("name 't'"))).toBe(false);
});
test("the translation exemption does not apply to event handlers", () => {
const diagnostics = checkWrnSource(
`page Contact { view { <button @click="t:contact.title"></button> } }`,
);
expect(
diagnostics.some((item) => item.code === "WRN-TYPE-2304" && item.message.includes("contact")),
).toBe(true);
});
test("form event statement lists have the correct currentTarget type", () => {
const source = `page Contact {
view {
<form @wrn:success="event.currentTarget.reset(); toast.success('Sent')"></form>
<input @input="toast.info(event.currentTarget.value)" />
<button @click="event.currentTarget.disabled = true; toast.info('Working')"></button>
}
}`;
expect(checkWrnSource(source)).toEqual([]);
const invalid = checkWrnSource(
source.replace("event.currentTarget.disabled", "event.currentTarget.notARealProperty"),
);
expect(invalid.some((item) => item.code === "WRN-TYPE-2339")).toBe(true);
});