chore: harden release checks and package coverage
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/typecheck",
|
||||
"version": "0.8.13",
|
||||
"version": "0.8.15",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -139,7 +139,7 @@ function functionDeclaration(fn: RuntimeFunctionDecl): string {
|
||||
const params = fn.parameters
|
||||
.map(
|
||||
(param) =>
|
||||
`${param.name}${param.optional ? "?" : ""}${param.valueType ? `: ${param.valueType}` : param.name === "event" ? ": CustomEvent<any> & { target: HTMLElement }" : ": any"}${param.default ? ` = ${param.default}` : ""}`,
|
||||
`${param.name}${param.optional ? "?" : ""}${param.valueType ? `: ${param.valueType}` : param.name === "event" ? ": Event & { target: HTMLElement }" : ": any"}${param.default ? ` = ${param.default}` : ""}`,
|
||||
)
|
||||
.join(", ");
|
||||
// An omitted WRN parameter type follows JavaScript semantics. Emitting it as
|
||||
@@ -214,6 +214,31 @@ function safeBindingName(name: string): boolean {
|
||||
return /^[A-Za-z_$][\w$]*$/.test(name) && !RESERVED_BINDING_NAMES.has(name);
|
||||
}
|
||||
|
||||
function safeViewExpression(expression: string, reservedProps: ReadonlySet<string>): string {
|
||||
if (!reservedProps.size) return expression;
|
||||
const scanner = ts.createScanner(
|
||||
ts.ScriptTarget.Latest,
|
||||
false,
|
||||
ts.LanguageVariant.Standard,
|
||||
expression,
|
||||
);
|
||||
let result = "";
|
||||
let copied = 0;
|
||||
for (let token = scanner.scan(); token !== ts.SyntaxKind.EndOfFileToken; token = scanner.scan()) {
|
||||
const start = scanner.getTokenPos();
|
||||
const end = scanner.getTextPos();
|
||||
const text = scanner.getTokenText();
|
||||
if (!reservedProps.has(text)) continue;
|
||||
const previous = expression.slice(0, start).trimEnd().at(-1);
|
||||
const following = expression.slice(end).trimStart().at(0);
|
||||
// Property access and object-literal keys are already valid TypeScript.
|
||||
if (previous === "." || following === ":") continue;
|
||||
result += expression.slice(copied, start) + `props.${text}`;
|
||||
copied = end;
|
||||
}
|
||||
return result + expression.slice(copied);
|
||||
}
|
||||
|
||||
export function virtualTypeScriptModule(
|
||||
source: string,
|
||||
filePath = "component.wrn",
|
||||
@@ -348,6 +373,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;
|
||||
const reservedProps = new Set(
|
||||
ast.props.map((prop) => prop.name).filter((name) => !safeBindingName(name)),
|
||||
);
|
||||
const appendViewBindings = (nodes: ViewNode[], locals: Set<string>): void => {
|
||||
for (const node of nodes) {
|
||||
if (node.type === "element") {
|
||||
@@ -359,10 +387,15 @@ export function virtualTypeScriptModule(
|
||||
if (!expression.trim()) continue;
|
||||
const declarations = [
|
||||
...[...locals].map((name) => `declare const ${name}: any;`),
|
||||
...(attr.event ? ["declare const payload: any;", "declare const event: Event;"] : []),
|
||||
...(attr.event
|
||||
? [
|
||||
"declare const payload: any;",
|
||||
"declare const event: Event & { target: HTMLElement };",
|
||||
]
|
||||
: []),
|
||||
].join(" ");
|
||||
append(
|
||||
`namespace __wrn_view_${bindingIndex++} { ${declarations} void (${expression}); }`,
|
||||
`namespace __wrn_view_${bindingIndex++} { ${declarations} void (${safeViewExpression(expression, reservedProps)}); }`,
|
||||
attr.value,
|
||||
);
|
||||
}
|
||||
@@ -372,13 +405,16 @@ export function virtualTypeScriptModule(
|
||||
for (const branch of node.branches) {
|
||||
if (branch.cond)
|
||||
append(
|
||||
`namespace __wrn_view_${bindingIndex++} { void (${branch.cond}); }`,
|
||||
`namespace __wrn_view_${bindingIndex++} { void (${safeViewExpression(branch.cond, reservedProps)}); }`,
|
||||
branch.cond,
|
||||
);
|
||||
appendViewBindings(branch.body, locals);
|
||||
}
|
||||
} else if (node.type === "each") {
|
||||
append(`namespace __wrn_view_${bindingIndex++} { void (${node.list}); }`, node.list);
|
||||
append(
|
||||
`namespace __wrn_view_${bindingIndex++} { void (${safeViewExpression(node.list, reservedProps)}); }`,
|
||||
node.list,
|
||||
);
|
||||
const nested = new Set(locals);
|
||||
nested.add(node.item);
|
||||
if (node.index) nested.add(node.index);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { checkWrnSource } from "../src/index.ts";
|
||||
@@ -82,6 +82,23 @@ page Home {
|
||||
expect(diagnostics.some((item) => item.code === "WRN-COMPONENT-PROP-TYPE")).toBe(false);
|
||||
expect(diagnostics.some((item) => item.code === "WRN-TYPE-1389")).toBe(false);
|
||||
expect(diagnostics.some((item) => item.code === "WRN-TYPE-1005")).toBe(false);
|
||||
expect(
|
||||
checkWrnSource(readFileSync(componentPath, "utf8"), { appRoot: root, filePath: componentPath }),
|
||||
).toEqual([]);
|
||||
}, 15_000);
|
||||
|
||||
test("implicit DOM events satisfy untyped event handler parameters", () => {
|
||||
const root = fixture();
|
||||
const filePath = join(root, "app", "components", "Submit.wrn");
|
||||
const diagnostics = checkWrnSource(
|
||||
`component Submit {
|
||||
outputs { submit(payload: { event: Event }) }
|
||||
functions { client function submitForm(event) { output.submit({ event }) } }
|
||||
view { <form @submit="submitForm(event)"></form> }
|
||||
}`,
|
||||
{ appRoot: root, filePath },
|
||||
);
|
||||
expect(diagnostics).toEqual([]);
|
||||
}, 15_000);
|
||||
|
||||
test("does not treat UI component imports as JavaScript exports", () => {
|
||||
|
||||
Reference in New Issue
Block a user