release: WRNexusJS 0.2.27
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ai",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/authz",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/authz — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
+11
-11
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/cli",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"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.26",
|
||||
"@wrnexus/router": "^0.2.26",
|
||||
"@wrnexus/csr": "^0.2.26",
|
||||
"@wrnexus/compiler": "^0.2.26",
|
||||
"@wrnexus/styles": "^0.2.26",
|
||||
"@wrnexus/dev-server": "^0.2.26",
|
||||
"@wrnexus/ui": "^0.2.26",
|
||||
"@wrnexus/validation": "^0.2.26",
|
||||
"@wrnexus/i18n": "^0.2.26",
|
||||
"@wrnexus/db": "^0.2.26"
|
||||
"@wrnexus/core": "^0.2.27",
|
||||
"@wrnexus/router": "^0.2.27",
|
||||
"@wrnexus/csr": "^0.2.27",
|
||||
"@wrnexus/compiler": "^0.2.27",
|
||||
"@wrnexus/styles": "^0.2.27",
|
||||
"@wrnexus/dev-server": "^0.2.27",
|
||||
"@wrnexus/ui": "^0.2.27",
|
||||
"@wrnexus/validation": "^0.2.27",
|
||||
"@wrnexus/i18n": "^0.2.27",
|
||||
"@wrnexus/db": "^0.2.27"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/compiler",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/compiler — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/core",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/core — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/csr",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/csr — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.2.26"
|
||||
"@wrnexus/core": "^0.2.27"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/db",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/db — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-server",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/dev-server — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -25,18 +25,18 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.2.26",
|
||||
"@wrnexus/router": "^0.2.26",
|
||||
"@wrnexus/ssr": "^0.2.26",
|
||||
"@wrnexus/csr": "^0.2.26",
|
||||
"@wrnexus/compiler": "^0.2.26",
|
||||
"@wrnexus/styles": "^0.2.26",
|
||||
"@wrnexus/ui": "^0.2.26",
|
||||
"@wrnexus/validation": "^0.2.26",
|
||||
"@wrnexus/i18n": "^0.2.26",
|
||||
"@wrnexus/db": "^0.2.26",
|
||||
"@wrnexus/pubsub": "^0.2.26",
|
||||
"@wrnexus/uploader": "^0.2.26"
|
||||
"@wrnexus/core": "^0.2.27",
|
||||
"@wrnexus/router": "^0.2.27",
|
||||
"@wrnexus/ssr": "^0.2.27",
|
||||
"@wrnexus/csr": "^0.2.27",
|
||||
"@wrnexus/compiler": "^0.2.27",
|
||||
"@wrnexus/styles": "^0.2.27",
|
||||
"@wrnexus/ui": "^0.2.27",
|
||||
"@wrnexus/validation": "^0.2.27",
|
||||
"@wrnexus/i18n": "^0.2.27",
|
||||
"@wrnexus/db": "^0.2.27",
|
||||
"@wrnexus/pubsub": "^0.2.27",
|
||||
"@wrnexus/uploader": "^0.2.27"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/encryption",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/encryption — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/helpers",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"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.26"
|
||||
"@wrnexus/core": "^0.2.27"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/i18n",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/i18n — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.2.26"
|
||||
"@wrnexus/core": "^0.2.27"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/jwt",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/jwt — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/mobile",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/mobile — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/native": "^0.2.26"
|
||||
"@wrnexus/native": "^0.2.27"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/native",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/native — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/oauth",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/oauth — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/pubsub",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/pubsub — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/queue",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/queue — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/reactive",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/reactive — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/router",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/router — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -21,8 +21,8 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/compiler": "^0.2.26",
|
||||
"@wrnexus/core": "^0.2.26"
|
||||
"@wrnexus/compiler": "^0.2.27",
|
||||
"@wrnexus/core": "^0.2.27"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ssr",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/ssr — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.2.26"
|
||||
"@wrnexus/core": "^0.2.27"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/styles",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/styles — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/uploader": "^0.2.26"
|
||||
"@wrnexus/uploader": "^0.2.27"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/test",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/test — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/tracking",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/tracking — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ui",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"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.26"
|
||||
"@wrnexus/core": "^0.2.27"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/uploader",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/uploader — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "^0.2.26"
|
||||
"@wrnexus/core": "^0.2.27"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/validation",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/validation — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"comments": {
|
||||
"lineComment": "//"
|
||||
"blockComment": ["<!--", "-->"]
|
||||
},
|
||||
"brackets": [
|
||||
["{", "}"],
|
||||
@@ -9,38 +9,45 @@
|
||||
["<", ">"]
|
||||
],
|
||||
"autoClosingPairs": [
|
||||
{ "open": "{", "close": "}" },
|
||||
{ "open": "[", "close": "]" },
|
||||
{ "open": "(", "close": ")" },
|
||||
{ "open": "\"", "close": "\"", "notIn": ["string"] },
|
||||
{ "open": "'", "close": "'", "notIn": ["string"] },
|
||||
{ "open": "<", "close": ">" }
|
||||
{
|
||||
"open": "{",
|
||||
"close": "}"
|
||||
},
|
||||
{
|
||||
"open": "[",
|
||||
"close": "]"
|
||||
},
|
||||
{
|
||||
"open": "(",
|
||||
"close": ")"
|
||||
},
|
||||
{
|
||||
"open": "\"",
|
||||
"close": "\"",
|
||||
"notIn": ["string"]
|
||||
},
|
||||
{
|
||||
"open": "'",
|
||||
"close": "'",
|
||||
"notIn": ["string"]
|
||||
}
|
||||
],
|
||||
"autoCloseBefore": ";:.,=}])> \n\t",
|
||||
"surroundingPairs": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"],
|
||||
["\"", "\""],
|
||||
["'", "'"],
|
||||
["<", ">"]
|
||||
["'", "'"]
|
||||
],
|
||||
"colorizedBracketPairs": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"]
|
||||
],
|
||||
"folding": {
|
||||
"markers": {
|
||||
"start": "^\\s*//\\s*#region\\b",
|
||||
"end": "^\\s*//\\s*#endregion\\b"
|
||||
}
|
||||
"indentationRules": {
|
||||
"increaseIndentPattern": "^.*(?:\\{|<(?!(?:area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)\\b)[A-Za-z][\\w:-]*(?:\\s[^>]*)?>)\\s*$",
|
||||
"decreaseIndentPattern": "^\\s*(?:\\}|<\\/[A-Za-z][\\w:-]*>)"
|
||||
},
|
||||
"onEnterRules": [
|
||||
{
|
||||
"beforeText": "^\\s*//.*$",
|
||||
"action": { "indent": "none", "appendText": "// " }
|
||||
"folding": {
|
||||
"offSide": false,
|
||||
"markers": {
|
||||
"start": "^\\s*(?:page|component|api|middleware|realtime|view|seo|props|functions)\\b.*\\{\\s*$",
|
||||
"end": "^\\s*\\}\\s*$"
|
||||
}
|
||||
],
|
||||
"wordPattern": "[A-Za-z_][A-Za-z0-9_-]*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "wrnexus",
|
||||
"displayName": "WRNexus Language Support",
|
||||
"description": "Syntax highlighting, formatting, diagnostics, snippets and completions for WrNexus .wrn files.",
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.4",
|
||||
"publisher": "wrnexus",
|
||||
"private": true,
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
|
||||
@@ -1,114 +1,196 @@
|
||||
{
|
||||
"Page": {
|
||||
"prefix": "page",
|
||||
"WRN page": {
|
||||
"prefix": ["wrn-page", "page"],
|
||||
"description": "Create a WRN page",
|
||||
"body": [
|
||||
"page ${1:Home} {",
|
||||
" layout = \"${2:public}\"",
|
||||
"page ${1:PageName} {",
|
||||
" layout = \"${2:default}\"",
|
||||
"",
|
||||
" seo {",
|
||||
" title = \"${3:Home}\"",
|
||||
" description = \"${4:A WrNexus page.}\"",
|
||||
" }",
|
||||
" seo {",
|
||||
" title = \"${3:Page title}\"",
|
||||
" description = \"${4:Page description}\"",
|
||||
" }",
|
||||
"",
|
||||
" view {",
|
||||
" <h1>$5</h1>",
|
||||
" }",
|
||||
"}",
|
||||
""
|
||||
],
|
||||
"description": "A WrNexus page (a route) with layout, seo and view."
|
||||
},
|
||||
"Component": {
|
||||
"prefix": "component",
|
||||
"body": [
|
||||
"component ${1:Counter} {",
|
||||
" props {",
|
||||
" ${2:start} = ${3:0}",
|
||||
" }",
|
||||
"",
|
||||
" state ${4:count} = ${2:start}",
|
||||
"",
|
||||
" view {",
|
||||
" <button @click=\"${4:count}++\">{${4:count}}</button>",
|
||||
" }",
|
||||
"}",
|
||||
""
|
||||
],
|
||||
"description": "A reusable, prop-driven component."
|
||||
},
|
||||
"View block": {
|
||||
"prefix": "view",
|
||||
"body": ["view {", " $0", "}"],
|
||||
"description": "The server-rendered HTML view block."
|
||||
},
|
||||
"State declaration": {
|
||||
"prefix": "state",
|
||||
"body": ["state ${1:count} = ${2:0}"],
|
||||
"description": "Reactive state seeded into the scope."
|
||||
},
|
||||
"Props block": {
|
||||
"prefix": "props",
|
||||
"body": ["props {", " ${1:label} = ${2:\"Label\"}", "}"],
|
||||
"description": "Component props with typed defaults."
|
||||
},
|
||||
"SEO block": {
|
||||
"prefix": "seo",
|
||||
"body": ["seo {", " title = \"${1:Title}\"", " description = \"${2:Description}\"", "}"],
|
||||
"description": "Per-page SEO metadata."
|
||||
},
|
||||
"API route": {
|
||||
"prefix": "api",
|
||||
"body": [
|
||||
"api ${1|GET,POST,PUT,PATCH,DELETE|} ${2:/path} {",
|
||||
" $0",
|
||||
" return Response.json({ ok: true });",
|
||||
" view {",
|
||||
" <main>",
|
||||
" <h1>${5:Page heading}</h1>",
|
||||
" </main>",
|
||||
" }",
|
||||
"}"
|
||||
],
|
||||
"description": "A colocated API route handler."
|
||||
]
|
||||
},
|
||||
"Functions block": {
|
||||
"prefix": "functions",
|
||||
"body": ["functions {", " $0", "}"],
|
||||
"description": "Shared helper functions for this file."
|
||||
|
||||
"WRN dynamic page": {
|
||||
"prefix": ["wrn-dynamic-page", "dynamic-page"],
|
||||
"description": "Create a WRN page with a route parameter",
|
||||
"body": [
|
||||
"page ${1:DynamicPage} {",
|
||||
" layout = \"${2:default}\"",
|
||||
"",
|
||||
" state ${3:id} = ctx.params.${3:id}",
|
||||
"",
|
||||
" view {",
|
||||
" <main>",
|
||||
" <h1>{${3:id}}</h1>",
|
||||
" </main>",
|
||||
" }",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"Style block": {
|
||||
"prefix": "style",
|
||||
"body": ["style {", " .${1:box} {", " $0", " }", "}"],
|
||||
"description": "Scoped CSS inlined with the page."
|
||||
|
||||
"WRN component": {
|
||||
"prefix": ["wrn-component", "component"],
|
||||
"description": "Create a WRN component",
|
||||
"body": [
|
||||
"component ${1:ComponentName} {",
|
||||
" props {",
|
||||
" ${2:title} = \"${3:Title}\"",
|
||||
" }",
|
||||
"",
|
||||
" view {",
|
||||
" <div>",
|
||||
" <h2>{${2:title}}</h2>",
|
||||
" </div>",
|
||||
" }",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"SSR data block": {
|
||||
"prefix": "ssr",
|
||||
"body": ["ssr {", " api ${1:load} ${2|GET,POST|} ${3:/api/data} {", " $0", " }", "}"],
|
||||
"description": "Server-side data fetching block."
|
||||
|
||||
"WRN state": {
|
||||
"prefix": ["wrn-state", "state"],
|
||||
"description": "Create reactive state",
|
||||
"body": ["state ${1:name} = ${2:\"value\"}"]
|
||||
},
|
||||
"Client data block": {
|
||||
"prefix": "client",
|
||||
"body": ["client {", " api ${1:load} ${2|GET,POST|} ${3:/api/data} {", " $0", " }", "}"],
|
||||
"description": "Client-side data fetching block."
|
||||
|
||||
"WRN SEO block": {
|
||||
"prefix": ["wrn-seo", "seo"],
|
||||
"description": "Create SEO metadata",
|
||||
"body": [
|
||||
"seo {",
|
||||
" title = \"${1:Page title}\"",
|
||||
" description = \"${2:Page description}\"",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"Realtime block": {
|
||||
"prefix": "realtime",
|
||||
"body": ["realtime ${1:chat} {", " on ${2:message}(${3:data}) {", " $0", " }", "}"],
|
||||
"description": "A realtime channel with event handlers."
|
||||
|
||||
"WRN form": {
|
||||
"prefix": ["wrn-form", "form"],
|
||||
"description": "Create a POST form",
|
||||
"body": [
|
||||
"<form",
|
||||
" method=\"post\"",
|
||||
" action=\"${1:/api/action}\"",
|
||||
">",
|
||||
" ${2}",
|
||||
"",
|
||||
" <button type=\"submit\">",
|
||||
" ${3:Submit}",
|
||||
" </button>",
|
||||
"</form>"
|
||||
]
|
||||
},
|
||||
"Component mount": {
|
||||
"prefix": "mount",
|
||||
"body": ["<div data-component=\"${1:counter}\" ${2:start=\"0\"}></div>"],
|
||||
"description": "Mount a component in a view."
|
||||
|
||||
"WRN input": {
|
||||
"prefix": ["wrn-input", "input"],
|
||||
"description": "Create a labeled input",
|
||||
"body": [
|
||||
"<label>",
|
||||
" <span>${1:Label}</span>",
|
||||
"",
|
||||
" <input",
|
||||
" type=\"${2|text,email,password,tel,url,number,date|}\"",
|
||||
" name=\"${3:name}\"",
|
||||
" value=\"${4}\"",
|
||||
" ${5:required}",
|
||||
" />",
|
||||
"</label>"
|
||||
]
|
||||
},
|
||||
"List rendering (data-for)": {
|
||||
"prefix": "for",
|
||||
"body": ["<${1:li} data-for=\"${2:item} in ${3:items}\" data-text=\"${2:item}\"></${1:li}>"],
|
||||
"description": "Repeat an element for each item."
|
||||
|
||||
"WRN conditional class": {
|
||||
"prefix": ["wrn-class-if", "class-if"],
|
||||
"description": "Add a conditional class directive",
|
||||
"body": ["class:${1:border-indigo-500}=\"${2:condition}\""]
|
||||
},
|
||||
"Conditional (data-show)": {
|
||||
"prefix": "show",
|
||||
"body": ["<div data-show=\"${1:condition}\">$0</div>"],
|
||||
"description": "Toggle visibility reactively."
|
||||
|
||||
"WRN click event": {
|
||||
"prefix": ["wrn-click", "click"],
|
||||
"description": "Add a click handler",
|
||||
"body": ["@click=\"${1:handler()}\""]
|
||||
},
|
||||
"Translation": {
|
||||
"prefix": "t",
|
||||
"body": ["{t:${1:key}}"],
|
||||
"description": "Localized text via the i18n dictionary."
|
||||
|
||||
"WRN show directive": {
|
||||
"prefix": ["wrn-show", "show"],
|
||||
"description": "Conditionally show an element",
|
||||
"body": ["data-show=\"${1:condition}\""]
|
||||
},
|
||||
|
||||
"WRN loop": {
|
||||
"prefix": ["wrn-for", "for"],
|
||||
"description": "Create a reactive list loop",
|
||||
"body": ["<div data-for=\"${1:item} in ${2:items}\">", " <span>{${1:item}}</span>", "</div>"]
|
||||
},
|
||||
|
||||
"WRN dynamic link": {
|
||||
"prefix": ["wrn-route-link", "route-link"],
|
||||
"description": "Create a link using route state",
|
||||
"body": ["<a href=\"/${1:resource}/{${2:id}}\">", " ${3:Open}", "</a>"]
|
||||
},
|
||||
|
||||
"WRN API GET": {
|
||||
"prefix": ["wrn-api-get", "api-get"],
|
||||
"description": "Create a GET API route",
|
||||
"body": [
|
||||
"api GET ${1:/api/resource} {",
|
||||
" return Response.json({",
|
||||
" ${2:success}: true",
|
||||
" });",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
|
||||
"WRN API POST": {
|
||||
"prefix": ["wrn-api-post", "api-post"],
|
||||
"description": "Create a POST API route",
|
||||
"body": [
|
||||
"api POST ${1:/api/resource} {",
|
||||
" 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",
|
||||
"body": [
|
||||
"state loading = false",
|
||||
"",
|
||||
"<button",
|
||||
" type=\"submit\"",
|
||||
" class:opacity-50=\"loading\"",
|
||||
" class:cursor-not-allowed=\"loading\"",
|
||||
" disabled=\"{loading}\"",
|
||||
">",
|
||||
" {loading ? \"Loading...\" : \"${1:Submit}\"}",
|
||||
"</button>"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
"use strict";
|
||||
|
||||
const vscode = require("vscode");
|
||||
|
||||
const BLOCK_COMPLETIONS = [
|
||||
{
|
||||
label: "page",
|
||||
detail: "WRN page",
|
||||
documentation: "Create a WRN page declaration.",
|
||||
snippet: [
|
||||
"page ${1:PageName} {",
|
||||
' layout = "${2:default}"',
|
||||
"",
|
||||
" view {",
|
||||
" $0",
|
||||
" }",
|
||||
"}",
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
label: "component",
|
||||
detail: "WRN component",
|
||||
documentation: "Create a reusable WRN component.",
|
||||
snippet: [
|
||||
"component ${1:ComponentName} {",
|
||||
" props {",
|
||||
' ${2:title} = "${3:Title}"',
|
||||
" }",
|
||||
"",
|
||||
" view {",
|
||||
" $0",
|
||||
" }",
|
||||
"}",
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
label: "seo",
|
||||
detail: "SEO metadata block",
|
||||
snippet: [
|
||||
"seo {",
|
||||
' title = "${1:Page title}"',
|
||||
' description = "${2:Page description}"',
|
||||
"}",
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
label: "view",
|
||||
detail: "WRN view block",
|
||||
snippet: ["view {", " $0", "}"].join("\n"),
|
||||
},
|
||||
{
|
||||
label: "state",
|
||||
detail: "Reactive state declaration",
|
||||
snippet: 'state ${1:name} = ${2:"value"}',
|
||||
},
|
||||
];
|
||||
|
||||
const ATTRIBUTE_COMPLETIONS = [
|
||||
["@click", "Click event handler", '@click="${1:handler()}"'],
|
||||
["@change", "Change event handler", '@change="${1:handler()}"'],
|
||||
["@input", "Input event handler", '@input="${1:handler()}"'],
|
||||
["@submit", "Submit event handler", '@submit="${1:handler()}"'],
|
||||
["@focus", "Focus event handler", '@focus="${1:handler()}"'],
|
||||
["@blur", "Blur event handler", '@blur="${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}"'],
|
||||
];
|
||||
|
||||
const CONTEXT_COMPLETIONS = [
|
||||
["ctx.params", "Dynamic route parameters"],
|
||||
["ctx.query", "URL query parameters"],
|
||||
["ctx.request", "Current Request object"],
|
||||
["ctx.user", "Authenticated user"],
|
||||
["ctx.session", "Current session"],
|
||||
["ctx.locals", "Request-local data"],
|
||||
];
|
||||
|
||||
function completionKindFor(label) {
|
||||
if (label.startsWith("@")) {
|
||||
return vscode.CompletionItemKind.Event;
|
||||
}
|
||||
|
||||
if (label.startsWith("class:") || label.startsWith("data-")) {
|
||||
return vscode.CompletionItemKind.Property;
|
||||
}
|
||||
|
||||
return vscode.CompletionItemKind.Keyword;
|
||||
}
|
||||
|
||||
function createCompletion(label, detail, snippet) {
|
||||
const item = new vscode.CompletionItem(label, completionKindFor(label));
|
||||
|
||||
item.detail = detail;
|
||||
item.insertText = new vscode.SnippetString(snippet || label);
|
||||
item.documentation = new vscode.MarkdownString(detail);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
function getCurrentOpeningTag(document, position) {
|
||||
const textBeforeCursor = document.getText(
|
||||
new vscode.Range(new vscode.Position(position.line, 0), position),
|
||||
);
|
||||
|
||||
const lastOpen = textBeforeCursor.lastIndexOf("<");
|
||||
const lastClose = textBeforeCursor.lastIndexOf(">");
|
||||
|
||||
if (lastOpen > lastClose) {
|
||||
return textBeforeCursor.slice(lastOpen);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractRouteParams(document) {
|
||||
const fileName = document.fileName.replace(/\\/g, "/");
|
||||
const matches = [...fileName.matchAll(/\[([A-Za-z_$][\w$]*)\]/g)];
|
||||
|
||||
return matches.map((match) => match[1]);
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
function provideCompletionItems(document, position) {
|
||||
const items = [];
|
||||
const linePrefix = document.lineAt(position.line).text.slice(0, position.character);
|
||||
|
||||
const openingTag = getCurrentOpeningTag(document, position);
|
||||
|
||||
if (openingTag !== null) {
|
||||
for (const [label, detail, snippet] of ATTRIBUTE_COMPLETIONS) {
|
||||
items.push(createCompletion(label, detail, snippet));
|
||||
}
|
||||
} else {
|
||||
for (const completion of BLOCK_COMPLETIONS) {
|
||||
items.push(createCompletion(completion.label, completion.detail, completion.snippet));
|
||||
}
|
||||
}
|
||||
|
||||
if (linePrefix.includes("ctx.") || linePrefix.includes("ctx.params.")) {
|
||||
for (const [label, detail] of CONTEXT_COMPLETIONS) {
|
||||
items.push(createCompletion(label, detail, label));
|
||||
}
|
||||
}
|
||||
|
||||
for (const param of extractRouteParams(document)) {
|
||||
const item = new vscode.CompletionItem(param, vscode.CompletionItemKind.Variable);
|
||||
|
||||
item.detail = `Route parameter from [${param}].wrn`;
|
||||
item.insertText = param;
|
||||
|
||||
items.push(item);
|
||||
|
||||
const fullItem = new vscode.CompletionItem(
|
||||
`ctx.params.${param}`,
|
||||
vscode.CompletionItemKind.Variable,
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
item.detail = "WRN state";
|
||||
item.insertText = state;
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function registerCompletionProvider(context) {
|
||||
const provider = vscode.languages.registerCompletionItemProvider(
|
||||
{ language: "wrn", scheme: "file" },
|
||||
{
|
||||
provideCompletionItems,
|
||||
},
|
||||
"@",
|
||||
":",
|
||||
".",
|
||||
"<",
|
||||
" ",
|
||||
);
|
||||
|
||||
context.subscriptions.push(provider);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
extractRouteParams,
|
||||
extractStates,
|
||||
registerCompletionProvider,
|
||||
};
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
const vscode = require("vscode");
|
||||
const { formatWrn } = require("./formatter");
|
||||
const { registerCompletionProvider } = require("./completion");
|
||||
|
||||
// The .wrn compiler, bundled to CJS by `bun run build:compiler`. Loaded
|
||||
// defensively so the rest of the extension (highlighting, snippets, completion)
|
||||
@@ -67,6 +68,8 @@ const EVENTS = [
|
||||
* @param {vscode.ExtensionContext} context
|
||||
*/
|
||||
function activate(context) {
|
||||
registerCompletionProvider(context);
|
||||
|
||||
const diagnostics = vscode.languages.createDiagnosticCollection("wrn");
|
||||
context.subscriptions.push(diagnostics);
|
||||
|
||||
|
||||
@@ -46,8 +46,7 @@ function findOpeningTagEnd(value) {
|
||||
|
||||
function parseAttributes(value) {
|
||||
const attributes = [];
|
||||
const pattern =
|
||||
/[^\s"'=<>`]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?/g;
|
||||
const pattern = /[^\s"'=<>`]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?/g;
|
||||
|
||||
let match;
|
||||
|
||||
@@ -121,9 +120,7 @@ function formatOpeningTag(value, unit, depth, printWidth = 100) {
|
||||
|
||||
const lines = [
|
||||
`${baseIndent}<${parsed.tagName}`,
|
||||
...parsed.attributes.map(
|
||||
(attribute) => `${attributeIndent}${attribute}`,
|
||||
),
|
||||
...parsed.attributes.map((attribute) => `${attributeIndent}${attribute}`),
|
||||
];
|
||||
|
||||
if (parsed.inlineClosing) {
|
||||
@@ -182,10 +179,7 @@ function isWrnBlockOpening(value) {
|
||||
}
|
||||
|
||||
function formatWrn(source, options = {}) {
|
||||
const unit =
|
||||
options.insertSpaces === false
|
||||
? "\t"
|
||||
: " ".repeat(options.tabSize || 4);
|
||||
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");
|
||||
@@ -251,12 +245,7 @@ function formatWrn(source, options = {}) {
|
||||
!value.startsWith("<!--") &&
|
||||
!isInlineElement(value)
|
||||
) {
|
||||
const formattedTag = formatOpeningTag(
|
||||
value,
|
||||
unit,
|
||||
depth,
|
||||
printWidth,
|
||||
);
|
||||
const formattedTag = formatOpeningTag(value, unit, depth, printWidth);
|
||||
|
||||
output.push(...formattedTag.lines);
|
||||
|
||||
@@ -284,4 +273,4 @@ function formatWrn(source, options = {}) {
|
||||
module.exports = {
|
||||
formatOpeningTag,
|
||||
formatWrn,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert");
|
||||
const { extractRouteParams, extractStates } = require("../src/completion");
|
||||
|
||||
test("extracts dynamic route params from filename", () => {
|
||||
const document = {
|
||||
fileName: "apps/sso/app/pages/organizations/[organization]/members/[member].wrn",
|
||||
};
|
||||
|
||||
assert.deepEqual(extractRouteParams(document), ["organization", "member"]);
|
||||
});
|
||||
|
||||
test("extracts WRN states", () => {
|
||||
const document = {
|
||||
getText() {
|
||||
return `
|
||||
page Test {
|
||||
state token = ctx.params.token
|
||||
state loading = false
|
||||
}
|
||||
`;
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(extractStates(document), ["token", "loading"]);
|
||||
});
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ai",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/authz",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/cli",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -313,6 +313,14 @@ const MIGRATIONS: Migration[] = [
|
||||
// No project file migration is required for this framework release.
|
||||
},
|
||||
},
|
||||
{
|
||||
version: "0.2.27",
|
||||
id: "component-improvements-tag",
|
||||
description: "Adds Component renders system allowed 2 ways.",
|
||||
apply() {
|
||||
// No project file migration is required for this framework release.
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** Release tooling uses this to require an explicit migration entry per version. */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/compiler",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -36,6 +36,10 @@ interface NamedDataBinding extends RenderBinding {
|
||||
mode: DataMode;
|
||||
}
|
||||
|
||||
function isComponentTag(tag: string): boolean {
|
||||
return /^[A-Z][A-Za-z0-9_$]*$/.test(tag);
|
||||
}
|
||||
|
||||
/** Escape a value placed inside a double-quoted HTML attribute. */
|
||||
function attrEscape(value: string): string {
|
||||
return value
|
||||
@@ -217,19 +221,49 @@ function bakeLoopAttr(raw: string): string {
|
||||
|
||||
/** Render one loop-body node to template-literal source (nested loops inline). */
|
||||
function renderLoopBody(node: ViewNode): string {
|
||||
if (node.type === "text") return bakeLoopText(node.value);
|
||||
if (node.type === "each") return compileEachExpr(node);
|
||||
if (node.type === "if") return compileIfExpr(node);
|
||||
if (node.type === "text") {
|
||||
return bakeLoopText(node.value);
|
||||
}
|
||||
|
||||
if (node.type === "each") {
|
||||
return compileEachExpr(node);
|
||||
}
|
||||
|
||||
if (node.type === "if") {
|
||||
return compileIfExpr(node);
|
||||
}
|
||||
|
||||
const componentTag = isComponentTag(node.tag);
|
||||
|
||||
const attrs = node.attrs
|
||||
.map((a) => {
|
||||
const name = a.event ? eventAttribute(a.name) : a.name;
|
||||
if (a.boolean) return escLit(` ${name}`);
|
||||
return escLit(` ${name}="`) + bakeLoopAttr(a.value) + escLit(`"`);
|
||||
.filter((attr) => attr.name !== "data-component")
|
||||
.map((attr) => {
|
||||
const name = attr.event ? eventAttribute(attr.name) : attr.name;
|
||||
|
||||
if (attr.boolean) {
|
||||
return escLit(` ${name}`);
|
||||
}
|
||||
|
||||
return escLit(` ${name}="`) + bakeLoopAttr(attr.value) + escLit(`"`);
|
||||
})
|
||||
.join("");
|
||||
if (VOID_ELEMENTS.has(node.tag.toLowerCase()))
|
||||
return escLit(`<${node.tag}`) + attrs + escLit(">");
|
||||
|
||||
const inner = node.children.map(renderLoopBody).join("");
|
||||
|
||||
if (componentTag) {
|
||||
return (
|
||||
escLit(`<div data-component="${attrEscape(node.tag)}"`) +
|
||||
attrs +
|
||||
escLit(">") +
|
||||
inner +
|
||||
escLit("</div>")
|
||||
);
|
||||
}
|
||||
|
||||
if (VOID_ELEMENTS.has(node.tag.toLowerCase())) {
|
||||
return escLit(`<${node.tag}`) + attrs + escLit(">");
|
||||
}
|
||||
|
||||
return escLit(`<${node.tag}`) + attrs + escLit(">") + inner + escLit(`</${node.tag}>`);
|
||||
}
|
||||
|
||||
@@ -314,6 +348,17 @@ function renderNode(
|
||||
return `\x00WRNEACH${loops.length - 1}\x00`;
|
||||
}
|
||||
|
||||
if (isComponentTag(node.tag)) {
|
||||
return renderPageComponentInvocation(
|
||||
node,
|
||||
ssrBindings,
|
||||
csrBindings,
|
||||
apiBindings,
|
||||
loops,
|
||||
reactive,
|
||||
);
|
||||
}
|
||||
|
||||
const apiName = attrValue(node.attrs, "api");
|
||||
const apiBinding = apiName ? apiBindings.get(apiName) : undefined;
|
||||
if (apiName && !apiBinding) {
|
||||
@@ -361,6 +406,71 @@ function renderNode(
|
||||
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive)}>${inner}</${node.tag}>`;
|
||||
}
|
||||
|
||||
function renderPageComponentInvocation(
|
||||
node: Extract<ViewNode, { type: "element" }>,
|
||||
ssrBindings: SsrBinding[],
|
||||
csrBindings: CsrBinding[],
|
||||
apiBindings: Map<string, NamedDataBinding>,
|
||||
loops: string[],
|
||||
reactive: PageReactive | null,
|
||||
): string {
|
||||
const attrs = node.attrs.filter((attr) => attr.name !== "data-component");
|
||||
|
||||
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>`;
|
||||
}
|
||||
|
||||
function renderNestedComponentInvocation(
|
||||
node: Extract<ViewNode, { type: "element" }>,
|
||||
ctx: CompCtx,
|
||||
): string {
|
||||
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)}"`;
|
||||
}
|
||||
|
||||
if (attr.boolean) {
|
||||
return ` ${attr.name}`;
|
||||
}
|
||||
|
||||
const rendered = ` ${attr.name}="` + `${compileAttrValue(attr.value, ctx)}"`;
|
||||
|
||||
if (!attr.value.includes("{") || !exprRefsState(attr.value, ctx.stateNames)) {
|
||||
return rendered;
|
||||
}
|
||||
|
||||
const marker = attrEscape(JSON.stringify([attr.name, attr.value]));
|
||||
|
||||
return rendered + ` data-wrn-bind-${bindIndex++}="${escLit(marker)}"`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
const loops = loopVarsOf(node);
|
||||
|
||||
const childCtx =
|
||||
loops.length > 0
|
||||
? {
|
||||
...ctx,
|
||||
loopVars: new Set([...(ctx.loopVars ?? []), ...loops]),
|
||||
}
|
||||
: ctx;
|
||||
|
||||
const inner = node.children.map((child) => renderComponentNode(child, childCtx)).join("");
|
||||
|
||||
return `<div data-component="${attrEscape(node.tag)}"${attrs}>${inner}</div>`;
|
||||
}
|
||||
|
||||
function ssrMarker(bindings: SsrBinding[], binding: RenderBinding): string {
|
||||
const marker = `<!--wrnexus-ssr:${bindings.length}-->`;
|
||||
bindings.push({ marker, ...binding });
|
||||
@@ -829,6 +939,9 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
|
||||
"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 (isComponentTag(node.tag)) {
|
||||
return renderNestedComponentInvocation(node, ctx);
|
||||
}
|
||||
|
||||
let bindIndex = 0;
|
||||
const staticClasses: string[] = [];
|
||||
|
||||
@@ -386,13 +386,15 @@ export function parseHtmlView(src: string, pos: number): { nodes: ViewNode[]; en
|
||||
let i = pos;
|
||||
|
||||
const isNameStart = (c: string): boolean => /[A-Za-z_]/.test(c);
|
||||
const isNamePart = (c: string): boolean => /[A-Za-z0-9_:.[\]%-]/.test(c);
|
||||
|
||||
const isTagNamePart = (c: string): boolean => /[A-Za-z0-9_$:.-]/.test(c);
|
||||
|
||||
const isAttributeNamePart = (c: string, next: string | undefined): boolean => {
|
||||
if (c === "/") {
|
||||
return next !== ">";
|
||||
}
|
||||
|
||||
return isNamePart(c);
|
||||
return /[A-Za-z0-9_$:.[\]%-]/.test(c);
|
||||
};
|
||||
const isWs = (c: string): boolean => c === " " || c === "\t" || c === "\n" || c === "\r";
|
||||
|
||||
@@ -429,18 +431,39 @@ export function parseHtmlView(src: string, pos: number): { nodes: ViewNode[]; en
|
||||
return value;
|
||||
};
|
||||
|
||||
const readName = (): string => {
|
||||
if (i >= src.length || !isNameStart(src[i]!)) return fail("Expected a tag or attribute name");
|
||||
const readTagName = (): string => {
|
||||
if (i >= src.length || !isNameStart(src[i]!)) {
|
||||
return fail("Expected a tag name");
|
||||
}
|
||||
|
||||
const start = i++;
|
||||
while (i < src.length && isAttributeNamePart(src[i], src[i + 1])) {
|
||||
|
||||
while (i < src.length && isTagNamePart(src[i]!)) {
|
||||
i++;
|
||||
}
|
||||
|
||||
return src.slice(start, i);
|
||||
};
|
||||
|
||||
const readAttributeName = (): string => {
|
||||
const first = src[i];
|
||||
|
||||
if (i >= src.length || (!isNameStart(first!) && first !== ":" && first !== "$")) {
|
||||
return fail("Expected an attribute name");
|
||||
}
|
||||
|
||||
const start = i++;
|
||||
|
||||
while (i < src.length && isAttributeNamePart(src[i]!, src[i + 1])) {
|
||||
i++;
|
||||
}
|
||||
|
||||
return src.slice(start, i);
|
||||
};
|
||||
|
||||
const parseTag = (): ViewNode => {
|
||||
i++; // consume '<'
|
||||
const tag = readName();
|
||||
const tag = readTagName();
|
||||
const attrs: Attr[] = [];
|
||||
|
||||
for (;;) {
|
||||
@@ -457,7 +480,7 @@ export function parseHtmlView(src: string, pos: number): { nodes: ViewNode[]; en
|
||||
}
|
||||
if (c === "@") {
|
||||
i++;
|
||||
const name = readName();
|
||||
const name = readAttributeName();
|
||||
skipWs();
|
||||
if (src[i] !== "=") return fail(`Expected '=' after @${name}`);
|
||||
i++;
|
||||
@@ -465,7 +488,7 @@ export function parseHtmlView(src: string, pos: number): { nodes: ViewNode[]; en
|
||||
attrs.push({ name, value: readQuoted(), event: true });
|
||||
continue;
|
||||
}
|
||||
const name = readName();
|
||||
const name = readAttributeName();
|
||||
skipWs();
|
||||
if (src[i] === "=") {
|
||||
i++;
|
||||
@@ -485,7 +508,7 @@ export function parseHtmlView(src: string, pos: number): { nodes: ViewNode[]; en
|
||||
if (src[i] !== "<" || src[i + 1] !== "/") return fail(`Expected </${tag}>`);
|
||||
i += 2;
|
||||
skipWs();
|
||||
const close = readName();
|
||||
const close = readTagName();
|
||||
if (close !== tag) return fail(`Mismatched </${close}>, expected </${tag}>`);
|
||||
skipWs();
|
||||
if (src[i] !== ">") return fail(`Expected '>' to close </${tag}>`);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { writeFileSync, mkdirSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { parse } from "../src/index.ts";
|
||||
import { generate, parse } from "../src/index.ts";
|
||||
import { compileWireFile } from "../src/index.ts";
|
||||
|
||||
let seq = 0;
|
||||
@@ -148,3 +148,67 @@ test("named slots pass through to the component output", () => {
|
||||
expect(out).toContain('<slot name="header">');
|
||||
expect(out).toContain("<slot></slot>");
|
||||
});
|
||||
|
||||
test("capitalized self-closing tags compile to component mounts", () => {
|
||||
const source = `
|
||||
page ComponentPage {
|
||||
view {
|
||||
<UserCard
|
||||
name="Ajay"
|
||||
role="Admin"
|
||||
/>
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const output = generate(parse(source));
|
||||
|
||||
expect(output).toContain('data-component="UserCard"');
|
||||
|
||||
expect(output).toContain('name="Ajay"');
|
||||
|
||||
expect(output).toContain('role="Admin"');
|
||||
|
||||
expect(output).not.toContain("<UserCard");
|
||||
});
|
||||
|
||||
test("capitalized component tags preserve slot children", () => {
|
||||
const source = `
|
||||
page SlotPage {
|
||||
view {
|
||||
<Card>
|
||||
<h2>Hello</h2>
|
||||
<p>Component content</p>
|
||||
</Card>
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const output = generate(parse(source));
|
||||
|
||||
expect(output).toContain('data-component="Card"');
|
||||
|
||||
expect(output).toContain("<h2>Hello</h2>");
|
||||
|
||||
expect(output).toContain("<p>Component content</p>");
|
||||
});
|
||||
|
||||
test("component tags work inside reusable components", () => {
|
||||
const source = `
|
||||
component ParentCard {
|
||||
props {
|
||||
title = "Hello"
|
||||
}
|
||||
|
||||
view {
|
||||
<ChildCard title="{title}" />
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const output = generate(parse(source));
|
||||
|
||||
expect(output).toContain('data-component="ChildCard"');
|
||||
|
||||
expect(output).toContain('title="${__wireAttr(title)}"');
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/core",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/csr",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/db",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-server",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/encryption",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/helpers",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"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.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/jwt",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/mobile",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/native",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/oauth",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/pubsub",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/queue",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/reactive",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/router",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ssr",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/styles",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/test",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/tracking",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ui",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/uploader",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/validation",
|
||||
"version": "0.2.26",
|
||||
"version": "0.2.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
Reference in New Issue
Block a user