Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9070a1ef9c | ||
|
|
4230de7065 | ||
|
|
0006aef2c4 | ||
|
|
7dfca4a190 | ||
|
|
29bbeefe92 | ||
|
|
98445c2ec7 | ||
|
|
17a535d4dc | ||
|
|
afb14c2fbf | ||
|
|
2c6e6f6de3 | ||
|
|
cade812569 | ||
|
|
936488ac06 | ||
|
|
959ed24009 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ai",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/authz",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/authz — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
+11
-11
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/cli",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"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.42",
|
||||
"@wrnexus/router": "^0.2.42",
|
||||
"@wrnexus/csr": "^0.2.42",
|
||||
"@wrnexus/compiler": "^0.2.42",
|
||||
"@wrnexus/styles": "^0.2.42",
|
||||
"@wrnexus/dev-server": "^0.2.42",
|
||||
"@wrnexus/ui": "^0.2.42",
|
||||
"@wrnexus/validation": "^0.2.42",
|
||||
"@wrnexus/i18n": "^0.2.42",
|
||||
"@wrnexus/db": "^0.2.42"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/compiler",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/compiler — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/core",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/core — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/csr",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"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.42"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/db",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/db — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-server",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"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.42",
|
||||
"@wrnexus/router": "^0.2.42",
|
||||
"@wrnexus/ssr": "^0.2.42",
|
||||
"@wrnexus/csr": "^0.2.42",
|
||||
"@wrnexus/compiler": "^0.2.42",
|
||||
"@wrnexus/styles": "^0.2.42",
|
||||
"@wrnexus/ui": "^0.2.42",
|
||||
"@wrnexus/validation": "^0.2.42",
|
||||
"@wrnexus/i18n": "^0.2.42",
|
||||
"@wrnexus/db": "^0.2.42",
|
||||
"@wrnexus/pubsub": "^0.2.42",
|
||||
"@wrnexus/uploader": "^0.2.42"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/encryption",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/encryption — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/helpers",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"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.42"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/i18n",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"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.42"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/jwt",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/jwt — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/mobile",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"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.42"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/native",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/native — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/oauth",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/oauth — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/pubsub",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/pubsub — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/queue",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/queue — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/reactive",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/reactive — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/router",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"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.42",
|
||||
"@wrnexus/core": "^0.2.42"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ssr",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"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.42"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/styles",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"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.42"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/test",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/test — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/tracking",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/tracking — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ui",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"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.42"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/uploader",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"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.42"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/validation",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/validation — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
Vendored
+17
-1
@@ -42,5 +42,21 @@
|
||||
"editors/vscode/src/compiler.cjs": true
|
||||
},
|
||||
|
||||
"wrnexus.diagnostics.enable": true
|
||||
"wrnexus.diagnostics.enable": true,
|
||||
|
||||
"editor.semanticHighlighting.enabled": true,
|
||||
"editor.semanticTokenColorCustomizations": {
|
||||
"rules": {
|
||||
"variable:wrn": {
|
||||
"foreground": "#8B5CF6"
|
||||
},
|
||||
"variable.declaration:wrn": {
|
||||
"foreground": "#A78BFA",
|
||||
"fontStyle": "bold"
|
||||
},
|
||||
"variable.modification:wrn": {
|
||||
"foreground": "#F59E0B"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+756
-47
@@ -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-<version>.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-<version>.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 {
|
||||
<main>
|
||||
<h1>User {id}</h1>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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 {
|
||||
<button @click="count++">
|
||||
Count: {count}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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 {
|
||||
<header>
|
||||
<nav>Navigation</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{content}
|
||||
</main>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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 {
|
||||
<button @click="increment()">
|
||||
Increment
|
||||
</button>
|
||||
|
||||
<button @click="reset()">
|
||||
Reset
|
||||
</button>
|
||||
|
||||
<p>{count}</p>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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 {
|
||||
<button @click="open = !open">
|
||||
Toggle menu
|
||||
</button>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
<div @window:scroll="updateScrollPosition()">
|
||||
</div>
|
||||
```
|
||||
|
||||
```wrn
|
||||
<div @window:resize="updateViewportSize()">
|
||||
</div>
|
||||
```
|
||||
|
||||
```wrn
|
||||
<div @window:keydown="handleKeyboardShortcut()">
|
||||
</div>
|
||||
```
|
||||
|
||||
### Document events
|
||||
|
||||
```wrn
|
||||
<div @document:click="closeMenu()">
|
||||
</div>
|
||||
```
|
||||
|
||||
```wrn
|
||||
<div @document:visibilitychange="handleVisibilityChange()">
|
||||
</div>
|
||||
```
|
||||
|
||||
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 {
|
||||
<button
|
||||
type="button"
|
||||
@click="scrollToTop()"
|
||||
aria-label="Back to top"
|
||||
class="fixed bottom-5 right-5"
|
||||
class:pointer-events-none="!visible"
|
||||
class:translate-y-4="!visible"
|
||||
class:opacity-0="!visible"
|
||||
class:pointer-events-auto="visible"
|
||||
class:translate-y-0="visible"
|
||||
class:opacity-100="visible"
|
||||
>
|
||||
Back to top
|
||||
</button>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Conditional classes
|
||||
|
||||
Use `class:<class-name>` to toggle CSS classes reactively:
|
||||
|
||||
```wrn
|
||||
<button
|
||||
class:opacity-100="visible"
|
||||
class:pointer-events-auto="visible"
|
||||
class:opacity-0="!visible"
|
||||
class:pointer-events-none="!visible"
|
||||
>
|
||||
Action
|
||||
</button>
|
||||
```
|
||||
|
||||
The expression is evaluated whenever its reactive dependencies change.
|
||||
|
||||
## Event bindings
|
||||
|
||||
Standard element events:
|
||||
|
||||
```wrn
|
||||
<button @click="submitForm()">
|
||||
Submit
|
||||
</button>
|
||||
```
|
||||
|
||||
```wrn
|
||||
<input @input="query = event.target.value" />
|
||||
```
|
||||
|
||||
```wrn
|
||||
<form @submit="handleSubmit()">
|
||||
</form>
|
||||
```
|
||||
|
||||
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
|
||||
<div data-show="open">
|
||||
Visible when open is true
|
||||
</div>
|
||||
```
|
||||
|
||||
### Reactive loops
|
||||
|
||||
```wrn
|
||||
<ul>
|
||||
<li data-for="user in users">
|
||||
{user.name}
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
### Reactive text
|
||||
|
||||
```wrn
|
||||
<span data-text="message"></span>
|
||||
```
|
||||
|
||||
### Dynamic attributes
|
||||
|
||||
```wrn
|
||||
<button disabled="{loading}">
|
||||
Save
|
||||
</button>
|
||||
```
|
||||
|
||||
## 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-<version>.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 <kbd>F5</kbd> to launch an Extension
|
||||
Development Host, then open any `.wrn` file.
|
||||
```text
|
||||
wrnexus-<version>.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-<version>.vsix --force
|
||||
```
|
||||
|
||||
Open the extension folder in VS Code and press <kbd>F5</kbd> 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.
|
||||
|
||||
+105
-8
@@ -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.10",
|
||||
"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": {
|
||||
@@ -151,7 +225,30 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"semanticTokenScopes": [
|
||||
{
|
||||
"language": "wrn",
|
||||
"scopes": {
|
||||
"variable.declaration": [
|
||||
"variable.other.readwrite.wrn",
|
||||
"variable.other.readwrite.declaration.wrn"
|
||||
],
|
||||
"variable": [
|
||||
"variable.other.readwrite.wrn"
|
||||
],
|
||||
"parameter": [
|
||||
"variable.parameter.wrn"
|
||||
],
|
||||
"function": [
|
||||
"entity.name.function.wrn"
|
||||
],
|
||||
"property": [
|
||||
"variable.other.property.wrn"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"build:compiler": "bun build ../../packages/compiler/src/index.ts --target=node --format=cjs --outfile=src/compiler.cjs",
|
||||
|
||||
@@ -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 {",
|
||||
" <div>",
|
||||
" <h2>{${2:title}}</h2>",
|
||||
@@ -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 {",
|
||||
" <div>{${2:active}}</div>",
|
||||
" }",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
|
||||
"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}\"}",
|
||||
"</button>"
|
||||
]
|
||||
},
|
||||
|
||||
"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 {",
|
||||
" <button",
|
||||
" type=\"button\"",
|
||||
" @click=\"scrollToTop()\"",
|
||||
" aria-label=\"Back to top\"",
|
||||
" class=\"fixed bottom-5 right-5\"",
|
||||
" class:pointer-events-none=\"!visible\"",
|
||||
" class:translate-y-4=\"!visible\"",
|
||||
" class:opacity-0=\"!visible\"",
|
||||
" class:pointer-events-auto=\"visible\"",
|
||||
" class:translate-y-0=\"visible\"",
|
||||
" class:opacity-100=\"visible\"",
|
||||
" >",
|
||||
" ${2:Back to top}",
|
||||
" </button>",
|
||||
" }",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+291
-34
@@ -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
|
||||
};
|
||||
@@ -449,12 +493,6 @@ function parseHtmlView(src, pos) {
|
||||
let i = pos;
|
||||
const isNameStart = (c) => /[A-Za-z_]/.test(c);
|
||||
const isTagNamePart = (c) => /[A-Za-z0-9_$:.-]/.test(c);
|
||||
const isAttributeNamePart = (c, next) => {
|
||||
if (c === "/") {
|
||||
return next !== ">";
|
||||
}
|
||||
return /[A-Za-z0-9_$:.[\]%-]/.test(c);
|
||||
};
|
||||
const isWs2 = (c) => c === " " || c === "\t" || c === `
|
||||
` || c === "\r";
|
||||
const fail = (msg) => {
|
||||
@@ -502,14 +540,22 @@ function parseHtmlView(src, pos) {
|
||||
return src.slice(start, i);
|
||||
};
|
||||
const readAttributeName = () => {
|
||||
const first = src[i];
|
||||
if (i >= src.length || !isNameStart(first) && first !== ":" && first !== "$") {
|
||||
if (i >= src.length) {
|
||||
return fail("Expected an attribute name");
|
||||
}
|
||||
const start = i++;
|
||||
while (i < src.length && isAttributeNamePart(src[i], src[i + 1])) {
|
||||
const start = i;
|
||||
while (i < src.length) {
|
||||
const char = src[i];
|
||||
const next = src[i + 1];
|
||||
if (char === "=" || char === ">" || char === '"' || char === "'" || char === " " || char === "\t" || char === `
|
||||
` || char === "\r" || char === "/" && next === ">") {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (i === start) {
|
||||
return fail("Expected an attribute name");
|
||||
}
|
||||
return src.slice(start, i);
|
||||
};
|
||||
const parseTag = () => {
|
||||
@@ -686,6 +732,7 @@ function parseHtmlView(src, pos) {
|
||||
}
|
||||
|
||||
// ../../packages/compiler/src/codegen.ts
|
||||
var import_node_buffer = require("node:buffer");
|
||||
function isComponentTag(tag) {
|
||||
return /^[A-Z][A-Za-z0-9_$]*$/.test(tag);
|
||||
}
|
||||
@@ -716,10 +763,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) {
|
||||
@@ -916,21 +971,23 @@ function renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive
|
||||
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive)}>${inner}</${node.tag}>`;
|
||||
}
|
||||
function renderPageComponentInvocation(node, ssrBindings, csrBindings, apiBindings, loops, reactive) {
|
||||
const attrs = node.attrs.filter((attr) => attr.name !== "data-component");
|
||||
const attrs = node.attrs.filter((attr) => attr.name !== "data-component").map((attr) => renderPageComponentAttr(attr, loops)).join("");
|
||||
const inner = node.children.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive)).join("");
|
||||
return `<div data-component="${attrEscape(node.tag)}"${renderAttrs(attrs, undefined, reactive)}>${inner}</div>`;
|
||||
return `<div data-component="${attrEscape(node.tag)}"` + `${attrs}>${inner}</div>`;
|
||||
}
|
||||
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}`;
|
||||
}
|
||||
const rendered = ` ${attr.name}="` + `${compileAttrValue(attr.value, ctx)}"`;
|
||||
if (!attr.value.includes("{") || !exprRefsState(attr.value, ctx.stateNames)) {
|
||||
const wholeExpression = wholeAttributeExpression(attr.value);
|
||||
const compiledValue = wholeExpression ? `\${__wireProp(${ctx.resolveExpr(wholeExpression)})}` : compileAttrValue(attr.value, ctx);
|
||||
const rendered = ` ${attr.name}="${compiledValue}"`;
|
||||
if (wholeExpression || !attr.value.includes("{") || !exprRefsState(attr.value, ctx.stateNames)) {
|
||||
return rendered;
|
||||
}
|
||||
const marker = attrEscape(JSON.stringify([attr.name, attr.value]));
|
||||
@@ -1031,6 +1088,28 @@ function __wrnexusEvalData(data: unknown, body: string, helpers = "", ctx: any):
|
||||
return new Function("$data", "$adapters", "const cookies = $adapters.cookies;\\nconst session = $adapters.session;\\nconst localStorage = $adapters.localStorage;\\nwith ($data ?? {}) {\\n" + helpers + "\\n" + body + "\\n}")(data, adapters);
|
||||
}
|
||||
|
||||
function __wrnexusPropAttr(
|
||||
value: unknown,
|
||||
): string {
|
||||
const serialized =
|
||||
value !== null &&
|
||||
typeof value === "object"
|
||||
? JSON.stringify(value)
|
||||
: String(value == null ? "" : value);
|
||||
|
||||
return serialized.replace(
|
||||
/[&<>"]/g,
|
||||
(character) =>
|
||||
character === "&"
|
||||
? "&"
|
||||
: character === "<"
|
||||
? "<"
|
||||
: character === ">"
|
||||
? ">"
|
||||
: """,
|
||||
);
|
||||
}
|
||||
|
||||
async function __wrnexusCallApi(path: string, method: string, ctx: any): Promise<unknown> {
|
||||
if (typeof ctx.__wrnexusCallApi === "function") {
|
||||
return await ctx.__wrnexusCallApi(path, method);
|
||||
@@ -1262,6 +1341,35 @@ 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 "";
|
||||
}
|
||||
const encoded = import_node_buffer.Buffer.from(JSON.stringify(behavior), "utf8").toString("base64");
|
||||
return ` data-wrn-behavior="${encoded}"`;
|
||||
}
|
||||
var INTERP_RE = /\{([^{}]+)\}/g;
|
||||
function exprRefsState(expr, stateNames) {
|
||||
for (const name of stateNames) {
|
||||
@@ -1271,7 +1379,17 @@ function exprRefsState(expr, stateNames) {
|
||||
return false;
|
||||
}
|
||||
function viewHasEvents(nodes) {
|
||||
return nodes.some((n) => n.type === "element" && (n.attrs.some((a) => a.event) || viewHasEvents(n.children)));
|
||||
return nodes.some((node) => {
|
||||
if (node.type === "text")
|
||||
return false;
|
||||
if (node.type === "each") {
|
||||
return viewHasEvents(node.body) || viewHasEvents(node.empty);
|
||||
}
|
||||
if (node.type === "if") {
|
||||
return node.branches.some((branch) => viewHasEvents(branch.body));
|
||||
}
|
||||
return node.attrs.some((attr) => attr.event) || viewHasEvents(node.children);
|
||||
});
|
||||
}
|
||||
function compileText(raw, ctx) {
|
||||
let out = "";
|
||||
@@ -1315,11 +1433,36 @@ function compileAttrValue(raw, ctx) {
|
||||
}
|
||||
return out + escLit(attrEscape(raw.slice(last)));
|
||||
}
|
||||
function renderComponentIfNode(node, ctx) {
|
||||
let expression = "``";
|
||||
for (let index = node.branches.length - 1;index >= 0; index--) {
|
||||
const branch = node.branches[index];
|
||||
const body = branch.body.map((child) => renderComponentNode(child, ctx)).join("");
|
||||
const bodyExpression = "`" + body + "`";
|
||||
expression = branch.cond === null ? bodyExpression : `(${ctx.resolveExpr(branch.cond)}) ? ${bodyExpression} : ${expression}`;
|
||||
}
|
||||
return "${" + expression + "}";
|
||||
}
|
||||
function renderComponentEachNode(node, ctx) {
|
||||
const item = node.item;
|
||||
const index = node.index ?? "__wi";
|
||||
const list = ctx.resolveExpr(node.list);
|
||||
const childCtx = {
|
||||
...ctx,
|
||||
serverLocals: new Set([...ctx.serverLocals ?? [], item, index])
|
||||
};
|
||||
const body = node.body.map((child) => renderComponentNode(child, childCtx)).join("");
|
||||
const empty = node.empty.map((child) => renderComponentNode(child, ctx)).join("");
|
||||
return "${(() => { const __wl = Array.isArray(" + list + ") ? (" + list + ") : []; return __wl.length ? __wl.map((" + item + ", " + index + ") => `" + body + '`).join("") : `' + empty + "`; })()}";
|
||||
}
|
||||
function renderComponentNode(node, ctx) {
|
||||
if (node.type === "text")
|
||||
return compileText(node.value, ctx);
|
||||
if (node.type === "each" || node.type === "if") {
|
||||
throw new Error("Server `{#each}` / `{#if}` blocks are supported in pages, not components. Move them into a page (or use data-for / data-show on the client).");
|
||||
if (node.type === "each") {
|
||||
return renderComponentEachNode(node, ctx);
|
||||
}
|
||||
if (node.type === "if") {
|
||||
return renderComponentIfNode(node, ctx);
|
||||
}
|
||||
if (isComponentTag(node.tag)) {
|
||||
return renderNestedComponentInvocation(node, ctx);
|
||||
@@ -1340,7 +1483,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}`;
|
||||
@@ -1355,14 +1498,19 @@ function renderComponentNode(node, ctx) {
|
||||
const initialConditionalClasses = conditionalClasses.map(({ className, expression }) => {
|
||||
return `\${(${ctx.resolveExpr(expression)}) ? ${JSON.stringify(` ${className}`)} : ""}`;
|
||||
}).join("");
|
||||
const classAttribute = staticClasses.length > 0 || conditionalClasses.length > 0 ? ` class="${compileAttrValue(staticClasses.join(" "), ctx)}${initialConditionalClasses}"` : "";
|
||||
const classBindings = conditionalClasses.map(({ className, expression }, index) => {
|
||||
const staticClassValue = staticClasses.join(" ");
|
||||
const classHasReactiveExpression = staticClassValue.includes("{") && exprRefsState(staticClassValue, ctx.stateNames);
|
||||
const classAttribute = staticClasses.length > 0 || conditionalClasses.length > 0 ? ` class="${compileAttrValue(staticClassValue, ctx)}${initialConditionalClasses}"` : "";
|
||||
const classReactiveBinding = classHasReactiveExpression ? ` data-wrn-bind-class="${escLit(attrEscape(JSON.stringify(["class", staticClassValue])))}"` : "";
|
||||
const classBindings = conditionalClasses.filter(({ expression }) => {
|
||||
return !ctx.serverLocals || !exprRefsState(expression, ctx.serverLocals);
|
||||
}).map(({ className, expression }, index) => {
|
||||
const marker = attrEscape(JSON.stringify([className, expression]));
|
||||
return ` data-wrn-class-${index}="${escLit(marker)}"`;
|
||||
}).join("");
|
||||
const loops = loopVarsOf(node);
|
||||
const childCtx = loops.length > 0 ? { ...ctx, loopVars: new Set([...ctx.loopVars ?? [], ...loops]) } : ctx;
|
||||
const allAttrs = `${classAttribute}${classBindings}${attrs}`;
|
||||
const allAttrs = `${classAttribute}` + `${classReactiveBinding}` + `${classBindings}` + `${attrs}`;
|
||||
if (VOID_ELEMENTS.has(node.tag.toLowerCase())) {
|
||||
return `<${node.tag}${allAttrs}>`;
|
||||
}
|
||||
@@ -1400,11 +1548,13 @@ function generateComponent(ast) {
|
||||
${styles.map(styleEscape).join(`
|
||||
`)}
|
||||
</style>`) : "";
|
||||
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 +1562,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 + '<div data-scope="${__scope}">' + viewCode + "</div>`" : "`" + styleTag + viewCode + "`";
|
||||
const returnExpr = needsScope ? "`" + styleTag + `<div data-scope="\${__scope}"${behaviorAttr}>` + viewCode + "</div>`" : "`" + 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,19 +1571,107 @@ ${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);
|
||||
if (typeof def === "boolean") return v === true || v === "" || v === "true";
|
||||
if (v === undefined || v === null) {
|
||||
return def;
|
||||
}
|
||||
|
||||
if (typeof def === "number") {
|
||||
return Number(v);
|
||||
}
|
||||
|
||||
if (typeof def === "boolean") {
|
||||
return v === true || v === "" || v === "true";
|
||||
}
|
||||
|
||||
if (Array.isArray(def)) {
|
||||
if (Array.isArray(v)) {
|
||||
return v;
|
||||
}
|
||||
|
||||
if (typeof v === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(v);
|
||||
return Array.isArray(parsed) ? parsed : def;
|
||||
} catch {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
return def;
|
||||
}
|
||||
|
||||
if (def !== null && typeof def === "object") {
|
||||
if (
|
||||
v !== null &&
|
||||
typeof v === "object" &&
|
||||
!Array.isArray(v)
|
||||
) {
|
||||
return v;
|
||||
}
|
||||
|
||||
if (typeof v === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(v);
|
||||
|
||||
return (
|
||||
parsed !== null &&
|
||||
typeof parsed === "object" &&
|
||||
!Array.isArray(parsed)
|
||||
)
|
||||
? parsed
|
||||
: def;
|
||||
} catch {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
return def;
|
||||
}
|
||||
|
||||
return String(v);
|
||||
}
|
||||
|
||||
function __wireHtml(v: any): string {
|
||||
return String(v == null ? "" : v).replace(/[&<>]/g, (c) => (c === "&" ? "&" : c === "<" ? "<" : ">"));
|
||||
}
|
||||
function __wireAttr(v: any): string {
|
||||
return String(v == null ? "" : v).replace(/[&<>"]/g, (c) =>
|
||||
c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : """,
|
||||
return String(v == null ? "" : v).replace(
|
||||
/[&<>]/g,
|
||||
(c) =>
|
||||
c === "&"
|
||||
? "&"
|
||||
: c === "<"
|
||||
? "<"
|
||||
: ">",
|
||||
);
|
||||
}
|
||||
|
||||
function __wireAttr(v: any): string {
|
||||
return String(v == null ? "" : v).replace(
|
||||
/[&<>"]/g,
|
||||
(c) =>
|
||||
c === "&"
|
||||
? "&"
|
||||
: c === "<"
|
||||
? "<"
|
||||
: c === ">"
|
||||
? ">"
|
||||
: """,
|
||||
);
|
||||
}
|
||||
|
||||
function __wireProp(v: any): string {
|
||||
const value =
|
||||
v !== null && typeof v === "object"
|
||||
? JSON.stringify(v)
|
||||
: String(v == null ? "" : v);
|
||||
|
||||
return __wireAttr(value);
|
||||
}
|
||||
|
||||
function __wireRaw(v: any): string {
|
||||
return String(v == null ? "" : v);
|
||||
}`);
|
||||
if (needsScope) {
|
||||
out.push(`function __wrnexusScopeDecl(obj: Record<string, any>): string {
|
||||
@@ -1461,6 +1699,25 @@ function __wireAttr(v: any): string {
|
||||
`) + `
|
||||
`;
|
||||
}
|
||||
function wholeAttributeExpression(value) {
|
||||
const match = /^\s*\{([\s\S]+)\}\s*$/.exec(value);
|
||||
return match?.[1]?.trim() || null;
|
||||
}
|
||||
function renderPageComponentAttr(attr, dynamicExpressions) {
|
||||
if (attr.event) {
|
||||
return ` ${eventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
|
||||
}
|
||||
if (attr.boolean) {
|
||||
return ` ${attr.name}`;
|
||||
}
|
||||
const expression = wholeAttributeExpression(attr.value);
|
||||
if (!expression) {
|
||||
return ` ${attr.name}="${attrEscape(attr.value)}"`;
|
||||
}
|
||||
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);
|
||||
const marker = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
|
||||
return ` ${attr.name}="${marker}"`;
|
||||
}
|
||||
|
||||
// ../../packages/compiler/src/native-codegen.ts
|
||||
class NativeCompileError extends Error {
|
||||
|
||||
@@ -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",
|
||||
" <div></div>",
|
||||
" }",
|
||||
"}",
|
||||
].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,
|
||||
};
|
||||
|
||||
@@ -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)) {
|
||||
const commentEnd = source.indexOf("-->", 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)) {
|
||||
const end = source.indexOf("-->", 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)) {
|
||||
const end = source.indexOf("-->", 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)) {
|
||||
const end = source.indexOf("-->", 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,
|
||||
};
|
||||
|
||||
+587
-227
@@ -2,257 +2,617 @@
|
||||
"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 <slot> 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 registerFormatter(context) {
|
||||
const selector = {
|
||||
language: WRN_LANGUAGE_ID,
|
||||
};
|
||||
|
||||
const provider = vscode.languages.registerDocumentFormattingEditProvider(selector, {
|
||||
provideDocumentFormattingEdits(document, options, token) {
|
||||
if (token.isCancellationRequested) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const configuration = vscode.workspace.getConfiguration("wrnexus", document.uri);
|
||||
|
||||
const enabled = configuration.get("format.enable", true);
|
||||
|
||||
if (!enabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const source = document.getText();
|
||||
|
||||
if (!source.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const printWidth = configuration.get("formatting.printWidth", 100);
|
||||
|
||||
try {
|
||||
const formatted = formatWrn(source, {
|
||||
tabSize: options.tabSize || 2,
|
||||
insertSpaces: options.insertSpaces !== false,
|
||||
printWidth,
|
||||
});
|
||||
|
||||
if (typeof formatted !== "string" || formatted === source) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const fullRange = new vscode.Range(
|
||||
document.positionAt(0),
|
||||
document.positionAt(source.length),
|
||||
);
|
||||
|
||||
return [vscode.TextEdit.replace(fullRange, formatted)];
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
console.error("[wrnexus] formatting failed:", message);
|
||||
|
||||
vscode.window.showErrorMessage(`WRNexus formatting failed: ${message}`);
|
||||
|
||||
return [];
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
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.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();
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register semantic highlighting for WRN state declarations and references.
|
||||
*
|
||||
* @param {vscode.ExtensionContext} context
|
||||
*/
|
||||
function registerSemanticTokens(context) {
|
||||
const semanticTokenLegend = new vscode.SemanticTokensLegend(
|
||||
["variable"],
|
||||
["declaration", "modification"],
|
||||
);
|
||||
|
||||
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
|
||||
const collectStateVariables = (text) => {
|
||||
const states = new Map();
|
||||
|
||||
const pattern = /\bstate\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?==|;|\r?$)/gm;
|
||||
|
||||
let match;
|
||||
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
const name = match[1];
|
||||
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const offset = match.index + match[0].lastIndexOf(name);
|
||||
|
||||
const declarations = states.get(name) ?? new Set();
|
||||
|
||||
declarations.add(offset);
|
||||
states.set(name, declarations);
|
||||
}
|
||||
|
||||
return states;
|
||||
};
|
||||
|
||||
const isModification = (text, offset, length) => {
|
||||
const after = text.slice(offset + length).match(/^\s*(=|\+=|-=|\*=|\/=|%=|\+\+|--)/);
|
||||
|
||||
if (after) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const before = text.slice(Math.max(0, offset - 8), offset).match(/(\+\+|--)\s*$/);
|
||||
|
||||
return Boolean(before);
|
||||
};
|
||||
|
||||
const provider = {
|
||||
provideDocumentSemanticTokens(document, token) {
|
||||
const builder = new vscode.SemanticTokensBuilder(semanticTokenLegend);
|
||||
|
||||
const text = document.getText();
|
||||
const states = collectStateVariables(text);
|
||||
|
||||
for (const [name, declarationOffsets] of states) {
|
||||
if (token.isCancellationRequested) {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
const pattern = new RegExp(`(?<![A-Za-z0-9_$])${escapeRegExp(name)}(?![A-Za-z0-9_$])`, "g");
|
||||
|
||||
let match;
|
||||
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
const offset = match.index;
|
||||
const position = document.positionAt(offset);
|
||||
|
||||
let modifiers = [];
|
||||
|
||||
if (declarationOffsets.has(offset)) {
|
||||
modifiers = ["declaration"];
|
||||
} else if (isModification(text, offset, name.length)) {
|
||||
modifiers = ["modification"];
|
||||
}
|
||||
|
||||
builder.push(position.line, position.character, name.length, "variable", modifiers);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
},
|
||||
};
|
||||
|
||||
const registration = vscode.languages.registerDocumentSemanticTokensProvider(
|
||||
{
|
||||
language: WRN_LANGUAGE_ID,
|
||||
},
|
||||
provider,
|
||||
semanticTokenLegend,
|
||||
);
|
||||
|
||||
context.subscriptions.push(registration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate the WRNexus VS Code extension.
|
||||
*
|
||||
* @param {vscode.ExtensionContext} context
|
||||
*/
|
||||
function activate(context) {
|
||||
registerDiagnostics(context);
|
||||
registerCompilerDiagnostics(context);
|
||||
registerCompletionProvider(context);
|
||||
registerDefinitionProvider(context);
|
||||
registerFormatter(context);
|
||||
registerSemanticTokens(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", {
|
||||
provideDocumentFormattingEdits(document, options) {
|
||||
const source = document.getText();
|
||||
|
||||
const formatted = formatWrn(source, {
|
||||
tabSize: options.tabSize,
|
||||
insertSpaces: options.insertSpaces,
|
||||
printWidth: 100,
|
||||
});
|
||||
|
||||
const range = new vscode.Range(document.positionAt(0), document.positionAt(source.length));
|
||||
|
||||
return [vscode.TextEdit.replace(range, formatted)];
|
||||
vscode.languages.registerDocumentSemanticTokensProvider(
|
||||
{
|
||||
language: "wrn",
|
||||
},
|
||||
}),
|
||||
vscode.languages.registerCompletionItemProvider(
|
||||
"wrn",
|
||||
{ provideCompletionItems: provideCompletions },
|
||||
"-",
|
||||
"@",
|
||||
"{",
|
||||
":",
|
||||
wrnSemanticTokensProvider,
|
||||
semanticTokenLegend,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a thrown ParseError to a VS Code diagnostic. The compiler encodes the
|
||||
* failure position as `... at offset <N>` 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}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 `<name> { ... }` 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;
|
||||
}
|
||||
|
||||
function deactivate() {}
|
||||
|
||||
module.exports = { activate, deactivate };
|
||||
const semanticTokenLegend = new vscode.SemanticTokensLegend(
|
||||
["variable"],
|
||||
["declaration", "modification"],
|
||||
);
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @returns {string}
|
||||
*/
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @returns {Map<string, Set<number>>}
|
||||
*/
|
||||
function collectStateVariables(text) {
|
||||
const states = new Map();
|
||||
const statePattern = /\bstate\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?==|;|\r?$)/gm;
|
||||
|
||||
let match;
|
||||
|
||||
while ((match = statePattern.exec(text)) !== null) {
|
||||
const name = match[1];
|
||||
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const offset = match.index + match[0].lastIndexOf(name);
|
||||
const declarations = states.get(name) || new Set();
|
||||
|
||||
declarations.add(offset);
|
||||
states.set(name, declarations);
|
||||
}
|
||||
|
||||
return states;
|
||||
}
|
||||
|
||||
/**
|
||||
* Comments are excluded, but quoted attribute expressions are deliberately
|
||||
* included because WRN state references commonly appear in values such as
|
||||
* class:flex="centered" and @click="enabled = !enabled".
|
||||
*
|
||||
* @param {string} text
|
||||
* @returns {Array<{start: number, end: number}>}
|
||||
*/
|
||||
function collectCommentRanges(text) {
|
||||
const ranges = [];
|
||||
let index = 0;
|
||||
let quote = null;
|
||||
let escaped = false;
|
||||
|
||||
while (index < text.length) {
|
||||
const current = text[index];
|
||||
const next = text[index + 1];
|
||||
|
||||
if (quote) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current === "\\") {
|
||||
escaped = true;
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current === quote) {
|
||||
quote = null;
|
||||
}
|
||||
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current === '"' || current === "'" || current === "`") {
|
||||
quote = current;
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current === "/" && next === "/") {
|
||||
const start = index;
|
||||
index += 2;
|
||||
|
||||
while (index < text.length && text[index] !== "\n") {
|
||||
index++;
|
||||
}
|
||||
|
||||
ranges.push({ start, end: index });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current === "/" && next === "*") {
|
||||
const start = index;
|
||||
index += 2;
|
||||
|
||||
while (index < text.length && !(text[index] === "*" && text[index + 1] === "/")) {
|
||||
index++;
|
||||
}
|
||||
|
||||
index = Math.min(text.length, index + 2);
|
||||
ranges.push({ start, end: index });
|
||||
continue;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @param {Array<{start: number, end: number}>} ranges
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isInsideComment(offset, ranges) {
|
||||
let low = 0;
|
||||
let high = ranges.length - 1;
|
||||
|
||||
while (low <= high) {
|
||||
const middle = Math.floor((low + high) / 2);
|
||||
const range = ranges[middle];
|
||||
|
||||
if (!range) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (offset < range.start) {
|
||||
high = middle - 1;
|
||||
} else if (offset >= range.end) {
|
||||
low = middle + 1;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {number} offset
|
||||
* @param {number} length
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isStateModification(text, offset, length) {
|
||||
const after = text.slice(offset + length).match(/^\s*(=|\+=|-=|\*=|\/=|%=|\+\+|--)/);
|
||||
|
||||
if (after) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const before = text.slice(Math.max(0, offset - 8), offset).match(/(\+\+|--)\s*$/);
|
||||
|
||||
return Boolean(before);
|
||||
}
|
||||
|
||||
const wrnSemanticTokensProvider = {
|
||||
/**
|
||||
* @param {vscode.TextDocument} document
|
||||
* @param {vscode.CancellationToken} token
|
||||
*/
|
||||
provideDocumentSemanticTokens(document, token) {
|
||||
const builder = new vscode.SemanticTokensBuilder(semanticTokenLegend);
|
||||
const text = document.getText();
|
||||
const states = collectStateVariables(text);
|
||||
const commentRanges = collectCommentRanges(text);
|
||||
|
||||
for (const [name, declarationOffsets] of states) {
|
||||
if (token.isCancellationRequested) {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
const pattern = new RegExp(`(?<![A-Za-z0-9_$])${escapeRegExp(name)}(?![A-Za-z0-9_$])`, "g");
|
||||
|
||||
let match;
|
||||
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
const offset = match.index;
|
||||
|
||||
if (isInsideComment(offset, commentRanges)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const position = document.positionAt(offset);
|
||||
let modifiers = [];
|
||||
|
||||
if (declarationOffsets.has(offset)) {
|
||||
modifiers = ["declaration"];
|
||||
} else if (isStateModification(text, offset, name.length)) {
|
||||
modifiers = ["modification"];
|
||||
}
|
||||
|
||||
builder.push(position.line, position.character, name.length, "variable", modifiers);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
activate,
|
||||
deactivate,
|
||||
registerCompilerDiagnostics,
|
||||
registerFormatter,
|
||||
registerSemanticTokens,
|
||||
toCompilerDiagnostic,
|
||||
};
|
||||
|
||||
+163
-45
@@ -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 === `</${tagName}>`;
|
||||
|
||||
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 = true;
|
||||
index += 3;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (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("</") &&
|
||||
!value.startsWith("<!--") &&
|
||||
!value.startsWith("<!") &&
|
||||
!value.startsWith("<?") &&
|
||||
!isInlineElement(value)
|
||||
) {
|
||||
const formattedTag = formatOpeningTag(value, unit, depth, printWidth);
|
||||
@@ -256,10 +364,17 @@ function formatWrn(source, options = {}) {
|
||||
output.push(`${unit.repeat(depth)}${value}`);
|
||||
}
|
||||
|
||||
if (isWrnBlockOpening(value)) {
|
||||
wrnDepth += 1;
|
||||
if (isClosingTag(value)) {
|
||||
htmlDepth = lineHtmlDepth;
|
||||
}
|
||||
|
||||
const braces = countStructuralBraces(value);
|
||||
|
||||
codeDepth = Math.max(
|
||||
0,
|
||||
lineCodeDepth + braces.openings - Math.max(0, braces.closings - leadingClosingBraces),
|
||||
);
|
||||
|
||||
index += 1;
|
||||
}
|
||||
|
||||
@@ -271,6 +386,9 @@ function formatWrn(source, options = {}) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
countStructuralBraces,
|
||||
formatOpeningTag,
|
||||
formatWrn,
|
||||
parseAttributes,
|
||||
parseOpeningTag,
|
||||
};
|
||||
|
||||
@@ -1,266 +1,651 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
|
||||
"name": "Wire",
|
||||
"name": "WRNexus",
|
||||
"scopeName": "source.wrn",
|
||||
"patterns": [{ "include": "#comments" }, { "include": "#declaration" }, { "include": "#blocks" }],
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#comments"
|
||||
},
|
||||
{
|
||||
"include": "#declaration"
|
||||
},
|
||||
{
|
||||
"include": "#blocks"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"comments": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "comment.line.double-slash.wrn",
|
||||
"match": "//.*$"
|
||||
},
|
||||
{
|
||||
"name": "comment.block.html.wrn",
|
||||
"begin": "<!--",
|
||||
"end": "-->"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"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": "#conditional-class-single"
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
"name": "meta.attribute.class-directive.wrn",
|
||||
"begin": "(?<=\\s)(class)(:)((?:\\[[^\\]\\r\\n]*\\]|[^\\s=])+)(\\s*=\\s*)(\")",
|
||||
"beginCaptures": {
|
||||
"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"
|
||||
},
|
||||
"5": {
|
||||
"name": "punctuation.definition.string.begin.wrn"
|
||||
}
|
||||
},
|
||||
"conditional-class-single": {
|
||||
"name": "meta.attribute.class-directive.wrn",
|
||||
"begin": "(?<=\\s)(class)(:)((?:\\[[^\\]\\r\\n]*\\]|[^\\s=])+)(\\s*=\\s*)(')",
|
||||
"beginCaptures": {
|
||||
"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"
|
||||
},
|
||||
"5": {
|
||||
"name": "punctuation.definition.string.begin.wrn"
|
||||
}
|
||||
},
|
||||
"end": "'",
|
||||
"endCaptures": {
|
||||
"0": {
|
||||
"name": "punctuation.definition.string.end.wrn"
|
||||
}
|
||||
},
|
||||
"contentName": "meta.embedded.expression.wrn",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "source.ts"
|
||||
}
|
||||
]
|
||||
},
|
||||
"end": "\"",
|
||||
"endCaptures": {
|
||||
"0": {
|
||||
"name": "punctuation.definition.string.end.wrn"
|
||||
}
|
||||
},
|
||||
"contentName": "meta.embedded.expression.wrn",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "source.ts"
|
||||
}
|
||||
]
|
||||
},
|
||||
"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 +653,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": "\\\\."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ai",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/authz",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/cli",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -357,6 +357,105 @@ 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.
|
||||
},
|
||||
},
|
||||
{
|
||||
version: "0.2.33",
|
||||
id: "fix-runtime-bug",
|
||||
description:
|
||||
"Fixes safe encoding and browser parsing of multiline component behavior metadata.",
|
||||
apply() {
|
||||
// No generated application files require automatic migration.
|
||||
},
|
||||
},
|
||||
{
|
||||
version: "0.2.34",
|
||||
id: "reactive-bug",
|
||||
description:
|
||||
"Fixes component behavior encoding, lifecycle and watcher metadata parsing, CSR cleanup, and reactive form control property updates.",
|
||||
apply() {
|
||||
// No generated application files require automatic migration.
|
||||
},
|
||||
},
|
||||
{
|
||||
version: "0.2.35",
|
||||
id: "class-bug",
|
||||
description:
|
||||
"Fixes component behavior encoding, lifecycle and watcher metadata parsing, CSR cleanup, and reactive form control property updates.",
|
||||
apply() {
|
||||
// No generated application files require automatic migration.
|
||||
},
|
||||
},
|
||||
{
|
||||
version: "0.2.36",
|
||||
id: "attribute-bug",
|
||||
description:
|
||||
"Adds support for Tailwind arbitrary-value class directives and improves reactive class bindings.",
|
||||
apply() {
|
||||
// No generated application files require automatic migration.
|
||||
},
|
||||
},
|
||||
{
|
||||
version: "0.2.37",
|
||||
id: "loop-condition-fix",
|
||||
description:
|
||||
"Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
|
||||
apply() {
|
||||
// No generated application files require automatic migration.
|
||||
},
|
||||
},
|
||||
{
|
||||
version: "0.2.38",
|
||||
id: "loop-condition-bug",
|
||||
description:
|
||||
"Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
|
||||
apply() {
|
||||
// No generated application files require automatic migration.
|
||||
},
|
||||
},
|
||||
{
|
||||
version: "0.2.39",
|
||||
id: "server-function-bug",
|
||||
description:
|
||||
"Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
|
||||
apply() {
|
||||
// No generated application files require automatic migration.
|
||||
},
|
||||
},
|
||||
{
|
||||
version: "0.2.40",
|
||||
id: "conditions-bug",
|
||||
description:
|
||||
"Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
|
||||
apply() {
|
||||
// No generated application files require automatic migration.
|
||||
},
|
||||
},
|
||||
{
|
||||
version: "0.2.41",
|
||||
id: "conditions-bug",
|
||||
description:
|
||||
"Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
|
||||
apply() {
|
||||
// No generated application files require automatic migration.
|
||||
},
|
||||
},
|
||||
{
|
||||
version: "0.2.42",
|
||||
id: "conditions-bug",
|
||||
description:
|
||||
"Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
|
||||
apply() {
|
||||
// No generated application files require automatic migration.
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** Release tooling uses this to require an explicit migration entry per version. */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/compiler",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
* realtime {..} -> export const websocket = { evt(ws, ...args) { b } }
|
||||
*/
|
||||
|
||||
import { Buffer } from "node:buffer";
|
||||
|
||||
import { VOID_ELEMENTS, type Attr, type DataMode, type PageAst, type ViewNode } from "./parser.ts";
|
||||
|
||||
interface RenderBinding {
|
||||
@@ -428,17 +430,16 @@ function renderPageComponentInvocation(
|
||||
loops: string[],
|
||||
reactive: PageReactive | null,
|
||||
): string {
|
||||
const attrs = node.attrs.filter((attr) => attr.name !== "data-component");
|
||||
const attrs = node.attrs
|
||||
.filter((attr) => attr.name !== "data-component")
|
||||
.map((attr) => renderPageComponentAttr(attr, loops))
|
||||
.join("");
|
||||
|
||||
const inner = node.children
|
||||
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
|
||||
.join("");
|
||||
|
||||
return `<div data-component="${attrEscape(node.tag)}"${renderAttrs(
|
||||
attrs,
|
||||
undefined,
|
||||
reactive,
|
||||
)}>${inner}</div>`;
|
||||
return `<div data-component="${attrEscape(node.tag)}"` + `${attrs}>${inner}</div>`;
|
||||
}
|
||||
|
||||
function renderNestedComponentInvocation(
|
||||
@@ -460,9 +461,19 @@ function renderNestedComponentInvocation(
|
||||
return ` ${attr.name}`;
|
||||
}
|
||||
|
||||
const rendered = ` ${attr.name}="` + `${compileAttrValue(attr.value, ctx)}"`;
|
||||
const wholeExpression = wholeAttributeExpression(attr.value);
|
||||
|
||||
if (!attr.value.includes("{") || !exprRefsState(attr.value, ctx.stateNames)) {
|
||||
const compiledValue = wholeExpression
|
||||
? `\${__wireProp(${ctx.resolveExpr(wholeExpression)})}`
|
||||
: compileAttrValue(attr.value, ctx);
|
||||
|
||||
const rendered = ` ${attr.name}="${compiledValue}"`;
|
||||
|
||||
if (
|
||||
wholeExpression ||
|
||||
!attr.value.includes("{") ||
|
||||
!exprRefsState(attr.value, ctx.stateNames)
|
||||
) {
|
||||
return rendered;
|
||||
}
|
||||
|
||||
@@ -589,6 +600,28 @@ function __wrnexusEvalData(data: unknown, body: string, helpers = "", ctx: any):
|
||||
return new Function("$data", "$adapters", "const cookies = $adapters.cookies;\\nconst session = $adapters.session;\\nconst localStorage = $adapters.localStorage;\\nwith ($data ?? {}) {\\n" + helpers + "\\n" + body + "\\n}")(data, adapters);
|
||||
}
|
||||
|
||||
function __wrnexusPropAttr(
|
||||
value: unknown,
|
||||
): string {
|
||||
const serialized =
|
||||
value !== null &&
|
||||
typeof value === "object"
|
||||
? JSON.stringify(value)
|
||||
: String(value == null ? "" : value);
|
||||
|
||||
return serialized.replace(
|
||||
/[&<>"]/g,
|
||||
(character) =>
|
||||
character === "&"
|
||||
? "&"
|
||||
: character === "<"
|
||||
? "<"
|
||||
: character === ">"
|
||||
? ">"
|
||||
: """,
|
||||
);
|
||||
}
|
||||
|
||||
async function __wrnexusCallApi(path: string, method: string, ctx: any): Promise<unknown> {
|
||||
if (typeof ctx.__wrnexusCallApi === "function") {
|
||||
return await ctx.__wrnexusCallApi(path, method);
|
||||
@@ -804,6 +837,8 @@ interface CompCtx {
|
||||
/** `data-for` loop variables in scope — their mustaches stay literal for the
|
||||
* client's list renderer (never baked server-side, since they have no value). */
|
||||
loopVars?: Set<string>;
|
||||
/** Local identifiers introduced by server-rendered `{#each}` blocks. */
|
||||
serverLocals?: Set<string>;
|
||||
}
|
||||
|
||||
interface ComponentBehavior {
|
||||
@@ -925,7 +960,9 @@ function behaviorAttribute(behavior: ComponentBehavior | null): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
return ` data-wrn-behavior="${attrEscape(JSON.stringify(behavior))}"`;
|
||||
const encoded = Buffer.from(JSON.stringify(behavior), "utf8").toString("base64");
|
||||
|
||||
return ` data-wrn-behavior="${encoded}"`;
|
||||
}
|
||||
|
||||
const INTERP_RE = /\{([^{}]+)\}/g;
|
||||
@@ -938,9 +975,19 @@ function exprRefsState(expr: string, stateNames: Set<string>): boolean {
|
||||
}
|
||||
|
||||
function viewHasEvents(nodes: ViewNode[]): boolean {
|
||||
return nodes.some(
|
||||
(n) => n.type === "element" && (n.attrs.some((a) => a.event) || viewHasEvents(n.children)),
|
||||
);
|
||||
return nodes.some((node) => {
|
||||
if (node.type === "text") return false;
|
||||
|
||||
if (node.type === "each") {
|
||||
return viewHasEvents(node.body) || viewHasEvents(node.empty);
|
||||
}
|
||||
|
||||
if (node.type === "if") {
|
||||
return node.branches.some((branch) => viewHasEvents(branch.body));
|
||||
}
|
||||
|
||||
return node.attrs.some((attr) => attr.event) || viewHasEvents(node.children);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1002,14 +1049,65 @@ function compileAttrValue(raw: string, ctx: CompCtx): string {
|
||||
return out + escLit(attrEscape(raw.slice(last)));
|
||||
}
|
||||
|
||||
function renderComponentIfNode(node: IfNode, ctx: CompCtx): string {
|
||||
let expression = "``";
|
||||
|
||||
for (let index = node.branches.length - 1; index >= 0; index--) {
|
||||
const branch = node.branches[index]!;
|
||||
const body = branch.body.map((child) => renderComponentNode(child, ctx)).join("");
|
||||
const bodyExpression = "`" + body + "`";
|
||||
|
||||
expression =
|
||||
branch.cond === null
|
||||
? bodyExpression
|
||||
: `(${ctx.resolveExpr(branch.cond)}) ? ${bodyExpression} : ${expression}`;
|
||||
}
|
||||
|
||||
return "${" + expression + "}";
|
||||
}
|
||||
|
||||
function renderComponentEachNode(node: EachNode, ctx: CompCtx): string {
|
||||
const item = node.item;
|
||||
const index = node.index ?? "__wi";
|
||||
const list = ctx.resolveExpr(node.list);
|
||||
|
||||
const childCtx: CompCtx = {
|
||||
...ctx,
|
||||
serverLocals: new Set([...(ctx.serverLocals ?? []), item, index]),
|
||||
};
|
||||
|
||||
const body = node.body.map((child) => renderComponentNode(child, childCtx)).join("");
|
||||
const empty = node.empty.map((child) => renderComponentNode(child, ctx)).join("");
|
||||
|
||||
return (
|
||||
"${(() => { const __wl = Array.isArray(" +
|
||||
list +
|
||||
") ? (" +
|
||||
list +
|
||||
") : []; return __wl.length ? __wl.map((" +
|
||||
item +
|
||||
", " +
|
||||
index +
|
||||
") => `" +
|
||||
body +
|
||||
'`).join("") : `' +
|
||||
empty +
|
||||
"`; })()}"
|
||||
);
|
||||
}
|
||||
|
||||
/** Render a component view node into template-literal-ready source. */
|
||||
function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
|
||||
if (node.type === "text") return compileText(node.value, ctx);
|
||||
if (node.type === "each" || node.type === "if") {
|
||||
throw new Error(
|
||||
"Server `{#each}` / `{#if}` blocks are supported in pages, not components. Move them into a page (or use data-for / data-show on the client).",
|
||||
);
|
||||
|
||||
if (node.type === "each") {
|
||||
return renderComponentEachNode(node, ctx);
|
||||
}
|
||||
|
||||
if (node.type === "if") {
|
||||
return renderComponentIfNode(node, ctx);
|
||||
}
|
||||
|
||||
if (isComponentTag(node.tag)) {
|
||||
return renderNestedComponentInvocation(node, ctx);
|
||||
}
|
||||
@@ -1063,12 +1161,24 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
|
||||
})
|
||||
.join("");
|
||||
|
||||
const staticClassValue = staticClasses.join(" ");
|
||||
|
||||
const classHasReactiveExpression =
|
||||
staticClassValue.includes("{") && exprRefsState(staticClassValue, ctx.stateNames);
|
||||
|
||||
const classAttribute =
|
||||
staticClasses.length > 0 || conditionalClasses.length > 0
|
||||
? ` class="${compileAttrValue(staticClasses.join(" "), ctx)}${initialConditionalClasses}"`
|
||||
? ` class="${compileAttrValue(staticClassValue, ctx)}${initialConditionalClasses}"`
|
||||
: "";
|
||||
|
||||
const classReactiveBinding = classHasReactiveExpression
|
||||
? ` data-wrn-bind-class="${escLit(attrEscape(JSON.stringify(["class", staticClassValue])))}"`
|
||||
: "";
|
||||
|
||||
const classBindings = conditionalClasses
|
||||
.filter(({ expression }) => {
|
||||
return !ctx.serverLocals || !exprRefsState(expression, ctx.serverLocals);
|
||||
})
|
||||
.map(({ className, expression }, index) => {
|
||||
const marker = attrEscape(JSON.stringify([className, expression]));
|
||||
|
||||
@@ -1081,7 +1191,8 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
|
||||
const childCtx =
|
||||
loops.length > 0 ? { ...ctx, loopVars: new Set([...(ctx.loopVars ?? []), ...loops]) } : ctx;
|
||||
|
||||
const allAttrs = `${classAttribute}${classBindings}${attrs}`;
|
||||
const allAttrs =
|
||||
`${classAttribute}` + `${classReactiveBinding}` + `${classBindings}` + `${attrs}`;
|
||||
|
||||
if (VOID_ELEMENTS.has(node.tag.toLowerCase())) {
|
||||
return `<${node.tag}${allAttrs}>`;
|
||||
@@ -1121,6 +1232,11 @@ function generateComponent(ast: PageAst): string {
|
||||
};
|
||||
const ctx: CompCtx = { stateNames, resolveExpr };
|
||||
|
||||
const serverFunctions = ast.functions
|
||||
.map((body) => body.trim())
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
|
||||
const viewCode = ast.view.map((node) => renderComponentNode(node, ctx)).join("");
|
||||
const styles = ast.styles.map((body) => body.trim()).filter(Boolean);
|
||||
const styleTag =
|
||||
@@ -1151,7 +1267,7 @@ function generateComponent(ast: PageAst): string {
|
||||
);
|
||||
}
|
||||
for (const state of ast.states) {
|
||||
decls.push(` const ${nameRefs.get(state.name)} = (${resolveExpr(state.expr)});`);
|
||||
decls.push(` let ${nameRefs.get(state.name)} = (${resolveExpr(state.expr)});`);
|
||||
}
|
||||
|
||||
const returnExpr = needsScope
|
||||
@@ -1176,18 +1292,103 @@ function generateComponent(ast: PageAst): string {
|
||||
}
|
||||
|
||||
out.push(`function __coerce(v: any, def: any): any {
|
||||
if (v === undefined || v === null) return def;
|
||||
if (typeof def === "number") return Number(v);
|
||||
if (typeof def === "boolean") return v === true || v === "" || v === "true";
|
||||
if (v === undefined || v === null) {
|
||||
return def;
|
||||
}
|
||||
|
||||
if (typeof def === "number") {
|
||||
return Number(v);
|
||||
}
|
||||
|
||||
if (typeof def === "boolean") {
|
||||
return v === true || v === "" || v === "true";
|
||||
}
|
||||
|
||||
if (Array.isArray(def)) {
|
||||
if (Array.isArray(v)) {
|
||||
return v;
|
||||
}
|
||||
|
||||
if (typeof v === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(v);
|
||||
return Array.isArray(parsed) ? parsed : def;
|
||||
} catch {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
return def;
|
||||
}
|
||||
|
||||
if (def !== null && typeof def === "object") {
|
||||
if (
|
||||
v !== null &&
|
||||
typeof v === "object" &&
|
||||
!Array.isArray(v)
|
||||
) {
|
||||
return v;
|
||||
}
|
||||
|
||||
if (typeof v === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(v);
|
||||
|
||||
return (
|
||||
parsed !== null &&
|
||||
typeof parsed === "object" &&
|
||||
!Array.isArray(parsed)
|
||||
)
|
||||
? parsed
|
||||
: def;
|
||||
} catch {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
return def;
|
||||
}
|
||||
|
||||
return String(v);
|
||||
}
|
||||
|
||||
function __wireHtml(v: any): string {
|
||||
return String(v == null ? "" : v).replace(/[&<>]/g, (c) => (c === "&" ? "&" : c === "<" ? "<" : ">"));
|
||||
}
|
||||
function __wireAttr(v: any): string {
|
||||
return String(v == null ? "" : v).replace(/[&<>"]/g, (c) =>
|
||||
c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : """,
|
||||
return String(v == null ? "" : v).replace(
|
||||
/[&<>]/g,
|
||||
(c) =>
|
||||
c === "&"
|
||||
? "&"
|
||||
: c === "<"
|
||||
? "<"
|
||||
: ">",
|
||||
);
|
||||
}
|
||||
|
||||
function __wireAttr(v: any): string {
|
||||
return String(v == null ? "" : v).replace(
|
||||
/[&<>"]/g,
|
||||
(c) =>
|
||||
c === "&"
|
||||
? "&"
|
||||
: c === "<"
|
||||
? "<"
|
||||
: c === ">"
|
||||
? ">"
|
||||
: """,
|
||||
);
|
||||
}
|
||||
|
||||
function __wireProp(v: any): string {
|
||||
const value =
|
||||
v !== null && typeof v === "object"
|
||||
? JSON.stringify(v)
|
||||
: String(v == null ? "" : v);
|
||||
|
||||
return __wireAttr(value);
|
||||
}
|
||||
|
||||
function __wireRaw(v: any): string {
|
||||
return String(v == null ? "" : v);
|
||||
}`);
|
||||
|
||||
if (needsScope) {
|
||||
@@ -1206,10 +1407,13 @@ function __wireAttr(v: any): string {
|
||||
}`);
|
||||
}
|
||||
|
||||
const serverFunctionSource = serverFunctions ? `${serverFunctions}\n` : "";
|
||||
|
||||
out.push(
|
||||
`export function render(props: Record<string, any> = {}): string {\n` +
|
||||
` const __p = props || {};\n` +
|
||||
(decls.length > 0 ? decls.join("\n") + "\n" : "") +
|
||||
serverFunctionSource +
|
||||
scopeLine +
|
||||
` return ${returnExpr};\n` +
|
||||
`}`,
|
||||
@@ -1218,12 +1422,51 @@ function __wireAttr(v: any): string {
|
||||
return out.join("\n\n") + "\n";
|
||||
}
|
||||
|
||||
function __wireRaw(v: any): string {
|
||||
return String(v == null ? "" : v);
|
||||
}
|
||||
|
||||
function wholeAttributeExpression(value: string): string | null {
|
||||
const match = /^\s*\{([\s\S]+)\}\s*$/.exec(value);
|
||||
|
||||
return match?.[1]?.trim() || null;
|
||||
}
|
||||
function __wireHtml(v: any): string {
|
||||
return String(v == null ? "" : v).replace(/[&<>]/g, (c) =>
|
||||
c === "&" ? "&" : c === "<" ? "<" : ">",
|
||||
);
|
||||
}
|
||||
|
||||
function __wireRaw(v: any): string {
|
||||
return String(v == null ? "" : v);
|
||||
function __wireAttr(v: any): string {
|
||||
return String(v == null ? "" : v).replace(/[&<>"]/g, (c) =>
|
||||
c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : """,
|
||||
);
|
||||
}
|
||||
|
||||
function __wireProp(v: any): string {
|
||||
const value =
|
||||
v !== null && typeof v === "object" ? JSON.stringify(v) : String(v == null ? "" : v);
|
||||
|
||||
return __wireAttr(value);
|
||||
}
|
||||
function renderPageComponentAttr(attr: Attr, dynamicExpressions: string[]): string {
|
||||
if (attr.event) {
|
||||
return ` ${eventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
|
||||
}
|
||||
|
||||
if (attr.boolean) {
|
||||
return ` ${attr.name}`;
|
||||
}
|
||||
|
||||
const expression = wholeAttributeExpression(attr.value);
|
||||
|
||||
if (!expression) {
|
||||
return ` ${attr.name}="${attrEscape(attr.value)}"`;
|
||||
}
|
||||
|
||||
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);
|
||||
|
||||
const marker = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
|
||||
|
||||
return ` ${attr.name}="${marker}"`;
|
||||
}
|
||||
|
||||
@@ -469,13 +469,6 @@ export function parseHtmlView(src: string, pos: number): { nodes: ViewNode[]; en
|
||||
|
||||
const isTagNamePart = (c: string): boolean => /[A-Za-z0-9_$:.-]/.test(c);
|
||||
|
||||
const isAttributeNamePart = (c: string, next: string | undefined): boolean => {
|
||||
if (c === "/") {
|
||||
return next !== ">";
|
||||
}
|
||||
|
||||
return /[A-Za-z0-9_$:.[\]%-]/.test(c);
|
||||
};
|
||||
const isWs = (c: string): boolean => c === " " || c === "\t" || c === "\n" || c === "\r";
|
||||
|
||||
const fail = (msg: string): never => {
|
||||
@@ -526,18 +519,37 @@ export function parseHtmlView(src: string, pos: number): { nodes: ViewNode[]; en
|
||||
};
|
||||
|
||||
const readAttributeName = (): string => {
|
||||
const first = src[i];
|
||||
|
||||
if (i >= src.length || (!isNameStart(first!) && first !== ":" && first !== "$")) {
|
||||
if (i >= src.length) {
|
||||
return fail("Expected an attribute name");
|
||||
}
|
||||
|
||||
const start = i++;
|
||||
const start = i;
|
||||
|
||||
while (i < src.length) {
|
||||
const char = src[i];
|
||||
const next = src[i + 1];
|
||||
|
||||
if (
|
||||
char === "=" ||
|
||||
char === ">" ||
|
||||
char === '"' ||
|
||||
char === "'" ||
|
||||
char === " " ||
|
||||
char === "\t" ||
|
||||
char === "\n" ||
|
||||
char === "\r" ||
|
||||
(char === "/" && next === ">")
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
while (i < src.length && isAttributeNamePart(src[i]!, src[i + 1])) {
|
||||
i++;
|
||||
}
|
||||
|
||||
if (i === start) {
|
||||
return fail("Expected an attribute name");
|
||||
}
|
||||
|
||||
return src.slice(start, i);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { generate, parse } from "../src/index.ts";
|
||||
import { compileWireFile } from "../src/index.ts";
|
||||
import { mountHtml } from "@wrnexus/test";
|
||||
|
||||
let seq = 0;
|
||||
/** Compile a `.wrn` source and import the resulting module. */
|
||||
@@ -16,6 +17,39 @@ async function compileAndImport(src: string): Promise<Record<string, unknown>> {
|
||||
return import(pathToFileURL(file).href);
|
||||
}
|
||||
|
||||
type CompiledBehavior = {
|
||||
functions: string;
|
||||
lifecycle: {
|
||||
mount?: string;
|
||||
update?: string;
|
||||
unmount?: string;
|
||||
};
|
||||
watches: Array<{
|
||||
state: string;
|
||||
body: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
function extractCompiledBehavior(output: string): CompiledBehavior {
|
||||
const marker = 'data-wrn-behavior="';
|
||||
|
||||
const start = output.indexOf(marker);
|
||||
|
||||
expect(start).toBeGreaterThanOrEqual(0);
|
||||
|
||||
const valueStart = start + marker.length;
|
||||
|
||||
const valueEnd = output.indexOf('"', valueStart);
|
||||
|
||||
expect(valueEnd).toBeGreaterThan(valueStart);
|
||||
|
||||
const encoded = output.slice(valueStart, valueEnd);
|
||||
|
||||
expect(encoded).toMatch(/^[A-Za-z0-9+/]+=*$/);
|
||||
|
||||
return JSON.parse(Buffer.from(encoded, "base64").toString("utf8")) as CompiledBehavior;
|
||||
}
|
||||
|
||||
test("parses a page with a layout member", () => {
|
||||
const ast = parse(`page Home {\n layout = "public"\n view { <h1>Hi</h1> }\n}`);
|
||||
expect(ast.kind).toBe("page");
|
||||
@@ -210,7 +244,9 @@ component ParentCard {
|
||||
|
||||
expect(output).toContain('data-component="ChildCard"');
|
||||
|
||||
expect(output).toContain('title="${__wireAttr(title)}"');
|
||||
expect(output).toContain('title="${__wireProp(title)}"');
|
||||
|
||||
expect(output).toContain("function __wireProp");
|
||||
});
|
||||
|
||||
test("parses a layout block", () => {
|
||||
@@ -382,11 +418,13 @@ component LifecycleTest {
|
||||
`),
|
||||
);
|
||||
|
||||
expect(output).toContain(""mount"");
|
||||
const behavior = extractCompiledBehavior(output);
|
||||
|
||||
expect(output).toContain(""update"");
|
||||
expect(behavior.lifecycle.mount).toContain("ready = true");
|
||||
|
||||
expect(output).toContain(""unmount"");
|
||||
expect(behavior.lifecycle.update).toContain("console.log(ready)");
|
||||
|
||||
expect(behavior.lifecycle.unmount).toContain("ready = false");
|
||||
});
|
||||
test("component watchers compile into behavior metadata", () => {
|
||||
const output = generate(
|
||||
@@ -405,9 +443,13 @@ component WatchTest {
|
||||
`),
|
||||
);
|
||||
|
||||
expect(output).toContain(""state":"visible"");
|
||||
const behavior = extractCompiledBehavior(output);
|
||||
|
||||
expect(output).toContain("console.log");
|
||||
expect(behavior.watches).toHaveLength(1);
|
||||
|
||||
expect(behavior.watches[0]?.state).toBe("visible");
|
||||
|
||||
expect(behavior.watches[0]?.body).toContain('console.log("changed", visible)');
|
||||
});
|
||||
test("behavior-only components still receive a reactive scope", () => {
|
||||
const output = generate(
|
||||
@@ -436,3 +478,364 @@ 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 {
|
||||
<button @click="scrollToTop()">
|
||||
Top
|
||||
</button>
|
||||
}
|
||||
}
|
||||
`),
|
||||
);
|
||||
|
||||
expect(output).toContain("data-wrn-behavior=");
|
||||
|
||||
expect(output).not.toContain('data-wrn-behavior="{');
|
||||
|
||||
expect(output).not.toContain('data-wrn-behavior="%7B');
|
||||
});
|
||||
|
||||
test("component watchers compile into behavior metadata", () => {
|
||||
const output = generate(
|
||||
parse(`
|
||||
component WatchTest {
|
||||
state visible = false
|
||||
|
||||
watch visible {
|
||||
console.log("changed", visible)
|
||||
}
|
||||
|
||||
view {
|
||||
<div>{visible}</div>
|
||||
}
|
||||
}
|
||||
`),
|
||||
);
|
||||
|
||||
const match = /data-wrn-behavior="([^"]+)"/.exec(output);
|
||||
|
||||
expect(match).not.toBeNull();
|
||||
|
||||
const behavior = extractBehavior(output);
|
||||
|
||||
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 {
|
||||
<div>{ready}</div>
|
||||
}
|
||||
}
|
||||
`),
|
||||
);
|
||||
|
||||
const match = /data-wrn-behavior="([^"]+)"/.exec(output);
|
||||
|
||||
expect(match).not.toBeNull();
|
||||
|
||||
const behavior = extractBehavior(output);
|
||||
|
||||
expect(behavior.lifecycle.mount).toContain("ready = true");
|
||||
|
||||
expect(behavior.lifecycle.update).toContain("console.log(ready)");
|
||||
|
||||
expect(behavior.lifecycle.unmount).toContain("ready = false");
|
||||
});
|
||||
function extractBehavior(output: string): {
|
||||
functions: string;
|
||||
lifecycle: {
|
||||
mount?: string;
|
||||
update?: string;
|
||||
unmount?: string;
|
||||
};
|
||||
watches: Array<{
|
||||
state: string;
|
||||
body: string;
|
||||
}>;
|
||||
} {
|
||||
const match = /data-wrn-behavior="([^"]+)"/.exec(output);
|
||||
|
||||
expect(match).not.toBeNull();
|
||||
|
||||
return JSON.parse(Buffer.from(match![1], "base64").toString("utf8"));
|
||||
}
|
||||
test("component interpolated class attributes remain reactive", () => {
|
||||
const output = generate(
|
||||
parse(`
|
||||
component CookieToggle {
|
||||
state personalizationCookies = false
|
||||
|
||||
view {
|
||||
<span
|
||||
class="base {personalizationCookies ? 'enabled' : 'disabled'}"
|
||||
>
|
||||
Cookie
|
||||
</span>
|
||||
}
|
||||
}
|
||||
`),
|
||||
);
|
||||
|
||||
const match = output.match(/data-wrn-bind-class="([^"]+)"/);
|
||||
|
||||
expect(match).not.toBeNull();
|
||||
|
||||
const encodedBinding = match?.[1] ?? "";
|
||||
|
||||
const decodedBinding = encodedBinding
|
||||
.replaceAll(""", '"')
|
||||
.replaceAll("'", "'")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">");
|
||||
|
||||
const binding = JSON.parse(decodedBinding) as [string, string];
|
||||
|
||||
expect(binding[0]).toBe("class");
|
||||
|
||||
expect(binding[1]).toBe("base {personalizationCookies ? 'enabled' : 'disabled'}");
|
||||
});
|
||||
test("parses class directives containing Tailwind arbitrary values", () => {
|
||||
const ast = parse(`
|
||||
component PageHeader {
|
||||
state centered = false
|
||||
|
||||
view {
|
||||
<div
|
||||
class="py-12"
|
||||
class:lg:grid-cols-[minmax(0,1fr)_auto]="!centered"
|
||||
class:dark:bg-white/10="centered"
|
||||
class:w-[calc(100%-2rem)]="centered"
|
||||
>
|
||||
Header
|
||||
</div>
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
expect(ast).toBeDefined();
|
||||
});
|
||||
test("compiles Tailwind arbitrary values in class directives", () => {
|
||||
const output = generate(
|
||||
parse(`
|
||||
component PageHeader {
|
||||
state centered = false
|
||||
|
||||
view {
|
||||
<div
|
||||
class="py-12"
|
||||
class:lg:grid-cols-[minmax(0,1fr)_auto]="!centered"
|
||||
class:dark:bg-white/10="centered"
|
||||
>
|
||||
Header
|
||||
</div>
|
||||
}
|
||||
}
|
||||
`),
|
||||
);
|
||||
|
||||
expect(output).toContain("lg:grid-cols-[minmax(0,1fr)_auto]");
|
||||
|
||||
expect(output).toContain("dark:bg-white/10");
|
||||
});
|
||||
test("components support each blocks", () => {
|
||||
const output = generate(
|
||||
parse(`
|
||||
component PageHeader {
|
||||
props {
|
||||
highlights = []
|
||||
}
|
||||
|
||||
view {
|
||||
<div>
|
||||
{#each highlights as highlight}
|
||||
<span>{highlight.title}</span>
|
||||
{/each}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
`),
|
||||
);
|
||||
|
||||
expect(output).toContain("highlights");
|
||||
|
||||
expect(output).toContain("highlight.title");
|
||||
|
||||
expect(output).not.toContain("supported in pages, not components");
|
||||
});
|
||||
test("components support if blocks", () => {
|
||||
const output = generate(
|
||||
parse(`
|
||||
component Banner {
|
||||
props {
|
||||
visible = true
|
||||
}
|
||||
|
||||
view {
|
||||
{#if visible}
|
||||
<div>Visible</div>
|
||||
{:else}
|
||||
<div>Hidden</div>
|
||||
{/if}
|
||||
}
|
||||
}
|
||||
`),
|
||||
);
|
||||
|
||||
expect(output).toContain("visible");
|
||||
expect(output).toContain("Visible");
|
||||
expect(output).toContain("Hidden");
|
||||
});
|
||||
test("component array props support each blocks", () => {
|
||||
const output = generate(
|
||||
parse(`
|
||||
component HighlightList {
|
||||
props {
|
||||
highlights = []
|
||||
}
|
||||
|
||||
view {
|
||||
{#each highlights as highlight}
|
||||
<p>{highlight.title}</p>
|
||||
{/each}
|
||||
}
|
||||
}
|
||||
`),
|
||||
);
|
||||
|
||||
expect(output).toContain("Array.isArray(def)");
|
||||
|
||||
expect(output).toContain("JSON.parse(v)");
|
||||
|
||||
expect(output).toContain("highlights");
|
||||
});
|
||||
test("page component props evaluate whole expressions", () => {
|
||||
const output = generate(
|
||||
parse(`
|
||||
page Home {
|
||||
view {
|
||||
<HighlightList
|
||||
enabled="{true}"
|
||||
highlights="{[
|
||||
{ title: 'First' },
|
||||
{ title: 'Second' }
|
||||
]}"
|
||||
/>
|
||||
}
|
||||
}
|
||||
`),
|
||||
);
|
||||
|
||||
expect(output).toContain("__wrnexusPropAttr(true)");
|
||||
|
||||
expect(output).toContain("__wrnexusPropAttr([");
|
||||
});
|
||||
test("component functions are available during server rendering", () => {
|
||||
const output = generate(
|
||||
parse(`
|
||||
component FAQAccordion {
|
||||
props {
|
||||
allowMultiple = false
|
||||
items = []
|
||||
}
|
||||
|
||||
state openIndexes = []
|
||||
|
||||
functions {
|
||||
function isOpen(index) {
|
||||
return openIndexes.includes(index)
|
||||
}
|
||||
|
||||
function toggleItem(index) {
|
||||
if (isOpen(index)) {
|
||||
openIndexes = openIndexes.filter(
|
||||
(itemIndex) =>
|
||||
itemIndex !== index
|
||||
)
|
||||
} else if (allowMultiple) {
|
||||
openIndexes = [
|
||||
...openIndexes,
|
||||
index
|
||||
]
|
||||
} else {
|
||||
openIndexes = [index]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
{#each items as item, index}
|
||||
<button
|
||||
class:hidden="!isOpen(index)"
|
||||
@click="toggleItem(index)"
|
||||
>
|
||||
{item.title}
|
||||
</button>
|
||||
{/each}
|
||||
}
|
||||
}
|
||||
`),
|
||||
);
|
||||
|
||||
expect(output).toContain("function isOpen(index)");
|
||||
|
||||
expect(output).toContain("function toggleItem(index)");
|
||||
|
||||
expect(output).toContain("let openIndexes =");
|
||||
|
||||
expect(output).toContain("isOpen(index)");
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/core",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/csr",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -43,14 +43,46 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
});
|
||||
}
|
||||
|
||||
function decodeBehaviorPayload(value) {
|
||||
var binary = window.atob(value);
|
||||
var bytes = new Uint8Array(binary.length);
|
||||
|
||||
for (var index = 0; index < binary.length; index++) {
|
||||
bytes[index] = binary.charCodeAt(index);
|
||||
}
|
||||
|
||||
return new TextDecoder("utf-8").decode(bytes);
|
||||
}
|
||||
|
||||
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 =
|
||||
decodeBehaviorPayload(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;
|
||||
}
|
||||
}
|
||||
@@ -132,6 +164,119 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
subscribe: function (f) { subs.add(f); return function () { subs.delete(f); }; }
|
||||
};
|
||||
}
|
||||
function applyReactiveAttribute(node, name, value) {
|
||||
var lowerName = String(name || "").toLowerCase();
|
||||
|
||||
if (
|
||||
lowerName === "value" &&
|
||||
(
|
||||
node instanceof HTMLInputElement ||
|
||||
node instanceof HTMLTextAreaElement ||
|
||||
node instanceof HTMLSelectElement
|
||||
)
|
||||
) {
|
||||
var nextValue =
|
||||
value == null ? "" : String(value);
|
||||
|
||||
if (node.value !== nextValue) {
|
||||
node.value = nextValue;
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
node.removeAttribute("value");
|
||||
} else {
|
||||
node.setAttribute(
|
||||
"value",
|
||||
nextValue,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
lowerName === "checked" &&
|
||||
node instanceof HTMLInputElement
|
||||
) {
|
||||
var checked = !!value;
|
||||
|
||||
if (node.checked !== checked) {
|
||||
node.checked = checked;
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
node.setAttribute(
|
||||
"checked",
|
||||
"",
|
||||
);
|
||||
} else {
|
||||
node.removeAttribute(
|
||||
"checked",
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
lowerName === "selected" &&
|
||||
node instanceof HTMLOptionElement
|
||||
) {
|
||||
var selected = !!value;
|
||||
|
||||
node.selected = selected;
|
||||
|
||||
if (selected) {
|
||||
node.setAttribute(
|
||||
"selected",
|
||||
"",
|
||||
);
|
||||
} else {
|
||||
node.removeAttribute(
|
||||
"selected",
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
lowerName === "disabled" ||
|
||||
lowerName === "required" ||
|
||||
lowerName === "readonly" ||
|
||||
lowerName === "multiple"
|
||||
) {
|
||||
var enabled = !!value;
|
||||
|
||||
if (lowerName in node) {
|
||||
try {
|
||||
node[lowerName] = enabled;
|
||||
} catch (_) {
|
||||
// Fall through to attribute handling.
|
||||
}
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
node.setAttribute(
|
||||
name,
|
||||
"",
|
||||
);
|
||||
} else {
|
||||
node.removeAttribute(name);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === false || value == null) {
|
||||
node.removeAttribute(name);
|
||||
} else {
|
||||
node.setAttribute(
|
||||
name,
|
||||
value === true ? "" : String(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function setupScope(el) {
|
||||
if (el.__wrnexusScope) return; // idempotent: safe to call again after an HMR morph
|
||||
@@ -454,8 +599,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
} catch (e) { return ""; }
|
||||
});
|
||||
}
|
||||
if (raw === false || raw == null) node.removeAttribute(name);
|
||||
else node.setAttribute(name, raw === true ? "" : String(raw));
|
||||
applyReactiveAttribute(node, name, raw);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -664,12 +808,269 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
write(name, next);
|
||||
}
|
||||
|
||||
function evaluateSpecialExpression(
|
||||
expression,
|
||||
read,
|
||||
) {
|
||||
var source = String(
|
||||
expression || "",
|
||||
).trim();
|
||||
|
||||
// Remove an optional trailing semicolon.
|
||||
if (
|
||||
source.charAt(
|
||||
source.length - 1,
|
||||
) === ";"
|
||||
) {
|
||||
source = source
|
||||
.slice(0, -1)
|
||||
.trim();
|
||||
}
|
||||
|
||||
/*
|
||||
* Array literals containing spreads:
|
||||
*
|
||||
* [...openIndexes, index]
|
||||
* [first, ...otherItems]
|
||||
*/
|
||||
if (
|
||||
source.charAt(0) === "[" &&
|
||||
source.charAt(
|
||||
source.length - 1,
|
||||
) === "]" &&
|
||||
source.indexOf("...") !== -1
|
||||
) {
|
||||
var arraySource = source.slice(
|
||||
1,
|
||||
-1,
|
||||
);
|
||||
|
||||
var arrayParts = splitTopLevel(
|
||||
arraySource,
|
||||
",",
|
||||
);
|
||||
|
||||
var arrayResult = [];
|
||||
|
||||
arrayParts.forEach(function (
|
||||
part,
|
||||
) {
|
||||
var partSource =
|
||||
part.trim();
|
||||
|
||||
if (!partSource) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
partSource.slice(0, 3) ===
|
||||
"..."
|
||||
) {
|
||||
var spreadResult =
|
||||
evaluateExpression(
|
||||
partSource
|
||||
.slice(3)
|
||||
.trim(),
|
||||
read,
|
||||
);
|
||||
|
||||
if (
|
||||
Array.isArray(
|
||||
spreadResult,
|
||||
)
|
||||
) {
|
||||
Array.prototype.push.apply(
|
||||
arrayResult,
|
||||
spreadResult,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
arrayResult.push(
|
||||
evaluateExpression(
|
||||
partSource,
|
||||
read,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
matched: true,
|
||||
value: arrayResult,
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* First recognize the array method call without
|
||||
* trying to parse the callback in the same regex.
|
||||
*/
|
||||
var methodMatch =
|
||||
/^([\s\S]+)\.(filter|map|some|every|find|findIndex)\(\s*([\s\S]*)\s*\)$/.exec(
|
||||
source,
|
||||
);
|
||||
|
||||
if (!methodMatch) {
|
||||
return {
|
||||
matched: false,
|
||||
value: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
var collectionSource =
|
||||
methodMatch[1].trim();
|
||||
|
||||
var method =
|
||||
methodMatch[2];
|
||||
|
||||
var callbackSource =
|
||||
methodMatch[3].trim();
|
||||
|
||||
/*
|
||||
* Supported callbacks:
|
||||
*
|
||||
* item => item.active
|
||||
* (item) => item.active
|
||||
* (item, index) => index > 0
|
||||
*/
|
||||
var callbackMatch =
|
||||
/^(?:\(\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*(?:,\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*)?\)|([A-Za-z_$][A-Za-z0-9_$]*))\s*=>\s*([\s\S]+)$/.exec(
|
||||
callbackSource,
|
||||
);
|
||||
|
||||
if (!callbackMatch) {
|
||||
return {
|
||||
matched: false,
|
||||
value: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
var itemName =
|
||||
callbackMatch[1] ||
|
||||
callbackMatch[3];
|
||||
|
||||
var indexName =
|
||||
callbackMatch[2];
|
||||
|
||||
var callbackBody =
|
||||
callbackMatch[4].trim();
|
||||
|
||||
/*
|
||||
* Support expression bodies wrapped in parentheses:
|
||||
*
|
||||
* item => (
|
||||
* item.active
|
||||
* )
|
||||
*/
|
||||
if (
|
||||
callbackBody.charAt(0) ===
|
||||
"(" &&
|
||||
callbackBody.charAt(
|
||||
callbackBody.length - 1,
|
||||
) === ")"
|
||||
) {
|
||||
callbackBody = callbackBody
|
||||
.slice(1, -1)
|
||||
.trim();
|
||||
}
|
||||
|
||||
var collection =
|
||||
evaluateExpression(
|
||||
collectionSource,
|
||||
read,
|
||||
);
|
||||
|
||||
if (!Array.isArray(collection)) {
|
||||
collection = [];
|
||||
}
|
||||
|
||||
var callback = function (
|
||||
itemValue,
|
||||
itemIndex,
|
||||
) {
|
||||
return evaluateExpression(
|
||||
callbackBody,
|
||||
function (name) {
|
||||
if (name === itemName) {
|
||||
return itemValue;
|
||||
}
|
||||
|
||||
if (
|
||||
indexName &&
|
||||
name === indexName
|
||||
) {
|
||||
return itemIndex;
|
||||
}
|
||||
|
||||
return read(name);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
var result;
|
||||
|
||||
if (method === "filter") {
|
||||
result =
|
||||
collection.filter(callback);
|
||||
} else if (
|
||||
method === "map"
|
||||
) {
|
||||
result =
|
||||
collection.map(callback);
|
||||
} else if (
|
||||
method === "some"
|
||||
) {
|
||||
result =
|
||||
collection.some(callback);
|
||||
} else if (
|
||||
method === "every"
|
||||
) {
|
||||
result =
|
||||
collection.every(callback);
|
||||
} else if (
|
||||
method === "find"
|
||||
) {
|
||||
result =
|
||||
collection.find(callback);
|
||||
} else {
|
||||
result =
|
||||
collection.findIndex(
|
||||
callback,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
matched: true,
|
||||
value: result,
|
||||
};
|
||||
}
|
||||
|
||||
// A small, eval-free expression evaluator (so a strict CSP needs no
|
||||
// 'unsafe-eval'). Supports: literals, identifiers, member access (a.b, a[b]),
|
||||
// function/method calls, arrays, objects, arithmetic, comparison, equality,
|
||||
// logical (&& ||), unary (! - +), and the ternary operator.
|
||||
function evaluateExpression(expr, read) {
|
||||
var tokens = tokenizeExpression(String(expr || ""));
|
||||
function evaluateExpression(
|
||||
expr,
|
||||
read,
|
||||
) {
|
||||
expr = String(
|
||||
expr || "",
|
||||
).trim();
|
||||
|
||||
var special =
|
||||
evaluateSpecialExpression(
|
||||
expr,
|
||||
read,
|
||||
);
|
||||
|
||||
if (special.matched) {
|
||||
return special.value;
|
||||
}
|
||||
|
||||
var tokens =
|
||||
tokenizeExpression(expr);
|
||||
|
||||
var index = 0;
|
||||
function peek() { return tokens[index]; }
|
||||
function next() { return tokens[index++]; }
|
||||
@@ -824,11 +1225,36 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
tokens.push({ type: "ident", value: input.slice(s, i) });
|
||||
continue;
|
||||
}
|
||||
var three = input.substr(i, 3);
|
||||
if (three === "===" || three === "!==") { tokens.push({ type: "op", value: three }); i += 3; continue; }
|
||||
var two = input.substr(i, 2);
|
||||
if (["==", "!=", "<=", ">=", "&&", "||"].indexOf(two) !== -1) {
|
||||
tokens.push({ type: "op", value: two });
|
||||
var three = input.slice(i, i + 3);
|
||||
|
||||
if (
|
||||
three === "===" ||
|
||||
three === "!=="
|
||||
) {
|
||||
tokens.push({
|
||||
type: "operator",
|
||||
value: three,
|
||||
});
|
||||
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
|
||||
var two = input.slice(i, i + 2);
|
||||
|
||||
if (
|
||||
two === "==" ||
|
||||
two === "!=" ||
|
||||
two === ">=" ||
|
||||
two === "<=" ||
|
||||
two === "&&" ||
|
||||
two === "||"
|
||||
) {
|
||||
tokens.push({
|
||||
type: "operator",
|
||||
value: two,
|
||||
});
|
||||
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/db",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-server",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { existsSync, rmSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
export interface ResetDevCacheOptions {
|
||||
rootDir?: string;
|
||||
cacheDir?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export function resetDevCache(options: ResetDevCacheOptions = {}): string {
|
||||
const rootDir = resolve(options.rootDir ?? process.cwd());
|
||||
|
||||
const cacheDir = resolve(rootDir, options.cacheDir ?? ".wrnexus");
|
||||
|
||||
if (options.enabled === false) {
|
||||
return cacheDir;
|
||||
}
|
||||
|
||||
// Safety: never allow deleting the project root.
|
||||
if (cacheDir === rootDir) {
|
||||
throw new Error("Refusing to remove the project root as the WRNexus cache directory.");
|
||||
}
|
||||
|
||||
if (existsSync(cacheDir)) {
|
||||
rmSync(cacheDir, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
maxRetries: 3,
|
||||
retryDelay: 100,
|
||||
});
|
||||
|
||||
console.log(`[wrnexus] cleared generated cache: ${cacheDir}`);
|
||||
}
|
||||
|
||||
return cacheDir;
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import { createDevAssetServer } from "./assets.ts";
|
||||
import { HmrHub } from "./hmr.ts";
|
||||
import { startWatcher } from "./watch.ts";
|
||||
export { RESTART_EXIT_CODE } from "./restart.ts";
|
||||
import { resetDevCache } from "./cache.ts";
|
||||
|
||||
export interface ServeOptions {
|
||||
appDir: string;
|
||||
@@ -122,6 +123,12 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
const router = buildRouter(appDir, { componentDirs: [uiComponentsDir()] });
|
||||
const styleEntry = opts.styleEntry ?? null;
|
||||
const appRoot = dirname(appDir);
|
||||
|
||||
resetDevCache({
|
||||
rootDir: appRoot,
|
||||
enabled: process.env.WRNEXUS_PRESERVE_CACHE !== "1",
|
||||
});
|
||||
|
||||
// Compile every `.wrn` into ONE cache dir at the project root, instead of a
|
||||
// `.wrnexus/` next to each source file (and inside node_modules UI dirs).
|
||||
setCompileCacheDir(join(appRoot, ".wrnexus"));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/encryption",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/helpers",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/i18n",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/jwt",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/mobile",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/native",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/oauth",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/pubsub",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/queue",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/reactive",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/router",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ssr",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/styles",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/test",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/tracking",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ui",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/uploader",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/validation",
|
||||
"version": "0.2.31",
|
||||
"version": "0.2.42",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const version = "0.2.42";
|
||||
|
||||
const dependencyGroups = [
|
||||
"dependencies",
|
||||
"devDependencies",
|
||||
"peerDependencies",
|
||||
"optionalDependencies",
|
||||
];
|
||||
|
||||
for (const entry of readdirSync("packages", {
|
||||
withFileTypes: true,
|
||||
})) {
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const packageFile = join("packages", entry.name, "package.json");
|
||||
|
||||
if (!existsSync(packageFile)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const packageJson = JSON.parse(readFileSync(packageFile, "utf8"));
|
||||
|
||||
packageJson.version = version;
|
||||
|
||||
for (const group of dependencyGroups) {
|
||||
const dependencies = packageJson[group];
|
||||
|
||||
if (!dependencies || typeof dependencies !== "object") {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const dependencyName of Object.keys(dependencies)) {
|
||||
if (!dependencyName.startsWith("@wrnexus/")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentVersion = dependencies[dependencyName];
|
||||
|
||||
if (typeof currentVersion !== "string") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentVersion.startsWith("workspace:")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentVersion.startsWith("^")) {
|
||||
dependencies[dependencyName] = `^${version}`;
|
||||
} else if (currentVersion.startsWith("~")) {
|
||||
dependencies[dependencyName] = `~${version}`;
|
||||
} else {
|
||||
dependencies[dependencyName] = version;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeFileSync(packageFile, `${JSON.stringify(packageJson, null, 2)}\n`);
|
||||
}
|
||||
Reference in New Issue
Block a user