From 72e4d3eceb420d70af16ac21e5209e96626f81e1 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Tue, 4 Aug 2026 12:19:09 +0530 Subject: [PATCH] release: WRNexusJS 0.8.0 --- .publish/ai/package.json | 2 +- .publish/authz/package.json | 2 +- .publish/cli/package.json | 34 +- .publish/compiler/package.json | 10 +- .publish/core/package.json | 2 +- .publish/csr/package.json | 4 +- .publish/db/package.json | 2 +- .publish/dev-server/package.json | 40 +- .publish/dev-toolbar/package.json | 2 +- .publish/encryption/package.json | 4 +- .publish/helpers/package.json | 4 +- .publish/i18n/package.json | 8 +- .publish/jwt/package.json | 4 +- .publish/mobile/package.json | 4 +- .publish/native/package.json | 2 +- .publish/oauth/package.json | 4 +- .publish/plugin/package.json | 4 +- .publish/pubsub/package.json | 2 +- .publish/queue/package.json | 4 +- .publish/reactive/package.json | 2 +- .publish/router/package.json | 6 +- .publish/ssr/package.json | 8 +- .publish/styles/package.json | 8 +- .publish/syntax/package.json | 2 +- .publish/test/package.json | 2 +- .publish/tracking/package.json | 2 +- .publish/ui/package.json | 4 +- .publish/uploader/package.json | 8 +- .publish/validation/package.json | 8 +- CHANGELOG.md | 18 + WRNexusJS-0.8.4-release-version-fix.patch | 64 +++ bun.lock | 92 ++--- docs/VALIDATION-0.8.3.md | 55 +++ docs/ui-visual-contract-0.8.json | 2 +- editors/vscode/CHANGELOG.md | 15 + editors/vscode/package-lock.json | 4 +- editors/vscode/package.json | 10 +- editors/vscode/src/compiler.cjs | 146 ++++++- editors/vscode/src/component-metadata.js | 68 +++- editors/vscode/src/diagnostics.js | 271 +++++++------ editors/vscode/src/extension.bundle.cjs | 378 +++++++++++++----- editors/vscode/src/extension.js | 5 +- editors/vscode/src/language-server.cjs | 311 ++++++++++++-- editors/vscode/syntaxes/wrn.tmLanguage.json | 27 +- .../vscode/test/component-metadata.test.js | 60 +++ editors/vscode/test/diagnostics.test.js | 33 ++ editors/vscode/test/formatter.test.js | 45 +++ editors/vscode/test/language-server.test.js | 141 +++++++ package.json | 2 +- packages/ai/package.json | 2 +- packages/auth/package.json | 2 +- packages/authz/package.json | 2 +- packages/benchmark/package.json | 2 +- packages/cache/package.json | 2 +- packages/captcha/package.json | 2 +- packages/cli/package.json | 2 +- packages/cli/src/update.ts | 14 + packages/compiler/package.json | 2 +- packages/content/package.json | 2 +- packages/core/package.json | 2 +- packages/csr/package.json | 2 +- packages/db/package.json | 2 +- packages/dev-server/package.json | 2 +- packages/dev-toolbar/package.json | 2 +- packages/encryption/package.json | 2 +- packages/graphql/package.json | 2 +- packages/helpers/package.json | 2 +- packages/i18n/package.json | 2 +- packages/identity/package.json | 2 +- packages/image/package.json | 2 +- packages/jwt/package.json | 2 +- packages/language-server/package.json | 2 +- packages/language-server/src/server.ts | 59 ++- packages/mcp/package.json | 2 +- packages/mobile/package.json | 2 +- packages/native/package.json | 2 +- packages/oauth/package.json | 2 +- packages/observability/package.json | 2 +- packages/playground/package.json | 2 +- packages/plugin/package.json | 2 +- packages/pubsub/package.json | 2 +- packages/pwa/package.json | 2 +- packages/queue/package.json | 2 +- packages/reactive/package.json | 2 +- packages/realtime/package.json | 2 +- packages/router/package.json | 2 +- packages/security/package.json | 2 +- packages/ssr/package.json | 2 +- packages/store/package.json | 2 +- packages/styles/package.json | 2 +- packages/syntax/package.json | 2 +- packages/syntax/src/formatter.ts | 143 ++++++- packages/syntax/src/types.ts | 21 +- packages/syntax/test/formatter.test.ts | 46 +++ packages/syntax/test/syntax.test.ts | 7 + packages/test/package.json | 2 +- packages/tracking/package.json | 2 +- packages/typecheck/package.json | 2 +- packages/typecheck/src/index.ts | 158 ++++++-- .../test/imports-and-components.test.ts | 23 ++ packages/typecheck/test/typecheck.test.ts | 18 + packages/ui/components/AuthForm.wrn | 4 +- packages/ui/package.json | 2 +- packages/uploader/package.json | 2 +- packages/validation/package.json | 2 +- scripts/build-editor-extension.mjs | 132 +++++- scripts/build-editor-language-server.mjs | 154 ++++++- update-package-versions.mjs | 2 +- 108 files changed, 2276 insertions(+), 532 deletions(-) create mode 100644 WRNexusJS-0.8.4-release-version-fix.patch create mode 100644 editors/vscode/test/language-server.test.js diff --git a/.publish/ai/package.json b/.publish/ai/package.json index 2a5e2acc..d44e29c7 100644 --- a/.publish/ai/package.json +++ b/.publish/ai/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ai", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", "license": "MIT", diff --git a/.publish/authz/package.json b/.publish/authz/package.json index 3491ed49..4bf8e00d 100644 --- a/.publish/authz/package.json +++ b/.publish/authz/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/authz", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "@wrnexus/authz — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/cli/package.json b/.publish/cli/package.json index 37d49559..b1038c88 100644 --- a/.publish/cli/package.json +++ b/.publish/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "@wrnexus/cli — part of the WrNexus framework.", "license": "MIT", @@ -44,22 +44,22 @@ "wrnexus": "./dist/index.js" }, "dependencies": { - "@wrnexus/core": "^0.8.3", - "@wrnexus/router": "^0.8.3", - "@wrnexus/csr": "^0.8.3", - "@wrnexus/compiler": "^0.8.3", - "@wrnexus/styles": "^0.8.3", - "@wrnexus/dev-server": "^0.8.3", - "@wrnexus/ui": "^0.8.3", - "@wrnexus/validation": "^0.8.3", - "@wrnexus/i18n": "^0.8.3", - "@wrnexus/mcp": "^0.8.3", - "@wrnexus/playground": "^0.8.3", - "@wrnexus/db": "^0.8.3", - "@wrnexus/plugin": "^0.8.3", - "@wrnexus/syntax": "^0.8.3", - "@wrnexus/typecheck": "^0.8.3", - "@wrnexus/security": "^0.8.3", + "@wrnexus/core": "^0.8.4", + "@wrnexus/router": "^0.8.4", + "@wrnexus/csr": "^0.8.4", + "@wrnexus/compiler": "^0.8.4", + "@wrnexus/styles": "^0.8.4", + "@wrnexus/dev-server": "^0.8.4", + "@wrnexus/ui": "^0.8.4", + "@wrnexus/validation": "^0.8.4", + "@wrnexus/i18n": "^0.8.4", + "@wrnexus/mcp": "^0.8.4", + "@wrnexus/playground": "^0.8.4", + "@wrnexus/db": "^0.8.4", + "@wrnexus/plugin": "^0.8.4", + "@wrnexus/syntax": "^0.8.4", + "@wrnexus/typecheck": "^0.8.4", + "@wrnexus/security": "^0.8.4", "selfsigned": "^5.5.0" }, "files": [ diff --git a/.publish/compiler/package.json b/.publish/compiler/package.json index 1944428f..0051dc95 100644 --- a/.publish/compiler/package.json +++ b/.publish/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "@wrnexus/compiler — part of the WrNexus framework.", "license": "MIT", @@ -37,10 +37,10 @@ } }, "dependencies": { - "@wrnexus/csr": "^0.8.3", - "@wrnexus/syntax": "^0.8.3", - "@wrnexus/store": "^0.8.3", - "@wrnexus/validation": "^0.8.3" + "@wrnexus/csr": "^0.8.4", + "@wrnexus/syntax": "^0.8.4", + "@wrnexus/store": "^0.8.4", + "@wrnexus/validation": "^0.8.4" }, "files": [ "dist", diff --git a/.publish/core/package.json b/.publish/core/package.json index e134f1f1..497d1d54 100644 --- a/.publish/core/package.json +++ b/.publish/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "@wrnexus/core — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/csr/package.json b/.publish/csr/package.json index ff7d6751..86d40d03 100644 --- a/.publish/csr/package.json +++ b/.publish/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "@wrnexus/csr — part of the WrNexus framework.", "license": "MIT", @@ -37,7 +37,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.8.3" + "@wrnexus/core": "^0.8.4" }, "files": [ "dist", diff --git a/.publish/db/package.json b/.publish/db/package.json index 59a36036..c1f51571 100644 --- a/.publish/db/package.json +++ b/.publish/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "Typed database drivers, migrations, instrumentation, repositories, pagination, and transaction helpers.", "license": "MIT", diff --git a/.publish/dev-server/package.json b/.publish/dev-server/package.json index ce626c15..8c4d4374 100644 --- a/.publish/dev-server/package.json +++ b/.publish/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "@wrnexus/dev-server — part of the WrNexus framework.", "license": "MIT", @@ -41,25 +41,25 @@ } }, "dependencies": { - "@wrnexus/core": "^0.8.3", - "@wrnexus/dev-toolbar": "^0.8.3", - "@wrnexus/router": "^0.8.3", - "@wrnexus/ssr": "^0.8.3", - "@wrnexus/csr": "^0.8.3", - "@wrnexus/compiler": "^0.8.3", - "@wrnexus/styles": "^0.8.3", - "@wrnexus/ui": "^0.8.3", - "@wrnexus/validation": "^0.8.3", - "@wrnexus/i18n": "^0.8.3", - "@wrnexus/db": "^0.8.3", - "@wrnexus/pubsub": "^0.8.3", - "@wrnexus/uploader": "^0.8.3", - "@wrnexus/plugin": "^0.8.3", - "@wrnexus/store": "^0.8.3", - "@wrnexus/security": "^0.8.3", - "@wrnexus/observability": "^0.8.3", - "@wrnexus/cache": "^0.8.3", - "@wrnexus/pwa": "^0.8.3" + "@wrnexus/core": "^0.8.4", + "@wrnexus/dev-toolbar": "^0.8.4", + "@wrnexus/router": "^0.8.4", + "@wrnexus/ssr": "^0.8.4", + "@wrnexus/csr": "^0.8.4", + "@wrnexus/compiler": "^0.8.4", + "@wrnexus/styles": "^0.8.4", + "@wrnexus/ui": "^0.8.4", + "@wrnexus/validation": "^0.8.4", + "@wrnexus/i18n": "^0.8.4", + "@wrnexus/db": "^0.8.4", + "@wrnexus/pubsub": "^0.8.4", + "@wrnexus/uploader": "^0.8.4", + "@wrnexus/plugin": "^0.8.4", + "@wrnexus/store": "^0.8.4", + "@wrnexus/security": "^0.8.4", + "@wrnexus/observability": "^0.8.4", + "@wrnexus/cache": "^0.8.4", + "@wrnexus/pwa": "^0.8.4" }, "files": [ "dist", diff --git a/.publish/dev-toolbar/package.json b/.publish/dev-toolbar/package.json index 3ebfd611..babace46 100644 --- a/.publish/dev-toolbar/package.json +++ b/.publish/dev-toolbar/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-toolbar", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "@wrnexus/dev-toolbar — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/encryption/package.json b/.publish/encryption/package.json index 1adf5c4f..03c619cb 100644 --- a/.publish/encryption/package.json +++ b/.publish/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "Authenticated encryption, key rotation, hashing, and optional application-layer encrypted HTTP envelopes.", "license": "MIT", @@ -37,7 +37,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.8.3" + "@wrnexus/core": "^0.8.4" }, "files": [ "dist", diff --git a/.publish/helpers/package.json b/.publish/helpers/package.json index ce5e810e..527d37d7 100644 --- a/.publish/helpers/package.json +++ b/.publish/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", "license": "MIT", @@ -37,7 +37,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.8.3" + "@wrnexus/core": "^0.8.4" }, "files": [ "dist", diff --git a/.publish/i18n/package.json b/.publish/i18n/package.json index 880a9579..ff7a6fe8 100644 --- a/.publish/i18n/package.json +++ b/.publish/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "Locale loading, fallback resolution, SSR/browser translations, formatters, and WRNexusJS language components.", "license": "MIT", @@ -42,9 +42,9 @@ "./components/*": "./components/*" }, "dependencies": { - "@wrnexus/core": "^0.8.3", - "@wrnexus/plugin": "^0.8.3", - "@wrnexus/ui": "^0.8.3" + "@wrnexus/core": "^0.8.4", + "@wrnexus/plugin": "^0.8.4", + "@wrnexus/ui": "^0.8.4" }, "wrnexus": { "plugin": { diff --git a/.publish/jwt/package.json b/.publish/jwt/package.json index 4a802485..98c824ad 100644 --- a/.publish/jwt/package.json +++ b/.publish/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "HS256 JSON Web Tokens, key rotation, access/refresh helpers, scopes, cookies, and auth middleware.", "license": "MIT", @@ -37,7 +37,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.8.3" + "@wrnexus/core": "^0.8.4" }, "files": [ "dist", diff --git a/.publish/mobile/package.json b/.publish/mobile/package.json index 13842f56..3eec6352 100644 --- a/.publish/mobile/package.json +++ b/.publish/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "@wrnexus/mobile — part of the WrNexus framework.", "license": "MIT", @@ -37,7 +37,7 @@ } }, "dependencies": { - "@wrnexus/native": "^0.8.3" + "@wrnexus/native": "^0.8.4" }, "files": [ "dist", diff --git a/.publish/native/package.json b/.publish/native/package.json index 36e1447a..ecae31cf 100644 --- a/.publish/native/package.json +++ b/.publish/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "@wrnexus/native — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/oauth/package.json b/.publish/oauth/package.json index 505b78eb..7800062a 100644 --- a/.publish/oauth/package.json +++ b/.publish/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "@wrnexus/oauth — part of the WrNexus framework.", "license": "MIT", @@ -37,7 +37,7 @@ } }, "dependencies": { - "@wrnexus/jwt": "^0.8.3" + "@wrnexus/jwt": "^0.8.4" }, "files": [ "dist", diff --git a/.publish/plugin/package.json b/.publish/plugin/package.json index 07f4f81a..d6222afc 100644 --- a/.publish/plugin/package.json +++ b/.publish/plugin/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/plugin", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "@wrnexus/plugin — part of the WrNexus framework.", "license": "MIT", @@ -49,7 +49,7 @@ } }, "dependencies": { - "@wrnexus/syntax": "^0.8.3" + "@wrnexus/syntax": "^0.8.4" }, "files": [ "dist", diff --git a/.publish/pubsub/package.json b/.publish/pubsub/package.json index ae77b1de..b5d7bd92 100644 --- a/.publish/pubsub/package.json +++ b/.publish/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "@wrnexus/pubsub — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/queue/package.json b/.publish/queue/package.json index 503b120d..b00444b6 100644 --- a/.publish/queue/package.json +++ b/.publish/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "@wrnexus/queue — part of the WrNexus framework.", "license": "MIT", @@ -37,7 +37,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.8.3" + "@wrnexus/core": "^0.8.4" }, "files": [ "dist", diff --git a/.publish/reactive/package.json b/.publish/reactive/package.json index 6c0073a2..c919bcb8 100644 --- a/.publish/reactive/package.json +++ b/.publish/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "@wrnexus/reactive — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/router/package.json b/.publish/router/package.json index dd042f51..85b2e4c0 100644 --- a/.publish/router/package.json +++ b/.publish/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "@wrnexus/router — part of the WrNexus framework.", "license": "MIT", @@ -37,8 +37,8 @@ } }, "dependencies": { - "@wrnexus/compiler": "^0.8.3", - "@wrnexus/core": "^0.8.3" + "@wrnexus/compiler": "^0.8.4", + "@wrnexus/core": "^0.8.4" }, "files": [ "dist", diff --git a/.publish/ssr/package.json b/.publish/ssr/package.json index ca19dc15..072db827 100644 --- a/.publish/ssr/package.json +++ b/.publish/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "@wrnexus/ssr — part of the WrNexus framework.", "license": "MIT", @@ -45,9 +45,9 @@ } }, "dependencies": { - "@wrnexus/core": "^0.8.3", - "@wrnexus/store": "^0.8.3", - "@wrnexus/security": "^0.8.3" + "@wrnexus/core": "^0.8.4", + "@wrnexus/store": "^0.8.4", + "@wrnexus/security": "^0.8.4" }, "files": [ "dist", diff --git a/.publish/styles/package.json b/.publish/styles/package.json index 31cd472b..f535d396 100644 --- a/.publish/styles/package.json +++ b/.publish/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "@wrnexus/styles — part of the WrNexus framework.", "license": "MIT", @@ -37,9 +37,9 @@ } }, "dependencies": { - "@wrnexus/uploader": "^0.8.3", - "@wrnexus/core": "^0.8.3", - "@wrnexus/plugin": "^0.8.3" + "@wrnexus/uploader": "^0.8.4", + "@wrnexus/core": "^0.8.4", + "@wrnexus/plugin": "^0.8.4" }, "files": [ "dist", diff --git a/.publish/syntax/package.json b/.publish/syntax/package.json index fd7d00b4..ff45af91 100644 --- a/.publish/syntax/package.json +++ b/.publish/syntax/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/syntax", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "@wrnexus/syntax — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/test/package.json b/.publish/test/package.json index 22bec8b2..9e8a1442 100644 --- a/.publish/test/package.json +++ b/.publish/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "@wrnexus/test — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/tracking/package.json b/.publish/tracking/package.json index 0ad122ab..f9ed7611 100644 --- a/.publish/tracking/package.json +++ b/.publish/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "@wrnexus/tracking — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/ui/package.json b/.publish/ui/package.json index 1fec4c6e..b10ea70d 100644 --- a/.publish/ui/package.json +++ b/.publish/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "@wrnexus/ui — part of the WrNexus framework.", "license": "MIT", @@ -42,7 +42,7 @@ "./ui.css": "./ui.css" }, "dependencies": { - "@wrnexus/core": "^0.8.3" + "@wrnexus/core": "^0.8.4" }, "files": [ "dist", diff --git a/.publish/uploader/package.json b/.publish/uploader/package.json index 28cea6a7..ea309ab2 100644 --- a/.publish/uploader/package.json +++ b/.publish/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "Secure upload drivers, policies, client runtime, helper functions, and reusable upload components.", "license": "MIT", @@ -42,9 +42,9 @@ "./components/*": "./components/*" }, "dependencies": { - "@wrnexus/core": "^0.8.3", - "@wrnexus/plugin": "^0.8.3", - "@wrnexus/ui": "^0.8.3" + "@wrnexus/core": "^0.8.4", + "@wrnexus/plugin": "^0.8.4", + "@wrnexus/ui": "^0.8.4" }, "wrnexus": { "plugin": { diff --git a/.publish/validation/package.json b/.publish/validation/package.json index b2f64839..49d04af8 100644 --- a/.publish/validation/package.json +++ b/.publish/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "description": "Shared server/browser schemas, validation helpers, form runtime, and reusable error components.", "license": "MIT", @@ -42,9 +42,9 @@ "./components/*": "./components/*" }, "dependencies": { - "@wrnexus/core": "^0.8.3", - "@wrnexus/plugin": "^0.8.3", - "@wrnexus/ui": "^0.8.3" + "@wrnexus/core": "^0.8.4", + "@wrnexus/plugin": "^0.8.4", + "@wrnexus/ui": "^0.8.4" }, "wrnexus": { "plugin": { diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fb593c6..bd03958c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,24 @@ entry in the CLI migration registry. - Made the VS Code embedded compiler freshness check deterministic across TypeScript patch versions, Windows/Linux line endings, and workspace environments by verifying normalized source and generator fingerprints while retaining exact-output checks for the same TypeScript version. +- Fixed the VS Code language-server bundle so it executes under Node instead of exiting after + defining an uninvoked Bun CommonJS wrapper, and added request-level crash containment. +- Scoped HTML diagnostics to `view` blocks, ignored TypeScript generic syntax and WRN expressions, + and preserved JavaScript-looking documentation inside `
` examples.
+- Fixed formatter corruption of preformatted examples, balanced compact sibling markup, long bare
+  tags, and repeated format-on-save indentation drift.
+- Fixed component prop intelligence for dynamic boolean/union expressions, boolean string literals,
+  literal-union runtime types, and reserved prop names such as `class`.
+- Prevented bundled editor type checking from publishing TypeScript standard-library path failures or
+  unmapped synthetic virtual-document diagnostics.
+- Fixed language-server virtual TypeScript inference for untyped dynamic handler parameters and
+  indexed output dispatch, while retaining strict diagnostics for explicitly typed parameters.
+- Made bundled editor type checking resolve TypeScript standard libraries from the active workspace,
+  and added a regression test that proves semantic diagnostics are actually running.
+- Fixed final release-gate lint failures by removing the obsolete editor `stripComments` helper and
+  importing Node `Buffer` explicitly in the language-server bundle generator.
+- Reviewed the UI visual-contract change caused only by corrected `AuthForm.wrn` indentation and
+  regenerated the 0.8 baseline after confirming no rendered component behavior changed.
 
 ## 0.8.0 - 2026-08-02
 
diff --git a/WRNexusJS-0.8.4-release-version-fix.patch b/WRNexusJS-0.8.4-release-version-fix.patch
new file mode 100644
index 00000000..358bc1ae
--- /dev/null
+++ b/WRNexusJS-0.8.4-release-version-fix.patch
@@ -0,0 +1,64 @@
+diff --git a/package.json b/package.json
+index f7c6a5f..6ac4583 100644
+--- a/package.json
++++ b/package.json
+@@ -1,6 +1,6 @@
+ {
+   "name": "wrnexus",
+-  "version": "0.8.3",
++  "version": "0.8.4",
+   "private": true,
+   "type": "module",
+   "description": "An SSR-first full-stack web framework with server-rendered reactive components. Bun-first, Node-friendly.",
+diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts
+index c27baa7..974d966 100644
+--- a/packages/language-server/src/server.ts
++++ b/packages/language-server/src/server.ts
+@@ -102,7 +102,7 @@ async function handle(message: JsonRpc): Promise {
+     case "initialize":
+       workspaceRoot = params.rootPath ?? rootFromUri(params.rootUri);
+       result(message.id, {
+-        serverInfo: { name: "WRNexus Language Server", version: "0.8.3" },
++        serverInfo: { name: "WRNexus Language Server", version: "0.8.4" },
+         capabilities: {
+           textDocumentSync: 1,
+           documentFormattingProvider: true,
+diff --git a/update-package-versions.mjs b/update-package-versions.mjs
+index 318b962..8b70170 100644
+--- a/update-package-versions.mjs
++++ b/update-package-versions.mjs
+@@ -1,7 +1,17 @@
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
+ import { join } from "node:path";
++import process from "node:process";
+ 
+-const version = "0.8.3";
++const version = process.argv[2] ?? "0.8.4";
++
++if (!/^0\.8\.\d+$/.test(version)) {
++  throw new Error(`Expected a 0.8.x version, received ${JSON.stringify(version)}`);
++}
++
++const rootPackageFile = "package.json";
++const rootPackage = JSON.parse(readFileSync(rootPackageFile, "utf8"));
++rootPackage.version = version;
++writeFileSync(rootPackageFile, `${JSON.stringify(rootPackage, null, 2)}\n`);
+ 
+ const dependencyGroups = [
+   "dependencies",
+@@ -62,6 +72,15 @@ for (const entry of readdirSync("packages", {
+   writeFileSync(packageFile, `${JSON.stringify(packageJson, null, 2)}\n`);
+ }
+ 
++const languageServerFile = join("packages", "language-server", "src", "server.ts");
++if (existsSync(languageServerFile)) {
++  const source = readFileSync(languageServerFile, "utf8").replace(
++    /serverInfo: \{ name: "WRNexus Language Server", version: "[^"]+" \}/,
++    `serverInfo: { name: "WRNexus Language Server", version: "${version}" }`,
++  );
++  writeFileSync(languageServerFile, source);
++}
++
+ const editorPackageFile = join("editors", "vscode", "package.json");
+ if (existsSync(editorPackageFile)) {
+   const editorPackage = JSON.parse(readFileSync(editorPackageFile, "utf8"));
diff --git a/bun.lock b/bun.lock
index 30dabf36..e33141a8 100644
--- a/bun.lock
+++ b/bun.lock
@@ -102,11 +102,11 @@
     },
     "packages/ai": {
       "name": "@wrnexus/ai",
-      "version": "0.8.3",
+      "version": "0.8.4",
     },
     "packages/auth": {
       "name": "@wrnexus/auth",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/authz": "workspace:*",
         "@wrnexus/captcha": "workspace:*",
@@ -128,22 +128,22 @@
     },
     "packages/authz": {
       "name": "@wrnexus/authz",
-      "version": "0.8.3",
+      "version": "0.8.4",
     },
     "packages/benchmark": {
       "name": "@wrnexus/benchmark",
-      "version": "0.8.3",
+      "version": "0.8.4",
     },
     "packages/cache": {
       "name": "@wrnexus/cache",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/core": "workspace:*",
       },
     },
     "packages/captcha": {
       "name": "@wrnexus/captcha",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/core": "workspace:*",
         "@wrnexus/plugin": "workspace:*",
@@ -158,7 +158,7 @@
     },
     "packages/cli": {
       "name": "@wrnexus/cli",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "bin": {
         "wrnexus": "src/index.ts",
       },
@@ -184,7 +184,7 @@
     },
     "packages/compiler": {
       "name": "@wrnexus/compiler",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/csr": "workspace:*",
         "@wrnexus/store": "workspace:*",
@@ -194,7 +194,7 @@
     },
     "packages/content": {
       "name": "@wrnexus/content",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "devDependencies": {
         "@types/bun": "^1.3.14",
         "typescript": "^5.9.2",
@@ -202,18 +202,18 @@
     },
     "packages/core": {
       "name": "@wrnexus/core",
-      "version": "0.8.3",
+      "version": "0.8.4",
     },
     "packages/csr": {
       "name": "@wrnexus/csr",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/core": "workspace:*",
       },
     },
     "packages/db": {
       "name": "@wrnexus/db",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "devDependencies": {
         "@types/bun": "^1.3.14",
         "typescript": "^5.9.2",
@@ -221,7 +221,7 @@
     },
     "packages/dev-server": {
       "name": "@wrnexus/dev-server",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/cache": "workspace:*",
         "@wrnexus/compiler": "workspace:*",
@@ -246,7 +246,7 @@
     },
     "packages/dev-toolbar": {
       "name": "@wrnexus/dev-toolbar",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "devDependencies": {
         "@types/bun": "^1.3.14",
         "typescript": "^5.9.2",
@@ -254,7 +254,7 @@
     },
     "packages/encryption": {
       "name": "@wrnexus/encryption",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/core": "workspace:*",
       },
@@ -265,21 +265,21 @@
     },
     "packages/graphql": {
       "name": "@wrnexus/graphql",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/plugin": "workspace:*",
       },
     },
     "packages/helpers": {
       "name": "@wrnexus/helpers",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/core": "workspace:*",
       },
     },
     "packages/i18n": {
       "name": "@wrnexus/i18n",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/core": "workspace:*",
         "@wrnexus/plugin": "workspace:*",
@@ -293,7 +293,7 @@
     },
     "packages/identity": {
       "name": "@wrnexus/identity",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/auth": "workspace:*",
         "@wrnexus/authz": "workspace:*",
@@ -302,7 +302,7 @@
     },
     "packages/image": {
       "name": "@wrnexus/image",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/plugin": "workspace:*",
         "@wrnexus/security": "workspace:*",
@@ -322,7 +322,7 @@
     },
     "packages/jwt": {
       "name": "@wrnexus/jwt",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/core": "workspace:*",
       },
@@ -333,7 +333,7 @@
     },
     "packages/language-server": {
       "name": "@wrnexus/language-server",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "bin": {
         "wrnexus-language-server": "src/server.ts",
       },
@@ -344,39 +344,39 @@
     },
     "packages/mcp": {
       "name": "@wrnexus/mcp",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "bin": {
         "wrnexus-mcp": "src/stdio.ts",
       },
     },
     "packages/mobile": {
       "name": "@wrnexus/mobile",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/native": "workspace:*",
       },
     },
     "packages/native": {
       "name": "@wrnexus/native",
-      "version": "0.8.3",
+      "version": "0.8.4",
     },
     "packages/oauth": {
       "name": "@wrnexus/oauth",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/jwt": "workspace:*",
       },
     },
     "packages/observability": {
       "name": "@wrnexus/observability",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/core": "workspace:*",
       },
     },
     "packages/playground": {
       "name": "@wrnexus/playground",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/compiler": "workspace:*",
         "@wrnexus/syntax": "workspace:*",
@@ -384,18 +384,18 @@
     },
     "packages/plugin": {
       "name": "@wrnexus/plugin",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/syntax": "workspace:*",
       },
     },
     "packages/pubsub": {
       "name": "@wrnexus/pubsub",
-      "version": "0.8.3",
+      "version": "0.8.4",
     },
     "packages/pwa": {
       "name": "@wrnexus/pwa",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "devDependencies": {
         "@types/bun": "^1.3.14",
         "typescript": "^5.9.2",
@@ -403,18 +403,18 @@
     },
     "packages/queue": {
       "name": "@wrnexus/queue",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/core": "workspace:*",
       },
     },
     "packages/reactive": {
       "name": "@wrnexus/reactive",
-      "version": "0.8.3",
+      "version": "0.8.4",
     },
     "packages/realtime": {
       "name": "@wrnexus/realtime",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/core": "workspace:*",
         "@wrnexus/plugin": "workspace:*",
@@ -428,7 +428,7 @@
     },
     "packages/router": {
       "name": "@wrnexus/router",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/compiler": "workspace:*",
         "@wrnexus/core": "workspace:*",
@@ -436,14 +436,14 @@
     },
     "packages/security": {
       "name": "@wrnexus/security",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/core": "workspace:*",
       },
     },
     "packages/ssr": {
       "name": "@wrnexus/ssr",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/core": "workspace:*",
         "@wrnexus/security": "workspace:*",
@@ -452,11 +452,11 @@
     },
     "packages/store": {
       "name": "@wrnexus/store",
-      "version": "0.8.3",
+      "version": "0.8.4",
     },
     "packages/styles": {
       "name": "@wrnexus/styles",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/core": "workspace:*",
         "@wrnexus/plugin": "workspace:*",
@@ -465,19 +465,19 @@
     },
     "packages/syntax": {
       "name": "@wrnexus/syntax",
-      "version": "0.8.3",
+      "version": "0.8.4",
     },
     "packages/test": {
       "name": "@wrnexus/test",
-      "version": "0.8.3",
+      "version": "0.8.4",
     },
     "packages/tracking": {
       "name": "@wrnexus/tracking",
-      "version": "0.8.3",
+      "version": "0.8.4",
     },
     "packages/typecheck": {
       "name": "@wrnexus/typecheck",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/syntax": "workspace:*",
         "typescript": "^5.5.0",
@@ -485,14 +485,14 @@
     },
     "packages/ui": {
       "name": "@wrnexus/ui",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/core": "workspace:*",
       },
     },
     "packages/uploader": {
       "name": "@wrnexus/uploader",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/core": "workspace:*",
         "@wrnexus/plugin": "workspace:*",
@@ -506,7 +506,7 @@
     },
     "packages/validation": {
       "name": "@wrnexus/validation",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "dependencies": {
         "@wrnexus/core": "workspace:*",
         "@wrnexus/plugin": "workspace:*",
diff --git a/docs/VALIDATION-0.8.3.md b/docs/VALIDATION-0.8.3.md
index 8a116c43..3bed8b3b 100644
--- a/docs/VALIDATION-0.8.3.md
+++ b/docs/VALIDATION-0.8.3.md
@@ -56,6 +56,61 @@ in the preparation environment and stops only when dependencies excluded from th
 are required. The complete TypeScript and Bun test gates still need to be rerun on the release
 machine after `bun install --frozen-lockfile`.
 
+## Editor and language-server correction validation
+
+The editor correction candidate includes framework-level fixes for the issues reproduced in the
+Framework Showcase and official UI components:
+
+- The packaged language server starts and completes initialize/shutdown over stdio under Node.
+- Diagnostic requests are isolated so one malformed or unsupported document cannot terminate the
+  language-server process.
+- HTML tag validation runs only against `view` markup and ignores TypeScript generics, typed output
+  payloads, WRN expressions, HTML comments, and JavaScript-looking text in `
` blocks.
+- `AuthForm.wrn` and `FeatureGrid.wrn` publish zero editor diagnostics in the packaged language-server
+  smoke test.
+- Dynamic boolean and literal-option component expressions are accepted while invalid static literals
+  remain errors or warnings.
+- WRN boolean string attributes such as `required="true"` are accepted by component contract checks.
+- Reserved prop names such as `class` are not emitted as invalid standalone TypeScript bindings.
+- Missing TypeScript standard libraries from a packaged bundle no longer create a flood of unrelated
+  global-type errors; runtime, component, and syntax diagnostics remain available.
+- Formatter tests cover raw code samples, compact adjacent/nested markup, long bare elements,
+  multiline component attributes, inline control blocks, and repeated-pass idempotence.
+- The embedded compiler, language server, and extension bundles carry normalized source/generator
+  fingerprints, and the language-server bundle is smoke-tested as a Node entrypoint.
+
+A Windows editor test run exposed four remaining semantic diagnostics in `AuthForm.wrn`: an untyped
+dynamic output name was emitted as `unknown`, its payload was narrowed to `{}`, and indexed output
+dispatch was rejected by the synthetic output contract. The virtual TypeScript generator now follows
+JavaScript semantics for omitted parameter types, keeps explicitly typed parameters strict, and gives
+the synthetic output object a callable string index used only for dynamic dispatch. Direct unknown
+`output.name()` calls remain protected by WRN syntax diagnostics.
+
+The language-server typecheck host now resolves TypeScript standard libraries from the active
+workspace when the bundled TypeScript compiler cannot find libraries beside `language-server.cjs`.
+A dedicated test intentionally introduces a typed payload error and requires `WRN-TYPE-2322`, so the
+editor suite can no longer pass merely because semantic checking was silently skipped.
+
+The Node editor test suite passes 41 tests, including zero diagnostics for `AuthForm.wrn` and
+`FeatureGrid.wrn` with workspace TypeScript libraries enabled. Extension manifest/grammar validation
+passes, all 21 Framework Showcase WRN files compile, and all 146 official WRN component files pass
+parser/codegen and explicit-import validation in the preparation environment.
+
+A subsequent Windows production-gate run exposed two final ESLint failures and one reviewed visual
+contract delta. The unused legacy `stripComments` helper was removed from the VS Code diagnostics
+source and generated extension bundle. `scripts/build-editor-language-server.mjs` now imports
+`Buffer` from `node:buffer`, so the release scripts remain compatible with the repository's
+`no-undef` policy. The language-server generator fingerprint was refreshed accordingly.
+
+The UI visual gate differed only for `packages/ui/components/AuthForm.wrn`. Comparison with the
+previous accepted baseline confirmed that the source change consisted solely of corrected indentation
+for the `size` prop and nested `Checkbox`; no classes, attributes, conditions, or rendered content
+changed. The 0.8 visual baseline was regenerated after that review.
+
+The editor suite passes 41/41 tests when workspace TypeScript libraries are present. UI visual
+contract validation passes for 109 artifacts, explicit component imports pass for 146 WRN files, the
+public API baseline matches all 46 packages, and extension manifest/grammar validation passes.
+
 ## Required final release verification
 
 The preparation environment did not include the Bun executable. Therefore the complete Bun test,
diff --git a/docs/ui-visual-contract-0.8.json b/docs/ui-visual-contract-0.8.json
index a7298b70..06cdce87 100644
--- a/docs/ui-visual-contract-0.8.json
+++ b/docs/ui-visual-contract-0.8.json
@@ -7,7 +7,7 @@
     "packages/ui/components/AdvancedRangeSlider.wrn": "e78b5cf825503d0cd306c6e490d71317ee3db1844882d13b500df1f7f1c408e1",
     "packages/ui/components/AdvancedSelect.wrn": "29d43f8019a3e0e98af7cf12440f41523665535a39a973441b4faea78e67114a",
     "packages/ui/components/AnnouncementBar.wrn": "5946ef5eb646f7414056f59d9cfe14680aa3c935acc9a1ca6528dc53b23caa48",
-    "packages/ui/components/AuthForm.wrn": "e6ab0cc4a557b3fd41528368c5aa7aaf39e9adcd49d9907df8d26d77bc515870",
+    "packages/ui/components/AuthForm.wrn": "496d240090269e69679e01cda3b071c012ec4e925a7c01ae883d25e359e381e1",
     "packages/ui/components/AuthSplitLayout.wrn": "72b82495dd14d40a2d594ccdcf3adc925d1567c155750fb1072347b8845aab7b",
     "packages/ui/components/AvatarGroup.wrn": "9c583f567213f8e1ad4506a7d2abc3fb462d11679fd2196d3828030f28a28964",
     "packages/ui/components/BackToTop.wrn": "b12c56ec8b7aabad68cbdc47f4b3237e2c21a61398f0aa51f9add6229b7357cd",
diff --git a/editors/vscode/CHANGELOG.md b/editors/vscode/CHANGELOG.md
index 7ca507c3..89d3962d 100644
--- a/editors/vscode/CHANGELOG.md
+++ b/editors/vscode/CHANGELOG.md
@@ -5,6 +5,21 @@
 - Rebuilt the embedded WRN compiler with the 0.8.3 SSR, computed-value, Async-scope, and typed loop-prop fixes.
 - Added compiler awareness for pruned and bundled browser hydration imports.
 - Aligned the extension version with the WRNexusJS 0.8.3 framework release.
+- Fixed repeated language-server crashes by producing a Node-executable bundle and containing
+  diagnostics failures without disposing the language-client connection.
+- Fixed false missing/mismatched HTML tags from TypeScript generics, object payload types, comments,
+  and code samples outside or inside WRN `view` blocks.
+- Fixed formatting of `
` examples, compact sibling markup, long text elements, and
+  multiline tags so repeated formatting is idempotent.
+- Fixed false boolean/string and literal-option diagnostics for dynamic component props such as
+  `external='{item.external || false}'`.
+- Kept component prop/event intelligence active while the shared language server is enabled.
+- Suppressed unavailable TypeScript standard-library and unmapped virtual-document implementation
+  diagnostics in packaged extension environments.
+- Fixed false `unknown`/index-access diagnostics in valid dynamic event forwarding handlers such as
+  `output[type](payload)` by preserving JavaScript semantics for omitted parameter types.
+- Resolved TypeScript standard libraries from the active workspace so semantic diagnostics run
+  consistently in the repository and extension development environment.
 
 ## 0.8.0
 
diff --git a/editors/vscode/package-lock.json b/editors/vscode/package-lock.json
index 6360b6b9..85d12a4a 100644
--- a/editors/vscode/package-lock.json
+++ b/editors/vscode/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "wrnexus",
-  "version": "0.8.3",
+  "version": "0.8.4",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "wrnexus",
-      "version": "0.8.3",
+      "version": "0.8.4",
       "license": "SEE LICENSE IN LICENSE",
       "dependencies": {
         "vscode-languageclient": "^10.1.0"
diff --git a/editors/vscode/package.json b/editors/vscode/package.json
index f2791737..d6816ed6 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.3",
+  "version": "0.8.4",
   "publisher": "wrnexus",
   "private": true,
   "license": "SEE LICENSE IN LICENSE",
@@ -126,6 +126,14 @@
           "default": true,
           "scope": "resource",
           "description": "Place opening-tag attributes on separate lines, with the closing delimiter on its own line."
+        },
+        "wrnexus.formatting.printWidth": {
+          "type": "number",
+          "default": 100,
+          "minimum": 60,
+          "maximum": 240,
+          "scope": "resource",
+          "description": "Preferred WRNexus formatter line width before long tags are expanded."
         }
       }
     },
diff --git a/editors/vscode/src/compiler.cjs b/editors/vscode/src/compiler.cjs
index ea27d8d3..494b59e1 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: b9e9a9ed0492c387198e368436696480ef2eca5a850ed94852c78c73c70d2959
+// WRN editor compiler source hash: 1d2e0d1e1c38a4513b0fce3631bae003f7c28069920c5386ecc68cbe728bf57b
 // WRN editor compiler generator hash: c71e7fe4258c97b73b384ff14b321f0cf0b30cc2ed0322f5f84b04e757159b18
 // Generated with TypeScript: 5.9.3
 const __nodeRequire = require;
@@ -4845,6 +4845,18 @@ function formatOpeningTag(value, unit, depth, printWidth = 100, multilineAttribu
             opensElement,
         };
     }
+    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}`);
+        }
+        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)),
@@ -4886,6 +4898,73 @@ function isMultilineOpeningTagStart(value) {
 function isClosingTag(value) {
     return /^<\/[A-Za-z][\w$:.-]*\s*>/.test(value);
 }
+/**
+ * Preserve compact, already-balanced HTML fragments as one line.
+ *
+ * A line such as `PageAPI` 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("", 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("$/.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);
 }
@@ -4991,6 +5070,41 @@ function collectOpeningTag(inputLines, startIndex) {
         endIndex: index,
     };
 }
+function isPreservedRawBlockStart(value) {
+    return (/)/i.test(value) &&
+        !/<\/pre\s*>/i.test(value.slice(0, value.search(/)/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.
  *
@@ -5000,7 +5114,7 @@ function collectOpeningTag(inputLines, startIndex) {
  */
 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;
@@ -5013,7 +5127,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];
@@ -5068,6 +5182,14 @@ function formatWrnPass(source, options = {}) {
             index += 1;
             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);
             value = collected.value;
@@ -5090,7 +5212,11 @@ function formatWrnPass(source, options = {}) {
             lineHtmlDepth = Math.max(0, htmlDepth - 1);
         }
         const depth = lineCodeDepth + lineHtmlDepth + lineControlDepth;
-        if (value.startsWith("<") &&
+        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(" 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))
+    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))
+    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";
diff --git a/editors/vscode/src/component-metadata.js b/editors/vscode/src/component-metadata.js
index 5d548721..4ccaa10f 100644
--- a/editors/vscode/src/component-metadata.js
+++ b/editors/vscode/src/component-metadata.js
@@ -39,9 +39,24 @@ function runtimeType(type) {
   const value = String(type || "")
     .trim()
     .replace(/^readonly\s+/, "");
-  if (/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(value)) return "string";
-  if (/^(?:number|Number)(?:\s*\|\s*(?:null|undefined))*$/.test(value)) return "number";
-  if (/^(?:boolean|Boolean)(?:\s*\|\s*(?:null|undefined))*$/.test(value)) return "boolean";
+  const unionParts = value.split("|").map((part) => part.trim());
+  const concreteParts = unionParts.filter((part) => !/^(?:null|undefined)$/.test(part));
+  if (
+    /^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(value) ||
+    (concreteParts.length > 0 && concreteParts.every((part) => /^(?:"[^"]*"|'[^']*')$/.test(part)))
+  )
+    return "string";
+  if (
+    /^(?:number|Number)(?:\s*\|\s*(?:null|undefined))*$/.test(value) ||
+    (concreteParts.length > 0 &&
+      concreteParts.every((part) => /^-?(?:\d+\.?\d*|\.\d+)$/.test(part)))
+  )
+    return "number";
+  if (
+    /^(?:boolean|Boolean)(?:\s*\|\s*(?:null|undefined))*$/.test(value) ||
+    (concreteParts.length > 0 && concreteParts.every((part) => /^(?:true|false)$/.test(part)))
+  )
+    return "boolean";
   if (/^bigint(?:\s*\|\s*(?:null|undefined))*$/.test(value)) return "bigint";
   if (/^(?:Array\s*<|ReadonlyArray\s*<|.+\[\])/.test(value) || /^\[/.test(value)) return "array";
   if (/^(?:Record\s*<|object\b|\{)/.test(value)) return "object";
@@ -138,15 +153,47 @@ function unwrapAttributeValue(value) {
   return trimmed;
 }
 
+function isWrappedExpression(value) {
+  const trimmed = value.trim();
+  return trimmed.startsWith("{") && trimmed.endsWith("}");
+}
+
+function expressionLiteralType(value) {
+  const expression = value.trim();
+  if (/^(?:true|false)$/.test(expression)) return "boolean";
+  if (/^-?(?:\d+\.?\d*|\.\d+)$/.test(expression)) return "number";
+  if (/^(?:"[\s\S]*"|'[\s\S]*'|`[\s\S]*`)$/.test(expression)) return "string";
+  if (expression.startsWith("[")) return "array";
+  if (expression.startsWith("{")) return "object";
+  if (expression === "null") return "null";
+  return null;
+}
+
 function attributeValueType(value, symbols = new Map()) {
+  const expression = isWrappedExpression(value);
   const unwrapped = unwrapAttributeValue(value);
   if (/^[A-Za-z_$][\w$]*$/.test(unwrapped) && symbols.has(unwrapped)) {
     return symbols.get(unwrapped);
   }
-  if (/^(?:true|false)$/.test(unwrapped)) return "boolean";
-  if (/^-?(?:\d+\.?\d*|\.\d+)$/.test(unwrapped)) return "number";
-  if (unwrapped.startsWith("[")) return "array";
-  if (unwrapped.startsWith("{")) return "object";
+
+  const literal = expressionLiteralType(unwrapped);
+  if (literal) return literal;
+
+  if (expression) {
+    if (
+      /^!\s*/.test(unwrapped) ||
+      /(?:===|!==|==|!=|<=|>=|<|>|\bin\b|\binstanceof\b)/.test(unwrapped)
+    ) {
+      return "boolean";
+    }
+
+    // Dynamic member access, calls, logical expressions and ternaries cannot be
+    // proven from component metadata alone. Treat them as unknown instead of as
+    // quoted strings so valid expressions such as `{item.external || false}` do
+    // not produce false boolean/string diagnostics.
+    return "unknown";
+  }
+
   return "string";
 }
 
@@ -154,6 +201,7 @@ function isTypeCompatible(prop, value, symbols = new Map()) {
   const expected = runtimeType(prop.type);
   if (expected === "unknown" || expected === "null") return true;
   const actual = runtimeType(attributeValueType(value, symbols));
+  if (actual === "unknown") return true;
   if (expected === actual) return true;
   if (expected === "number" && actual === "string") return Number.isFinite(Number(value));
   if (expected === "boolean" && actual === "string")
@@ -255,8 +303,10 @@ function validateComponentTags(source, components) {
           end: attribute.nameEnd,
         });
       }
-      const literal = stringLiteral(attribute.value) ?? attribute.value;
-      if (prop.options.length > 0 && !prop.options.includes(literal)) {
+      const optionValue = isWrappedExpression(attribute.value)
+        ? stringLiteral(unwrapAttributeValue(attribute.value))
+        : (stringLiteral(attribute.value) ?? attribute.value);
+      if (optionValue !== null && prop.options.length > 0 && !prop.options.includes(optionValue)) {
         diagnostics.push({
           severity: "warning",
           code: "wrn-component-prop-option",
diff --git a/editors/vscode/src/diagnostics.js b/editors/vscode/src/diagnostics.js
index a0e1cfb7..f03fac7c 100644
--- a/editors/vscode/src/diagnostics.js
+++ b/editors/vscode/src/diagnostics.js
@@ -94,65 +94,6 @@ function lineDiagnostic(
   return diagnostic;
 }
 
-function stripComments(source) {
-  const masked = [...source];
-  let quote = null;
-  let escaped = false;
-
-  const maskRange = (start, end) => {
-    for (let index = start; index < end; index += 1) {
-      if (source[index] !== "\n" && source[index] !== "\r") {
-        masked[index] = " ";
-      }
-    }
-  };
-
-  for (let index = 0; index < source.length; index += 1) {
-    const character = source[index];
-
-    if (quote !== null) {
-      if (escaped) {
-        escaped = false;
-      } else if (character === "\\") {
-        escaped = true;
-      } else if (character === quote) {
-        quote = null;
-      }
-      continue;
-    }
-
-    if (character === '"' || character === "'" || character === "`") {
-      quote = character;
-      continue;
-    }
-
-    if (source.startsWith("//", index)) {
-      const lineEnd = source.indexOf("\n", index + 2);
-      const end = lineEnd === -1 ? source.length : lineEnd;
-      maskRange(index, end);
-      index = end - 1;
-      continue;
-    }
-
-    if (source.startsWith("/*", index)) {
-      const commentEnd = source.indexOf("*/", index + 2);
-      const end = commentEnd === -1 ? source.length : commentEnd + 2;
-      maskRange(index, end);
-      index = end - 1;
-      continue;
-    }
-
-    if (source.startsWith("", index + 4);
-      const end = commentEnd === -1 ? source.length : commentEnd + 3;
-      maskRange(index, end);
-      index = end - 1;
-    }
-  }
-
-  return masked.join("");
-}
-
 function maskLeadingTrivia(source) {
   const masked = [...source];
   let offset = 0;
@@ -350,10 +291,96 @@ function validateBalancedCharacters(document, source) {
   return diagnostics;
 }
 
+function maskHtmlComments(source) {
+  const masked = [...source];
+  let index = 0;
+
+  while (index < source.length) {
+    if (!source.startsWith("", index + 4);
+    const end = commentEnd === -1 ? source.length : commentEnd + 3;
+    for (let cursor = index; cursor < end; cursor += 1) {
+      if (source[cursor] !== "\n" && source[cursor] !== "\r") masked[cursor] = " ";
+    }
+    index = end;
+  }
+
+  return masked.join("");
+}
+
+function maskTemplateExpressions(source) {
+  const masked = [...source];
+  let index = 0;
+
+  while (index < source.length) {
+    if (source[index] !== "{") {
+      index += 1;
+      continue;
+    }
+
+    const start = index;
+    let depth = 0;
+    let quote = null;
+    let escaped = false;
+
+    while (index < source.length) {
+      const character = source[index];
+
+      if (quote !== null) {
+        if (escaped) escaped = false;
+        else if (character === "\\") escaped = true;
+        else if (character === quote) quote = null;
+        index += 1;
+        continue;
+      }
+
+      if (character === '"' || character === "'" || character === "`") {
+        quote = character;
+        index += 1;
+        continue;
+      }
+
+      if (character === "{") depth += 1;
+      else if (character === "}") {
+        depth -= 1;
+        index += 1;
+        if (depth === 0) break;
+        continue;
+      }
+
+      index += 1;
+    }
+
+    for (let cursor = start; cursor < index; cursor += 1) {
+      if (source[cursor] !== "\n" && source[cursor] !== "\r") masked[cursor] = " ";
+    }
+  }
+
+  return masked.join("");
+}
+
+function findViewRanges(source) {
+  const ranges = [];
+  const pattern = /\bview\s*\{/g;
+  let match;
+
+  while ((match = pattern.exec(source)) !== null) {
+    const openingBrace = source.indexOf("{", match.index);
+    const closingBrace = findMatchingBrace(source, openingBrace);
+    if (closingBrace === -1) break;
+    ranges.push({ start: openingBrace + 1, end: closingBrace });
+    pattern.lastIndex = closingBrace + 1;
+  }
+
+  return ranges;
+}
+
 function validateHtmlTags(document, source) {
   const diagnostics = [];
-  const stack = [];
-
   const voidElements = new Set([
     "area",
     "base",
@@ -371,76 +398,82 @@ function validateHtmlTags(document, source) {
     "wbr",
   ]);
 
-  const cleaned = stripComments(source);
-  const tagPattern = /<\/?([A-Za-z][A-Za-z0-9_$:.-]*)(?:\s[\s\S]*?)?\/?>/g;
+  for (const range of findViewRanges(source)) {
+    const fragment = source.slice(range.start, range.end);
+    // JavaScript-style `//` is valid rendered text inside 
/. Only
+    // HTML comments and WRN expressions are masked before tag validation.
+    const cleaned = maskTemplateExpressions(maskHtmlComments(fragment));
+    const stack = [];
+    const tagPattern = /<\/?([A-Za-z][A-Za-z0-9_$:.-]*)(?:\s[\s\S]*?)?\/?>/g;
+    let match;
 
-  let match;
+    while ((match = tagPattern.exec(cleaned)) !== null) {
+      const completeTag = match[0];
+      const tagName = match[1];
+      const normalizedName = /^[a-z]/.test(tagName) ? tagName.toLowerCase() : tagName;
+      const lowerTag = tagName.toLowerCase();
+      const absoluteStart = range.start + match.index;
+      const isClosing = completeTag.startsWith("$/.test(completeTag);
+      const isVoid = voidElements.has(lowerTag);
 
-  while ((match = tagPattern.exec(cleaned)) !== null) {
-    const completeTag = match[0];
-    const tagName = match[1];
-    const lowerTag = tagName.toLowerCase();
-
-    const isClosing = completeTag.startsWith("");
-    const isVoid = voidElements.has(lowerTag);
-
-    if (isClosing) {
-      const last = stack.pop();
-
-      if (!last) {
-        diagnostics.push(
-          createDiagnostic(
-            document,
-            match.index,
-            match.index + completeTag.length,
-            `Unexpected closing tag .`,
-            vscode.DiagnosticSeverity.Error,
-            "wrn-unexpected-html-close",
-          ),
-        );
+      if (isClosing) {
+        const matchingIndex = stack.findLastIndex((item) => item.normalizedName === normalizedName);
+        if (matchingIndex === -1) {
+          diagnostics.push(
+            createDiagnostic(
+              document,
+              absoluteStart,
+              absoluteStart + completeTag.length,
+              `Unexpected closing tag .`,
+              vscode.DiagnosticSeverity.Error,
+              "wrn-unexpected-html-close",
+            ),
+          );
+          continue;
+        }
 
+        const last = stack.at(-1);
+        if (last.normalizedName !== normalizedName) {
+          diagnostics.push(
+            createDiagnostic(
+              document,
+              absoluteStart,
+              absoluteStart + completeTag.length,
+              `Mismatched closing tag . Expected .`,
+              vscode.DiagnosticSeverity.Error,
+              "wrn-mismatched-html-tag",
+            ),
+          );
+        }
+        stack.splice(matchingIndex);
         continue;
       }
 
-      if (last.tagName !== tagName) {
-        diagnostics.push(
-          createDiagnostic(
-            document,
-            match.index,
-            match.index + completeTag.length,
-            `Mismatched closing tag . Expected .`,
-            vscode.DiagnosticSeverity.Error,
-            "wrn-mismatched-html-tag",
-          ),
-        );
+      if (!isSelfClosing && !isVoid) {
+        stack.push({
+          tagName,
+          normalizedName,
+          offset: absoluteStart,
+          length: completeTag.length,
+        });
       }
-
-      continue;
     }
 
-    if (!isSelfClosing && !isVoid) {
-      stack.push({
-        tagName,
-        offset: match.index,
-        length: completeTag.length,
-      });
+    for (const tag of stack) {
+      diagnostics.push(
+        createDiagnostic(
+          document,
+          tag.offset,
+          tag.offset + tag.length,
+          `Missing closing tag .`,
+          vscode.DiagnosticSeverity.Error,
+          "wrn-missing-html-close",
+        ),
+      );
     }
   }
 
-  for (const tag of stack) {
-    diagnostics.push(
-      createDiagnostic(
-        document,
-        tag.offset,
-        tag.offset + tag.length,
-        `Missing closing tag .`,
-        vscode.DiagnosticSeverity.Error,
-        "wrn-missing-html-close",
-      ),
-    );
-  }
-
   return diagnostics;
 }
 
diff --git a/editors/vscode/src/extension.bundle.cjs b/editors/vscode/src/extension.bundle.cjs
index f40a228a..54a65613 100644
--- a/editors/vscode/src/extension.bundle.cjs
+++ b/editors/vscode/src/extension.bundle.cjs
@@ -1,3 +1,5 @@
+// WRN editor extension source hash: 825c406dea4b196976b1b6e53b3c71c3263bd63f403acdaf2fe7e5ad174275da
+// WRN editor extension generator hash: 456d1d614e44e5fb1f19b784176c09cf2ade9b64ef73a17934c2698150b62728
 "use strict";
 var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
 
@@ -27187,6 +27189,17 @@ ${serverFunctions}
             opensElement
           };
         }
+        if (parsed.attributes.length === 0 && !parsed.selfClosing) {
+          const lines2 = [`${baseIndent}<${parsed.tagName}>`];
+          if (parsed.trailingClosing) {
+            if (parsed.inlineContent)
+              lines2.push(`${childIndent}${parsed.inlineContent}`);
+            lines2.push(`${baseIndent}`);
+          } else if (parsed.remainder) {
+            lines2.push(`${childIndent}${parsed.remainder}`);
+          }
+          return { lines: lines2, opensElement };
+        }
         const lines = [
           `${baseIndent}<${parsed.tagName}`,
           ...parsed.attributes.flatMap((attribute) => formatAttribute(attribute, childIndent, unit))
@@ -27224,6 +27237,63 @@ ${serverFunctions}
       function isClosingTag(value) {
         return /^<\/[A-Za-z][\w$:.-]*\s*>/.test(value);
       }
+      function isBalancedInlineHtmlFragment(value) {
+        if (!value.startsWith("<") || value.startsWith("", 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("$/.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);
       }
@@ -27319,9 +27389,43 @@ ${serverFunctions}
           endIndex: index
         };
       }
+      function isPreservedRawBlockStart(value) {
+        return /)/i.test(value) && !/<\/pre\s*>/i.test(value.slice(0, value.search(/)/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 };
+      }
       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;
@@ -27331,7 +27435,7 @@ ${serverFunctions}
         });
       }
       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];
@@ -27387,6 +27491,14 @@ ${serverFunctions}
             index += 1;
             continue;
           }
+          if (isPreservedRawBlockStart(value)) {
+            const collected = collectPreservedRawBlock(inputLines, index);
+            const depth2 = codeDepth + htmlDepth + controlDepth;
+            output.push(`${unit.repeat(depth2)}${collected.lines[0].trimStart()}`);
+            output.push(...collected.lines.slice(1));
+            index = collected.endIndex + 1;
+            continue;
+          }
           if (isMultilineOpeningTagStart(value)) {
             const collected = collectOpeningTag(inputLines, index);
             value = collected.value;
@@ -27409,7 +27521,10 @@ ${serverFunctions}
             lineHtmlDepth = Math.max(0, htmlDepth - 1);
           }
           const depth = lineCodeDepth + lineHtmlDepth + lineControlDepth;
-          if (value.startsWith("<") && !value.startsWith(" 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))
+        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))
+        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";
@@ -30196,11 +30313,13 @@ var require_component_metadata = __commonJS((exports2, module2) => {
   }
   function runtimeType(type) {
     const value = String(type || "").trim().replace(/^readonly\s+/, "");
-    if (/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(value))
+    const unionParts = value.split("|").map((part) => part.trim());
+    const concreteParts = unionParts.filter((part) => !/^(?:null|undefined)$/.test(part));
+    if (/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(value) || concreteParts.length > 0 && concreteParts.every((part) => /^(?:"[^"]*"|'[^']*')$/.test(part)))
       return "string";
-    if (/^(?:number|Number)(?:\s*\|\s*(?:null|undefined))*$/.test(value))
+    if (/^(?:number|Number)(?:\s*\|\s*(?:null|undefined))*$/.test(value) || concreteParts.length > 0 && concreteParts.every((part) => /^-?(?:\d+\.?\d*|\.\d+)$/.test(part)))
       return "number";
-    if (/^(?:boolean|Boolean)(?:\s*\|\s*(?:null|undefined))*$/.test(value))
+    if (/^(?:boolean|Boolean)(?:\s*\|\s*(?:null|undefined))*$/.test(value) || concreteParts.length > 0 && concreteParts.every((part) => /^(?:true|false)$/.test(part)))
       return "boolean";
     if (/^bigint(?:\s*\|\s*(?:null|undefined))*$/.test(value))
       return "bigint";
@@ -30289,19 +30408,41 @@ var require_component_metadata = __commonJS((exports2, module2) => {
       return trimmed.slice(1, -1).trim();
     return trimmed;
   }
+  function isWrappedExpression(value) {
+    const trimmed = value.trim();
+    return trimmed.startsWith("{") && trimmed.endsWith("}");
+  }
+  function expressionLiteralType(value) {
+    const expression = value.trim();
+    if (/^(?:true|false)$/.test(expression))
+      return "boolean";
+    if (/^-?(?:\d+\.?\d*|\.\d+)$/.test(expression))
+      return "number";
+    if (/^(?:"[\s\S]*"|'[\s\S]*'|`[\s\S]*`)$/.test(expression))
+      return "string";
+    if (expression.startsWith("["))
+      return "array";
+    if (expression.startsWith("{"))
+      return "object";
+    if (expression === "null")
+      return "null";
+    return null;
+  }
   function attributeValueType(value, symbols = new Map) {
+    const expression = isWrappedExpression(value);
     const unwrapped = unwrapAttributeValue(value);
     if (/^[A-Za-z_$][\w$]*$/.test(unwrapped) && symbols.has(unwrapped)) {
       return symbols.get(unwrapped);
     }
-    if (/^(?:true|false)$/.test(unwrapped))
-      return "boolean";
-    if (/^-?(?:\d+\.?\d*|\.\d+)$/.test(unwrapped))
-      return "number";
-    if (unwrapped.startsWith("["))
-      return "array";
-    if (unwrapped.startsWith("{"))
-      return "object";
+    const literal = expressionLiteralType(unwrapped);
+    if (literal)
+      return literal;
+    if (expression) {
+      if (/^!\s*/.test(unwrapped) || /(?:===|!==|==|!=|<=|>=|<|>|\bin\b|\binstanceof\b)/.test(unwrapped)) {
+        return "boolean";
+      }
+      return "unknown";
+    }
     return "string";
   }
   function isTypeCompatible(prop, value, symbols = new Map) {
@@ -30309,6 +30450,8 @@ var require_component_metadata = __commonJS((exports2, module2) => {
     if (expected === "unknown" || expected === "null")
       return true;
     const actual = runtimeType(attributeValueType(value, symbols));
+    if (actual === "unknown")
+      return true;
     if (expected === actual)
       return true;
     if (expected === "number" && actual === "string")
@@ -30409,8 +30552,8 @@ var require_component_metadata = __commonJS((exports2, module2) => {
             end: attribute.nameEnd
           });
         }
-        const literal = stringLiteral(attribute.value) ?? attribute.value;
-        if (prop.options.length > 0 && !prop.options.includes(literal)) {
+        const optionValue = isWrappedExpression(attribute.value) ? stringLiteral(unwrapAttributeValue(attribute.value)) : stringLiteral(attribute.value) ?? attribute.value;
+        if (optionValue !== null && prop.options.length > 0 && !prop.options.includes(optionValue)) {
           diagnostics.push({
             severity: "warning",
             code: "wrn-component-prop-option",
@@ -30727,58 +30870,6 @@ var require_diagnostics = __commonJS((exports2, module2) => {
     }
     return diagnostic;
   }
-  function stripComments(source) {
-    const masked = [...source];
-    let quote = null;
-    let escaped = false;
-    const maskRange = (start, end) => {
-      for (let index = start;index < end; index += 1) {
-        if (source[index] !== `
-` && source[index] !== "\r") {
-          masked[index] = " ";
-        }
-      }
-    };
-    for (let index = 0;index < source.length; index += 1) {
-      const character = source[index];
-      if (quote !== null) {
-        if (escaped) {
-          escaped = false;
-        } else if (character === "\\") {
-          escaped = true;
-        } else if (character === quote) {
-          quote = null;
-        }
-        continue;
-      }
-      if (character === '"' || character === "'" || character === "`") {
-        quote = character;
-        continue;
-      }
-      if (source.startsWith("//", index)) {
-        const lineEnd = source.indexOf(`
-`, index + 2);
-        const end = lineEnd === -1 ? source.length : lineEnd;
-        maskRange(index, end);
-        index = end - 1;
-        continue;
-      }
-      if (source.startsWith("/*", index)) {
-        const commentEnd = source.indexOf("*/", index + 2);
-        const end = commentEnd === -1 ? source.length : commentEnd + 2;
-        maskRange(index, end);
-        index = end - 1;
-        continue;
-      }
-      if (source.startsWith("", index + 4);
-        const end = commentEnd === -1 ? source.length : commentEnd + 3;
-        maskRange(index, end);
-        index = end - 1;
-      }
-    }
-    return masked.join("");
-  }
   function maskLeadingTrivia(source) {
     const masked = [...source];
     let offset = 0;
@@ -30897,9 +30988,89 @@ var require_diagnostics = __commonJS((exports2, module2) => {
     }
     return diagnostics;
   }
+  function maskHtmlComments(source) {
+    const masked = [...source];
+    let index = 0;
+    while (index < source.length) {
+      if (!source.startsWith("", index + 4);
+      const end = commentEnd === -1 ? source.length : commentEnd + 3;
+      for (let cursor = index;cursor < end; cursor += 1) {
+        if (source[cursor] !== `
+` && source[cursor] !== "\r")
+          masked[cursor] = " ";
+      }
+      index = end;
+    }
+    return masked.join("");
+  }
+  function maskTemplateExpressions(source) {
+    const masked = [...source];
+    let index = 0;
+    while (index < source.length) {
+      if (source[index] !== "{") {
+        index += 1;
+        continue;
+      }
+      const start = index;
+      let depth = 0;
+      let quote = null;
+      let escaped = false;
+      while (index < source.length) {
+        const character = source[index];
+        if (quote !== null) {
+          if (escaped)
+            escaped = false;
+          else if (character === "\\")
+            escaped = true;
+          else if (character === quote)
+            quote = null;
+          index += 1;
+          continue;
+        }
+        if (character === '"' || character === "'" || character === "`") {
+          quote = character;
+          index += 1;
+          continue;
+        }
+        if (character === "{")
+          depth += 1;
+        else if (character === "}") {
+          depth -= 1;
+          index += 1;
+          if (depth === 0)
+            break;
+          continue;
+        }
+        index += 1;
+      }
+      for (let cursor = start;cursor < index; cursor += 1) {
+        if (source[cursor] !== `
+` && source[cursor] !== "\r")
+          masked[cursor] = " ";
+      }
+    }
+    return masked.join("");
+  }
+  function findViewRanges(source) {
+    const ranges = [];
+    const pattern = /\bview\s*\{/g;
+    let match;
+    while ((match = pattern.exec(source)) !== null) {
+      const openingBrace = source.indexOf("{", match.index);
+      const closingBrace = findMatchingBrace(source, openingBrace);
+      if (closingBrace === -1)
+        break;
+      ranges.push({ start: openingBrace + 1, end: closingBrace });
+      pattern.lastIndex = closingBrace + 1;
+    }
+    return ranges;
+  }
   function validateHtmlTags(document, source) {
     const diagnostics = [];
-    const stack = [];
     const voidElements = new Set([
       "area",
       "base",
@@ -30916,38 +31087,47 @@ var require_diagnostics = __commonJS((exports2, module2) => {
       "track",
       "wbr"
     ]);
-    const cleaned = stripComments(source);
-    const tagPattern = /<\/?([A-Za-z][A-Za-z0-9_$:.-]*)(?:\s[\s\S]*?)?\/?>/g;
-    let match;
-    while ((match = tagPattern.exec(cleaned)) !== null) {
-      const completeTag = match[0];
-      const tagName = match[1];
-      const lowerTag = tagName.toLowerCase();
-      const isClosing = completeTag.startsWith("");
-      const isVoid = voidElements.has(lowerTag);
-      if (isClosing) {
-        const last = stack.pop();
-        if (!last) {
-          diagnostics.push(createDiagnostic(document, match.index, match.index + completeTag.length, `Unexpected closing tag .`, vscode.DiagnosticSeverity.Error, "wrn-unexpected-html-close"));
+    for (const range of findViewRanges(source)) {
+      const fragment = source.slice(range.start, range.end);
+      const cleaned = maskTemplateExpressions(maskHtmlComments(fragment));
+      const stack = [];
+      const tagPattern = /<\/?([A-Za-z][A-Za-z0-9_$:.-]*)(?:\s[\s\S]*?)?\/?>/g;
+      let match;
+      while ((match = tagPattern.exec(cleaned)) !== null) {
+        const completeTag = match[0];
+        const tagName = match[1];
+        const normalizedName = /^[a-z]/.test(tagName) ? tagName.toLowerCase() : tagName;
+        const lowerTag = tagName.toLowerCase();
+        const absoluteStart = range.start + match.index;
+        const isClosing = completeTag.startsWith("$/.test(completeTag);
+        const isVoid = voidElements.has(lowerTag);
+        if (isClosing) {
+          const matchingIndex = stack.findLastIndex((item) => item.normalizedName === normalizedName);
+          if (matchingIndex === -1) {
+            diagnostics.push(createDiagnostic(document, absoluteStart, absoluteStart + completeTag.length, `Unexpected closing tag .`, vscode.DiagnosticSeverity.Error, "wrn-unexpected-html-close"));
+            continue;
+          }
+          const last = stack.at(-1);
+          if (last.normalizedName !== normalizedName) {
+            diagnostics.push(createDiagnostic(document, absoluteStart, absoluteStart + completeTag.length, `Mismatched closing tag . Expected .`, vscode.DiagnosticSeverity.Error, "wrn-mismatched-html-tag"));
+          }
+          stack.splice(matchingIndex);
           continue;
         }
-        if (last.tagName !== tagName) {
-          diagnostics.push(createDiagnostic(document, match.index, match.index + completeTag.length, `Mismatched closing tag . Expected .`, vscode.DiagnosticSeverity.Error, "wrn-mismatched-html-tag"));
+        if (!isSelfClosing && !isVoid) {
+          stack.push({
+            tagName,
+            normalizedName,
+            offset: absoluteStart,
+            length: completeTag.length
+          });
         }
-        continue;
       }
-      if (!isSelfClosing && !isVoid) {
-        stack.push({
-          tagName,
-          offset: match.index,
-          length: completeTag.length
-        });
+      for (const tag of stack) {
+        diagnostics.push(createDiagnostic(document, tag.offset, tag.offset + tag.length, `Missing closing tag .`, vscode.DiagnosticSeverity.Error, "wrn-missing-html-close"));
       }
     }
-    for (const tag of stack) {
-      diagnostics.push(createDiagnostic(document, tag.offset, tag.offset + tag.length, `Missing closing tag .`, vscode.DiagnosticSeverity.Error, "wrn-missing-html-close"));
-    }
     return diagnostics;
   }
   function getRootBodyRange(source, rootMatch) {
@@ -31923,9 +32103,9 @@ async function activate(context) {
     registerCompilerDiagnostics(context);
     registerCompletionProvider(context);
     registerDefinitionProvider(context);
-    registerComponentIntelligence(context);
     registerFormatter(context);
   }
+  registerComponentIntelligence(context);
   registerSemanticTokens(context);
   registerV060LanguageFeatures(context);
 }
diff --git a/editors/vscode/src/extension.js b/editors/vscode/src/extension.js
index ecba98b8..8a54b857 100644
--- a/editors/vscode/src/extension.js
+++ b/editors/vscode/src/extension.js
@@ -376,9 +376,12 @@ async function activate(context) {
     registerCompilerDiagnostics(context);
     registerCompletionProvider(context);
     registerDefinitionProvider(context);
-    registerComponentIntelligence(context);
     registerFormatter(context);
   }
+  // Package/local component metadata is not yet part of the LSP workspace
+  // catalog, so keep prop/event completion, hover, and validation available in
+  // both LSP and built-in-provider modes.
+  registerComponentIntelligence(context);
   registerSemanticTokens(context);
   registerV060LanguageFeatures(context);
 }
diff --git a/editors/vscode/src/language-server.cjs b/editors/vscode/src/language-server.cjs
index 05e152ba..9e086258 100644
--- a/editors/vscode/src/language-server.cjs
+++ b/editors/vscode/src/language-server.cjs
@@ -1,4 +1,6 @@
-#!/usr/bin/env bun
+#!/usr/bin/env node
+// WRN editor language server source hash: 5397c7912894dd5ab330f6fafc0f4093adbb94d95c23366ba7883605a724a7ae
+// WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72
 // @bun @bun-cjs
 (function(exports, require, module, __filename, __dirname) {var __create = Object.create;
 var __getProtoOf = Object.getPrototypeOf;
@@ -169438,6 +169440,17 @@ function formatOpeningTag(value, unit, depth, printWidth = 100, multilineAttribu
       opensElement
     };
   }
+  if (parsed.attributes.length === 0 && !parsed.selfClosing) {
+    const lines2 = [`${baseIndent}<${parsed.tagName}>`];
+    if (parsed.trailingClosing) {
+      if (parsed.inlineContent)
+        lines2.push(`${childIndent}${parsed.inlineContent}`);
+      lines2.push(`${baseIndent}`);
+    } else if (parsed.remainder) {
+      lines2.push(`${childIndent}${parsed.remainder}`);
+    }
+    return { lines: lines2, opensElement };
+  }
   const lines = [
     `${baseIndent}<${parsed.tagName}`,
     ...parsed.attributes.flatMap((attribute) => formatAttribute(attribute, childIndent, unit))
@@ -169475,6 +169488,63 @@ function isMultilineOpeningTagStart(value) {
 function isClosingTag(value) {
   return /^<\/[A-Za-z][\w$:.-]*\s*>/.test(value);
 }
+function isBalancedInlineHtmlFragment(value) {
+  if (!value.startsWith("<") || value.startsWith("", 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("$/.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);
 }
@@ -169570,9 +169640,43 @@ function collectOpeningTag(inputLines, startIndex) {
     endIndex: index
   };
 }
+function isPreservedRawBlockStart(value) {
+  return /)/i.test(value) && !/<\/pre\s*>/i.test(value.slice(0, value.search(/)/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 };
+}
 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;
@@ -169582,7 +169686,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];
@@ -169638,6 +169742,14 @@ function formatWrnPass(source, options = {}) {
       index += 1;
       continue;
     }
+    if (isPreservedRawBlockStart(value)) {
+      const collected = collectPreservedRawBlock(inputLines, index);
+      const depth2 = codeDepth + htmlDepth + controlDepth;
+      output.push(`${unit.repeat(depth2)}${collected.lines[0].trimStart()}`);
+      output.push(...collected.lines.slice(1));
+      index = collected.endIndex + 1;
+      continue;
+    }
     if (isMultilineOpeningTagStart(value)) {
       const collected = collectOpeningTag(inputLines, index);
       value = collected.value;
@@ -169660,7 +169772,10 @@ function formatWrnPass(source, options = {}) {
       lineHtmlDepth = Math.max(0, htmlDepth - 1);
     }
     const depth = lineCodeDepth + lineHtmlDepth + lineControlDepth;
-    if (value.startsWith("<") && !value.startsWith(" 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))
+  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))
+  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";
@@ -171689,7 +171806,7 @@ function runtimeNamespace(runtime) {
   return `__wrn_${runtime}`;
 }
 function functionDeclaration(fn) {
-  const params = fn.parameters.map((param) => `${param.name}${param.optional ? "?" : ""}${param.valueType ? `: ${param.valueType}` : param.name === "event" ? ": CustomEvent & { target: HTMLElement }" : ": unknown"}${param.default ? ` = ${param.default}` : ""}`).join(", ");
+  const params = fn.parameters.map((param) => `${param.name}${param.optional ? "?" : ""}${param.valueType ? `: ${param.valueType}` : param.name === "event" ? ": CustomEvent & { target: HTMLElement }" : ": any"}${param.default ? ` = ${param.default}` : ""}`).join(", ");
   return `export ${fn.async ? "async " : ""}function ${fn.name}(${params})${fn.returnType ? `: ${fn.returnType}` : ""} {${fn.body}}`;
 }
 function retainedImports(ast) {
@@ -171702,6 +171819,51 @@ function retainedImports(ast) {
   }).map((entry) => entry.raw).join(`
 `);
 }
+var 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) {
+  return /^[A-Za-z_$][\w$]*$/.test(name) && !RESERVED_BINDING_NAMES.has(name);
+}
 function virtualTypeScriptModule(source, filePath = "component.wrn", appRoot = findAppRoot(filePath)) {
   const ast = parse(source);
   const chunks = [];
@@ -171728,6 +171890,8 @@ function virtualTypeScriptModule(source, filePath = "component.wrn", appRoot = f
 
 `), ast.types[0]?.trim());
   for (const prop of ast.props) {
+    if (!safeBindingName(prop.name))
+      continue;
     append(`declare const ${prop.name}: Readonly<${prop.valueType ?? "unknown"}>;`, prop.name);
   }
   for (const state of ast.states) {
@@ -171739,7 +171903,7 @@ function virtualTypeScriptModule(source, filePath = "component.wrn", appRoot = f
   const outputType = ast.outputs.map((output) => `${output.name}: (${output.payload ? `${output.payload.name}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}) => void`).join("; ");
   const serverFunctions = ast.runtimeFunctions.filter((fn) => fn.runtime === "server");
   const serverType = serverFunctions.map((fn) => `${fn.name}: (${fn.parameters.map((param) => `${param.name}${param.optional ? "?" : ""}: ${param.valueType ?? "unknown"}`).join(", ")}) => ${fn.returnType ?? (fn.async ? "Promise" : "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;`);
   for (const match of source.matchAll(/@event\s+([A-Za-z_$][\w$]*)\s*=\s*function/g))
@@ -171923,7 +172087,8 @@ function componentUsageDiagnostics(source, ast, filePath, appRoot) {
         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",
@@ -171956,8 +172121,12 @@ function componentUsageDiagnostics(source, ast, filePath, appRoot) {
 function category(value) {
   return value === import_typescript.default.DiagnosticCategory.Error ? "error" : value === import_typescript.default.DiagnosticCategory.Warning ? "warning" : "info";
 }
-function hostWithVirtualFiles(files, options) {
+function hostWithVirtualFiles(files, options, standardLibraryDirectory) {
   const host = import_typescript.default.createCompilerHost(options, true);
+  if (standardLibraryDirectory) {
+    host.getDefaultLibFileName = (compilerOptions) => import_node_path2.join(standardLibraryDirectory, import_typescript.default.getDefaultLibFileName(compilerOptions));
+    host.getDefaultLibLocation = () => standardLibraryDirectory;
+  }
   const originalGet = host.getSourceFile.bind(host);
   host.fileExists = (fileName) => files.has(import_node_path2.normalize(fileName)) || import_typescript.default.sys.fileExists(fileName);
   host.readFile = (fileName) => files.get(import_node_path2.normalize(fileName)) ?? import_typescript.default.sys.readFile(fileName);
@@ -171967,10 +172136,23 @@ function hostWithVirtualFiles(files, options) {
   };
   return host;
 }
+function resolveStandardLibraryDirectory(appRoot, options) {
+  const candidates = [
+    import_node_path2.dirname(import_typescript.default.getDefaultLibFilePath(options)),
+    import_node_path2.join(appRoot, "node_modules", "typescript", "lib"),
+    import_node_path2.join(process.cwd(), "node_modules", "typescript", "lib")
+  ];
+  for (const candidate of candidates) {
+    if ((options.lib ?? []).every((name) => import_typescript.default.sys.fileExists(import_node_path2.join(candidate, name)))) {
+      return candidate;
+    }
+  }
+  return null;
+}
 function mappedPosition(virtual, line, column) {
   const mapping = virtual.mappings.find((entry) => line >= entry.virtualStartLine && line <= entry.virtualEndLine);
   if (!mapping)
-    return { line: 1, column: 1 };
+    return null;
   const offset = line - mapping.virtualStartLine;
   return {
     line: mapping.sourceStartLine + offset,
@@ -172099,25 +172281,37 @@ function checkWrnSource(source, options = {}) {
     lib: ["lib.esnext.d.ts", "lib.dom.d.ts", "lib.dom.iterable.d.ts"]
   };
   const rootNames = [virtual.fileName, ...appTypes.files.keys()];
-  const program = import_typescript.default.createProgram(rootNames, compilerOptions, hostWithVirtualFiles(files, compilerOptions));
-  const tsDiagnostics = import_typescript.default.getPreEmitDiagnostics(program).map((diagnostic) => {
-    const file = diagnostic.file;
-    const start = diagnostic.start ?? 0;
-    const virtualPosition = file?.getLineAndCharacterOfPosition(start) ?? { line: 0, character: 0 };
-    const isVirtual = import_node_path2.normalize(file?.fileName ?? "") === import_node_path2.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: import_typescript.default.flattenDiagnosticMessageText(diagnostic.messageText, `
+  const standardLibraryDirectory = resolveStandardLibraryDirectory(appRoot, compilerOptions);
+  const tsDiagnostics = [];
+  if (standardLibraryDirectory) {
+    const program = import_typescript.default.createProgram(rootNames, compilerOptions, hostWithVirtualFiles(files, compilerOptions, standardLibraryDirectory));
+    for (const diagnostic of import_typescript.default.getPreEmitDiagnostics(program)) {
+      const file = diagnostic.file;
+      if (!file)
+        continue;
+      const normalizedFile = import_node_path2.normalize(file.fileName);
+      const isVirtual = normalizedFile === import_node_path2.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 };
+      if (!sourcePosition)
+        continue;
+      tsDiagnostics.push({
+        code: `WRN-TYPE-${diagnostic.code}`,
+        category: category(diagnostic.category),
+        message: import_typescript.default.flattenDiagnosticMessageText(diagnostic.messageText, `
 `),
-      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."
-    };
-  });
+        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 ? [] : runtimeDiagnostics(source, virtual.ast, filePath),
     ...componentUsageDiagnostics(source, virtual.ast, filePath, appRoot),
@@ -172527,11 +172721,30 @@ function send(value) {
 function result(id, value) {
   send({ jsonrpc: "2.0", id, result: value });
 }
+function internalDiagnostic(document, error) {
+  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) {
+  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)}
+`);
+    return [internalDiagnostic(document, error)];
+  }
+}
 function publish(document) {
   send({
     jsonrpc: "2.0",
     method: "textDocument/publishDiagnostics",
-    params: { uri: document.uri, diagnostics: documentDiagnostics(document) }
+    params: { uri: document.uri, diagnostics: safeDocumentDiagnostics(document) }
   });
 }
 function clearDiagnosticTimer(uri) {
@@ -172567,7 +172780,7 @@ async function handle(message) {
     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,
@@ -172676,7 +172889,7 @@ async function handle(message) {
         result(message.id, []);
         break;
       }
-      const actions = documentDiagnostics(document).filter((item) => item.code === "WRNA11Y001").map((item) => ({
+      const actions = safeDocumentDiagnostics(document).filter((item) => item.code === "WRNA11Y001").map((item) => ({
         title: "Add empty alt attribute",
         kind: "quickfix",
         diagnostics: [item],
@@ -172740,12 +172953,36 @@ function consume() {
       return;
     const body = buffer.subarray(bodyStart, bodyStart + length).toString();
     buffer = buffer.subarray(bodyStart + length);
-    handle(JSON.parse(body));
+    let message;
+    try {
+      message = JSON.parse(body);
+    } catch (error) {
+      process.stderr.write(`[wrnexus-lsp] invalid JSON-RPC payload: ${error instanceof Error ? error.message : String(error)}
+`);
+      continue;
+    }
+    handle(message).catch((error) => {
+      const detail = error instanceof Error ? error.stack ?? error.message : String(error);
+      process.stderr.write(`[wrnexus-lsp] request failed: ${detail}
+`);
+      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)}
+`);
+  }
 });
 process.stdin.resume();
-})
+})(exports, require, module, __filename, __dirname);
diff --git a/editors/vscode/syntaxes/wrn.tmLanguage.json b/editors/vscode/syntaxes/wrn.tmLanguage.json
index ba2e4685..96994153 100644
--- a/editors/vscode/syntaxes/wrn.tmLanguage.json
+++ b/editors/vscode/syntaxes/wrn.tmLanguage.json
@@ -195,8 +195,12 @@
       "patterns": [
         {
           "include": "source.ts"
+        },
+        {
+          "include": "#ts-braces"
         }
-      ]
+      ],
+      "applyEndPatternLast": true
     },
     "props-block": {
       "begin": "\\b(props)\\b\\s*(\\{)",
@@ -254,8 +258,15 @@
         },
         {
           "include": "#strings"
+        },
+        {
+          "include": "#ts-braces"
+        },
+        {
+          "include": "source.ts"
         }
-      ]
+      ],
+      "applyEndPatternLast": true
     },
     "seo-block": {
       "begin": "\\b(seo)\\b\\s*(\\{)",
@@ -651,7 +662,8 @@
         {
           "include": "text.html.basic"
         }
-      ]
+      ],
+      "applyEndPatternLast": true
     },
     "wire-event": {
       "match": "(@)(?:(window|document)(:))?([A-Za-z][A-Za-z0-9_-]*)(\\s*=)",
@@ -1067,8 +1079,15 @@
               "name": "punctuation.definition.parameters.end.wrn"
             }
           }
+        },
+        {
+          "include": "#ts-braces"
+        },
+        {
+          "include": "source.ts"
         }
-      ]
+      ],
+      "applyEndPatternLast": true
     },
     "grouped-state-block": {
       "begin": "\\b(?:(client|server|shared)\\s+)?(state)\\b\\s*(\\{)",
diff --git a/editors/vscode/test/component-metadata.test.js b/editors/vscode/test/component-metadata.test.js
index d4a2bbb7..4dd7d1f4 100644
--- a/editors/vscode/test/component-metadata.test.js
+++ b/editors/vscode/test/component-metadata.test.js
@@ -149,3 +149,63 @@ test("extracts v0.6 optional props, declared union options, and outputs", () =>
     ],
   );
 });
+
+test("accepts unresolved dynamic expressions for typed component props", () => {
+  const featureCard = parseComponentMetadata(`component FeatureCard {
+    props {
+      external: boolean = false
+      disabled: boolean = false
+      showArrow: boolean = true
+    }
+    view { 
} + }`); + const source = `component FeatureGrid { + props { items: unknown[] = [] } + view { + {#each items as item} + + {/each} + } + }`; + + assert.equal(attributeValueType("{item.external || false}"), "unknown"); + assert.equal(attributeValueType("{item.showArrow !== false}"), "boolean"); + assert.deepEqual(validateComponentTags(source, new Map([["FeatureCard", featureCard]])), []); +}); + +test("continues to reject invalid literal boolean props", () => { + const target = parseComponentMetadata(`component Toggle { + props { enabled: boolean = false } + view { } + }`); + const diagnostics = validateComponentTags( + ``, + new Map([["Toggle", target]]), + ); + assert.deepEqual( + diagnostics.map(({ code }) => code), + ["wrn-component-prop-type"], + ); +}); + +test("accepts unresolved dynamic expressions for declared string options", () => { + const target = parseComponentMetadata(`component FeatureCard { + props { target: "_blank" | "_self" = "_self" } + view {
} + }`); + const dynamic = ``; + assert.deepEqual(validateComponentTags(dynamic, new Map([["FeatureCard", target]])), []); + + const invalidLiteral = validateComponentTags( + ``, + new Map([["FeatureCard", target]]), + ); + assert.deepEqual( + invalidLiteral.map(({ code }) => code), + ["wrn-component-prop-option"], + ); +}); diff --git a/editors/vscode/test/diagnostics.test.js b/editors/vscode/test/diagnostics.test.js index d769d574..c0ce36d4 100644 --- a/editors/vscode/test/diagnostics.test.js +++ b/editors/vscode/test/diagnostics.test.js @@ -201,3 +201,36 @@ page Test { assert.deepEqual(validateHtmlTags(mockDocument(source), source), []); }); + +test("does not interpret TypeScript generic types in outputs as HTML tags", () => { + const source = `component AuthForm { + outputs { + change(payload: { values?: Array; sourceEvent?: Event }) + } + view {
Sign in
} +}`; + + assert.deepEqual(validateHtmlTags(mockDocument(source), source), []); +}); + +test("allows JavaScript-looking documentation text inside pre and code", () => { + const source = `page Docs { + view { +
// One project, one language
+page Dashboard {
+  <button>Ship</button>
+}
+ } +}`; + + assert.deepEqual(validateHtmlTags(mockDocument(source), source), []); +}); + +test("still reports genuinely mismatched view tags", () => { + const source = `page Broken { + view {
Broken
} +}`; + const diagnostics = validateHtmlTags(mockDocument(source), source); + assert.equal(diagnostics.length, 1); + assert.equal(diagnostics[0].code, "wrn-mismatched-html-tag"); +}); diff --git a/editors/vscode/test/formatter.test.js b/editors/vscode/test/formatter.test.js index 4dc9ca1c..417e7469 100644 --- a/editors/vscode/test/formatter.test.js +++ b/editors/vscode/test/formatter.test.js @@ -255,3 +255,48 @@ test("formats v0.6 grouped state and outputs blocks", () => { /\n {2}outputs \{\n {4}confirm\(payload: ConfirmPayload\)\n {4}cancel\(\)\n {2}\}/, ); }); + +test("preserves preformatted WRN code examples", () => { + const source = `page Docs { + view { +
// One project, one language
+page ProductDashboard {
+  state count: number = 0
+  <button @click='count++'>Ship {count}</button>
+}
+ } +} +`; + const options = { insertSpaces: true, tabSize: 2, printWidth: 100 }; + const formatted = formatWrn(source, options); + assert.match( + formatted, + /
\/\/ One project, one language<\/span>\npage<\/span>/,
+  );
+  assert.equal(formatWrn(formatted, options), formatted);
+});
+
+test("keeps attribute-free opening tags intact when content is long", () => {
+  const source = `page Docs {
+  view {
+    

This documentation sentence is deliberately long enough to exceed the formatter print width without splitting the p opening delimiter.

+ } +} +`; + const options = { insertSpaces: true, tabSize: 2, printWidth: 70 }; + const formatted = formatWrn(source, options); + assert.match(formatted, /^ {4}

$/m); + assert.doesNotMatch(formatted, /^ {4}$/m); + assert.equal(formatWrn(formatted, options), formatted); +}); + +test("preserves balanced compact sibling markup without indentation drift", () => { + const source = `page Compact { + view { +

PageAPI
+ } +}\n`; + const formatted = formatWrn(source, { tabSize: 2, printWidth: 80 }); + assert.match(formatted, /
Page<\/span>→<\/b>API<\/span><\/div>/); + assert.equal(formatWrn(formatted, { tabSize: 2, printWidth: 80 }), formatted); +}); diff --git a/editors/vscode/test/language-server.test.js b/editors/vscode/test/language-server.test.js new file mode 100644 index 00000000..364f35c6 --- /dev/null +++ b/editors/vscode/test/language-server.test.js @@ -0,0 +1,141 @@ +"use strict"; + +const assert = require("node:assert"); +const { spawnSync } = require("node:child_process"); +const fs = require("node:fs"); +const path = require("node:path"); +const { test } = require("node:test"); + +const root = path.resolve(__dirname, "../../.."); +const server = path.join(root, "editors", "vscode", "src", "language-server.cjs"); + +function rpc(value) { + const body = Buffer.from(JSON.stringify(value)); + return Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r\n\r\n`), body]); +} + +function parseResponses(output) { + const responses = []; + let position = 0; + while (position < output.length) { + const headerEnd = output.indexOf(Buffer.from("\r\n\r\n"), position); + if (headerEnd < 0) break; + const header = output.subarray(position, headerEnd).toString(); + const length = Number(/Content-Length:\s*(\d+)/i.exec(header)?.[1] || 0); + const start = headerEnd + 4; + const end = start + length; + if (!length || end > output.length) break; + responses.push(JSON.parse(output.subarray(start, end).toString())); + position = end; + } + return responses; +} + +function run(messages) { + const result = spawnSync(process.execPath, [server], { + cwd: root, + input: Buffer.concat(messages.map(rpc)), + timeout: 10_000, + }); + assert.equal(result.status, 0, (result.stderr || Buffer.alloc(0)).toString()); + return parseResponses(result.stdout || Buffer.alloc(0)); +} + +test("language server starts under Node and keeps official typed components clean", () => { + const files = [ + path.join(root, "packages", "ui", "components", "AuthForm.wrn"), + path.join(root, "packages", "ui", "components", "FeatureGrid.wrn"), + ]; + const messages = [ + { jsonrpc: "2.0", id: 1, method: "initialize", params: { rootPath: root } }, + { jsonrpc: "2.0", method: "initialized", params: {} }, + ]; + files.forEach((file, index) => { + messages.push({ + jsonrpc: "2.0", + method: "textDocument/didOpen", + params: { + textDocument: { + uri: `file://${file}`, + languageId: "wrn", + version: index + 1, + text: fs.readFileSync(file, "utf8"), + }, + }, + }); + }); + messages.push({ jsonrpc: "2.0", id: 2, method: "shutdown", params: {} }); + messages.push({ jsonrpc: "2.0", method: "exit", params: {} }); + + const responses = run(messages); + assert.equal(responses.find((item) => item.id === 1)?.result?.serverInfo?.version, "0.8.3"); + const diagnostics = responses.filter((item) => item.method === "textDocument/publishDiagnostics"); + assert.equal(diagnostics.length, 2); + assert.deepEqual( + diagnostics.map((item) => item.params.diagnostics), + [[], []], + ); +}); + +test("language server formatter preserves code samples and compact sibling markup", () => { + const source = `page Docs { +view { +
// One project, one language
+
PageAPI
+} +}\n`; + const uri = "file:///tmp/wrnexus-docs.wrn"; + 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: source } }, + }, + { + jsonrpc: "2.0", + id: 2, + method: "textDocument/formatting", + params: { textDocument: { uri }, options: { tabSize: 2, insertSpaces: true } }, + }, + { jsonrpc: "2.0", id: 3, method: "shutdown", params: {} }, + { jsonrpc: "2.0", method: "exit", params: {} }, + ]); + const formatted = responses.find((item) => item.id === 2)?.result?.[0]?.newText; + assert.ok(formatted); + assert.match(formatted, /\/\/ One project, one language/); + assert.match(formatted, /Page<\/span>→<\/b>API<\/span>/); +}); + +test("language server runs TypeScript diagnostics with workspace standard libraries", () => { + const uri = "file:///tmp/wrnexus-invalid-state.wrn"; + const source = `component InvalidState { + outputs { save(payload: { id: string }) } + functions { + client function submit(value: string): void { + output.save({ id: 123 }) + } + } + 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: source } }, + }, + { jsonrpc: "2.0", id: 2, method: "shutdown", params: {} }, + { jsonrpc: "2.0", method: "exit", params: {} }, + ]); + const diagnostics = responses.find((item) => item.method === "textDocument/publishDiagnostics") + ?.params?.diagnostics; + assert.ok(Array.isArray(diagnostics)); + assert.ok( + diagnostics.some((diagnostic) => diagnostic.code === "WRN-TYPE-2322"), + JSON.stringify(diagnostics, null, 2), + ); +}); diff --git a/package.json b/package.json index f7c6a5fc..6ac4583d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wrnexus", - "version": "0.8.3", + "version": "0.8.4", "private": true, "type": "module", "description": "An SSR-first full-stack web framework with server-rendered reactive components. Bun-first, Node-friendly.", diff --git a/packages/ai/package.json b/packages/ai/package.json index 24b1fc74..02ab7bd2 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -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.", diff --git a/packages/auth/package.json b/packages/auth/package.json index 8dfc51d5..37d83d22 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -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, diff --git a/packages/authz/package.json b/packages/authz/package.json index 30bffdc7..a1dcf4ec 100644 --- a/packages/authz/package.json +++ b/packages/authz/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/authz", - "version": "0.8.3", + "version": "0.8.4", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/benchmark/package.json b/packages/benchmark/package.json index dc7c5f6d..d966a4bc 100644 --- a/packages/benchmark/package.json +++ b/packages/benchmark/package.json @@ -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", diff --git a/packages/cache/package.json b/packages/cache/package.json index 7770b1c8..a469ff33 100644 --- a/packages/cache/package.json +++ b/packages/cache/package.json @@ -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", diff --git a/packages/captcha/package.json b/packages/captcha/package.json index 8762f931..3b33aec7 100644 --- a/packages/captcha/package.json +++ b/packages/captcha/package.json @@ -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, diff --git a/packages/cli/package.json b/packages/cli/package.json index 9ec1f2e7..2ef75a6d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/cli/src/update.ts b/packages/cli/src/update.ts index de03e600..9efc78c9 100644 --- a/packages/cli/src/update.ts +++ b/packages/cli/src/update.ts @@ -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. */ diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 6847ad93..e6f5bcc0 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/content/package.json b/packages/content/package.json index 70c95142..d2da2f9f 100644 --- a/packages/content/package.json +++ b/packages/content/package.json @@ -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", diff --git a/packages/core/package.json b/packages/core/package.json index 0eab294e..e5b316c6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/csr/package.json b/packages/csr/package.json index 0b9b685e..d7043d18 100644 --- a/packages/csr/package.json +++ b/packages/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/db/package.json b/packages/db/package.json index bd42a3e8..9d6aa074 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.8.3", + "version": "0.8.4", "private": true, "type": "module", "main": "./src/index.ts", diff --git a/packages/dev-server/package.json b/packages/dev-server/package.json index 97298b3a..33256894 100644 --- a/packages/dev-server/package.json +++ b/packages/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/dev-toolbar/package.json b/packages/dev-toolbar/package.json index 7a72f241..251f0b2e 100644 --- a/packages/dev-toolbar/package.json +++ b/packages/dev-toolbar/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-toolbar", - "version": "0.8.3", + "version": "0.8.4", "private": true, "type": "module", "sideEffects": false, diff --git a/packages/encryption/package.json b/packages/encryption/package.json index e3418d56..d4d40b76 100644 --- a/packages/encryption/package.json +++ b/packages/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.8.3", + "version": "0.8.4", "private": true, "type": "module", "main": "./src/index.ts", diff --git a/packages/graphql/package.json b/packages/graphql/package.json index f914813c..95ccc651 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/graphql", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "main": "./src/index.ts", "exports": { diff --git a/packages/helpers/package.json b/packages/helpers/package.json index 6d3234cd..5ec872f1 100644 --- a/packages/helpers/package.json +++ b/packages/helpers/package.json @@ -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.", diff --git a/packages/i18n/package.json b/packages/i18n/package.json index cf933c9d..2eb7aab6 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.8.3", + "version": "0.8.4", "private": true, "type": "module", "main": "./src/index.ts", diff --git a/packages/identity/package.json b/packages/identity/package.json index 8a08b70f..e79fdbfb 100644 --- a/packages/identity/package.json +++ b/packages/identity/package.json @@ -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", diff --git a/packages/image/package.json b/packages/image/package.json index 1646eb48..fda6e747 100644 --- a/packages/image/package.json +++ b/packages/image/package.json @@ -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", diff --git a/packages/jwt/package.json b/packages/jwt/package.json index 0fceb691..af76ba1c 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.8.3", + "version": "0.8.4", "private": true, "type": "module", "main": "./src/index.ts", diff --git a/packages/language-server/package.json b/packages/language-server/package.json index f240d418..6519b200 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -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", diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index 5a76ce5d..c8b2b6c7 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -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 { 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 { 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(); diff --git a/packages/mcp/package.json b/packages/mcp/package.json index a107e1fd..ea3c1ced 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -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", diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 687faf7a..e42376fa 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/native/package.json b/packages/native/package.json index 2da17559..97db4e9f 100644 --- a/packages/native/package.json +++ b/packages/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/oauth/package.json b/packages/oauth/package.json index b70efccf..f3bee46e 100644 --- a/packages/oauth/package.json +++ b/packages/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.8.3", + "version": "0.8.4", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/observability/package.json b/packages/observability/package.json index 7ec6f5e1..04c00d11 100644 --- a/packages/observability/package.json +++ b/packages/observability/package.json @@ -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", diff --git a/packages/playground/package.json b/packages/playground/package.json index cffd13e7..5d63990b 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -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", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index f58e3cad..b2fede70 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/plugin", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/pubsub/package.json b/packages/pubsub/package.json index 7c7eff4a..491f12f3 100644 --- a/packages/pubsub/package.json +++ b/packages/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.8.3", + "version": "0.8.4", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/pwa/package.json b/packages/pwa/package.json index ad98507f..1ab23837 100644 --- a/packages/pwa/package.json +++ b/packages/pwa/package.json @@ -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", diff --git a/packages/queue/package.json b/packages/queue/package.json index 4ed158ab..1f3fb43d 100644 --- a/packages/queue/package.json +++ b/packages/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.8.3", + "version": "0.8.4", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/reactive/package.json b/packages/reactive/package.json index b0a66fe0..4f561bd3 100644 --- a/packages/reactive/package.json +++ b/packages/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/realtime/package.json b/packages/realtime/package.json index 6478c17c..f2381b77 100644 --- a/packages/realtime/package.json +++ b/packages/realtime/package.json @@ -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, diff --git a/packages/router/package.json b/packages/router/package.json index 9edb7b8d..a0f873ef 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/security/package.json b/packages/security/package.json index f75daeec..efc5bee9 100644 --- a/packages/security/package.json +++ b/packages/security/package.json @@ -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", diff --git a/packages/ssr/package.json b/packages/ssr/package.json index 60d7a175..daf70129 100644 --- a/packages/ssr/package.json +++ b/packages/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/store/package.json b/packages/store/package.json index 56c49de0..17653abb 100644 --- a/packages/store/package.json +++ b/packages/store/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/store", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/styles/package.json b/packages/styles/package.json index 14e9edb7..5bcaaebc 100644 --- a/packages/styles/package.json +++ b/packages/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/syntax/package.json b/packages/syntax/package.json index b99970a3..1a2aae8d 100644 --- a/packages/syntax/package.json +++ b/packages/syntax/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/syntax", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/syntax/src/formatter.ts b/packages/syntax/src/formatter.ts index 470a5518..303254a8 100644 --- a/packages/syntax/src/formatter.ts +++ b/packages/syntax/src/formatter.ts @@ -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}`); + } 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 `PageAPI` 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("", 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("$/.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 ( + /)/i.test(value) && + !/<\/pre\s*>/i.test(value.slice(0, value.search(/)/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(" 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"; diff --git a/packages/syntax/test/formatter.test.ts b/packages/syntax/test/formatter.test.ts index be2344ec..4220f71b 100644 --- a/packages/syntax/test/formatter.test.ts +++ b/packages/syntax/test/formatter.test.ts @@ -37,3 +37,49 @@ view { expect(formatted).not.toContain("\r"); }); }); + +test("preserves preformatted code examples while formatting surrounding markup", () => { + const source = `page Docs { + view { +
+
// One project, one language
+page ProductDashboard {
+  state count: number = 0
+  <button @click='count++'>Ship {count}</button>
+}
+
+ } +} +`; + const formatted = formatWrn(source, options); + expect(formatted) + .toContain(`
// One project, one language
+page ProductDashboard {
+  state 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 {
+    

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.

+ } +} +`; + const formatted = formatWrn(source, options); + expect(formatted).toContain("

\n"); + expect(formatted).not.toContain(" "); + expect(formatWrn(formatted, options)).toBe(formatted); +}); + +test("preserves balanced compact sibling markup without indentation drift", () => { + const source = `page Compact { + view { +

PageAPI
+ } +}\n`; + const formatted = formatWrn(source, { tabSize: 2, printWidth: 80 }); + expect(formatted).toContain('
PageAPI
'); + expect(formatWrn(formatted, { tabSize: 2, printWidth: 80 })).toBe(formatted); +}); diff --git a/packages/syntax/test/syntax.test.ts b/packages/syntax/test/syntax.test.ts index 0ef4bff5..88c4a6ae 100644 --- a/packages/syntax/test/syntax.test.ts +++ b/packages/syntax/test/syntax.test.ts @@ -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"); +}); diff --git a/packages/test/package.json b/packages/test/package.json index 135eefe9..02b8de4a 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.8.3", + "version": "0.8.4", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/tracking/package.json b/packages/tracking/package.json index 33e6aea4..9b2dc04b 100644 --- a/packages/tracking/package.json +++ b/packages/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.8.3", + "version": "0.8.4", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/typecheck/package.json b/packages/typecheck/package.json index 5c150274..87b46c7c 100644 --- a/packages/typecheck/package.json +++ b/packages/typecheck/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/typecheck", - "version": "0.8.3", + "version": "0.8.4", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/typecheck/src/index.ts b/packages/typecheck/src/index.ts index 510e718e..655e773f 100644 --- a/packages/typecheck/src/index.ts +++ b/packages/typecheck/src/index.ts @@ -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 & { target: HTMLElement }" : ": unknown"}${param.default ? ` = ${param.default}` : ""}`, + `${param.name}${param.optional ? "?" : ""}${param.valueType ? `: ${param.valueType}` : param.name === "event" ? ": CustomEvent & { 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")}`, ) .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, 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 ? [] diff --git a/packages/typecheck/test/imports-and-components.test.ts b/packages/typecheck/test/imports-and-components.test.ts index 68638fd2..0abd2863 100644 --- a/packages/typecheck/test/imports-and-components.test.ts +++ b/packages/typecheck/test/imports-and-components.test.ts @@ -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 { } + }`, + ); + const filePath = join(root, "app", "pages", "home.wrn"); + const diagnostics = checkWrnSource( + `import Toggle from "@/components/Toggle.wrn" +page Home { + view { } +}`, + { 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); diff --git a/packages/typecheck/test/typecheck.test.ts b/packages/typecheck/test/typecheck.test.ts index 8deb2b7e..5b9532c1 100644 --- a/packages/typecheck/test/typecheck.test.ts +++ b/packages/typecheck/test/typecheck.test.ts @@ -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 { } + }`, + { appRoot: root, filePath: join(root, "app", "components", "Demo.wrn") }, + ); + expect(diagnostics.filter((diagnostic) => diagnostic.code.startsWith("WRN-TYPE-"))).toEqual([]); +}); diff --git a/packages/ui/components/AuthForm.wrn b/packages/ui/components/AuthForm.wrn index b5aee449..4423d7d7 100644 --- a/packages/ui/components/AuthForm.wrn +++ b/packages/ui/components/AuthForm.wrn @@ -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} {/if} diff --git a/packages/ui/package.json b/packages/ui/package.json index 465bdcb8..71a99bd6 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.8.3", + "version": "0.8.4", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/uploader/package.json b/packages/uploader/package.json index 2d577ee4..e98c9c73 100644 --- a/packages/uploader/package.json +++ b/packages/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.8.3", + "version": "0.8.4", "private": true, "type": "module", "main": "./src/index.ts", diff --git a/packages/validation/package.json b/packages/validation/package.json index a32892e3..8cfb831a 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.8.3", + "version": "0.8.4", "private": true, "type": "module", "main": "./src/index.ts", diff --git a/scripts/build-editor-extension.mjs b/scripts/build-editor-extension.mjs index 5c0c6857..dfbc65cc 100644 --- a/scripts/build-editor-extension.mjs +++ b/scripts/build-editor-extension.mjs @@ -1,31 +1,141 @@ -import console from "node:console"; -import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { createHash } from "node:crypto"; +import { + existsSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; -import { join, relative, resolve } from "node:path"; +import { join, relative, resolve, sep } from "node:path"; import { spawnSync } from "node:child_process"; import process from "node:process"; +import { fileURLToPath } from "node:url"; const root = resolve(import.meta.dirname, ".."); const source = join(root, "editors", "vscode", "src", "extension.js"); const destination = join(root, "editors", "vscode", "src", "extension.bundle.cjs"); +const scriptPath = fileURLToPath(import.meta.url); const check = process.argv.includes("--check"); -const temporary = check ? mkdtempSync(join(tmpdir(), "wrnexus-editor-extension-")) : null; -const output = temporary ? join(temporary, "extension.bundle.cjs") : destination; +const temporary = mkdtempSync(join(tmpdir(), "wrnexus-editor-extension-")); +const output = join(temporary, "extension.bundle.cjs"); + +function normalizeText(value) { + return value.replace(/\r\n?/g, "\n"); +} + +function sha256(value) { + return createHash("sha256").update(value).digest("hex"); +} + +function walk(directory) { + const files = []; + for (const entry of readdirSync(directory)) { + const path = join(directory, entry); + const stat = statSync(path); + if (stat.isDirectory()) { + files.push(...walk(path)); + continue; + } + if (!stat.isFile()) continue; + if (!/\.(?:js|json|cjs)$/.test(path)) continue; + if (path === destination || path.endsWith(`${sep}language-server.cjs`)) continue; + files.push(path); + } + return files; +} + +const sourceFiles = [ + ...walk(join(root, "editors", "vscode", "src")), + join(root, "editors", "vscode", "package.json"), + join(root, "editors", "vscode", "language-configuration.json"), +].sort(); + +function fingerprint(files) { + const hash = createHash("sha256"); + for (const file of files) { + hash.update(relative(root, file).split(sep).join("/")); + hash.update("\0"); + hash.update(normalizeText(readFileSync(file, "utf8"))); + hash.update("\0"); + } + return hash.digest("hex"); +} + +const sourceHash = fingerprint(sourceFiles); +const generatorHash = sha256(normalizeText(readFileSync(scriptPath, "utf8"))); + +function stampBundle(bundle) { + const withoutOldStamp = normalizeText(bundle) + .replace(/^\/\/ WRN editor extension source hash: [a-f0-9]{64}\n/m, "") + .replace(/^\/\/ WRN editor extension generator hash: [a-f0-9]{64}\n/m, ""); + const lines = withoutOldStamp.split("\n"); + const insertion = lines[0]?.startsWith("#!") ? 1 : 0; + lines.splice( + insertion, + 0, + `// WRN editor extension source hash: ${sourceHash}`, + `// WRN editor extension generator hash: ${generatorHash}`, + ); + return `${lines.join("\n").replace(/\n+$/, "")}\n`; +} + +function verifySyntax(file) { + const result = spawnSync(process.execPath, ["--check", file], { + cwd: root, + encoding: "utf8", + timeout: 10_000, + }); + if (result.error) throw result.error; + if (result.status !== 0) { + throw new Error( + `VS Code extension bundle syntax check failed.\n${result.stderr || result.stdout || `exit ${result.status}`}`, + ); + } +} + try { const built = spawnSync( process.env.WRNEXUS_BUN_BINARY || "bun", ["build", source, "--target=node", "--format=cjs", "--external=vscode", `--outfile=${output}`], { cwd: root, encoding: "utf8" }, ); - if (built.status !== 0) + if (built.status !== 0) { throw new Error(built.stderr || built.stdout || "extension bundle failed"); + } + + const generated = stampBundle(readFileSync(output, "utf8")); + writeFileSync(output, generated, "utf8"); + verifySyntax(output); + if (check) { - if (!existsSync(destination) || readFileSync(destination).compare(readFileSync(output)) !== 0) + const existing = existsSync(destination) + ? normalizeText(readFileSync(destination, "utf8")) + : ""; + const embeddedSourceHash = existing.match( + /^\/\/ WRN editor extension source hash: ([a-f0-9]{64})$/m, + )?.[1]; + const embeddedGeneratorHash = existing.match( + /^\/\/ WRN editor extension generator hash: ([a-f0-9]{64})$/m, + )?.[1]; + const exact = existing === generated; + const sourceCompatible = + embeddedSourceHash === sourceHash && embeddedGeneratorHash === generatorHash; + if (!exact && !sourceCompatible) { throw new Error( - `${relative(root, destination)} is stale. Run bun run --cwd editors/vscode build.`, + `${relative(root, destination)} is stale. Run bun run --cwd editors/vscode build and commit the result.`, ); - console.log(`Verified ${relative(root, destination)} matches extension sources.`); - } else console.log(`Built ${relative(root, destination)}.`); + } + verifySyntax(destination); + process.stdout.write( + `Verified ${relative(root, destination)} matches the extension sources and has valid Node syntax.\n`, + ); + } else { + writeFileSync(destination, generated, "utf8"); + process.stdout.write(`Built ${relative(root, destination)}.\n`); + } } finally { - if (temporary) rmSync(temporary, { recursive: true, force: true }); + rmSync(temporary, { recursive: true, force: true }); } diff --git a/scripts/build-editor-language-server.mjs b/scripts/build-editor-language-server.mjs index 22968847..eeeb9059 100644 --- a/scripts/build-editor-language-server.mjs +++ b/scripts/build-editor-language-server.mjs @@ -1,16 +1,124 @@ -import console from "node:console"; -import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { Buffer } from "node:buffer"; +import { createHash } from "node:crypto"; +import { + existsSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; -import { join, relative, resolve } from "node:path"; +import { join, relative, resolve, sep } from "node:path"; import { spawnSync } from "node:child_process"; import process from "node:process"; +import { fileURLToPath } from "node:url"; const root = resolve(import.meta.dirname, ".."); const source = join(root, "packages", "language-server", "src", "server.ts"); const destination = join(root, "editors", "vscode", "src", "language-server.cjs"); +const scriptPath = fileURLToPath(import.meta.url); const check = process.argv.includes("--check"); -const temporary = check ? mkdtempSync(join(tmpdir(), "wrnexus-editor-lsp-")) : null; -const output = temporary ? join(temporary, "language-server.cjs") : destination; +const temporary = mkdtempSync(join(tmpdir(), "wrnexus-editor-lsp-")); +const output = join(temporary, "language-server.cjs"); + +function normalizeText(value) { + return value.replace(/\r\n?/g, "\n"); +} + +function sha256(value) { + return createHash("sha256").update(value).digest("hex"); +} + +function walk(directory) { + const files = []; + for (const entry of readdirSync(directory)) { + const path = join(directory, entry); + const stat = statSync(path); + if (stat.isDirectory()) files.push(...walk(path)); + else if (stat.isFile() && path.endsWith(".ts") && !path.endsWith(".test.ts")) files.push(path); + } + return files; +} + +const sourceFiles = [ + ...walk(join(root, "packages", "language-server", "src")), + ...walk(join(root, "packages", "syntax", "src")), + ...walk(join(root, "packages", "typecheck", "src")), + join(root, "packages", "language-server", "package.json"), + join(root, "packages", "syntax", "package.json"), + join(root, "packages", "typecheck", "package.json"), +].sort(); + +function fingerprint(files) { + const hash = createHash("sha256"); + for (const file of files) { + hash.update(relative(root, file).split(sep).join("/")); + hash.update("\0"); + hash.update(normalizeText(readFileSync(file, "utf8"))); + hash.update("\0"); + } + return hash.digest("hex"); +} + +const sourceHash = fingerprint(sourceFiles); +const generatorHash = sha256(normalizeText(readFileSync(scriptPath, "utf8"))); + +function makeNodeExecutable(bundle) { + let value = normalizeText(bundle).replace(/^#!\/usr\/bin\/env bun\n/, "#!/usr/bin/env node\n"); + const bunWrapper = "(function(exports, require, module, __filename, __dirname) {"; + const invocation = "})(exports, require, module, __filename, __dirname);"; + if ( + value.includes("// @bun @bun-cjs") && + value.includes(bunWrapper) && + !value.trimEnd().endsWith(invocation) + ) { + value = value.replace(/\}\)\s*$/, `${invocation}\n`); + } + return value; +} + +function stampBundle(bundle) { + const withoutOldStamp = bundle + .replace(/^\/\/ WRN editor language server source hash: [a-f0-9]{64}\n/m, "") + .replace(/^\/\/ WRN editor language server generator hash: [a-f0-9]{64}\n/m, ""); + const lines = withoutOldStamp.split("\n"); + const insertion = lines[0]?.startsWith("#!") ? 1 : 0; + lines.splice( + insertion, + 0, + `// WRN editor language server source hash: ${sourceHash}`, + `// WRN editor language server generator hash: ${generatorHash}`, + ); + return `${lines.join("\n").replace(/\n+$/, "")}\n`; +} + +function rpc(value) { + const body = JSON.stringify(value); + return `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`; +} + +function verifyStarts(file) { + const input = [ + rpc({ jsonrpc: "2.0", id: 1, method: "initialize", params: { rootPath: root } }), + rpc({ jsonrpc: "2.0", id: 2, method: "shutdown", params: {} }), + rpc({ jsonrpc: "2.0", method: "exit", params: {} }), + ].join(""); + const result = spawnSync(process.execPath, [file], { + cwd: root, + input, + encoding: "utf8", + timeout: 10_000, + }); + if (result.error) throw result.error; + if (result.status !== 0 || !result.stdout.includes("WRNexus Language Server")) { + throw new Error( + `VS Code language server smoke test failed.\n${result.stderr || result.stdout || `exit ${result.status}`}`, + ); + } +} + try { const built = spawnSync( process.env.WRNEXUS_BUN_BINARY || "bun", @@ -18,13 +126,39 @@ try { { cwd: root, encoding: "utf8" }, ); if (built.status !== 0) throw new Error(built.stderr || built.stdout || "LSP bundle failed"); + + const generated = stampBundle(makeNodeExecutable(readFileSync(output, "utf8"))); + writeFileSync(output, generated, "utf8"); + verifyStarts(output); + if (check) { - if (!existsSync(destination) || readFileSync(destination).compare(readFileSync(output)) !== 0) + const existing = existsSync(destination) + ? normalizeText(readFileSync(destination, "utf8")) + : ""; + const embeddedSourceHash = existing.match( + /^\/\/ WRN editor language server source hash: ([a-f0-9]{64})$/m, + )?.[1]; + const embeddedGeneratorHash = existing.match( + /^\/\/ WRN editor language server generator hash: ([a-f0-9]{64})$/m, + )?.[1]; + const exact = existing === generated; + const sourceCompatible = + embeddedSourceHash === sourceHash && embeddedGeneratorHash === generatorHash; + if (!exact && !sourceCompatible) { throw new Error( - `${relative(root, destination)} is stale. Run bun run --cwd editors/vscode build.`, + `${relative(root, destination)} is stale. Run bun run --cwd editors/vscode build and commit the result.`, ); - console.log(`Verified ${relative(root, destination)} matches the language server sources.`); - } else console.log(`Built ${relative(root, destination)}.`); + } + verifyStarts(destination); + process.stdout.write( + `Verified ${relative(root, destination)} matches the language server sources and starts under Node.\n`, + ); + } else { + writeFileSync(destination, generated, "utf8"); + process.stdout.write( + `Built ${relative(root, destination)} and verified its Node entrypoint.\n`, + ); + } } finally { - if (temporary) rmSync(temporary, { recursive: true, force: true }); + rmSync(temporary, { recursive: true, force: true }); } diff --git a/update-package-versions.mjs b/update-package-versions.mjs index 80d7830c..6deecfac 100644 --- a/update-package-versions.mjs +++ b/update-package-versions.mjs @@ -1,7 +1,7 @@ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -const version = "0.8.3"; +const version = "0.8.4"; const dependencyGroups = [ "dependencies",