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/ai",
"version": "0.8.3",
"version": "0.8.4",
"private": true,
"type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/auth",
"version": "0.8.3",
"version": "0.8.4",
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
"type": "module",
"sideEffects": false,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/authz",
"version": "0.8.3",
"version": "0.8.4",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/benchmark",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"description": "Deterministic benchmark runner and performance regression budgets for WRNexusJS.",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/cache",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"description": "Tag-aware memory and response caching with stale-while-revalidate for WRNexusJS.",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/captcha",
"version": "0.8.3",
"version": "0.8.4",
"description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.",
"type": "module",
"sideEffects": false,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/cli",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"main": "src/index.ts",
"exports": {
+14
View File
@@ -1999,6 +1999,20 @@ const MIGRATIONS: Migration[] = [
// The compile-cache generation changes automatically invalidate old artifacts.
},
},
{
version: "0.8.4",
id: "0.8.4-editor-language-support-stability",
description:
"Fixes WRNexus Language Server crashes, WRN formatting stability, false closing-tag diagnostics, typed event inference, dynamic component prop validation, and VS Code extension reliability.",
apply() {
// Compiler, diagnostics, formatter, language-server, and VS Code
// extension fixes require no application source migration.
//
// Existing projects receive the framework package changes through the
// dependency update. Developers must separately update/reinstall the
// WRNexus Language Support VS Code extension.
},
},
];
/** Release tooling uses this to require an explicit migration entry per version. */
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/compiler",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/content",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"description": "Typed Markdown content collections, loaders, indexes, feeds, and preview workflows for WRNexusJS.",
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/core",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/csr",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/db",
"version": "0.8.3",
"version": "0.8.4",
"private": true,
"type": "module",
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-server",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-toolbar",
"version": "0.8.3",
"version": "0.8.4",
"private": true,
"type": "module",
"sideEffects": false,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/encryption",
"version": "0.8.3",
"version": "0.8.4",
"private": true,
"type": "module",
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/graphql",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"main": "./src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/helpers",
"version": "0.8.3",
"version": "0.8.4",
"private": true,
"type": "module",
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/i18n",
"version": "0.8.3",
"version": "0.8.4",
"private": true,
"type": "module",
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/identity",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"description": "Enterprise federation, provisioning, machine identity, and privacy governance for WRNexusJS.",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/image",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"description": "Responsive image planning, secure remote image policies, and performance auditing for WRNexusJS.",
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/jwt",
"version": "0.8.3",
"version": "0.8.4",
"private": true,
"type": "module",
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/language-server",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"description": "Editor-neutral Language Server Protocol implementation for WRNexus .wrn files.",
"main": "src/index.ts",
+53 -6
View File
@@ -36,11 +36,33 @@ function send(value: unknown): void {
function result(id: JsonRpc["id"], value: unknown): void {
send({ jsonrpc: "2.0", id, result: value });
}
function internalDiagnostic(document: TextDocument, error: unknown) {
const message = error instanceof Error ? error.message : String(error);
return {
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
severity: 1,
code: "WRN-LSP-INTERNAL",
source: "wrnexus",
message: `WRNexus language analysis failed safely: ${message}`,
};
}
function safeDocumentDiagnostics(document: TextDocument) {
try {
return documentDiagnostics(document);
} catch (error) {
process.stderr.write(
`[wrnexus-lsp] diagnostics failed for ${document.uri}: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`,
);
return [internalDiagnostic(document, error)];
}
}
function publish(document: TextDocument): void {
send({
jsonrpc: "2.0",
method: "textDocument/publishDiagnostics",
params: { uri: document.uri, diagnostics: documentDiagnostics(document) },
params: { uri: document.uri, diagnostics: safeDocumentDiagnostics(document) },
});
}
@@ -80,7 +102,7 @@ async function handle(message: JsonRpc): Promise<void> {
case "initialize":
workspaceRoot = params.rootPath ?? rootFromUri(params.rootUri);
result(message.id, {
serverInfo: { name: "WRNexus Language Server", version: "0.8.0" },
serverInfo: { name: "WRNexus Language Server", version: "0.8.3" },
capabilities: {
textDocumentSync: 1,
documentFormattingProvider: true,
@@ -195,7 +217,7 @@ async function handle(message: JsonRpc): Promise<void> {
result(message.id, []);
break;
}
const actions: any[] = documentDiagnostics(document)
const actions: any[] = safeDocumentDiagnostics(document)
.filter((item) => item.code === "WRNA11Y001")
.map((item) => ({
title: "Add empty alt attribute",
@@ -268,11 +290,36 @@ function consume(): void {
if (buffer.length < bodyStart + length) return;
const body = buffer.subarray(bodyStart, bodyStart + length).toString();
buffer = buffer.subarray(bodyStart + length);
void handle(JSON.parse(body));
let message: JsonRpc;
try {
message = JSON.parse(body) as JsonRpc;
} catch (error) {
process.stderr.write(
`[wrnexus-lsp] invalid JSON-RPC payload: ${error instanceof Error ? error.message : String(error)}\n`,
);
continue;
}
void handle(message).catch((error) => {
const detail = error instanceof Error ? (error.stack ?? error.message) : String(error);
process.stderr.write(`[wrnexus-lsp] request failed: ${detail}\n`);
if (message.id !== undefined) {
send({
jsonrpc: "2.0",
id: message.id,
error: { code: -32603, message: "WRNexus language server request failed", data: detail },
});
}
});
}
}
process.stdin.on("data", (chunk) => {
buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
consume();
try {
buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
consume();
} catch (error) {
process.stderr.write(
`[wrnexus-lsp] input processing failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`,
);
}
});
process.stdin.resume();
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/mcp",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"description": "Model Context Protocol server exposing WRNexus application and framework context.",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/mobile",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/native",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/oauth",
"version": "0.8.3",
"version": "0.8.4",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/observability",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"description": "Metrics, Web Vitals collection, request instrumentation, and exporter adapters for WRNexusJS.",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/playground",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"description": "Secure, shareable WRNexus compiler and UI playground.",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/plugin",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/pubsub",
"version": "0.8.3",
"version": "0.8.4",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/pwa",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"description": "Service workers, manifests, offline queues, background sync, push, and conflict resolution for WRNexusJS.",
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/queue",
"version": "0.8.3",
"version": "0.8.4",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/reactive",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/realtime",
"version": "0.8.3",
"version": "0.8.4",
"description": "Typed realtime rooms, message helpers, presence utilities, package UI blocks, and WRNexusJS integration.",
"type": "module",
"sideEffects": false,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/router",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/security",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"description": "Security policies, safe serialization, SSRF protection, request limits, and secure cookie helpers for WRNexusJS.",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ssr",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/store",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/styles",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/syntax",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"main": "src/index.ts",
"exports": {
+140 -3
View File
@@ -416,6 +416,17 @@ function formatOpeningTag(value, unit, depth, printWidth = 100, multilineAttribu
};
}
if (parsed.attributes.length === 0 && !parsed.selfClosing) {
const lines = [`${baseIndent}<${parsed.tagName}>`];
if (parsed.trailingClosing) {
if (parsed.inlineContent) lines.push(`${childIndent}${parsed.inlineContent}`);
lines.push(`${baseIndent}</${parsed.tagName}>`);
} else if (parsed.remainder) {
lines.push(`${childIndent}${parsed.remainder}`);
}
return { lines, opensElement };
}
const lines = [
`${baseIndent}<${parsed.tagName}`,
...parsed.attributes.flatMap((attribute) => formatAttribute(attribute, childIndent, unit)),
@@ -468,6 +479,75 @@ function isClosingTag(value) {
return /^<\/[A-Za-z][\w$:.-]*\s*>/.test(value);
}
/**
* Preserve compact, already-balanced HTML fragments as one line.
*
* A line such as `<span>Page</span><b>→</b><span>API</span>` is valid and
* intentionally compact. Expanding only its first opening tag makes later
* formatter passes treat the remaining siblings as children, causing runaway
* indentation and false closing-tag diagnostics. Balanced fragments are kept
* intact while normal multiline opening tags continue through the formatter.
*/
function isBalancedInlineHtmlFragment(value) {
if (!value.startsWith("<") || value.startsWith("</") || value.startsWith("<!--")) {
return false;
}
const stack = [];
let tagCount = 0;
let rootCount = 0;
let hasNestedElement = false;
let hasOutsideText = false;
let index = 0;
while (index < value.length) {
const tagStart = value.indexOf("<", index);
if (tagStart === -1) {
if (stack.length === 0 && value.slice(index).trim()) hasOutsideText = true;
break;
}
if (stack.length === 0 && value.slice(index, tagStart).trim()) hasOutsideText = true;
if (value.startsWith("<!--", tagStart)) {
const commentEnd = value.indexOf("-->", tagStart + 4);
if (commentEnd === -1) return false;
index = commentEnd + 3;
continue;
}
const relativeEnd = findOpeningTagEnd(value.slice(tagStart));
if (relativeEnd === -1) return false;
const tag = value.slice(tagStart, tagStart + relativeEnd + 1);
const match = /^<\/?([A-Za-z][\w$:.-]*)[\s\S]*?>$/.exec(tag);
if (!match) {
index = tagStart + 1;
continue;
}
tagCount += 1;
const tagName = match[1];
const normalizedName = /^[a-z]/.test(tagName) ? tagName.toLowerCase() : tagName;
const closing = tag.startsWith("</");
const selfClosing = /\/\s*>$/.test(tag);
const voidElement = VOID_ELEMENTS.has(tagName.toLowerCase());
if (closing) {
if (stack.at(-1) !== normalizedName) return false;
stack.pop();
} else {
if (stack.length === 0) rootCount += 1;
else hasNestedElement = true;
if (!selfClosing && !voidElement) stack.push(normalizedName);
}
index = tagStart + relativeEnd + 1;
}
return (
tagCount >= 2 && stack.length === 0 && (rootCount > 1 || hasNestedElement || hasOutsideText)
);
}
function isControlBlockOpen(value) {
return /^\{#(?:if|each)\b[\s\S]*\}$/.test(value);
}
@@ -597,6 +677,50 @@ function collectOpeningTag(inputLines, startIndex) {
};
}
function isPreservedRawBlockStart(value) {
return (
/<pre(?:\s|>)/i.test(value) &&
!/<\/pre\s*>/i.test(value.slice(0, value.search(/<pre(?:\s|>)/i)))
);
}
function hasPreservedRawBlockEnd(value) {
return /<\/pre\s*>/i.test(value);
}
function transformOutsidePreservedRawBlocks(lines, transformLine) {
const output = [];
let preserving = false;
for (const line of lines) {
if (preserving) {
output.push(line);
if (hasPreservedRawBlockEnd(line)) preserving = false;
continue;
}
if (isPreservedRawBlockStart(line)) {
output.push(line);
preserving = !hasPreservedRawBlockEnd(line);
continue;
}
output.push(...transformLine(line));
}
return output;
}
function collectPreservedRawBlock(lines, startIndex) {
const collected = [lines[startIndex]];
let index = startIndex;
while (!hasPreservedRawBlockEnd(collected.at(-1) || "") && index + 1 < lines.length) {
index += 1;
collected.push(lines[index]);
}
return { lines: collected, endIndex: index };
}
/**
* Put WRN template control markers on their own lines before indentation.
*
@@ -608,7 +732,7 @@ function expandInlineControlBlocks(lines) {
const marker =
/(\{#(?:if|each)\b[^}]*\}|\{:(?:else(?:\s+if\b[^}]*)?|empty)\}|\{\/(?:if|each)\})/g;
return lines.flatMap((line) => {
return transformOutsidePreservedRawBlocks(lines, (line) => {
if (!marker.test(line)) return [line];
marker.lastIndex = 0;
@@ -623,7 +747,7 @@ function expandInlineControlBlocks(lines) {
}
function expandStructuredStateDeclarations(lines, unit) {
return lines.flatMap((line) => {
return transformOutsidePreservedRawBlocks(lines, (line) => {
const match = /^(\s*state\s+[A-Za-z_$][\w$]*\s*=\s*)([\\[{][\s\S]*)$/.exec(line);
if (!match) return [line];
@@ -695,6 +819,15 @@ function formatWrnPass(source: string, options: FormatWrnOptions = {}): string {
continue;
}
if (isPreservedRawBlockStart(value)) {
const collected = collectPreservedRawBlock(inputLines, index);
const depth = codeDepth + htmlDepth + controlDepth;
output.push(`${unit.repeat(depth)}${collected.lines[0].trimStart()}`);
output.push(...collected.lines.slice(1));
index = collected.endIndex + 1;
continue;
}
if (isMultilineOpeningTagStart(value)) {
const collected = collectOpeningTag(inputLines, index);
@@ -730,7 +863,11 @@ function formatWrnPass(source: string, options: FormatWrnOptions = {}): string {
const depth = lineCodeDepth + lineHtmlDepth + lineControlDepth;
if (
const inlineFragmentFits = unit.repeat(depth).length + value.length <= printWidth;
if (isBalancedInlineHtmlFragment(value) && inlineFragmentFits) {
output.push(`${unit.repeat(depth)}${value}`);
} else if (
value.startsWith("<") &&
!value.startsWith("</") &&
!value.startsWith("<!--") &&
+18 -3
View File
@@ -6,9 +6,24 @@ export type RuntimeType =
export function runtimeTypeOf(annotation: string | undefined): RuntimeType {
if (!annotation) return "unknown";
const type = annotation.trim().replace(/^readonly\s+/, "");
if (/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(type)) return "string";
if (/^(?:number|Number)(?:\s*\|\s*(?:null|undefined))*$/.test(type)) return "number";
if (/^(?:boolean|Boolean)(?:\s*\|\s*(?:null|undefined))*$/.test(type)) return "boolean";
const unionParts = type.split("|").map((part) => part.trim());
const concreteParts = unionParts.filter((part) => !/^(?:null|undefined)$/.test(part));
if (
/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(type) ||
(concreteParts.length > 0 && concreteParts.every((part) => /^(?:"[^"]*"|'[^']*')$/.test(part)))
)
return "string";
if (
/^(?:number|Number)(?:\s*\|\s*(?:null|undefined))*$/.test(type) ||
(concreteParts.length > 0 &&
concreteParts.every((part) => /^-?(?:\d+\.?\d*|\.\d+)$/.test(part)))
)
return "number";
if (
/^(?:boolean|Boolean)(?:\s*\|\s*(?:null|undefined))*$/.test(type) ||
(concreteParts.length > 0 && concreteParts.every((part) => /^(?:true|false)$/.test(part)))
)
return "boolean";
if (/^bigint(?:\s*\|\s*(?:null|undefined))*$/.test(type)) return "bigint";
if (/^(?:Array\s*<|ReadonlyArray\s*<|.+\[\])/.test(type) || /^\[/.test(type)) return "array";
if (/^(?:Record\s*<|object\b|\{)/.test(type)) return "object";
+46
View File
@@ -37,3 +37,49 @@ view {
expect(formatted).not.toContain("\r");
});
});
test("preserves preformatted code examples while formatting surrounding markup", () => {
const source = `page Docs {
view {
<section class="docs">
<pre><code><span class="code-muted">// One project, one language</span>
<span class="code-key">page</span> ProductDashboard {
<span class="code-key">state</span> count: number = 0
&lt;button @click='count++'&gt;Ship {count}&lt;/button&gt;
}</code></pre>
</section>
}
}
`;
const formatted = formatWrn(source, options);
expect(formatted)
.toContain(`<pre><code><span class="code-muted">// One project, one language</span>
<span class="code-key">page</span> ProductDashboard {
<span class="code-key">state</span> count: number = 0`);
expect(formatWrn(formatted, options)).toBe(formatted);
expect(() => parse(formatted)).not.toThrow();
});
test("does not split an attribute-free opening tag delimiter", () => {
const source = `page Docs {
view {
<p>This is intentionally long documentation copy that should wrap as content without formatting the opening p tag as separate less-than and greater-than lines.</p>
}
}
`;
const formatted = formatWrn(source, options);
expect(formatted).toContain(" <p>\n");
expect(formatted).not.toContain(" <p\n >");
expect(formatWrn(formatted, options)).toBe(formatted);
});
test("preserves balanced compact sibling markup without indentation drift", () => {
const source = `page Compact {
view {
<div class="strip"><span>Page</span><b>→</b><span>API</span></div>
}
}\n`;
const formatted = formatWrn(source, { tabSize: 2, printWidth: 80 });
expect(formatted).toContain('<div class="strip"><span>Page</span><b>→</b><span>API</span></div>');
expect(formatWrn(formatted, { tabSize: 2, printWidth: 80 })).toBe(formatted);
});
+7
View File
@@ -200,3 +200,10 @@ test("parses edge and worker execution targets", () => {
);
}
});
test("maps literal unions to their runtime primitive types", async () => {
const { runtimeTypeOf } = await import("../src/types.ts");
expect(runtimeTypeOf('"_blank" | "_self"')).toBe("string");
expect(runtimeTypeOf("1 | 2 | 3")).toBe("number");
expect(runtimeTypeOf("true | false")).toBe("boolean");
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/test",
"version": "0.8.3",
"version": "0.8.4",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/tracking",
"version": "0.8.3",
"version": "0.8.4",
"private": true,
"type": "module",
"main": "src/index.ts",
+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([]);
});
+2 -2
View File
@@ -13,7 +13,7 @@ component AuthForm {
}
props {
size: string = "default"
size: string = "default"
color: string = "primary"
mode: string = "sign-in"
action: string = "/api/auth/login"
@@ -80,7 +80,7 @@ size: string = "default"
{/if}
{#if mode === "sign-in" && showRemember}
<div class="wire-auth-form__options">
<Checkbox label="Remember this device" name="rememberDevice" value="on" />
<Checkbox label="Remember this device" name="rememberDevice" value="on" />
<a href="/forgot-password">Forgot password?</a>
</div>
{/if}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ui",
"version": "0.8.3",
"version": "0.8.4",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/uploader",
"version": "0.8.3",
"version": "0.8.4",
"private": true,
"type": "module",
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/validation",
"version": "0.8.3",
"version": "0.8.4",
"private": true,
"type": "module",
"main": "./src/index.ts",