release: WRNexusJS 0.8.0
Quality / quality (ubuntu-latest) (push) Failing after 12m19s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-04 12:19:09 +05:30
parent 4cebacadfe
commit 72e4d3eceb
108 changed files with 2276 additions and 532 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/typecheck",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"main": "src/index.ts",
"exports": {
+129 -29
View File
@@ -139,9 +139,13 @@ 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 }" : ": unknown"}${param.default ? ` = ${param.default}` : ""}`,
`${param.name}${param.optional ? "?" : ""}${param.valueType ? `: ${param.valueType}` : param.name === "event" ? ": CustomEvent<any> & { target: HTMLElement }" : ": any"}${param.default ? ` = ${param.default}` : ""}`,
)
.join(", ");
// An omitted WRN parameter type follows JavaScript semantics. Emitting it as
// explicit `any` prevents the virtual TypeScript document from inventing
// false errors for valid dynamic handlers such as output[type](payload).
// Developers can opt into strict checking by declaring the parameter type.
return `export ${fn.async ? "async " : ""}function ${fn.name}(${params})${fn.returnType ? `: ${fn.returnType}` : ""} {${fn.body}}`;
}
@@ -160,6 +164,53 @@ function retainedImports(ast: PageAst): string {
.join("\n");
}
const RESERVED_BINDING_NAMES = new Set([
"await",
"break",
"case",
"catch",
"class",
"const",
"continue",
"debugger",
"default",
"delete",
"do",
"else",
"enum",
"export",
"extends",
"false",
"finally",
"for",
"function",
"if",
"import",
"in",
"instanceof",
"let",
"new",
"null",
"return",
"static",
"super",
"switch",
"this",
"throw",
"true",
"try",
"typeof",
"var",
"void",
"while",
"with",
"yield",
]);
function safeBindingName(name: string): boolean {
return /^[A-Za-z_$][\w$]*$/.test(name) && !RESERVED_BINDING_NAMES.has(name);
}
export function virtualTypeScriptModule(
source: string,
filePath = "component.wrn",
@@ -190,6 +241,10 @@ export function virtualTypeScriptModule(
append(ast.types.join("\n\n"), ast.types[0]?.trim());
for (const prop of ast.props) {
// JavaScript keywords such as the conventional `class` component prop are
// valid WRN prop names but cannot be emitted as standalone TS bindings.
// They remain available through the typed `props` object.
if (!safeBindingName(prop.name)) continue;
append(`declare const ${prop.name}: Readonly<${prop.valueType ?? "unknown"}>;`, prop.name);
}
for (const state of ast.states) {
@@ -218,7 +273,7 @@ export function virtualTypeScriptModule(
`${fn.name}: (${fn.parameters.map((param) => `${param.name}${param.optional ? "?" : ""}: ${param.valueType ?? "unknown"}`).join(", ")}) => ${fn.returnType ?? (fn.async ? "Promise<unknown>" : "unknown")}`,
)
.join("; ");
append(`declare const output: { ${outputType} };`);
append(`declare const output: { [name: string]: (...args: any[]) => void; ${outputType} };`);
for (const output of ast.outputs)
append(
`declare const ${output.name}: (${output.payload ? `${output.payload.name}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}) => void;`,
@@ -439,7 +494,15 @@ function componentUsageDiagnostics(
if (attr.value.startsWith("{") && attr.value.endsWith("}")) continue;
const received = attr.boolean ? "boolean" : inferredRuntimeType(JSON.stringify(attr.value));
const expected = runtimeTypeOf(prop.type);
if (expected !== "unknown" && received !== "unknown" && expected !== received) {
const compatibleLiteral =
(expected === "boolean" && /^(?:true|false|1|0|yes|no|on|off)?$/i.test(attr.value)) ||
(expected === "number" && attr.value.trim() !== "" && Number.isFinite(Number(attr.value)));
if (
expected !== "unknown" &&
received !== "unknown" &&
expected !== received &&
!compatibleLiteral
) {
diagnostics.push({
code: "WRN-COMPONENT-PROP-TYPE",
category: "error",
@@ -481,8 +544,15 @@ function category(value: ts.DiagnosticCategory): WrnTypeDiagnostic["category"] {
function hostWithVirtualFiles(
files: Map<string, string>,
options: ts.CompilerOptions,
standardLibraryDirectory?: string,
): ts.CompilerHost {
const host = ts.createCompilerHost(options, true);
if (standardLibraryDirectory) {
host.getDefaultLibFileName = (compilerOptions) =>
join(standardLibraryDirectory, ts.getDefaultLibFileName(compilerOptions));
(host as ts.CompilerHost & { getDefaultLibLocation?: () => string }).getDefaultLibLocation =
() => standardLibraryDirectory;
}
const originalGet = host.getSourceFile.bind(host);
host.fileExists = (fileName) => files.has(normalize(fileName)) || ts.sys.fileExists(fileName);
host.readFile = (fileName) => files.get(normalize(fileName)) ?? ts.sys.readFile(fileName);
@@ -495,15 +565,32 @@ function hostWithVirtualFiles(
return host;
}
function resolveStandardLibraryDirectory(
appRoot: string,
options: ts.CompilerOptions,
): string | null {
const candidates = [
dirname(ts.getDefaultLibFilePath(options)),
join(appRoot, "node_modules", "typescript", "lib"),
join(process.cwd(), "node_modules", "typescript", "lib"),
];
for (const candidate of candidates) {
if ((options.lib ?? []).every((name) => ts.sys.fileExists(join(candidate, name)))) {
return candidate;
}
}
return null;
}
function mappedPosition(
virtual: VirtualTypeScriptModule,
line: number,
column: number,
): { line: number; column: number } {
): { line: number; column: number } | null {
const mapping = virtual.mappings.find(
(entry) => line >= entry.virtualStartLine && line <= entry.virtualEndLine,
);
if (!mapping) return { line: 1, column: 1 };
if (!mapping) return null;
const offset = line - mapping.virtualStartLine;
return {
line: mapping.sourceStartLine + offset,
@@ -644,30 +731,43 @@ export function checkWrnSource(
lib: ["lib.esnext.d.ts", "lib.dom.d.ts", "lib.dom.iterable.d.ts"],
};
const rootNames = [virtual.fileName, ...appTypes.files.keys()];
const program = ts.createProgram(
rootNames,
compilerOptions,
hostWithVirtualFiles(files, compilerOptions),
);
const tsDiagnostics = ts.getPreEmitDiagnostics(program).map((diagnostic): WrnTypeDiagnostic => {
const file = diagnostic.file;
const start = diagnostic.start ?? 0;
const virtualPosition = file?.getLineAndCharacterOfPosition(start) ?? { line: 0, character: 0 };
const isVirtual = normalize(file?.fileName ?? "") === normalize(virtual.fileName);
const sourcePosition = isVirtual
? mappedPosition(virtual, virtualPosition.line + 1, virtualPosition.character + 1)
: { line: virtualPosition.line + 1, column: virtualPosition.character + 1 };
return {
code: `WRN-TYPE-${diagnostic.code}`,
category: category(diagnostic.category),
message: ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"),
file: isVirtual ? filePath : (file?.fileName ?? filePath),
line: sourcePosition.line,
column: sourcePosition.column,
length: diagnostic.length ?? 1,
hint: "Fix the TypeScript contract or expression in the related .wrn declaration.",
};
});
const standardLibraryDirectory = resolveStandardLibraryDirectory(appRoot, compilerOptions);
const tsDiagnostics: WrnTypeDiagnostic[] = [];
if (standardLibraryDirectory) {
const program = ts.createProgram(
rootNames,
compilerOptions,
hostWithVirtualFiles(files, compilerOptions, standardLibraryDirectory),
);
for (const diagnostic of ts.getPreEmitDiagnostics(program)) {
const file = diagnostic.file;
if (!file) continue;
const normalizedFile = normalize(file.fileName);
const isVirtual = normalizedFile === normalize(virtual.fileName);
const isApplicationType = files.has(normalizedFile);
if (!isVirtual && !isApplicationType) continue;
const start = diagnostic.start ?? 0;
const virtualPosition = file.getLineAndCharacterOfPosition(start);
const sourcePosition = isVirtual
? mappedPosition(virtual, virtualPosition.line + 1, virtualPosition.character + 1)
: { line: virtualPosition.line + 1, column: virtualPosition.character + 1 };
// Synthetic declarations without a source mapping are implementation
// details of the virtual document and must not appear as editor errors.
if (!sourcePosition) continue;
tsDiagnostics.push({
code: `WRN-TYPE-${diagnostic.code}`,
category: category(diagnostic.category),
message: ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"),
file: isVirtual ? filePath : file.fileName,
line: sourcePosition.line,
column: sourcePosition.column,
length: diagnostic.length ?? 1,
hint: "Fix the TypeScript contract or expression in the related .wrn declaration.",
});
}
}
return [
...(options.checkRuntimeBoundaries === false
? []
@@ -60,3 +60,26 @@ page Home {
);
expect(diagnostics.some((item) => item.code === "WRN-TYPE-2345")).toBe(true);
}, 15_000);
test("accepts WRN boolean string literals and reserved class props", () => {
const root = fixture();
const componentPath = join(root, "app", "components", "Toggle.wrn");
writeFileSync(
componentPath,
`component Toggle {
props { enabled: boolean = false class: string = "" }
view { <button class='{class}'></button> }
}`,
);
const filePath = join(root, "app", "pages", "home.wrn");
const diagnostics = checkWrnSource(
`import Toggle from "@/components/Toggle.wrn"
page Home {
view { <Toggle enabled="true" class="demo" /> }
}`,
{ appRoot: root, filePath },
);
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);
}, 15_000);
+18
View File
@@ -55,3 +55,21 @@ test("allows the same function name in client and server runtimes", () => {
diagnostics.filter((diagnostic) => diagnostic.code === "WRN-FUNCTION-DUPLICATE"),
).toHaveLength(0);
});
test("treats omitted runtime parameter types as dynamic JavaScript values", () => {
const root = app();
const diagnostics = checkWrnSource(
`component Demo {
outputs { change(payload: { value?: string; sourceEvent?: Event }) }
functions {
client function forward(type, payload) {
const detail = payload || {}
output[type]({ value: detail.value || "", sourceEvent: detail.sourceEvent })
}
}
view { <button @click="forward('change', payload)"></button> }
}`,
{ appRoot: root, filePath: join(root, "app", "components", "Demo.wrn") },
);
expect(diagnostics.filter((diagnostic) => diagnostic.code.startsWith("WRN-TYPE-"))).toEqual([]);
});