fix(tooling): reconcile framework fixes in canonical WireJS checkout
This commit is contained in:
1 parent
a077515336
commit
65d63dfbe7
19 files changed
+312
-156
No files matched your search
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/typecheck",
|
||||
"version": "0.8.16",
|
||||
"version": "0.8.17",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in new issue
Block a user