From 959ed24009172b6e219a930d8d3e79912975aff8 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Tue, 14 Jul 2026 22:39:50 +0530 Subject: [PATCH] release: WRNexusJS 0.2.32 --- .publish/ai/package.json | 2 +- .publish/authz/package.json | 2 +- .publish/cli/package.json | 22 +- .publish/compiler/package.json | 2 +- .publish/core/package.json | 2 +- .publish/csr/package.json | 4 +- .publish/db/package.json | 2 +- .publish/dev-server/package.json | 26 +- .publish/encryption/package.json | 2 +- .publish/helpers/package.json | 4 +- .publish/i18n/package.json | 4 +- .publish/jwt/package.json | 2 +- .publish/mobile/package.json | 4 +- .publish/native/package.json | 2 +- .publish/oauth/package.json | 2 +- .publish/pubsub/package.json | 2 +- .publish/queue/package.json | 2 +- .publish/reactive/package.json | 2 +- .publish/router/package.json | 6 +- .publish/ssr/package.json | 4 +- .publish/styles/package.json | 4 +- .publish/test/package.json | 2 +- .publish/tracking/package.json | 2 +- .publish/ui/package.json | 4 +- .publish/uploader/package.json | 4 +- .publish/validation/package.json | 2 +- editors/vscode/README.md | 803 ++++++++++++++++++-- editors/vscode/package.json | 88 ++- editors/vscode/snippets/wrn.json | 216 +++++- editors/vscode/src/compiler.cjs | 97 ++- editors/vscode/src/completion.js | 386 +++++++++- editors/vscode/src/diagnostics.js | 512 ++++++++++++- editors/vscode/src/extension.js | 460 +++++------ editors/vscode/src/formatter.js | 208 +++-- editors/vscode/syntaxes/wrn.tmLanguage.json | 677 +++++++++++++---- editors/vscode/wrnexus-0.2.7.vsix | Bin 0 -> 48585 bytes packages/ai/package.json | 2 +- packages/authz/package.json | 2 +- packages/cli/package.json | 2 +- packages/cli/src/update.ts | 9 + packages/compiler/package.json | 2 +- packages/compiler/src/codegen.ts | 4 +- packages/compiler/test/compiler.test.ts | 126 ++- packages/core/package.json | 2 +- packages/csr/package.json | 2 +- packages/csr/src/reactive-runtime.ts | 29 +- packages/db/package.json | 2 +- packages/dev-server/package.json | 2 +- packages/encryption/package.json | 2 +- packages/helpers/package.json | 2 +- packages/i18n/package.json | 2 +- packages/jwt/package.json | 2 +- packages/mobile/package.json | 2 +- packages/native/package.json | 2 +- packages/oauth/package.json | 2 +- packages/pubsub/package.json | 2 +- packages/queue/package.json | 2 +- packages/reactive/package.json | 2 +- packages/router/package.json | 2 +- packages/ssr/package.json | 2 +- packages/styles/package.json | 2 +- packages/test/package.json | 2 +- packages/tracking/package.json | 2 +- packages/ui/package.json | 2 +- packages/uploader/package.json | 2 +- packages/validation/package.json | 2 +- 66 files changed, 3135 insertions(+), 648 deletions(-) create mode 100644 editors/vscode/wrnexus-0.2.7.vsix diff --git a/.publish/ai/package.json b/.publish/ai/package.json index 8c12fc31..f5508aec 100644 --- a/.publish/ai/package.json +++ b/.publish/ai/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ai", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", "license": "MIT", diff --git a/.publish/authz/package.json b/.publish/authz/package.json index c32c98b4..3caffcb9 100644 --- a/.publish/authz/package.json +++ b/.publish/authz/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/authz", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "@wrnexus/authz — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/cli/package.json b/.publish/cli/package.json index bc09ae28..e7abfd3a 100644 --- a/.publish/cli/package.json +++ b/.publish/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "@wrnexus/cli — part of the WrNexus framework.", "license": "MIT", @@ -28,16 +28,16 @@ "wrnexus": "./dist/index.js" }, "dependencies": { - "@wrnexus/core": "^0.2.31", - "@wrnexus/router": "^0.2.31", - "@wrnexus/csr": "^0.2.31", - "@wrnexus/compiler": "^0.2.31", - "@wrnexus/styles": "^0.2.31", - "@wrnexus/dev-server": "^0.2.31", - "@wrnexus/ui": "^0.2.31", - "@wrnexus/validation": "^0.2.31", - "@wrnexus/i18n": "^0.2.31", - "@wrnexus/db": "^0.2.31" + "@wrnexus/core": "^0.2.32", + "@wrnexus/router": "^0.2.32", + "@wrnexus/csr": "^0.2.32", + "@wrnexus/compiler": "^0.2.32", + "@wrnexus/styles": "^0.2.32", + "@wrnexus/dev-server": "^0.2.32", + "@wrnexus/ui": "^0.2.32", + "@wrnexus/validation": "^0.2.32", + "@wrnexus/i18n": "^0.2.32", + "@wrnexus/db": "^0.2.32" }, "files": [ "dist" diff --git a/.publish/compiler/package.json b/.publish/compiler/package.json index e5a5c01c..455aab8a 100644 --- a/.publish/compiler/package.json +++ b/.publish/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "@wrnexus/compiler — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/core/package.json b/.publish/core/package.json index 99291dae..da0c67b4 100644 --- a/.publish/core/package.json +++ b/.publish/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "@wrnexus/core — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/csr/package.json b/.publish/csr/package.json index 1d740a6e..8b0c485a 100644 --- a/.publish/csr/package.json +++ b/.publish/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "@wrnexus/csr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.31" + "@wrnexus/core": "^0.2.32" }, "files": [ "dist" diff --git a/.publish/db/package.json b/.publish/db/package.json index d2bb4a36..2b800e80 100644 --- a/.publish/db/package.json +++ b/.publish/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "@wrnexus/db — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/dev-server/package.json b/.publish/dev-server/package.json index ea690999..d86956d3 100644 --- a/.publish/dev-server/package.json +++ b/.publish/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "@wrnexus/dev-server — part of the WrNexus framework.", "license": "MIT", @@ -25,18 +25,18 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.31", - "@wrnexus/router": "^0.2.31", - "@wrnexus/ssr": "^0.2.31", - "@wrnexus/csr": "^0.2.31", - "@wrnexus/compiler": "^0.2.31", - "@wrnexus/styles": "^0.2.31", - "@wrnexus/ui": "^0.2.31", - "@wrnexus/validation": "^0.2.31", - "@wrnexus/i18n": "^0.2.31", - "@wrnexus/db": "^0.2.31", - "@wrnexus/pubsub": "^0.2.31", - "@wrnexus/uploader": "^0.2.31" + "@wrnexus/core": "^0.2.32", + "@wrnexus/router": "^0.2.32", + "@wrnexus/ssr": "^0.2.32", + "@wrnexus/csr": "^0.2.32", + "@wrnexus/compiler": "^0.2.32", + "@wrnexus/styles": "^0.2.32", + "@wrnexus/ui": "^0.2.32", + "@wrnexus/validation": "^0.2.32", + "@wrnexus/i18n": "^0.2.32", + "@wrnexus/db": "^0.2.32", + "@wrnexus/pubsub": "^0.2.32", + "@wrnexus/uploader": "^0.2.32" }, "files": [ "dist" diff --git a/.publish/encryption/package.json b/.publish/encryption/package.json index ddf393c2..de67047a 100644 --- a/.publish/encryption/package.json +++ b/.publish/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "@wrnexus/encryption — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/helpers/package.json b/.publish/helpers/package.json index b5d5912b..7e19edc7 100644 --- a/.publish/helpers/package.json +++ b/.publish/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.31" + "@wrnexus/core": "^0.2.32" }, "files": [ "dist" diff --git a/.publish/i18n/package.json b/.publish/i18n/package.json index 96ebfa1d..e0e0a116 100644 --- a/.publish/i18n/package.json +++ b/.publish/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "@wrnexus/i18n — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.31" + "@wrnexus/core": "^0.2.32" }, "files": [ "dist" diff --git a/.publish/jwt/package.json b/.publish/jwt/package.json index fe9e1f9c..8eb3a37e 100644 --- a/.publish/jwt/package.json +++ b/.publish/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "@wrnexus/jwt — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/mobile/package.json b/.publish/mobile/package.json index 00c92765..aa83797b 100644 --- a/.publish/mobile/package.json +++ b/.publish/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "@wrnexus/mobile — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/native": "^0.2.31" + "@wrnexus/native": "^0.2.32" }, "files": [ "dist" diff --git a/.publish/native/package.json b/.publish/native/package.json index b29abc42..7bbc1466 100644 --- a/.publish/native/package.json +++ b/.publish/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "@wrnexus/native — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/oauth/package.json b/.publish/oauth/package.json index e4368bb9..f1b662cc 100644 --- a/.publish/oauth/package.json +++ b/.publish/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "@wrnexus/oauth — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/pubsub/package.json b/.publish/pubsub/package.json index 62def059..95173413 100644 --- a/.publish/pubsub/package.json +++ b/.publish/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "@wrnexus/pubsub — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/queue/package.json b/.publish/queue/package.json index 8cb6b62d..c36090b4 100644 --- a/.publish/queue/package.json +++ b/.publish/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "@wrnexus/queue — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/reactive/package.json b/.publish/reactive/package.json index 11cb802b..755aa01c 100644 --- a/.publish/reactive/package.json +++ b/.publish/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "@wrnexus/reactive — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/router/package.json b/.publish/router/package.json index 51cdc407..a6b83545 100644 --- a/.publish/router/package.json +++ b/.publish/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "@wrnexus/router — part of the WrNexus framework.", "license": "MIT", @@ -21,8 +21,8 @@ } }, "dependencies": { - "@wrnexus/compiler": "^0.2.31", - "@wrnexus/core": "^0.2.31" + "@wrnexus/compiler": "^0.2.32", + "@wrnexus/core": "^0.2.32" }, "files": [ "dist" diff --git a/.publish/ssr/package.json b/.publish/ssr/package.json index 68c399b5..1c41c05e 100644 --- a/.publish/ssr/package.json +++ b/.publish/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "@wrnexus/ssr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.31" + "@wrnexus/core": "^0.2.32" }, "files": [ "dist" diff --git a/.publish/styles/package.json b/.publish/styles/package.json index ad8391d8..0e9e9c45 100644 --- a/.publish/styles/package.json +++ b/.publish/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "@wrnexus/styles — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/uploader": "^0.2.31" + "@wrnexus/uploader": "^0.2.32" }, "files": [ "dist" diff --git a/.publish/test/package.json b/.publish/test/package.json index b3a55130..7db15391 100644 --- a/.publish/test/package.json +++ b/.publish/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "@wrnexus/test — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/tracking/package.json b/.publish/tracking/package.json index 59d3de43..d86a763b 100644 --- a/.publish/tracking/package.json +++ b/.publish/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "@wrnexus/tracking — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/ui/package.json b/.publish/ui/package.json index 0861f921..fba5bbc8 100644 --- a/.publish/ui/package.json +++ b/.publish/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "@wrnexus/ui — part of the WrNexus framework.", "license": "MIT", @@ -22,7 +22,7 @@ "./ui.css": "./ui.css" }, "dependencies": { - "@wrnexus/core": "^0.2.31" + "@wrnexus/core": "^0.2.32" }, "files": [ "dist", diff --git a/.publish/uploader/package.json b/.publish/uploader/package.json index 1a6414b5..be788875 100644 --- a/.publish/uploader/package.json +++ b/.publish/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "@wrnexus/uploader — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.31" + "@wrnexus/core": "^0.2.32" }, "files": [ "dist" diff --git a/.publish/validation/package.json b/.publish/validation/package.json index 6e2ef57d..33f4a40a 100644 --- a/.publish/validation/package.json +++ b/.publish/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "description": "@wrnexus/validation — part of the WrNexus framework.", "license": "MIT", diff --git a/editors/vscode/README.md b/editors/vscode/README.md index 4bd94aa6..706c1b54 100644 --- a/editors/vscode/README.md +++ b/editors/vscode/README.md @@ -1,54 +1,614 @@ # WRNexus Language Support -Language support for [WRNexus](https://wrnexusjs.dev) `.wrn` files — the SSR-first, -Bun-powered full-stack framework. +Complete VS Code language support for [WRNexus](https://wrnexusjs.dev) `.wrn` files. + +WRNexus is an SSR-first, Bun-powered full-stack framework with reactive components, layouts, colocated APIs, browser functions, lifecycle hooks, state watchers, client-side navigation, and reusable UI components. ## Installation -After the first Marketplace release, search for **WRNexus Language Support** in -VS Code or run: +Search for **WRNexus Language Support** in the VS Code Extensions view or run: ```text ext install wrnexus.wrnexus ``` -For a local package, open the Command Palette, choose **Extensions: Install from -VSIX**, and select the generated `wrnexus-.vsix` file. +For a local build: + +1. Package the extension: + +```bash +bun run package +``` + +2. Open the VS Code Command Palette. +3. Select **Extensions: Install from VSIX**. +4. Choose the generated `wrnexus-.vsix` file. + +You can also install it from the terminal: + +```bash +code --install-extension ./wrnexus-0.2.7.vsix --force +``` + +Reload VS Code after installation: + +```text +Developer: Reload Window +``` ## Features -- **Syntax highlighting** ("color-full code"). Keywords (`page`, `component`, - `state`, `view`, `api`, `realtime`, …) plus **embedded highlighting** for the - languages inside each block: - - `view { … }` → HTML, with `{expr}` interpolation and `{t:key}` translations - and `@event="…"` bindings picked out. - - `style { … }` → CSS. - - `functions { … }`, `api … { … }`, `ssr`/`client`, `realtime on(…) { … }` → TypeScript. -- **WrNexus attributes in distinct colors.** A grammar injection recolors the - framework's own attributes so they pop even inside plain HTML: `@event` - bindings and `{t:…}` in one accent, runtime directives (`data-component`, - `data-for`, `data-show`, `data-text`, `data-scope`, `data-slot`, `data-on-*`, - `data-wire-*`) in another. Default colors ship with the extension; change them - under `editor.tokenColorCustomizations` (scopes - `entity.other.attribute-name.wrn.directive` and `…wire.event`). -- **Diagnostics.** Parse errors from the real `@wrnexus/compiler` are shown inline - as you type, anchored to the exact offset. -- **Formatting.** Format Document and format-on-save normalize WRN block, HTML, - CSS, and function indentation without rewriting application expressions. -- **Snippets.** `page`, `component`, `view`, `state`, `props`, `seo`, `api`, - `ssr`, `client`, `realtime`, `functions`, `style`, plus view helpers `mount`, - `for`, `show`, `t`. -- **Completions.** Block keywords at file scope, `data-*` runtime attributes and - `@event` bindings inside a `view`, and HTTP methods after `api`. +### Syntax highlighting -## Settings +The extension provides highlighting for WRNexus declarations and embedded languages. -| Setting | Default | Description | -| ---------------------------- | ------- | ----------------------------------------- | -| `wrnexus.diagnostics.enable` | `true` | Show parse errors for `.wrn` files. | -| `wrnexus.format.enable` | `true` | Enable the built-in formatter for `.wrn`. | +Supported top-level declarations: -To format automatically when saving, add this to VS Code settings: +```wrn +page HomePage { +} + +component UserCard { +} + +layout PublicLayout { +} +``` + +Supported WRNexus blocks include: + +```wrn +props { +} + +state count = 0 + +seo { +} + +functions { +} + +lifecycle { +} + +watch count { +} + +view { +} + +style { +} + +api GET /api/users { +} + +ssr { +} + +client { +} + +realtime chat { +} +``` + +Embedded language highlighting: + +- `view { ... }` uses HTML highlighting. +- `style { ... }` uses CSS highlighting. +- `functions { ... }` uses TypeScript highlighting. +- Lifecycle hook bodies use TypeScript highlighting. +- Watcher bodies use TypeScript highlighting. +- API, SSR, client, and realtime bodies use TypeScript highlighting. +- `{expression}` interpolation uses TypeScript expression highlighting. + +### Pages + +Create route pages with layouts, SEO metadata, state, APIs, and views: + +```wrn +page UserPage { + layout = "public" + + state id = ctx.params.id + + seo { + title = "User profile" + description = "View user information" + } + + view { +
+

User {id}

+
+ } +} +``` + +Dynamic route parameters are available through: + +```wrn +state id = ctx.params.id +``` + +For a route such as: + +```text +pages/users/[id].wrn +``` + +autocomplete suggests: + +```text +id +ctx.params.id +``` + +### Components + +Create reusable reactive components: + +```wrn +component Counter { + state count = 0 + + view { + + } +} +``` + +Components support: + +- props +- reactive state +- browser functions +- lifecycle hooks +- state watchers +- event handlers +- conditional classes +- reactive attributes +- reactive loops +- conditional visibility + +### Layouts + +Create reusable layouts: + +```wrn +layout PublicLayout { + view { +
+ +
+ +
+ {content} +
+ } +} +``` + +Layouts can also use state, props, functions, lifecycle hooks, and watchers. + +## Browser functions + +Functions declared inside a component or layout are available to browser events, lifecycle hooks, and watchers. + +```wrn +component Counter { + state count = 0 + + functions { + function increment() { + count += 1 + } + + function reset() { + count = 0 + } + } + + view { + + + + +

{count}

+ } +} +``` + +Functions may use browser APIs: + +```wrn +functions { + function scrollToTop() { + window.scrollTo({ + top: 0, + behavior: "smooth" + }) + } +} +``` + +Asynchronous functions are also supported: + +```wrn +functions { + async function loadUsers() { + const response = await fetch("/api/users") + const users = await response.json() + } +} +``` + +## Lifecycle hooks + +Components and layouts support three lifecycle hooks: + +```wrn +lifecycle { + mount { + } + + update { + } + + unmount { + } +} +``` + +### `mount` + +Runs once after the component is connected to the page and hydrated. + +```wrn +lifecycle { + mount { + console.log("Component mounted") + } +} +``` + +Use it to: + +- read browser state +- initialize third-party libraries +- register global event listeners +- start timers +- perform initial browser-only work + +### `update` + +Runs after reactive state changes. + +Multiple synchronous state changes are batched into one update cycle. + +```wrn +lifecycle { + update { + console.log("Component state updated") + } +} +``` + +### `unmount` + +Runs before the component is removed. + +```wrn +lifecycle { + unmount { + console.log("Component removed") + } +} +``` + +Use it to: + +- remove global event listeners +- clear timers +- disconnect observers +- release browser resources +- clean up third-party libraries + +Lifecycle cleanup also runs during WRNexus client-side navigation. + +## State watchers + +Watch one declared state value: + +```wrn +component Menu { + state open = false + + watch open { + console.log("Current value:", value) + console.log("Previous value:", previous) + } + + view { + + } +} +``` + +Watcher variables: + +| Variable | Description | +| ---------- | -------------------- | +| `value` | Current state value | +| `previous` | Previous state value | + +A watcher runs only when its declared state changes. + +Invalid watcher declarations are reported: + +```wrn +watch missingState { +} +``` + +The editor reports that `missingState` has not been declared. + +## Window and document events + +WRNexus supports events attached directly to `window` and `document`. + +### Window events + +```wrn +
+
+``` + +```wrn +
+
+``` + +```wrn +
+
+``` + +### Document events + +```wrn +
+
+``` + +```wrn +
+
+``` + +Global event listeners created manually inside `mount` should be removed inside `unmount`. + +## Complete BackToTop example + +```wrn +component BackToTop { + state visible = false + + functions { + function updateBackToTopVisibility() { + visible = window.scrollY > 500 + } + + function scrollToTop() { + window.scrollTo({ + top: 0, + behavior: "smooth" + }) + } + } + + lifecycle { + mount { + updateBackToTopVisibility() + + window.addEventListener( + "scroll", + updateBackToTopVisibility, + { passive: true } + ) + } + + unmount { + window.removeEventListener( + "scroll", + updateBackToTopVisibility + ) + } + } + + watch visible { + console.log( + "BackToTop visibility changed:", + value, + previous + ) + } + + view { + + } +} +``` + +## Conditional classes + +Use `class:` to toggle CSS classes reactively: + +```wrn + +``` + +The expression is evaluated whenever its reactive dependencies change. + +## Event bindings + +Standard element events: + +```wrn + +``` + +```wrn + +``` + +```wrn +
+
+``` + +Supported completion suggestions include: + +- `@click` +- `@input` +- `@change` +- `@submit` +- `@focus` +- `@blur` +- `@keydown` +- `@keyup` +- `@mouseenter` +- `@mouseleave` +- `@window:scroll` +- `@window:resize` +- `@window:keydown` +- `@document:click` +- `@document:visibilitychange` + +## Reactive directives + +### Conditional visibility + +```wrn +
+ Visible when open is true +
+``` + +### Reactive loops + +```wrn +
    +
  • + {user.name} +
  • +
+``` + +### Reactive text + +```wrn + +``` + +### Dynamic attributes + +```wrn + +``` + +## Diagnostics + +The extension provides two diagnostic layers. + +### Editor diagnostics + +Fast local validation checks: + +- invalid root declarations +- unknown page, component, or layout members +- unbalanced braces, brackets, and parentheses +- unclosed strings +- invalid HTML closing tags +- missing HTML closing tags +- invalid lifecycle hooks +- duplicate lifecycle hooks +- duplicate lifecycle blocks +- undeclared watched states +- missing watcher bodies +- invalid layout usage +- missing view blocks + +### Compiler diagnostics + +The extension bundles the real WRNexus compiler and validates `.wrn` files as you type. + +Compiler errors are attached to the reported source offset whenever available. + +Disable diagnostics with: + +```json +"wrnexus.diagnostics.enable": false +``` + +## Formatting + +The built-in formatter supports: + +- page, component, and layout indentation +- functions and nested function bodies +- lifecycle blocks +- watcher blocks +- multiline function calls +- object literals +- HTML elements +- multiline HTML attributes +- conditional class directives +- window and document events +- CSS blocks +- API blocks + +Format a document using: + +```text +Shift+Alt+F +``` + +Enable format-on-save: ```json "[wrn]": { @@ -57,29 +617,178 @@ To format automatically when saving, add this to VS Code settings: } ``` +Disable the formatter: + +```json +"wrnexus.format.enable": false +``` + +Configure preferred line width: + +```json +"wrnexus.formatting.printWidth": 100 +``` + +## Autocomplete + +Autocomplete is available for: + +- pages +- components +- layouts +- state declarations +- props +- views +- SEO metadata +- functions +- lifecycle hooks +- watchers +- declared state names +- declared props +- declared functions +- watcher variables +- route parameters +- context values +- standard events +- window events +- document events +- reactive attributes +- conditional classes + +After typing: + +```wrn +watch +``` + +the extension suggests declared state names. + +Inside a watcher, it suggests: + +```text +value +previous +``` + +Declared component functions are suggested as callable snippets: + +```text +updateVisibility() +scrollToTop() +``` + +## Go to definition + +Use Ctrl+Click or **Go to Definition** for supported WRNexus symbols. + +The extension can navigate to component declarations and locally declared functions when supported by the definition provider. + +## Snippets + +Useful snippet prefixes include: + +| Prefix | Purpose | +| ------------------------ | ------------------------------------- | +| `wrn-page` | Create a page | +| `wrn-dynamic-page` | Create a dynamic route page | +| `wrn-component` | Create a component | +| `wrn-reactive-component` | Create a reactive component | +| `wrn-layout` | Create a layout | +| `wrn-state` | Declare state | +| `wrn-functions` | Create a functions block | +| `wrn-function` | Create a function | +| `wrn-async-function` | Create an async function | +| `wrn-lifecycle` | Create all lifecycle hooks | +| `wrn-mount` | Create a mount hook | +| `wrn-update` | Create an update hook | +| `wrn-unmount` | Create an unmount hook | +| `wrn-watch` | Create a watcher | +| `wrn-window-scroll` | Add a window scroll event | +| `wrn-window-resize` | Add a window resize event | +| `wrn-document-event` | Add a document event | +| `wrn-class-if` | Add a conditional class | +| `wrn-class-toggle` | Toggle two classes | +| `wrn-back-to-top` | Create a complete BackToTop component | + +## Settings + +| Setting | Default | Description | +| ------------------------------- | ------- | -------------------------------------- | +| `wrnexus.diagnostics.enable` | `true` | Enable editor and compiler diagnostics | +| `wrnexus.format.enable` | `true` | Enable the built-in formatter | +| `wrnexus.formatting.printWidth` | `100` | Preferred formatter line width | + +Recommended VS Code configuration: + +```json +{ + "[wrn]": { + "editor.defaultFormatter": "wrnexus.wrnexus", + "editor.formatOnSave": true, + "editor.tabSize": 4, + "editor.insertSpaces": true, + "editor.snippetSuggestions": "top" + }, + "wrnexus.diagnostics.enable": true, + "wrnexus.format.enable": true, + "wrnexus.formatting.printWidth": 100 +} +``` + ## Privacy -The extension does not collect telemetry and does not send source code or usage -data over the network. Formatting, completion, and diagnostics run locally. +The extension does not collect telemetry. + +It does not send source code, diagnostics, formatting data, or usage information over the network. + +Syntax highlighting, formatting, autocomplete, snippets, definition navigation, and diagnostics run locally. ## Development -Diagnostics use a bundled copy of the compiler. Build and validate everything -after changing the extension or `@wrnexus/compiler`: +Install dependencies: ```bash bun install +``` + +Rebuild the bundled compiler: + +```bash +bun run build:compiler +``` + +Validate the extension: + +```bash bun run check +``` + +Package a VSIX: + +```bash bun run package ``` -`bun run package` creates `wrnexus-.vsix`. The extension still -highlights, provides snippets, and completes without the compiler bundle; only -inline error reporting requires it. +This creates: -Open this folder in VS Code and press F5 to launch an Extension -Development Host, then open any `.wrn` file. +```text +wrnexus-.vsix +``` -See the -[Marketplace publishing guide](https://git.workroot.in/WorkRoot/WRNexusJS/src/branch/main/editors/vscode/PUBLISHING.md) -for the release procedure. +Install the local package: + +```bash +code --install-extension ./wrnexus-.vsix --force +``` + +Open the extension folder in VS Code and press F5 to launch an Extension Development Host. + +After changing the parser or compiler, always rebuild: + +```bash +bun run build:compiler +``` + +Without the compiler bundle, syntax highlighting, snippets, formatting, and autocomplete continue to work, but compiler diagnostics are unavailable. + +See the [Marketplace publishing guide](https://git.workroot.in/WorkRoot/WRNexusJS/src/branch/main/editors/vscode/PUBLISHING.md) for publishing instructions. diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 7c39087f..1b9d47fe 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -1,8 +1,8 @@ { "name": "wrnexus", "displayName": "WRNexus Language Support", - "description": "Syntax highlighting, formatting, diagnostics, snippets and completions for WrNexus .wrn files.", - "version": "0.2.6", + "description": "Complete language support for WRNexus .wrn files, including highlighting, formatting, diagnostics, snippets, lifecycle hooks, state watchers, component functions, completions, and definition navigation.", + "version": "0.2.7", "publisher": "wrnexus", "private": true, "license": "SEE LICENSE IN LICENSE", @@ -32,15 +32,22 @@ "Formatters" ], "keywords": [ - "wire", "wrnexus", "wrn", + "wire", "wirejs", "language support", "formatter", + "diagnostics", "syntax highlighting", + "autocomplete", + "snippets", + "lifecycle", + "state watcher", + "components", "full-stack", "ssr", + "csr", "bun" ], "main": "./src/extension.js", @@ -52,7 +59,8 @@ { "id": "wrn", "aliases": [ - "WrNexus", + "WRNexus", + "WRN", "wrn" ], "extensions": [ @@ -92,23 +100,82 @@ } ], "configuration": { - "title": "WrNexus", + "title": "WRNexus", "properties": { "wrnexus.diagnostics.enable": { "type": "boolean", "default": true, - "description": "Show parse errors for .wrn files as you type." + "scope": "resource", + "description": "Enable WRNexus editor and compiler diagnostics for .wrn files." }, "wrnexus.format.enable": { "type": "boolean", "default": true, - "description": "Enable the built-in formatter for .wrn files." + "scope": "resource", + "description": "Enable the built-in WRNexus formatter for .wrn files." + }, + "wrnexus.formatting.printWidth": { + "type": "number", + "default": 100, + "minimum": 60, + "maximum": 240, + "scope": "resource", + "description": "Preferred maximum line width used by the WRNexus formatter." } } }, "configurationDefaults": { + "[wrn]": { + "editor.defaultFormatter": "wrnexus.wrnexus", + "editor.formatOnSave": false, + "editor.tabSize": 4, + "editor.insertSpaces": true, + "editor.autoClosingBrackets": "always", + "editor.autoClosingQuotes": "always", + "editor.suggest.showSnippets": true, + "editor.snippetSuggestions": "top" + }, "editor.tokenColorCustomizations": { "textMateRules": [ + { + "scope": "storage.type.wrn", + "settings": { + "foreground": "#c084fc", + "fontStyle": "bold" + } + }, + { + "scope": "entity.name.type.wrn", + "settings": { + "foreground": "#67e8f9" + } + }, + { + "scope": "entity.name.function.wrn", + "settings": { + "foreground": "#a7f3d0" + } + }, + { + "scope": "entity.name.function.lifecycle.wrn", + "settings": { + "foreground": "#fbbf24", + "fontStyle": "bold" + } + }, + { + "scope": "variable.other.readwrite.wrn", + "settings": { + "foreground": "#93c5fd" + } + }, + { + "scope": "variable.language.wrn", + "settings": { + "foreground": "#f0abfc", + "fontStyle": "italic" + } + }, { "scope": "entity.other.attribute-name.wrn.directive", "settings": { @@ -123,6 +190,13 @@ "fontStyle": "bold" } }, + { + "scope": "entity.other.attribute-name.event.wrn", + "settings": { + "foreground": "#f472b6", + "fontStyle": "bold" + } + }, { "scope": "punctuation.definition.keyword.wrn", "settings": { diff --git a/editors/vscode/snippets/wrn.json b/editors/vscode/snippets/wrn.json index 12952f52..cd12f832 100644 --- a/editors/vscode/snippets/wrn.json +++ b/editors/vscode/snippets/wrn.json @@ -22,7 +22,7 @@ "WRN dynamic page": { "prefix": ["wrn-dynamic-page", "dynamic-page"], - "description": "Create a WRN page with a route parameter", + "description": "Create a WRN page using a route parameter", "body": [ "page ${1:DynamicPage} {", " layout = \"${2:default}\"", @@ -40,13 +40,15 @@ "WRN component": { "prefix": ["wrn-component", "component"], - "description": "Create a WRN component", + "description": "Create a reusable WRN component", "body": [ "component ${1:ComponentName} {", " props {", " ${2:title} = \"${3:Title}\"", " }", "", + " state ${4:ready} = false", + "", " view {", "
", "

{${2:title}}

", @@ -56,9 +58,43 @@ ] }, + "WRN reactive component": { + "prefix": ["wrn-reactive-component", "reactive-component"], + "description": "Create a component with functions, lifecycle hooks, and a watcher", + "body": [ + "component ${1:ComponentName} {", + " state ${2:active} = false", + "", + " functions {", + " function ${3:updateState}() {", + " ${2:active} = true", + " }", + " }", + "", + " lifecycle {", + " mount {", + " ${3:updateState}()", + " }", + "", + " unmount {", + " $0", + " }", + " }", + "", + " watch ${2:active} {", + " console.log(value, previous)", + " }", + "", + " view {", + "
{${2:active}}
", + " }", + "}" + ] + }, + "WRN layout": { "prefix": ["wrn-layout", "layout"], - "description": "Create a WRN layout", + "description": "Create a reusable WRN layout", "body": [ "layout ${1:LayoutName} {", " view {", @@ -76,6 +112,74 @@ "body": ["state ${1:name} = ${2:\"value\"}"] }, + "WRN functions block": { + "prefix": ["wrn-functions", "functions"], + "description": "Create a browser-side functions block", + "body": ["functions {", " function ${1:handler}(${2}) {", " $0", " }", "}"] + }, + + "WRN function": { + "prefix": ["wrn-function", "function"], + "description": "Create a function inside a functions block", + "body": ["function ${1:name}(${2}) {", " $0", "}"] + }, + + "WRN async function": { + "prefix": ["wrn-async-function", "async-function"], + "description": "Create an asynchronous component function", + "body": ["async function ${1:name}(${2}) {", " $0", "}"] + }, + + "WRN lifecycle": { + "prefix": ["wrn-lifecycle", "lifecycle"], + "description": "Create mount, update, and unmount lifecycle hooks", + "body": [ + "lifecycle {", + " mount {", + " ${1}", + " }", + "", + " update {", + " ${2}", + " }", + "", + " unmount {", + " $0", + " }", + "}" + ] + }, + + "WRN mount hook": { + "prefix": ["wrn-mount", "mount"], + "description": "Create a component mount lifecycle hook", + "body": ["mount {", " $0", "}"] + }, + + "WRN update hook": { + "prefix": ["wrn-update", "update-hook"], + "description": "Create a batched component update lifecycle hook", + "body": ["update {", " $0", "}"] + }, + + "WRN unmount hook": { + "prefix": ["wrn-unmount", "unmount"], + "description": "Create a component unmount lifecycle hook", + "body": ["unmount {", " $0", "}"] + }, + + "WRN watcher": { + "prefix": ["wrn-watch", "watch"], + "description": "Watch a declared state value", + "body": ["watch ${1:stateName} {", " console.log(value, previous)", " $0", "}"] + }, + + "WRN watcher with condition": { + "prefix": ["wrn-watch-if", "watch-if"], + "description": "Watch state and react to a specific value", + "body": ["watch ${1:stateName} {", " if (value === ${2:true}) {", " $0", " }", "}"] + }, + "WRN SEO block": { "prefix": ["wrn-seo", "seo"], "description": "Create SEO metadata", @@ -127,12 +231,36 @@ "body": ["class:${1:border-indigo-500}=\"${2:condition}\""] }, + "WRN conditional class pair": { + "prefix": ["wrn-class-toggle", "class-toggle"], + "description": "Toggle two classes from one condition", + "body": ["class:${1:opacity-100}=\"${3:visible}\"", "class:${2:opacity-0}=\"!${3:visible}\""] + }, + "WRN click event": { "prefix": ["wrn-click", "click"], - "description": "Add a click handler", + "description": "Add a click event handler", "body": ["@click=\"${1:handler()}\""] }, + "WRN window scroll event": { + "prefix": ["wrn-window-scroll", "window-scroll"], + "description": "Add a window scroll event handler", + "body": ["@window:scroll=\"${1:handler()}\""] + }, + + "WRN window resize event": { + "prefix": ["wrn-window-resize", "window-resize"], + "description": "Add a window resize event handler", + "body": ["@window:resize=\"${1:handler()}\""] + }, + + "WRN document event": { + "prefix": ["wrn-document-event", "document-event"], + "description": "Add a document-level event handler", + "body": ["@document:${1:click}=\"${2:handler()}\""] + }, + "WRN show directive": { "prefix": ["wrn-show", "show"], "description": "Conditionally show an element", @@ -158,7 +286,7 @@ "api GET ${1:/api/resource} {", " return Response.json({", " ${2:success}: true", - " });", + " })", "}" ] }, @@ -168,32 +296,19 @@ "description": "Create a POST API route", "body": [ "api POST ${1:/api/resource} {", - " const body = await ctx.request.json();", + " const body = await ctx.request.json()", "", " return Response.json({", " success: true,", " data: body", - " });", - "}" - ] - }, - - "WRN middleware": { - "prefix": ["wrn-middleware", "middleware"], - "description": "Create middleware", - "body": [ - "middleware ${1:auth} {", - " async function handle(ctx, next) {", - " ${2}", - " return await next();", - " }", + " })", "}" ] }, "WRN loading state": { "prefix": ["wrn-loading", "loading-state"], - "description": "Create loading state and button", + "description": "Create loading state and a reactive button", "body": [ "state loading = false", "", @@ -206,5 +321,64 @@ " {loading ? \"Loading...\" : \"${1:Submit}\"}", "" ] + }, + + "WRN BackToTop component": { + "prefix": ["wrn-back-to-top", "back-to-top"], + "description": "Create an optimized BackToTop component using lifecycle cleanup", + "body": [ + "component BackToTop {", + " state visible = false", + "", + " functions {", + " function updateBackToTopVisibility() {", + " visible = window.scrollY > ${1:500}", + " }", + "", + " function scrollToTop() {", + " window.scrollTo({", + " top: 0,", + " behavior: \"smooth\"", + " })", + " }", + " }", + "", + " lifecycle {", + " mount {", + " updateBackToTopVisibility()", + "", + " window.addEventListener(", + " \"scroll\",", + " updateBackToTopVisibility,", + " { passive: true }", + " )", + " }", + "", + " unmount {", + " window.removeEventListener(", + " \"scroll\",", + " updateBackToTopVisibility", + " )", + " }", + " }", + "", + " view {", + " ", + " ${2:Back to top}", + " ", + " }", + "}" + ] } } diff --git a/editors/vscode/src/compiler.cjs b/editors/vscode/src/compiler.cjs index efb59e19..ff6acee3 100644 --- a/editors/vscode/src/compiler.cjs +++ b/editors/vscode/src/compiler.cjs @@ -289,6 +289,8 @@ function parse(source) { const functions = []; const dataApis = []; const modeFunctions = []; + const lifecycle = {}; + const watches = []; const apis = []; const realtimes = []; while (lx.peek().type !== "rbrace") { @@ -413,6 +415,40 @@ function parse(source) { styles.push(lx.readBalancedBraces()); break; } + case "lifecycle": { + lx.next(); + expect("lbrace"); + while (lx.peek().type !== "rbrace") { + const hook = lx.peek(); + if (hook.type === "eof") { + throw new ParseError("Unexpected end of input inside lifecycle block"); + } + if (hook.type !== "ident") { + throw new ParseError(`Expected a lifecycle hook at offset ${hook.pos}`); + } + if (hook.value !== "mount" && hook.value !== "update" && hook.value !== "unmount") { + throw new ParseError(`Unknown lifecycle hook '${hook.value}' at offset ${hook.pos}`); + } + const hookName = hook.value; + lx.next(); + if (lifecycle[hookName] !== undefined) { + throw new ParseError(`Duplicate lifecycle hook '${hookName}' at offset ${hook.pos}`); + } + lifecycle[hookName] = lx.readBalancedBraces(); + } + expect("rbrace"); + break; + } + case "watch": { + lx.next(); + const stateName = expect("ident").value; + const body = lx.readBalancedBraces(); + watches.push({ + state: stateName, + body + }); + break; + } case "functions": { lx.next(); functions.push(lx.readBalancedBraces()); @@ -423,6 +459,12 @@ function parse(source) { } } expect("rbrace"); + const declaredStates = new Set(states.map((state) => state.name)); + for (const watcher of watches) { + if (!declaredStates.has(watcher.state)) { + throw new ParseError(`Cannot watch undeclared state '${watcher.state}'`); + } + } return { type: "page", kind, @@ -436,6 +478,8 @@ function parse(source) { functions, dataApis, modeFunctions, + lifecycle, + watches, apis, realtimes }; @@ -716,10 +760,18 @@ function renderAttr(attr) { } } function eventAttribute(name) { - if (name.startsWith("browser-")) + if (name.startsWith("window:")) { + return `data-on-window-${name.slice("window:".length)}`; + } + if (name.startsWith("document:")) { + return `data-on-document-${name.slice("document:".length)}`; + } + if (name.startsWith("browser-")) { return `data-on-wrnexus-browser-${name.slice(8)}`; - if (name.startsWith("mobile-")) + } + if (name.startsWith("mobile-")) { return `data-on-wrnexus-mobile-${name.slice(7)}`; + } return `data-on-${name}`; } function reactiveAttrValue(raw, reactive) { @@ -924,7 +976,7 @@ function renderNestedComponentInvocation(node, ctx) { let bindIndex = 0; const attrs = node.attrs.filter((attr) => attr.name !== "data-component").map((attr) => { if (attr.event) { - return ` ${eventAttribute(attr.name)}="${compileAttrValue(attr.value, ctx)}"`; + return escLit(` ${eventAttribute(attr.name)}="`) + escLit(attrEscape(attr.value)) + escLit(`"`); } if (attr.boolean) { return ` ${attr.name}`; @@ -1262,6 +1314,34 @@ function safeRef(name) { function escLit(s) { return s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${"); } +function componentBehavior(ast) { + const functions = ast.functions.map((body) => body.trim()).filter(Boolean).join(` + +`); + const lifecycle = { + ...ast.lifecycle.mount?.trim() ? { mount: ast.lifecycle.mount.trim() } : {}, + ...ast.lifecycle.update?.trim() ? { update: ast.lifecycle.update.trim() } : {}, + ...ast.lifecycle.unmount?.trim() ? { unmount: ast.lifecycle.unmount.trim() } : {} + }; + const watches = ast.watches.map((watch) => ({ + state: watch.state, + body: watch.body.trim() + })); + if (!functions && Object.keys(lifecycle).length === 0 && watches.length === 0) { + return null; + } + return { + functions, + lifecycle, + watches + }; +} +function behaviorAttribute(behavior) { + if (!behavior) { + return ""; + } + return ` data-wrn-behavior="${attrEscape(JSON.stringify(behavior))}"`; +} var INTERP_RE = /\{([^{}]+)\}/g; function exprRefsState(expr, stateNames) { for (const name of stateNames) { @@ -1340,7 +1420,7 @@ function renderComponentNode(node, ctx) { } const attrs = node.attrs.filter((a) => a.name !== "class" && !a.name.startsWith("class:")).map((a) => { if (a.event) { - return ` ${eventAttribute(a.name)}="${compileAttrValue(a.value, ctx)}"`; + return ` ${eventAttribute(a.name)}="${escLit(attrEscape(a.value))}"`; } if (a.boolean) { return ` ${a.name}`; @@ -1400,11 +1480,13 @@ function generateComponent(ast) { ${styles.map(styleEscape).join(` `)} `) : ""; - const needsScope = ast.states.length > 0 || viewHasEvents(ast.view); + const behavior = componentBehavior(ast); + const needsScope = ast.states.length > 0 || viewHasEvents(ast.view) || behavior !== null; const scopeKeys = [ ...effectiveProps.map((prop) => prop.name), ...ast.states.map((state) => state.name) ]; + const behaviorAttr = behaviorAttribute(behavior); const decls = []; for (const prop of effectiveProps) { decls.push(` const ${nameRefs.get(prop.name)} = __coerce(__p[${JSON.stringify(prop.name)}], (${resolveExpr(prop.default)}));`); @@ -1412,7 +1494,7 @@ ${styles.map(styleEscape).join(` for (const state of ast.states) { decls.push(` const ${nameRefs.get(state.name)} = (${resolveExpr(state.expr)});`); } - const returnExpr = needsScope ? "`" + styleTag + '
' + viewCode + "
`" : "`" + styleTag + viewCode + "`"; + const returnExpr = needsScope ? "`" + styleTag + `
` + viewCode + "
`" : "`" + styleTag + viewCode + "`"; const scopeLine = needsScope && scopeKeys.length > 0 ? ` const __scope = __wrnexusScopeDecl({ ${scopeKeys.map((k) => `${JSON.stringify(k)}: ${nameRefs.get(k)}`).join(", ")} }); ` : needsScope ? ` const __scope = ""; ` : ""; @@ -1421,6 +1503,9 @@ ${styles.map(styleEscape).join(` } else { out.push(`export const __wrnexusComponent = ${JSON.stringify(ast.name)};`); } + if (behavior) { + out.push(`export const __wrnexusBehavior = ${JSON.stringify(behavior, null, 2)};`); + } out.push(`function __coerce(v: any, def: any): any { if (v === undefined || v === null) return def; if (typeof def === "number") return Number(v); diff --git a/editors/vscode/src/completion.js b/editors/vscode/src/completion.js index 8020fb00..8f87760d 100644 --- a/editors/vscode/src/completion.js +++ b/editors/vscode/src/completion.js @@ -20,15 +20,26 @@ const BLOCK_COMPLETIONS = [ { label: "component", detail: "WRN component", - documentation: "Create a reusable WRN component.", + documentation: + "Create a reusable WRN component with state, functions, lifecycle hooks, watchers, and a view.", snippet: [ "component ${1:ComponentName} {", - " props {", - ' ${2:title} = "${3:Title}"', + " state ${2:ready} = false", + "", + " functions {", + " function ${3:initialize}() {", + " $0", + " }", + " }", + "", + " lifecycle {", + " mount {", + " ${3:initialize}()", + " }", " }", "", " view {", - " $0", + "
", " }", "}", ].join("\n"), @@ -47,9 +58,16 @@ const BLOCK_COMPLETIONS = [ "}", ].join("\n"), }, + { + label: "props", + detail: "Component or layout props block", + documentation: "Declare values accepted by a component or layout.", + snippet: ["props {", ' ${1:title} = "${2:Title}"', "}"].join("\n"), + }, { label: "seo", detail: "SEO metadata block", + documentation: "Declare title, description, and other page metadata.", snippet: [ "seo {", ' title = "${1:Page title}"', @@ -60,13 +78,84 @@ const BLOCK_COMPLETIONS = [ { label: "view", detail: "WRN view block", + documentation: "Declare the HTML view rendered by the page, component, or layout.", snippet: ["view {", " $0", "}"].join("\n"), }, { label: "state", detail: "Reactive state declaration", + documentation: "Declare reactive state owned by the current component, layout, or page.", snippet: 'state ${1:name} = ${2:"value"}', }, + { + label: "functions", + detail: "Browser component functions block", + documentation: + "Declare functions that can be called from events, lifecycle hooks, and watchers.", + snippet: ["functions {", " function ${1:handler}(${2}) {", " $0", " }", "}"].join( + "\n", + ), + }, + { + label: "function", + detail: "WRN component function", + documentation: "Declare a browser-side function inside a functions block.", + snippet: ["function ${1:name}(${2}) {", " $0", "}"].join("\n"), + }, + { + label: "lifecycle", + detail: "Component lifecycle block", + documentation: "Declare mount, update, and unmount hooks for a component or layout.", + snippet: [ + "lifecycle {", + " mount {", + " $1", + " }", + "", + " update {", + " $2", + " }", + "", + " unmount {", + " $0", + " }", + "}", + ].join("\n"), + }, + { + label: "mount", + detail: "Lifecycle mount hook", + documentation: "Runs once after the component is connected and hydrated.", + snippet: ["mount {", " $0", "}"].join("\n"), + }, + { + label: "update", + detail: "Lifecycle update hook", + documentation: "Runs once after a batch of reactive state changes.", + snippet: ["update {", " $0", "}"].join("\n"), + }, + { + label: "unmount", + detail: "Lifecycle unmount hook", + documentation: + "Runs before the component is removed. Use it to remove global listeners and release resources.", + snippet: ["unmount {", " $0", "}"].join("\n"), + }, + { + label: "watch", + detail: "Reactive state watcher", + documentation: + "Run code when one declared state value changes. The watcher receives `value` and `previous`.", + snippet: ["watch ${1:stateName} {", " console.log(value, previous)", " $0", "}"].join( + "\n", + ), + }, + { + label: "style", + detail: "Scoped style block", + documentation: "Declare styles for the current WRN declaration.", + snippet: ["style {", " $0", "}"].join("\n"), + }, ]; const ATTRIBUTE_COMPLETIONS = [ @@ -76,6 +165,19 @@ const ATTRIBUTE_COMPLETIONS = [ ["@submit", "Submit event handler", '@submit="${1:handler()}"'], ["@focus", "Focus event handler", '@focus="${1:handler()}"'], ["@blur", "Blur event handler", '@blur="${1:handler()}"'], + ["@keydown", "Keyboard key-down event handler", '@keydown="${1:handler()}"'], + ["@keyup", "Keyboard key-up event handler", '@keyup="${1:handler()}"'], + ["@mouseenter", "Pointer enter event handler", '@mouseenter="${1:handler()}"'], + ["@mouseleave", "Pointer leave event handler", '@mouseleave="${1:handler()}"'], + ["@window:scroll", "Window scroll event handler", '@window:scroll="${1:handler()}"'], + ["@window:resize", "Window resize event handler", '@window:resize="${1:handler()}"'], + ["@window:keydown", "Window key-down event handler", '@window:keydown="${1:handler()}"'], + ["@document:click", "Document click event handler", '@document:click="${1:handler()}"'], + [ + "@document:visibilitychange", + "Document visibility-change event handler", + '@document:visibilitychange="${1:handler()}"', + ], ["data-show", "Conditional visibility", 'data-show="${1:condition}"'], ["data-for", "Reactive loop", 'data-for="${1:item} in ${2:items}"'], ["class:", "Conditional CSS class", 'class:${1:border-indigo-500}="${2:condition}"'], @@ -90,6 +192,11 @@ const CONTEXT_COMPLETIONS = [ ["ctx.locals", "Request-local data"], ]; +const WATCH_VALUE_COMPLETIONS = [ + ["value", "Current watcher state value"], + ["previous", "Previous watcher state value"], +]; + function completionKindFor(label) { if (label.startsWith("@")) { return vscode.CompletionItemKind.Event; @@ -99,15 +206,47 @@ function completionKindFor(label) { return vscode.CompletionItemKind.Property; } + if (label === "function" || label === "functions") { + return vscode.CompletionItemKind.Function; + } + return vscode.CompletionItemKind.Keyword; } -function createCompletion(label, detail, snippet) { +function createCompletion(label, detail, snippet, documentation) { const item = new vscode.CompletionItem(label, completionKindFor(label)); item.detail = detail; item.insertText = new vscode.SnippetString(snippet || label); - item.documentation = new vscode.MarkdownString(detail); + + item.documentation = new vscode.MarkdownString(documentation || detail); + + return item; +} + +function createVariableCompletion(label, detail, insertion = label) { + const item = new vscode.CompletionItem(label, vscode.CompletionItemKind.Variable); + + item.detail = detail; + item.insertText = insertion; + + return item; +} + +function createFunctionCompletion(name, parameters = []) { + const item = new vscode.CompletionItem(name, vscode.CompletionItemKind.Function); + + item.detail = `WRN component function${ + parameters.length > 0 ? ` (${parameters.join(", ")})` : "" + }`; + + item.documentation = new vscode.MarkdownString( + `Call the browser function \`${name}\` declared in the current \`functions { ... }\` block.`, + ); + + const placeholders = parameters.map((parameter, index) => `\${${index + 1}:${parameter}}`); + + item.insertText = new vscode.SnippetString(`${name}(${placeholders.join(", ")})`); return item; } @@ -118,6 +257,7 @@ function getCurrentOpeningTag(document, position) { ); const lastOpen = textBeforeCursor.lastIndexOf("<"); + const lastClose = textBeforeCursor.lastIndexOf(">"); if (lastOpen > lastClose) { @@ -129,6 +269,7 @@ function getCurrentOpeningTag(document, position) { function extractRouteParams(document) { const fileName = document.fileName.replace(/\\/g, "/"); + const matches = [...fileName.matchAll(/\[([A-Za-z_$][\w$]*)\]/g)]; return matches.map((match) => match[1]); @@ -136,13 +277,195 @@ function extractRouteParams(document) { function extractStates(document) { const source = document.getText(); + const matches = [...source.matchAll(/^\s*state\s+([A-Za-z_$][\w$]*)\s*=/gm)]; - return matches.map((match) => match[1]); + return [...new Set(matches.map((match) => match[1]))]; +} + +function extractProps(document) { + const source = document.getText(); + const propsBlockPattern = /\bprops\s*\{([\s\S]*?)\}/g; + + const props = new Set(); + let blockMatch; + + while ((blockMatch = propsBlockPattern.exec(source)) !== null) { + const body = blockMatch[1]; + + for (const match of body.matchAll(/^\s*([A-Za-z_$][\w$]*)\s*=/gm)) { + props.add(match[1]); + } + } + + return [...props]; +} + +function extractFunctions(document) { + const source = document.getText(); + const functions = []; + const seen = new Set(); + + const pattern = /(?:^|\s)(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)/g; + + let match; + + while ((match = pattern.exec(source)) !== null) { + const name = match[1]; + + if (seen.has(name)) { + continue; + } + + seen.add(name); + + const parameters = match[2] + .split(",") + .map((parameter) => parameter.trim()) + .filter(Boolean) + .map((parameter) => parameter.replace(/=.*$/, "").trim()); + + functions.push({ + name, + parameters, + }); + } + + return functions; +} + +function sourceBeforePosition(document, position) { + return document.getText(new vscode.Range(new vscode.Position(0, 0), position)); +} + +function isInsideNamedBlock(document, position, blockName) { + const source = sourceBeforePosition(document, position); + + const tokenPattern = new RegExp(`\\b${blockName}\\s*\\{|\\{|\\}`, "g"); + + const stack = []; + let match; + + while ((match = tokenPattern.exec(source)) !== null) { + const token = match[0]; + + if (token.startsWith(blockName)) { + stack.push(blockName); + continue; + } + + if (token === "{") { + stack.push(null); + continue; + } + + if (token === "}") { + stack.pop(); + } + } + + return stack.includes(blockName); +} + +function isInsideLifecycle(document, position) { + return isInsideNamedBlock(document, position, "lifecycle"); +} + +function isInsideFunctions(document, position) { + return isInsideNamedBlock(document, position, "functions"); +} + +function isInsideWatch(document, position) { + const source = sourceBeforePosition(document, position); + + const watchMatches = [...source.matchAll(/\bwatch\s+[A-Za-z_$][\w$]*\s*\{/g)]; + + if (watchMatches.length === 0) { + return false; + } + + const latest = watchMatches[watchMatches.length - 1]; + + const tail = source.slice(latest.index); + let depth = 0; + + for (const character of tail) { + if (character === "{") { + depth += 1; + } else if (character === "}") { + depth -= 1; + } + } + + return depth > 0; +} + +function isAfterWatchKeyword(document, position) { + const linePrefix = document.lineAt(position.line).text.slice(0, position.character); + + return /^\s*watch\s+[A-Za-z0-9_$]*$/.test(linePrefix); +} + +function addBlockCompletions(items, document, position) { + const insideLifecycle = isInsideLifecycle(document, position); + + const insideFunctions = isInsideFunctions(document, position); + + for (const completion of BLOCK_COMPLETIONS) { + if (insideLifecycle && !["mount", "update", "unmount"].includes(completion.label)) { + continue; + } + + if (insideFunctions && completion.label !== "function") { + continue; + } + + if (!insideLifecycle && ["mount", "update", "unmount"].includes(completion.label)) { + continue; + } + + if (!insideFunctions && completion.label === "function") { + continue; + } + + items.push( + createCompletion( + completion.label, + completion.detail, + completion.snippet, + completion.documentation, + ), + ); + } +} + +function addStateCompletions(items, document, position) { + const states = extractStates(document); + const afterWatch = isAfterWatchKeyword(document, position); + + for (const state of states) { + const item = createVariableCompletion( + state, + afterWatch ? "Declared WRN state available for watching" : "WRN reactive state", + ); + + if (afterWatch) { + item.sortText = `0-${state}`; + } + + items.push(item); + } +} + +function addFunctionCompletions(items, document) { + for (const fn of extractFunctions(document)) { + items.push(createFunctionCompletion(fn.name, fn.parameters)); + } } function provideCompletionItems(document, position) { const items = []; + const linePrefix = document.lineAt(position.line).text.slice(0, position.character); const openingTag = getCurrentOpeningTag(document, position); @@ -151,10 +474,10 @@ function provideCompletionItems(document, position) { for (const [label, detail, snippet] of ATTRIBUTE_COMPLETIONS) { items.push(createCompletion(label, detail, snippet)); } + } else if (isAfterWatchKeyword(document, position)) { + addStateCompletions(items, document, position); } else { - for (const completion of BLOCK_COMPLETIONS) { - items.push(createCompletion(completion.label, completion.detail, completion.snippet)); - } + addBlockCompletions(items, document, position); } if (linePrefix.includes("ctx.") || linePrefix.includes("ctx.params.")) { @@ -164,31 +487,27 @@ function provideCompletionItems(document, position) { } for (const param of extractRouteParams(document)) { - const item = new vscode.CompletionItem(param, vscode.CompletionItemKind.Variable); + items.push(createVariableCompletion(param, `Route parameter from [${param}].wrn`)); - item.detail = `Route parameter from [${param}].wrn`; - item.insertText = param; - - items.push(item); - - const fullItem = new vscode.CompletionItem( - `ctx.params.${param}`, - vscode.CompletionItemKind.Variable, + items.push( + createVariableCompletion(`ctx.params.${param}`, `Route parameter from [${param}].wrn`), ); - - fullItem.detail = `Route parameter from [${param}].wrn`; - fullItem.insertText = `ctx.params.${param}`; - - items.push(fullItem); } - for (const state of extractStates(document)) { - const item = new vscode.CompletionItem(state, vscode.CompletionItemKind.Variable); + if (!isAfterWatchKeyword(document, position)) { + addStateCompletions(items, document, position); + } - item.detail = "WRN state"; - item.insertText = state; + for (const prop of extractProps(document)) { + items.push(createVariableCompletion(prop, "WRN component or layout prop")); + } - items.push(item); + addFunctionCompletions(items, document); + + if (isInsideWatch(document, position)) { + for (const [label, detail] of WATCH_VALUE_COMPLETIONS) { + items.push(createVariableCompletion(label, detail)); + } } return items; @@ -196,7 +515,10 @@ function provideCompletionItems(document, position) { function registerCompletionProvider(context) { const provider = vscode.languages.registerCompletionItemProvider( - { language: "wrn", scheme: "file" }, + { + language: "wrn", + scheme: "file", + }, { provideCompletionItems, }, @@ -205,13 +527,17 @@ function registerCompletionProvider(context) { ".", "<", " ", + "(", ); context.subscriptions.push(provider); } module.exports = { + extractFunctions, + extractProps, extractRouteParams, extractStates, + provideCompletionItems, registerCompletionProvider, }; diff --git a/editors/vscode/src/diagnostics.js b/editors/vscode/src/diagnostics.js index ee27b798..02e054dd 100644 --- a/editors/vscode/src/diagnostics.js +++ b/editors/vscode/src/diagnostics.js @@ -9,6 +9,8 @@ const TOP_LEVEL_PATTERN = /^\s*(page|component|layout)\s+([A-Za-z_$][\w$]*)\s*\{ const VALID_TOP_LEVEL_KINDS = new Set(["page", "component", "layout"]); +const VALID_LIFECYCLE_HOOKS = new Set(["mount", "update", "unmount"]); + const VALID_MEMBERS = { page: new Set([ "layout", @@ -23,9 +25,9 @@ const VALID_MEMBERS = { "realtime", ]), - component: new Set(["props", "state", "view", "style", "functions"]), + component: new Set(["props", "state", "view", "style", "functions", "lifecycle", "watch"]), - layout: new Set(["props", "state", "view", "style", "functions"]), + layout: new Set(["props", "state", "view", "style", "functions", "lifecycle", "watch"]), }; function createDiagnostic( @@ -59,7 +61,6 @@ function lineDiagnostic( code, ) { const line = document.lineAt(lineNumber); - const diagnostic = new vscode.Diagnostic(line.range, message, severity); diagnostic.source = "WRNexus"; @@ -316,6 +317,15 @@ function validateHtmlTags(document, source) { function getRootBodyRange(source, rootMatch) { const openingBrace = rootMatch.index + rootMatch[0].lastIndexOf("{"); + const closingBrace = findMatchingBrace(source, openingBrace); + + return { + start: openingBrace + 1, + end: closingBrace === -1 ? source.length : closingBrace, + }; +} + +function findMatchingBrace(source, openingBrace) { let depth = 0; let quote = null; let escaped = false; @@ -346,6 +356,17 @@ function getRootBodyRange(source, rootMatch) { continue; } + if (source.startsWith("", index + 4); + + if (commentEnd === -1) { + return -1; + } + + index = commentEnd + 2; + continue; + } + if (character === "{") { depth += 1; continue; @@ -355,22 +376,44 @@ function getRootBodyRange(source, rootMatch) { depth -= 1; if (depth === 0) { - return { - start: openingBrace + 1, - end: index, - }; + return index; } } } + return -1; +} + +function skipWhitespace(source, index, end) { + while (index < end && /\s/.test(source[index])) { + index += 1; + } + + return index; +} + +function readIdentifier(source, index, end) { + if (index >= end || !/[A-Za-z_$]/.test(source[index])) { + return null; + } + + const start = index; + index += 1; + + while (index < end && /[A-Za-z0-9_$-]/.test(source[index])) { + index += 1; + } + return { - start: openingBrace + 1, - end: source.length, + name: source.slice(start, index), + start, + end: index, }; } function findRootMembers(source, bodyStart, bodyEnd) { const members = []; + let index = bodyStart; let depth = 0; let quote = null; @@ -414,7 +457,9 @@ function findRootMembers(source, bodyStart, bodyEnd) { if (source.startsWith("", index + 4); + index = end === -1 ? bodyEnd : end + 3; + continue; } @@ -430,25 +475,35 @@ function findRootMembers(source, bodyStart, bodyEnd) { continue; } - if (depth === 0 && /[A-Za-z_]/.test(character)) { - const start = index; - index += 1; + if (depth === 0 && /[A-Za-z_$]/.test(character)) { + const identifier = readIdentifier(source, index, bodyEnd); - while (index < bodyEnd && /[A-Za-z0-9_-]/.test(source[index])) { + if (!identifier) { index += 1; + continue; } - const name = source.slice(start, index); + index = identifier.end; members.push({ - name, - start, - end: index, + name: identifier.name, + start: identifier.start, + end: identifier.end, }); - // Single-line declarations must skip the rest of the line. - if (name === "state" || name === "layout") { + if (identifier.name === "state" || identifier.name === "layout") { skipLine(); + continue; + } + + if (identifier.name === "watch") { + index = skipWhitespace(source, index, bodyEnd); + + const watchedState = readIdentifier(source, index, bodyEnd); + + if (watchedState) { + index = watchedState.end; + } } continue; @@ -460,6 +515,234 @@ function findRootMembers(source, bodyStart, bodyEnd) { return members; } +function findNamedBlocks(source, bodyStart, bodyEnd, blockName) { + const blocks = []; + let index = bodyStart; + + while (index < bodyEnd) { + index = skipWhitespace(source, index, bodyEnd); + + const identifier = readIdentifier(source, index, bodyEnd); + + if (!identifier) { + index += 1; + continue; + } + + index = identifier.end; + + if (identifier.name !== blockName) { + const possibleBrace = skipWhitespace(source, index, bodyEnd); + + if (source[possibleBrace] === "{") { + const end = findMatchingBrace(source, possibleBrace); + + index = end === -1 ? bodyEnd : end + 1; + } + + continue; + } + + const openingBrace = skipWhitespace(source, index, bodyEnd); + + if (source[openingBrace] !== "{") { + blocks.push({ + name: blockName, + nameStart: identifier.start, + nameEnd: identifier.end, + openingBrace: -1, + closingBrace: -1, + }); + + continue; + } + + const closingBrace = findMatchingBrace(source, openingBrace); + + blocks.push({ + name: blockName, + nameStart: identifier.start, + nameEnd: identifier.end, + openingBrace, + closingBrace, + }); + + index = closingBrace === -1 ? bodyEnd : closingBrace + 1; + } + + return blocks; +} + +function findStateDeclarations(source, bodyStart, bodyEnd) { + const states = new Map(); + let index = bodyStart; + let depth = 0; + let quote = null; + let escaped = false; + + while (index < bodyEnd) { + 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 === "'") { + quote = character; + index += 1; + continue; + } + + if (source.startsWith("", index + 4); + + index = end === -1 ? bodyEnd : end + 3; + + continue; + } + + if (character === "{") { + depth += 1; + index += 1; + continue; + } + + if (character === "}") { + depth = Math.max(0, depth - 1); + index += 1; + continue; + } + + if ( + depth === 0 && + source.startsWith("state", index) && + !/[A-Za-z0-9_$]/.test(source[index - 1] || "") && + !/[A-Za-z0-9_$]/.test(source[index + 5] || "") + ) { + let cursor = skipWhitespace(source, index + 5, bodyEnd); + + const state = readIdentifier(source, cursor, bodyEnd); + + if (state) { + states.set(state.name, state); + index = state.end; + continue; + } + } + + index += 1; + } + + return states; +} + +function findWatchDeclarations(source, bodyStart, bodyEnd) { + const watches = []; + let index = bodyStart; + let depth = 0; + let quote = null; + let escaped = false; + + while (index < bodyEnd) { + 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 === "'") { + quote = character; + index += 1; + continue; + } + + if (source.startsWith("", index + 4); + + index = end === -1 ? bodyEnd : end + 3; + + continue; + } + + if (character === "{") { + depth += 1; + index += 1; + continue; + } + + if (character === "}") { + depth = Math.max(0, depth - 1); + index += 1; + continue; + } + + if ( + depth === 0 && + source.startsWith("watch", index) && + !/[A-Za-z0-9_$]/.test(source[index - 1] || "") && + !/[A-Za-z0-9_$]/.test(source[index + 5] || "") + ) { + const watchStart = index; + let cursor = skipWhitespace(source, index + 5, bodyEnd); + + const watchedState = readIdentifier(source, cursor, bodyEnd); + + if (!watchedState) { + watches.push({ + watchStart, + watchEnd: index + 5, + state: null, + openingBrace: -1, + closingBrace: -1, + }); + + index += 5; + continue; + } + + cursor = skipWhitespace(source, watchedState.end, bodyEnd); + + const openingBrace = source[cursor] === "{" ? cursor : -1; + + const closingBrace = openingBrace === -1 ? -1 : findMatchingBrace(source, openingBrace); + + watches.push({ + watchStart, + watchEnd: index + 5, + state: watchedState, + openingBrace, + closingBrace, + }); + + index = closingBrace === -1 ? watchedState.end : closingBrace + 1; + + continue; + } + + index += 1; + } + + return watches; +} + function validateRootMembers(document, source, rootKind, rootMatch) { const diagnostics = []; const allowed = VALID_MEMBERS[rootKind]; @@ -469,6 +752,7 @@ function validateRootMembers(document, source, rootKind, rootMatch) { } const bodyRange = getRootBodyRange(source, rootMatch); + const members = findRootMembers(source, bodyRange.start, bodyRange.end); for (const member of members) { @@ -491,8 +775,182 @@ function validateRootMembers(document, source, rootKind, rootMatch) { return diagnostics; } +function validateLifecycleBlocks(document, source, rootKind, rootMatch) { + if (rootKind !== "component" && rootKind !== "layout") { + return []; + } + + const diagnostics = []; + const bodyRange = getRootBodyRange(source, rootMatch); + + const blocks = findNamedBlocks(source, bodyRange.start, bodyRange.end, "lifecycle"); + + if (blocks.length > 1) { + for (const block of blocks.slice(1)) { + diagnostics.push( + createDiagnostic( + document, + block.nameStart, + block.nameEnd, + "Only one `lifecycle { ... }` block is allowed.", + vscode.DiagnosticSeverity.Error, + "wrn-duplicate-lifecycle", + ), + ); + } + } + + for (const block of blocks) { + if (block.openingBrace === -1) { + diagnostics.push( + createDiagnostic( + document, + block.nameStart, + block.nameEnd, + "`lifecycle` must be followed by a block.", + vscode.DiagnosticSeverity.Error, + "wrn-invalid-lifecycle", + ), + ); + + continue; + } + + const lifecycleEnd = block.closingBrace === -1 ? bodyRange.end : block.closingBrace; + + let index = block.openingBrace + 1; + const seenHooks = new Set(); + + while (index < lifecycleEnd) { + index = skipWhitespace(source, index, lifecycleEnd); + + if (index >= lifecycleEnd) { + break; + } + + const hook = readIdentifier(source, index, lifecycleEnd); + + if (!hook) { + index += 1; + continue; + } + + index = hook.end; + + if (!VALID_LIFECYCLE_HOOKS.has(hook.name)) { + diagnostics.push( + createDiagnostic( + document, + hook.start, + hook.end, + `Unknown lifecycle hook \`${hook.name}\`. Use \`mount\`, \`update\`, or \`unmount\`.`, + vscode.DiagnosticSeverity.Error, + "wrn-invalid-lifecycle-hook", + ), + ); + } else if (seenHooks.has(hook.name)) { + diagnostics.push( + createDiagnostic( + document, + hook.start, + hook.end, + `Duplicate lifecycle hook \`${hook.name}\`.`, + vscode.DiagnosticSeverity.Error, + "wrn-duplicate-lifecycle-hook", + ), + ); + } else { + seenHooks.add(hook.name); + } + + const openingBrace = skipWhitespace(source, index, lifecycleEnd); + + if (source[openingBrace] !== "{") { + diagnostics.push( + createDiagnostic( + document, + hook.start, + hook.end, + `Lifecycle hook \`${hook.name}\` must be followed by a block.`, + vscode.DiagnosticSeverity.Error, + "wrn-invalid-lifecycle-hook", + ), + ); + + index = hook.end; + continue; + } + + const closingBrace = findMatchingBrace(source, openingBrace); + + index = closingBrace === -1 ? lifecycleEnd : closingBrace + 1; + } + } + + return diagnostics; +} + +function validateWatchBlocks(document, source, rootKind, rootMatch) { + if (rootKind !== "component" && rootKind !== "layout") { + return []; + } + + const diagnostics = []; + const bodyRange = getRootBodyRange(source, rootMatch); + + const states = findStateDeclarations(source, bodyRange.start, bodyRange.end); + + const watches = findWatchDeclarations(source, bodyRange.start, bodyRange.end); + + for (const watch of watches) { + if (!watch.state) { + diagnostics.push( + createDiagnostic( + document, + watch.watchStart, + watch.watchEnd, + "`watch` must be followed by a declared state name.", + vscode.DiagnosticSeverity.Error, + "wrn-invalid-watch", + ), + ); + + continue; + } + + if (!states.has(watch.state.name)) { + diagnostics.push( + createDiagnostic( + document, + watch.state.start, + watch.state.end, + `Cannot watch undeclared state \`${watch.state.name}\`.`, + vscode.DiagnosticSeverity.Error, + "wrn-unknown-watch-state", + ), + ); + } + + if (watch.openingBrace === -1) { + diagnostics.push( + createDiagnostic( + document, + watch.state.start, + watch.state.end, + `Watcher for \`${watch.state.name}\` must be followed by a block.`, + vscode.DiagnosticSeverity.Error, + "wrn-invalid-watch", + ), + ); + } + } + + return diagnostics; +} + function validateRequiredView(document, source, rootKind, rootMatch) { const bodyRange = getRootBodyRange(source, rootMatch); + const body = source.slice(bodyRange.start, bodyRange.end); if (/\bview\s*\{/.test(body)) { @@ -550,6 +1008,7 @@ function validateDocument(document) { if (declaration.diagnostic) { diagnostics.push(declaration.diagnostic); + diagnostics.push(...validateBalancedCharacters(document, source)); return diagnostics; @@ -576,6 +1035,12 @@ function validateDocument(document) { diagnostics.push(...validateRootMembers(document, source, declaration.kind, declaration.match)); + diagnostics.push( + ...validateLifecycleBlocks(document, source, declaration.kind, declaration.match), + ); + + diagnostics.push(...validateWatchBlocks(document, source, declaration.kind, declaration.match)); + diagnostics.push(...validateRequiredView(document, source, declaration.kind, declaration.match)); diagnostics.push(...validateLayoutUsage(document, source, declaration.kind)); @@ -593,19 +1058,20 @@ function registerDiagnostics(context) { return; } - const previousTimer = timers.get(document.uri.toString()); + const key = document.uri.toString(); + const previousTimer = timers.get(key); if (previousTimer) { clearTimeout(previousTimer); } const timer = setTimeout(() => { - timers.delete(document.uri.toString()); + timers.delete(key); collection.set(document.uri, validateDocument(document)); }, 150); - timers.set(document.uri.toString(), timer); + timers.set(key, timer); }; for (const document of vscode.workspace.textDocuments) { @@ -652,5 +1118,7 @@ module.exports = { validateBalancedCharacters, validateDocument, validateHtmlTags, + validateLifecycleBlocks, validateRootMembers, + validateWatchBlocks, }; diff --git a/editors/vscode/src/extension.js b/editors/vscode/src/extension.js index 435f4d1c..10442c1d 100644 --- a/editors/vscode/src/extension.js +++ b/editors/vscode/src/extension.js @@ -2,257 +2,283 @@ "use strict"; const vscode = require("vscode"); + const { formatWrn } = require("./formatter"); + const { registerCompletionProvider } = require("./completion"); + const { registerDefinitionProvider } = require("./definition"); + const { registerDiagnostics } = require("./diagnostics"); -// The .wrn compiler, bundled to CJS by `bun run build:compiler`. Loaded -// defensively so the rest of the extension (highlighting, snippets, completion) -// still works even if the bundle is missing. +/** + * The WRN compiler is bundled to CommonJS using: + * + * bun run build:compiler + * + * Loading is optional so highlighting, formatting, snippets, + * autocomplete and navigation continue working when the compiler + * bundle is temporarily unavailable. + */ let compiler = null; + try { compiler = require("./compiler.cjs"); -} catch (err) { - console.warn("[wrnexus] compiler bundle not found; diagnostics disabled.", err && err.message); +} catch (error) { + console.warn( + "[wrnexus] compiler bundle not found; compiler diagnostics disabled.", + error instanceof Error ? error.message : String(error), + ); } -/** Top-level block keywords offered at file scope. */ -const BLOCK_KEYWORDS = [ - ["page", "page ${1:Name} {\n\t$0\n}", "A route page"], - ["component", "component ${1:Name} {\n\t$0\n}", "A reusable, prop-driven component"], - ["layout", 'layout = "${1:public}"', "Select a layout for this page"], - ["state", "state ${1:count} = ${2:0}", "Reactive state seeded into the scope"], - ["props", 'props {\n\t${1:label} = ${2:"Label"}\n}', "Component props with typed defaults"], - ["view", "view {\n\t$0\n}", "Server-rendered HTML"], - ["seo", 'seo {\n\ttitle = "$1"\n\tdescription = "$2"\n}', "Per-page SEO metadata"], - ["api", "api ${1|GET,POST,PUT,PATCH,DELETE|} ${2:/path} {\n\t$0\n}", "Colocated API route"], - ["ssr", "ssr {\n\tapi ${1:load} GET ${2:/api/data} {\n\t\t$0\n\t}\n}", "Server-side data block"], - [ - "client", - "client {\n\tapi ${1:load} GET ${2:/api/data} {\n\t\t$0\n\t}\n}", - "Client-side data block", - ], - [ - "realtime", - "realtime ${1:chat} {\n\ton ${2:message}(${3:data}) {\n\t\t$0\n\t}\n}", - "Realtime channel", - ], - ["functions", "functions {\n\t$0\n}", "Shared helper functions"], - ["style", "style {\n\t$0\n}", "Scoped CSS"], -]; - -/** `data-*` attributes understood by the reactive runtime. */ -const DATA_ATTRS = [ - ["data-component", 'data-component="$1"', "Mount a component by name"], - ["data-for", 'data-for="${1:item} in ${2:items}"', "Repeat this element per item"], - ["data-show", 'data-show="${1:condition}"', "Toggle visibility reactively"], - ["data-text", 'data-text="${1:expr}"', "Bind text content to an expression"], - ["data-scope", 'data-scope="${1:key}: ${2:value}"', "Declare a local reactive scope"], - ["data-slot", 'data-slot="${1:name}"', "Fill a named on a component"], -]; - -/** Client event bindings. */ -const EVENTS = [ - "click", - "input", - "change", - "submit", - "keydown", - "keyup", - "focus", - "blur", - "mouseenter", - "mouseleave", -]; +const WRN_LANGUAGE_ID = "wrn"; +const COMPILER_DIAGNOSTIC_COLLECTION = "wrnexus-compiler"; /** + * Register the WRN document formatter. + * * @param {vscode.ExtensionContext} context */ -function activate(context) { - registerDiagnostics(context); - registerCompletionProvider(context); - registerDefinitionProvider(context); - - const diagnostics = vscode.languages.createDiagnosticCollection("wrn"); - context.subscriptions.push(diagnostics); - - const timers = new Map(); - const runDiagnostics = (doc) => { - if (doc.languageId !== "wrn") return; - if (!vscode.workspace.getConfiguration("wrnexus").get("diagnostics.enable", true)) { - diagnostics.delete(doc.uri); - return; - } - if (!compiler || typeof compiler.compileWireFile !== "function") return; - - const text = doc.getText(); - /** @type {vscode.Diagnostic[]} */ - const found = []; - try { - compiler.compileWireFile(text); - } catch (err) { - found.push(toDiagnostic(doc, err)); - } - diagnostics.set(doc.uri, found); - }; - - const schedule = (doc) => { - const key = doc.uri.toString(); - clearTimeout(timers.get(key)); - timers.set( - key, - setTimeout(() => { - timers.delete(key); - runDiagnostics(doc); - }, 250), - ); - }; - - // Lint on open, edit and save; clear on close. - vscode.workspace.textDocuments.forEach(runDiagnostics); - context.subscriptions.push( - vscode.workspace.onDidOpenTextDocument(runDiagnostics), - vscode.workspace.onDidChangeTextDocument((e) => schedule(e.document)), - vscode.workspace.onDidSaveTextDocument(runDiagnostics), - vscode.workspace.onDidCloseTextDocument((doc) => diagnostics.delete(doc.uri)), - ); - - // Completions. - context.subscriptions.push( - vscode.languages.registerDocumentFormattingEditProvider("wrn", { +function registerFormatter(context) { + const provider = vscode.languages.registerDocumentFormattingEditProvider( + { + language: WRN_LANGUAGE_ID, + scheme: "file", + }, + { provideDocumentFormattingEdits(document, options) { + const configuration = vscode.workspace.getConfiguration("wrnexus", document.uri); + + const enabled = configuration.get("format.enable", true); + + if (!enabled) { + return []; + } + const source = document.getText(); + const printWidth = configuration.get("formatting.printWidth", 100); + const formatted = formatWrn(source, { tabSize: options.tabSize, insertSpaces: options.insertSpaces, - printWidth: 100, + printWidth, }); + if (formatted === source) { + return []; + } + const range = new vscode.Range(document.positionAt(0), document.positionAt(source.length)); return [vscode.TextEdit.replace(range, formatted)]; }, + }, + ); + + context.subscriptions.push(provider); +} + +/** + * Convert a compiler ParseError into a VS Code diagnostic. + * + * Compiler errors normally contain: + * + * at offset 123 + * + * When no offset exists, the diagnostic is attached to the last + * line of the document. + * + * @param {vscode.TextDocument} document + * @param {unknown} error + * @returns {vscode.Diagnostic} + */ +function toCompilerDiagnostic(document, error) { + const message = + error && typeof error === "object" && "message" in error + ? String(error.message) + : "Failed to compile .wrn file."; + + const offsetMatch = /offset\s+(\d+)/i.exec(message); + + let range; + + if (offsetMatch) { + const requestedOffset = Number(offsetMatch[1]); + + const safeOffset = Math.max(0, Math.min(requestedOffset, document.getText().length)); + + const start = document.positionAt(safeOffset); + + const wordRange = document.getWordRangeAtPosition(start); + + range = + wordRange || + new vscode.Range( + start, + document.positionAt(Math.min(safeOffset + 1, document.getText().length)), + ); + } else { + const lastLine = document.lineAt(Math.max(0, document.lineCount - 1)); + + range = lastLine.range; + } + + const diagnostic = new vscode.Diagnostic(range, message, vscode.DiagnosticSeverity.Error); + + diagnostic.source = "WRNexus Compiler"; + + diagnostic.code = "wrn-compiler-error"; + + return diagnostic; +} + +/** + * Register diagnostics produced by the actual WRN compiler. + * + * The lightweight diagnostics in diagnostics.js provide immediate + * editor feedback. Compiler diagnostics verify that the document + * can also be parsed and generated by the real framework compiler. + * + * @param {vscode.ExtensionContext} context + */ +function registerCompilerDiagnostics(context) { + const collection = vscode.languages.createDiagnosticCollection(COMPILER_DIAGNOSTIC_COLLECTION); + + const timers = new Map(); + + /** + * @param {vscode.TextDocument} document + */ + const run = (document) => { + if (document.languageId !== WRN_LANGUAGE_ID) { + return; + } + + const configuration = vscode.workspace.getConfiguration("wrnexus", document.uri); + + const enabled = configuration.get("diagnostics.enable", true); + + if (!enabled) { + collection.delete(document.uri); + return; + } + + if (!compiler || typeof compiler.compileWireFile !== "function") { + collection.delete(document.uri); + return; + } + + const source = document.getText(); + + if (!source.trim()) { + collection.delete(document.uri); + return; + } + + try { + compiler.compileWireFile(source); + + collection.set(document.uri, []); + } catch (error) { + collection.set(document.uri, [toCompilerDiagnostic(document, error)]); + } + }; + + /** + * @param {vscode.TextDocument} document + */ + const schedule = (document) => { + if (document.languageId !== WRN_LANGUAGE_ID) { + return; + } + + const key = document.uri.toString(); + + const existing = timers.get(key); + + if (existing) { + clearTimeout(existing); + } + + const timer = setTimeout(() => { + timers.delete(key); + run(document); + }, 250); + + timers.set(key, timer); + }; + + for (const document of vscode.workspace.textDocuments) { + run(document); + } + + context.subscriptions.push( + collection, + + vscode.workspace.onDidOpenTextDocument(run), + + vscode.workspace.onDidChangeTextDocument((event) => { + schedule(event.document); }), - vscode.languages.registerCompletionItemProvider( - "wrn", - { provideCompletionItems: provideCompletions }, - "-", - "@", - "{", - ":", - ), + + vscode.workspace.onDidSaveTextDocument(run), + + vscode.workspace.onDidCloseTextDocument((document) => { + const key = document.uri.toString(); + + const timer = timers.get(key); + + if (timer) { + clearTimeout(timer); + timers.delete(key); + } + + collection.delete(document.uri); + }), + + vscode.workspace.onDidChangeConfiguration((event) => { + if (!event.affectsConfiguration("wrnexus.diagnostics.enable")) { + return; + } + + for (const document of vscode.workspace.textDocuments) { + run(document); + } + }), + + { + dispose() { + for (const timer of timers.values()) { + clearTimeout(timer); + } + + timers.clear(); + }, + }, ); } /** - * Map a thrown ParseError to a VS Code diagnostic. The compiler encodes the - * failure position as `... at offset ` in the message; we resolve it to a - * range. Errors without an offset (e.g. "Unexpected end of input") anchor to the - * end of the document. - * @param {vscode.TextDocument} doc - * @param {unknown} err - * @returns {vscode.Diagnostic} + * Activate the WRNexus VS Code extension. + * + * @param {vscode.ExtensionContext} context */ -function toDiagnostic(doc, err) { - const message = err && err.message ? String(err.message) : "Failed to parse .wrn file"; - const match = /offset\s+(\d+)/.exec(message); - let range; - if (match) { - const offset = Number(match[1]); - const start = doc.positionAt(offset); - const wordRange = doc.getWordRangeAtPosition(start); - range = wordRange || new vscode.Range(start, doc.positionAt(offset + 1)); - } else { - const last = doc.lineAt(Math.max(0, doc.lineCount - 1)); - range = new vscode.Range(last.range.start, last.range.end); - } - const diag = new vscode.Diagnostic(range, message, vscode.DiagnosticSeverity.Error); - diag.source = "wrn"; - return diag; -} +function activate(context) { + registerDiagnostics(context); + registerCompilerDiagnostics(context); -/** - * @param {vscode.TextDocument} document - * @param {vscode.Position} position - * @returns {vscode.CompletionItem[]} - */ -function provideCompletions(document, position) { - const line = document.lineAt(position).text; - const before = line.slice(0, position.character); - const full = document.getText(); - const upto = document.offsetAt(position); - const inView = isInsideBlock(full, upto, "view"); - - // `{t:` — offer nothing structured, just let the user type the key. - // `@` inside a view — event bindings. - if (inView && /@[A-Za-z-]*$/.test(before)) { - return EVENTS.map((ev) => { - const item = new vscode.CompletionItem(ev, vscode.CompletionItemKind.Event); - item.insertText = new vscode.SnippetString(`${ev}="$0"`); - item.detail = "wrn event binding"; - // Replace the `@`-less part already typed. - return item; - }); - } - - // `data-` inside a view — reactive attributes. - if (inView && /(^|\s)data-[A-Za-z-]*$/.test(before)) { - return DATA_ATTRS.map(([label, snippet, doc]) => { - const item = new vscode.CompletionItem(label, vscode.CompletionItemKind.Property); - item.insertText = new vscode.SnippetString(String(snippet)); - item.documentation = new vscode.MarkdownString(String(doc)); - item.detail = "wrn runtime attribute"; - const dashIdx = before.lastIndexOf("data-"); - item.range = new vscode.Range(position.line, dashIdx, position.line, position.character); - return item; - }); - } - - // Top-level block keywords, when not inside a view/style/functions body. - if (!inView && !isInsideBlock(full, upto, "style") && !isInsideBlock(full, upto, "functions")) { - return BLOCK_KEYWORDS.map(([label, snippet, doc]) => { - const item = new vscode.CompletionItem(String(label), vscode.CompletionItemKind.Keyword); - item.insertText = new vscode.SnippetString(String(snippet)); - item.documentation = new vscode.MarkdownString(String(doc)); - item.detail = "wrn block"; - return item; - }); - } - - return []; -} - -/** - * Very small heuristic: is `offset` inside a ` { ... }` block? Finds the - * nearest preceding `name {` opener and checks braces don't balance before the - * cursor. Good enough to gate view/style/functions-aware completions. - * @param {string} text - * @param {number} offset - * @param {string} name - * @returns {boolean} - */ -function isInsideBlock(text, offset, name) { - const re = new RegExp("\\b" + name + "\\b\\s*(?:[^\\n{]*)\\{", "g"); - let opener = -1; - let m; - while ((m = re.exec(text)) !== null) { - const bracePos = m.index + m[0].length - 1; - if (bracePos >= offset) break; - opener = bracePos; - } - if (opener === -1) return false; - // Count braces between the opener and the cursor; still open => inside. - let depth = 0; - for (let i = opener; i < offset && i < text.length; i++) { - const c = text[i]; - if (c === "{") depth++; - else if (c === "}") depth--; - } - return depth > 0; + registerCompletionProvider(context); + registerDefinitionProvider(context); + registerFormatter(context); } function deactivate() {} -module.exports = { activate, deactivate }; +module.exports = { + activate, + deactivate, + registerCompilerDiagnostics, + registerFormatter, + toCompilerDiagnostic, +}; diff --git a/editors/vscode/src/formatter.js b/editors/vscode/src/formatter.js index 4fd1d8f4..e2890f03 100644 --- a/editors/vscode/src/formatter.js +++ b/editors/vscode/src/formatter.js @@ -19,24 +19,35 @@ const VOID_ELEMENTS = new Set([ function findOpeningTagEnd(value) { let quote = null; + let escaped = false; for (let index = 0; index < value.length; index += 1) { - const char = value[index]; + const character = value[index]; if (quote !== null) { - if (char === quote && value[index - 1] !== "\\") { + if (escaped) { + escaped = false; + continue; + } + + if (character === "\\") { + escaped = true; + continue; + } + + if (character === quote) { quote = null; } continue; } - if (char === '"' || char === "'") { - quote = char; + if (character === '"' || character === "'") { + quote = character; continue; } - if (char === ">") { + if (character === ">") { return index; } } @@ -46,7 +57,8 @@ function findOpeningTagEnd(value) { function parseAttributes(value) { const attributes = []; - const pattern = /[^\s"'=<>`]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?/g; + + const pattern = /[^\s"'=<>`]+(?:\s*=\s*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s"'=<>`]+))?/g; let match; @@ -65,17 +77,21 @@ function parseOpeningTag(value) { } const openingPart = value.slice(0, endIndex + 1); + const remainder = value.slice(endIndex + 1).trim(); - const match = /^<([A-Za-z][\w:-]*)([\s\S]*?)(\/?)>$/.exec(openingPart); + const match = /^<([A-Za-z][\w$:.-]*)([\s\S]*?)(\/?)>$/.exec(openingPart); if (!match) { return null; } const tagName = match[1]; + const attributes = parseAttributes(match[2].trim()); + const selfClosing = match[3] === "/"; + const inlineClosing = remainder === ``; return { @@ -98,7 +114,9 @@ function formatOpeningTag(value, unit, depth, printWidth = 100) { } const baseIndent = unit.repeat(depth); + const attributeIndent = unit.repeat(depth + 1); + const normalizedSingleLine = value.replace(/\s+/g, " ").trim(); const shouldBreak = @@ -159,42 +177,144 @@ function isMultilineOpeningTagStart(value) { } function isClosingTag(value) { - return /^<\/[A-Za-z][\w:-]*\s*>/.test(value); + return /^<\/[A-Za-z][\w$:.-]*\s*>/.test(value); } function isInlineElement(value) { - return /^<([A-Za-z][\w:-]*)\b[^>]*>[\s\S]*<\/\1\s*>$/.test(value); + return /^<([A-Za-z][\w$:.-]*)\b[^>]*>[\s\S]*<\/\1\s*>$/.test(value); } -function isWrnBlockClosing(value) { - return value === "}" || value.startsWith("} "); -} +function countLeadingClosingBraces(value) { + let index = 0; + let count = 0; -function isWrnBlockOpening(value) { - if (!value.endsWith("{")) { - return false; + while (index < value.length) { + while (index < value.length && /\s/.test(value[index])) { + index += 1; + } + + if (value[index] !== "}") { + break; + } + + count += 1; + index += 1; } - return !value.startsWith("{"); + return count; +} + +/** + * Count braces outside strings and HTML comments. + * + * This supports WRN blocks, function bodies, lifecycle hooks, + * watcher bodies and multiline JavaScript object literals. + */ +function countStructuralBraces(value) { + let openings = 0; + let closings = 0; + + let quote = null; + let escaped = false; + let htmlComment = false; + + for (let index = 0; index < value.length; index += 1) { + if (!quote && !htmlComment && value.startsWith("", index)) { + htmlComment = false; + index += 2; + continue; + } + + if (htmlComment) { + continue; + } + + const character = value[index]; + + if (quote !== null) { + if (escaped) { + escaped = false; + continue; + } + + if (character === "\\") { + escaped = true; + continue; + } + + if (character === quote) { + quote = null; + } + + continue; + } + + if (character === '"' || character === "'" || character === "`") { + quote = character; + continue; + } + + if (character === "{") { + openings += 1; + } else if (character === "}") { + closings += 1; + } + } + + return { + openings, + closings, + }; +} + +function collectOpeningTag(inputLines, startIndex) { + const collected = [inputLines[startIndex].trim()]; + + let index = startIndex; + + while (index + 1 < inputLines.length) { + const joined = collected.join(" "); + + if (findOpeningTagEnd(joined) !== -1) { + break; + } + + index += 1; + collected.push(inputLines[index].trim()); + } + + return { + value: collected.join(" "), + endIndex: index, + }; } function formatWrn(source, options = {}) { const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize || 4); const printWidth = options.printWidth || 100; + const inputLines = source.replace(/\r\n/g, "\n").split("\n"); + const output = []; - let wrnDepth = 0; + let codeDepth = 0; let htmlDepth = 0; let index = 0; let previousWasBlank = false; while (index < inputLines.length) { const originalLine = inputLines[index]; - const trimmed = originalLine.trim(); - if (trimmed === "") { + let value = originalLine.trim(); + + if (value === "") { if (!previousWasBlank && output.length > 0) { output.push(""); } @@ -206,43 +326,31 @@ function formatWrn(source, options = {}) { previousWasBlank = false; - let value = trimmed; - if (isMultilineOpeningTagStart(value)) { - const collected = [value]; - let cursor = index + 1; + const collected = collectOpeningTag(inputLines, index); - while (cursor < inputLines.length) { - const nextPart = inputLines[cursor].trim(); - collected.push(nextPart); - - const joined = collected.join(" "); - - if (findOpeningTagEnd(joined) !== -1) { - break; - } - - cursor += 1; - } - - value = collected.join(" "); - index = cursor; + value = collected.value; + index = collected.endIndex; } - if (isWrnBlockClosing(value)) { - wrnDepth = Math.max(0, wrnDepth - 1); - } + const leadingClosingBraces = countLeadingClosingBraces(value); + + const lineCodeDepth = Math.max(0, codeDepth - leadingClosingBraces); + + let lineHtmlDepth = htmlDepth; if (isClosingTag(value)) { - htmlDepth = Math.max(0, htmlDepth - 1); + lineHtmlDepth = Math.max(0, htmlDepth - 1); } - const depth = wrnDepth + htmlDepth; + const depth = lineCodeDepth + lineHtmlDepth; if ( value.startsWith("<") && !value.startsWith("" } ] }, - "declaration": { - "match": "\\b(page|component|layout)\\b\\s+([A-Za-z_][A-Za-z0-9_]*)?", + "match": "\\b(page|component|layout)\\b\\s+([A-Za-z_$][A-Za-z0-9_$]*)?", "captures": { - "1": { "name": "storage.type.wrn keyword.control.wrn" }, - "2": { "name": "entity.name.type.wrn" } + "1": { + "name": "storage.type.wrn keyword.control.wrn" + }, + "2": { + "name": "entity.name.type.wrn" + } } }, - "blocks": { "patterns": [ - { "include": "#view-block" }, - { "include": "#style-block" }, - { "include": "#seo-block" }, - { "include": "#props-block" }, - { "include": "#realtime-block" }, - { "include": "#mode-block" }, - { "include": "#api-block" }, - { "include": "#functions-block" }, - { "include": "#state-decl" }, - { "include": "#layout-decl" }, - { "include": "#strings" } + { + "include": "#view-block" + }, + { + "include": "#style-block" + }, + { + "include": "#seo-block" + }, + { + "include": "#props-block" + }, + { + "include": "#lifecycle-block" + }, + { + "include": "#watch-block" + }, + { + "include": "#realtime-block" + }, + { + "include": "#mode-block" + }, + { + "include": "#api-block" + }, + { + "include": "#functions-block" + }, + { + "include": "#state-decl" + }, + { + "include": "#layout-decl" + }, + { + "include": "#strings" + } ] }, - "layout-decl": { "match": "\\b(layout)\\b\\s*(=)", "captures": { - "1": { "name": "keyword.control.wrn" }, - "2": { "name": "keyword.operator.assignment.wrn" } + "1": { + "name": "keyword.control.wrn" + }, + "2": { + "name": "keyword.operator.assignment.wrn" + } } }, - "state-decl": { - "begin": "\\b(state)\\b\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*(=)", + "begin": "\\b(state)\\b\\s+([A-Za-z_$][A-Za-z0-9_$]*)\\s*(=)", "beginCaptures": { - "1": { "name": "keyword.control.wrn" }, - "2": { "name": "variable.other.wrn" }, - "3": { "name": "keyword.operator.assignment.wrn" } + "1": { + "name": "keyword.control.wrn" + }, + "2": { + "name": "variable.other.wrn" + }, + "3": { + "name": "keyword.operator.assignment.wrn" + } }, "end": "$", "contentName": "meta.embedded.expr.wrn", - "patterns": [{ "include": "source.ts" }] + "patterns": [ + { + "include": "source.ts" + } + ] }, - "props-block": { "begin": "\\b(props)\\b\\s*(\\{)", "beginCaptures": { - "1": { "name": "keyword.control.wrn" }, - "2": { "name": "punctuation.definition.block.begin.wrn" } + "1": { + "name": "keyword.control.wrn" + }, + "2": { + "name": "punctuation.definition.block.begin.wrn" + } }, "end": "\\}", "endCaptures": { - "0": { "name": "punctuation.definition.block.end.wrn" } + "0": { + "name": "punctuation.definition.block.end.wrn" + } }, "patterns": [ - { "include": "#comments" }, { - "match": "\\b([A-Za-z_][A-Za-z0-9_]*)\\s*(=)", + "include": "#comments" + }, + { + "match": "\\b([A-Za-z_$][A-Za-z0-9_$]*)\\s*(=)", "captures": { - "1": { "name": "variable.parameter.wrn" }, - "2": { "name": "keyword.operator.assignment.wrn" } + "1": { + "name": "variable.parameter.wrn" + }, + "2": { + "name": "keyword.operator.assignment.wrn" + } } }, - { "include": "#strings" } + { + "include": "#strings" + } ] }, - "seo-block": { "begin": "\\b(seo)\\b\\s*(\\{)", "beginCaptures": { - "1": { "name": "keyword.control.wrn" }, - "2": { "name": "punctuation.definition.block.begin.wrn" } + "1": { + "name": "keyword.control.wrn" + }, + "2": { + "name": "punctuation.definition.block.begin.wrn" + } }, "end": "\\}", "endCaptures": { - "0": { "name": "punctuation.definition.block.end.wrn" } + "0": { + "name": "punctuation.definition.block.end.wrn" + } }, "patterns": [ - { "include": "#comments" }, { - "match": "\\b([A-Za-z_][A-Za-z0-9_-]*)\\s*(=)", + "include": "#comments" + }, + { + "match": "\\b([A-Za-z_$][A-Za-z0-9_$-]*)\\s*(=)", "captures": { - "1": { "name": "support.type.property-name.wrn" }, - "2": { "name": "keyword.operator.assignment.wrn" } + "1": { + "name": "support.type.property-name.wrn" + }, + "2": { + "name": "keyword.operator.assignment.wrn" + } } }, - { "include": "#strings" } + { + "include": "#strings" + } ] }, - "style-block": { "begin": "\\b(style)\\b\\s*(\\{)", "beginCaptures": { - "1": { "name": "keyword.control.wrn" }, - "2": { "name": "punctuation.definition.block.begin.wrn" } + "1": { + "name": "keyword.control.wrn" + }, + "2": { + "name": "punctuation.definition.block.begin.wrn" + } }, "end": "\\}", "endCaptures": { - "0": { "name": "punctuation.definition.block.end.wrn" } + "0": { + "name": "punctuation.definition.block.end.wrn" + } }, "contentName": "meta.embedded.block.css", - "patterns": [{ "include": "#css-braces" }, { "include": "source.css" }] + "patterns": [ + { + "include": "#css-braces" + }, + { + "include": "source.css" + } + ] }, - "css-braces": { "begin": "\\{", "end": "\\}", "contentName": "meta.embedded.block.css", - "patterns": [{ "include": "#css-braces" }, { "include": "source.css" }] + "patterns": [ + { + "include": "#css-braces" + }, + { + "include": "source.css" + } + ] }, - "functions-block": { "begin": "\\b(functions)\\b\\s*(\\{)", "beginCaptures": { - "1": { "name": "keyword.control.wrn" }, - "2": { "name": "punctuation.definition.block.begin.wrn" } + "1": { + "name": "keyword.control.wrn" + }, + "2": { + "name": "punctuation.definition.block.begin.wrn" + } }, "end": "\\}", "endCaptures": { - "0": { "name": "punctuation.definition.block.end.wrn" } + "0": { + "name": "punctuation.definition.block.end.wrn" + } }, "contentName": "meta.embedded.block.ts", - "patterns": [{ "include": "#ts-braces" }, { "include": "source.ts" }] + "patterns": [ + { + "match": "\\b(async\\s+)?(function)\\s+([A-Za-z_$][A-Za-z0-9_$]*)", + "captures": { + "1": { + "name": "storage.modifier.async.ts" + }, + "2": { + "name": "storage.type.function.ts" + }, + "3": { + "name": "entity.name.function.wrn" + } + } + }, + { + "include": "#ts-braces" + }, + { + "include": "source.ts" + } + ] + }, + "lifecycle-block": { + "begin": "\\b(lifecycle)\\b\\s*(\\{)", + "beginCaptures": { + "1": { + "name": "keyword.control.wrn" + }, + "2": { + "name": "punctuation.definition.block.begin.wrn" + } + }, + "end": "\\}", + "endCaptures": { + "0": { + "name": "punctuation.definition.block.end.wrn" + } + }, + "patterns": [ + { + "include": "#comments" + }, + { + "begin": "\\b(mount|update|unmount)\\b\\s*(\\{)", + "beginCaptures": { + "1": { + "name": "entity.name.function.lifecycle.wrn keyword.control.wrn" + }, + "2": { + "name": "punctuation.definition.block.begin.wrn" + } + }, + "end": "\\}", + "endCaptures": { + "0": { + "name": "punctuation.definition.block.end.wrn" + } + }, + "contentName": "meta.embedded.block.ts", + "patterns": [ + { + "include": "#ts-braces" + }, + { + "include": "source.ts" + } + ] + } + ] + }, + "watch-block": { + "begin": "\\b(watch)\\b\\s+([A-Za-z_$][A-Za-z0-9_$]*)\\s*(\\{)", + "beginCaptures": { + "1": { + "name": "keyword.control.wrn" + }, + "2": { + "name": "variable.other.readwrite.wrn" + }, + "3": { + "name": "punctuation.definition.block.begin.wrn" + } + }, + "end": "\\}", + "endCaptures": { + "0": { + "name": "punctuation.definition.block.end.wrn" + } + }, + "contentName": "meta.embedded.block.ts", + "patterns": [ + { + "match": "\\b(value|previous)\\b", + "name": "variable.language.wrn" + }, + { + "include": "#ts-braces" + }, + { + "include": "source.ts" + } + ] }, - "ts-braces": { "begin": "\\{", "end": "\\}", "contentName": "meta.embedded.block.ts", - "patterns": [{ "include": "#ts-braces" }, { "include": "source.ts" }] - }, - - "api-block": { - "begin": "\\b(api)\\b\\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\\b\\s*([^\\s{]*)\\s*(\\{)", - "beginCaptures": { - "1": { "name": "keyword.control.wrn" }, - "2": { "name": "constant.language.http-method.wrn" }, - "3": { "name": "string.unquoted.route.wrn" }, - "4": { "name": "punctuation.definition.block.begin.wrn" } - }, - "end": "\\}", - "endCaptures": { - "0": { "name": "punctuation.definition.block.end.wrn" } - }, - "contentName": "meta.embedded.block.ts", - "patterns": [{ "include": "#ts-braces" }, { "include": "source.ts" }] - }, - - "mode-block": { - "begin": "\\b(ssr|client)\\b\\s*(\\{)", - "beginCaptures": { - "1": { "name": "keyword.control.wrn" }, - "2": { "name": "punctuation.definition.block.begin.wrn" } - }, - "end": "\\}", - "endCaptures": { - "0": { "name": "punctuation.definition.block.end.wrn" } - }, "patterns": [ - { "include": "#comments" }, { - "begin": "\\b(api)\\b\\s+([A-Za-z_][A-Za-z0-9_]*)\\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\\b\\s*([^\\s{]*)\\s*(\\{)", - "beginCaptures": { - "1": { "name": "keyword.control.wrn" }, - "2": { "name": "entity.name.function.wrn" }, - "3": { "name": "constant.language.http-method.wrn" }, - "4": { "name": "string.unquoted.route.wrn" }, - "5": { "name": "punctuation.definition.block.begin.wrn" } - }, - "end": "\\}", - "endCaptures": { - "0": { "name": "punctuation.definition.block.end.wrn" } - }, - "contentName": "meta.embedded.block.ts", - "patterns": [{ "include": "#ts-braces" }, { "include": "source.ts" }] + "include": "#ts-braces" }, - { "include": "#functions-block" } - ] - }, - - "realtime-block": { - "begin": "\\b(realtime)\\b\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*(\\{)", - "beginCaptures": { - "1": { "name": "keyword.control.wrn" }, - "2": { "name": "entity.name.function.wrn" }, - "3": { "name": "punctuation.definition.block.begin.wrn" } - }, - "end": "\\}", - "endCaptures": { - "0": { "name": "punctuation.definition.block.end.wrn" } - }, - "patterns": [ - { "include": "#comments" }, { - "begin": "\\b(on)\\b\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*(\\([^)]*\\))\\s*(\\{)", - "beginCaptures": { - "1": { "name": "keyword.control.wrn" }, - "2": { "name": "entity.name.function.event.wrn" }, - "3": { "name": "variable.parameter.wrn" }, - "4": { "name": "punctuation.definition.block.begin.wrn" } - }, - "end": "\\}", - "endCaptures": { - "0": { "name": "punctuation.definition.block.end.wrn" } - }, - "contentName": "meta.embedded.block.ts", - "patterns": [{ "include": "#ts-braces" }, { "include": "source.ts" }] + "include": "source.ts" } ] }, - - "view-block": { - "begin": "\\b(view)\\b\\s*(\\{)", + "api-block": { + "begin": "\\b(api)\\b\\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\\b\\s*([^\\s{]*)\\s*(\\{)", "beginCaptures": { - "1": { "name": "keyword.control.wrn" }, - "2": { "name": "punctuation.definition.block.begin.wrn" } + "1": { + "name": "keyword.control.wrn" + }, + "2": { + "name": "constant.language.http-method.wrn" + }, + "3": { + "name": "string.unquoted.route.wrn" + }, + "4": { + "name": "punctuation.definition.block.begin.wrn" + } }, "end": "\\}", "endCaptures": { - "0": { "name": "punctuation.definition.block.end.wrn" } + "0": { + "name": "punctuation.definition.block.end.wrn" + } + }, + "contentName": "meta.embedded.block.ts", + "patterns": [ + { + "include": "#ts-braces" + }, + { + "include": "source.ts" + } + ] + }, + "mode-block": { + "begin": "\\b(ssr|client)\\b\\s*(\\{)", + "beginCaptures": { + "1": { + "name": "keyword.control.wrn" + }, + "2": { + "name": "punctuation.definition.block.begin.wrn" + } + }, + "end": "\\}", + "endCaptures": { + "0": { + "name": "punctuation.definition.block.end.wrn" + } + }, + "patterns": [ + { + "include": "#comments" + }, + { + "begin": "\\b(api)\\b\\s+([A-Za-z_$][A-Za-z0-9_$]*)\\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\\b\\s*([^\\s{]*)\\s*(\\{)", + "beginCaptures": { + "1": { + "name": "keyword.control.wrn" + }, + "2": { + "name": "entity.name.function.wrn" + }, + "3": { + "name": "constant.language.http-method.wrn" + }, + "4": { + "name": "string.unquoted.route.wrn" + }, + "5": { + "name": "punctuation.definition.block.begin.wrn" + } + }, + "end": "\\}", + "endCaptures": { + "0": { + "name": "punctuation.definition.block.end.wrn" + } + }, + "contentName": "meta.embedded.block.ts", + "patterns": [ + { + "include": "#ts-braces" + }, + { + "include": "source.ts" + } + ] + }, + { + "include": "#functions-block" + } + ] + }, + "realtime-block": { + "begin": "\\b(realtime)\\b\\s+([A-Za-z_$][A-Za-z0-9_$]*)\\s*(\\{)", + "beginCaptures": { + "1": { + "name": "keyword.control.wrn" + }, + "2": { + "name": "entity.name.function.wrn" + }, + "3": { + "name": "punctuation.definition.block.begin.wrn" + } + }, + "end": "\\}", + "endCaptures": { + "0": { + "name": "punctuation.definition.block.end.wrn" + } + }, + "patterns": [ + { + "include": "#comments" + }, + { + "begin": "\\b(on)\\b\\s+([A-Za-z_$][A-Za-z0-9_$]*)\\s*(\\([^)]*\\))\\s*(\\{)", + "beginCaptures": { + "1": { + "name": "keyword.control.wrn" + }, + "2": { + "name": "entity.name.function.event.wrn" + }, + "3": { + "name": "variable.parameter.wrn" + }, + "4": { + "name": "punctuation.definition.block.begin.wrn" + } + }, + "end": "\\}", + "endCaptures": { + "0": { + "name": "punctuation.definition.block.end.wrn" + } + }, + "contentName": "meta.embedded.block.ts", + "patterns": [ + { + "include": "#ts-braces" + }, + { + "include": "source.ts" + } + ] + } + ] + }, + "view-block": { + "begin": "\\b(view)\\b\\s*(\\{)", + "beginCaptures": { + "1": { + "name": "keyword.control.wrn" + }, + "2": { + "name": "punctuation.definition.block.begin.wrn" + } + }, + "end": "\\}", + "endCaptures": { + "0": { + "name": "punctuation.definition.block.end.wrn" + } }, "contentName": "meta.embedded.block.html", "patterns": [ - { "include": "#interpolation" }, - { "include": "#wire-event" }, - { "include": "text.html.basic" } + { + "include": "#interpolation" + }, + { + "include": "#wire-event" + }, + { + "include": "#conditional-class" + }, + { + "include": "text.html.basic" + } ] }, - "wire-event": { - "match": "(@)([A-Za-z][A-Za-z0-9_-]*)(=)", + "match": "(@)(?:(window|document)(:))?([A-Za-z][A-Za-z0-9_-]*)(\\s*=)", "captures": { - "1": { "name": "punctuation.definition.keyword.wrn" }, - "2": { "name": "entity.other.attribute-name.event.wrn" }, - "3": { "name": "keyword.operator.assignment.wrn" } + "1": { + "name": "punctuation.definition.keyword.wrn" + }, + "2": { + "name": "support.type.object.wrn" + }, + "3": { + "name": "punctuation.separator.key-value.wrn" + }, + "4": { + "name": "entity.other.attribute-name.event.wrn" + }, + "5": { + "name": "keyword.operator.assignment.wrn" + } + } + }, + "conditional-class": { + "match": "\\b(class)(:)([^\\s=]+)(\\s*=)", + "captures": { + "1": { + "name": "entity.other.attribute-name.class.wrn" + }, + "2": { + "name": "punctuation.separator.key-value.wrn" + }, + "3": { + "name": "string.unquoted.class-name.wrn" + }, + "4": { + "name": "keyword.operator.assignment.wrn" + } } }, - "interpolation": { "patterns": [ { "name": "meta.interpolation.i18n.wrn", "match": "(\\{)(t:)([^}]*)(\\})", "captures": { - "1": { "name": "punctuation.section.interpolation.begin.wrn" }, - "2": { "name": "keyword.control.i18n.wrn" }, - "3": { "name": "string.other.i18n-key.wrn" }, - "4": { "name": "punctuation.section.interpolation.end.wrn" } + "1": { + "name": "punctuation.section.interpolation.begin.wrn" + }, + "2": { + "name": "keyword.control.i18n.wrn" + }, + "3": { + "name": "string.other.i18n-key.wrn" + }, + "4": { + "name": "punctuation.section.interpolation.end.wrn" + } } }, { @@ -268,37 +601,63 @@ "begin": "\\{", "end": "\\}", "beginCaptures": { - "0": { "name": "punctuation.section.interpolation.begin.wrn" } + "0": { + "name": "punctuation.section.interpolation.begin.wrn" + } }, "endCaptures": { - "0": { "name": "punctuation.section.interpolation.end.wrn" } + "0": { + "name": "punctuation.section.interpolation.end.wrn" + } }, "contentName": "meta.embedded.expr.wrn", - "patterns": [{ "include": "#interp-braces" }, { "include": "source.ts" }] + "patterns": [ + { + "include": "#interp-braces" + }, + { + "include": "source.ts" + } + ] } ] }, - "interp-braces": { "begin": "\\{", "end": "\\}", "contentName": "meta.embedded.expr.wrn", - "patterns": [{ "include": "#interp-braces" }, { "include": "source.ts" }] + "patterns": [ + { + "include": "#interp-braces" + }, + { + "include": "source.ts" + } + ] }, - "strings": { "patterns": [ { "name": "string.quoted.double.wrn", "begin": "\"", "end": "\"", - "patterns": [{ "name": "constant.character.escape.wrn", "match": "\\\\." }] + "patterns": [ + { + "name": "constant.character.escape.wrn", + "match": "\\\\." + } + ] }, { "name": "string.quoted.single.wrn", "begin": "'", "end": "'", - "patterns": [{ "name": "constant.character.escape.wrn", "match": "\\\\." }] + "patterns": [ + { + "name": "constant.character.escape.wrn", + "match": "\\\\." + } + ] } ] } diff --git a/editors/vscode/wrnexus-0.2.7.vsix b/editors/vscode/wrnexus-0.2.7.vsix new file mode 100644 index 0000000000000000000000000000000000000000..8312e3e9f959fdd556b3eca7318715b31664d8d4 GIT binary patch literal 48585 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 I+Wx%zU(ZHu-2eap literal 0 HcmV?d00001 diff --git a/packages/ai/package.json b/packages/ai/package.json index 182e5687..bb0b66b2 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ai", - "version": "0.2.31", + "version": "0.2.32", "private": true, "type": "module", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", diff --git a/packages/authz/package.json b/packages/authz/package.json index 8ca553a8..bc7ede0d 100644 --- a/packages/authz/package.json +++ b/packages/authz/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/authz", - "version": "0.2.31", + "version": "0.2.32", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/cli/package.json b/packages/cli/package.json index 47e61186..f8f096e3 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/cli/src/update.ts b/packages/cli/src/update.ts index 47ccb7be..04ce7f66 100644 --- a/packages/cli/src/update.ts +++ b/packages/cli/src/update.ts @@ -357,6 +357,15 @@ const MIGRATIONS: Migration[] = [ // No generated application files require automatic migration. }, }, + { + version: "0.2.32", + id: "fix-runtime-bug", + description: + "Fixes safe encoding and browser parsing of multiline component behavior metadata.", + apply() { + // No generated application files require automatic migration. + }, + }, ]; /** Release tooling uses this to require an explicit migration entry per version. */ diff --git a/packages/compiler/package.json b/packages/compiler/package.json index c99b5de1..9c13b16c 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/compiler/src/codegen.ts b/packages/compiler/src/codegen.ts index ddc9da00..64177722 100644 --- a/packages/compiler/src/codegen.ts +++ b/packages/compiler/src/codegen.ts @@ -925,7 +925,9 @@ function behaviorAttribute(behavior: ComponentBehavior | null): string { return ""; } - return ` data-wrn-behavior="${attrEscape(JSON.stringify(behavior))}"`; + const encoded = encodeURIComponent(JSON.stringify(behavior)); + + return ` data-wrn-behavior="${encoded}"`; } const INTERP_RE = /\{([^{}]+)\}/g; diff --git a/packages/compiler/test/compiler.test.ts b/packages/compiler/test/compiler.test.ts index 03fee9de..fa51d935 100644 --- a/packages/compiler/test/compiler.test.ts +++ b/packages/compiler/test/compiler.test.ts @@ -382,11 +382,11 @@ component LifecycleTest { `), ); - expect(output).toContain(""mount""); + expect(output).toContain("%22mount%22"); - expect(output).toContain(""update""); + expect(output).toContain("%22update%22"); - expect(output).toContain(""unmount""); + expect(output).toContain("%22unmount%22"); }); test("component watchers compile into behavior metadata", () => { const output = generate( @@ -405,7 +405,7 @@ component WatchTest { `), ); - expect(output).toContain(""state":"visible""); + expect(output).toContain("%22state%22%3A%22visible%22"); expect(output).toContain("console.log"); }); @@ -436,3 +436,121 @@ component BehaviorOnly { expect(output).toContain("data-wrn-behavior"); }); +test("component behavior metadata safely encodes multiline code", () => { + const output = generate( + parse(` +component BackToTop { + state visible = false + + functions { + function updateVisibility() { + visible = window.scrollY > 500 + } + + function scrollToTop() { + window.scrollTo({ + top: 0, + behavior: "smooth" + }) + } + } + + lifecycle { + mount { + updateVisibility() + } + } + + watch visible { + console.log( + value, + previous + ) + } + + view { + + } +} +`), + ); + + expect(output).toContain("data-wrn-behavior="); + + expect(output).toContain("%7B"); + expect(output).toContain("%22functions%22"); + + expect(output).not.toContain('data-wrn-behavior="{'); +}); + +test("component watchers compile into behavior metadata", () => { + const output = generate( + parse(` +component WatchTest { + state visible = false + + watch visible { + console.log("changed", visible) + } + + view { +
{visible}
+ } +} +`), + ); + + const match = /data-wrn-behavior="([^"]+)"/.exec(output); + + expect(match).not.toBeNull(); + + const behavior = JSON.parse(decodeURIComponent(match![1])); + + expect(behavior.watches).toEqual([ + { + state: "visible", + body: expect.stringContaining('console.log("changed", visible)'), + }, + ]); +}); +test("component lifecycle hooks compile into behavior metadata", () => { + const output = generate( + parse(` +component LifecycleTest { + state ready = false + + lifecycle { + mount { + ready = true + } + + update { + console.log(ready) + } + + unmount { + ready = false + } + } + + view { +
{ready}
+ } +} +`), + ); + + const match = /data-wrn-behavior="([^"]+)"/.exec(output); + + expect(match).not.toBeNull(); + + const behavior = JSON.parse(decodeURIComponent(match![1])); + + expect(behavior.lifecycle.mount).toContain("ready = true"); + + expect(behavior.lifecycle.update).toContain("console.log(ready)"); + + expect(behavior.lifecycle.unmount).toContain("ready = false"); +}); diff --git a/packages/core/package.json b/packages/core/package.json index 4abf7cb0..74c2243e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/csr/package.json b/packages/csr/package.json index c113a266..e3fc0984 100644 --- a/packages/csr/package.json +++ b/packages/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/csr/src/reactive-runtime.ts b/packages/csr/src/reactive-runtime.ts index 872e47de..1b129d46 100644 --- a/packages/csr/src/reactive-runtime.ts +++ b/packages/csr/src/reactive-runtime.ts @@ -44,13 +44,32 @@ export const REACTIVE_RUNTIME = String.raw` } function parseBehavior(element) { - var raw = element.getAttribute("data-wrn-behavior"); - if (!raw) return null; + var raw = element.getAttribute( + "data-wrn-behavior", + ); + + if (!raw) { + return null; + } + try { - var parsed = JSON.parse(raw); - return parsed && typeof parsed === "object" ? parsed : null; + var decoded = decodeURIComponent(raw); + var parsed = JSON.parse(decoded); + + if ( + !parsed || + typeof parsed !== "object" + ) { + return null; + } + + return parsed; } catch (error) { - console.error("[wrnexus] failed to parse component behavior", error); + console.error( + "[wrnexus] failed to parse component behavior", + error, + ); + return null; } } diff --git a/packages/db/package.json b/packages/db/package.json index 5f7945ef..b6f1ee1c 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.2.31", + "version": "0.2.32", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/dev-server/package.json b/packages/dev-server/package.json index 3079b3c3..11de4324 100644 --- a/packages/dev-server/package.json +++ b/packages/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/encryption/package.json b/packages/encryption/package.json index 277e1ca2..d723b969 100644 --- a/packages/encryption/package.json +++ b/packages/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.2.31", + "version": "0.2.32", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/helpers/package.json b/packages/helpers/package.json index 1be148c7..19869f3b 100644 --- a/packages/helpers/package.json +++ b/packages/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.2.31", + "version": "0.2.32", "private": true, "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 96e2d25a..367f8e92 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.2.31", + "version": "0.2.32", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/jwt/package.json b/packages/jwt/package.json index c9976710..85e77084 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.2.31", + "version": "0.2.32", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 5987d781..5fb8eb79 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/native/package.json b/packages/native/package.json index e94dc4e2..e5eb35e6 100644 --- a/packages/native/package.json +++ b/packages/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/oauth/package.json b/packages/oauth/package.json index 5246c971..e2af2548 100644 --- a/packages/oauth/package.json +++ b/packages/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.2.31", + "version": "0.2.32", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/pubsub/package.json b/packages/pubsub/package.json index 2666ed0e..9dde2fe9 100644 --- a/packages/pubsub/package.json +++ b/packages/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.2.31", + "version": "0.2.32", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/queue/package.json b/packages/queue/package.json index e1f30a7e..239e67ec 100644 --- a/packages/queue/package.json +++ b/packages/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.2.31", + "version": "0.2.32", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/reactive/package.json b/packages/reactive/package.json index 80fbc1fb..49cf2b8e 100644 --- a/packages/reactive/package.json +++ b/packages/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/router/package.json b/packages/router/package.json index cd0c372b..3ade68c0 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/ssr/package.json b/packages/ssr/package.json index 84293f68..7492720a 100644 --- a/packages/ssr/package.json +++ b/packages/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/styles/package.json b/packages/styles/package.json index b7b8059d..7a019e39 100644 --- a/packages/styles/package.json +++ b/packages/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.2.31", + "version": "0.2.32", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/test/package.json b/packages/test/package.json index 08b19609..a0072d6f 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.2.31", + "version": "0.2.32", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/tracking/package.json b/packages/tracking/package.json index 4ec18f8b..17393a6b 100644 --- a/packages/tracking/package.json +++ b/packages/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.2.31", + "version": "0.2.32", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index 30706e92..73118372 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.2.31", + "version": "0.2.32", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/uploader/package.json b/packages/uploader/package.json index 7ed7f3e4..7f6ec631 100644 --- a/packages/uploader/package.json +++ b/packages/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.2.31", + "version": "0.2.32", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/validation/package.json b/packages/validation/package.json index b713242c..0b7d6b7a 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.2.31", + "version": "0.2.32", "private": true, "type": "module", "main": "src/index.ts",