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

This commit is contained in:
2026-08-04 12:19:09 +05:30
parent 4cebacadfe
commit 72e4d3eceb
108 changed files with 2276 additions and 532 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ai", "name": "@wrnexus/ai",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/authz", "name": "@wrnexus/authz",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "@wrnexus/authz — part of the WrNexus framework.", "description": "@wrnexus/authz — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+17 -17
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "@wrnexus/cli — part of the WrNexus framework.", "description": "@wrnexus/cli — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -44,22 +44,22 @@
"wrnexus": "./dist/index.js" "wrnexus": "./dist/index.js"
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.8.3", "@wrnexus/core": "^0.8.4",
"@wrnexus/router": "^0.8.3", "@wrnexus/router": "^0.8.4",
"@wrnexus/csr": "^0.8.3", "@wrnexus/csr": "^0.8.4",
"@wrnexus/compiler": "^0.8.3", "@wrnexus/compiler": "^0.8.4",
"@wrnexus/styles": "^0.8.3", "@wrnexus/styles": "^0.8.4",
"@wrnexus/dev-server": "^0.8.3", "@wrnexus/dev-server": "^0.8.4",
"@wrnexus/ui": "^0.8.3", "@wrnexus/ui": "^0.8.4",
"@wrnexus/validation": "^0.8.3", "@wrnexus/validation": "^0.8.4",
"@wrnexus/i18n": "^0.8.3", "@wrnexus/i18n": "^0.8.4",
"@wrnexus/mcp": "^0.8.3", "@wrnexus/mcp": "^0.8.4",
"@wrnexus/playground": "^0.8.3", "@wrnexus/playground": "^0.8.4",
"@wrnexus/db": "^0.8.3", "@wrnexus/db": "^0.8.4",
"@wrnexus/plugin": "^0.8.3", "@wrnexus/plugin": "^0.8.4",
"@wrnexus/syntax": "^0.8.3", "@wrnexus/syntax": "^0.8.4",
"@wrnexus/typecheck": "^0.8.3", "@wrnexus/typecheck": "^0.8.4",
"@wrnexus/security": "^0.8.3", "@wrnexus/security": "^0.8.4",
"selfsigned": "^5.5.0" "selfsigned": "^5.5.0"
}, },
"files": [ "files": [
+5 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/compiler", "name": "@wrnexus/compiler",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "@wrnexus/compiler — part of the WrNexus framework.", "description": "@wrnexus/compiler — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -37,10 +37,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/csr": "^0.8.3", "@wrnexus/csr": "^0.8.4",
"@wrnexus/syntax": "^0.8.3", "@wrnexus/syntax": "^0.8.4",
"@wrnexus/store": "^0.8.3", "@wrnexus/store": "^0.8.4",
"@wrnexus/validation": "^0.8.3" "@wrnexus/validation": "^0.8.4"
}, },
"files": [ "files": [
"dist", "dist",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/core", "name": "@wrnexus/core",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "@wrnexus/core — part of the WrNexus framework.", "description": "@wrnexus/core — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/csr", "name": "@wrnexus/csr",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "@wrnexus/csr — part of the WrNexus framework.", "description": "@wrnexus/csr — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -37,7 +37,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.8.3" "@wrnexus/core": "^0.8.4"
}, },
"files": [ "files": [
"dist", "dist",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/db", "name": "@wrnexus/db",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "Typed database drivers, migrations, instrumentation, repositories, pagination, and transaction helpers.", "description": "Typed database drivers, migrations, instrumentation, repositories, pagination, and transaction helpers.",
"license": "MIT", "license": "MIT",
+20 -20
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "@wrnexus/dev-server — part of the WrNexus framework.", "description": "@wrnexus/dev-server — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -41,25 +41,25 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.8.3", "@wrnexus/core": "^0.8.4",
"@wrnexus/dev-toolbar": "^0.8.3", "@wrnexus/dev-toolbar": "^0.8.4",
"@wrnexus/router": "^0.8.3", "@wrnexus/router": "^0.8.4",
"@wrnexus/ssr": "^0.8.3", "@wrnexus/ssr": "^0.8.4",
"@wrnexus/csr": "^0.8.3", "@wrnexus/csr": "^0.8.4",
"@wrnexus/compiler": "^0.8.3", "@wrnexus/compiler": "^0.8.4",
"@wrnexus/styles": "^0.8.3", "@wrnexus/styles": "^0.8.4",
"@wrnexus/ui": "^0.8.3", "@wrnexus/ui": "^0.8.4",
"@wrnexus/validation": "^0.8.3", "@wrnexus/validation": "^0.8.4",
"@wrnexus/i18n": "^0.8.3", "@wrnexus/i18n": "^0.8.4",
"@wrnexus/db": "^0.8.3", "@wrnexus/db": "^0.8.4",
"@wrnexus/pubsub": "^0.8.3", "@wrnexus/pubsub": "^0.8.4",
"@wrnexus/uploader": "^0.8.3", "@wrnexus/uploader": "^0.8.4",
"@wrnexus/plugin": "^0.8.3", "@wrnexus/plugin": "^0.8.4",
"@wrnexus/store": "^0.8.3", "@wrnexus/store": "^0.8.4",
"@wrnexus/security": "^0.8.3", "@wrnexus/security": "^0.8.4",
"@wrnexus/observability": "^0.8.3", "@wrnexus/observability": "^0.8.4",
"@wrnexus/cache": "^0.8.3", "@wrnexus/cache": "^0.8.4",
"@wrnexus/pwa": "^0.8.3" "@wrnexus/pwa": "^0.8.4"
}, },
"files": [ "files": [
"dist", "dist",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/dev-toolbar", "name": "@wrnexus/dev-toolbar",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "@wrnexus/dev-toolbar — part of the WrNexus framework.", "description": "@wrnexus/dev-toolbar — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/encryption", "name": "@wrnexus/encryption",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "Authenticated encryption, key rotation, hashing, and optional application-layer encrypted HTTP envelopes.", "description": "Authenticated encryption, key rotation, hashing, and optional application-layer encrypted HTTP envelopes.",
"license": "MIT", "license": "MIT",
@@ -37,7 +37,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.8.3" "@wrnexus/core": "^0.8.4"
}, },
"files": [ "files": [
"dist", "dist",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/helpers", "name": "@wrnexus/helpers",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
"license": "MIT", "license": "MIT",
@@ -37,7 +37,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.8.3" "@wrnexus/core": "^0.8.4"
}, },
"files": [ "files": [
"dist", "dist",
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/i18n", "name": "@wrnexus/i18n",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "Locale loading, fallback resolution, SSR/browser translations, formatters, and WRNexusJS language components.", "description": "Locale loading, fallback resolution, SSR/browser translations, formatters, and WRNexusJS language components.",
"license": "MIT", "license": "MIT",
@@ -42,9 +42,9 @@
"./components/*": "./components/*" "./components/*": "./components/*"
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.8.3", "@wrnexus/core": "^0.8.4",
"@wrnexus/plugin": "^0.8.3", "@wrnexus/plugin": "^0.8.4",
"@wrnexus/ui": "^0.8.3" "@wrnexus/ui": "^0.8.4"
}, },
"wrnexus": { "wrnexus": {
"plugin": { "plugin": {
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/jwt", "name": "@wrnexus/jwt",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "HS256 JSON Web Tokens, key rotation, access/refresh helpers, scopes, cookies, and auth middleware.", "description": "HS256 JSON Web Tokens, key rotation, access/refresh helpers, scopes, cookies, and auth middleware.",
"license": "MIT", "license": "MIT",
@@ -37,7 +37,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.8.3" "@wrnexus/core": "^0.8.4"
}, },
"files": [ "files": [
"dist", "dist",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/mobile", "name": "@wrnexus/mobile",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "@wrnexus/mobile — part of the WrNexus framework.", "description": "@wrnexus/mobile — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -37,7 +37,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/native": "^0.8.3" "@wrnexus/native": "^0.8.4"
}, },
"files": [ "files": [
"dist", "dist",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/native", "name": "@wrnexus/native",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "@wrnexus/native — part of the WrNexus framework.", "description": "@wrnexus/native — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/oauth", "name": "@wrnexus/oauth",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "@wrnexus/oauth — part of the WrNexus framework.", "description": "@wrnexus/oauth — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -37,7 +37,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/jwt": "^0.8.3" "@wrnexus/jwt": "^0.8.4"
}, },
"files": [ "files": [
"dist", "dist",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/plugin", "name": "@wrnexus/plugin",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "@wrnexus/plugin — part of the WrNexus framework.", "description": "@wrnexus/plugin — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -49,7 +49,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/syntax": "^0.8.3" "@wrnexus/syntax": "^0.8.4"
}, },
"files": [ "files": [
"dist", "dist",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/pubsub", "name": "@wrnexus/pubsub",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "@wrnexus/pubsub — part of the WrNexus framework.", "description": "@wrnexus/pubsub — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/queue", "name": "@wrnexus/queue",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "@wrnexus/queue — part of the WrNexus framework.", "description": "@wrnexus/queue — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -37,7 +37,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.8.3" "@wrnexus/core": "^0.8.4"
}, },
"files": [ "files": [
"dist", "dist",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/reactive", "name": "@wrnexus/reactive",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "@wrnexus/reactive — part of the WrNexus framework.", "description": "@wrnexus/reactive — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/router", "name": "@wrnexus/router",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "@wrnexus/router — part of the WrNexus framework.", "description": "@wrnexus/router — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -37,8 +37,8 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/compiler": "^0.8.3", "@wrnexus/compiler": "^0.8.4",
"@wrnexus/core": "^0.8.3" "@wrnexus/core": "^0.8.4"
}, },
"files": [ "files": [
"dist", "dist",
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ssr", "name": "@wrnexus/ssr",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "@wrnexus/ssr — part of the WrNexus framework.", "description": "@wrnexus/ssr — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -45,9 +45,9 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.8.3", "@wrnexus/core": "^0.8.4",
"@wrnexus/store": "^0.8.3", "@wrnexus/store": "^0.8.4",
"@wrnexus/security": "^0.8.3" "@wrnexus/security": "^0.8.4"
}, },
"files": [ "files": [
"dist", "dist",
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/styles", "name": "@wrnexus/styles",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "@wrnexus/styles — part of the WrNexus framework.", "description": "@wrnexus/styles — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -37,9 +37,9 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/uploader": "^0.8.3", "@wrnexus/uploader": "^0.8.4",
"@wrnexus/core": "^0.8.3", "@wrnexus/core": "^0.8.4",
"@wrnexus/plugin": "^0.8.3" "@wrnexus/plugin": "^0.8.4"
}, },
"files": [ "files": [
"dist", "dist",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/syntax", "name": "@wrnexus/syntax",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "@wrnexus/syntax — part of the WrNexus framework.", "description": "@wrnexus/syntax — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/test", "name": "@wrnexus/test",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "@wrnexus/test — part of the WrNexus framework.", "description": "@wrnexus/test — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/tracking", "name": "@wrnexus/tracking",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "@wrnexus/tracking — part of the WrNexus framework.", "description": "@wrnexus/tracking — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ui", "name": "@wrnexus/ui",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "@wrnexus/ui — part of the WrNexus framework.", "description": "@wrnexus/ui — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -42,7 +42,7 @@
"./ui.css": "./ui.css" "./ui.css": "./ui.css"
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.8.3" "@wrnexus/core": "^0.8.4"
}, },
"files": [ "files": [
"dist", "dist",
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/uploader", "name": "@wrnexus/uploader",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "Secure upload drivers, policies, client runtime, helper functions, and reusable upload components.", "description": "Secure upload drivers, policies, client runtime, helper functions, and reusable upload components.",
"license": "MIT", "license": "MIT",
@@ -42,9 +42,9 @@
"./components/*": "./components/*" "./components/*": "./components/*"
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.8.3", "@wrnexus/core": "^0.8.4",
"@wrnexus/plugin": "^0.8.3", "@wrnexus/plugin": "^0.8.4",
"@wrnexus/ui": "^0.8.3" "@wrnexus/ui": "^0.8.4"
}, },
"wrnexus": { "wrnexus": {
"plugin": { "plugin": {
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/validation", "name": "@wrnexus/validation",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "Shared server/browser schemas, validation helpers, form runtime, and reusable error components.", "description": "Shared server/browser schemas, validation helpers, form runtime, and reusable error components.",
"license": "MIT", "license": "MIT",
@@ -42,9 +42,9 @@
"./components/*": "./components/*" "./components/*": "./components/*"
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.8.3", "@wrnexus/core": "^0.8.4",
"@wrnexus/plugin": "^0.8.3", "@wrnexus/plugin": "^0.8.4",
"@wrnexus/ui": "^0.8.3" "@wrnexus/ui": "^0.8.4"
}, },
"wrnexus": { "wrnexus": {
"plugin": { "plugin": {
+18
View File
@@ -22,6 +22,24 @@ entry in the CLI migration registry.
- Made the VS Code embedded compiler freshness check deterministic across TypeScript patch - Made the VS Code embedded compiler freshness check deterministic across TypeScript patch
versions, Windows/Linux line endings, and workspace environments by verifying normalized source 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. 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 `<pre><code>` 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 ## 0.8.0 - 2026-08-02
+64
View File
@@ -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<void> {
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"));
+46 -46
View File
@@ -102,11 +102,11 @@
}, },
"packages/ai": { "packages/ai": {
"name": "@wrnexus/ai", "name": "@wrnexus/ai",
"version": "0.8.3", "version": "0.8.4",
}, },
"packages/auth": { "packages/auth": {
"name": "@wrnexus/auth", "name": "@wrnexus/auth",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/authz": "workspace:*", "@wrnexus/authz": "workspace:*",
"@wrnexus/captcha": "workspace:*", "@wrnexus/captcha": "workspace:*",
@@ -128,22 +128,22 @@
}, },
"packages/authz": { "packages/authz": {
"name": "@wrnexus/authz", "name": "@wrnexus/authz",
"version": "0.8.3", "version": "0.8.4",
}, },
"packages/benchmark": { "packages/benchmark": {
"name": "@wrnexus/benchmark", "name": "@wrnexus/benchmark",
"version": "0.8.3", "version": "0.8.4",
}, },
"packages/cache": { "packages/cache": {
"name": "@wrnexus/cache", "name": "@wrnexus/cache",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
}, },
}, },
"packages/captcha": { "packages/captcha": {
"name": "@wrnexus/captcha", "name": "@wrnexus/captcha",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*", "@wrnexus/plugin": "workspace:*",
@@ -158,7 +158,7 @@
}, },
"packages/cli": { "packages/cli": {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.8.3", "version": "0.8.4",
"bin": { "bin": {
"wrnexus": "src/index.ts", "wrnexus": "src/index.ts",
}, },
@@ -184,7 +184,7 @@
}, },
"packages/compiler": { "packages/compiler": {
"name": "@wrnexus/compiler", "name": "@wrnexus/compiler",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/csr": "workspace:*", "@wrnexus/csr": "workspace:*",
"@wrnexus/store": "workspace:*", "@wrnexus/store": "workspace:*",
@@ -194,7 +194,7 @@
}, },
"packages/content": { "packages/content": {
"name": "@wrnexus/content", "name": "@wrnexus/content",
"version": "0.8.3", "version": "0.8.4",
"devDependencies": { "devDependencies": {
"@types/bun": "^1.3.14", "@types/bun": "^1.3.14",
"typescript": "^5.9.2", "typescript": "^5.9.2",
@@ -202,18 +202,18 @@
}, },
"packages/core": { "packages/core": {
"name": "@wrnexus/core", "name": "@wrnexus/core",
"version": "0.8.3", "version": "0.8.4",
}, },
"packages/csr": { "packages/csr": {
"name": "@wrnexus/csr", "name": "@wrnexus/csr",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
}, },
}, },
"packages/db": { "packages/db": {
"name": "@wrnexus/db", "name": "@wrnexus/db",
"version": "0.8.3", "version": "0.8.4",
"devDependencies": { "devDependencies": {
"@types/bun": "^1.3.14", "@types/bun": "^1.3.14",
"typescript": "^5.9.2", "typescript": "^5.9.2",
@@ -221,7 +221,7 @@
}, },
"packages/dev-server": { "packages/dev-server": {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/cache": "workspace:*", "@wrnexus/cache": "workspace:*",
"@wrnexus/compiler": "workspace:*", "@wrnexus/compiler": "workspace:*",
@@ -246,7 +246,7 @@
}, },
"packages/dev-toolbar": { "packages/dev-toolbar": {
"name": "@wrnexus/dev-toolbar", "name": "@wrnexus/dev-toolbar",
"version": "0.8.3", "version": "0.8.4",
"devDependencies": { "devDependencies": {
"@types/bun": "^1.3.14", "@types/bun": "^1.3.14",
"typescript": "^5.9.2", "typescript": "^5.9.2",
@@ -254,7 +254,7 @@
}, },
"packages/encryption": { "packages/encryption": {
"name": "@wrnexus/encryption", "name": "@wrnexus/encryption",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
}, },
@@ -265,21 +265,21 @@
}, },
"packages/graphql": { "packages/graphql": {
"name": "@wrnexus/graphql", "name": "@wrnexus/graphql",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/plugin": "workspace:*", "@wrnexus/plugin": "workspace:*",
}, },
}, },
"packages/helpers": { "packages/helpers": {
"name": "@wrnexus/helpers", "name": "@wrnexus/helpers",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
}, },
}, },
"packages/i18n": { "packages/i18n": {
"name": "@wrnexus/i18n", "name": "@wrnexus/i18n",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*", "@wrnexus/plugin": "workspace:*",
@@ -293,7 +293,7 @@
}, },
"packages/identity": { "packages/identity": {
"name": "@wrnexus/identity", "name": "@wrnexus/identity",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/auth": "workspace:*", "@wrnexus/auth": "workspace:*",
"@wrnexus/authz": "workspace:*", "@wrnexus/authz": "workspace:*",
@@ -302,7 +302,7 @@
}, },
"packages/image": { "packages/image": {
"name": "@wrnexus/image", "name": "@wrnexus/image",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/plugin": "workspace:*", "@wrnexus/plugin": "workspace:*",
"@wrnexus/security": "workspace:*", "@wrnexus/security": "workspace:*",
@@ -322,7 +322,7 @@
}, },
"packages/jwt": { "packages/jwt": {
"name": "@wrnexus/jwt", "name": "@wrnexus/jwt",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
}, },
@@ -333,7 +333,7 @@
}, },
"packages/language-server": { "packages/language-server": {
"name": "@wrnexus/language-server", "name": "@wrnexus/language-server",
"version": "0.8.3", "version": "0.8.4",
"bin": { "bin": {
"wrnexus-language-server": "src/server.ts", "wrnexus-language-server": "src/server.ts",
}, },
@@ -344,39 +344,39 @@
}, },
"packages/mcp": { "packages/mcp": {
"name": "@wrnexus/mcp", "name": "@wrnexus/mcp",
"version": "0.8.3", "version": "0.8.4",
"bin": { "bin": {
"wrnexus-mcp": "src/stdio.ts", "wrnexus-mcp": "src/stdio.ts",
}, },
}, },
"packages/mobile": { "packages/mobile": {
"name": "@wrnexus/mobile", "name": "@wrnexus/mobile",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/native": "workspace:*", "@wrnexus/native": "workspace:*",
}, },
}, },
"packages/native": { "packages/native": {
"name": "@wrnexus/native", "name": "@wrnexus/native",
"version": "0.8.3", "version": "0.8.4",
}, },
"packages/oauth": { "packages/oauth": {
"name": "@wrnexus/oauth", "name": "@wrnexus/oauth",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/jwt": "workspace:*", "@wrnexus/jwt": "workspace:*",
}, },
}, },
"packages/observability": { "packages/observability": {
"name": "@wrnexus/observability", "name": "@wrnexus/observability",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
}, },
}, },
"packages/playground": { "packages/playground": {
"name": "@wrnexus/playground", "name": "@wrnexus/playground",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/compiler": "workspace:*", "@wrnexus/compiler": "workspace:*",
"@wrnexus/syntax": "workspace:*", "@wrnexus/syntax": "workspace:*",
@@ -384,18 +384,18 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@wrnexus/plugin", "name": "@wrnexus/plugin",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/syntax": "workspace:*", "@wrnexus/syntax": "workspace:*",
}, },
}, },
"packages/pubsub": { "packages/pubsub": {
"name": "@wrnexus/pubsub", "name": "@wrnexus/pubsub",
"version": "0.8.3", "version": "0.8.4",
}, },
"packages/pwa": { "packages/pwa": {
"name": "@wrnexus/pwa", "name": "@wrnexus/pwa",
"version": "0.8.3", "version": "0.8.4",
"devDependencies": { "devDependencies": {
"@types/bun": "^1.3.14", "@types/bun": "^1.3.14",
"typescript": "^5.9.2", "typescript": "^5.9.2",
@@ -403,18 +403,18 @@
}, },
"packages/queue": { "packages/queue": {
"name": "@wrnexus/queue", "name": "@wrnexus/queue",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
}, },
}, },
"packages/reactive": { "packages/reactive": {
"name": "@wrnexus/reactive", "name": "@wrnexus/reactive",
"version": "0.8.3", "version": "0.8.4",
}, },
"packages/realtime": { "packages/realtime": {
"name": "@wrnexus/realtime", "name": "@wrnexus/realtime",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*", "@wrnexus/plugin": "workspace:*",
@@ -428,7 +428,7 @@
}, },
"packages/router": { "packages/router": {
"name": "@wrnexus/router", "name": "@wrnexus/router",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/compiler": "workspace:*", "@wrnexus/compiler": "workspace:*",
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
@@ -436,14 +436,14 @@
}, },
"packages/security": { "packages/security": {
"name": "@wrnexus/security", "name": "@wrnexus/security",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
}, },
}, },
"packages/ssr": { "packages/ssr": {
"name": "@wrnexus/ssr", "name": "@wrnexus/ssr",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
"@wrnexus/security": "workspace:*", "@wrnexus/security": "workspace:*",
@@ -452,11 +452,11 @@
}, },
"packages/store": { "packages/store": {
"name": "@wrnexus/store", "name": "@wrnexus/store",
"version": "0.8.3", "version": "0.8.4",
}, },
"packages/styles": { "packages/styles": {
"name": "@wrnexus/styles", "name": "@wrnexus/styles",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*", "@wrnexus/plugin": "workspace:*",
@@ -465,19 +465,19 @@
}, },
"packages/syntax": { "packages/syntax": {
"name": "@wrnexus/syntax", "name": "@wrnexus/syntax",
"version": "0.8.3", "version": "0.8.4",
}, },
"packages/test": { "packages/test": {
"name": "@wrnexus/test", "name": "@wrnexus/test",
"version": "0.8.3", "version": "0.8.4",
}, },
"packages/tracking": { "packages/tracking": {
"name": "@wrnexus/tracking", "name": "@wrnexus/tracking",
"version": "0.8.3", "version": "0.8.4",
}, },
"packages/typecheck": { "packages/typecheck": {
"name": "@wrnexus/typecheck", "name": "@wrnexus/typecheck",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/syntax": "workspace:*", "@wrnexus/syntax": "workspace:*",
"typescript": "^5.5.0", "typescript": "^5.5.0",
@@ -485,14 +485,14 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@wrnexus/ui", "name": "@wrnexus/ui",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
}, },
}, },
"packages/uploader": { "packages/uploader": {
"name": "@wrnexus/uploader", "name": "@wrnexus/uploader",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*", "@wrnexus/plugin": "workspace:*",
@@ -506,7 +506,7 @@
}, },
"packages/validation": { "packages/validation": {
"name": "@wrnexus/validation", "name": "@wrnexus/validation",
"version": "0.8.3", "version": "0.8.4",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*", "@wrnexus/plugin": "workspace:*",
+55
View File
@@ -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 are required. The complete TypeScript and Bun test gates still need to be rerun on the release
machine after `bun install --frozen-lockfile`. 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 `<pre><code>` 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 ## Required final release verification
The preparation environment did not include the Bun executable. Therefore the complete Bun test, The preparation environment did not include the Bun executable. Therefore the complete Bun test,
+1 -1
View File
@@ -7,7 +7,7 @@
"packages/ui/components/AdvancedRangeSlider.wrn": "e78b5cf825503d0cd306c6e490d71317ee3db1844882d13b500df1f7f1c408e1", "packages/ui/components/AdvancedRangeSlider.wrn": "e78b5cf825503d0cd306c6e490d71317ee3db1844882d13b500df1f7f1c408e1",
"packages/ui/components/AdvancedSelect.wrn": "29d43f8019a3e0e98af7cf12440f41523665535a39a973441b4faea78e67114a", "packages/ui/components/AdvancedSelect.wrn": "29d43f8019a3e0e98af7cf12440f41523665535a39a973441b4faea78e67114a",
"packages/ui/components/AnnouncementBar.wrn": "5946ef5eb646f7414056f59d9cfe14680aa3c935acc9a1ca6528dc53b23caa48", "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/AuthSplitLayout.wrn": "72b82495dd14d40a2d594ccdcf3adc925d1567c155750fb1072347b8845aab7b",
"packages/ui/components/AvatarGroup.wrn": "9c583f567213f8e1ad4506a7d2abc3fb462d11679fd2196d3828030f28a28964", "packages/ui/components/AvatarGroup.wrn": "9c583f567213f8e1ad4506a7d2abc3fb462d11679fd2196d3828030f28a28964",
"packages/ui/components/BackToTop.wrn": "b12c56ec8b7aabad68cbdc47f4b3237e2c21a61398f0aa51f9add6229b7357cd", "packages/ui/components/BackToTop.wrn": "b12c56ec8b7aabad68cbdc47f4b3237e2c21a61398f0aa51f9add6229b7357cd",
+15
View File
@@ -5,6 +5,21 @@
- Rebuilt the embedded WRN compiler with the 0.8.3 SSR, computed-value, Async-scope, and typed loop-prop fixes. - 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. - Added compiler awareness for pruned and bundled browser hydration imports.
- Aligned the extension version with the WRNexusJS 0.8.3 framework release. - 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 `<pre><code>` 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 ## 0.8.0
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "wrnexus", "name": "wrnexus",
"version": "0.8.3", "version": "0.8.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "wrnexus", "name": "wrnexus",
"version": "0.8.3", "version": "0.8.4",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"dependencies": { "dependencies": {
"vscode-languageclient": "^10.1.0" "vscode-languageclient": "^10.1.0"
+9 -1
View File
@@ -2,7 +2,7 @@
"name": "wrnexus", "name": "wrnexus",
"displayName": "WRNexus Language Support", "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.", "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", "publisher": "wrnexus",
"private": true, "private": true,
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
@@ -126,6 +126,14 @@
"default": true, "default": true,
"scope": "resource", "scope": "resource",
"description": "Place opening-tag attributes on separate lines, with the closing delimiter on its own line." "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."
} }
} }
}, },
+139 -7
View File
@@ -1,6 +1,6 @@
"use strict"; "use strict";
// Generated by scripts/build-editor-compiler.mjs. Do not edit directly. // 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 // WRN editor compiler generator hash: c71e7fe4258c97b73b384ff14b321f0cf0b30cc2ed0322f5f84b04e757159b18
// Generated with TypeScript: 5.9.3 // Generated with TypeScript: 5.9.3
const __nodeRequire = require; const __nodeRequire = require;
@@ -4845,6 +4845,18 @@ function formatOpeningTag(value, unit, depth, printWidth = 100, multilineAttribu
opensElement, 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}</${parsed.tagName}>`);
}
else if (parsed.remainder) {
lines.push(`${childIndent}${parsed.remainder}`);
}
return { lines, opensElement };
}
const lines = [ const lines = [
`${baseIndent}<${parsed.tagName}`, `${baseIndent}<${parsed.tagName}`,
...parsed.attributes.flatMap((attribute) => formatAttribute(attribute, childIndent, unit)), ...parsed.attributes.flatMap((attribute) => formatAttribute(attribute, childIndent, unit)),
@@ -4886,6 +4898,73 @@ function isMultilineOpeningTagStart(value) {
function isClosingTag(value) { function isClosingTag(value) {
return /^<\/[A-Za-z][\w$:.-]*\s*>/.test(value); return /^<\/[A-Za-z][\w$:.-]*\s*>/.test(value);
} }
/**
* Preserve compact, already-balanced HTML fragments as one line.
*
* A line such as `<span>Page</span><b>→</b><span>API</span>` is valid and
* intentionally compact. Expanding only its first opening tag makes later
* formatter passes treat the remaining siblings as children, causing runaway
* indentation and false closing-tag diagnostics. Balanced fragments are kept
* intact while normal multiline opening tags continue through the formatter.
*/
function isBalancedInlineHtmlFragment(value) {
if (!value.startsWith("<") || value.startsWith("</") || value.startsWith("<!--")) {
return false;
}
const stack = [];
let tagCount = 0;
let rootCount = 0;
let hasNestedElement = false;
let hasOutsideText = false;
let index = 0;
while (index < value.length) {
const tagStart = value.indexOf("<", index);
if (tagStart === -1) {
if (stack.length === 0 && value.slice(index).trim())
hasOutsideText = true;
break;
}
if (stack.length === 0 && value.slice(index, tagStart).trim())
hasOutsideText = true;
if (value.startsWith("<!--", tagStart)) {
const commentEnd = value.indexOf("-->", tagStart + 4);
if (commentEnd === -1)
return false;
index = commentEnd + 3;
continue;
}
const relativeEnd = findOpeningTagEnd(value.slice(tagStart));
if (relativeEnd === -1)
return false;
const tag = value.slice(tagStart, tagStart + relativeEnd + 1);
const match = /^<\/?([A-Za-z][\w$:.-]*)[\s\S]*?>$/.exec(tag);
if (!match) {
index = tagStart + 1;
continue;
}
tagCount += 1;
const tagName = match[1];
const normalizedName = /^[a-z]/.test(tagName) ? tagName.toLowerCase() : tagName;
const closing = tag.startsWith("</");
const selfClosing = /\/\s*>$/.test(tag);
const voidElement = VOID_ELEMENTS.has(tagName.toLowerCase());
if (closing) {
if (stack.at(-1) !== normalizedName)
return false;
stack.pop();
}
else {
if (stack.length === 0)
rootCount += 1;
else
hasNestedElement = true;
if (!selfClosing && !voidElement)
stack.push(normalizedName);
}
index = tagStart + relativeEnd + 1;
}
return (tagCount >= 2 && stack.length === 0 && (rootCount > 1 || hasNestedElement || hasOutsideText));
}
function isControlBlockOpen(value) { function isControlBlockOpen(value) {
return /^\{#(?:if|each)\b[\s\S]*\}$/.test(value); return /^\{#(?:if|each)\b[\s\S]*\}$/.test(value);
} }
@@ -4991,6 +5070,41 @@ function collectOpeningTag(inputLines, startIndex) {
endIndex: index, endIndex: index,
}; };
} }
function isPreservedRawBlockStart(value) {
return (/<pre(?:\s|>)/i.test(value) &&
!/<\/pre\s*>/i.test(value.slice(0, value.search(/<pre(?:\s|>)/i))));
}
function hasPreservedRawBlockEnd(value) {
return /<\/pre\s*>/i.test(value);
}
function transformOutsidePreservedRawBlocks(lines, transformLine) {
const output = [];
let preserving = false;
for (const line of lines) {
if (preserving) {
output.push(line);
if (hasPreservedRawBlockEnd(line))
preserving = false;
continue;
}
if (isPreservedRawBlockStart(line)) {
output.push(line);
preserving = !hasPreservedRawBlockEnd(line);
continue;
}
output.push(...transformLine(line));
}
return output;
}
function collectPreservedRawBlock(lines, startIndex) {
const collected = [lines[startIndex]];
let index = startIndex;
while (!hasPreservedRawBlockEnd(collected.at(-1) || "") && index + 1 < lines.length) {
index += 1;
collected.push(lines[index]);
}
return { lines: collected, endIndex: index };
}
/** /**
* Put WRN template control markers on their own lines before indentation. * Put WRN template control markers on their own lines before indentation.
* *
@@ -5000,7 +5114,7 @@ function collectOpeningTag(inputLines, startIndex) {
*/ */
function expandInlineControlBlocks(lines) { function expandInlineControlBlocks(lines) {
const marker = /(\{#(?:if|each)\b[^}]*\}|\{:(?:else(?:\s+if\b[^}]*)?|empty)\}|\{\/(?:if|each)\})/g; 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)) if (!marker.test(line))
return [line]; return [line];
marker.lastIndex = 0; marker.lastIndex = 0;
@@ -5013,7 +5127,7 @@ function expandInlineControlBlocks(lines) {
}); });
} }
function expandStructuredStateDeclarations(lines, unit) { 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); const match = /^(\s*state\s+[A-Za-z_$][\w$]*\s*=\s*)([\\[{][\s\S]*)$/.exec(line);
if (!match) if (!match)
return [line]; return [line];
@@ -5068,6 +5182,14 @@ function formatWrnPass(source, options = {}) {
index += 1; index += 1;
continue; 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)) { if (isMultilineOpeningTagStart(value)) {
const collected = collectOpeningTag(inputLines, index); const collected = collectOpeningTag(inputLines, index);
value = collected.value; value = collected.value;
@@ -5090,7 +5212,11 @@ function formatWrnPass(source, options = {}) {
lineHtmlDepth = Math.max(0, htmlDepth - 1); lineHtmlDepth = Math.max(0, htmlDepth - 1);
} }
const depth = lineCodeDepth + lineHtmlDepth + lineControlDepth; 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("</") && !value.startsWith("</") &&
!value.startsWith("<!--") && !value.startsWith("<!--") &&
!value.startsWith("<!") && !value.startsWith("<!") &&
@@ -6639,11 +6765,17 @@ function runtimeTypeOf(annotation) {
if (!annotation) if (!annotation)
return "unknown"; return "unknown";
const type = annotation.trim().replace(/^readonly\s+/, ""); const type = annotation.trim().replace(/^readonly\s+/, "");
if (/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(type)) const unionParts = type.split("|").map((part) => part.trim());
const concreteParts = unionParts.filter((part) => !/^(?:null|undefined)$/.test(part));
if (/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(type) ||
(concreteParts.length > 0 && concreteParts.every((part) => /^(?:"[^"]*"|'[^']*')$/.test(part))))
return "string"; 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"; 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"; return "boolean";
if (/^bigint(?:\s*\|\s*(?:null|undefined))*$/.test(type)) if (/^bigint(?:\s*\|\s*(?:null|undefined))*$/.test(type))
return "bigint"; return "bigint";
+59 -9
View File
@@ -39,9 +39,24 @@ function runtimeType(type) {
const value = String(type || "") const value = String(type || "")
.trim() .trim()
.replace(/^readonly\s+/, ""); .replace(/^readonly\s+/, "");
if (/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(value)) return "string"; const unionParts = value.split("|").map((part) => part.trim());
if (/^(?:number|Number)(?:\s*\|\s*(?:null|undefined))*$/.test(value)) return "number"; const concreteParts = unionParts.filter((part) => !/^(?:null|undefined)$/.test(part));
if (/^(?:boolean|Boolean)(?:\s*\|\s*(?:null|undefined))*$/.test(value)) return "boolean"; 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 (/^bigint(?:\s*\|\s*(?:null|undefined))*$/.test(value)) return "bigint";
if (/^(?:Array\s*<|ReadonlyArray\s*<|.+\[\])/.test(value) || /^\[/.test(value)) return "array"; if (/^(?:Array\s*<|ReadonlyArray\s*<|.+\[\])/.test(value) || /^\[/.test(value)) return "array";
if (/^(?:Record\s*<|object\b|\{)/.test(value)) return "object"; if (/^(?:Record\s*<|object\b|\{)/.test(value)) return "object";
@@ -138,15 +153,47 @@ function unwrapAttributeValue(value) {
return trimmed; 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()) { function attributeValueType(value, symbols = new Map()) {
const expression = isWrappedExpression(value);
const unwrapped = unwrapAttributeValue(value); const unwrapped = unwrapAttributeValue(value);
if (/^[A-Za-z_$][\w$]*$/.test(unwrapped) && symbols.has(unwrapped)) { if (/^[A-Za-z_$][\w$]*$/.test(unwrapped) && symbols.has(unwrapped)) {
return symbols.get(unwrapped); return symbols.get(unwrapped);
} }
if (/^(?:true|false)$/.test(unwrapped)) return "boolean";
if (/^-?(?:\d+\.?\d*|\.\d+)$/.test(unwrapped)) return "number"; const literal = expressionLiteralType(unwrapped);
if (unwrapped.startsWith("[")) return "array"; if (literal) return literal;
if (unwrapped.startsWith("{")) return "object";
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"; return "string";
} }
@@ -154,6 +201,7 @@ function isTypeCompatible(prop, value, symbols = new Map()) {
const expected = runtimeType(prop.type); const expected = runtimeType(prop.type);
if (expected === "unknown" || expected === "null") return true; if (expected === "unknown" || expected === "null") return true;
const actual = runtimeType(attributeValueType(value, symbols)); const actual = runtimeType(attributeValueType(value, symbols));
if (actual === "unknown") return true;
if (expected === actual) return true; if (expected === actual) return true;
if (expected === "number" && actual === "string") return Number.isFinite(Number(value)); if (expected === "number" && actual === "string") return Number.isFinite(Number(value));
if (expected === "boolean" && actual === "string") if (expected === "boolean" && actual === "string")
@@ -255,8 +303,10 @@ function validateComponentTags(source, components) {
end: attribute.nameEnd, end: attribute.nameEnd,
}); });
} }
const literal = stringLiteral(attribute.value) ?? attribute.value; const optionValue = isWrappedExpression(attribute.value)
if (prop.options.length > 0 && !prop.options.includes(literal)) { ? stringLiteral(unwrapAttributeValue(attribute.value))
: (stringLiteral(attribute.value) ?? attribute.value);
if (optionValue !== null && prop.options.length > 0 && !prop.options.includes(optionValue)) {
diagnostics.push({ diagnostics.push({
severity: "warning", severity: "warning",
code: "wrn-component-prop-option", code: "wrn-component-prop-option",
+152 -119
View File
@@ -94,65 +94,6 @@ function lineDiagnostic(
return diagnostic; 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)) {
const commentEnd = source.indexOf("-->", index + 4);
const end = commentEnd === -1 ? source.length : commentEnd + 3;
maskRange(index, end);
index = end - 1;
}
}
return masked.join("");
}
function maskLeadingTrivia(source) { function maskLeadingTrivia(source) {
const masked = [...source]; const masked = [...source];
let offset = 0; let offset = 0;
@@ -350,10 +291,96 @@ function validateBalancedCharacters(document, source) {
return diagnostics; return diagnostics;
} }
function maskHtmlComments(source) {
const masked = [...source];
let index = 0;
while (index < source.length) {
if (!source.startsWith("<!--", index)) {
index += 1;
continue;
}
const commentEnd = source.indexOf("-->", 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) { function validateHtmlTags(document, source) {
const diagnostics = []; const diagnostics = [];
const stack = [];
const voidElements = new Set([ const voidElements = new Set([
"area", "area",
"base", "base",
@@ -371,76 +398,82 @@ function validateHtmlTags(document, source) {
"wbr", "wbr",
]); ]);
const cleaned = stripComments(source); for (const range of findViewRanges(source)) {
const tagPattern = /<\/?([A-Za-z][A-Za-z0-9_$:.-]*)(?:\s[\s\S]*?)?\/?>/g; const fragment = source.slice(range.start, range.end);
// JavaScript-style `//` is valid rendered text inside <pre>/<code>. 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("</");
const isSelfClosing = /\/\s*>$/.test(completeTag);
const isVoid = voidElements.has(lowerTag);
while ((match = tagPattern.exec(cleaned)) !== null) { if (isClosing) {
const completeTag = match[0]; const matchingIndex = stack.findLastIndex((item) => item.normalizedName === normalizedName);
const tagName = match[1]; if (matchingIndex === -1) {
const lowerTag = tagName.toLowerCase(); diagnostics.push(
createDiagnostic(
const isClosing = completeTag.startsWith("</"); document,
const isSelfClosing = completeTag.endsWith("/>"); absoluteStart,
const isVoid = voidElements.has(lowerTag); absoluteStart + completeTag.length,
`Unexpected closing tag </${tagName}>.`,
if (isClosing) { vscode.DiagnosticSeverity.Error,
const last = stack.pop(); "wrn-unexpected-html-close",
),
if (!last) { );
diagnostics.push( continue;
createDiagnostic( }
document,
match.index,
match.index + completeTag.length,
`Unexpected closing tag </${tagName}>.`,
vscode.DiagnosticSeverity.Error,
"wrn-unexpected-html-close",
),
);
const last = stack.at(-1);
if (last.normalizedName !== normalizedName) {
diagnostics.push(
createDiagnostic(
document,
absoluteStart,
absoluteStart + completeTag.length,
`Mismatched closing tag </${tagName}>. Expected </${last.tagName}>.`,
vscode.DiagnosticSeverity.Error,
"wrn-mismatched-html-tag",
),
);
}
stack.splice(matchingIndex);
continue; continue;
} }
if (last.tagName !== tagName) { if (!isSelfClosing && !isVoid) {
diagnostics.push( stack.push({
createDiagnostic( tagName,
document, normalizedName,
match.index, offset: absoluteStart,
match.index + completeTag.length, length: completeTag.length,
`Mismatched closing tag </${tagName}>. Expected </${last.tagName}>.`, });
vscode.DiagnosticSeverity.Error,
"wrn-mismatched-html-tag",
),
);
} }
continue;
} }
if (!isSelfClosing && !isVoid) { for (const tag of stack) {
stack.push({ diagnostics.push(
tagName, createDiagnostic(
offset: match.index, document,
length: completeTag.length, tag.offset,
}); tag.offset + tag.length,
`Missing closing tag </${tag.tagName}>.`,
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 </${tag.tagName}>.`,
vscode.DiagnosticSeverity.Error,
"wrn-missing-html-close",
),
);
}
return diagnostics; return diagnostics;
} }
+279 -99
View File
@@ -1,3 +1,5 @@
// WRN editor extension source hash: 825c406dea4b196976b1b6e53b3c71c3263bd63f403acdaf2fe7e5ad174275da
// WRN editor extension generator hash: 456d1d614e44e5fb1f19b784176c09cf2ade9b64ef73a17934c2698150b62728
"use strict"; "use strict";
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports); var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
@@ -27187,6 +27189,17 @@ ${serverFunctions}
opensElement 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}</${parsed.tagName}>`);
} else if (parsed.remainder) {
lines2.push(`${childIndent}${parsed.remainder}`);
}
return { lines: lines2, opensElement };
}
const lines = [ const lines = [
`${baseIndent}<${parsed.tagName}`, `${baseIndent}<${parsed.tagName}`,
...parsed.attributes.flatMap((attribute) => formatAttribute(attribute, childIndent, unit)) ...parsed.attributes.flatMap((attribute) => formatAttribute(attribute, childIndent, unit))
@@ -27224,6 +27237,63 @@ ${serverFunctions}
function isClosingTag(value) { function isClosingTag(value) {
return /^<\/[A-Za-z][\w$:.-]*\s*>/.test(value); return /^<\/[A-Za-z][\w$:.-]*\s*>/.test(value);
} }
function isBalancedInlineHtmlFragment(value) {
if (!value.startsWith("<") || value.startsWith("</") || value.startsWith("<!--")) {
return false;
}
const stack = [];
let tagCount = 0;
let rootCount = 0;
let hasNestedElement = false;
let hasOutsideText = false;
let index = 0;
while (index < value.length) {
const tagStart = value.indexOf("<", index);
if (tagStart === -1) {
if (stack.length === 0 && value.slice(index).trim())
hasOutsideText = true;
break;
}
if (stack.length === 0 && value.slice(index, tagStart).trim())
hasOutsideText = true;
if (value.startsWith("<!--", tagStart)) {
const commentEnd = value.indexOf("-->", tagStart + 4);
if (commentEnd === -1)
return false;
index = commentEnd + 3;
continue;
}
const relativeEnd = findOpeningTagEnd(value.slice(tagStart));
if (relativeEnd === -1)
return false;
const tag = value.slice(tagStart, tagStart + relativeEnd + 1);
const match = /^<\/?([A-Za-z][\w$:.-]*)[\s\S]*?>$/.exec(tag);
if (!match) {
index = tagStart + 1;
continue;
}
tagCount += 1;
const tagName = match[1];
const normalizedName = /^[a-z]/.test(tagName) ? tagName.toLowerCase() : tagName;
const closing = tag.startsWith("</");
const selfClosing = /\/\s*>$/.test(tag);
const voidElement = VOID_ELEMENTS.has(tagName.toLowerCase());
if (closing) {
if (stack.at(-1) !== normalizedName)
return false;
stack.pop();
} else {
if (stack.length === 0)
rootCount += 1;
else
hasNestedElement = true;
if (!selfClosing && !voidElement)
stack.push(normalizedName);
}
index = tagStart + relativeEnd + 1;
}
return tagCount >= 2 && stack.length === 0 && (rootCount > 1 || hasNestedElement || hasOutsideText);
}
function isControlBlockOpen(value) { function isControlBlockOpen(value) {
return /^\{#(?:if|each)\b[\s\S]*\}$/.test(value); return /^\{#(?:if|each)\b[\s\S]*\}$/.test(value);
} }
@@ -27319,9 +27389,43 @@ ${serverFunctions}
endIndex: index endIndex: index
}; };
} }
function isPreservedRawBlockStart(value) {
return /<pre(?:\s|>)/i.test(value) && !/<\/pre\s*>/i.test(value.slice(0, value.search(/<pre(?:\s|>)/i)));
}
function hasPreservedRawBlockEnd(value) {
return /<\/pre\s*>/i.test(value);
}
function transformOutsidePreservedRawBlocks(lines, transformLine) {
const output = [];
let preserving = false;
for (const line of lines) {
if (preserving) {
output.push(line);
if (hasPreservedRawBlockEnd(line))
preserving = false;
continue;
}
if (isPreservedRawBlockStart(line)) {
output.push(line);
preserving = !hasPreservedRawBlockEnd(line);
continue;
}
output.push(...transformLine(line));
}
return output;
}
function collectPreservedRawBlock(lines, startIndex) {
const collected = [lines[startIndex]];
let index = startIndex;
while (!hasPreservedRawBlockEnd(collected.at(-1) || "") && index + 1 < lines.length) {
index += 1;
collected.push(lines[index]);
}
return { lines: collected, endIndex: index };
}
function expandInlineControlBlocks(lines) { function expandInlineControlBlocks(lines) {
const marker = /(\{#(?:if|each)\b[^}]*\}|\{:(?:else(?:\s+if\b[^}]*)?|empty)\}|\{\/(?:if|each)\})/g; 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)) if (!marker.test(line))
return [line]; return [line];
marker.lastIndex = 0; marker.lastIndex = 0;
@@ -27331,7 +27435,7 @@ ${serverFunctions}
}); });
} }
function expandStructuredStateDeclarations(lines, unit) { 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); const match = /^(\s*state\s+[A-Za-z_$][\w$]*\s*=\s*)([\\[{][\s\S]*)$/.exec(line);
if (!match) if (!match)
return [line]; return [line];
@@ -27387,6 +27491,14 @@ ${serverFunctions}
index += 1; index += 1;
continue; 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)) { if (isMultilineOpeningTagStart(value)) {
const collected = collectOpeningTag(inputLines, index); const collected = collectOpeningTag(inputLines, index);
value = collected.value; value = collected.value;
@@ -27409,7 +27521,10 @@ ${serverFunctions}
lineHtmlDepth = Math.max(0, htmlDepth - 1); lineHtmlDepth = Math.max(0, htmlDepth - 1);
} }
const depth = lineCodeDepth + lineHtmlDepth + lineControlDepth; const depth = lineCodeDepth + lineHtmlDepth + lineControlDepth;
if (value.startsWith("<") && !value.startsWith("</") && !value.startsWith("<!--") && !value.startsWith("<!") && !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("</") && !value.startsWith("<!--") && !value.startsWith("<!") && !value.startsWith("<?")) {
const formattedTag = formatOpeningTag(value, unit, depth, printWidth, multilineAttributes); const formattedTag = formatOpeningTag(value, unit, depth, printWidth, multilineAttributes);
output.push(...formattedTag.lines); output.push(...formattedTag.lines);
if (formattedTag.opensElement) { if (formattedTag.opensElement) {
@@ -28895,11 +29010,13 @@ ${serverFunctions}
if (!annotation) if (!annotation)
return "unknown"; return "unknown";
const type = annotation.trim().replace(/^readonly\s+/, ""); const type = annotation.trim().replace(/^readonly\s+/, "");
if (/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(type)) const unionParts = type.split("|").map((part) => part.trim());
const concreteParts = unionParts.filter((part) => !/^(?:null|undefined)$/.test(part));
if (/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(type) || concreteParts.length > 0 && concreteParts.every((part) => /^(?:"[^"]*"|'[^']*')$/.test(part)))
return "string"; 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"; 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"; return "boolean";
if (/^bigint(?:\s*\|\s*(?:null|undefined))*$/.test(type)) if (/^bigint(?:\s*\|\s*(?:null|undefined))*$/.test(type))
return "bigint"; return "bigint";
@@ -30196,11 +30313,13 @@ var require_component_metadata = __commonJS((exports2, module2) => {
} }
function runtimeType(type) { function runtimeType(type) {
const value = String(type || "").trim().replace(/^readonly\s+/, ""); 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"; 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"; 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"; return "boolean";
if (/^bigint(?:\s*\|\s*(?:null|undefined))*$/.test(value)) if (/^bigint(?:\s*\|\s*(?:null|undefined))*$/.test(value))
return "bigint"; return "bigint";
@@ -30289,19 +30408,41 @@ var require_component_metadata = __commonJS((exports2, module2) => {
return trimmed.slice(1, -1).trim(); return trimmed.slice(1, -1).trim();
return trimmed; 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) { function attributeValueType(value, symbols = new Map) {
const expression = isWrappedExpression(value);
const unwrapped = unwrapAttributeValue(value); const unwrapped = unwrapAttributeValue(value);
if (/^[A-Za-z_$][\w$]*$/.test(unwrapped) && symbols.has(unwrapped)) { if (/^[A-Za-z_$][\w$]*$/.test(unwrapped) && symbols.has(unwrapped)) {
return symbols.get(unwrapped); return symbols.get(unwrapped);
} }
if (/^(?:true|false)$/.test(unwrapped)) const literal = expressionLiteralType(unwrapped);
return "boolean"; if (literal)
if (/^-?(?:\d+\.?\d*|\.\d+)$/.test(unwrapped)) return literal;
return "number"; if (expression) {
if (unwrapped.startsWith("[")) if (/^!\s*/.test(unwrapped) || /(?:===|!==|==|!=|<=|>=|<|>|\bin\b|\binstanceof\b)/.test(unwrapped)) {
return "array"; return "boolean";
if (unwrapped.startsWith("{")) }
return "object"; return "unknown";
}
return "string"; return "string";
} }
function isTypeCompatible(prop, value, symbols = new Map) { function isTypeCompatible(prop, value, symbols = new Map) {
@@ -30309,6 +30450,8 @@ var require_component_metadata = __commonJS((exports2, module2) => {
if (expected === "unknown" || expected === "null") if (expected === "unknown" || expected === "null")
return true; return true;
const actual = runtimeType(attributeValueType(value, symbols)); const actual = runtimeType(attributeValueType(value, symbols));
if (actual === "unknown")
return true;
if (expected === actual) if (expected === actual)
return true; return true;
if (expected === "number" && actual === "string") if (expected === "number" && actual === "string")
@@ -30409,8 +30552,8 @@ var require_component_metadata = __commonJS((exports2, module2) => {
end: attribute.nameEnd end: attribute.nameEnd
}); });
} }
const literal = stringLiteral(attribute.value) ?? attribute.value; const optionValue = isWrappedExpression(attribute.value) ? stringLiteral(unwrapAttributeValue(attribute.value)) : stringLiteral(attribute.value) ?? attribute.value;
if (prop.options.length > 0 && !prop.options.includes(literal)) { if (optionValue !== null && prop.options.length > 0 && !prop.options.includes(optionValue)) {
diagnostics.push({ diagnostics.push({
severity: "warning", severity: "warning",
code: "wrn-component-prop-option", code: "wrn-component-prop-option",
@@ -30727,58 +30870,6 @@ var require_diagnostics = __commonJS((exports2, module2) => {
} }
return diagnostic; 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)) {
const commentEnd = source.indexOf("-->", index + 4);
const end = commentEnd === -1 ? source.length : commentEnd + 3;
maskRange(index, end);
index = end - 1;
}
}
return masked.join("");
}
function maskLeadingTrivia(source) { function maskLeadingTrivia(source) {
const masked = [...source]; const masked = [...source];
let offset = 0; let offset = 0;
@@ -30897,9 +30988,89 @@ var require_diagnostics = __commonJS((exports2, module2) => {
} }
return diagnostics; return diagnostics;
} }
function maskHtmlComments(source) {
const masked = [...source];
let index = 0;
while (index < source.length) {
if (!source.startsWith("<!--", index)) {
index += 1;
continue;
}
const commentEnd = source.indexOf("-->", 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) { function validateHtmlTags(document, source) {
const diagnostics = []; const diagnostics = [];
const stack = [];
const voidElements = new Set([ const voidElements = new Set([
"area", "area",
"base", "base",
@@ -30916,38 +31087,47 @@ var require_diagnostics = __commonJS((exports2, module2) => {
"track", "track",
"wbr" "wbr"
]); ]);
const cleaned = stripComments(source); for (const range of findViewRanges(source)) {
const tagPattern = /<\/?([A-Za-z][A-Za-z0-9_$:.-]*)(?:\s[\s\S]*?)?\/?>/g; const fragment = source.slice(range.start, range.end);
let match; const cleaned = maskTemplateExpressions(maskHtmlComments(fragment));
while ((match = tagPattern.exec(cleaned)) !== null) { const stack = [];
const completeTag = match[0]; const tagPattern = /<\/?([A-Za-z][A-Za-z0-9_$:.-]*)(?:\s[\s\S]*?)?\/?>/g;
const tagName = match[1]; let match;
const lowerTag = tagName.toLowerCase(); while ((match = tagPattern.exec(cleaned)) !== null) {
const isClosing = completeTag.startsWith("</"); const completeTag = match[0];
const isSelfClosing = completeTag.endsWith("/>"); const tagName = match[1];
const isVoid = voidElements.has(lowerTag); const normalizedName = /^[a-z]/.test(tagName) ? tagName.toLowerCase() : tagName;
if (isClosing) { const lowerTag = tagName.toLowerCase();
const last = stack.pop(); const absoluteStart = range.start + match.index;
if (!last) { const isClosing = completeTag.startsWith("</");
diagnostics.push(createDiagnostic(document, match.index, match.index + completeTag.length, `Unexpected closing tag </${tagName}>.`, vscode.DiagnosticSeverity.Error, "wrn-unexpected-html-close")); const isSelfClosing = /\/\s*>$/.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 </${tagName}>.`, 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 </${tagName}>. Expected </${last.tagName}>.`, vscode.DiagnosticSeverity.Error, "wrn-mismatched-html-tag"));
}
stack.splice(matchingIndex);
continue; continue;
} }
if (last.tagName !== tagName) { if (!isSelfClosing && !isVoid) {
diagnostics.push(createDiagnostic(document, match.index, match.index + completeTag.length, `Mismatched closing tag </${tagName}>. Expected </${last.tagName}>.`, vscode.DiagnosticSeverity.Error, "wrn-mismatched-html-tag")); stack.push({
tagName,
normalizedName,
offset: absoluteStart,
length: completeTag.length
});
} }
continue;
} }
if (!isSelfClosing && !isVoid) { for (const tag of stack) {
stack.push({ diagnostics.push(createDiagnostic(document, tag.offset, tag.offset + tag.length, `Missing closing tag </${tag.tagName}>.`, vscode.DiagnosticSeverity.Error, "wrn-missing-html-close"));
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 </${tag.tagName}>.`, vscode.DiagnosticSeverity.Error, "wrn-missing-html-close"));
}
return diagnostics; return diagnostics;
} }
function getRootBodyRange(source, rootMatch) { function getRootBodyRange(source, rootMatch) {
@@ -31923,9 +32103,9 @@ async function activate(context) {
registerCompilerDiagnostics(context); registerCompilerDiagnostics(context);
registerCompletionProvider(context); registerCompletionProvider(context);
registerDefinitionProvider(context); registerDefinitionProvider(context);
registerComponentIntelligence(context);
registerFormatter(context); registerFormatter(context);
} }
registerComponentIntelligence(context);
registerSemanticTokens(context); registerSemanticTokens(context);
registerV060LanguageFeatures(context); registerV060LanguageFeatures(context);
} }
+4 -1
View File
@@ -376,9 +376,12 @@ async function activate(context) {
registerCompilerDiagnostics(context); registerCompilerDiagnostics(context);
registerCompletionProvider(context); registerCompletionProvider(context);
registerDefinitionProvider(context); registerDefinitionProvider(context);
registerComponentIntelligence(context);
registerFormatter(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); registerSemanticTokens(context);
registerV060LanguageFeatures(context); registerV060LanguageFeatures(context);
} }
+274 -37
View File
@@ -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 // @bun @bun-cjs
(function(exports, require, module, __filename, __dirname) {var __create = Object.create; (function(exports, require, module, __filename, __dirname) {var __create = Object.create;
var __getProtoOf = Object.getPrototypeOf; var __getProtoOf = Object.getPrototypeOf;
@@ -169438,6 +169440,17 @@ function formatOpeningTag(value, unit, depth, printWidth = 100, multilineAttribu
opensElement 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}</${parsed.tagName}>`);
} else if (parsed.remainder) {
lines2.push(`${childIndent}${parsed.remainder}`);
}
return { lines: lines2, opensElement };
}
const lines = [ const lines = [
`${baseIndent}<${parsed.tagName}`, `${baseIndent}<${parsed.tagName}`,
...parsed.attributes.flatMap((attribute) => formatAttribute(attribute, childIndent, unit)) ...parsed.attributes.flatMap((attribute) => formatAttribute(attribute, childIndent, unit))
@@ -169475,6 +169488,63 @@ function isMultilineOpeningTagStart(value) {
function isClosingTag(value) { function isClosingTag(value) {
return /^<\/[A-Za-z][\w$:.-]*\s*>/.test(value); return /^<\/[A-Za-z][\w$:.-]*\s*>/.test(value);
} }
function isBalancedInlineHtmlFragment(value) {
if (!value.startsWith("<") || value.startsWith("</") || value.startsWith("<!--")) {
return false;
}
const stack = [];
let tagCount = 0;
let rootCount = 0;
let hasNestedElement = false;
let hasOutsideText = false;
let index = 0;
while (index < value.length) {
const tagStart = value.indexOf("<", index);
if (tagStart === -1) {
if (stack.length === 0 && value.slice(index).trim())
hasOutsideText = true;
break;
}
if (stack.length === 0 && value.slice(index, tagStart).trim())
hasOutsideText = true;
if (value.startsWith("<!--", tagStart)) {
const commentEnd = value.indexOf("-->", tagStart + 4);
if (commentEnd === -1)
return false;
index = commentEnd + 3;
continue;
}
const relativeEnd = findOpeningTagEnd(value.slice(tagStart));
if (relativeEnd === -1)
return false;
const tag = value.slice(tagStart, tagStart + relativeEnd + 1);
const match = /^<\/?([A-Za-z][\w$:.-]*)[\s\S]*?>$/.exec(tag);
if (!match) {
index = tagStart + 1;
continue;
}
tagCount += 1;
const tagName = match[1];
const normalizedName = /^[a-z]/.test(tagName) ? tagName.toLowerCase() : tagName;
const closing = tag.startsWith("</");
const selfClosing = /\/\s*>$/.test(tag);
const voidElement = VOID_ELEMENTS.has(tagName.toLowerCase());
if (closing) {
if (stack.at(-1) !== normalizedName)
return false;
stack.pop();
} else {
if (stack.length === 0)
rootCount += 1;
else
hasNestedElement = true;
if (!selfClosing && !voidElement)
stack.push(normalizedName);
}
index = tagStart + relativeEnd + 1;
}
return tagCount >= 2 && stack.length === 0 && (rootCount > 1 || hasNestedElement || hasOutsideText);
}
function isControlBlockOpen(value) { function isControlBlockOpen(value) {
return /^\{#(?:if|each)\b[\s\S]*\}$/.test(value); return /^\{#(?:if|each)\b[\s\S]*\}$/.test(value);
} }
@@ -169570,9 +169640,43 @@ function collectOpeningTag(inputLines, startIndex) {
endIndex: index endIndex: index
}; };
} }
function isPreservedRawBlockStart(value) {
return /<pre(?:\s|>)/i.test(value) && !/<\/pre\s*>/i.test(value.slice(0, value.search(/<pre(?:\s|>)/i)));
}
function hasPreservedRawBlockEnd(value) {
return /<\/pre\s*>/i.test(value);
}
function transformOutsidePreservedRawBlocks(lines, transformLine) {
const output = [];
let preserving = false;
for (const line of lines) {
if (preserving) {
output.push(line);
if (hasPreservedRawBlockEnd(line))
preserving = false;
continue;
}
if (isPreservedRawBlockStart(line)) {
output.push(line);
preserving = !hasPreservedRawBlockEnd(line);
continue;
}
output.push(...transformLine(line));
}
return output;
}
function collectPreservedRawBlock(lines, startIndex) {
const collected = [lines[startIndex]];
let index = startIndex;
while (!hasPreservedRawBlockEnd(collected.at(-1) || "") && index + 1 < lines.length) {
index += 1;
collected.push(lines[index]);
}
return { lines: collected, endIndex: index };
}
function expandInlineControlBlocks(lines) { function expandInlineControlBlocks(lines) {
const marker = /(\{#(?:if|each)\b[^}]*\}|\{:(?:else(?:\s+if\b[^}]*)?|empty)\}|\{\/(?:if|each)\})/g; 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)) if (!marker.test(line))
return [line]; return [line];
marker.lastIndex = 0; marker.lastIndex = 0;
@@ -169582,7 +169686,7 @@ function expandInlineControlBlocks(lines) {
}); });
} }
function expandStructuredStateDeclarations(lines, unit) { 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); const match = /^(\s*state\s+[A-Za-z_$][\w$]*\s*=\s*)([\\[{][\s\S]*)$/.exec(line);
if (!match) if (!match)
return [line]; return [line];
@@ -169638,6 +169742,14 @@ function formatWrnPass(source, options = {}) {
index += 1; index += 1;
continue; 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)) { if (isMultilineOpeningTagStart(value)) {
const collected = collectOpeningTag(inputLines, index); const collected = collectOpeningTag(inputLines, index);
value = collected.value; value = collected.value;
@@ -169660,7 +169772,10 @@ function formatWrnPass(source, options = {}) {
lineHtmlDepth = Math.max(0, htmlDepth - 1); lineHtmlDepth = Math.max(0, htmlDepth - 1);
} }
const depth = lineCodeDepth + lineHtmlDepth + lineControlDepth; const depth = lineCodeDepth + lineHtmlDepth + lineControlDepth;
if (value.startsWith("<") && !value.startsWith("</") && !value.startsWith("<!--") && !value.startsWith("<!") && !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("</") && !value.startsWith("<!--") && !value.startsWith("<!") && !value.startsWith("<?")) {
const formattedTag = formatOpeningTag(value, unit, depth, printWidth, multilineAttributes); const formattedTag = formatOpeningTag(value, unit, depth, printWidth, multilineAttributes);
output.push(...formattedTag.lines); output.push(...formattedTag.lines);
if (formattedTag.opensElement) { if (formattedTag.opensElement) {
@@ -169745,11 +169860,13 @@ function runtimeTypeOf(annotation) {
if (!annotation) if (!annotation)
return "unknown"; return "unknown";
const type = annotation.trim().replace(/^readonly\s+/, ""); const type = annotation.trim().replace(/^readonly\s+/, "");
if (/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(type)) const unionParts = type.split("|").map((part) => part.trim());
const concreteParts = unionParts.filter((part) => !/^(?:null|undefined)$/.test(part));
if (/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(type) || concreteParts.length > 0 && concreteParts.every((part) => /^(?:"[^"]*"|'[^']*')$/.test(part)))
return "string"; 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"; 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"; return "boolean";
if (/^bigint(?:\s*\|\s*(?:null|undefined))*$/.test(type)) if (/^bigint(?:\s*\|\s*(?:null|undefined))*$/.test(type))
return "bigint"; return "bigint";
@@ -171689,7 +171806,7 @@ function runtimeNamespace(runtime) {
return `__wrn_${runtime}`; return `__wrn_${runtime}`;
} }
function functionDeclaration(fn) { function functionDeclaration(fn) {
const params = fn.parameters.map((param) => `${param.name}${param.optional ? "?" : ""}${param.valueType ? `: ${param.valueType}` : param.name === "event" ? ": CustomEvent<any> & { target: HTMLElement }" : ": unknown"}${param.default ? ` = ${param.default}` : ""}`).join(", "); const params = fn.parameters.map((param) => `${param.name}${param.optional ? "?" : ""}${param.valueType ? `: ${param.valueType}` : param.name === "event" ? ": CustomEvent<any> & { target: HTMLElement }" : ": any"}${param.default ? ` = ${param.default}` : ""}`).join(", ");
return `export ${fn.async ? "async " : ""}function ${fn.name}(${params})${fn.returnType ? `: ${fn.returnType}` : ""} {${fn.body}}`; return `export ${fn.async ? "async " : ""}function ${fn.name}(${params})${fn.returnType ? `: ${fn.returnType}` : ""} {${fn.body}}`;
} }
function retainedImports(ast) { function retainedImports(ast) {
@@ -171702,6 +171819,51 @@ function retainedImports(ast) {
}).map((entry) => entry.raw).join(` }).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)) { function virtualTypeScriptModule(source, filePath = "component.wrn", appRoot = findAppRoot(filePath)) {
const ast = parse(source); const ast = parse(source);
const chunks = []; const chunks = [];
@@ -171728,6 +171890,8 @@ function virtualTypeScriptModule(source, filePath = "component.wrn", appRoot = f
`), ast.types[0]?.trim()); `), ast.types[0]?.trim());
for (const prop of ast.props) { for (const prop of ast.props) {
if (!safeBindingName(prop.name))
continue;
append(`declare const ${prop.name}: Readonly<${prop.valueType ?? "unknown"}>;`, prop.name); append(`declare const ${prop.name}: Readonly<${prop.valueType ?? "unknown"}>;`, prop.name);
} }
for (const state of ast.states) { 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 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 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>" : "unknown")}`).join("; "); 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>" : "unknown")}`).join("; ");
append(`declare const output: { ${outputType} };`); append(`declare const output: { [name: string]: (...args: any[]) => void; ${outputType} };`);
for (const output of ast.outputs) for (const output of ast.outputs)
append(`declare const ${output.name}: (${output.payload ? `${output.payload.name}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}) => void;`); 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)) 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; continue;
const received = attr.boolean ? "boolean" : inferredRuntimeType(JSON.stringify(attr.value)); const received = attr.boolean ? "boolean" : inferredRuntimeType(JSON.stringify(attr.value));
const expected = runtimeTypeOf(prop.type); 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({ diagnostics.push({
code: "WRN-COMPONENT-PROP-TYPE", code: "WRN-COMPONENT-PROP-TYPE",
category: "error", category: "error",
@@ -171956,8 +172121,12 @@ function componentUsageDiagnostics(source, ast, filePath, appRoot) {
function category(value) { function category(value) {
return value === import_typescript.default.DiagnosticCategory.Error ? "error" : value === import_typescript.default.DiagnosticCategory.Warning ? "warning" : "info"; 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); 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); const originalGet = host.getSourceFile.bind(host);
host.fileExists = (fileName) => files.has(import_node_path2.normalize(fileName)) || import_typescript.default.sys.fileExists(fileName); 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); 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; 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) { function mappedPosition(virtual, line, column) {
const mapping = virtual.mappings.find((entry) => line >= entry.virtualStartLine && line <= entry.virtualEndLine); const mapping = virtual.mappings.find((entry) => line >= entry.virtualStartLine && line <= entry.virtualEndLine);
if (!mapping) if (!mapping)
return { line: 1, column: 1 }; return null;
const offset = line - mapping.virtualStartLine; const offset = line - mapping.virtualStartLine;
return { return {
line: mapping.sourceStartLine + offset, 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"] lib: ["lib.esnext.d.ts", "lib.dom.d.ts", "lib.dom.iterable.d.ts"]
}; };
const rootNames = [virtual.fileName, ...appTypes.files.keys()]; const rootNames = [virtual.fileName, ...appTypes.files.keys()];
const program = import_typescript.default.createProgram(rootNames, compilerOptions, hostWithVirtualFiles(files, compilerOptions)); const standardLibraryDirectory = resolveStandardLibraryDirectory(appRoot, compilerOptions);
const tsDiagnostics = import_typescript.default.getPreEmitDiagnostics(program).map((diagnostic) => { const tsDiagnostics = [];
const file = diagnostic.file; if (standardLibraryDirectory) {
const start = diagnostic.start ?? 0; const program = import_typescript.default.createProgram(rootNames, compilerOptions, hostWithVirtualFiles(files, compilerOptions, standardLibraryDirectory));
const virtualPosition = file?.getLineAndCharacterOfPosition(start) ?? { line: 0, character: 0 }; for (const diagnostic of import_typescript.default.getPreEmitDiagnostics(program)) {
const isVirtual = import_node_path2.normalize(file?.fileName ?? "") === import_node_path2.normalize(virtual.fileName); const file = diagnostic.file;
const sourcePosition = isVirtual ? mappedPosition(virtual, virtualPosition.line + 1, virtualPosition.character + 1) : { line: virtualPosition.line + 1, column: virtualPosition.character + 1 }; if (!file)
return { continue;
code: `WRN-TYPE-${diagnostic.code}`, const normalizedFile = import_node_path2.normalize(file.fileName);
category: category(diagnostic.category), const isVirtual = normalizedFile === import_node_path2.normalize(virtual.fileName);
message: import_typescript.default.flattenDiagnosticMessageText(diagnostic.messageText, ` 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, file: isVirtual ? filePath : file.fileName,
line: sourcePosition.line, line: sourcePosition.line,
column: sourcePosition.column, column: sourcePosition.column,
length: diagnostic.length ?? 1, length: diagnostic.length ?? 1,
hint: "Fix the TypeScript contract or expression in the related .wrn declaration." hint: "Fix the TypeScript contract or expression in the related .wrn declaration."
}; });
}); }
}
return [ return [
...options.checkRuntimeBoundaries === false ? [] : runtimeDiagnostics(source, virtual.ast, filePath), ...options.checkRuntimeBoundaries === false ? [] : runtimeDiagnostics(source, virtual.ast, filePath),
...componentUsageDiagnostics(source, virtual.ast, filePath, appRoot), ...componentUsageDiagnostics(source, virtual.ast, filePath, appRoot),
@@ -172527,11 +172721,30 @@ function send(value) {
function result(id, value) { function result(id, value) {
send({ jsonrpc: "2.0", id, result: 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) { function publish(document) {
send({ send({
jsonrpc: "2.0", jsonrpc: "2.0",
method: "textDocument/publishDiagnostics", method: "textDocument/publishDiagnostics",
params: { uri: document.uri, diagnostics: documentDiagnostics(document) } params: { uri: document.uri, diagnostics: safeDocumentDiagnostics(document) }
}); });
} }
function clearDiagnosticTimer(uri) { function clearDiagnosticTimer(uri) {
@@ -172567,7 +172780,7 @@ async function handle(message) {
case "initialize": case "initialize":
workspaceRoot = params.rootPath ?? rootFromUri(params.rootUri); workspaceRoot = params.rootPath ?? rootFromUri(params.rootUri);
result(message.id, { result(message.id, {
serverInfo: { name: "WRNexus Language Server", version: "0.8.0" }, serverInfo: { name: "WRNexus Language Server", version: "0.8.3" },
capabilities: { capabilities: {
textDocumentSync: 1, textDocumentSync: 1,
documentFormattingProvider: true, documentFormattingProvider: true,
@@ -172676,7 +172889,7 @@ async function handle(message) {
result(message.id, []); result(message.id, []);
break; 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", title: "Add empty alt attribute",
kind: "quickfix", kind: "quickfix",
diagnostics: [item], diagnostics: [item],
@@ -172740,12 +172953,36 @@ function consume() {
return; return;
const body = buffer.subarray(bodyStart, bodyStart + length).toString(); const body = buffer.subarray(bodyStart, bodyStart + length).toString();
buffer = buffer.subarray(bodyStart + length); 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) => { process.stdin.on("data", (chunk) => {
buffer = Buffer.concat([buffer, Buffer.from(chunk)]); try {
consume(); 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(); process.stdin.resume();
}) })(exports, require, module, __filename, __dirname);
+23 -4
View File
@@ -195,8 +195,12 @@
"patterns": [ "patterns": [
{ {
"include": "source.ts" "include": "source.ts"
},
{
"include": "#ts-braces"
} }
] ],
"applyEndPatternLast": true
}, },
"props-block": { "props-block": {
"begin": "\\b(props)\\b\\s*(\\{)", "begin": "\\b(props)\\b\\s*(\\{)",
@@ -254,8 +258,15 @@
}, },
{ {
"include": "#strings" "include": "#strings"
},
{
"include": "#ts-braces"
},
{
"include": "source.ts"
} }
] ],
"applyEndPatternLast": true
}, },
"seo-block": { "seo-block": {
"begin": "\\b(seo)\\b\\s*(\\{)", "begin": "\\b(seo)\\b\\s*(\\{)",
@@ -651,7 +662,8 @@
{ {
"include": "text.html.basic" "include": "text.html.basic"
} }
] ],
"applyEndPatternLast": true
}, },
"wire-event": { "wire-event": {
"match": "(@)(?:(window|document)(:))?([A-Za-z][A-Za-z0-9_-]*)(\\s*=)", "match": "(@)(?:(window|document)(:))?([A-Za-z][A-Za-z0-9_-]*)(\\s*=)",
@@ -1067,8 +1079,15 @@
"name": "punctuation.definition.parameters.end.wrn" "name": "punctuation.definition.parameters.end.wrn"
} }
} }
},
{
"include": "#ts-braces"
},
{
"include": "source.ts"
} }
] ],
"applyEndPatternLast": true
}, },
"grouped-state-block": { "grouped-state-block": {
"begin": "\\b(?:(client|server|shared)\\s+)?(state)\\b\\s*(\\{)", "begin": "\\b(?:(client|server|shared)\\s+)?(state)\\b\\s*(\\{)",
@@ -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 { <article></article> }
}`);
const source = `component FeatureGrid {
props { items: unknown[] = [] }
view {
{#each items as item}
<FeatureCard
external='{item.external || false}'
disabled='{item.disabled || false}'
showArrow='{item.showArrow !== false}'
/>
{/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 { <button></button> }
}`);
const diagnostics = validateComponentTags(
`<Toggle enabled="definitely" />`,
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 { <article></article> }
}`);
const dynamic = `<FeatureCard target='{item.target || ""}' />`;
assert.deepEqual(validateComponentTags(dynamic, new Map([["FeatureCard", target]])), []);
const invalidLiteral = validateComponentTags(
`<FeatureCard target="popup" />`,
new Map([["FeatureCard", target]]),
);
assert.deepEqual(
invalidLiteral.map(({ code }) => code),
["wrn-component-prop-option"],
);
});
+33
View File
@@ -201,3 +201,36 @@ page Test {
assert.deepEqual(validateHtmlTags(mockDocument(source), source), []); 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<string | number | boolean | null | object>; sourceEvent?: Event })
}
view { <form><strong>Sign in</strong></form> }
}`;
assert.deepEqual(validateHtmlTags(mockDocument(source), source), []);
});
test("allows JavaScript-looking documentation text inside pre and code", () => {
const source = `page Docs {
view {
<pre><code><span class="code-muted">// One project, one language</span>
<span class="code-key">page</span> Dashboard {
&lt;button&gt;Ship&lt;/button&gt;
}</code></pre>
}
}`;
assert.deepEqual(validateHtmlTags(mockDocument(source), source), []);
});
test("still reports genuinely mismatched view tags", () => {
const source = `page Broken {
view { <main><strong>Broken</main> }
}`;
const diagnostics = validateHtmlTags(mockDocument(source), source);
assert.equal(diagnostics.length, 1);
assert.equal(diagnostics[0].code, "wrn-mismatched-html-tag");
});
+45
View File
@@ -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}\}/, /\n {2}outputs \{\n {4}confirm\(payload: ConfirmPayload\)\n {4}cancel\(\)\n {2}\}/,
); );
}); });
test("preserves preformatted WRN code examples", () => {
const source = `page Docs {
view {
<pre><code><span class="code-muted">// One project, one language</span>
<span class="code-key">page</span> ProductDashboard {
<span class="code-key">state</span> count: number = 0
&lt;button @click='count++'&gt;Ship {count}&lt;/button&gt;
}</code></pre>
}
}
`;
const options = { insertSpaces: true, tabSize: 2, printWidth: 100 };
const formatted = formatWrn(source, options);
assert.match(
formatted,
/<pre><code><span class="code-muted">\/\/ One project, one language<\/span>\n<span class="code-key">page<\/span>/,
);
assert.equal(formatWrn(formatted, options), formatted);
});
test("keeps attribute-free opening tags intact when content is long", () => {
const source = `page Docs {
view {
<p>This documentation sentence is deliberately long enough to exceed the formatter print width without splitting the p opening delimiter.</p>
}
}
`;
const options = { insertSpaces: true, tabSize: 2, printWidth: 70 };
const formatted = formatWrn(source, options);
assert.match(formatted, /^ {4}<p>$/m);
assert.doesNotMatch(formatted, /^ {4}<p\n {4}>$/m);
assert.equal(formatWrn(formatted, options), formatted);
});
test("preserves balanced compact sibling markup without indentation drift", () => {
const source = `page Compact {
view {
<div class="strip"><span>Page</span><b>→</b><span>API</span></div>
}
}\n`;
const formatted = formatWrn(source, { tabSize: 2, printWidth: 80 });
assert.match(formatted, /<div class="strip"><span>Page<\/span><b>→<\/b><span>API<\/span><\/div>/);
assert.equal(formatWrn(formatted, { tabSize: 2, printWidth: 80 }), formatted);
});
+141
View File
@@ -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 {
<pre><code><span class="muted">// One project, one language</span></code></pre>
<div class="strip"><span>Page</span><b>→</b><span>API</span></div>
}
}\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, /<span>Page<\/span><b>→<\/b><span>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 { <button @click="submit('demo')"></button> }
}
`;
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),
);
});
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "wrnexus", "name": "wrnexus",
"version": "0.8.3", "version": "0.8.4",
"private": true, "private": true,
"type": "module", "type": "module",
"description": "An SSR-first full-stack web framework with server-rendered reactive components. Bun-first, Node-friendly.", "description": "An SSR-first full-stack web framework with server-rendered reactive components. Bun-first, Node-friendly.",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ai", "name": "@wrnexus/ai",
"version": "0.8.3", "version": "0.8.4",
"private": true, "private": true,
"type": "module", "type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/auth", "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.", "description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/authz", "name": "@wrnexus/authz",
"version": "0.8.3", "version": "0.8.4",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/benchmark", "name": "@wrnexus/benchmark",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "Deterministic benchmark runner and performance regression budgets for WRNexusJS.", "description": "Deterministic benchmark runner and performance regression budgets for WRNexusJS.",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/cache", "name": "@wrnexus/cache",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "Tag-aware memory and response caching with stale-while-revalidate for WRNexusJS.", "description": "Tag-aware memory and response caching with stale-while-revalidate for WRNexusJS.",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/captcha", "name": "@wrnexus/captcha",
"version": "0.8.3", "version": "0.8.4",
"description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.", "description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.",
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+14
View File
@@ -1999,6 +1999,20 @@ const MIGRATIONS: Migration[] = [
// The compile-cache generation changes automatically invalidate old artifacts. // 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. */ /** Release tooling uses this to require an explicit migration entry per version. */
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/compiler", "name": "@wrnexus/compiler",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/content", "name": "@wrnexus/content",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "Typed Markdown content collections, loaders, indexes, feeds, and preview workflows for WRNexusJS.", "description": "Typed Markdown content collections, loaders, indexes, feeds, and preview workflows for WRNexusJS.",
"main": "./src/index.ts", "main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/core", "name": "@wrnexus/core",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/csr", "name": "@wrnexus/csr",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/db", "name": "@wrnexus/db",
"version": "0.8.3", "version": "0.8.4",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "./src/index.ts", "main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/dev-toolbar", "name": "@wrnexus/dev-toolbar",
"version": "0.8.3", "version": "0.8.4",
"private": true, "private": true,
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/encryption", "name": "@wrnexus/encryption",
"version": "0.8.3", "version": "0.8.4",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "./src/index.ts", "main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/graphql", "name": "@wrnexus/graphql",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"main": "./src/index.ts", "main": "./src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/helpers", "name": "@wrnexus/helpers",
"version": "0.8.3", "version": "0.8.4",
"private": true, "private": true,
"type": "module", "type": "module",
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/i18n", "name": "@wrnexus/i18n",
"version": "0.8.3", "version": "0.8.4",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "./src/index.ts", "main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/identity", "name": "@wrnexus/identity",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "Enterprise federation, provisioning, machine identity, and privacy governance for WRNexusJS.", "description": "Enterprise federation, provisioning, machine identity, and privacy governance for WRNexusJS.",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/image", "name": "@wrnexus/image",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "Responsive image planning, secure remote image policies, and performance auditing for WRNexusJS.", "description": "Responsive image planning, secure remote image policies, and performance auditing for WRNexusJS.",
"main": "./src/index.ts", "main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/jwt", "name": "@wrnexus/jwt",
"version": "0.8.3", "version": "0.8.4",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "./src/index.ts", "main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/language-server", "name": "@wrnexus/language-server",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "Editor-neutral Language Server Protocol implementation for WRNexus .wrn files.", "description": "Editor-neutral Language Server Protocol implementation for WRNexus .wrn files.",
"main": "src/index.ts", "main": "src/index.ts",
+53 -6
View File
@@ -36,11 +36,33 @@ function send(value: unknown): void {
function result(id: JsonRpc["id"], value: unknown): void { function result(id: JsonRpc["id"], value: unknown): void {
send({ jsonrpc: "2.0", id, result: value }); 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 { function publish(document: TextDocument): void {
send({ send({
jsonrpc: "2.0", jsonrpc: "2.0",
method: "textDocument/publishDiagnostics", method: "textDocument/publishDiagnostics",
params: { uri: document.uri, diagnostics: documentDiagnostics(document) }, params: { uri: document.uri, diagnostics: safeDocumentDiagnostics(document) },
}); });
} }
@@ -80,7 +102,7 @@ async function handle(message: JsonRpc): Promise<void> {
case "initialize": case "initialize":
workspaceRoot = params.rootPath ?? rootFromUri(params.rootUri); workspaceRoot = params.rootPath ?? rootFromUri(params.rootUri);
result(message.id, { result(message.id, {
serverInfo: { name: "WRNexus Language Server", version: "0.8.0" }, serverInfo: { name: "WRNexus Language Server", version: "0.8.3" },
capabilities: { capabilities: {
textDocumentSync: 1, textDocumentSync: 1,
documentFormattingProvider: true, documentFormattingProvider: true,
@@ -195,7 +217,7 @@ async function handle(message: JsonRpc): Promise<void> {
result(message.id, []); result(message.id, []);
break; break;
} }
const actions: any[] = documentDiagnostics(document) const actions: any[] = safeDocumentDiagnostics(document)
.filter((item) => item.code === "WRNA11Y001") .filter((item) => item.code === "WRNA11Y001")
.map((item) => ({ .map((item) => ({
title: "Add empty alt attribute", title: "Add empty alt attribute",
@@ -268,11 +290,36 @@ function consume(): void {
if (buffer.length < bodyStart + length) return; if (buffer.length < bodyStart + length) return;
const body = buffer.subarray(bodyStart, bodyStart + length).toString(); const body = buffer.subarray(bodyStart, bodyStart + length).toString();
buffer = buffer.subarray(bodyStart + length); 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) => { process.stdin.on("data", (chunk) => {
buffer = Buffer.concat([buffer, Buffer.from(chunk)]); try {
consume(); 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(); process.stdin.resume();
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/mcp", "name": "@wrnexus/mcp",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "Model Context Protocol server exposing WRNexus application and framework context.", "description": "Model Context Protocol server exposing WRNexus application and framework context.",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/mobile", "name": "@wrnexus/mobile",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/native", "name": "@wrnexus/native",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/oauth", "name": "@wrnexus/oauth",
"version": "0.8.3", "version": "0.8.4",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/observability", "name": "@wrnexus/observability",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "Metrics, Web Vitals collection, request instrumentation, and exporter adapters for WRNexusJS.", "description": "Metrics, Web Vitals collection, request instrumentation, and exporter adapters for WRNexusJS.",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/playground", "name": "@wrnexus/playground",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "Secure, shareable WRNexus compiler and UI playground.", "description": "Secure, shareable WRNexus compiler and UI playground.",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/plugin", "name": "@wrnexus/plugin",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/pubsub", "name": "@wrnexus/pubsub",
"version": "0.8.3", "version": "0.8.4",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/pwa", "name": "@wrnexus/pwa",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "Service workers, manifests, offline queues, background sync, push, and conflict resolution for WRNexusJS.", "description": "Service workers, manifests, offline queues, background sync, push, and conflict resolution for WRNexusJS.",
"main": "./src/index.ts", "main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/queue", "name": "@wrnexus/queue",
"version": "0.8.3", "version": "0.8.4",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/reactive", "name": "@wrnexus/reactive",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/realtime", "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.", "description": "Typed realtime rooms, message helpers, presence utilities, package UI blocks, and WRNexusJS integration.",
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/router", "name": "@wrnexus/router",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/security", "name": "@wrnexus/security",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"description": "Security policies, safe serialization, SSRF protection, request limits, and secure cookie helpers for WRNexusJS.", "description": "Security policies, safe serialization, SSRF protection, request limits, and secure cookie helpers for WRNexusJS.",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ssr", "name": "@wrnexus/ssr",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/store", "name": "@wrnexus/store",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/styles", "name": "@wrnexus/styles",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/syntax", "name": "@wrnexus/syntax",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+140 -3
View File
@@ -416,6 +416,17 @@ function formatOpeningTag(value, unit, depth, printWidth = 100, multilineAttribu
}; };
} }
if (parsed.attributes.length === 0 && !parsed.selfClosing) {
const lines = [`${baseIndent}<${parsed.tagName}>`];
if (parsed.trailingClosing) {
if (parsed.inlineContent) lines.push(`${childIndent}${parsed.inlineContent}`);
lines.push(`${baseIndent}</${parsed.tagName}>`);
} else if (parsed.remainder) {
lines.push(`${childIndent}${parsed.remainder}`);
}
return { lines, opensElement };
}
const lines = [ const lines = [
`${baseIndent}<${parsed.tagName}`, `${baseIndent}<${parsed.tagName}`,
...parsed.attributes.flatMap((attribute) => formatAttribute(attribute, childIndent, unit)), ...parsed.attributes.flatMap((attribute) => formatAttribute(attribute, childIndent, unit)),
@@ -468,6 +479,75 @@ function isClosingTag(value) {
return /^<\/[A-Za-z][\w$:.-]*\s*>/.test(value); return /^<\/[A-Za-z][\w$:.-]*\s*>/.test(value);
} }
/**
* Preserve compact, already-balanced HTML fragments as one line.
*
* A line such as `<span>Page</span><b>→</b><span>API</span>` is valid and
* intentionally compact. Expanding only its first opening tag makes later
* formatter passes treat the remaining siblings as children, causing runaway
* indentation and false closing-tag diagnostics. Balanced fragments are kept
* intact while normal multiline opening tags continue through the formatter.
*/
function isBalancedInlineHtmlFragment(value) {
if (!value.startsWith("<") || value.startsWith("</") || value.startsWith("<!--")) {
return false;
}
const stack = [];
let tagCount = 0;
let rootCount = 0;
let hasNestedElement = false;
let hasOutsideText = false;
let index = 0;
while (index < value.length) {
const tagStart = value.indexOf("<", index);
if (tagStart === -1) {
if (stack.length === 0 && value.slice(index).trim()) hasOutsideText = true;
break;
}
if (stack.length === 0 && value.slice(index, tagStart).trim()) hasOutsideText = true;
if (value.startsWith("<!--", tagStart)) {
const commentEnd = value.indexOf("-->", tagStart + 4);
if (commentEnd === -1) return false;
index = commentEnd + 3;
continue;
}
const relativeEnd = findOpeningTagEnd(value.slice(tagStart));
if (relativeEnd === -1) return false;
const tag = value.slice(tagStart, tagStart + relativeEnd + 1);
const match = /^<\/?([A-Za-z][\w$:.-]*)[\s\S]*?>$/.exec(tag);
if (!match) {
index = tagStart + 1;
continue;
}
tagCount += 1;
const tagName = match[1];
const normalizedName = /^[a-z]/.test(tagName) ? tagName.toLowerCase() : tagName;
const closing = tag.startsWith("</");
const selfClosing = /\/\s*>$/.test(tag);
const voidElement = VOID_ELEMENTS.has(tagName.toLowerCase());
if (closing) {
if (stack.at(-1) !== normalizedName) return false;
stack.pop();
} else {
if (stack.length === 0) rootCount += 1;
else hasNestedElement = true;
if (!selfClosing && !voidElement) stack.push(normalizedName);
}
index = tagStart + relativeEnd + 1;
}
return (
tagCount >= 2 && stack.length === 0 && (rootCount > 1 || hasNestedElement || hasOutsideText)
);
}
function isControlBlockOpen(value) { function isControlBlockOpen(value) {
return /^\{#(?:if|each)\b[\s\S]*\}$/.test(value); return /^\{#(?:if|each)\b[\s\S]*\}$/.test(value);
} }
@@ -597,6 +677,50 @@ function collectOpeningTag(inputLines, startIndex) {
}; };
} }
function isPreservedRawBlockStart(value) {
return (
/<pre(?:\s|>)/i.test(value) &&
!/<\/pre\s*>/i.test(value.slice(0, value.search(/<pre(?:\s|>)/i)))
);
}
function hasPreservedRawBlockEnd(value) {
return /<\/pre\s*>/i.test(value);
}
function transformOutsidePreservedRawBlocks(lines, transformLine) {
const output = [];
let preserving = false;
for (const line of lines) {
if (preserving) {
output.push(line);
if (hasPreservedRawBlockEnd(line)) preserving = false;
continue;
}
if (isPreservedRawBlockStart(line)) {
output.push(line);
preserving = !hasPreservedRawBlockEnd(line);
continue;
}
output.push(...transformLine(line));
}
return output;
}
function collectPreservedRawBlock(lines, startIndex) {
const collected = [lines[startIndex]];
let index = startIndex;
while (!hasPreservedRawBlockEnd(collected.at(-1) || "") && index + 1 < lines.length) {
index += 1;
collected.push(lines[index]);
}
return { lines: collected, endIndex: index };
}
/** /**
* Put WRN template control markers on their own lines before indentation. * Put WRN template control markers on their own lines before indentation.
* *
@@ -608,7 +732,7 @@ function expandInlineControlBlocks(lines) {
const marker = const marker =
/(\{#(?:if|each)\b[^}]*\}|\{:(?:else(?:\s+if\b[^}]*)?|empty)\}|\{\/(?:if|each)\})/g; /(\{#(?: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]; if (!marker.test(line)) return [line];
marker.lastIndex = 0; marker.lastIndex = 0;
@@ -623,7 +747,7 @@ function expandInlineControlBlocks(lines) {
} }
function expandStructuredStateDeclarations(lines, unit) { 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); const match = /^(\s*state\s+[A-Za-z_$][\w$]*\s*=\s*)([\\[{][\s\S]*)$/.exec(line);
if (!match) return [line]; if (!match) return [line];
@@ -695,6 +819,15 @@ function formatWrnPass(source: string, options: FormatWrnOptions = {}): string {
continue; 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)) { if (isMultilineOpeningTagStart(value)) {
const collected = collectOpeningTag(inputLines, index); const collected = collectOpeningTag(inputLines, index);
@@ -730,7 +863,11 @@ function formatWrnPass(source: string, options: FormatWrnOptions = {}): string {
const depth = lineCodeDepth + lineHtmlDepth + lineControlDepth; const depth = lineCodeDepth + lineHtmlDepth + lineControlDepth;
if ( const inlineFragmentFits = unit.repeat(depth).length + value.length <= printWidth;
if (isBalancedInlineHtmlFragment(value) && inlineFragmentFits) {
output.push(`${unit.repeat(depth)}${value}`);
} else if (
value.startsWith("<") && value.startsWith("<") &&
!value.startsWith("</") && !value.startsWith("</") &&
!value.startsWith("<!--") && !value.startsWith("<!--") &&
+18 -3
View File
@@ -6,9 +6,24 @@ export type RuntimeType =
export function runtimeTypeOf(annotation: string | undefined): RuntimeType { export function runtimeTypeOf(annotation: string | undefined): RuntimeType {
if (!annotation) return "unknown"; if (!annotation) return "unknown";
const type = annotation.trim().replace(/^readonly\s+/, ""); const type = annotation.trim().replace(/^readonly\s+/, "");
if (/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(type)) return "string"; const unionParts = type.split("|").map((part) => part.trim());
if (/^(?:number|Number)(?:\s*\|\s*(?:null|undefined))*$/.test(type)) return "number"; const concreteParts = unionParts.filter((part) => !/^(?:null|undefined)$/.test(part));
if (/^(?:boolean|Boolean)(?:\s*\|\s*(?:null|undefined))*$/.test(type)) return "boolean"; 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 (/^bigint(?:\s*\|\s*(?:null|undefined))*$/.test(type)) return "bigint";
if (/^(?:Array\s*<|ReadonlyArray\s*<|.+\[\])/.test(type) || /^\[/.test(type)) return "array"; if (/^(?:Array\s*<|ReadonlyArray\s*<|.+\[\])/.test(type) || /^\[/.test(type)) return "array";
if (/^(?:Record\s*<|object\b|\{)/.test(type)) return "object"; if (/^(?:Record\s*<|object\b|\{)/.test(type)) return "object";
+46
View File
@@ -37,3 +37,49 @@ view {
expect(formatted).not.toContain("\r"); expect(formatted).not.toContain("\r");
}); });
}); });
test("preserves preformatted code examples while formatting surrounding markup", () => {
const source = `page Docs {
view {
<section class="docs">
<pre><code><span class="code-muted">// One project, one language</span>
<span class="code-key">page</span> ProductDashboard {
<span class="code-key">state</span> count: number = 0
&lt;button @click='count++'&gt;Ship {count}&lt;/button&gt;
}</code></pre>
</section>
}
}
`;
const formatted = formatWrn(source, options);
expect(formatted)
.toContain(`<pre><code><span class="code-muted">// One project, one language</span>
<span class="code-key">page</span> ProductDashboard {
<span class="code-key">state</span> count: number = 0`);
expect(formatWrn(formatted, options)).toBe(formatted);
expect(() => parse(formatted)).not.toThrow();
});
test("does not split an attribute-free opening tag delimiter", () => {
const source = `page Docs {
view {
<p>This is intentionally long documentation copy that should wrap as content without formatting the opening p tag as separate less-than and greater-than lines.</p>
}
}
`;
const formatted = formatWrn(source, options);
expect(formatted).toContain(" <p>\n");
expect(formatted).not.toContain(" <p\n >");
expect(formatWrn(formatted, options)).toBe(formatted);
});
test("preserves balanced compact sibling markup without indentation drift", () => {
const source = `page Compact {
view {
<div class="strip"><span>Page</span><b></b><span>API</span></div>
}
}\n`;
const formatted = formatWrn(source, { tabSize: 2, printWidth: 80 });
expect(formatted).toContain('<div class="strip"><span>Page</span><b>→</b><span>API</span></div>');
expect(formatWrn(formatted, { tabSize: 2, printWidth: 80 })).toBe(formatted);
});
+7
View File
@@ -200,3 +200,10 @@ test("parses edge and worker execution targets", () => {
); );
} }
}); });
test("maps literal unions to their runtime primitive types", async () => {
const { runtimeTypeOf } = await import("../src/types.ts");
expect(runtimeTypeOf('"_blank" | "_self"')).toBe("string");
expect(runtimeTypeOf("1 | 2 | 3")).toBe("number");
expect(runtimeTypeOf("true | false")).toBe("boolean");
});
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/test", "name": "@wrnexus/test",
"version": "0.8.3", "version": "0.8.4",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/tracking", "name": "@wrnexus/tracking",
"version": "0.8.3", "version": "0.8.4",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/typecheck", "name": "@wrnexus/typecheck",
"version": "0.8.3", "version": "0.8.4",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+129 -29
View File
@@ -139,9 +139,13 @@ function functionDeclaration(fn: RuntimeFunctionDecl): string {
const params = fn.parameters const params = fn.parameters
.map( .map(
(param) => (param) =>
`${param.name}${param.optional ? "?" : ""}${param.valueType ? `: ${param.valueType}` : param.name === "event" ? ": CustomEvent<any> & { target: HTMLElement }" : ": unknown"}${param.default ? ` = ${param.default}` : ""}`, `${param.name}${param.optional ? "?" : ""}${param.valueType ? `: ${param.valueType}` : param.name === "event" ? ": CustomEvent<any> & { target: HTMLElement }" : ": any"}${param.default ? ` = ${param.default}` : ""}`,
) )
.join(", "); .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}}`; 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"); .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( export function virtualTypeScriptModule(
source: string, source: string,
filePath = "component.wrn", filePath = "component.wrn",
@@ -190,6 +241,10 @@ export function virtualTypeScriptModule(
append(ast.types.join("\n\n"), ast.types[0]?.trim()); append(ast.types.join("\n\n"), ast.types[0]?.trim());
for (const prop of ast.props) { 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); append(`declare const ${prop.name}: Readonly<${prop.valueType ?? "unknown"}>;`, prop.name);
} }
for (const state of ast.states) { for (const state of ast.states) {
@@ -218,7 +273,7 @@ export function virtualTypeScriptModule(
`${fn.name}: (${fn.parameters.map((param) => `${param.name}${param.optional ? "?" : ""}: ${param.valueType ?? "unknown"}`).join(", ")}) => ${fn.returnType ?? (fn.async ? "Promise<unknown>" : "unknown")}`, `${fn.name}: (${fn.parameters.map((param) => `${param.name}${param.optional ? "?" : ""}: ${param.valueType ?? "unknown"}`).join(", ")}) => ${fn.returnType ?? (fn.async ? "Promise<unknown>" : "unknown")}`,
) )
.join("; "); .join("; ");
append(`declare const output: { ${outputType} };`); append(`declare const output: { [name: string]: (...args: any[]) => void; ${outputType} };`);
for (const output of ast.outputs) for (const output of ast.outputs)
append( append(
`declare const ${output.name}: (${output.payload ? `${output.payload.name}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}) => void;`, `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; if (attr.value.startsWith("{") && attr.value.endsWith("}")) continue;
const received = attr.boolean ? "boolean" : inferredRuntimeType(JSON.stringify(attr.value)); const received = attr.boolean ? "boolean" : inferredRuntimeType(JSON.stringify(attr.value));
const expected = runtimeTypeOf(prop.type); 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({ diagnostics.push({
code: "WRN-COMPONENT-PROP-TYPE", code: "WRN-COMPONENT-PROP-TYPE",
category: "error", category: "error",
@@ -481,8 +544,15 @@ function category(value: ts.DiagnosticCategory): WrnTypeDiagnostic["category"] {
function hostWithVirtualFiles( function hostWithVirtualFiles(
files: Map<string, string>, files: Map<string, string>,
options: ts.CompilerOptions, options: ts.CompilerOptions,
standardLibraryDirectory?: string,
): ts.CompilerHost { ): ts.CompilerHost {
const host = ts.createCompilerHost(options, true); 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); const originalGet = host.getSourceFile.bind(host);
host.fileExists = (fileName) => files.has(normalize(fileName)) || ts.sys.fileExists(fileName); host.fileExists = (fileName) => files.has(normalize(fileName)) || ts.sys.fileExists(fileName);
host.readFile = (fileName) => files.get(normalize(fileName)) ?? ts.sys.readFile(fileName); host.readFile = (fileName) => files.get(normalize(fileName)) ?? ts.sys.readFile(fileName);
@@ -495,15 +565,32 @@ function hostWithVirtualFiles(
return host; 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( function mappedPosition(
virtual: VirtualTypeScriptModule, virtual: VirtualTypeScriptModule,
line: number, line: number,
column: number, column: number,
): { line: number; column: number } { ): { line: number; column: number } | null {
const mapping = virtual.mappings.find( const mapping = virtual.mappings.find(
(entry) => line >= entry.virtualStartLine && line <= entry.virtualEndLine, (entry) => line >= entry.virtualStartLine && line <= entry.virtualEndLine,
); );
if (!mapping) return { line: 1, column: 1 }; if (!mapping) return null;
const offset = line - mapping.virtualStartLine; const offset = line - mapping.virtualStartLine;
return { return {
line: mapping.sourceStartLine + offset, 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"], lib: ["lib.esnext.d.ts", "lib.dom.d.ts", "lib.dom.iterable.d.ts"],
}; };
const rootNames = [virtual.fileName, ...appTypes.files.keys()]; const rootNames = [virtual.fileName, ...appTypes.files.keys()];
const program = ts.createProgram( const standardLibraryDirectory = resolveStandardLibraryDirectory(appRoot, compilerOptions);
rootNames,
compilerOptions, const tsDiagnostics: WrnTypeDiagnostic[] = [];
hostWithVirtualFiles(files, compilerOptions), if (standardLibraryDirectory) {
); const program = ts.createProgram(
const tsDiagnostics = ts.getPreEmitDiagnostics(program).map((diagnostic): WrnTypeDiagnostic => { rootNames,
const file = diagnostic.file; compilerOptions,
const start = diagnostic.start ?? 0; hostWithVirtualFiles(files, compilerOptions, standardLibraryDirectory),
const virtualPosition = file?.getLineAndCharacterOfPosition(start) ?? { line: 0, character: 0 }; );
const isVirtual = normalize(file?.fileName ?? "") === normalize(virtual.fileName); for (const diagnostic of ts.getPreEmitDiagnostics(program)) {
const sourcePosition = isVirtual const file = diagnostic.file;
? mappedPosition(virtual, virtualPosition.line + 1, virtualPosition.character + 1) if (!file) continue;
: { line: virtualPosition.line + 1, column: virtualPosition.character + 1 }; const normalizedFile = normalize(file.fileName);
return { const isVirtual = normalizedFile === normalize(virtual.fileName);
code: `WRN-TYPE-${diagnostic.code}`, const isApplicationType = files.has(normalizedFile);
category: category(diagnostic.category), if (!isVirtual && !isApplicationType) continue;
message: ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"),
file: isVirtual ? filePath : (file?.fileName ?? filePath), const start = diagnostic.start ?? 0;
line: sourcePosition.line, const virtualPosition = file.getLineAndCharacterOfPosition(start);
column: sourcePosition.column, const sourcePosition = isVirtual
length: diagnostic.length ?? 1, ? mappedPosition(virtual, virtualPosition.line + 1, virtualPosition.character + 1)
hint: "Fix the TypeScript contract or expression in the related .wrn declaration.", : { 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 [ return [
...(options.checkRuntimeBoundaries === false ...(options.checkRuntimeBoundaries === false
? [] ? []
@@ -60,3 +60,26 @@ page Home {
); );
expect(diagnostics.some((item) => item.code === "WRN-TYPE-2345")).toBe(true); expect(diagnostics.some((item) => item.code === "WRN-TYPE-2345")).toBe(true);
}, 15_000); }, 15_000);
test("accepts WRN boolean string literals and reserved class props", () => {
const root = fixture();
const componentPath = join(root, "app", "components", "Toggle.wrn");
writeFileSync(
componentPath,
`component Toggle {
props { enabled: boolean = false class: string = "" }
view { <button class='{class}'></button> }
}`,
);
const filePath = join(root, "app", "pages", "home.wrn");
const diagnostics = checkWrnSource(
`import Toggle from "@/components/Toggle.wrn"
page Home {
view { <Toggle enabled="true" class="demo" /> }
}`,
{ appRoot: root, filePath },
);
expect(diagnostics.some((item) => item.code === "WRN-COMPONENT-PROP-TYPE")).toBe(false);
expect(diagnostics.some((item) => item.code === "WRN-TYPE-1389")).toBe(false);
expect(diagnostics.some((item) => item.code === "WRN-TYPE-1005")).toBe(false);
}, 15_000);

Some files were not shown because too many files have changed in this diff Show More