From 65d63dfbe7dbce641e50a89a4064446129388eb6 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Wed, 26 Aug 2026 18:10:58 +0530 Subject: [PATCH] fix(tooling): reconcile framework fixes in canonical WireJS checkout --- bun.lock | 6 +- editors/vscode/CHANGELOG.md | 6 + editors/vscode/bun.lock | 22 ++ editors/vscode/package.json | 2 +- editors/vscode/src/compiler.cjs | 65 ++++-- editors/vscode/src/extension.bundle.cjs | 192 +++++++----------- editors/vscode/src/language-server.cjs | 11 +- editors/vscode/test/formatter.test.js | 4 +- editors/vscode/test/language-server.test.js | 45 ++++ packages/auth/test/mfa-runtime.test.ts | 2 +- .../test/translation-bindings.test.ts | 26 +++ packages/syntax/package.json | 2 +- packages/syntax/src/formatter.ts | 4 +- packages/typecheck/README.md | 2 + packages/typecheck/package.json | 2 +- packages/typecheck/src/index.ts | 14 +- .../test/translation-bindings.test.ts | 59 ++++++ packages/ui/package.json | 2 +- packages/ui/src/navigation.ts | 2 +- 19 files changed, 312 insertions(+), 156 deletions(-) create mode 100644 packages/language-server/test/translation-bindings.test.ts create mode 100644 packages/typecheck/test/translation-bindings.test.ts diff --git a/bun.lock b/bun.lock index 169b95db..95a85fc5 100644 --- a/bun.lock +++ b/bun.lock @@ -668,7 +668,7 @@ }, "packages/syntax": { "name": "@wrnexus/syntax", - "version": "0.8.18", + "version": "0.8.19", }, "packages/test": { "name": "@wrnexus/test", @@ -680,7 +680,7 @@ }, "packages/typecheck": { "name": "@wrnexus/typecheck", - "version": "0.8.16", + "version": "0.8.17", "dependencies": { "@wrnexus/syntax": "workspace:*", "typescript": "^6.0.3", @@ -688,7 +688,7 @@ }, "packages/ui": { "name": "@wrnexus/ui", - "version": "0.8.61", + "version": "0.8.62", "dependencies": { "@wrnexus/core": "workspace:*", }, diff --git a/editors/vscode/CHANGELOG.md b/editors/vscode/CHANGELOG.md index bb785306..c175b75a 100644 --- a/editors/vscode/CHANGELOG.md +++ b/editors/vscode/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.8.12 + +- Fixed false TypeScript diagnostics for `{t:key}` translations in component props and native HTML attributes. Mixed translated/dynamic attributes still validate ordinary expressions. +- Fixed inline event handlers containing multiple statements, and infer `event.currentTarget` from the bound HTML/SVG element. +- Rebuilt the bundled language server from the shared framework typechecker; real unknown-name, invalid-property and nullable DOM diagnostics remain enabled. + ## 0.8.9 - Added highlighting for the page-level `apis { }` block and its ` ` entries, diff --git a/editors/vscode/bun.lock b/editors/vscode/bun.lock index f3c8233f..8b8868d2 100644 --- a/editors/vscode/bun.lock +++ b/editors/vscode/bun.lock @@ -4,6 +4,10 @@ "workspaces": { "": { "name": "wrnexus", + "dependencies": { + "vscode-html-languageservice": "^5.6.2", + "vscode-languageclient": "^10.1.0", + }, "devDependencies": { "@vscode/vsce": "^3.9.2", }, @@ -88,6 +92,8 @@ "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.6", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-jIXhD0eWQ1JA6ln/5Dltyx22UxWNrw0hZmhy2rlv6m6KgF7kplHx3g0fzi09lNmTJQRR91OlemYp3xFnvDK9og=="], + "@vscode/l10n": ["@vscode/l10n@0.0.18", "", {}, "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ=="], + "@vscode/vsce": ["@vscode/vsce@3.9.2", "", { "dependencies": { "@azure/identity": "^4.1.0", "@secretlint/node": "^10.1.2", "@secretlint/secretlint-formatter-sarif": "^10.1.2", "@secretlint/secretlint-rule-no-dotenv": "^10.1.2", "@secretlint/secretlint-rule-preset-recommend": "^10.1.2", "@vscode/vsce-sign": "^2.0.0", "azure-devops-node-api": "^12.5.0", "chalk": "^4.1.2", "cheerio": "^1.0.0-rc.9", "cockatiel": "^3.1.2", "commander": "^12.1.0", "form-data": "^4.0.0", "glob": "^13.0.6", "hosted-git-info": "^4.0.2", "jsonc-parser": "^3.2.0", "leven": "^3.1.0", "markdown-it": "^14.1.0", "mime": "^1.3.4", "minimatch": "^10.2.2", "parse-semver": "^1.1.1", "read": "^1.0.7", "secretlint": "^10.1.2", "semver": "^7.5.2", "tmp": "^0.2.3", "typed-rest-client": "^1.8.4", "url-join": "^4.0.1", "xml2js": "^0.5.0", "yauzl": "^3.2.1", "yazl": "^2.2.2" }, "optionalDependencies": { "keytar": "^7.7.0" }, "bin": { "vsce": "vsce" } }, "sha512-XSxMosEEDO6vLxELAHVkwmhC0qe0ijZni2jB9Rcs8kQsW4lhTDQ/wMzmwFs/buotAWSnpmUp/dRWD2ufG3UYKA=="], "@vscode/vsce-sign": ["@vscode/vsce-sign@2.0.9", "", { "optionalDependencies": { "@vscode/vsce-sign-alpine-arm64": "2.0.6", "@vscode/vsce-sign-alpine-x64": "2.0.6", "@vscode/vsce-sign-darwin-arm64": "2.0.6", "@vscode/vsce-sign-darwin-x64": "2.0.6", "@vscode/vsce-sign-linux-arm": "2.0.6", "@vscode/vsce-sign-linux-arm64": "2.0.6", "@vscode/vsce-sign-linux-x64": "2.0.6", "@vscode/vsce-sign-win32-arm64": "2.0.6", "@vscode/vsce-sign-win32-x64": "2.0.6" } }, "sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g=="], @@ -542,6 +548,20 @@ "version-range": ["version-range@4.15.0", "", {}, "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg=="], + "vscode-html-languageservice": ["vscode-html-languageservice@5.6.2", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "^3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-ulCrSnFnfQ16YzvwnYUgEbUEl/ZG7u2eV27YhvLObSHKkb8fw1Z9cgsnUwjTEeDIdJDoTDTDpxuhQwoenoLNMg=="], + + "vscode-jsonrpc": ["vscode-jsonrpc@9.0.1", "", {}, "sha512-rfuA6T75H6m5EkbhtEPzre9pT0HPcDI2MMy4+nPFIBks5J8JBAUHD4tRYSgaBOijIEC7SRkC1kKyXTLqbmh9jw=="], + + "vscode-languageclient": ["vscode-languageclient@10.1.0", "", { "dependencies": { "minimatch": "^10.2.5", "semver": "^7.8.1", "vscode-languageserver-protocol": "3.18.2", "vscode-languageserver-textdocument": "1.0.13" } }, "sha512-XXRx6lqVitQy/oOLr9MfNYRG+MbQkhXkDaxbQMiKxEm8zZNfheRFUKNb8UYNh2stn9btl2wQM5wZFJjJvoc+jA=="], + + "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.18.2", "", { "dependencies": { "vscode-jsonrpc": "9.0.1", "vscode-languageserver-types": "3.18.0" } }, "sha512-XRyDbT0Pp3sSNti3JmxVEUMySWCSi1hhM+/KUlCy1hV1zmrqpM1OwO12EAki8blhmLuIMpaJrYbo0OzGVfK2Qg=="], + + "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="], + + "vscode-languageserver-types": ["vscode-languageserver-types@3.18.0", "", {}, "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g=="], + + "vscode-uri": ["vscode-uri@3.2.0", "", {}, "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg=="], + "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], @@ -582,6 +602,8 @@ "table/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "vscode-languageclient/vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.13", "", {}, "sha512-nx0ZHwMGIsVkzFG3/VLeJYBLTaFBRuNdGDvevvjuoayU5EOS2fEYazOhtCM3PI9ClMMg5igc0uwXtAq4tJj+Dw=="], + "@textlint/linter-formatter/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "normalize-package-data/hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 71964391..d469e272 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -2,7 +2,7 @@ "name": "wrnexus", "displayName": "WRNexus Language Support", "description": "Complete WRNexus v0.8.3 language support for typed imports, props, state, outputs, runtime functions, stores, diagnostics, formatting, navigation, and migration assistance.", - "version": "0.8.11", + "version": "0.8.12", "publisher": "wrnexus", "private": true, "license": "SEE LICENSE IN LICENSE", diff --git a/editors/vscode/src/compiler.cjs b/editors/vscode/src/compiler.cjs index 1d61a9f7..f8e33f07 100644 --- a/editors/vscode/src/compiler.cjs +++ b/editors/vscode/src/compiler.cjs @@ -1,6 +1,6 @@ "use strict"; // Generated by scripts/build-editor-compiler.mjs. Do not edit directly. -// WRN editor compiler source hash: 3d346a264807ba7b107584dc9a7b505b1d14057b2ad1aa85c5ea890f9b09c272 +// WRN editor compiler source hash: 89199942be0196a3d06ea3d30dff80d361253192108bd978b5b80d921195c59f // WRN editor compiler generator hash: f8c1094b47e695151205899de28cd63d574356d26ac49219c4a43db0f2391ef5 // Generated with TypeScript: 6.0.3 const __nodeRequire = require; @@ -1202,10 +1202,16 @@ function renderAttrs(attrs, csrId, reactive = null, dynamicExpressions) { if (expression && exprRefsState(expression, reactive.runtimeStateNames) && dynamicExpressions) { + const marker = exprRefsState(expression, reactive.stateNames) + ? ` data-wrn-bind-${bindIndex++}="${attrEscape(JSON.stringify([attr.name, attr.value]))}"` + : ""; + if (isHtmlBooleanAttribute(attr.name)) { + dynamicExpressions.push(`\${(${expression}) ? ${JSON.stringify(` ${attr.name}`)} : ""}`); + return `\x00WRNEACH${dynamicExpressions.length - 1}\x00${marker}`; + } dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`); const sentinel = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`; - const marker = JSON.stringify([attr.name, attr.value]); - return ` ${attr.name}="${sentinel}" data-wrn-bind-${bindIndex++}="${attrEscape(marker)}"`; + return ` ${attr.name}="${sentinel}"${marker}`; } const initial = reactiveAttrValue(attr.value, reactive); if (initial === null) @@ -1253,17 +1259,20 @@ function evalStateSeeds(states) { */ function substituteReactiveText(raw, reactive, dynamicExpressions) { const text = substituteTMarkers(raw); - if (!reactive || reactive.stateNames.size === 0) + if (!reactive) return text; return text.replace(/\{([^{}]+)\}/g, (whole, inner) => { const expr = inner.trim(); - if (expr.startsWith("t:") || !exprRefsState(expr, reactive.stateNames)) + const clientReactive = exprRefsState(expr, reactive.stateNames); + if (expr.startsWith("t:")) return whole; if (exprRefsState(expr, reactive.runtimeStateNames) && dynamicExpressions) { dynamicExpressions.push(`\${__wrnexusEscapeHtml(${expr})}`); const sentinel = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`; - return `${sentinel}`; + return clientReactive ? `${sentinel}` : sentinel; } + if (!clientReactive) + return whole; let value; try { value = new Function("with(this){return (" + expr + ");}").call(reactive.scope); @@ -1339,6 +1348,18 @@ function renderLoopBody(node, locals = []) { if (attr.boolean) { return escLit(` ${name}`); } + // Native boolean attributes inside #if/#each must use presence semantics, + // just like attributes on the page root. disabled="false" still disables + // an HTML input. Component props retain their serialized boolean value. + if (!componentTag && + !node.attrs.some((a) => a.name === "data-component") && + isHtmlBooleanAttribute(name)) { + const expression = wholeAttributeExpression(attr.value); + if (expression) + return "${(" + expression + ") ? " + JSON.stringify(` ${name}`) + ' : ""}'; + if (attr.value === "false") + return ""; + } return escLit(` ${name}="`) + bakeLoopAttr(attr.value, componentTag) + escLit(`"`); }) .join(""); @@ -1458,6 +1479,16 @@ function renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive // templateEscape, swapped for its real `${…}` code after escaping. if (node.type === "each" || node.type === "if") { loops.push(node.type === "each" ? compileEachExpr(node) : compileIfExpr(node)); + const conditions = node.type === "each" + ? [node.list] + : node.branches.flatMap((branch) => (branch.cond ? [branch.cond] : [])); + // Server-only loader values are not serialized into browser state. Their + // controls must not be re-evaluated by a surrounding component's hydration. + if (reactive && + conditions.some((condition) => exprRefsState(condition, reactive.runtimeStateNames)) && + !conditions.some((condition) => exprRefsState(condition, reactive.stateNames))) { + return `\x00WRNEACH${loops.length - 1}\x00`; + } const definition = node.type === "each" ? { list: node.list, @@ -1621,7 +1652,7 @@ function renderNestedComponentInvocation(node, ctx) { const wholeExpression = wholeAttributeExpression(attr.value); const compiledValue = wholeExpression ? `\${__wrnProp(${ctx.resolveExpr(wholeExpression)})}` - : compileAttrValue(attr.value, ctx); + : compileAttrValue(attr.value, ctx, attr.name === "pattern"); const rendered = ` ${attr.name}="${compiledValue}"`; if (!attr.value.includes("{") || (!exprRefsState(attr.value, ctx.stateNames) && !exprRefsState(attr.value, ctx.propNames))) { @@ -2250,7 +2281,7 @@ function generateInner(ast) { .map((load) => load.name), ]; const runtimeStateNames = runtimeComputedNames(orderedStates, orderedComputed, runtimeRoots); - const reactive = reactiveNames.length > 0 + const reactive = reactiveNames.length > 0 || runtimeStateNames.size > 0 ? { stateNames: new Set(reactiveNames), runtimeStateNames, scope: seedScope } : null; const loops = []; @@ -2739,7 +2770,7 @@ function compileText(raw, ctx) { return out + escLit(raw.slice(last)); } /** Compile an attribute value; `{expr}` is baked server-side (loop vars stay literal). */ -function compileAttrValue(raw, ctx) { +function compileAttrValue(raw, ctx, regexPattern = false) { if (!raw.includes("{")) return escLit(attrEscape(raw)); let out = ""; @@ -2749,7 +2780,10 @@ function compileAttrValue(raw, ctx) { while ((m = INTERP_RE.exec(raw))) { out += escLit(attrEscape(raw.slice(last, m.index))); const expr = m[1].trim(); - if (ctx.loopVars && exprRefsState(expr, ctx.loopVars)) { + if (regexPattern && /^\d+(?:,\d*)?$/.test(expr)) { + out += escLit(attrEscape(m[0])); // Regex quantifiers are not template expressions. + } + else if (ctx.loopVars && exprRefsState(expr, ctx.loopVars)) { out += escLit(`{${expr}}`); // hydrated per-item by the list renderer } else { @@ -3047,7 +3081,7 @@ function renderComponentNode(node, ctx) { } const compiledValue = isExplicitComponentMount && wholeExpression ? `\${__wrnProp(${elementContext.resolveExpr(wholeExpression)})}` - : compileAttrValue(a.value, elementContext); + : compileAttrValue(a.value, elementContext, a.name === "pattern"); /* * A constraint attribute whose value is only known at render time (a * component prop defaulting to "") must be decided at render time, so @@ -3528,7 +3562,7 @@ function __wrnProp(v) { const value = v !== null && typeof v === "object" ? JSON.stringify(v) : String(v == null ? "" : v); return __wrnAttr(value); } -function renderPageComponentAttr(attr, dynamicExpressions, _reactive) { +function renderPageComponentAttr(attr, dynamicExpressions, reactive) { if (attr.event) { return ` ${componentEventAttribute(attr.name)}="${attrEscape(attr.value)}"`; } @@ -3541,6 +3575,11 @@ function renderPageComponentAttr(attr, dynamicExpressions, _reactive) { } dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`); const marker = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`; + if (reactive && + exprRefsState(expression, reactive.runtimeStateNames) && + !exprRefsState(expression, reactive.stateNames)) { + return ` ${attr.name}="${marker}"`; + } // The mount disappears during SSR. Preserve every dynamic component prop // as a parent-owned binding; server-only expressions simply fail closed in // the browser, while page state can continue driving the mounted child. @@ -6229,7 +6268,7 @@ function expandStructuredStateDeclarations(lines, unit) { }); } function formatStructuredPropDeclaration(value, unit, depth, printWidth) { - const match = /^([A-Za-z_$][\w$]*\??\s*(?::\s*[^=]+?)?\s*=\s*)([\[{][\s\S]*)$/.exec(value); + const match = /^([A-Za-z_$][\w$]*\??\s*(?::\s*[^=]+?)?\s*=\s*)([[{][\s\S]*)$/.exec(value); if (!match) return null; try { diff --git a/editors/vscode/src/extension.bundle.cjs b/editors/vscode/src/extension.bundle.cjs index d79cee7a..1cd8384d 100644 --- a/editors/vscode/src/extension.bundle.cjs +++ b/editors/vscode/src/extension.bundle.cjs @@ -1,4 +1,4 @@ -// WRN editor extension source hash: d07c535bd69332e90f207cbc254aae06982b40dd863fbc8de63fb386ecf31f50 +// WRN editor extension source hash: fd585db47099c8b781bf49e4ce7c86146bad583f781d0aa93196eb200a95b20f // WRN editor extension generator hash: 456d1d614e44e5fb1f19b784176c09cf2ade9b64ef73a17934c2698150b62728 "use strict"; var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports); @@ -10089,7 +10089,7 @@ var require_commonjs = __commonJS(function(exports2) { // editors/vscode/node_modules/brace-expansion/dist/commonjs/index.js var require_commonjs2 = __commonJS(function(exports2) { Object.defineProperty(exports2, "__esModule", { value: true }); - exports2.EXPANSION_MAX_LENGTH = exports2.EXPANSION_MAX = undefined; + exports2.EXPANSION_MAX = undefined; exports2.expand = expand; var balanced_match_1 = require_commonjs(); var escSlash = "\x00SLASH" + Math.random() + "\x00"; @@ -10108,7 +10108,6 @@ var require_commonjs2 = __commonJS(function(exports2) { var commaPattern = /\\,/g; var periodPattern = /\\\./g; exports2.EXPANSION_MAX = 1e5; - exports2.EXPANSION_MAX_LENGTH = 4000000; function numeric(str) { return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0); } @@ -10142,11 +10141,11 @@ var require_commonjs2 = __commonJS(function(exports2) { if (!str) { return []; } - const { max = exports2.EXPANSION_MAX, maxLength = exports2.EXPANSION_MAX_LENGTH } = options; + const { max = exports2.EXPANSION_MAX } = options; if (str.slice(0, 2) === "{}") { str = "\\{\\}" + str.slice(2); } - return expand_(escapeBraces(str), max, maxLength, true).map(unescapeBraces); + return expand_(escapeBraces(str), max, true).map(unescapeBraces); } function embrace(str) { return "{" + str + "}"; @@ -10160,87 +10159,20 @@ var require_commonjs2 = __commonJS(function(exports2) { function gte(i, y) { return i >= y; } - function combine(acc, pre, values, max, maxLength, dropEmpties) { - const out = []; - let length = 0; - for (let a = 0;a < acc.length; a++) { - for (let v = 0;v < values.length; v++) { - if (out.length >= max) - return out; - const expansion = acc[a] + pre + values[v]; - if (dropEmpties && !expansion) - continue; - if (length + expansion.length > maxLength) - return out; - out.push(expansion); - length += expansion.length; - } - } - return out; - } - function expandSequence(body, isAlphaSequence, max, maxLength) { - const n = body.split(/\.\./); - const N = []; - if (n[0] === undefined || n[1] === undefined) { - return N; - } - const x = numeric(n[0]); - const y = numeric(n[1]); - const width = Math.max(n[0].length, n[1].length); - let incr = n.length === 3 && n[2] !== undefined ? Math.max(Math.abs(numeric(n[2])), 1) : 1; - let test = lte; - const reverse = y < x; - if (reverse) { - incr *= -1; - test = gte; - } - const pad = n.some(isPadded); - let length = 0; - for (let i = x;test(i, y) && N.length < max; i += incr) { - let c; - if (isAlphaSequence) { - c = String.fromCharCode(i); - if (c === "\\") { - c = ""; - } - } else { - c = String(i); - if (pad) { - const need = width - c.length; - if (need > 0) { - const z = new Array(need + 1).join("0"); - if (i < 0) { - c = "-" + z + c.slice(1); - } else { - c = z + c; - } - } - } - } - if (length + c.length > maxLength) - break; - N.push(c); - length += c.length; - } - return N; - } - function expand_(str, max, maxLength, isTop) { - let acc = [""]; - let dropEmpties = false; - let firstGroup = true; + function expand_(str, max, isTop) { + const expansions = []; for (;; ) { const m = (0, balanced_match_1.balanced)("{", "}", str); - if (!m) { - return combine(acc, str, [""], max, maxLength, dropEmpties); - } + if (!m) + return [str]; const pre = m.pre; - if (/\$$/.test(pre)) { - acc = combine(acc, pre + "{" + m.body + "}", [""], max, maxLength, dropEmpties && !m.post.length); - firstGroup = false; - if (!m.post.length) - break; - str = m.post; - continue; + if (/\$$/.test(m.pre)) { + const post2 = m.post.length ? expand_(m.post, max, false) : [""]; + for (let k = 0;k < post2.length && k < max; k++) { + const expansion = pre + "{" + m.body + "}" + post2[k]; + expansions.push(expansion); + } + return expansions; } const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); @@ -10252,56 +10184,74 @@ var require_commonjs2 = __commonJS(function(exports2) { isTop = true; continue; } - return combine(acc, pre + "{" + m.body + "}" + m.post, [""], max, maxLength, dropEmpties); + return [str]; } - if (firstGroup) { - dropEmpties = isTop && !isSequence; - firstGroup = false; - } - let values; + const post = m.post.length ? expand_(m.post, max, false) : [""]; + let n; if (isSequence) { - values = expandSequence(m.body, isAlphaSequence, max, maxLength); + n = m.body.split(/\.\./); } else { - let n = parseCommaParts(m.body); + n = parseCommaParts(m.body); if (n.length === 1 && n[0] !== undefined) { - n = expand_(n[0], max, maxLength, false).map(embrace); + n = expand_(n[0], max, false).map(embrace); if (n.length === 1) { - acc = combine(acc, pre + n[0], [""], max, maxLength, dropEmpties && !m.post.length); - if (!m.post.length) - break; - str = m.post; - continue; + return post.map((p) => m.pre + n[0] + p); } } - let dropsEmpties = dropEmpties && !m.post.length && !pre; - for (let d = 0;dropsEmpties && d < acc.length; d++) { - if (acc[d]) { - dropsEmpties = false; - } + } + let N; + if (isSequence && n[0] !== undefined && n[1] !== undefined) { + const x = numeric(n[0]); + const y = numeric(n[1]); + const width = Math.max(n[0].length, n[1].length); + let incr = n.length === 3 && n[2] !== undefined ? Math.max(Math.abs(numeric(n[2])), 1) : 1; + let test = lte; + const reverse = y < x; + if (reverse) { + incr *= -1; + test = gte; } - values = []; - let valuesLength = 0; - outer: - for (let j = 0;j < n.length; j++) { - const expanded = expand_(n[j], max, maxLength, false); - for (let k = 0;k < expanded.length; k++) { - const v = expanded[k]; - if (dropsEmpties && !v) - continue; - if (values.length >= max || valuesLength + v.length > maxLength) { - break outer; + const pad = n.some(isPadded); + N = []; + for (let i = x;test(i, y) && N.length < max; i += incr) { + let c; + if (isAlphaSequence) { + c = String.fromCharCode(i); + if (c === "\\") { + c = ""; + } + } else { + c = String(i); + if (pad) { + const need = width - c.length; + if (need > 0) { + const z = new Array(need + 1).join("0"); + if (i < 0) { + c = "-" + z + c.slice(1); + } else { + c = z + c; + } } - values.push(v); - valuesLength += v.length; } } + N.push(c); + } + } else { + N = []; + for (let j = 0;j < n.length; j++) { + N.push.apply(N, expand_(n[j], max, false)); + } } - acc = combine(acc, pre, values, max, maxLength, dropEmpties && !m.post.length); - if (!m.post.length) - break; - str = m.post; + for (let j = 0;j < N.length; j++) { + for (let k = 0;k < post.length && expansions.length < max; k++) { + const expansion = pre + N[j] + post[k]; + if (!isTop || isSequence || expansion) { + expansions.push(expansion); + } + } + } + return expansions; } - return acc; } }); @@ -13933,7 +13883,7 @@ var require_configuration = __commonJS(function(exports2) { exports2.SyncConfigurationFeature = SyncConfigurationFeature; }); -// editors/vscode/node_modules/vscode-languageserver-textdocument/lib/umd/main.js +// editors/vscode/node_modules/vscode-languageclient/node_modules/vscode-languageserver-textdocument/lib/umd/main.js var require_main2 = __commonJS(function(exports2, module2) { var __spreadArray = exports2 && exports2.__spreadArray || function(to, from, pack) { if (pack || arguments.length === 2) diff --git a/editors/vscode/src/language-server.cjs b/editors/vscode/src/language-server.cjs index 18d82ac7..d87ac133 100644 --- a/editors/vscode/src/language-server.cjs +++ b/editors/vscode/src/language-server.cjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -// WRN editor language server source hash: 5258b3e76cec47df752d4bb886fbd57a3f3c4af9ca9b6b2bf6742c8d78025ff1 +// WRN editor language server source hash: d192eefe86b41eaa54eb48a69202842dbeb8ec4ee4e962c912b288820a65b162 // WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72 // @bun @bun-cjs (function(exports, require, module, __filename, __dirname) {var __create = Object.create; @@ -170723,7 +170723,7 @@ function expandStructuredStateDeclarations(lines, unit) { }); } function formatStructuredPropDeclaration(value, unit, depth, printWidth) { - const match = /^([A-Za-z_$][\w$]*\??\s*(?::\s*[^=]+?)?\s*=\s*)([\[{][\s\S]*)$/.exec(value); + const match = /^([A-Za-z_$][\w$]*\??\s*(?::\s*[^=]+?)?\s*=\s*)([[{][\s\S]*)$/.exec(value); if (!match) return null; try { @@ -173217,12 +173217,13 @@ ${sharedAliases}`); append(`declare const ${name}: typeof ${runtimeNamespace(runtime)}.${name};`); } let bindingIndex = 0; + append("type __WrnEventElement = 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))); const appendViewBindings = (nodes, locals) => { for (const node of nodes) { if (node.type === "element") { for (const attr of node.attrs) { - const expressions = attr.event ? [attr.value] : extractInterpolations(attr.value).map((segment) => segment.expression); + const expressions = attr.event ? [attr.value] : extractInterpolations(attr.value).filter((segment) => !/^t:[^{}]+$/.test(segment.expression)).map((segment) => segment.expression); for (const expression of expressions) { if (!expression.trim()) continue; @@ -173230,10 +173231,10 @@ ${sharedAliases}`); ...[...locals].map((name) => `declare const ${name}: any;`), ...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)}); }`, attr.value); + append(`namespace __wrn_view_${bindingIndex++} { ${declarations} ${attr.event ? `void (() => { ${safeViewExpression(expression, reservedProps)} });` : `void (${safeViewExpression(expression, reservedProps)});`} }`, attr.value); } } appendViewBindings(node.children, locals); diff --git a/editors/vscode/test/formatter.test.js b/editors/vscode/test/formatter.test.js index 2ea1b5d9..844c8e94 100644 --- a/editors/vscode/test/formatter.test.js +++ b/editors/vscode/test/formatter.test.js @@ -29,8 +29,8 @@ test("formats large typed JSON prop defaults with the bundled formatter", () => }`; const formatted = formatWrn(source, { insertSpaces: true, tabSize: 2, printWidth: 70 }); - assert.match(formatted, /categories: unknown\[\] = \[\n \{/); - assert.match(formatted, /"apps": \[\n \{/); + assert.match(formatted, /categories: unknown\[\] = \[\n {6}\{/); + assert.match(formatted, /"apps": \[\n {10}\{/); assert.equal(formatWrn(formatted, { insertSpaces: true, tabSize: 2, printWidth: 70 }), formatted); }); diff --git a/editors/vscode/test/language-server.test.js b/editors/vscode/test/language-server.test.js index 5b03ae8d..21d6b527 100644 --- a/editors/vscode/test/language-server.test.js +++ b/editors/vscode/test/language-server.test.js @@ -41,6 +41,51 @@ function run(messages) { return parseResponses(result.stdout || Buffer.alloc(0)); } +test("bundled language server accepts translated attributes and form statement handlers", () => { + const uri = "file:///tmp/wrnexus-translated-contact.wrn"; + const valid = `page Contact { + view { +
+
+ } + }`; + const responses = run([ + { jsonrpc: "2.0", id: 1, method: "initialize", params: { rootPath: root } }, + { jsonrpc: "2.0", method: "initialized", params: {} }, + { + jsonrpc: "2.0", + method: "textDocument/didOpen", + params: { textDocument: { uri, languageId: "wrn", version: 1, text: valid } }, + }, + { + jsonrpc: "2.0", + method: "textDocument/didChange", + params: { + textDocument: { uri, version: 2 }, + contentChanges: [ + { text: valid.replace("{t:contact.title}", "{t:contact.title} {missingValue}") }, + ], + }, + }, + { jsonrpc: "2.0", method: "textDocument/didSave", params: { textDocument: { uri } } }, + { jsonrpc: "2.0", id: 2, method: "shutdown", params: {} }, + { jsonrpc: "2.0", method: "exit", params: {} }, + ]); + const publications = responses.filter( + (item) => item.method === "textDocument/publishDiagnostics", + ); + assert.equal(publications.length, 2); + assert.deepEqual( + publications[0].params.diagnostics.filter((item) => item.source === "wrnexus-types"), + [], + ); + const errors = publications[1].params.diagnostics.filter( + (item) => item.source === "wrnexus-types", + ); + assert.equal(errors.length, 1); + assert.match(errors[0].message, /missingValue/); +}); + test("language server starts under Node and keeps official typed components clean", () => { const files = [ path.join(root, "packages", "ui", "components", "AuthForm.wrn"), diff --git a/packages/auth/test/mfa-runtime.test.ts b/packages/auth/test/mfa-runtime.test.ts index 95d9544b..df920cff 100644 --- a/packages/auth/test/mfa-runtime.test.ts +++ b/packages/auth/test/mfa-runtime.test.ts @@ -1,6 +1,6 @@ import { expect, test } from "bun:test"; import { readFileSync } from "node:fs"; -import { Window } from "happy-dom"; +import { Window, type HTMLInputElement, type HTMLOptionElement, type HTMLElement } from "happy-dom"; test("MFA uses the nested Select and restores continuation after hydration without reviving expired challenges", async () => { const window = new Window({ url: "https://sso.example.test/mfa" }); diff --git a/packages/language-server/test/translation-bindings.test.ts b/packages/language-server/test/translation-bindings.test.ts new file mode 100644 index 00000000..3d4f6eae --- /dev/null +++ b/packages/language-server/test/translation-bindings.test.ts @@ -0,0 +1,26 @@ +import { expect, test } from "bun:test"; +import { documentDiagnostics, virtualTypeScriptDocument } from "../src/index.ts"; + +test("editor diagnostics accept translated PageHeader attributes without hiding real errors", () => { + const document = { + uri: "file:///contact.wrn", + text: `page Contact { + view { +
+

+ } + }`, + }; + const diagnostics = documentDiagnostics(document).filter( + (item) => item.source === "wrnexus-types", + ); + expect(diagnostics).toHaveLength(1); + expect(diagnostics[0]?.message).toContain("missingValue"); + expect(diagnostics[0]?.range.start.line).toBe(3); + expect(virtualTypeScriptDocument(document).text).not.toContain("void (t:"); + expect( + documentDiagnostics({ ...document, text: document.text.replace("{missingValue}", "") }).filter( + (item) => item.source === "wrnexus-types", + ), + ).toEqual([]); +}); diff --git a/packages/syntax/package.json b/packages/syntax/package.json index 9f9bbd20..05e57855 100644 --- a/packages/syntax/package.json +++ b/packages/syntax/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/syntax", - "version": "0.8.18", + "version": "0.8.19", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/syntax/src/formatter.ts b/packages/syntax/src/formatter.ts index cba4bb58..4d874759 100644 --- a/packages/syntax/src/formatter.ts +++ b/packages/syntax/src/formatter.ts @@ -768,9 +768,7 @@ function expandStructuredStateDeclarations(lines, unit) { } function formatStructuredPropDeclaration(value, unit, depth, printWidth) { - const match = /^([A-Za-z_$][\w$]*\??\s*(?::\s*[^=]+?)?\s*=\s*)([\[{][\s\S]*)$/.exec( - value, - ); + const match = /^([A-Za-z_$][\w$]*\??\s*(?::\s*[^=]+?)?\s*=\s*)([[{][\s\S]*)$/.exec(value); if (!match) return null; try { diff --git a/packages/typecheck/README.md b/packages/typecheck/README.md index bae62bfe..dac9481a 100644 --- a/packages/typecheck/README.md +++ b/packages/typecheck/README.md @@ -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. diff --git a/packages/typecheck/package.json b/packages/typecheck/package.json index 848dcdf4..9cd54f76 100644 --- a/packages/typecheck/package.json +++ b/packages/typecheck/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/typecheck", - "version": "0.8.16", + "version": "0.8.17", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/typecheck/src/index.ts b/packages/typecheck/src/index.ts index c7acaa7d..691fcdaf 100644 --- a/packages/typecheck/src/index.ts +++ b/packages/typecheck/src/index.ts @@ -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 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, ); } diff --git a/packages/typecheck/test/translation-bindings.test.ts b/packages/typecheck/test/translation-bindings.test.ts new file mode 100644 index 00000000..092d88e5 --- /dev/null +++ b/packages/typecheck/test/translation-bindings.test.ts @@ -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 { +
+ +
{t:contact.title}
+ + } + }`; + 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 { +
+
+
+ } + }`; + 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 { } }`, + ); + 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 { +
+ + + } + }`; + 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); +}); diff --git a/packages/ui/package.json b/packages/ui/package.json index ef7feacc..2e778586 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.8.61", + "version": "0.8.62", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/ui/src/navigation.ts b/packages/ui/src/navigation.ts index fb225ab2..7d097710 100644 --- a/packages/ui/src/navigation.ts +++ b/packages/ui/src/navigation.ts @@ -62,7 +62,7 @@ export interface MegaMenuColumn { actionHref?: string; } -export interface MegaMenuRailItem extends NavigationLink {} +export type MegaMenuRailItem = NavigationLink; export interface MegaMenuFeatured { eyebrow?: string;