release: WRNexusJS 0.8.3
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ai",
|
"name": "@wrnexus/ai",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
|
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/authz",
|
"name": "@wrnexus/authz",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/authz — part of the WrNexus framework.",
|
"description": "@wrnexus/authz — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
+17
-17
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/cli",
|
"name": "@wrnexus/cli",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/cli — part of the WrNexus framework.",
|
"description": "@wrnexus/cli — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -44,22 +44,22 @@
|
|||||||
"wrnexus": "./dist/index.js"
|
"wrnexus": "./dist/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.8.2",
|
"@wrnexus/core": "^0.8.3",
|
||||||
"@wrnexus/router": "^0.8.2",
|
"@wrnexus/router": "^0.8.3",
|
||||||
"@wrnexus/csr": "^0.8.2",
|
"@wrnexus/csr": "^0.8.3",
|
||||||
"@wrnexus/compiler": "^0.8.2",
|
"@wrnexus/compiler": "^0.8.3",
|
||||||
"@wrnexus/styles": "^0.8.2",
|
"@wrnexus/styles": "^0.8.3",
|
||||||
"@wrnexus/dev-server": "^0.8.2",
|
"@wrnexus/dev-server": "^0.8.3",
|
||||||
"@wrnexus/ui": "^0.8.2",
|
"@wrnexus/ui": "^0.8.3",
|
||||||
"@wrnexus/validation": "^0.8.2",
|
"@wrnexus/validation": "^0.8.3",
|
||||||
"@wrnexus/i18n": "^0.8.2",
|
"@wrnexus/i18n": "^0.8.3",
|
||||||
"@wrnexus/mcp": "^0.8.2",
|
"@wrnexus/mcp": "^0.8.3",
|
||||||
"@wrnexus/playground": "^0.8.2",
|
"@wrnexus/playground": "^0.8.3",
|
||||||
"@wrnexus/db": "^0.8.2",
|
"@wrnexus/db": "^0.8.3",
|
||||||
"@wrnexus/plugin": "^0.8.2",
|
"@wrnexus/plugin": "^0.8.3",
|
||||||
"@wrnexus/syntax": "^0.8.2",
|
"@wrnexus/syntax": "^0.8.3",
|
||||||
"@wrnexus/typecheck": "^0.8.2",
|
"@wrnexus/typecheck": "^0.8.3",
|
||||||
"@wrnexus/security": "^0.8.2",
|
"@wrnexus/security": "^0.8.3",
|
||||||
"selfsigned": "^5.5.0"
|
"selfsigned": "^5.5.0"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/compiler",
|
"name": "@wrnexus/compiler",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/compiler — part of the WrNexus framework.",
|
"description": "@wrnexus/compiler — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -37,10 +37,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/csr": "^0.8.2",
|
"@wrnexus/csr": "^0.8.3",
|
||||||
"@wrnexus/syntax": "^0.8.2",
|
"@wrnexus/syntax": "^0.8.3",
|
||||||
"@wrnexus/store": "^0.8.2",
|
"@wrnexus/store": "^0.8.3",
|
||||||
"@wrnexus/validation": "^0.8.2"
|
"@wrnexus/validation": "^0.8.3"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/core",
|
"name": "@wrnexus/core",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/core — part of the WrNexus framework.",
|
"description": "@wrnexus/core — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/csr",
|
"name": "@wrnexus/csr",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/csr — part of the WrNexus framework.",
|
"description": "@wrnexus/csr — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.8.2"
|
"@wrnexus/core": "^0.8.3"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/db",
|
"name": "@wrnexus/db",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Typed database drivers, migrations, instrumentation, repositories, pagination, and transaction helpers.",
|
"description": "Typed database drivers, migrations, instrumentation, repositories, pagination, and transaction helpers.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/dev-server",
|
"name": "@wrnexus/dev-server",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/dev-server — part of the WrNexus framework.",
|
"description": "@wrnexus/dev-server — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -41,25 +41,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.8.2",
|
"@wrnexus/core": "^0.8.3",
|
||||||
"@wrnexus/dev-toolbar": "^0.8.2",
|
"@wrnexus/dev-toolbar": "^0.8.3",
|
||||||
"@wrnexus/router": "^0.8.2",
|
"@wrnexus/router": "^0.8.3",
|
||||||
"@wrnexus/ssr": "^0.8.2",
|
"@wrnexus/ssr": "^0.8.3",
|
||||||
"@wrnexus/csr": "^0.8.2",
|
"@wrnexus/csr": "^0.8.3",
|
||||||
"@wrnexus/compiler": "^0.8.2",
|
"@wrnexus/compiler": "^0.8.3",
|
||||||
"@wrnexus/styles": "^0.8.2",
|
"@wrnexus/styles": "^0.8.3",
|
||||||
"@wrnexus/ui": "^0.8.2",
|
"@wrnexus/ui": "^0.8.3",
|
||||||
"@wrnexus/validation": "^0.8.2",
|
"@wrnexus/validation": "^0.8.3",
|
||||||
"@wrnexus/i18n": "^0.8.2",
|
"@wrnexus/i18n": "^0.8.3",
|
||||||
"@wrnexus/db": "^0.8.2",
|
"@wrnexus/db": "^0.8.3",
|
||||||
"@wrnexus/pubsub": "^0.8.2",
|
"@wrnexus/pubsub": "^0.8.3",
|
||||||
"@wrnexus/uploader": "^0.8.2",
|
"@wrnexus/uploader": "^0.8.3",
|
||||||
"@wrnexus/plugin": "^0.8.2",
|
"@wrnexus/plugin": "^0.8.3",
|
||||||
"@wrnexus/store": "^0.8.2",
|
"@wrnexus/store": "^0.8.3",
|
||||||
"@wrnexus/security": "^0.8.2",
|
"@wrnexus/security": "^0.8.3",
|
||||||
"@wrnexus/observability": "^0.8.2",
|
"@wrnexus/observability": "^0.8.3",
|
||||||
"@wrnexus/cache": "^0.8.2",
|
"@wrnexus/cache": "^0.8.3",
|
||||||
"@wrnexus/pwa": "^0.8.2"
|
"@wrnexus/pwa": "^0.8.3"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/dev-toolbar",
|
"name": "@wrnexus/dev-toolbar",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/dev-toolbar — part of the WrNexus framework.",
|
"description": "@wrnexus/dev-toolbar — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/encryption",
|
"name": "@wrnexus/encryption",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Authenticated encryption, key rotation, hashing, and optional application-layer encrypted HTTP envelopes.",
|
"description": "Authenticated encryption, key rotation, hashing, and optional application-layer encrypted HTTP envelopes.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.8.2"
|
"@wrnexus/core": "^0.8.3"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/helpers",
|
"name": "@wrnexus/helpers",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
|
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.8.2"
|
"@wrnexus/core": "^0.8.3"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/i18n",
|
"name": "@wrnexus/i18n",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Locale loading, fallback resolution, SSR/browser translations, formatters, and WRNexusJS language components.",
|
"description": "Locale loading, fallback resolution, SSR/browser translations, formatters, and WRNexusJS language components.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -42,9 +42,9 @@
|
|||||||
"./components/*": "./components/*"
|
"./components/*": "./components/*"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.8.2",
|
"@wrnexus/core": "^0.8.3",
|
||||||
"@wrnexus/plugin": "^0.8.2",
|
"@wrnexus/plugin": "^0.8.3",
|
||||||
"@wrnexus/ui": "^0.8.2"
|
"@wrnexus/ui": "^0.8.3"
|
||||||
},
|
},
|
||||||
"wrnexus": {
|
"wrnexus": {
|
||||||
"plugin": {
|
"plugin": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/jwt",
|
"name": "@wrnexus/jwt",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "HS256 JSON Web Tokens, key rotation, access/refresh helpers, scopes, cookies, and auth middleware.",
|
"description": "HS256 JSON Web Tokens, key rotation, access/refresh helpers, scopes, cookies, and auth middleware.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.8.2"
|
"@wrnexus/core": "^0.8.3"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/mobile",
|
"name": "@wrnexus/mobile",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/mobile — part of the WrNexus framework.",
|
"description": "@wrnexus/mobile — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/native": "^0.8.2"
|
"@wrnexus/native": "^0.8.3"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/native",
|
"name": "@wrnexus/native",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/native — part of the WrNexus framework.",
|
"description": "@wrnexus/native — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/oauth",
|
"name": "@wrnexus/oauth",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/oauth — part of the WrNexus framework.",
|
"description": "@wrnexus/oauth — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/jwt": "^0.8.2"
|
"@wrnexus/jwt": "^0.8.3"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/plugin",
|
"name": "@wrnexus/plugin",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/plugin — part of the WrNexus framework.",
|
"description": "@wrnexus/plugin — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/syntax": "^0.8.2"
|
"@wrnexus/syntax": "^0.8.3"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/pubsub",
|
"name": "@wrnexus/pubsub",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/pubsub — part of the WrNexus framework.",
|
"description": "@wrnexus/pubsub — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/queue",
|
"name": "@wrnexus/queue",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/queue — part of the WrNexus framework.",
|
"description": "@wrnexus/queue — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.8.2"
|
"@wrnexus/core": "^0.8.3"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/reactive",
|
"name": "@wrnexus/reactive",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/reactive — part of the WrNexus framework.",
|
"description": "@wrnexus/reactive — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/router",
|
"name": "@wrnexus/router",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/router — part of the WrNexus framework.",
|
"description": "@wrnexus/router — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -37,8 +37,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/compiler": "^0.8.2",
|
"@wrnexus/compiler": "^0.8.3",
|
||||||
"@wrnexus/core": "^0.8.2"
|
"@wrnexus/core": "^0.8.3"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ssr",
|
"name": "@wrnexus/ssr",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/ssr — part of the WrNexus framework.",
|
"description": "@wrnexus/ssr — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -45,9 +45,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.8.2",
|
"@wrnexus/core": "^0.8.3",
|
||||||
"@wrnexus/store": "^0.8.2",
|
"@wrnexus/store": "^0.8.3",
|
||||||
"@wrnexus/security": "^0.8.2"
|
"@wrnexus/security": "^0.8.3"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/styles",
|
"name": "@wrnexus/styles",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/styles — part of the WrNexus framework.",
|
"description": "@wrnexus/styles — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -37,9 +37,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/uploader": "^0.8.2",
|
"@wrnexus/uploader": "^0.8.3",
|
||||||
"@wrnexus/core": "^0.8.2",
|
"@wrnexus/core": "^0.8.3",
|
||||||
"@wrnexus/plugin": "^0.8.2"
|
"@wrnexus/plugin": "^0.8.3"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/syntax",
|
"name": "@wrnexus/syntax",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/syntax — part of the WrNexus framework.",
|
"description": "@wrnexus/syntax — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/test",
|
"name": "@wrnexus/test",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/test — part of the WrNexus framework.",
|
"description": "@wrnexus/test — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/tracking",
|
"name": "@wrnexus/tracking",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/tracking — part of the WrNexus framework.",
|
"description": "@wrnexus/tracking — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ui",
|
"name": "@wrnexus/ui",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/ui — part of the WrNexus framework.",
|
"description": "@wrnexus/ui — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
"./ui.css": "./ui.css"
|
"./ui.css": "./ui.css"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.8.2"
|
"@wrnexus/core": "^0.8.3"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/uploader",
|
"name": "@wrnexus/uploader",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Secure upload drivers, policies, client runtime, helper functions, and reusable upload components.",
|
"description": "Secure upload drivers, policies, client runtime, helper functions, and reusable upload components.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -42,9 +42,9 @@
|
|||||||
"./components/*": "./components/*"
|
"./components/*": "./components/*"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.8.2",
|
"@wrnexus/core": "^0.8.3",
|
||||||
"@wrnexus/plugin": "^0.8.2",
|
"@wrnexus/plugin": "^0.8.3",
|
||||||
"@wrnexus/ui": "^0.8.2"
|
"@wrnexus/ui": "^0.8.3"
|
||||||
},
|
},
|
||||||
"wrnexus": {
|
"wrnexus": {
|
||||||
"plugin": {
|
"plugin": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/validation",
|
"name": "@wrnexus/validation",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Shared server/browser schemas, validation helpers, form runtime, and reusable error components.",
|
"description": "Shared server/browser schemas, validation helpers, form runtime, and reusable error components.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -42,9 +42,9 @@
|
|||||||
"./components/*": "./components/*"
|
"./components/*": "./components/*"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.8.2",
|
"@wrnexus/core": "^0.8.3",
|
||||||
"@wrnexus/plugin": "^0.8.2",
|
"@wrnexus/plugin": "^0.8.3",
|
||||||
"@wrnexus/ui": "^0.8.2"
|
"@wrnexus/ui": "^0.8.3"
|
||||||
},
|
},
|
||||||
"wrnexus": {
|
"wrnexus": {
|
||||||
"plugin": {
|
"plugin": {
|
||||||
|
|||||||
@@ -3,6 +3,26 @@
|
|||||||
All notable framework changes are recorded here. Every release must also include an idempotent
|
All notable framework changes are recorded here. Every release must also include an idempotent
|
||||||
entry in the CLI migration registry.
|
entry in the CLI migration registry.
|
||||||
|
|
||||||
|
## 0.8.3 - 2026-08-03
|
||||||
|
|
||||||
|
- Bundled hydrated browser modules so package aliases and filesystem imports do not leak into
|
||||||
|
browser-native modules.
|
||||||
|
- Fixed SSR computed values, Async branch aliases, typed object props in loops, reactive route
|
||||||
|
state, loader invalidation, realtime identity isolation, and client-function hydration.
|
||||||
|
- Fixed template-literal parsing in the reactive fallback evaluator and removed the unsupported
|
||||||
|
default `unload` Permissions Policy directive.
|
||||||
|
- Made official package components compatible with strict explicit imports and added regression
|
||||||
|
coverage for the TeamSpace application failures.
|
||||||
|
- Fixed Bun 1.3.14 and TypeScript 5.9 release-gate compatibility in client bundling, alias
|
||||||
|
resolution, and Happy DOM event tests.
|
||||||
|
- Fixed ESLint compatibility in Node release scripts by declaring Node globals and using explicit
|
||||||
|
regex space quantifiers.
|
||||||
|
- Fixed package component import validation after workspace installation by excluding generated and
|
||||||
|
dependency directories such as nested `node_modules` from first-party source scans.
|
||||||
|
- Made the VS Code embedded compiler freshness check deterministic across TypeScript patch
|
||||||
|
versions, Windows/Linux line endings, and workspace environments by verifying normalized source
|
||||||
|
and generator fingerprints while retaining exact-output checks for the same TypeScript version.
|
||||||
|
|
||||||
## 0.8.0 - 2026-08-02
|
## 0.8.0 - 2026-08-02
|
||||||
|
|
||||||
- Added package-owned helper and component kits across all 39 framework packages.
|
- Added package-owned helper and component kits across all 39 framework packages.
|
||||||
|
|||||||
@@ -102,11 +102,11 @@
|
|||||||
},
|
},
|
||||||
"packages/ai": {
|
"packages/ai": {
|
||||||
"name": "@wrnexus/ai",
|
"name": "@wrnexus/ai",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
},
|
},
|
||||||
"packages/auth": {
|
"packages/auth": {
|
||||||
"name": "@wrnexus/auth",
|
"name": "@wrnexus/auth",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/authz": "workspace:*",
|
"@wrnexus/authz": "workspace:*",
|
||||||
"@wrnexus/captcha": "workspace:*",
|
"@wrnexus/captcha": "workspace:*",
|
||||||
@@ -128,22 +128,22 @@
|
|||||||
},
|
},
|
||||||
"packages/authz": {
|
"packages/authz": {
|
||||||
"name": "@wrnexus/authz",
|
"name": "@wrnexus/authz",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
},
|
},
|
||||||
"packages/benchmark": {
|
"packages/benchmark": {
|
||||||
"name": "@wrnexus/benchmark",
|
"name": "@wrnexus/benchmark",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
},
|
},
|
||||||
"packages/cache": {
|
"packages/cache": {
|
||||||
"name": "@wrnexus/cache",
|
"name": "@wrnexus/cache",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/captcha": {
|
"packages/captcha": {
|
||||||
"name": "@wrnexus/captcha",
|
"name": "@wrnexus/captcha",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
"@wrnexus/plugin": "workspace:*",
|
"@wrnexus/plugin": "workspace:*",
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@wrnexus/cli",
|
"name": "@wrnexus/cli",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"bin": {
|
"bin": {
|
||||||
"wrnexus": "src/index.ts",
|
"wrnexus": "src/index.ts",
|
||||||
},
|
},
|
||||||
@@ -184,7 +184,7 @@
|
|||||||
},
|
},
|
||||||
"packages/compiler": {
|
"packages/compiler": {
|
||||||
"name": "@wrnexus/compiler",
|
"name": "@wrnexus/compiler",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/csr": "workspace:*",
|
"@wrnexus/csr": "workspace:*",
|
||||||
"@wrnexus/store": "workspace:*",
|
"@wrnexus/store": "workspace:*",
|
||||||
@@ -194,7 +194,7 @@
|
|||||||
},
|
},
|
||||||
"packages/content": {
|
"packages/content": {
|
||||||
"name": "@wrnexus/content",
|
"name": "@wrnexus/content",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
@@ -202,18 +202,18 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@wrnexus/core",
|
"name": "@wrnexus/core",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
},
|
},
|
||||||
"packages/csr": {
|
"packages/csr": {
|
||||||
"name": "@wrnexus/csr",
|
"name": "@wrnexus/csr",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/db": {
|
"packages/db": {
|
||||||
"name": "@wrnexus/db",
|
"name": "@wrnexus/db",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
@@ -221,7 +221,7 @@
|
|||||||
},
|
},
|
||||||
"packages/dev-server": {
|
"packages/dev-server": {
|
||||||
"name": "@wrnexus/dev-server",
|
"name": "@wrnexus/dev-server",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/cache": "workspace:*",
|
"@wrnexus/cache": "workspace:*",
|
||||||
"@wrnexus/compiler": "workspace:*",
|
"@wrnexus/compiler": "workspace:*",
|
||||||
@@ -246,7 +246,7 @@
|
|||||||
},
|
},
|
||||||
"packages/dev-toolbar": {
|
"packages/dev-toolbar": {
|
||||||
"name": "@wrnexus/dev-toolbar",
|
"name": "@wrnexus/dev-toolbar",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
@@ -254,7 +254,7 @@
|
|||||||
},
|
},
|
||||||
"packages/encryption": {
|
"packages/encryption": {
|
||||||
"name": "@wrnexus/encryption",
|
"name": "@wrnexus/encryption",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
@@ -265,21 +265,21 @@
|
|||||||
},
|
},
|
||||||
"packages/graphql": {
|
"packages/graphql": {
|
||||||
"name": "@wrnexus/graphql",
|
"name": "@wrnexus/graphql",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/plugin": "workspace:*",
|
"@wrnexus/plugin": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/helpers": {
|
"packages/helpers": {
|
||||||
"name": "@wrnexus/helpers",
|
"name": "@wrnexus/helpers",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/i18n": {
|
"packages/i18n": {
|
||||||
"name": "@wrnexus/i18n",
|
"name": "@wrnexus/i18n",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
"@wrnexus/plugin": "workspace:*",
|
"@wrnexus/plugin": "workspace:*",
|
||||||
@@ -293,7 +293,7 @@
|
|||||||
},
|
},
|
||||||
"packages/identity": {
|
"packages/identity": {
|
||||||
"name": "@wrnexus/identity",
|
"name": "@wrnexus/identity",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/auth": "workspace:*",
|
"@wrnexus/auth": "workspace:*",
|
||||||
"@wrnexus/authz": "workspace:*",
|
"@wrnexus/authz": "workspace:*",
|
||||||
@@ -302,7 +302,7 @@
|
|||||||
},
|
},
|
||||||
"packages/image": {
|
"packages/image": {
|
||||||
"name": "@wrnexus/image",
|
"name": "@wrnexus/image",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/plugin": "workspace:*",
|
"@wrnexus/plugin": "workspace:*",
|
||||||
"@wrnexus/security": "workspace:*",
|
"@wrnexus/security": "workspace:*",
|
||||||
@@ -322,7 +322,7 @@
|
|||||||
},
|
},
|
||||||
"packages/jwt": {
|
"packages/jwt": {
|
||||||
"name": "@wrnexus/jwt",
|
"name": "@wrnexus/jwt",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
@@ -333,7 +333,7 @@
|
|||||||
},
|
},
|
||||||
"packages/language-server": {
|
"packages/language-server": {
|
||||||
"name": "@wrnexus/language-server",
|
"name": "@wrnexus/language-server",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"bin": {
|
"bin": {
|
||||||
"wrnexus-language-server": "src/server.ts",
|
"wrnexus-language-server": "src/server.ts",
|
||||||
},
|
},
|
||||||
@@ -344,39 +344,39 @@
|
|||||||
},
|
},
|
||||||
"packages/mcp": {
|
"packages/mcp": {
|
||||||
"name": "@wrnexus/mcp",
|
"name": "@wrnexus/mcp",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"bin": {
|
"bin": {
|
||||||
"wrnexus-mcp": "src/stdio.ts",
|
"wrnexus-mcp": "src/stdio.ts",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/mobile": {
|
"packages/mobile": {
|
||||||
"name": "@wrnexus/mobile",
|
"name": "@wrnexus/mobile",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/native": "workspace:*",
|
"@wrnexus/native": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/native": {
|
"packages/native": {
|
||||||
"name": "@wrnexus/native",
|
"name": "@wrnexus/native",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
},
|
},
|
||||||
"packages/oauth": {
|
"packages/oauth": {
|
||||||
"name": "@wrnexus/oauth",
|
"name": "@wrnexus/oauth",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/jwt": "workspace:*",
|
"@wrnexus/jwt": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/observability": {
|
"packages/observability": {
|
||||||
"name": "@wrnexus/observability",
|
"name": "@wrnexus/observability",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/playground": {
|
"packages/playground": {
|
||||||
"name": "@wrnexus/playground",
|
"name": "@wrnexus/playground",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/compiler": "workspace:*",
|
"@wrnexus/compiler": "workspace:*",
|
||||||
"@wrnexus/syntax": "workspace:*",
|
"@wrnexus/syntax": "workspace:*",
|
||||||
@@ -384,18 +384,18 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@wrnexus/plugin",
|
"name": "@wrnexus/plugin",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/syntax": "workspace:*",
|
"@wrnexus/syntax": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/pubsub": {
|
"packages/pubsub": {
|
||||||
"name": "@wrnexus/pubsub",
|
"name": "@wrnexus/pubsub",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
},
|
},
|
||||||
"packages/pwa": {
|
"packages/pwa": {
|
||||||
"name": "@wrnexus/pwa",
|
"name": "@wrnexus/pwa",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
@@ -403,18 +403,18 @@
|
|||||||
},
|
},
|
||||||
"packages/queue": {
|
"packages/queue": {
|
||||||
"name": "@wrnexus/queue",
|
"name": "@wrnexus/queue",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/reactive": {
|
"packages/reactive": {
|
||||||
"name": "@wrnexus/reactive",
|
"name": "@wrnexus/reactive",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
},
|
},
|
||||||
"packages/realtime": {
|
"packages/realtime": {
|
||||||
"name": "@wrnexus/realtime",
|
"name": "@wrnexus/realtime",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
"@wrnexus/plugin": "workspace:*",
|
"@wrnexus/plugin": "workspace:*",
|
||||||
@@ -428,7 +428,7 @@
|
|||||||
},
|
},
|
||||||
"packages/router": {
|
"packages/router": {
|
||||||
"name": "@wrnexus/router",
|
"name": "@wrnexus/router",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/compiler": "workspace:*",
|
"@wrnexus/compiler": "workspace:*",
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
@@ -436,14 +436,14 @@
|
|||||||
},
|
},
|
||||||
"packages/security": {
|
"packages/security": {
|
||||||
"name": "@wrnexus/security",
|
"name": "@wrnexus/security",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/ssr": {
|
"packages/ssr": {
|
||||||
"name": "@wrnexus/ssr",
|
"name": "@wrnexus/ssr",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
"@wrnexus/security": "workspace:*",
|
"@wrnexus/security": "workspace:*",
|
||||||
@@ -452,11 +452,11 @@
|
|||||||
},
|
},
|
||||||
"packages/store": {
|
"packages/store": {
|
||||||
"name": "@wrnexus/store",
|
"name": "@wrnexus/store",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
},
|
},
|
||||||
"packages/styles": {
|
"packages/styles": {
|
||||||
"name": "@wrnexus/styles",
|
"name": "@wrnexus/styles",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
"@wrnexus/plugin": "workspace:*",
|
"@wrnexus/plugin": "workspace:*",
|
||||||
@@ -465,19 +465,19 @@
|
|||||||
},
|
},
|
||||||
"packages/syntax": {
|
"packages/syntax": {
|
||||||
"name": "@wrnexus/syntax",
|
"name": "@wrnexus/syntax",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
},
|
},
|
||||||
"packages/test": {
|
"packages/test": {
|
||||||
"name": "@wrnexus/test",
|
"name": "@wrnexus/test",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
},
|
},
|
||||||
"packages/tracking": {
|
"packages/tracking": {
|
||||||
"name": "@wrnexus/tracking",
|
"name": "@wrnexus/tracking",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
},
|
},
|
||||||
"packages/typecheck": {
|
"packages/typecheck": {
|
||||||
"name": "@wrnexus/typecheck",
|
"name": "@wrnexus/typecheck",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/syntax": "workspace:*",
|
"@wrnexus/syntax": "workspace:*",
|
||||||
"typescript": "^5.5.0",
|
"typescript": "^5.5.0",
|
||||||
@@ -485,14 +485,14 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@wrnexus/ui",
|
"name": "@wrnexus/ui",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/uploader": {
|
"packages/uploader": {
|
||||||
"name": "@wrnexus/uploader",
|
"name": "@wrnexus/uploader",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
"@wrnexus/plugin": "workspace:*",
|
"@wrnexus/plugin": "workspace:*",
|
||||||
@@ -506,7 +506,7 @@
|
|||||||
},
|
},
|
||||||
"packages/validation": {
|
"packages/validation": {
|
||||||
"name": "@wrnexus/validation",
|
"name": "@wrnexus/validation",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
"@wrnexus/plugin": "workspace:*",
|
"@wrnexus/plugin": "workspace:*",
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Upgrade to WRNexusJS 0.8.3
|
||||||
|
|
||||||
|
WRNexusJS 0.8.3 is a framework stability release. It fixes browser hydration modules,
|
||||||
|
SSR computed values, Async scopes, typed props in loops, realtime connection identity,
|
||||||
|
loader invalidation, strict component imports, and development diagnostics.
|
||||||
|
|
||||||
|
## Application migration
|
||||||
|
|
||||||
|
No application source rewrite is required. The `0.8.3-client-ssr-realtime-stability`
|
||||||
|
migration is intentionally idempotent and records the upgrade in the CLI migration registry.
|
||||||
|
|
||||||
|
After updating all `@wrnexus/*` dependencies to `0.8.3`, remove generated artifacts so the
|
||||||
|
new browser-module cache format is used:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm -rf .wrnexus dist
|
||||||
|
bun install
|
||||||
|
bun run check
|
||||||
|
```
|
||||||
|
|
||||||
|
On Windows PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Remove-Item -Recurse -Force .wrnexus, dist -ErrorAction SilentlyContinue
|
||||||
|
bun install
|
||||||
|
bun run check
|
||||||
|
```
|
||||||
|
|
||||||
|
## Behavior changes
|
||||||
|
|
||||||
|
- Hydrated `.wrn` browser modules are bundled before they are served or emitted. Browser code
|
||||||
|
no longer receives unresolved bare package imports or `file:` filesystem imports.
|
||||||
|
- Generated native `client` and `shared` functions take precedence over the CSP-safe expression
|
||||||
|
fallback. The fallback also accepts template literals inside call arguments.
|
||||||
|
- Page computed values are declared for SSR in dependency order. Computed cycles fail with
|
||||||
|
`WRN-COMPUTED-CYCLE`.
|
||||||
|
- Whole-expression object and array props inside `{#each}` are serialized as typed props.
|
||||||
|
- `<Success data="...">` and `<Error error="...">` aliases have branch-local scope.
|
||||||
|
- Realtime sockets are keyed by room plus normalized query identity and reconnect when identity
|
||||||
|
attributes change.
|
||||||
|
- `$route` and `route` expose reactive pathname, query, hash, and params to hydrated views.
|
||||||
|
- Uploader success events dispatch cache invalidation tags. Matching client Async boundaries
|
||||||
|
refresh automatically.
|
||||||
|
- The unsupported default `unload` Permissions Policy directive has been removed.
|
||||||
|
- Official package components now satisfy `imports.mode = "explicit"`.
|
||||||
|
|
||||||
|
## Publishing 0.8.3
|
||||||
|
|
||||||
|
Run the complete release gate with Bun before publishing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
bun run check:production
|
||||||
|
bun run release:prepare
|
||||||
|
bun run release:private
|
||||||
|
```
|
||||||
|
|
||||||
|
`release:prepare` rebuilds package distributions and verifies that version `0.8.3` has a CLI
|
||||||
|
migration entry. Do not publish stale `dist` directories from an older release.
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# WRNexusJS 0.8.3 validation report
|
||||||
|
|
||||||
|
## Validated in the prepared source archive
|
||||||
|
|
||||||
|
- All 47 root/package manifests use version `0.8.3` and all non-workspace internal dependency
|
||||||
|
ranges resolve to `0.8.3`.
|
||||||
|
- The CLI migration registry contains `0.8.3-client-ssr-realtime-stability`.
|
||||||
|
- All official package `.wrn` files pass parse/code-generation checks.
|
||||||
|
- All official reusable components pass explicit-import validation.
|
||||||
|
- The UI visual contract, public API baseline, workspace repair check, security framework audit,
|
||||||
|
and ASVS policy checks pass.
|
||||||
|
- Regression scripts verify request-derived SSR computed values, view-only browser imports,
|
||||||
|
typed loop props, Async aliases, template literals in fallback expressions, native client
|
||||||
|
function preservation, settings persistence, realtime identity isolation, and the TeamSpace
|
||||||
|
example browser-module graph.
|
||||||
|
- The generated VS Code compiler bundle was rebuilt from the updated syntax/compiler source.
|
||||||
|
|
||||||
|
## Release-candidate corrections
|
||||||
|
|
||||||
|
After the first Windows release-gate run, the source archive was corrected for four TypeScript
|
||||||
|
errors:
|
||||||
|
|
||||||
|
- Removed the unsupported `write` property from `Bun.build()` configuration objects while keeping
|
||||||
|
in-memory artifact reads through `BuildOutput.outputs`.
|
||||||
|
- Typed the merged import-alias map as `Record<string, string>`.
|
||||||
|
- Cast Happy DOM `CustomEvent` values to the exact `Window.dispatchEvent` parameter type in CSR
|
||||||
|
tests.
|
||||||
|
- Removed local `.env` and `.env.uat` files from the source archive; equivalent `.example`
|
||||||
|
templates remain included.
|
||||||
|
|
||||||
|
A second Windows release-gate run exposed ten ESLint errors in release helper scripts. The source
|
||||||
|
archive was corrected by importing Node globals explicitly in `scripts/check-component-imports.mjs`
|
||||||
|
and replacing hard-counted regex spaces with `{4}` and `{6}` quantifiers in
|
||||||
|
`scripts/validate-0.8.mjs` and `update-package-versions.mjs`.
|
||||||
|
|
||||||
|
A third Windows release-gate run showed that the package component validator was traversing
|
||||||
|
workspace-installed dependency copies under nested `node_modules` directories. Those files are not
|
||||||
|
first-party package sources and were being checked against the manifest of the containing workspace
|
||||||
|
package. The validator now excludes dependency and generated directories (`node_modules`, `dist`,
|
||||||
|
`coverage`, `.git`, `.wrnexus`, and `.publish`) while still scanning every official source component.
|
||||||
|
A regression fixture confirms that an invalid nested dependency component is ignored and the 146
|
||||||
|
official source files remain validated.
|
||||||
|
|
||||||
|
A fourth Windows release-gate run found that `editors/vscode/src/compiler.cjs` was stale after the
|
||||||
|
final compiler changes. The bundle was regenerated from all 26 current syntax/compiler TypeScript
|
||||||
|
modules. A fifth Windows run then exposed a cross-environment false positive: the archive had been
|
||||||
|
generated with TypeScript 5.8.3 while the frozen release install uses TypeScript 5.9.3, and the old
|
||||||
|
checker compared transpiled output byte-for-byte. The editor compiler generator now normalizes source
|
||||||
|
and line endings, emits source and generator SHA-256 fingerprints, and performs an exact-output check
|
||||||
|
when the TypeScript version matches. When only the TypeScript patch version differs, it validates the
|
||||||
|
source/generator fingerprints instead. Genuine source changes still fail the freshness gate.
|
||||||
|
|
||||||
|
The corrected helper scripts pass Node syntax checks. Package component import validation passes
|
||||||
|
for all 146 official WRN files. The 0.8 validation script advances through its source-only checks
|
||||||
|
in the preparation environment and stops only when dependencies excluded from the release archive
|
||||||
|
are required. The complete TypeScript and Bun test gates still need to be rerun on the release
|
||||||
|
machine after `bun install --frozen-lockfile`.
|
||||||
|
|
||||||
|
## Required final release verification
|
||||||
|
|
||||||
|
The preparation environment did not include the Bun executable. Therefore the complete Bun test,
|
||||||
|
lint, formatting, package build, editor bundle comparison, staged-consumer, and publish dry-run
|
||||||
|
must still be executed on the release machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
bun run check:production
|
||||||
|
bun run release:prepare
|
||||||
|
bun run validate:staging
|
||||||
|
bun run test:staged-consumers
|
||||||
|
```
|
||||||
|
|
||||||
|
A release should not be published if any command above fails.
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
"packages/ui/components/AdvancedRangeSlider.wrn": "e78b5cf825503d0cd306c6e490d71317ee3db1844882d13b500df1f7f1c408e1",
|
"packages/ui/components/AdvancedRangeSlider.wrn": "e78b5cf825503d0cd306c6e490d71317ee3db1844882d13b500df1f7f1c408e1",
|
||||||
"packages/ui/components/AdvancedSelect.wrn": "29d43f8019a3e0e98af7cf12440f41523665535a39a973441b4faea78e67114a",
|
"packages/ui/components/AdvancedSelect.wrn": "29d43f8019a3e0e98af7cf12440f41523665535a39a973441b4faea78e67114a",
|
||||||
"packages/ui/components/AnnouncementBar.wrn": "5946ef5eb646f7414056f59d9cfe14680aa3c935acc9a1ca6528dc53b23caa48",
|
"packages/ui/components/AnnouncementBar.wrn": "5946ef5eb646f7414056f59d9cfe14680aa3c935acc9a1ca6528dc53b23caa48",
|
||||||
"packages/ui/components/AuthForm.wrn": "5441adc5c4fd94621633f3a178442fd7970de3afb0dd3827b7aae6cd43545e40",
|
"packages/ui/components/AuthForm.wrn": "e6ab0cc4a557b3fd41528368c5aa7aaf39e9adcd49d9907df8d26d77bc515870",
|
||||||
"packages/ui/components/AuthSplitLayout.wrn": "72b82495dd14d40a2d594ccdcf3adc925d1567c155750fb1072347b8845aab7b",
|
"packages/ui/components/AuthSplitLayout.wrn": "72b82495dd14d40a2d594ccdcf3adc925d1567c155750fb1072347b8845aab7b",
|
||||||
"packages/ui/components/AvatarGroup.wrn": "9c583f567213f8e1ad4506a7d2abc3fb462d11679fd2196d3828030f28a28964",
|
"packages/ui/components/AvatarGroup.wrn": "9c583f567213f8e1ad4506a7d2abc3fb462d11679fd2196d3828030f28a28964",
|
||||||
"packages/ui/components/BackToTop.wrn": "b12c56ec8b7aabad68cbdc47f4b3237e2c21a61398f0aa51f9add6229b7357cd",
|
"packages/ui/components/BackToTop.wrn": "b12c56ec8b7aabad68cbdc47f4b3237e2c21a61398f0aa51f9add6229b7357cd",
|
||||||
@@ -39,15 +39,15 @@
|
|||||||
"packages/ui/components/Drawer.wrn": "7ecfc54494402e8758eb3f648418df70b6cde598f38836a620a1fa179d1bb1bd",
|
"packages/ui/components/Drawer.wrn": "7ecfc54494402e8758eb3f648418df70b6cde598f38836a620a1fa179d1bb1bd",
|
||||||
"packages/ui/components/Dropdown.wrn": "703c354dfb304ad640a600398317b0a5eefeea326df8a1fc51c87e0ed3c26ebc",
|
"packages/ui/components/Dropdown.wrn": "703c354dfb304ad640a600398317b0a5eefeea326df8a1fc51c87e0ed3c26ebc",
|
||||||
"packages/ui/components/FeatureCard.wrn": "cf122df37d5de72a9556fea501b13b8bfad49ee4a5e88daec2a32d204ee9e784",
|
"packages/ui/components/FeatureCard.wrn": "cf122df37d5de72a9556fea501b13b8bfad49ee4a5e88daec2a32d204ee9e784",
|
||||||
"packages/ui/components/FeatureGrid.wrn": "b9c793e47f6a86fddc0f42c010544002509a11256c92c030ae29743ab0083a55",
|
"packages/ui/components/FeatureGrid.wrn": "9e3c0440d08c6982861d3e732eeb20f1023413a58243844dfb9d7033bac13d2b",
|
||||||
"packages/ui/components/FeatureIconCard.wrn": "7d053cf642ab572a254383a72fb60323118a6b41b433696cf6ac62857dea7d5e",
|
"packages/ui/components/FeatureIconCard.wrn": "afe56213543dd78932b060fa19c4e645e558b211b380c42b6d4b0d18e9d543df",
|
||||||
"packages/ui/components/FileInput.wrn": "8b63811deb90a03763620bedf0d5d3c7d05a20b8ae34292eaface9b756b32ecd",
|
"packages/ui/components/FileInput.wrn": "8b63811deb90a03763620bedf0d5d3c7d05a20b8ae34292eaface9b756b32ecd",
|
||||||
"packages/ui/components/FileUpload.wrn": "3346d8978e134c3a1e6bc742201a3fbb0cffb89da40da6fcd856a2adbb971393",
|
"packages/ui/components/FileUpload.wrn": "3346d8978e134c3a1e6bc742201a3fbb0cffb89da40da6fcd856a2adbb971393",
|
||||||
"packages/ui/components/FileUploadProgress.wrn": "83bcc8558c70fbe0e0670642f65998abd222c235f1031be46bd7c65e6b5c5154",
|
"packages/ui/components/FileUploadProgress.wrn": "83bcc8558c70fbe0e0670642f65998abd222c235f1031be46bd7c65e6b5c5154",
|
||||||
"packages/ui/components/Footer.wrn": "a5dfaa5f9548b7fdd4b556c63428e0ea8d22e5e776a5714d76619999438cf618",
|
"packages/ui/components/Footer.wrn": "a5dfaa5f9548b7fdd4b556c63428e0ea8d22e5e776a5714d76619999438cf618",
|
||||||
"packages/ui/components/Grid.wrn": "b5546b899c509c1f3e8f5edad801bc59acf9fe3008a627f0e4476cd61b5b672e",
|
"packages/ui/components/Grid.wrn": "b5546b899c509c1f3e8f5edad801bc59acf9fe3008a627f0e4476cd61b5b672e",
|
||||||
"packages/ui/components/Hero.wrn": "c7608d8a81df53ffcde5d857abc06c2eff813d4a10223a47a70602a68e7b025c",
|
"packages/ui/components/Hero.wrn": "e4e39dcd59c273192c7e4136f6583279e20b0956bcba4cfbc3f6e01a1fade152",
|
||||||
"packages/ui/components/HeroActions.wrn": "8f5d2d86c928c7499610f58a8b5c078e368d67f8924311472357a70c5d1dae1a",
|
"packages/ui/components/HeroActions.wrn": "c5720efe2ce7f6c114eb2ea4ccc3e8102a1099078ba23fd13770eddeb6aec640",
|
||||||
"packages/ui/components/Image.wrn": "4294358e2eddf06ae4384217dbff7693b609c16bf6549f54d6ad27df74d7ae27",
|
"packages/ui/components/Image.wrn": "4294358e2eddf06ae4384217dbff7693b609c16bf6549f54d6ad27df74d7ae27",
|
||||||
"packages/ui/components/Input.wrn": "9ad394eed60f1b05bbd2ccd50dc2de7b78245c52c87afd46055d772c38b7b0c9",
|
"packages/ui/components/Input.wrn": "9ad394eed60f1b05bbd2ccd50dc2de7b78245c52c87afd46055d772c38b7b0c9",
|
||||||
"packages/ui/components/InputGroup.wrn": "0e23ea541a60e893c9d9e6f95113911ee459468d8d6105bf03aeb55395359123",
|
"packages/ui/components/InputGroup.wrn": "0e23ea541a60e893c9d9e6f95113911ee459468d8d6105bf03aeb55395359123",
|
||||||
@@ -59,15 +59,15 @@
|
|||||||
"packages/ui/components/List.wrn": "02f960a8e82fa049aac477bac9c1d9371394c86d9c47513138f61bffc79dc7d8",
|
"packages/ui/components/List.wrn": "02f960a8e82fa049aac477bac9c1d9371394c86d9c47513138f61bffc79dc7d8",
|
||||||
"packages/ui/components/ListGroup.wrn": "37e2ec61ceab20ed020a834da03208c493702b41abcf586816e29f2e50d8ee11",
|
"packages/ui/components/ListGroup.wrn": "37e2ec61ceab20ed020a834da03208c493702b41abcf586816e29f2e50d8ee11",
|
||||||
"packages/ui/components/Map.wrn": "7b8a0d5412a464fd7cab8977d816542c506d8c5dab8230a3c984c2caee407c91",
|
"packages/ui/components/Map.wrn": "7b8a0d5412a464fd7cab8977d816542c506d8c5dab8230a3c984c2caee407c91",
|
||||||
"packages/ui/components/MarketingSectionHeader.wrn": "5147f3d86f18404c66111a52cccb44be56ca4cbfd7a7f4e885e41f82c1212c1e",
|
"packages/ui/components/MarketingSectionHeader.wrn": "7d6ffcc01a9331474475ea57ca58598be757abd9428b66bdfaf6f3603c5b145b",
|
||||||
"packages/ui/components/Marquee.wrn": "b2b35eedf3297ba12ab3776385a9f3eab001027648d87a2a1968f576eee6c025",
|
"packages/ui/components/Marquee.wrn": "b2b35eedf3297ba12ab3776385a9f3eab001027648d87a2a1968f576eee6c025",
|
||||||
"packages/ui/components/MegaMenu.wrn": "4a084eaf6aae77bb9023d2f3589bc6b80119b9be63982a90a80f9d280cc9c0a5",
|
"packages/ui/components/MegaMenu.wrn": "4a084eaf6aae77bb9023d2f3589bc6b80119b9be63982a90a80f9d280cc9c0a5",
|
||||||
"packages/ui/components/MetricCard.wrn": "6451182739298691908f68258c0250cce2a78b0dc27c97115579ece30d7d9f92",
|
"packages/ui/components/MetricCard.wrn": "6451182739298691908f68258c0250cce2a78b0dc27c97115579ece30d7d9f92",
|
||||||
"packages/ui/components/MetricGrid.wrn": "49742c27d988755316be54eae3722bb88600dd1add8c851fc95d4b4498db80a8",
|
"packages/ui/components/MetricGrid.wrn": "6018a98c10628ed240ed236ed916c0ff60994d192c3aadafd10bc876be0f9364",
|
||||||
"packages/ui/components/Modal.wrn": "1821428492e510403dd029c4766e71272899299f65141ae4fdaf09c4a26719e5",
|
"packages/ui/components/Modal.wrn": "1821428492e510403dd029c4766e71272899299f65141ae4fdaf09c4a26719e5",
|
||||||
"packages/ui/components/Nav.wrn": "78f215c94caf68e0968449a23e23bd3409a689e0c52a6c1770aa8373c767d080",
|
"packages/ui/components/Nav.wrn": "78f215c94caf68e0968449a23e23bd3409a689e0c52a6c1770aa8373c767d080",
|
||||||
"packages/ui/components/Navbar.wrn": "e68f9d3643e500e43124e9c7a6d4c7f6722657377ea7313f3e3cf5093a81b360",
|
"packages/ui/components/Navbar.wrn": "e68f9d3643e500e43124e9c7a6d4c7f6722657377ea7313f3e3cf5093a81b360",
|
||||||
"packages/ui/components/PageHeader.wrn": "e9f6c4c2b8723f3f035aa840ba8232f393a4038fae20c55dcd2a78276ac6738c",
|
"packages/ui/components/PageHeader.wrn": "adb3bed81ce040044405e35a52d0304f49d5a0d162f923fbd54a4242a25c7362",
|
||||||
"packages/ui/components/Pagination.wrn": "9169e724f89992dacd10e9492a95438c8016affb39c0fd17a4f6fe26bd21d8b4",
|
"packages/ui/components/Pagination.wrn": "9169e724f89992dacd10e9492a95438c8016affb39c0fd17a4f6fe26bd21d8b4",
|
||||||
"packages/ui/components/PinInput.wrn": "5196f584de8d548a5dfa03688c3c95da3c948cead2662b05e927386ccea74299",
|
"packages/ui/components/PinInput.wrn": "5196f584de8d548a5dfa03688c3c95da3c948cead2662b05e927386ccea74299",
|
||||||
"packages/ui/components/Popover.wrn": "fd9982f60e37e500586f788470f41a8510d6b96d3a85c80d2859fa01cac6731e",
|
"packages/ui/components/Popover.wrn": "fd9982f60e37e500586f788470f41a8510d6b96d3a85c80d2859fa01cac6731e",
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
"packages/ui/components/Rating.wrn": "a2d74dc748fc7d98892684c760518b87fa65411b1102e3611252b87245b3ffcb",
|
"packages/ui/components/Rating.wrn": "a2d74dc748fc7d98892684c760518b87fa65411b1102e3611252b87245b3ffcb",
|
||||||
"packages/ui/components/Scrollspy.wrn": "bbe0788f63c2bc8850649c1dee78391d485a1c93876015bc536d838273d5fae4",
|
"packages/ui/components/Scrollspy.wrn": "bbe0788f63c2bc8850649c1dee78391d485a1c93876015bc536d838273d5fae4",
|
||||||
"packages/ui/components/SearchBox.wrn": "315f54f1feaaa47a815d1e2d7a35b492f09fbfadfc6d37228e1009e19e0652ad",
|
"packages/ui/components/SearchBox.wrn": "315f54f1feaaa47a815d1e2d7a35b492f09fbfadfc6d37228e1009e19e0652ad",
|
||||||
"packages/ui/components/Section.wrn": "dc18952a6342f3af04fdab6a44a5755b6767e65bf37a11528fd9e67dde39802e",
|
"packages/ui/components/Section.wrn": "21485de07d8bb986837a65c61c5dd01ba6c2f3d4e37d58c154a584c7b58de0ed",
|
||||||
"packages/ui/components/SectionHeader.wrn": "512cb96636ee1f0540a0f4b4c75603fc0fe7536e7ed7ed7dc82173a16f48a4d3",
|
"packages/ui/components/SectionHeader.wrn": "512cb96636ee1f0540a0f4b4c75603fc0fe7536e7ed7ed7dc82173a16f48a4d3",
|
||||||
"packages/ui/components/Select.wrn": "7f046bb11b7c2470dae26d91a4b2261c66a40ae054d0e04c69b6748912d1e204",
|
"packages/ui/components/Select.wrn": "7f046bb11b7c2470dae26d91a4b2261c66a40ae054d0e04c69b6748912d1e204",
|
||||||
"packages/ui/components/Sidebar.wrn": "05f4bd216ae8bfcee2460fe638e431174d5f3c8e10003f346644e5a2939e7ce5",
|
"packages/ui/components/Sidebar.wrn": "05f4bd216ae8bfcee2460fe638e431174d5f3c8e10003f346644e5a2939e7ce5",
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.8.3
|
||||||
|
|
||||||
|
- Rebuilt the embedded WRN compiler with the 0.8.3 SSR, computed-value, Async-scope, and typed loop-prop fixes.
|
||||||
|
- Added compiler awareness for pruned and bundled browser hydration imports.
|
||||||
|
- Aligned the extension version with the WRNexusJS 0.8.3 framework release.
|
||||||
|
|
||||||
## 0.8.0
|
## 0.8.0
|
||||||
|
|
||||||
- Fixed excessive extension-host and language-server memory growth in large workspaces.
|
- Fixed excessive extension-host and language-server memory growth in large workspaces.
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "wrnexus",
|
"name": "wrnexus",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "wrnexus",
|
"name": "wrnexus",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vscode-languageclient": "^10.1.0"
|
"vscode-languageclient": "^10.1.0"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "wrnexus",
|
"name": "wrnexus",
|
||||||
"displayName": "WRNexus Language Support",
|
"displayName": "WRNexus Language Support",
|
||||||
"description": "Complete WRNexus v0.6 language support for typed imports, props, state, outputs, runtime functions, stores, diagnostics, formatting, navigation, and migration assistance.",
|
"description": "Complete WRNexus v0.8.3 language support for typed imports, props, state, outputs, runtime functions, stores, diagnostics, formatting, navigation, and migration assistance.",
|
||||||
"version": "0.8.0",
|
"version": "0.8.3",
|
||||||
"publisher": "wrnexus",
|
"publisher": "wrnexus",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
|
|||||||
+303
-61
@@ -1,5 +1,8 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
// Generated by scripts/build-editor-compiler.mjs. Do not edit directly.
|
// Generated by scripts/build-editor-compiler.mjs. Do not edit directly.
|
||||||
|
// WRN editor compiler source hash: b9e9a9ed0492c387198e368436696480ef2eca5a850ed94852c78c73c70d2959
|
||||||
|
// WRN editor compiler generator hash: c71e7fe4258c97b73b384ff14b321f0cf0b30cc2ed0322f5f84b04e757159b18
|
||||||
|
// Generated with TypeScript: 5.9.3
|
||||||
const __nodeRequire = require;
|
const __nodeRequire = require;
|
||||||
const __path = __nodeRequire("node:path");
|
const __path = __nodeRequire("node:path");
|
||||||
const __modules = {
|
const __modules = {
|
||||||
@@ -406,6 +409,7 @@ exports.DependencyGraph = DependencyGraph;
|
|||||||
"packages/compiler/src/client-codegen.ts": function (module, exports, require, __filename, __dirname) {
|
"packages/compiler/src/client-codegen.ts": function (module, exports, require, __filename, __dirname) {
|
||||||
"use strict";
|
"use strict";
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.browserModuleRequired = browserModuleRequired;
|
||||||
exports.generateBrowserModule = generateBrowserModule;
|
exports.generateBrowserModule = generateBrowserModule;
|
||||||
const syntax_1 = require("@wrnexus/syntax");
|
const syntax_1 = require("@wrnexus/syntax");
|
||||||
const RESERVED_BINDINGS = new Set([
|
const RESERVED_BINDINGS = new Set([
|
||||||
@@ -469,6 +473,91 @@ const RUNTIME_BINDINGS = new Set([
|
|||||||
function safeIdentifier(name) {
|
function safeIdentifier(name) {
|
||||||
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && !RESERVED_BINDINGS.has(name);
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && !RESERVED_BINDINGS.has(name);
|
||||||
}
|
}
|
||||||
|
function identifierReferenced(source, name) {
|
||||||
|
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
return new RegExp(`(?:^|[^A-Za-z0-9_$])${escaped}(?![A-Za-z0-9_$])`).test(source);
|
||||||
|
}
|
||||||
|
function viewReferenceSource(nodes) {
|
||||||
|
return nodes.flatMap((node) => {
|
||||||
|
if (node.type === "text")
|
||||||
|
return [node.value];
|
||||||
|
if (node.type === "each") {
|
||||||
|
return [
|
||||||
|
node.list,
|
||||||
|
node.key ?? "",
|
||||||
|
...viewReferenceSource(node.body),
|
||||||
|
...viewReferenceSource(node.empty),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (node.type === "if") {
|
||||||
|
return node.branches.flatMap((branch) => [
|
||||||
|
branch.cond ?? "",
|
||||||
|
...viewReferenceSource(branch.body),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return [...node.attrs.map((attr) => attr.value), ...viewReferenceSource(node.children)];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function browserReferenceSource(ast, functions) {
|
||||||
|
return [
|
||||||
|
...functions.flatMap((fn) => [fn.source, fn.body]),
|
||||||
|
...ast.computed.map((entry) => entry.expr),
|
||||||
|
...ast.effects,
|
||||||
|
ast.lifecycle.mount ?? "",
|
||||||
|
ast.lifecycle.update ?? "",
|
||||||
|
ast.lifecycle.unmount ?? "",
|
||||||
|
...viewReferenceSource(ast.view),
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
function renderSelectedImport(entry, source) {
|
||||||
|
if (entry.typeOnly)
|
||||||
|
return null;
|
||||||
|
const isStore = entry.source.endsWith(".wrn") && /(?:^|\/)stores?\//.test(entry.source);
|
||||||
|
if (entry.source.endsWith(".wrn") && !isStore)
|
||||||
|
return null;
|
||||||
|
const defaultImport = entry.defaultImport && (isStore || identifierReferenced(source, entry.defaultImport))
|
||||||
|
? entry.defaultImport
|
||||||
|
: undefined;
|
||||||
|
const namespaceImport = entry.namespaceImport && (isStore || identifierReferenced(source, entry.namespaceImport))
|
||||||
|
? entry.namespaceImport
|
||||||
|
: undefined;
|
||||||
|
const namedImports = entry.namedImports.filter((item) => !item.typeOnly && (isStore || identifierReferenced(source, item.local)));
|
||||||
|
const bindings = [
|
||||||
|
...(defaultImport ? [defaultImport] : []),
|
||||||
|
...(namespaceImport ? [namespaceImport] : []),
|
||||||
|
...namedImports.map((item) => item.local),
|
||||||
|
].filter(safeIdentifier);
|
||||||
|
const hasBindings = Boolean(defaultImport || namespaceImport || namedImports.length);
|
||||||
|
const sideEffectOnly = !entry.defaultImport && !entry.namespaceImport && entry.namedImports.length === 0;
|
||||||
|
if (!hasBindings && !sideEffectOnly)
|
||||||
|
return null;
|
||||||
|
if (sideEffectOnly)
|
||||||
|
return { code: entry.raw, bindings: [] };
|
||||||
|
const clauses = [];
|
||||||
|
if (defaultImport)
|
||||||
|
clauses.push(defaultImport);
|
||||||
|
if (namespaceImport)
|
||||||
|
clauses.push(`* as ${namespaceImport}`);
|
||||||
|
if (namedImports.length) {
|
||||||
|
clauses.push(`{ ${namedImports
|
||||||
|
.map((item) => item.imported === item.local ? item.imported : `${item.imported} as ${item.local}`)
|
||||||
|
.join(", ")} }`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
code: `import ${clauses.join(", ")} from ${JSON.stringify(entry.source)};`,
|
||||||
|
bindings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function selectedBrowserImports(ast, functions) {
|
||||||
|
const referenceSource = browserReferenceSource(ast, functions);
|
||||||
|
return ast.structuredImports
|
||||||
|
.map((entry) => renderSelectedImport(entry, referenceSource))
|
||||||
|
.filter((entry) => entry !== null);
|
||||||
|
}
|
||||||
|
function browserModuleRequired(ast) {
|
||||||
|
const functions = ast.runtimeFunctions.filter((fn) => ["legacy", "client", "shared"].includes(fn.runtime));
|
||||||
|
return functions.length > 0 || selectedBrowserImports(ast, functions).length > 0;
|
||||||
|
}
|
||||||
function functionEntry(ast, fn, availableFunctions) {
|
function functionEntry(ast, fn, availableFunctions) {
|
||||||
const parameterNames = new Set(fn.parameters.map((parameter) => parameter.name));
|
const parameterNames = new Set(fn.parameters.map((parameter) => parameter.name));
|
||||||
const stateNames = ast.states
|
const stateNames = ast.states
|
||||||
@@ -551,19 +640,9 @@ function generateBrowserModule(ast) {
|
|||||||
const functions = ast.runtimeFunctions.filter((fn) => ["legacy", "client", "shared"].includes(fn.runtime));
|
const functions = ast.runtimeFunctions.filter((fn) => ["legacy", "client", "shared"].includes(fn.runtime));
|
||||||
const functionNames = functions.map((fn) => fn.name);
|
const functionNames = functions.map((fn) => fn.name);
|
||||||
const state = ast.states.filter((entry) => entry.runtime !== "server").map((entry) => entry.name);
|
const state = ast.states.filter((entry) => entry.runtime !== "server").map((entry) => entry.name);
|
||||||
const storeImports = ast.structuredImports.filter((entry) => !entry.typeOnly && entry.source.endsWith(".wrn") && /(?:^|\/)stores?\//.test(entry.source));
|
const selectedImports = selectedBrowserImports(ast, functions);
|
||||||
const imports = ast.structuredImports
|
const imports = selectedImports.map((entry) => entry.code).join("\n");
|
||||||
.filter((entry) => !entry.typeOnly)
|
const importedBindings = [...new Set(selectedImports.flatMap((entry) => entry.bindings))];
|
||||||
.filter((entry) => !entry.source.endsWith(".wrn") || /(?:^|\/)stores?\//.test(entry.source))
|
|
||||||
.map((entry) => entry.raw)
|
|
||||||
.join("\n");
|
|
||||||
const importedBindings = storeImports
|
|
||||||
.flatMap((entry) => [
|
|
||||||
...(entry.defaultImport ? [entry.defaultImport] : []),
|
|
||||||
...(entry.namespaceImport ? [entry.namespaceImport] : []),
|
|
||||||
...entry.namedImports.map((item) => item.local),
|
|
||||||
])
|
|
||||||
.filter(safeIdentifier);
|
|
||||||
return `// generated WRNexusJS browser module for ${ast.name}
|
return `// generated WRNexusJS browser module for ${ast.name}
|
||||||
${imports}
|
${imports}
|
||||||
export const __wrnexusClientFunctions = {
|
export const __wrnexusClientFunctions = {
|
||||||
@@ -611,6 +690,7 @@ const types_ts_1 = require("./types.js");
|
|||||||
const syntax_1 = require("@wrnexus/syntax");
|
const syntax_1 = require("@wrnexus/syntax");
|
||||||
const store_codegen_ts_1 = require("./store-codegen.js");
|
const store_codegen_ts_1 = require("./store-codegen.js");
|
||||||
const analysis_ts_1 = require("./analysis.js");
|
const analysis_ts_1 = require("./analysis.js");
|
||||||
|
const client_codegen_ts_1 = require("./client-codegen.js");
|
||||||
function isComponentTag(tag) {
|
function isComponentTag(tag) {
|
||||||
return /^[A-Z][A-Za-z0-9_$]*$/.test(tag);
|
return /^[A-Z][A-Za-z0-9_$]*$/.test(tag);
|
||||||
}
|
}
|
||||||
@@ -802,7 +882,7 @@ function evalStateSeeds(states) {
|
|||||||
* runtime keeps it live. Non-state `{expr}` and un-evaluable expressions are
|
* runtime keeps it live. Non-state `{expr}` and un-evaluable expressions are
|
||||||
* left as literal client mustaches.
|
* left as literal client mustaches.
|
||||||
*/
|
*/
|
||||||
function substituteReactiveText(raw, reactive) {
|
function substituteReactiveText(raw, reactive, dynamicExpressions) {
|
||||||
const text = substituteTMarkers(raw);
|
const text = substituteTMarkers(raw);
|
||||||
if (!reactive || reactive.stateNames.size === 0)
|
if (!reactive || reactive.stateNames.size === 0)
|
||||||
return text;
|
return text;
|
||||||
@@ -810,6 +890,11 @@ function substituteReactiveText(raw, reactive) {
|
|||||||
const expr = inner.trim();
|
const expr = inner.trim();
|
||||||
if (expr.startsWith("t:") || !exprRefsState(expr, reactive.stateNames))
|
if (expr.startsWith("t:") || !exprRefsState(expr, reactive.stateNames))
|
||||||
return whole;
|
return whole;
|
||||||
|
if (exprRefsState(expr, reactive.runtimeStateNames) && dynamicExpressions) {
|
||||||
|
dynamicExpressions.push(`\${__wrnexusEscapeHtml(${expr})}`);
|
||||||
|
const sentinel = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
|
||||||
|
return `<span data-text="${attrEscape(expr)}">${sentinel}</span>`;
|
||||||
|
}
|
||||||
let value;
|
let value;
|
||||||
try {
|
try {
|
||||||
value = new Function("with(this){return (" + expr + ");}").call(reactive.scope);
|
value = new Function("with(this){return (" + expr + ");}").call(reactive.scope);
|
||||||
@@ -845,7 +930,10 @@ function bakeLoopText(raw) {
|
|||||||
return out + escLit(raw.slice(last));
|
return out + escLit(raw.slice(last));
|
||||||
}
|
}
|
||||||
/** Bake a loop-body attribute value (same rules as text; escapeHtml is attribute-safe). */
|
/** Bake a loop-body attribute value (same rules as text; escapeHtml is attribute-safe). */
|
||||||
function bakeLoopAttr(raw) {
|
function bakeLoopAttr(raw, typed = false) {
|
||||||
|
const wholeExpression = wholeAttributeExpression(raw);
|
||||||
|
if (typed && wholeExpression)
|
||||||
|
return "${__wrnexusPropAttr(" + wholeExpression + ")}";
|
||||||
if (!raw.includes("{"))
|
if (!raw.includes("{"))
|
||||||
return escLit(attrEscape(raw));
|
return escLit(attrEscape(raw));
|
||||||
let out = "";
|
let out = "";
|
||||||
@@ -878,7 +966,7 @@ function renderLoopBody(node) {
|
|||||||
if (attr.boolean) {
|
if (attr.boolean) {
|
||||||
return escLit(` ${name}`);
|
return escLit(` ${name}`);
|
||||||
}
|
}
|
||||||
return escLit(` ${name}="`) + bakeLoopAttr(attr.value) + escLit(`"`);
|
return escLit(` ${name}="`) + bakeLoopAttr(attr.value, componentTag) + escLit(`"`);
|
||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
const inner = node.children.map(renderLoopBody).join("");
|
const inner = node.children.map(renderLoopBody).join("");
|
||||||
@@ -998,7 +1086,7 @@ function collectControlExprs(nodes, out = []) {
|
|||||||
}
|
}
|
||||||
function renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive = null) {
|
function renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive = null) {
|
||||||
if (node.type === "text")
|
if (node.type === "text")
|
||||||
return substituteReactiveText(node.value, reactive); // {t:key} + state baking
|
return substituteReactiveText(node.value, reactive, loops); // {t:key} + state baking
|
||||||
// Server control block (loop / conditional) → a sentinel that survives
|
// Server control block (loop / conditional) → a sentinel that survives
|
||||||
// templateEscape, swapped for its real `${…}` code after escaping.
|
// templateEscape, swapped for its real `${…}` code after escaping.
|
||||||
if (node.type === "each" || node.type === "if") {
|
if (node.type === "each" || node.type === "if") {
|
||||||
@@ -1033,10 +1121,11 @@ function renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive
|
|||||||
if (node.tag === "Async") {
|
if (node.tag === "Async") {
|
||||||
const source = attrValue(node.attrs, "source") ?? "data";
|
const source = attrValue(node.attrs, "source") ?? "data";
|
||||||
const retries = attrValue(node.attrs, "retries") ?? "2";
|
const retries = attrValue(node.attrs, "retries") ?? "2";
|
||||||
|
const tags = attrValue(node.attrs, "tags") ?? source;
|
||||||
const serverResolved = attrValue(node.attrs, "data-wrn-async-server") === "true";
|
const serverResolved = attrValue(node.attrs, "data-wrn-async-server") === "true";
|
||||||
const asyncIndex = serverResolved ? loops.push("") - 1 : -1;
|
const branchElement = (name) => node.children.find((child) => child.type === "element" && child.tag === name);
|
||||||
const branch = (name) => {
|
const branch = (name) => {
|
||||||
const element = node.children.find((child) => child.type === "element" && child.tag === name);
|
const element = branchElement(name);
|
||||||
return (element?.children ?? [])
|
return (element?.children ?? [])
|
||||||
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
|
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
|
||||||
.join("");
|
.join("");
|
||||||
@@ -1044,16 +1133,26 @@ function renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive
|
|||||||
const loading = branch("Loading");
|
const loading = branch("Loading");
|
||||||
const success = branch("Success");
|
const success = branch("Success");
|
||||||
const error = branch("Error");
|
const error = branch("Error");
|
||||||
|
const successElement = branchElement("Success");
|
||||||
|
const errorElement = branchElement("Error");
|
||||||
|
const identifier = (value, fallback) => value && isSafeGeneratedIdentifier(value) ? value : fallback;
|
||||||
|
const successAlias = identifier(successElement ? attrValue(successElement.attrs, "data") : undefined, identifier(source, "data"));
|
||||||
|
const errorAlias = identifier(errorElement ? attrValue(errorElement.attrs, "error") : undefined, "error");
|
||||||
|
const nested = (value) => value.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
||||||
|
const scoped = (value, alias, expression) => {
|
||||||
|
const index = loops.push(`\${(() => { const ${alias} = ${expression}; return \`${nested(value)}\`; })()}`) - 1;
|
||||||
|
return `\x00WRNEACH${index}\x00`;
|
||||||
|
};
|
||||||
|
const successTemplate = scoped(success, successAlias, `(ctx[${JSON.stringify(source)}] ?? {})`);
|
||||||
|
const errorTemplate = scoped(error, errorAlias, `{ message: "" }`);
|
||||||
let initial = loading;
|
let initial = loading;
|
||||||
if (serverResolved) {
|
if (serverResolved) {
|
||||||
const nested = (value) => value.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
const sourcePattern = successAlias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
const sourcePattern = source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
||||||
const serverSuccess = success.replace(new RegExp(`\\{\\s*(${sourcePattern}(?:\\.[A-Za-z_$][\\w$]*)*)\\s*\\}`, "g"), (_whole, expression) => `\${__wrnexusEscapeHtml(${expression})}`);
|
const serverSuccess = success.replace(new RegExp(`\\{\\s*(${sourcePattern}(?:\\.[A-Za-z_$][\\w$]*)*)\\s*\\}`, "g"), (_whole, expression) => `\${__wrnexusEscapeHtml(${expression})}`);
|
||||||
loops[asyncIndex] =
|
const index = loops.push(`\${ctx[${JSON.stringify(source)}] !== undefined ? (() => { const ${successAlias} = ctx[${JSON.stringify(source)}]; return \`${nested(serverSuccess)}\`; })() : \`${nested(loading)}\`}`) - 1;
|
||||||
`\${ctx[${JSON.stringify(source)}] !== undefined ? \`${nested(serverSuccess)}\` : \`${nested(loading)}\`}`;
|
initial = `\x00WRNEACH${index}\x00`;
|
||||||
initial = `\x00WRNEACH${asyncIndex}\x00`;
|
|
||||||
}
|
}
|
||||||
return `<section data-wrn-async="${attrEscape(source)}" data-wrn-async-retries="${attrEscape(retries)}"${serverResolved ? ' data-wrn-async-resolved="true"' : ""} aria-busy="${serverResolved ? "false" : "true"}"><div data-wrn-async-content>${initial}</div><template data-wrn-async-loading>${loading}</template><template data-wrn-async-success>${success}</template><template data-wrn-async-error>${error}</template></section>`;
|
return `<section data-wrn-async="${attrEscape(source)}" data-wrn-async-tags="${attrEscape(tags)}" data-wrn-async-retries="${attrEscape(retries)}"${serverResolved ? ' data-wrn-async-resolved="true"' : ""} aria-busy="${serverResolved ? "false" : "true"}"><div data-wrn-async-content>${initial}</div><template data-wrn-async-loading>${loading}</template><template data-wrn-async-success data-wrn-async-alias="${attrEscape(successAlias)}">${successTemplate}</template><template data-wrn-async-error data-wrn-async-alias="${attrEscape(errorAlias)}">${errorTemplate}</template></section>`;
|
||||||
}
|
}
|
||||||
if (node.tag === "KeepAlive") {
|
if (node.tag === "KeepAlive") {
|
||||||
const key = attrValue(node.attrs, "key") ?? "default";
|
const key = attrValue(node.attrs, "key") ?? "default";
|
||||||
@@ -1371,11 +1470,115 @@ function generateSsrStateAliases(stateNames) {
|
|||||||
}
|
}
|
||||||
return `const { ${names.join(", ")} } = __state;\n`;
|
return `const { ${names.join(", ")} } = __state;\n`;
|
||||||
}
|
}
|
||||||
|
function orderPageStates(entries) {
|
||||||
|
if (entries.length < 2)
|
||||||
|
return entries;
|
||||||
|
const byName = new Map(entries.map((entry) => [entry.name, entry]));
|
||||||
|
const visiting = new Set();
|
||||||
|
const visited = new Set();
|
||||||
|
const ordered = [];
|
||||||
|
const visit = (name) => {
|
||||||
|
if (visited.has(name))
|
||||||
|
return;
|
||||||
|
if (visiting.has(name)) {
|
||||||
|
throw new Error(`WRN-STATE-CYCLE: state value '${name}' has a dependency cycle.`);
|
||||||
|
}
|
||||||
|
const entry = byName.get(name);
|
||||||
|
if (!entry)
|
||||||
|
return;
|
||||||
|
visiting.add(name);
|
||||||
|
for (const dependency of byName.keys()) {
|
||||||
|
if (dependency !== name &&
|
||||||
|
new RegExp(`\\b${dependency.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(entry.expr)) {
|
||||||
|
visit(dependency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visiting.delete(name);
|
||||||
|
visited.add(name);
|
||||||
|
ordered.push(entry);
|
||||||
|
};
|
||||||
|
for (const entry of entries)
|
||||||
|
visit(entry.name);
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
function generateSsrStateInitializer(entries) {
|
||||||
|
const ordered = orderPageStates(entries);
|
||||||
|
const declarations = ordered
|
||||||
|
.filter((entry) => isSafeGeneratedIdentifier(entry.name))
|
||||||
|
.map((entry) => `const ${entry.name} = (() => { try { return (${entry.expr}); } catch { return undefined; } })();`)
|
||||||
|
.join(" ");
|
||||||
|
const values = entries
|
||||||
|
.map((entry) => {
|
||||||
|
if (isSafeGeneratedIdentifier(entry.name)) {
|
||||||
|
return `${JSON.stringify(entry.name)}: ${entry.name}`;
|
||||||
|
}
|
||||||
|
return `${JSON.stringify(entry.name)}: (() => { try { return (${entry.expr}); } catch { return undefined; } })()`;
|
||||||
|
})
|
||||||
|
.join(", ");
|
||||||
|
return `(() => { ${declarations} return { ${values} }; })()`;
|
||||||
|
}
|
||||||
|
function orderPageComputed(entries) {
|
||||||
|
if (entries.length < 2)
|
||||||
|
return entries;
|
||||||
|
const byName = new Map(entries.map((entry) => [entry.name, entry]));
|
||||||
|
const visiting = new Set();
|
||||||
|
const visited = new Set();
|
||||||
|
const ordered = [];
|
||||||
|
const visit = (name) => {
|
||||||
|
if (visited.has(name))
|
||||||
|
return;
|
||||||
|
if (visiting.has(name)) {
|
||||||
|
throw new Error(`WRN-COMPUTED-CYCLE: computed value '${name}' has a dependency cycle.`);
|
||||||
|
}
|
||||||
|
const entry = byName.get(name);
|
||||||
|
if (!entry)
|
||||||
|
return;
|
||||||
|
visiting.add(name);
|
||||||
|
for (const dependency of byName.keys()) {
|
||||||
|
if (dependency !== name &&
|
||||||
|
new RegExp(`\\b${dependency.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(entry.expr)) {
|
||||||
|
visit(dependency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visiting.delete(name);
|
||||||
|
visited.add(name);
|
||||||
|
ordered.push(entry);
|
||||||
|
};
|
||||||
|
for (const entry of entries)
|
||||||
|
visit(entry.name);
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
function generateSsrComputedAliases(entries) {
|
||||||
|
return orderPageComputed(entries)
|
||||||
|
.filter((entry) => isSafeGeneratedIdentifier(entry.name))
|
||||||
|
.map((entry) => `const ${entry.name} = (() => { try { return (${entry.expr}); } catch { return undefined; } })();`)
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
function runtimeComputedNames(states, computed, runtimeRoots = []) {
|
||||||
|
const entries = [...states, ...computed];
|
||||||
|
const runtime = new Set([
|
||||||
|
...runtimeRoots,
|
||||||
|
...entries.filter((entry) => /\bctx\b/.test(entry.expr)).map((entry) => entry.name),
|
||||||
|
]);
|
||||||
|
let changed = true;
|
||||||
|
while (changed) {
|
||||||
|
changed = false;
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (runtime.has(entry.name))
|
||||||
|
continue;
|
||||||
|
if ([...runtime].some((name) => new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(entry.expr))) {
|
||||||
|
runtime.add(entry.name);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return runtime;
|
||||||
|
}
|
||||||
function hydrationAttribute(ast) {
|
function hydrationAttribute(ast) {
|
||||||
const strategy = ["static", "server"].includes(ast.renderMode ?? "")
|
const strategy = ["static", "server"].includes(ast.renderMode ?? "")
|
||||||
? "none"
|
? "none"
|
||||||
: (ast.hydrate ?? "load");
|
: (ast.hydrate ?? "load");
|
||||||
const hasBrowserModule = ast.runtimeFunctions.some((fn) => ["legacy", "client", "shared"].includes(fn.runtime));
|
const hasBrowserModule = (0, client_codegen_ts_1.browserModuleRequired)(ast);
|
||||||
const moduleAttribute = hasBrowserModule
|
const moduleAttribute = hasBrowserModule
|
||||||
? ' data-wrn-client-module="__WRNEXUS_CLIENT_MODULE__"'
|
? ' data-wrn-client-module="__WRNEXUS_CLIENT_MODULE__"'
|
||||||
: "";
|
: "";
|
||||||
@@ -1506,9 +1709,11 @@ function generate(ast) {
|
|||||||
out.push(`export const __wrnexusNavigation = ${JSON.stringify(ast.navigation, null, 2)};`);
|
out.push(`export const __wrnexusNavigation = ${JSON.stringify(ast.navigation, null, 2)};`);
|
||||||
}
|
}
|
||||||
// --- View -> default page component ---
|
// --- View -> default page component ---
|
||||||
|
const orderedStates = orderPageStates(ast.states);
|
||||||
const browserStates = ast.states.filter((state) => state.runtime !== "server");
|
const browserStates = ast.states.filter((state) => state.runtime !== "server");
|
||||||
const seedScope = evalStateSeeds(ast.states);
|
const seedScope = evalStateSeeds(orderedStates);
|
||||||
for (const entry of ast.computed) {
|
const orderedComputed = orderPageComputed(ast.computed);
|
||||||
|
for (const entry of orderedComputed) {
|
||||||
try {
|
try {
|
||||||
seedScope[entry.name] = new Function("with(this){return (" + entry.expr + ");}").call(seedScope);
|
seedScope[entry.name] = new Function("with(this){return (" + entry.expr + ");}").call(seedScope);
|
||||||
}
|
}
|
||||||
@@ -1520,7 +1725,14 @@ function generate(ast) {
|
|||||||
...browserStates.map((entry) => entry.name),
|
...browserStates.map((entry) => entry.name),
|
||||||
...ast.computed.map((entry) => entry.name),
|
...ast.computed.map((entry) => entry.name),
|
||||||
];
|
];
|
||||||
const runtimeStateNames = new Set(ast.states.filter((entry) => /\bctx\b/.test(entry.expr)).map((entry) => entry.name));
|
const storeBindings = importedStoreBindings(ast);
|
||||||
|
const runtimeRoots = [
|
||||||
|
...storeBindings.map((entry) => entry.local),
|
||||||
|
...ast.loads
|
||||||
|
.filter((load) => load.mode === "server" && !load.deferred && load.name)
|
||||||
|
.map((load) => load.name),
|
||||||
|
];
|
||||||
|
const runtimeStateNames = runtimeComputedNames(orderedStates, orderedComputed, runtimeRoots);
|
||||||
const reactive = reactiveNames.length > 0
|
const reactive = reactiveNames.length > 0
|
||||||
? { stateNames: new Set(reactiveNames), runtimeStateNames, scope: seedScope }
|
? { stateNames: new Set(reactiveNames), runtimeStateNames, scope: seedScope }
|
||||||
: null;
|
: null;
|
||||||
@@ -1564,9 +1776,7 @@ function generate(ast) {
|
|||||||
const shellHtml = html.replace(/<wrn-dynamic-region\b[^>]*>[\s\S]*?<\/wrn-dynamic-region>/gi, '<wrn-dynamic-region data-wrn-dynamic="true"></wrn-dynamic-region>');
|
const shellHtml = html.replace(/<wrn-dynamic-region\b[^>]*>[\s\S]*?<\/wrn-dynamic-region>/gi, '<wrn-dynamic-region data-wrn-dynamic="true"></wrn-dynamic-region>');
|
||||||
staticShellBody = templateEscape(shellHtml);
|
staticShellBody = templateEscape(shellHtml);
|
||||||
}
|
}
|
||||||
const dynamicStateScope = ast.states
|
const dynamicStateInitializer = generateSsrStateInitializer(orderedStates);
|
||||||
.map((state) => `${JSON.stringify(state.name)}: (() => { try { return (${state.expr}); } catch { return undefined; } })()`)
|
|
||||||
.join(", ");
|
|
||||||
const stateType = ast.states.length > 0
|
const stateType = ast.states.length > 0
|
||||||
? `{ ${ast.states
|
? `{ ${ast.states
|
||||||
.map((state) => `${JSON.stringify(state.name)}: ${state.valueType ?? "unknown"}`)
|
.map((state) => `${JSON.stringify(state.name)}: ${state.valueType ?? "unknown"}`)
|
||||||
@@ -1574,7 +1784,7 @@ function generate(ast) {
|
|||||||
: "Record<string, never>";
|
: "Record<string, never>";
|
||||||
const hydrationStateNames = JSON.stringify(browserStates.map((state) => state.name));
|
const hydrationStateNames = JSON.stringify(browserStates.map((state) => state.name));
|
||||||
const ssrStateAliases = generateSsrStateAliases(ast.states.map((state) => state.name));
|
const ssrStateAliases = generateSsrStateAliases(ast.states.map((state) => state.name));
|
||||||
const storeBindings = importedStoreBindings(ast);
|
const ssrComputedAliases = generateSsrComputedAliases(orderedComputed);
|
||||||
const storeDeclarations = storeBindings
|
const storeDeclarations = storeBindings
|
||||||
.map((entry) => ` const ${entry.local} = await ctx.__wrnexusUseStore(${entry.internal});`)
|
.map((entry) => ` const ${entry.local} = await ctx.__wrnexusUseStore(${entry.internal});`)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
@@ -1582,12 +1792,16 @@ function generate(ast) {
|
|||||||
.filter((load) => load.mode === "server" && !load.deferred && load.name)
|
.filter((load) => load.mode === "server" && !load.deferred && load.name)
|
||||||
.map((load) => ` const ${load.name} = ctx[${JSON.stringify(load.name)}];`)
|
.map((load) => ` const ${load.name} = ctx[${JSON.stringify(load.name)}];`)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
loops.forEach((code, idx) => {
|
// Inner dynamic expressions are registered before the wrapper that scopes
|
||||||
body = body.replace(`\x00WRNEACH${idx}\x00`, () => code);
|
// them (for example an Async branch alias). Resolve from the outside in so a
|
||||||
|
// wrapper sentinel is expanded before its nested sentinels are visited.
|
||||||
|
for (let idx = loops.length - 1; idx >= 0; idx--) {
|
||||||
|
const code = loops[idx];
|
||||||
|
body = body.replaceAll(`\x00WRNEACH${idx}\x00`, code);
|
||||||
if (staticShellBody?.includes(`\x00WRNEACH${idx}\x00`)) {
|
if (staticShellBody?.includes(`\x00WRNEACH${idx}\x00`)) {
|
||||||
staticShellBody = staticShellBody.replace(`\x00WRNEACH${idx}\x00`, () => code);
|
staticShellBody = staticShellBody.replaceAll(`\x00WRNEACH${idx}\x00`, code);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
// Server loops iterate raw SSR data. Declare a named const for every `ssr` data
|
// Server loops iterate raw SSR data. Declare a named const for every `ssr` data
|
||||||
// binding a loop references, so `{#each <name> as …}` can iterate the real value.
|
// binding a loop references, so `{#each <name> as …}` can iterate the real value.
|
||||||
const loopConsts = [];
|
const loopConsts = [];
|
||||||
@@ -1601,9 +1815,7 @@ function generate(ast) {
|
|||||||
loopConsts.push(` const ${name} = __wrnexusEvalData(await __wrnexusCallApi(${JSON.stringify(binding.path)}, ${JSON.stringify(binding.method)}, ctx), ${JSON.stringify(binding.body)}, ${JSON.stringify(binding.helpers)}, ctx);`);
|
loopConsts.push(` const ${name} = __wrnexusEvalData(await __wrnexusCallApi(${JSON.stringify(binding.path)}, ${JSON.stringify(binding.method)}, ctx), ${JSON.stringify(binding.body)}, ${JSON.stringify(binding.helpers)}, ctx);`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const needsSsrRuntime = ssrBindings.length > 0 ||
|
const needsSsrRuntime = ssrBindings.length > 0 || loops.length > 0 || runtimeStateNames.size > 0;
|
||||||
loops.length > 0 ||
|
|
||||||
ast.states.some((state) => /\bctx\b/.test(state.expr));
|
|
||||||
if (needsSsrRuntime) {
|
if (needsSsrRuntime) {
|
||||||
out.push(ssrRuntimeSource());
|
out.push(ssrRuntimeSource());
|
||||||
out.push(`const __wrnexusSsrBindings = ${JSON.stringify(ssrBindings, null, 2)};`);
|
out.push(`const __wrnexusSsrBindings = ${JSON.stringify(ssrBindings, null, 2)};`);
|
||||||
@@ -1612,16 +1824,18 @@ function generate(ast) {
|
|||||||
${storeDeclarations}
|
${storeDeclarations}
|
||||||
${serverLoadAliases}
|
${serverLoadAliases}
|
||||||
${decls}
|
${decls}
|
||||||
const __state: ${stateType} = { ${dynamicStateScope} };
|
const __state: ${stateType} = ${dynamicStateInitializer};
|
||||||
${ssrStateAliases}
|
${ssrStateAliases}
|
||||||
|
${ssrComputedAliases}
|
||||||
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
|
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
|
||||||
const __scopeValue = Object.entries(__hydrationState)
|
const __scopeValue = Object.entries(__hydrationState)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
const encoded =
|
let encoded: string;
|
||||||
typeof value === "number" || typeof value === "boolean"
|
try {
|
||||||
? String(value)
|
encoded = value === undefined ? "undefined" : JSON.stringify(value);
|
||||||
: JSON.stringify(value == null ? "" : String(value));
|
} catch {
|
||||||
|
encoded = JSON.stringify(String(value));
|
||||||
|
}
|
||||||
return key + ": " + encoded;
|
return key + ": " + encoded;
|
||||||
})
|
})
|
||||||
.join(", ")
|
.join(", ")
|
||||||
@@ -1642,16 +1856,18 @@ function generate(ast) {
|
|||||||
out.push(`export default ${storeBindings.length > 0 ? "async " : ""}function ${ast.name}(ctx: any) {
|
out.push(`export default ${storeBindings.length > 0 ? "async " : ""}function ${ast.name}(ctx: any) {
|
||||||
${storeDeclarations}
|
${storeDeclarations}
|
||||||
${serverLoadAliases}
|
${serverLoadAliases}
|
||||||
const __state: ${stateType} = { ${dynamicStateScope} };
|
const __state: ${stateType} = ${dynamicStateInitializer};
|
||||||
${ssrStateAliases}
|
${ssrStateAliases}
|
||||||
|
${ssrComputedAliases}
|
||||||
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
|
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
|
||||||
const __scopeValue = Object.entries(__hydrationState)
|
const __scopeValue = Object.entries(__hydrationState)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
const encoded =
|
let encoded: string;
|
||||||
typeof value === "number" || typeof value === "boolean"
|
try {
|
||||||
? String(value)
|
encoded = value === undefined ? "undefined" : JSON.stringify(value);
|
||||||
: JSON.stringify(value == null ? "" : String(value));
|
} catch {
|
||||||
|
encoded = JSON.stringify(String(value));
|
||||||
|
}
|
||||||
return key + ": " + encoded;
|
return key + ": " + encoded;
|
||||||
})
|
})
|
||||||
.join(", ")
|
.join(", ")
|
||||||
@@ -1671,14 +1887,18 @@ function generate(ast) {
|
|||||||
${storeDeclarations}
|
${storeDeclarations}
|
||||||
${serverLoadAliases}
|
${serverLoadAliases}
|
||||||
${loopConsts.length > 0 ? loopConsts.join("\n") : ""}
|
${loopConsts.length > 0 ? loopConsts.join("\n") : ""}
|
||||||
const __state: ${stateType} = { ${dynamicStateScope} };
|
const __state: ${stateType} = ${dynamicStateInitializer};
|
||||||
${ssrStateAliases}
|
${ssrStateAliases}
|
||||||
|
${ssrComputedAliases}
|
||||||
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
|
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
|
||||||
const __scopeValue = Object.entries(__hydrationState)
|
const __scopeValue = Object.entries(__hydrationState)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
const encoded = typeof value === "number" || typeof value === "boolean"
|
let encoded: string;
|
||||||
? String(value)
|
try {
|
||||||
: JSON.stringify(value == null ? "" : String(value));
|
encoded = value === undefined ? "undefined" : JSON.stringify(value);
|
||||||
|
} catch {
|
||||||
|
encoded = JSON.stringify(String(value));
|
||||||
|
}
|
||||||
return key + ": " + encoded;
|
return key + ": " + encoded;
|
||||||
})
|
})
|
||||||
.join(", ")
|
.join(", ")
|
||||||
@@ -1845,6 +2065,13 @@ const JS_RESERVED = new Set([
|
|||||||
"enum",
|
"enum",
|
||||||
"with",
|
"with",
|
||||||
"debugger",
|
"debugger",
|
||||||
|
"implements",
|
||||||
|
"interface",
|
||||||
|
"package",
|
||||||
|
"private",
|
||||||
|
"protected",
|
||||||
|
"public",
|
||||||
|
"static",
|
||||||
]);
|
]);
|
||||||
/** A JS reference for a prop/state name (reserved words get a `__p_` prefix). */
|
/** A JS reference for a prop/state name (reserved words get a `__p_` prefix). */
|
||||||
function safeRef(name) {
|
function safeRef(name) {
|
||||||
@@ -2739,13 +2966,28 @@ function candidates(path) {
|
|||||||
}
|
}
|
||||||
function resolveWrnImport(declaration, importer, options) {
|
function resolveWrnImport(declaration, importer, options) {
|
||||||
const source = declaration.source;
|
const source = declaration.source;
|
||||||
if (!source.startsWith(".") && !source.startsWith("@/"))
|
const aliases = {
|
||||||
|
"@": "./app",
|
||||||
|
...(options.aliases ?? {}),
|
||||||
|
};
|
||||||
|
const alias = Object.keys(aliases)
|
||||||
|
.filter((key) => key.length > 0 && (source === key || source.startsWith(`${key}/`)))
|
||||||
|
.sort((left, right) => right.length - left.length)[0];
|
||||||
|
if (!source.startsWith(".") && !alias)
|
||||||
return { declaration, resolved: source };
|
return { declaration, resolved: source };
|
||||||
const aliasRoot = options.aliases?.["@"] ?? "./app";
|
const base = alias
|
||||||
const base = source.startsWith("@/")
|
? (0, node_path_1.resolve)(options.appRoot, aliases[alias], source === alias ? "" : source.slice(alias.length + 1))
|
||||||
? (0, node_path_1.resolve)(options.appRoot, aliasRoot, source.slice(2))
|
|
||||||
: (0, node_path_1.resolve)((0, node_path_1.dirname)(importer), source);
|
: (0, node_path_1.resolve)((0, node_path_1.dirname)(importer), source);
|
||||||
const found = candidates(base).find(node_fs_1.existsSync);
|
const found = candidates(base).find((candidate) => {
|
||||||
|
if (!(0, node_fs_1.existsSync)(candidate))
|
||||||
|
return false;
|
||||||
|
try {
|
||||||
|
return (0, node_fs_1.statSync)(candidate).isFile();
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
if (found)
|
if (found)
|
||||||
return { declaration, resolved: (0, node_fs_1.realpathSync)(found) };
|
return { declaration, resolved: (0, node_fs_1.realpathSync)(found) };
|
||||||
const severity = (options.mode ?? "compatible") === "explicit" ? "error" : "warning";
|
const severity = (options.mode ?? "compatible") === "explicit" ? "error" : "warning";
|
||||||
|
|||||||
@@ -23058,6 +23058,7 @@ ${(0, codegen_ts_1.generate)(ast)}`,
|
|||||||
},
|
},
|
||||||
"packages/compiler/src/client-codegen.ts": function(module3, exports3, require2, __filename2, __dirname2) {
|
"packages/compiler/src/client-codegen.ts": function(module3, exports3, require2, __filename2, __dirname2) {
|
||||||
Object.defineProperty(exports3, "__esModule", { value: true });
|
Object.defineProperty(exports3, "__esModule", { value: true });
|
||||||
|
exports3.browserModuleRequired = browserModuleRequired;
|
||||||
exports3.generateBrowserModule = generateBrowserModule;
|
exports3.generateBrowserModule = generateBrowserModule;
|
||||||
const syntax_1 = require2("@wrnexus/syntax");
|
const syntax_1 = require2("@wrnexus/syntax");
|
||||||
const RESERVED_BINDINGS = new Set([
|
const RESERVED_BINDINGS = new Set([
|
||||||
@@ -23121,6 +23122,84 @@ ${(0, codegen_ts_1.generate)(ast)}`,
|
|||||||
function safeIdentifier(name) {
|
function safeIdentifier(name) {
|
||||||
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && !RESERVED_BINDINGS.has(name);
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && !RESERVED_BINDINGS.has(name);
|
||||||
}
|
}
|
||||||
|
function identifierReferenced(source, name) {
|
||||||
|
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
return new RegExp(`(?:^|[^A-Za-z0-9_$])${escaped}(?![A-Za-z0-9_$])`).test(source);
|
||||||
|
}
|
||||||
|
function viewReferenceSource(nodes) {
|
||||||
|
return nodes.flatMap((node) => {
|
||||||
|
if (node.type === "text")
|
||||||
|
return [node.value];
|
||||||
|
if (node.type === "each") {
|
||||||
|
return [
|
||||||
|
node.list,
|
||||||
|
node.key ?? "",
|
||||||
|
...viewReferenceSource(node.body),
|
||||||
|
...viewReferenceSource(node.empty)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (node.type === "if") {
|
||||||
|
return node.branches.flatMap((branch) => [
|
||||||
|
branch.cond ?? "",
|
||||||
|
...viewReferenceSource(branch.body)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return [...node.attrs.map((attr) => attr.value), ...viewReferenceSource(node.children)];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function browserReferenceSource(ast, functions) {
|
||||||
|
return [
|
||||||
|
...functions.flatMap((fn) => [fn.source, fn.body]),
|
||||||
|
...ast.computed.map((entry) => entry.expr),
|
||||||
|
...ast.effects,
|
||||||
|
ast.lifecycle.mount ?? "",
|
||||||
|
ast.lifecycle.update ?? "",
|
||||||
|
ast.lifecycle.unmount ?? "",
|
||||||
|
...viewReferenceSource(ast.view)
|
||||||
|
].join(`
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
function renderSelectedImport(entry, source) {
|
||||||
|
if (entry.typeOnly)
|
||||||
|
return null;
|
||||||
|
const isStore = entry.source.endsWith(".wrn") && /(?:^|\/)stores?\//.test(entry.source);
|
||||||
|
if (entry.source.endsWith(".wrn") && !isStore)
|
||||||
|
return null;
|
||||||
|
const defaultImport = entry.defaultImport && (isStore || identifierReferenced(source, entry.defaultImport)) ? entry.defaultImport : undefined;
|
||||||
|
const namespaceImport = entry.namespaceImport && (isStore || identifierReferenced(source, entry.namespaceImport)) ? entry.namespaceImport : undefined;
|
||||||
|
const namedImports = entry.namedImports.filter((item) => !item.typeOnly && (isStore || identifierReferenced(source, item.local)));
|
||||||
|
const bindings = [
|
||||||
|
...defaultImport ? [defaultImport] : [],
|
||||||
|
...namespaceImport ? [namespaceImport] : [],
|
||||||
|
...namedImports.map((item) => item.local)
|
||||||
|
].filter(safeIdentifier);
|
||||||
|
const hasBindings = Boolean(defaultImport || namespaceImport || namedImports.length);
|
||||||
|
const sideEffectOnly = !entry.defaultImport && !entry.namespaceImport && entry.namedImports.length === 0;
|
||||||
|
if (!hasBindings && !sideEffectOnly)
|
||||||
|
return null;
|
||||||
|
if (sideEffectOnly)
|
||||||
|
return { code: entry.raw, bindings: [] };
|
||||||
|
const clauses = [];
|
||||||
|
if (defaultImport)
|
||||||
|
clauses.push(defaultImport);
|
||||||
|
if (namespaceImport)
|
||||||
|
clauses.push(`* as ${namespaceImport}`);
|
||||||
|
if (namedImports.length) {
|
||||||
|
clauses.push(`{ ${namedImports.map((item) => item.imported === item.local ? item.imported : `${item.imported} as ${item.local}`).join(", ")} }`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
code: `import ${clauses.join(", ")} from ${JSON.stringify(entry.source)};`,
|
||||||
|
bindings
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function selectedBrowserImports(ast, functions) {
|
||||||
|
const referenceSource = browserReferenceSource(ast, functions);
|
||||||
|
return ast.structuredImports.map((entry) => renderSelectedImport(entry, referenceSource)).filter((entry) => entry !== null);
|
||||||
|
}
|
||||||
|
function browserModuleRequired(ast) {
|
||||||
|
const functions = ast.runtimeFunctions.filter((fn) => ["legacy", "client", "shared"].includes(fn.runtime));
|
||||||
|
return functions.length > 0 || selectedBrowserImports(ast, functions).length > 0;
|
||||||
|
}
|
||||||
function functionEntry(ast, fn, availableFunctions) {
|
function functionEntry(ast, fn, availableFunctions) {
|
||||||
const parameterNames = new Set(fn.parameters.map((parameter) => parameter.name));
|
const parameterNames = new Set(fn.parameters.map((parameter) => parameter.name));
|
||||||
const stateNames = ast.states.filter((state) => state.runtime !== "server" && safeIdentifier(state.name) && !RUNTIME_BINDINGS.has(state.name) && !parameterNames.has(state.name)).map((state) => state.name);
|
const stateNames = ast.states.filter((state) => state.runtime !== "server" && safeIdentifier(state.name) && !RUNTIME_BINDINGS.has(state.name) && !parameterNames.has(state.name)).map((state) => state.name);
|
||||||
@@ -23182,14 +23261,10 @@ ${(0, codegen_ts_1.generate)(ast)}`,
|
|||||||
const functions = ast.runtimeFunctions.filter((fn) => ["legacy", "client", "shared"].includes(fn.runtime));
|
const functions = ast.runtimeFunctions.filter((fn) => ["legacy", "client", "shared"].includes(fn.runtime));
|
||||||
const functionNames = functions.map((fn) => fn.name);
|
const functionNames = functions.map((fn) => fn.name);
|
||||||
const state = ast.states.filter((entry) => entry.runtime !== "server").map((entry) => entry.name);
|
const state = ast.states.filter((entry) => entry.runtime !== "server").map((entry) => entry.name);
|
||||||
const storeImports = ast.structuredImports.filter((entry) => !entry.typeOnly && entry.source.endsWith(".wrn") && /(?:^|\/)stores?\//.test(entry.source));
|
const selectedImports = selectedBrowserImports(ast, functions);
|
||||||
const imports = ast.structuredImports.filter((entry) => !entry.typeOnly).filter((entry) => !entry.source.endsWith(".wrn") || /(?:^|\/)stores?\//.test(entry.source)).map((entry) => entry.raw).join(`
|
const imports = selectedImports.map((entry) => entry.code).join(`
|
||||||
`);
|
`);
|
||||||
const importedBindings = storeImports.flatMap((entry) => [
|
const importedBindings = [...new Set(selectedImports.flatMap((entry) => entry.bindings))];
|
||||||
...entry.defaultImport ? [entry.defaultImport] : [],
|
|
||||||
...entry.namespaceImport ? [entry.namespaceImport] : [],
|
|
||||||
...entry.namedImports.map((item) => item.local)
|
|
||||||
]).filter(safeIdentifier);
|
|
||||||
return `// generated WRNexusJS browser module for ${ast.name}
|
return `// generated WRNexusJS browser module for ${ast.name}
|
||||||
${imports}
|
${imports}
|
||||||
export const __wrnexusClientFunctions = {
|
export const __wrnexusClientFunctions = {
|
||||||
@@ -23220,6 +23295,7 @@ export function bindClientScope(context) {
|
|||||||
const syntax_1 = require2("@wrnexus/syntax");
|
const syntax_1 = require2("@wrnexus/syntax");
|
||||||
const store_codegen_ts_1 = require2("./store-codegen.js");
|
const store_codegen_ts_1 = require2("./store-codegen.js");
|
||||||
const analysis_ts_1 = require2("./analysis.js");
|
const analysis_ts_1 = require2("./analysis.js");
|
||||||
|
const client_codegen_ts_1 = require2("./client-codegen.js");
|
||||||
function isComponentTag(tag) {
|
function isComponentTag(tag) {
|
||||||
return /^[A-Z][A-Za-z0-9_$]*$/.test(tag);
|
return /^[A-Z][A-Za-z0-9_$]*$/.test(tag);
|
||||||
}
|
}
|
||||||
@@ -23380,7 +23456,7 @@ export function bindClientScope(context) {
|
|||||||
}
|
}
|
||||||
return scope;
|
return scope;
|
||||||
}
|
}
|
||||||
function substituteReactiveText(raw, reactive) {
|
function substituteReactiveText(raw, reactive, dynamicExpressions) {
|
||||||
const text = substituteTMarkers(raw);
|
const text = substituteTMarkers(raw);
|
||||||
if (!reactive || reactive.stateNames.size === 0)
|
if (!reactive || reactive.stateNames.size === 0)
|
||||||
return text;
|
return text;
|
||||||
@@ -23388,6 +23464,11 @@ export function bindClientScope(context) {
|
|||||||
const expr = inner.trim();
|
const expr = inner.trim();
|
||||||
if (expr.startsWith("t:") || !exprRefsState(expr, reactive.stateNames))
|
if (expr.startsWith("t:") || !exprRefsState(expr, reactive.stateNames))
|
||||||
return whole;
|
return whole;
|
||||||
|
if (exprRefsState(expr, reactive.runtimeStateNames) && dynamicExpressions) {
|
||||||
|
dynamicExpressions.push(`\${__wrnexusEscapeHtml(${expr})}`);
|
||||||
|
const sentinel = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
|
||||||
|
return `<span data-text="${attrEscape(expr)}">${sentinel}</span>`;
|
||||||
|
}
|
||||||
let value;
|
let value;
|
||||||
try {
|
try {
|
||||||
value = new Function("with(this){return (" + expr + ");}").call(reactive.scope);
|
value = new Function("with(this){return (" + expr + ");}").call(reactive.scope);
|
||||||
@@ -23415,7 +23496,10 @@ export function bindClientScope(context) {
|
|||||||
}
|
}
|
||||||
return out + escLit(raw.slice(last));
|
return out + escLit(raw.slice(last));
|
||||||
}
|
}
|
||||||
function bakeLoopAttr(raw) {
|
function bakeLoopAttr(raw, typed = false) {
|
||||||
|
const wholeExpression = wholeAttributeExpression(raw);
|
||||||
|
if (typed && wholeExpression)
|
||||||
|
return "${__wrnexusPropAttr(" + wholeExpression + ")}";
|
||||||
if (!raw.includes("{"))
|
if (!raw.includes("{"))
|
||||||
return escLit(attrEscape(raw));
|
return escLit(attrEscape(raw));
|
||||||
let out = "";
|
let out = "";
|
||||||
@@ -23445,7 +23529,7 @@ export function bindClientScope(context) {
|
|||||||
if (attr.boolean) {
|
if (attr.boolean) {
|
||||||
return escLit(` ${name}`);
|
return escLit(` ${name}`);
|
||||||
}
|
}
|
||||||
return escLit(` ${name}="`) + bakeLoopAttr(attr.value) + escLit(`"`);
|
return escLit(` ${name}="`) + bakeLoopAttr(attr.value, componentTag) + escLit(`"`);
|
||||||
}).join("");
|
}).join("");
|
||||||
const inner = node.children.map(renderLoopBody).join("");
|
const inner = node.children.map(renderLoopBody).join("");
|
||||||
if (node.tag === "Static")
|
if (node.tag === "Static")
|
||||||
@@ -23514,7 +23598,7 @@ export function bindClientScope(context) {
|
|||||||
}
|
}
|
||||||
function renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive = null) {
|
function renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive = null) {
|
||||||
if (node.type === "text")
|
if (node.type === "text")
|
||||||
return substituteReactiveText(node.value, reactive);
|
return substituteReactiveText(node.value, reactive, loops);
|
||||||
if (node.type === "each" || node.type === "if") {
|
if (node.type === "each" || node.type === "if") {
|
||||||
loops.push(node.type === "each" ? compileEachExpr(node) : compileIfExpr(node));
|
loops.push(node.type === "each" ? compileEachExpr(node) : compileIfExpr(node));
|
||||||
return `\x00WRNEACH${loops.length - 1}\x00`;
|
return `\x00WRNEACH${loops.length - 1}\x00`;
|
||||||
@@ -23535,24 +23619,36 @@ export function bindClientScope(context) {
|
|||||||
if (node.tag === "Async") {
|
if (node.tag === "Async") {
|
||||||
const source = attrValue(node.attrs, "source") ?? "data";
|
const source = attrValue(node.attrs, "source") ?? "data";
|
||||||
const retries = attrValue(node.attrs, "retries") ?? "2";
|
const retries = attrValue(node.attrs, "retries") ?? "2";
|
||||||
|
const tags = attrValue(node.attrs, "tags") ?? source;
|
||||||
const serverResolved = attrValue(node.attrs, "data-wrn-async-server") === "true";
|
const serverResolved = attrValue(node.attrs, "data-wrn-async-server") === "true";
|
||||||
const asyncIndex = serverResolved ? loops.push("") - 1 : -1;
|
const branchElement = (name) => node.children.find((child) => child.type === "element" && child.tag === name);
|
||||||
const branch = (name) => {
|
const branch = (name) => {
|
||||||
const element = node.children.find((child) => child.type === "element" && child.tag === name);
|
const element = branchElement(name);
|
||||||
return (element?.children ?? []).map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive)).join("");
|
return (element?.children ?? []).map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive)).join("");
|
||||||
};
|
};
|
||||||
const loading = branch("Loading");
|
const loading = branch("Loading");
|
||||||
const success = branch("Success");
|
const success = branch("Success");
|
||||||
const error = branch("Error");
|
const error = branch("Error");
|
||||||
|
const successElement = branchElement("Success");
|
||||||
|
const errorElement = branchElement("Error");
|
||||||
|
const identifier = (value, fallback) => value && isSafeGeneratedIdentifier(value) ? value : fallback;
|
||||||
|
const successAlias = identifier(successElement ? attrValue(successElement.attrs, "data") : undefined, identifier(source, "data"));
|
||||||
|
const errorAlias = identifier(errorElement ? attrValue(errorElement.attrs, "error") : undefined, "error");
|
||||||
|
const nested = (value) => value.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
||||||
|
const scoped = (value, alias, expression) => {
|
||||||
|
const index = loops.push(`\${(() => { const ${alias} = ${expression}; return \`${nested(value)}\`; })()}`) - 1;
|
||||||
|
return `\x00WRNEACH${index}\x00`;
|
||||||
|
};
|
||||||
|
const successTemplate = scoped(success, successAlias, `(ctx[${JSON.stringify(source)}] ?? {})`);
|
||||||
|
const errorTemplate = scoped(error, errorAlias, `{ message: "" }`);
|
||||||
let initial = loading;
|
let initial = loading;
|
||||||
if (serverResolved) {
|
if (serverResolved) {
|
||||||
const nested = (value) => value.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
const sourcePattern = successAlias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
const sourcePattern = source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
||||||
const serverSuccess = success.replace(new RegExp(`\\{\\s*(${sourcePattern}(?:\\.[A-Za-z_$][\\w$]*)*)\\s*\\}`, "g"), (_whole, expression) => `\${__wrnexusEscapeHtml(${expression})}`);
|
const serverSuccess = success.replace(new RegExp(`\\{\\s*(${sourcePattern}(?:\\.[A-Za-z_$][\\w$]*)*)\\s*\\}`, "g"), (_whole, expression) => `\${__wrnexusEscapeHtml(${expression})}`);
|
||||||
loops[asyncIndex] = `\${ctx[${JSON.stringify(source)}] !== undefined ? \`${nested(serverSuccess)}\` : \`${nested(loading)}\`}`;
|
const index = loops.push(`\${ctx[${JSON.stringify(source)}] !== undefined ? (() => { const ${successAlias} = ctx[${JSON.stringify(source)}]; return \`${nested(serverSuccess)}\`; })() : \`${nested(loading)}\`}`) - 1;
|
||||||
initial = `\x00WRNEACH${asyncIndex}\x00`;
|
initial = `\x00WRNEACH${index}\x00`;
|
||||||
}
|
}
|
||||||
return `<section data-wrn-async="${attrEscape(source)}" data-wrn-async-retries="${attrEscape(retries)}"${serverResolved ? ' data-wrn-async-resolved="true"' : ""} aria-busy="${serverResolved ? "false" : "true"}"><div data-wrn-async-content>${initial}</div><template data-wrn-async-loading>${loading}</template><template data-wrn-async-success>${success}</template><template data-wrn-async-error>${error}</template></section>`;
|
return `<section data-wrn-async="${attrEscape(source)}" data-wrn-async-tags="${attrEscape(tags)}" data-wrn-async-retries="${attrEscape(retries)}"${serverResolved ? ' data-wrn-async-resolved="true"' : ""} aria-busy="${serverResolved ? "false" : "true"}"><div data-wrn-async-content>${initial}</div><template data-wrn-async-loading>${loading}</template><template data-wrn-async-success data-wrn-async-alias="${attrEscape(successAlias)}">${successTemplate}</template><template data-wrn-async-error data-wrn-async-alias="${attrEscape(errorAlias)}">${errorTemplate}</template></section>`;
|
||||||
}
|
}
|
||||||
if (node.tag === "KeepAlive") {
|
if (node.tag === "KeepAlive") {
|
||||||
const key = attrValue(node.attrs, "key") ?? "default";
|
const key = attrValue(node.attrs, "key") ?? "default";
|
||||||
@@ -23836,9 +23932,104 @@ ${css}
|
|||||||
return `const { ${names.join(", ")} } = __state;
|
return `const { ${names.join(", ")} } = __state;
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
function orderPageStates(entries) {
|
||||||
|
if (entries.length < 2)
|
||||||
|
return entries;
|
||||||
|
const byName = new Map(entries.map((entry) => [entry.name, entry]));
|
||||||
|
const visiting = new Set;
|
||||||
|
const visited = new Set;
|
||||||
|
const ordered = [];
|
||||||
|
const visit = (name) => {
|
||||||
|
if (visited.has(name))
|
||||||
|
return;
|
||||||
|
if (visiting.has(name)) {
|
||||||
|
throw new Error(`WRN-STATE-CYCLE: state value '${name}' has a dependency cycle.`);
|
||||||
|
}
|
||||||
|
const entry = byName.get(name);
|
||||||
|
if (!entry)
|
||||||
|
return;
|
||||||
|
visiting.add(name);
|
||||||
|
for (const dependency of byName.keys()) {
|
||||||
|
if (dependency !== name && new RegExp(`\\b${dependency.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(entry.expr)) {
|
||||||
|
visit(dependency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visiting.delete(name);
|
||||||
|
visited.add(name);
|
||||||
|
ordered.push(entry);
|
||||||
|
};
|
||||||
|
for (const entry of entries)
|
||||||
|
visit(entry.name);
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
function generateSsrStateInitializer(entries) {
|
||||||
|
const ordered = orderPageStates(entries);
|
||||||
|
const declarations = ordered.filter((entry) => isSafeGeneratedIdentifier(entry.name)).map((entry) => `const ${entry.name} = (() => { try { return (${entry.expr}); } catch { return undefined; } })();`).join(" ");
|
||||||
|
const values = entries.map((entry) => {
|
||||||
|
if (isSafeGeneratedIdentifier(entry.name)) {
|
||||||
|
return `${JSON.stringify(entry.name)}: ${entry.name}`;
|
||||||
|
}
|
||||||
|
return `${JSON.stringify(entry.name)}: (() => { try { return (${entry.expr}); } catch { return undefined; } })()`;
|
||||||
|
}).join(", ");
|
||||||
|
return `(() => { ${declarations} return { ${values} }; })()`;
|
||||||
|
}
|
||||||
|
function orderPageComputed(entries) {
|
||||||
|
if (entries.length < 2)
|
||||||
|
return entries;
|
||||||
|
const byName = new Map(entries.map((entry) => [entry.name, entry]));
|
||||||
|
const visiting = new Set;
|
||||||
|
const visited = new Set;
|
||||||
|
const ordered = [];
|
||||||
|
const visit = (name) => {
|
||||||
|
if (visited.has(name))
|
||||||
|
return;
|
||||||
|
if (visiting.has(name)) {
|
||||||
|
throw new Error(`WRN-COMPUTED-CYCLE: computed value '${name}' has a dependency cycle.`);
|
||||||
|
}
|
||||||
|
const entry = byName.get(name);
|
||||||
|
if (!entry)
|
||||||
|
return;
|
||||||
|
visiting.add(name);
|
||||||
|
for (const dependency of byName.keys()) {
|
||||||
|
if (dependency !== name && new RegExp(`\\b${dependency.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(entry.expr)) {
|
||||||
|
visit(dependency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visiting.delete(name);
|
||||||
|
visited.add(name);
|
||||||
|
ordered.push(entry);
|
||||||
|
};
|
||||||
|
for (const entry of entries)
|
||||||
|
visit(entry.name);
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
function generateSsrComputedAliases(entries) {
|
||||||
|
return orderPageComputed(entries).filter((entry) => isSafeGeneratedIdentifier(entry.name)).map((entry) => `const ${entry.name} = (() => { try { return (${entry.expr}); } catch { return undefined; } })();`).join(`
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
function runtimeComputedNames(states, computed, runtimeRoots = []) {
|
||||||
|
const entries = [...states, ...computed];
|
||||||
|
const runtime = new Set([
|
||||||
|
...runtimeRoots,
|
||||||
|
...entries.filter((entry) => /\bctx\b/.test(entry.expr)).map((entry) => entry.name)
|
||||||
|
]);
|
||||||
|
let changed = true;
|
||||||
|
while (changed) {
|
||||||
|
changed = false;
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (runtime.has(entry.name))
|
||||||
|
continue;
|
||||||
|
if ([...runtime].some((name) => new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(entry.expr))) {
|
||||||
|
runtime.add(entry.name);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return runtime;
|
||||||
|
}
|
||||||
function hydrationAttribute(ast) {
|
function hydrationAttribute(ast) {
|
||||||
const strategy = ["static", "server"].includes(ast.renderMode ?? "") ? "none" : ast.hydrate ?? "load";
|
const strategy = ["static", "server"].includes(ast.renderMode ?? "") ? "none" : ast.hydrate ?? "load";
|
||||||
const hasBrowserModule = ast.runtimeFunctions.some((fn) => ["legacy", "client", "shared"].includes(fn.runtime));
|
const hasBrowserModule = (0, client_codegen_ts_1.browserModuleRequired)(ast);
|
||||||
const moduleAttribute = hasBrowserModule ? ' data-wrn-client-module="__WRNEXUS_CLIENT_MODULE__"' : "";
|
const moduleAttribute = hasBrowserModule ? ' data-wrn-client-module="__WRNEXUS_CLIENT_MODULE__"' : "";
|
||||||
return ` data-wrn-hydration="${attrEscape(hydrationId(ast))}" data-wrn-hydrate="${attrEscape(strategy)}" data-wrn-runtime="${attrEscape(ast.runtime ?? "universal")}"${moduleAttribute}`;
|
return ` data-wrn-hydration="${attrEscape(hydrationId(ast))}" data-wrn-hydrate="${attrEscape(strategy)}" data-wrn-runtime="${attrEscape(ast.runtime ?? "universal")}"${moduleAttribute}`;
|
||||||
}
|
}
|
||||||
@@ -23960,9 +24151,11 @@ ${helpers}`);
|
|||||||
if (Object.keys(ast.navigation).length > 0) {
|
if (Object.keys(ast.navigation).length > 0) {
|
||||||
out.push(`export const __wrnexusNavigation = ${JSON.stringify(ast.navigation, null, 2)};`);
|
out.push(`export const __wrnexusNavigation = ${JSON.stringify(ast.navigation, null, 2)};`);
|
||||||
}
|
}
|
||||||
|
const orderedStates = orderPageStates(ast.states);
|
||||||
const browserStates = ast.states.filter((state) => state.runtime !== "server");
|
const browserStates = ast.states.filter((state) => state.runtime !== "server");
|
||||||
const seedScope = evalStateSeeds(ast.states);
|
const seedScope = evalStateSeeds(orderedStates);
|
||||||
for (const entry of ast.computed) {
|
const orderedComputed = orderPageComputed(ast.computed);
|
||||||
|
for (const entry of orderedComputed) {
|
||||||
try {
|
try {
|
||||||
seedScope[entry.name] = new Function("with(this){return (" + entry.expr + ");}").call(seedScope);
|
seedScope[entry.name] = new Function("with(this){return (" + entry.expr + ");}").call(seedScope);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -23973,7 +24166,12 @@ ${helpers}`);
|
|||||||
...browserStates.map((entry) => entry.name),
|
...browserStates.map((entry) => entry.name),
|
||||||
...ast.computed.map((entry) => entry.name)
|
...ast.computed.map((entry) => entry.name)
|
||||||
];
|
];
|
||||||
const runtimeStateNames = new Set(ast.states.filter((entry) => /\bctx\b/.test(entry.expr)).map((entry) => entry.name));
|
const storeBindings = importedStoreBindings(ast);
|
||||||
|
const runtimeRoots = [
|
||||||
|
...storeBindings.map((entry) => entry.local),
|
||||||
|
...ast.loads.filter((load) => load.mode === "server" && !load.deferred && load.name).map((load) => load.name)
|
||||||
|
];
|
||||||
|
const runtimeStateNames = runtimeComputedNames(orderedStates, orderedComputed, runtimeRoots);
|
||||||
const reactive = reactiveNames.length > 0 ? { stateNames: new Set(reactiveNames), runtimeStateNames, scope: seedScope } : null;
|
const reactive = reactiveNames.length > 0 ? { stateNames: new Set(reactiveNames), runtimeStateNames, scope: seedScope } : null;
|
||||||
const loops = [];
|
const loops = [];
|
||||||
let html = ast.view.map((node) => renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive)).join("");
|
let html = ast.view.map((node) => renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive)).join("");
|
||||||
@@ -24007,21 +24205,22 @@ ${helpers}`);
|
|||||||
const shellHtml = html.replace(/<wrn-dynamic-region\b[^>]*>[\s\S]*?<\/wrn-dynamic-region>/gi, '<wrn-dynamic-region data-wrn-dynamic="true"></wrn-dynamic-region>');
|
const shellHtml = html.replace(/<wrn-dynamic-region\b[^>]*>[\s\S]*?<\/wrn-dynamic-region>/gi, '<wrn-dynamic-region data-wrn-dynamic="true"></wrn-dynamic-region>');
|
||||||
staticShellBody = templateEscape(shellHtml);
|
staticShellBody = templateEscape(shellHtml);
|
||||||
}
|
}
|
||||||
const dynamicStateScope = ast.states.map((state) => `${JSON.stringify(state.name)}: (() => { try { return (${state.expr}); } catch { return undefined; } })()`).join(", ");
|
const dynamicStateInitializer = generateSsrStateInitializer(orderedStates);
|
||||||
const stateType = ast.states.length > 0 ? `{ ${ast.states.map((state) => `${JSON.stringify(state.name)}: ${state.valueType ?? "unknown"}`).join("; ")} }` : "Record<string, never>";
|
const stateType = ast.states.length > 0 ? `{ ${ast.states.map((state) => `${JSON.stringify(state.name)}: ${state.valueType ?? "unknown"}`).join("; ")} }` : "Record<string, never>";
|
||||||
const hydrationStateNames = JSON.stringify(browserStates.map((state) => state.name));
|
const hydrationStateNames = JSON.stringify(browserStates.map((state) => state.name));
|
||||||
const ssrStateAliases = generateSsrStateAliases(ast.states.map((state) => state.name));
|
const ssrStateAliases = generateSsrStateAliases(ast.states.map((state) => state.name));
|
||||||
const storeBindings = importedStoreBindings(ast);
|
const ssrComputedAliases = generateSsrComputedAliases(orderedComputed);
|
||||||
const storeDeclarations = storeBindings.map((entry) => ` const ${entry.local} = await ctx.__wrnexusUseStore(${entry.internal});`).join(`
|
const storeDeclarations = storeBindings.map((entry) => ` const ${entry.local} = await ctx.__wrnexusUseStore(${entry.internal});`).join(`
|
||||||
`);
|
`);
|
||||||
const serverLoadAliases = ast.loads.filter((load) => load.mode === "server" && !load.deferred && load.name).map((load) => ` const ${load.name} = ctx[${JSON.stringify(load.name)}];`).join(`
|
const serverLoadAliases = ast.loads.filter((load) => load.mode === "server" && !load.deferred && load.name).map((load) => ` const ${load.name} = ctx[${JSON.stringify(load.name)}];`).join(`
|
||||||
`);
|
`);
|
||||||
loops.forEach((code, idx) => {
|
for (let idx = loops.length - 1;idx >= 0; idx--) {
|
||||||
body = body.replace(`\x00WRNEACH${idx}\x00`, () => code);
|
const code = loops[idx];
|
||||||
|
body = body.replaceAll(`\x00WRNEACH${idx}\x00`, code);
|
||||||
if (staticShellBody?.includes(`\x00WRNEACH${idx}\x00`)) {
|
if (staticShellBody?.includes(`\x00WRNEACH${idx}\x00`)) {
|
||||||
staticShellBody = staticShellBody.replace(`\x00WRNEACH${idx}\x00`, () => code);
|
staticShellBody = staticShellBody.replaceAll(`\x00WRNEACH${idx}\x00`, code);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
const loopConsts = [];
|
const loopConsts = [];
|
||||||
if (loops.length > 0) {
|
if (loops.length > 0) {
|
||||||
const lists = collectControlExprs(ast.view);
|
const lists = collectControlExprs(ast.view);
|
||||||
@@ -24033,7 +24232,7 @@ ${helpers}`);
|
|||||||
loopConsts.push(` const ${name} = __wrnexusEvalData(await __wrnexusCallApi(${JSON.stringify(binding.path)}, ${JSON.stringify(binding.method)}, ctx), ${JSON.stringify(binding.body)}, ${JSON.stringify(binding.helpers)}, ctx);`);
|
loopConsts.push(` const ${name} = __wrnexusEvalData(await __wrnexusCallApi(${JSON.stringify(binding.path)}, ${JSON.stringify(binding.method)}, ctx), ${JSON.stringify(binding.body)}, ${JSON.stringify(binding.helpers)}, ctx);`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const needsSsrRuntime = ssrBindings.length > 0 || loops.length > 0 || ast.states.some((state) => /\bctx\b/.test(state.expr));
|
const needsSsrRuntime = ssrBindings.length > 0 || loops.length > 0 || runtimeStateNames.size > 0;
|
||||||
if (needsSsrRuntime) {
|
if (needsSsrRuntime) {
|
||||||
out.push(ssrRuntimeSource());
|
out.push(ssrRuntimeSource());
|
||||||
out.push(`const __wrnexusSsrBindings = ${JSON.stringify(ssrBindings, null, 2)};`);
|
out.push(`const __wrnexusSsrBindings = ${JSON.stringify(ssrBindings, null, 2)};`);
|
||||||
@@ -24044,16 +24243,18 @@ ${helpers}`);
|
|||||||
${storeDeclarations}
|
${storeDeclarations}
|
||||||
${serverLoadAliases}
|
${serverLoadAliases}
|
||||||
${decls}
|
${decls}
|
||||||
const __state: ${stateType} = { ${dynamicStateScope} };
|
const __state: ${stateType} = ${dynamicStateInitializer};
|
||||||
${ssrStateAliases}
|
${ssrStateAliases}
|
||||||
|
${ssrComputedAliases}
|
||||||
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
|
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
|
||||||
const __scopeValue = Object.entries(__hydrationState)
|
const __scopeValue = Object.entries(__hydrationState)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
const encoded =
|
let encoded: string;
|
||||||
typeof value === "number" || typeof value === "boolean"
|
try {
|
||||||
? String(value)
|
encoded = value === undefined ? "undefined" : JSON.stringify(value);
|
||||||
: JSON.stringify(value == null ? "" : String(value));
|
} catch {
|
||||||
|
encoded = JSON.stringify(String(value));
|
||||||
|
}
|
||||||
return key + ": " + encoded;
|
return key + ": " + encoded;
|
||||||
})
|
})
|
||||||
.join(", ")
|
.join(", ")
|
||||||
@@ -24073,16 +24274,18 @@ ${helpers}`);
|
|||||||
out.push(`export default ${storeBindings.length > 0 ? "async " : ""}function ${ast.name}(ctx: any) {
|
out.push(`export default ${storeBindings.length > 0 ? "async " : ""}function ${ast.name}(ctx: any) {
|
||||||
${storeDeclarations}
|
${storeDeclarations}
|
||||||
${serverLoadAliases}
|
${serverLoadAliases}
|
||||||
const __state: ${stateType} = { ${dynamicStateScope} };
|
const __state: ${stateType} = ${dynamicStateInitializer};
|
||||||
${ssrStateAliases}
|
${ssrStateAliases}
|
||||||
|
${ssrComputedAliases}
|
||||||
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
|
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
|
||||||
const __scopeValue = Object.entries(__hydrationState)
|
const __scopeValue = Object.entries(__hydrationState)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
const encoded =
|
let encoded: string;
|
||||||
typeof value === "number" || typeof value === "boolean"
|
try {
|
||||||
? String(value)
|
encoded = value === undefined ? "undefined" : JSON.stringify(value);
|
||||||
: JSON.stringify(value == null ? "" : String(value));
|
} catch {
|
||||||
|
encoded = JSON.stringify(String(value));
|
||||||
|
}
|
||||||
return key + ": " + encoded;
|
return key + ": " + encoded;
|
||||||
})
|
})
|
||||||
.join(", ")
|
.join(", ")
|
||||||
@@ -24103,14 +24306,18 @@ ${helpers}`);
|
|||||||
${serverLoadAliases}
|
${serverLoadAliases}
|
||||||
${loopConsts.length > 0 ? loopConsts.join(`
|
${loopConsts.length > 0 ? loopConsts.join(`
|
||||||
`) : ""}
|
`) : ""}
|
||||||
const __state: ${stateType} = { ${dynamicStateScope} };
|
const __state: ${stateType} = ${dynamicStateInitializer};
|
||||||
${ssrStateAliases}
|
${ssrStateAliases}
|
||||||
|
${ssrComputedAliases}
|
||||||
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
|
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
|
||||||
const __scopeValue = Object.entries(__hydrationState)
|
const __scopeValue = Object.entries(__hydrationState)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
const encoded = typeof value === "number" || typeof value === "boolean"
|
let encoded: string;
|
||||||
? String(value)
|
try {
|
||||||
: JSON.stringify(value == null ? "" : String(value));
|
encoded = value === undefined ? "undefined" : JSON.stringify(value);
|
||||||
|
} catch {
|
||||||
|
encoded = JSON.stringify(String(value));
|
||||||
|
}
|
||||||
return key + ": " + encoded;
|
return key + ": " + encoded;
|
||||||
})
|
})
|
||||||
.join(", ")
|
.join(", ")
|
||||||
@@ -24270,7 +24477,14 @@ ${handlers.join(`
|
|||||||
"await",
|
"await",
|
||||||
"enum",
|
"enum",
|
||||||
"with",
|
"with",
|
||||||
"debugger"
|
"debugger",
|
||||||
|
"implements",
|
||||||
|
"interface",
|
||||||
|
"package",
|
||||||
|
"private",
|
||||||
|
"protected",
|
||||||
|
"public",
|
||||||
|
"static"
|
||||||
]);
|
]);
|
||||||
function safeRef(name) {
|
function safeRef(name) {
|
||||||
return JS_RESERVED.has(name) ? `__p_${name}` : name;
|
return JS_RESERVED.has(name) ? `__p_${name}` : name;
|
||||||
@@ -25044,11 +25258,23 @@ function __wireRaw(v: any): string {
|
|||||||
}
|
}
|
||||||
function resolveWrnImport(declaration, importer, options) {
|
function resolveWrnImport(declaration, importer, options) {
|
||||||
const source = declaration.source;
|
const source = declaration.source;
|
||||||
if (!source.startsWith(".") && !source.startsWith("@/"))
|
const aliases = {
|
||||||
|
"@": "./app",
|
||||||
|
...options.aliases ?? {}
|
||||||
|
};
|
||||||
|
const alias = Object.keys(aliases).filter((key) => key.length > 0 && (source === key || source.startsWith(`${key}/`))).sort((left, right) => right.length - left.length)[0];
|
||||||
|
if (!source.startsWith(".") && !alias)
|
||||||
return { declaration, resolved: source };
|
return { declaration, resolved: source };
|
||||||
const aliasRoot = options.aliases?.["@"] ?? "./app";
|
const base = alias ? (0, node_path_1.resolve)(options.appRoot, aliases[alias], source === alias ? "" : source.slice(alias.length + 1)) : (0, node_path_1.resolve)((0, node_path_1.dirname)(importer), source);
|
||||||
const base = source.startsWith("@/") ? (0, node_path_1.resolve)(options.appRoot, aliasRoot, source.slice(2)) : (0, node_path_1.resolve)((0, node_path_1.dirname)(importer), source);
|
const found = candidates(base).find((candidate) => {
|
||||||
const found = candidates(base).find(node_fs_1.existsSync);
|
if (!(0, node_fs_1.existsSync)(candidate))
|
||||||
|
return false;
|
||||||
|
try {
|
||||||
|
return (0, node_fs_1.statSync)(candidate).isFile();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
if (found)
|
if (found)
|
||||||
return { declaration, resolved: (0, node_fs_1.realpathSync)(found) };
|
return { declaration, resolved: (0, node_fs_1.realpathSync)(found) };
|
||||||
const severity = (options.mode ?? "compatible") === "explicit" ? "error" : "warning";
|
const severity = (options.mode ?? "compatible") === "explicit" ? "error" : "warning";
|
||||||
|
|||||||
+5
-4
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wrnexus",
|
"name": "wrnexus",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "An SSR-first full-stack web framework with server-rendered reactive components. Bun-first, Node-friendly.",
|
"description": "An SSR-first full-stack web framework with server-rendered reactive components. Bun-first, Node-friendly.",
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"format": "prettier . --write",
|
"format": "prettier . --write",
|
||||||
"format:check": "prettier . --check",
|
"format:check": "prettier . --check",
|
||||||
"check": "bun run typecheck && bun run lint && bun run test && bun run format:check",
|
"check": "bun run typecheck && bun run lint && bun run check:component-imports && bun run test && bun run format:check",
|
||||||
"verify:0.3": "node scripts/verify-0.3.mjs",
|
"verify:0.3": "node scripts/verify-0.3.mjs",
|
||||||
"verify:0.4": "node scripts/verify-0.4.mjs",
|
"verify:0.4": "node scripts/verify-0.4.mjs",
|
||||||
"validate:0.4": "node scripts/validate-0.4.mjs",
|
"validate:0.4": "node scripts/validate-0.4.mjs",
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
"generate:ui-visual": "node scripts/check-ui-visual-contract.mjs --write",
|
"generate:ui-visual": "node scripts/check-ui-visual-contract.mjs --write",
|
||||||
"sbom": "node scripts/generate-sbom.mjs",
|
"sbom": "node scripts/generate-sbom.mjs",
|
||||||
"benchmark:framework": "node --experimental-transform-types scripts/benchmark-framework.mjs",
|
"benchmark:framework": "node --experimental-transform-types scripts/benchmark-framework.mjs",
|
||||||
"check:production": "bun run check:workspace && bun run check:public-api && bun run check:ui-visual && bun run validate:0.8 && bun run security:framework && bun run security:asvs && bun run check",
|
"check:production": "bun run check:workspace && bun run check:public-api && bun run check:ui-visual && bun run validate:0.8 && bun run security:framework && bun run security:asvs && bun run check:editor-compiler && bun run check:editor-language-server && bun run check:editor-extension && bun run check",
|
||||||
"validate:staging": "node --experimental-transform-types scripts/test-package-integrity.mjs",
|
"validate:staging": "node --experimental-transform-types scripts/test-package-integrity.mjs",
|
||||||
"stage:packages": "bun run scripts/publish-packages.ts",
|
"stage:packages": "bun run scripts/publish-packages.ts",
|
||||||
"test:staged-consumers": "node scripts/test-staged-consumers.mjs",
|
"test:staged-consumers": "node scripts/test-staged-consumers.mjs",
|
||||||
@@ -61,7 +61,8 @@
|
|||||||
"validate:0.8": "node scripts/validate-0.8.mjs",
|
"validate:0.8": "node scripts/validate-0.8.mjs",
|
||||||
"audit:packages": "node scripts/audit-package-kits.mjs",
|
"audit:packages": "node scripts/audit-package-kits.mjs",
|
||||||
"generate:package-audit": "node scripts/audit-package-kits.mjs --write",
|
"generate:package-audit": "node scripts/audit-package-kits.mjs --write",
|
||||||
"test:package-kits": "node --experimental-transform-types scripts/test-package-kits.mjs"
|
"test:package-kits": "node --experimental-transform-types scripts/test-package-kits.mjs",
|
||||||
|
"check:component-imports": "node scripts/check-component-imports.mjs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ai",
|
"name": "@wrnexus/ai",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
|
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Badge, Button, Card } from "@wrnexus/ui"
|
||||||
|
|
||||||
component AccountStatus {
|
component AccountStatus {
|
||||||
props {
|
props {
|
||||||
status: string = "active"
|
status: string = "active"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Button } from "@wrnexus/ui"
|
||||||
|
|
||||||
component AuthProviderButtons {
|
component AuthProviderButtons {
|
||||||
outputs {
|
outputs {
|
||||||
select(payload: { provider: string; href: string })
|
select(payload: { provider: string; href: string })
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Alert } from "@wrnexus/ui"
|
||||||
|
|
||||||
component AuthSecurityNotice {
|
component AuthSecurityNotice {
|
||||||
props {
|
props {
|
||||||
title: string = "Security notice"
|
title: string = "Security notice"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Card } from "@wrnexus/ui"
|
||||||
|
|
||||||
component AuthShell {
|
component AuthShell {
|
||||||
props {
|
props {
|
||||||
title: string = "Welcome"
|
title: string = "Welcome"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Button, Input, PinInput } from "@wrnexus/ui"
|
||||||
|
|
||||||
component AuthenticatorSetup {
|
component AuthenticatorSetup {
|
||||||
props {
|
props {
|
||||||
setupAction = "/api/auth/totp/setup"
|
setupAction = "/api/auth/totp/setup"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Avatar, Badge, Button, Card } from "@wrnexus/ui"
|
||||||
|
|
||||||
component DeviceSessions {
|
component DeviceSessions {
|
||||||
props {
|
props {
|
||||||
sessions: unknown[] = []
|
sessions: unknown[] = []
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Button, Input } from "@wrnexus/ui"
|
||||||
|
|
||||||
component ForgotPassword {
|
component ForgotPassword {
|
||||||
props {
|
props {
|
||||||
action = "/api/auth/password/request"
|
action = "/api/auth/password/request"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Alert, Button } from "@wrnexus/ui"
|
||||||
|
|
||||||
component ImpersonationBanner {
|
component ImpersonationBanner {
|
||||||
props {
|
props {
|
||||||
visible: boolean = true
|
visible: boolean = true
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Button, Input, StrongPassword } from "@wrnexus/ui"
|
||||||
|
|
||||||
component InvitationAccept {
|
component InvitationAccept {
|
||||||
props {
|
props {
|
||||||
action = "/api/auth/invitations/accept"
|
action = "/api/auth/invitations/accept"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Button, Input } from "@wrnexus/ui"
|
||||||
|
|
||||||
component MagicLinkSignIn {
|
component MagicLinkSignIn {
|
||||||
props {
|
props {
|
||||||
action = "/api/auth/magic-link/request"
|
action = "/api/auth/magic-link/request"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Button, Input, PinInput } from "@wrnexus/ui"
|
||||||
|
|
||||||
component OtpSignIn {
|
component OtpSignIn {
|
||||||
props {
|
props {
|
||||||
requestAction = "/api/auth/otp/login/request"
|
requestAction = "/api/auth/otp/login/request"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Button } from "@wrnexus/ui"
|
||||||
|
|
||||||
component PasskeyButton {
|
component PasskeyButton {
|
||||||
outputs {
|
outputs {
|
||||||
passkeyRegistered(payload: { credentialId: string; response?: object })
|
passkeyRegistered(payload: { credentialId: string; response?: object })
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Alert, Button, Card } from "@wrnexus/ui"
|
||||||
|
|
||||||
component RecoveryCodes {
|
component RecoveryCodes {
|
||||||
props {
|
props {
|
||||||
codes: unknown[] = []
|
codes: unknown[] = []
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Button, StrongPassword } from "@wrnexus/ui"
|
||||||
|
|
||||||
component ResetPassword {
|
component ResetPassword {
|
||||||
props {
|
props {
|
||||||
action = "/api/auth/password/reset"
|
action = "/api/auth/password/reset"
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import PasskeyButton from "./PasskeyButton.wrn"
|
||||||
|
import { Button, Checkbox, Input, TogglePassword } from "@wrnexus/ui"
|
||||||
|
|
||||||
component SignIn {
|
component SignIn {
|
||||||
props {
|
props {
|
||||||
action = "/api/auth/login"
|
action = "/api/auth/login"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Button, Checkbox, Input, StrongPassword } from "@wrnexus/ui"
|
||||||
|
|
||||||
component SignUp {
|
component SignUp {
|
||||||
props {
|
props {
|
||||||
action = "/api/auth/register"
|
action = "/api/auth/register"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Button, PinInput, Select } from "@wrnexus/ui"
|
||||||
|
|
||||||
component TwoFactorChallenge {
|
component TwoFactorChallenge {
|
||||||
props {
|
props {
|
||||||
action = "/api/auth/mfa/complete"
|
action = "/api/auth/mfa/complete"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Button, Input } from "@wrnexus/ui"
|
||||||
|
|
||||||
component VerifyEmail {
|
component VerifyEmail {
|
||||||
props {
|
props {
|
||||||
kind = "email"
|
kind = "email"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Button, PinInput } from "@wrnexus/ui"
|
||||||
|
|
||||||
component VerifyPhone {
|
component VerifyPhone {
|
||||||
props {
|
props {
|
||||||
kind = "phone"
|
kind = "phone"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/auth",
|
"name": "@wrnexus/auth",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
|
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/authz",
|
"name": "@wrnexus/authz",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/benchmark",
|
"name": "@wrnexus/benchmark",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Deterministic benchmark runner and performance regression budgets for WRNexusJS.",
|
"description": "Deterministic benchmark runner and performance regression budgets for WRNexusJS.",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/cache",
|
"name": "@wrnexus/cache",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Tag-aware memory and response caching with stale-while-revalidate for WRNexusJS.",
|
"description": "Tag-aware memory and response caching with stale-while-revalidate for WRNexusJS.",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import Captcha from "./Captcha.wrn"
|
||||||
|
import { Card } from "@wrnexus/ui"
|
||||||
|
|
||||||
component CaptchaField {
|
component CaptchaField {
|
||||||
props {
|
props {
|
||||||
title: string = "Security verification"
|
title: string = "Security verification"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Alert } from "@wrnexus/ui"
|
||||||
|
|
||||||
component CaptchaStatus {
|
component CaptchaStatus {
|
||||||
props {
|
props {
|
||||||
success: boolean = false
|
success: boolean = false
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/captcha",
|
"name": "@wrnexus/captcha",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.",
|
"description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/cli",
|
"name": "@wrnexus/cli",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
+98
-10
@@ -29,6 +29,7 @@ import {
|
|||||||
analyzeRuntimeRequirements,
|
analyzeRuntimeRequirements,
|
||||||
assertValidAst,
|
assertValidAst,
|
||||||
generate,
|
generate,
|
||||||
|
generateTargets,
|
||||||
parse,
|
parse,
|
||||||
type RuntimeRequirements,
|
type RuntimeRequirements,
|
||||||
type DeploymentRuntime,
|
type DeploymentRuntime,
|
||||||
@@ -106,6 +107,7 @@ export async function runBuild(appRoot: string): Promise<void> {
|
|||||||
const appDir = join(root, "app");
|
const appDir = join(root, "app");
|
||||||
const distDir = join(root, "dist");
|
const distDir = join(root, "dist");
|
||||||
const compiledDir = join(distDir, "compiled");
|
const compiledDir = join(distDir, "compiled");
|
||||||
|
const clientModulesDir = join(distDir, "client");
|
||||||
const reactivePath = join(distDir, "reactive.js");
|
const reactivePath = join(distDir, "reactive.js");
|
||||||
const publicDir = join(root, "public");
|
const publicDir = join(root, "public");
|
||||||
const distPublicDir = join(distDir, "public");
|
const distPublicDir = join(distDir, "public");
|
||||||
@@ -117,6 +119,7 @@ export async function runBuild(appRoot: string): Promise<void> {
|
|||||||
// Clean output.
|
// Clean output.
|
||||||
rmSync(distDir, { recursive: true, force: true });
|
rmSync(distDir, { recursive: true, force: true });
|
||||||
mkdirSync(compiledDir, { recursive: true });
|
mkdirSync(compiledDir, { recursive: true });
|
||||||
|
mkdirSync(clientModulesDir, { recursive: true });
|
||||||
if (existsSync(publicDir)) {
|
if (existsSync(publicDir)) {
|
||||||
cpSync(publicDir, distPublicDir, { recursive: true });
|
cpSync(publicDir, distPublicDir, { recursive: true });
|
||||||
console.log(`✓ Public: ${distPublicDir}`);
|
console.log(`✓ Public: ${distPublicDir}`);
|
||||||
@@ -169,15 +172,29 @@ export async function runBuild(appRoot: string): Promise<void> {
|
|||||||
// keep the exact compiler path and output contract by default.
|
// keep the exact compiler path and output contract by default.
|
||||||
let compiledCount = 0;
|
let compiledCount = 0;
|
||||||
const compiledFiles = new Map<string, string>();
|
const compiledFiles = new Map<string, string>();
|
||||||
|
const clientFiles = new Map<string, string>();
|
||||||
const runtimeAnalysis = new Map<string, RuntimeRequirements>();
|
const runtimeAnalysis = new Map<string, RuntimeRequirements>();
|
||||||
const partialStaticFiles = new Set<string>();
|
const partialStaticFiles = new Set<string>();
|
||||||
const compileWrn = async (file: string): Promise<void> => {
|
const compileWrn = async (file: string): Promise<void> => {
|
||||||
if (!file.endsWith(".wrn") || compiledFiles.has(file)) return;
|
if (!file.endsWith(".wrn") || compiledFiles.has(file)) return;
|
||||||
const out = join(compiledDir, `route${compiledCount++}.ts`);
|
|
||||||
// Reserve the artifact before resolving imports so cycles terminate and
|
|
||||||
// mutually dependent generated modules can point at deterministic paths.
|
|
||||||
compiledFiles.set(file, out);
|
|
||||||
const source = readFileSync(file, "utf8");
|
const source = readFileSync(file, "utf8");
|
||||||
|
const artifactId = compiledCount++;
|
||||||
|
const out = join(compiledDir, `route${artifactId}.ts`);
|
||||||
|
const clientHash = createHash("sha256")
|
||||||
|
.update(fwd(file))
|
||||||
|
.update("\0")
|
||||||
|
.update(source)
|
||||||
|
.digest("hex")
|
||||||
|
.slice(0, 16);
|
||||||
|
const clientOut = join(clientModulesDir, `${basename(file, ".wrn")}-${clientHash}.mjs`);
|
||||||
|
const clientUrl = `/__wrnexus/client/${basename(clientOut)}`;
|
||||||
|
const clientEntry = join(compiledDir, `client${artifactId}.entry.mjs`);
|
||||||
|
|
||||||
|
// Reserve both artifacts before resolving imports so dependency cycles
|
||||||
|
// terminate and every generated import has a deterministic target.
|
||||||
|
compiledFiles.set(file, out);
|
||||||
|
clientFiles.set(file, clientOut);
|
||||||
|
|
||||||
let ast = parse(source);
|
let ast = parse(source);
|
||||||
assertValidAst(ast, { file, accessibility: true });
|
assertValidAst(ast, { file, accessibility: true });
|
||||||
ast = await pluginRunner.transformAst(ast, file);
|
ast = await pluginRunner.transformAst(ast, file);
|
||||||
@@ -193,18 +210,35 @@ export async function runBuild(appRoot: string): Promise<void> {
|
|||||||
errors.map((diagnostic) => `[${diagnostic.code}] ${diagnostic.message}`).join("\n"),
|
errors.map((diagnostic) => `[${diagnostic.code}] ${diagnostic.message}`).join("\n"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let code = `// compiled from .wrn\n${generate(ast)}`;
|
const targets = generateTargets(ast);
|
||||||
|
let code = `// compiled from ${fwd(relative(root, file))}\n${generate(ast)}`.replaceAll(
|
||||||
|
"__WRNEXUS_CLIENT_MODULE__",
|
||||||
|
clientUrl,
|
||||||
|
);
|
||||||
|
let browserCode = `// browser module compiled from ${fwd(relative(root, file))}\n${targets.browser}`;
|
||||||
code = await pluginRunner.transformCode(code, file);
|
code = await pluginRunner.transformCode(code, file);
|
||||||
|
browserCode = await pluginRunner.transformCode(browserCode, file);
|
||||||
|
|
||||||
for (const [id, target] of virtualModules) {
|
for (const [id, target] of virtualModules) {
|
||||||
const sourcePattern = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
const sourcePattern = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
const relativeTarget = relative(dirname(out), target).replace(/\\/g, "/");
|
const mainRelative = relative(dirname(out), target).replace(/\\/g, "/");
|
||||||
const specifier = relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`;
|
const mainSpecifier = mainRelative.startsWith(".") ? mainRelative : `./${mainRelative}`;
|
||||||
code = code.replace(
|
code = code.replace(
|
||||||
new RegExp(`(["'])${sourcePattern}\\1`, "g"),
|
new RegExp(`(["'])${sourcePattern}\\1`, "g"),
|
||||||
JSON.stringify(specifier),
|
JSON.stringify(mainSpecifier),
|
||||||
|
);
|
||||||
|
const browserRelative = relative(dirname(clientEntry), target).replace(/\\/g, "/");
|
||||||
|
const browserSpecifier = browserRelative.startsWith(".")
|
||||||
|
? browserRelative
|
||||||
|
: `./${browserRelative}`;
|
||||||
|
browserCode = browserCode.replace(
|
||||||
|
new RegExp(`(["'])${sourcePattern}\\1`, "g"),
|
||||||
|
JSON.stringify(browserSpecifier),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedImports = resolveWrnImports(ast.structuredImports, file, {
|
const resolvedImports = resolveWrnImports(ast.structuredImports, file, {
|
||||||
appRoot: root,
|
appRoot: root,
|
||||||
mode: config.imports?.mode ?? "compatible",
|
mode: config.imports?.mode ?? "compatible",
|
||||||
@@ -214,13 +248,23 @@ export async function runBuild(appRoot: string): Promise<void> {
|
|||||||
if (imported.diagnostic?.severity === "error") {
|
if (imported.diagnostic?.severity === "error") {
|
||||||
throw new Error(`${imported.diagnostic.code}: ${imported.diagnostic.message}`);
|
throw new Error(`${imported.diagnostic.code}: ${imported.diagnostic.message}`);
|
||||||
}
|
}
|
||||||
if (!imported.resolved || !imported.declaration.source.startsWith(".")) continue;
|
if (!imported.resolved) continue;
|
||||||
|
const sourcePattern = imported.declaration.source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
|
||||||
|
const importAliases = { "@": "./app", ...(config.imports?.aliases ?? {}) };
|
||||||
|
const isApplicationImport =
|
||||||
|
imported.declaration.source.startsWith(".") ||
|
||||||
|
Object.keys(importAliases).some(
|
||||||
|
(alias) =>
|
||||||
|
imported.declaration.source === alias ||
|
||||||
|
imported.declaration.source.startsWith(`${alias}/`),
|
||||||
|
);
|
||||||
|
if (isApplicationImport) {
|
||||||
let target = imported.resolved;
|
let target = imported.resolved;
|
||||||
if (target.endsWith(".wrn")) {
|
if (target.endsWith(".wrn")) {
|
||||||
await compileWrn(target);
|
await compileWrn(target);
|
||||||
target = compiledFiles.get(target)!;
|
target = compiledFiles.get(target)!;
|
||||||
}
|
}
|
||||||
const sourcePattern = imported.declaration.source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
||||||
const relativeTarget = relative(dirname(out), target).replace(/\\/g, "/");
|
const relativeTarget = relative(dirname(out), target).replace(/\\/g, "/");
|
||||||
const specifier = relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`;
|
const specifier = relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`;
|
||||||
code = code.replace(
|
code = code.replace(
|
||||||
@@ -228,9 +272,50 @@ export async function runBuild(appRoot: string): Promise<void> {
|
|||||||
JSON.stringify(specifier),
|
JSON.stringify(specifier),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Browser modules keep only imports referenced by client/shared code.
|
||||||
|
// Resolve those imports relative to the original .wrn source before
|
||||||
|
// bundling so no filesystem or bare package specifier reaches browsers.
|
||||||
|
if (isApplicationImport && new RegExp(`(["'])${sourcePattern}\\1`).test(browserCode)) {
|
||||||
|
let browserTarget = imported.resolved;
|
||||||
|
if (browserTarget.endsWith(".wrn")) {
|
||||||
|
await compileWrn(browserTarget);
|
||||||
|
browserTarget = clientFiles.get(browserTarget)!;
|
||||||
|
}
|
||||||
|
const browserRelative = relative(dirname(clientEntry), browserTarget).replace(/\\/g, "/");
|
||||||
|
const browserSpecifier = browserRelative.startsWith(".")
|
||||||
|
? browserRelative
|
||||||
|
: `./${browserRelative}`;
|
||||||
|
browserCode = browserCode.replace(
|
||||||
|
new RegExp(`(["'])${sourcePattern}\\1`, "g"),
|
||||||
|
JSON.stringify(browserSpecifier),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
writeFileSync(out, code, "utf8");
|
writeFileSync(out, code, "utf8");
|
||||||
|
writeFileSync(clientEntry, browserCode, "utf8");
|
||||||
|
const browserResult = await Bun.build({
|
||||||
|
entrypoints: [clientEntry],
|
||||||
|
target: "browser",
|
||||||
|
format: "esm",
|
||||||
|
splitting: false,
|
||||||
|
minify: true,
|
||||||
|
sourcemap: config.build?.sourceMaps ? "inline" : "none",
|
||||||
|
});
|
||||||
|
if (!browserResult.success || !browserResult.outputs.length) {
|
||||||
|
throw new Error(
|
||||||
|
`WRN-CLIENT-BUNDLE: failed to bundle ${file}\n${browserResult.logs.map(String).join("\n")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
writeFileSync(clientOut, await browserResult.outputs[0]!.text(), "utf8");
|
||||||
|
rmSync(clientEntry, { force: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
compiledFiles.delete(file);
|
compiledFiles.delete(file);
|
||||||
|
clientFiles.delete(file);
|
||||||
|
rmSync(out, { force: true });
|
||||||
|
rmSync(clientOut, { force: true });
|
||||||
|
rmSync(clientEntry, { force: true });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -369,6 +454,8 @@ export async function runBuild(appRoot: string): Promise<void> {
|
|||||||
console.log(`✓ Partial shells: ${partialShells.size} build-time route shell(s)`);
|
console.log(`✓ Partial shells: ${partialShells.size} build-time route shell(s)`);
|
||||||
}
|
}
|
||||||
const assetHash = createHash("sha256");
|
const assetHash = createHash("sha256");
|
||||||
|
for (const file of clientFiles.values()) assetHash.update(readFileSync(file));
|
||||||
|
if (clientFiles.size) console.log(`✓ Client modules: ${clientFiles.size} bundled module(s)`);
|
||||||
const emittedPluginAssets = await emitPluginAssets(
|
const emittedPluginAssets = await emitPluginAssets(
|
||||||
pluginContributions,
|
pluginContributions,
|
||||||
distDir,
|
distDir,
|
||||||
@@ -533,6 +620,7 @@ await createProductionServer(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
reactivePath: join(import.meta.dir, "reactive.js"),
|
reactivePath: join(import.meta.dir, "reactive.js"),
|
||||||
|
clientModulesDir: join(import.meta.dir, "client"),
|
||||||
themePath: join(import.meta.dir, "theme.css"),
|
themePath: join(import.meta.dir, "theme.css"),
|
||||||
themeJsPath: join(import.meta.dir, "theme.js"),
|
themeJsPath: join(import.meta.dir, "theme.js"),
|
||||||
theme: ${JSON.stringify(theme)},
|
theme: ${JSON.stringify(theme)},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { basename, extname, join, relative, resolve } from "node:path";
|
|||||||
import { buildRouter, createRouteManifest, nameRoutes } from "@wrnexus/router";
|
import { buildRouter, createRouteManifest, nameRoutes } from "@wrnexus/router";
|
||||||
import { checkWrnFile, type WrnTypeDiagnostic } from "@wrnexus/typecheck";
|
import { checkWrnFile, type WrnTypeDiagnostic } from "@wrnexus/typecheck";
|
||||||
import { parse } from "@wrnexus/syntax";
|
import { parse } from "@wrnexus/syntax";
|
||||||
|
import { generate, generateTargets } from "@wrnexus/compiler";
|
||||||
import { regenerateRoutes } from "./routes.ts";
|
import { regenerateRoutes } from "./routes.ts";
|
||||||
import { loadAppConfig } from "@wrnexus/styles";
|
import { loadAppConfig } from "@wrnexus/styles";
|
||||||
import { createPluginRunner, discoverPlugins, type PluginContributions } from "@wrnexus/plugin";
|
import { createPluginRunner, discoverPlugins, type PluginContributions } from "@wrnexus/plugin";
|
||||||
@@ -258,11 +259,51 @@ export async function generateApplicationTypesWithPlugins(
|
|||||||
return generateApplicationTypes(root, await runner.contributions());
|
return generateApplicationTypes(root, await runner.contributions());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function codegenDiagnostics(file: string): WrnTypeDiagnostic[] {
|
||||||
|
try {
|
||||||
|
const ast = parse(readFileSync(file, "utf8"));
|
||||||
|
const main = generate(ast);
|
||||||
|
const targets = generateTargets(ast);
|
||||||
|
const bun = (
|
||||||
|
globalThis as unknown as {
|
||||||
|
Bun?: {
|
||||||
|
Transpiler?: new (options: { loader: string }) => {
|
||||||
|
transformSync(code: string): unknown;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
).Bun;
|
||||||
|
if (bun?.Transpiler) {
|
||||||
|
const transpiler = new bun.Transpiler({ loader: "ts" });
|
||||||
|
transpiler.transformSync(main);
|
||||||
|
transpiler.transformSync(targets.browser);
|
||||||
|
transpiler.transformSync(targets.server);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} catch (error) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
code: "WRN-CODEGEN",
|
||||||
|
category: "error",
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
file,
|
||||||
|
line: 1,
|
||||||
|
column: 1,
|
||||||
|
length: 1,
|
||||||
|
hint: "Fix the reported SSR/client code generation issue before starting or building the app.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function checkApplication(appRoot: string): WrnTypeDiagnostic[] {
|
export function checkApplication(appRoot: string): WrnTypeDiagnostic[] {
|
||||||
const root = resolve(appRoot);
|
const root = resolve(appRoot);
|
||||||
return files(join(root, "app"), (path) => extname(path) === ".wrn").flatMap((file) =>
|
return files(join(root, "app"), (path) => extname(path) === ".wrn").flatMap((file) => {
|
||||||
checkWrnFile(file, { appRoot: root }),
|
const diagnostics = checkWrnFile(file, { appRoot: root });
|
||||||
);
|
return diagnostics.some((item) => item.category === "error")
|
||||||
|
? diagnostics
|
||||||
|
: [...diagnostics, ...codegenDiagnostics(file)];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runTypecheck(appRoot: string): Promise<boolean> {
|
export async function runTypecheck(appRoot: string): Promise<boolean> {
|
||||||
|
|||||||
@@ -1989,6 +1989,16 @@ const MIGRATIONS: Migration[] = [
|
|||||||
// Runtime stability fixes require no application source rewrite.
|
// Runtime stability fixes require no application source rewrite.
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
version: "0.8.3",
|
||||||
|
id: "0.8.3-client-ssr-realtime-stability",
|
||||||
|
description:
|
||||||
|
"Fixes browser function bundling, SSR computed values, Async aliases, typed loop props, realtime identity isolation, loader invalidation, and strict package imports.",
|
||||||
|
apply() {
|
||||||
|
// Framework runtime and compiler fixes require no application source rewrite.
|
||||||
|
// The compile-cache generation changes automatically invalidate old artifacts.
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Release tooling uses this to require an explicit migration entry per version. */
|
/** Release tooling uses this to require an explicit migration entry per version. */
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/compiler",
|
"name": "@wrnexus/compiler",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { eraseFunctionTypes, type PageAst, type RuntimeFunctionDecl } from "@wrnexus/syntax";
|
import {
|
||||||
|
eraseFunctionTypes,
|
||||||
|
type PageAst,
|
||||||
|
type RuntimeFunctionDecl,
|
||||||
|
type StructuredImportDecl,
|
||||||
|
type ViewNode,
|
||||||
|
} from "@wrnexus/syntax";
|
||||||
|
|
||||||
const RESERVED_BINDINGS = new Set([
|
const RESERVED_BINDINGS = new Set([
|
||||||
"await",
|
"await",
|
||||||
@@ -63,6 +69,111 @@ function safeIdentifier(name: string): boolean {
|
|||||||
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && !RESERVED_BINDINGS.has(name);
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && !RESERVED_BINDINGS.has(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function identifierReferenced(source: string, name: string): boolean {
|
||||||
|
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
return new RegExp(`(?:^|[^A-Za-z0-9_$])${escaped}(?![A-Za-z0-9_$])`).test(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewReferenceSource(nodes: ViewNode[]): string[] {
|
||||||
|
return nodes.flatMap((node): string[] => {
|
||||||
|
if (node.type === "text") return [node.value];
|
||||||
|
if (node.type === "each") {
|
||||||
|
return [
|
||||||
|
node.list,
|
||||||
|
node.key ?? "",
|
||||||
|
...viewReferenceSource(node.body),
|
||||||
|
...viewReferenceSource(node.empty),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (node.type === "if") {
|
||||||
|
return node.branches.flatMap((branch) => [
|
||||||
|
branch.cond ?? "",
|
||||||
|
...viewReferenceSource(branch.body),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return [...node.attrs.map((attr) => attr.value), ...viewReferenceSource(node.children)];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function browserReferenceSource(ast: PageAst, functions: RuntimeFunctionDecl[]): string {
|
||||||
|
return [
|
||||||
|
...functions.flatMap((fn) => [fn.source, fn.body]),
|
||||||
|
...ast.computed.map((entry) => entry.expr),
|
||||||
|
...ast.effects,
|
||||||
|
ast.lifecycle.mount ?? "",
|
||||||
|
ast.lifecycle.update ?? "",
|
||||||
|
ast.lifecycle.unmount ?? "",
|
||||||
|
...viewReferenceSource(ast.view),
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSelectedImport(
|
||||||
|
entry: StructuredImportDecl,
|
||||||
|
source: string,
|
||||||
|
): { code: string; bindings: string[] } | null {
|
||||||
|
if (entry.typeOnly) return null;
|
||||||
|
const isStore = entry.source.endsWith(".wrn") && /(?:^|\/)stores?\//.test(entry.source);
|
||||||
|
if (entry.source.endsWith(".wrn") && !isStore) return null;
|
||||||
|
|
||||||
|
const defaultImport =
|
||||||
|
entry.defaultImport && (isStore || identifierReferenced(source, entry.defaultImport))
|
||||||
|
? entry.defaultImport
|
||||||
|
: undefined;
|
||||||
|
const namespaceImport =
|
||||||
|
entry.namespaceImport && (isStore || identifierReferenced(source, entry.namespaceImport))
|
||||||
|
? entry.namespaceImport
|
||||||
|
: undefined;
|
||||||
|
const namedImports = entry.namedImports.filter(
|
||||||
|
(item) => !item.typeOnly && (isStore || identifierReferenced(source, item.local)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const bindings = [
|
||||||
|
...(defaultImport ? [defaultImport] : []),
|
||||||
|
...(namespaceImport ? [namespaceImport] : []),
|
||||||
|
...namedImports.map((item) => item.local),
|
||||||
|
].filter(safeIdentifier);
|
||||||
|
|
||||||
|
const hasBindings = Boolean(defaultImport || namespaceImport || namedImports.length);
|
||||||
|
const sideEffectOnly =
|
||||||
|
!entry.defaultImport && !entry.namespaceImport && entry.namedImports.length === 0;
|
||||||
|
if (!hasBindings && !sideEffectOnly) return null;
|
||||||
|
if (sideEffectOnly) return { code: entry.raw, bindings: [] };
|
||||||
|
|
||||||
|
const clauses: string[] = [];
|
||||||
|
if (defaultImport) clauses.push(defaultImport);
|
||||||
|
if (namespaceImport) clauses.push(`* as ${namespaceImport}`);
|
||||||
|
if (namedImports.length) {
|
||||||
|
clauses.push(
|
||||||
|
`{ ${namedImports
|
||||||
|
.map((item) =>
|
||||||
|
item.imported === item.local ? item.imported : `${item.imported} as ${item.local}`,
|
||||||
|
)
|
||||||
|
.join(", ")} }`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
code: `import ${clauses.join(", ")} from ${JSON.stringify(entry.source)};`,
|
||||||
|
bindings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedBrowserImports(
|
||||||
|
ast: PageAst,
|
||||||
|
functions: RuntimeFunctionDecl[],
|
||||||
|
): { code: string; bindings: string[] }[] {
|
||||||
|
const referenceSource = browserReferenceSource(ast, functions);
|
||||||
|
return ast.structuredImports
|
||||||
|
.map((entry) => renderSelectedImport(entry, referenceSource))
|
||||||
|
.filter((entry): entry is { code: string; bindings: string[] } => entry !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function browserModuleRequired(ast: PageAst): boolean {
|
||||||
|
const functions = ast.runtimeFunctions.filter((fn) =>
|
||||||
|
["legacy", "client", "shared"].includes(fn.runtime),
|
||||||
|
);
|
||||||
|
return functions.length > 0 || selectedBrowserImports(ast, functions).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
function functionEntry(
|
function functionEntry(
|
||||||
ast: PageAst,
|
ast: PageAst,
|
||||||
fn: RuntimeFunctionDecl,
|
fn: RuntimeFunctionDecl,
|
||||||
@@ -164,22 +275,9 @@ export function generateBrowserModule(ast: PageAst): string {
|
|||||||
);
|
);
|
||||||
const functionNames = functions.map((fn) => fn.name);
|
const functionNames = functions.map((fn) => fn.name);
|
||||||
const state = ast.states.filter((entry) => entry.runtime !== "server").map((entry) => entry.name);
|
const state = ast.states.filter((entry) => entry.runtime !== "server").map((entry) => entry.name);
|
||||||
const storeImports = ast.structuredImports.filter(
|
const selectedImports = selectedBrowserImports(ast, functions);
|
||||||
(entry) =>
|
const imports = selectedImports.map((entry) => entry.code).join("\n");
|
||||||
!entry.typeOnly && entry.source.endsWith(".wrn") && /(?:^|\/)stores?\//.test(entry.source),
|
const importedBindings = [...new Set(selectedImports.flatMap((entry) => entry.bindings))];
|
||||||
);
|
|
||||||
const imports = ast.structuredImports
|
|
||||||
.filter((entry) => !entry.typeOnly)
|
|
||||||
.filter((entry) => !entry.source.endsWith(".wrn") || /(?:^|\/)stores?\//.test(entry.source))
|
|
||||||
.map((entry) => entry.raw)
|
|
||||||
.join("\n");
|
|
||||||
const importedBindings = storeImports
|
|
||||||
.flatMap((entry) => [
|
|
||||||
...(entry.defaultImport ? [entry.defaultImport] : []),
|
|
||||||
...(entry.namespaceImport ? [entry.namespaceImport] : []),
|
|
||||||
...entry.namedImports.map((item) => item.local),
|
|
||||||
])
|
|
||||||
.filter(safeIdentifier);
|
|
||||||
return `// generated WRNexusJS browser module for ${ast.name}
|
return `// generated WRNexusJS browser module for ${ast.name}
|
||||||
${imports}
|
${imports}
|
||||||
export const __wrnexusClientFunctions = {
|
export const __wrnexusClientFunctions = {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { eraseFunctionTypes, runtimeTypeOf } from "./types.ts";
|
|||||||
import { stripRuntimeFunctionModifiers } from "@wrnexus/syntax";
|
import { stripRuntimeFunctionModifiers } from "@wrnexus/syntax";
|
||||||
import { generateStoreModule } from "./store-codegen.ts";
|
import { generateStoreModule } from "./store-codegen.ts";
|
||||||
import { optimizeAst } from "./analysis.ts";
|
import { optimizeAst } from "./analysis.ts";
|
||||||
|
import { browserModuleRequired } from "./client-codegen.ts";
|
||||||
|
|
||||||
interface RenderBinding {
|
interface RenderBinding {
|
||||||
method: string;
|
method: string;
|
||||||
@@ -264,12 +265,21 @@ function evalStateSeeds(states: { name: string; expr: string }[]): Record<string
|
|||||||
* runtime keeps it live. Non-state `{expr}` and un-evaluable expressions are
|
* runtime keeps it live. Non-state `{expr}` and un-evaluable expressions are
|
||||||
* left as literal client mustaches.
|
* left as literal client mustaches.
|
||||||
*/
|
*/
|
||||||
function substituteReactiveText(raw: string, reactive: PageReactive | null): string {
|
function substituteReactiveText(
|
||||||
|
raw: string,
|
||||||
|
reactive: PageReactive | null,
|
||||||
|
dynamicExpressions?: string[],
|
||||||
|
): string {
|
||||||
const text = substituteTMarkers(raw);
|
const text = substituteTMarkers(raw);
|
||||||
if (!reactive || reactive.stateNames.size === 0) return text;
|
if (!reactive || reactive.stateNames.size === 0) return text;
|
||||||
return text.replace(/\{([^{}]+)\}/g, (whole, inner: string) => {
|
return text.replace(/\{([^{}]+)\}/g, (whole, inner: string) => {
|
||||||
const expr = inner.trim();
|
const expr = inner.trim();
|
||||||
if (expr.startsWith("t:") || !exprRefsState(expr, reactive.stateNames)) return whole;
|
if (expr.startsWith("t:") || !exprRefsState(expr, reactive.stateNames)) return whole;
|
||||||
|
if (exprRefsState(expr, reactive.runtimeStateNames) && dynamicExpressions) {
|
||||||
|
dynamicExpressions.push(`\${__wrnexusEscapeHtml(${expr})}`);
|
||||||
|
const sentinel = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
|
||||||
|
return `<span data-text="${attrEscape(expr)}">${sentinel}</span>`;
|
||||||
|
}
|
||||||
let value: unknown;
|
let value: unknown;
|
||||||
try {
|
try {
|
||||||
value = new Function("with(this){return (" + expr + ");}").call(reactive.scope);
|
value = new Function("with(this){return (" + expr + ");}").call(reactive.scope);
|
||||||
@@ -308,7 +318,9 @@ function bakeLoopText(raw: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Bake a loop-body attribute value (same rules as text; escapeHtml is attribute-safe). */
|
/** Bake a loop-body attribute value (same rules as text; escapeHtml is attribute-safe). */
|
||||||
function bakeLoopAttr(raw: string): string {
|
function bakeLoopAttr(raw: string, typed = false): string {
|
||||||
|
const wholeExpression = wholeAttributeExpression(raw);
|
||||||
|
if (typed && wholeExpression) return "${__wrnexusPropAttr(" + wholeExpression + ")}";
|
||||||
if (!raw.includes("{")) return escLit(attrEscape(raw));
|
if (!raw.includes("{")) return escLit(attrEscape(raw));
|
||||||
let out = "";
|
let out = "";
|
||||||
let last = 0;
|
let last = 0;
|
||||||
@@ -347,7 +359,7 @@ function renderLoopBody(node: ViewNode): string {
|
|||||||
return escLit(` ${name}`);
|
return escLit(` ${name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return escLit(` ${name}="`) + bakeLoopAttr(attr.value) + escLit(`"`);
|
return escLit(` ${name}="`) + bakeLoopAttr(attr.value, componentTag) + escLit(`"`);
|
||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
@@ -492,7 +504,7 @@ function renderNode(
|
|||||||
loops: string[],
|
loops: string[],
|
||||||
reactive: PageReactive | null = null,
|
reactive: PageReactive | null = null,
|
||||||
): string {
|
): string {
|
||||||
if (node.type === "text") return substituteReactiveText(node.value, reactive); // {t:key} + state baking
|
if (node.type === "text") return substituteReactiveText(node.value, reactive, loops); // {t:key} + state baking
|
||||||
|
|
||||||
// Server control block (loop / conditional) → a sentinel that survives
|
// Server control block (loop / conditional) → a sentinel that survives
|
||||||
// templateEscape, swapped for its real `${…}` code after escaping.
|
// templateEscape, swapped for its real `${…}` code after escaping.
|
||||||
@@ -532,13 +544,15 @@ function renderNode(
|
|||||||
if (node.tag === "Async") {
|
if (node.tag === "Async") {
|
||||||
const source = attrValue(node.attrs, "source") ?? "data";
|
const source = attrValue(node.attrs, "source") ?? "data";
|
||||||
const retries = attrValue(node.attrs, "retries") ?? "2";
|
const retries = attrValue(node.attrs, "retries") ?? "2";
|
||||||
|
const tags = attrValue(node.attrs, "tags") ?? source;
|
||||||
const serverResolved = attrValue(node.attrs, "data-wrn-async-server") === "true";
|
const serverResolved = attrValue(node.attrs, "data-wrn-async-server") === "true";
|
||||||
const asyncIndex = serverResolved ? loops.push("") - 1 : -1;
|
const branchElement = (name: string) =>
|
||||||
const branch = (name: string) => {
|
node.children.find(
|
||||||
const element = node.children.find(
|
|
||||||
(child): child is Extract<ViewNode, { type: "element" }> =>
|
(child): child is Extract<ViewNode, { type: "element" }> =>
|
||||||
child.type === "element" && child.tag === name,
|
child.type === "element" && child.tag === name,
|
||||||
);
|
);
|
||||||
|
const branch = (name: string) => {
|
||||||
|
const element = branchElement(name);
|
||||||
return (element?.children ?? [])
|
return (element?.children ?? [])
|
||||||
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
|
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
|
||||||
.join("");
|
.join("");
|
||||||
@@ -546,19 +560,42 @@ function renderNode(
|
|||||||
const loading = branch("Loading");
|
const loading = branch("Loading");
|
||||||
const success = branch("Success");
|
const success = branch("Success");
|
||||||
const error = branch("Error");
|
const error = branch("Error");
|
||||||
|
const successElement = branchElement("Success");
|
||||||
|
const errorElement = branchElement("Error");
|
||||||
|
const identifier = (value: string | undefined, fallback: string) =>
|
||||||
|
value && isSafeGeneratedIdentifier(value) ? value : fallback;
|
||||||
|
const successAlias = identifier(
|
||||||
|
successElement ? attrValue(successElement.attrs, "data") : undefined,
|
||||||
|
identifier(source, "data"),
|
||||||
|
);
|
||||||
|
const errorAlias = identifier(
|
||||||
|
errorElement ? attrValue(errorElement.attrs, "error") : undefined,
|
||||||
|
"error",
|
||||||
|
);
|
||||||
|
const nested = (value: string) => value.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
||||||
|
const scoped = (value: string, alias: string, expression: string) => {
|
||||||
|
const index =
|
||||||
|
loops.push(
|
||||||
|
`\${(() => { const ${alias} = ${expression}; return \`${nested(value)}\`; })()}`,
|
||||||
|
) - 1;
|
||||||
|
return `\x00WRNEACH${index}\x00`;
|
||||||
|
};
|
||||||
|
const successTemplate = scoped(success, successAlias, `(ctx[${JSON.stringify(source)}] ?? {})`);
|
||||||
|
const errorTemplate = scoped(error, errorAlias, `{ message: "" }`);
|
||||||
let initial = loading;
|
let initial = loading;
|
||||||
if (serverResolved) {
|
if (serverResolved) {
|
||||||
const nested = (value: string) => value.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
const sourcePattern = successAlias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
const sourcePattern = source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
||||||
const serverSuccess = success.replace(
|
const serverSuccess = success.replace(
|
||||||
new RegExp(`\\{\\s*(${sourcePattern}(?:\\.[A-Za-z_$][\\w$]*)*)\\s*\\}`, "g"),
|
new RegExp(`\\{\\s*(${sourcePattern}(?:\\.[A-Za-z_$][\\w$]*)*)\\s*\\}`, "g"),
|
||||||
(_whole, expression: string) => `\${__wrnexusEscapeHtml(${expression})}`,
|
(_whole, expression: string) => `\${__wrnexusEscapeHtml(${expression})}`,
|
||||||
);
|
);
|
||||||
loops[asyncIndex] =
|
const index =
|
||||||
`\${ctx[${JSON.stringify(source)}] !== undefined ? \`${nested(serverSuccess)}\` : \`${nested(loading)}\`}`;
|
loops.push(
|
||||||
initial = `\x00WRNEACH${asyncIndex}\x00`;
|
`\${ctx[${JSON.stringify(source)}] !== undefined ? (() => { const ${successAlias} = ctx[${JSON.stringify(source)}]; return \`${nested(serverSuccess)}\`; })() : \`${nested(loading)}\`}`,
|
||||||
|
) - 1;
|
||||||
|
initial = `\x00WRNEACH${index}\x00`;
|
||||||
}
|
}
|
||||||
return `<section data-wrn-async="${attrEscape(source)}" data-wrn-async-retries="${attrEscape(retries)}"${serverResolved ? ' data-wrn-async-resolved="true"' : ""} aria-busy="${serverResolved ? "false" : "true"}"><div data-wrn-async-content>${initial}</div><template data-wrn-async-loading>${loading}</template><template data-wrn-async-success>${success}</template><template data-wrn-async-error>${error}</template></section>`;
|
return `<section data-wrn-async="${attrEscape(source)}" data-wrn-async-tags="${attrEscape(tags)}" data-wrn-async-retries="${attrEscape(retries)}"${serverResolved ? ' data-wrn-async-resolved="true"' : ""} aria-busy="${serverResolved ? "false" : "true"}"><div data-wrn-async-content>${initial}</div><template data-wrn-async-loading>${loading}</template><template data-wrn-async-success data-wrn-async-alias="${attrEscape(successAlias)}">${successTemplate}</template><template data-wrn-async-error data-wrn-async-alias="${attrEscape(errorAlias)}">${errorTemplate}</template></section>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.tag === "KeepAlive") {
|
if (node.tag === "KeepAlive") {
|
||||||
@@ -965,13 +1002,133 @@ function generateSsrStateAliases(stateNames: string[]): string {
|
|||||||
return `const { ${names.join(", ")} } = __state;\n`;
|
return `const { ${names.join(", ")} } = __state;\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function orderPageStates(entries: PageAst["states"]): PageAst["states"] {
|
||||||
|
if (entries.length < 2) return entries;
|
||||||
|
const byName = new Map(entries.map((entry) => [entry.name, entry]));
|
||||||
|
const visiting = new Set<string>();
|
||||||
|
const visited = new Set<string>();
|
||||||
|
const ordered: PageAst["states"] = [];
|
||||||
|
|
||||||
|
const visit = (name: string): void => {
|
||||||
|
if (visited.has(name)) return;
|
||||||
|
if (visiting.has(name)) {
|
||||||
|
throw new Error(`WRN-STATE-CYCLE: state value '${name}' has a dependency cycle.`);
|
||||||
|
}
|
||||||
|
const entry = byName.get(name);
|
||||||
|
if (!entry) return;
|
||||||
|
visiting.add(name);
|
||||||
|
for (const dependency of byName.keys()) {
|
||||||
|
if (
|
||||||
|
dependency !== name &&
|
||||||
|
new RegExp(`\\b${dependency.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(entry.expr)
|
||||||
|
) {
|
||||||
|
visit(dependency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visiting.delete(name);
|
||||||
|
visited.add(name);
|
||||||
|
ordered.push(entry);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const entry of entries) visit(entry.name);
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSsrStateInitializer(entries: PageAst["states"]): string {
|
||||||
|
const ordered = orderPageStates(entries);
|
||||||
|
const declarations = ordered
|
||||||
|
.filter((entry) => isSafeGeneratedIdentifier(entry.name))
|
||||||
|
.map(
|
||||||
|
(entry) =>
|
||||||
|
`const ${entry.name} = (() => { try { return (${entry.expr}); } catch { return undefined; } })();`,
|
||||||
|
)
|
||||||
|
.join(" ");
|
||||||
|
const values = entries
|
||||||
|
.map((entry) => {
|
||||||
|
if (isSafeGeneratedIdentifier(entry.name)) {
|
||||||
|
return `${JSON.stringify(entry.name)}: ${entry.name}`;
|
||||||
|
}
|
||||||
|
return `${JSON.stringify(entry.name)}: (() => { try { return (${entry.expr}); } catch { return undefined; } })()`;
|
||||||
|
})
|
||||||
|
.join(", ");
|
||||||
|
return `(() => { ${declarations} return { ${values} }; })()`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function orderPageComputed(entries: PageAst["computed"]): PageAst["computed"] {
|
||||||
|
if (entries.length < 2) return entries;
|
||||||
|
const byName = new Map(entries.map((entry) => [entry.name, entry]));
|
||||||
|
const visiting = new Set<string>();
|
||||||
|
const visited = new Set<string>();
|
||||||
|
const ordered: PageAst["computed"] = [];
|
||||||
|
|
||||||
|
const visit = (name: string): void => {
|
||||||
|
if (visited.has(name)) return;
|
||||||
|
if (visiting.has(name)) {
|
||||||
|
throw new Error(`WRN-COMPUTED-CYCLE: computed value '${name}' has a dependency cycle.`);
|
||||||
|
}
|
||||||
|
const entry = byName.get(name);
|
||||||
|
if (!entry) return;
|
||||||
|
visiting.add(name);
|
||||||
|
for (const dependency of byName.keys()) {
|
||||||
|
if (
|
||||||
|
dependency !== name &&
|
||||||
|
new RegExp(`\\b${dependency.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(entry.expr)
|
||||||
|
) {
|
||||||
|
visit(dependency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visiting.delete(name);
|
||||||
|
visited.add(name);
|
||||||
|
ordered.push(entry);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const entry of entries) visit(entry.name);
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSsrComputedAliases(entries: PageAst["computed"]): string {
|
||||||
|
return orderPageComputed(entries)
|
||||||
|
.filter((entry) => isSafeGeneratedIdentifier(entry.name))
|
||||||
|
.map(
|
||||||
|
(entry) =>
|
||||||
|
`const ${entry.name} = (() => { try { return (${entry.expr}); } catch { return undefined; } })();`,
|
||||||
|
)
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function runtimeComputedNames(
|
||||||
|
states: PageAst["states"],
|
||||||
|
computed: PageAst["computed"],
|
||||||
|
runtimeRoots: Iterable<string> = [],
|
||||||
|
): Set<string> {
|
||||||
|
const entries = [...states, ...computed];
|
||||||
|
const runtime = new Set([
|
||||||
|
...runtimeRoots,
|
||||||
|
...entries.filter((entry) => /\bctx\b/.test(entry.expr)).map((entry) => entry.name),
|
||||||
|
]);
|
||||||
|
let changed = true;
|
||||||
|
while (changed) {
|
||||||
|
changed = false;
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (runtime.has(entry.name)) continue;
|
||||||
|
if (
|
||||||
|
[...runtime].some((name) =>
|
||||||
|
new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(entry.expr),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
runtime.add(entry.name);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return runtime;
|
||||||
|
}
|
||||||
|
|
||||||
function hydrationAttribute(ast: PageAst): string {
|
function hydrationAttribute(ast: PageAst): string {
|
||||||
const strategy = ["static", "server"].includes(ast.renderMode ?? "")
|
const strategy = ["static", "server"].includes(ast.renderMode ?? "")
|
||||||
? "none"
|
? "none"
|
||||||
: (ast.hydrate ?? "load");
|
: (ast.hydrate ?? "load");
|
||||||
const hasBrowserModule = ast.runtimeFunctions.some((fn) =>
|
const hasBrowserModule = browserModuleRequired(ast);
|
||||||
["legacy", "client", "shared"].includes(fn.runtime),
|
|
||||||
);
|
|
||||||
const moduleAttribute = hasBrowserModule
|
const moduleAttribute = hasBrowserModule
|
||||||
? ' data-wrn-client-module="__WRNEXUS_CLIENT_MODULE__"'
|
? ' data-wrn-client-module="__WRNEXUS_CLIENT_MODULE__"'
|
||||||
: "";
|
: "";
|
||||||
@@ -1119,9 +1276,11 @@ export function generate(ast: PageAst): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- View -> default page component ---
|
// --- View -> default page component ---
|
||||||
|
const orderedStates = orderPageStates(ast.states);
|
||||||
const browserStates = ast.states.filter((state) => state.runtime !== "server");
|
const browserStates = ast.states.filter((state) => state.runtime !== "server");
|
||||||
const seedScope = evalStateSeeds(ast.states);
|
const seedScope = evalStateSeeds(orderedStates);
|
||||||
for (const entry of ast.computed) {
|
const orderedComputed = orderPageComputed(ast.computed);
|
||||||
|
for (const entry of orderedComputed) {
|
||||||
try {
|
try {
|
||||||
seedScope[entry.name] = new Function("with(this){return (" + entry.expr + ");}").call(
|
seedScope[entry.name] = new Function("with(this){return (" + entry.expr + ");}").call(
|
||||||
seedScope,
|
seedScope,
|
||||||
@@ -1134,9 +1293,14 @@ export function generate(ast: PageAst): string {
|
|||||||
...browserStates.map((entry) => entry.name),
|
...browserStates.map((entry) => entry.name),
|
||||||
...ast.computed.map((entry) => entry.name),
|
...ast.computed.map((entry) => entry.name),
|
||||||
];
|
];
|
||||||
const runtimeStateNames = new Set(
|
const storeBindings = importedStoreBindings(ast);
|
||||||
ast.states.filter((entry) => /\bctx\b/.test(entry.expr)).map((entry) => entry.name),
|
const runtimeRoots = [
|
||||||
);
|
...storeBindings.map((entry) => entry.local),
|
||||||
|
...ast.loads
|
||||||
|
.filter((load) => load.mode === "server" && !load.deferred && load.name)
|
||||||
|
.map((load) => load.name!),
|
||||||
|
];
|
||||||
|
const runtimeStateNames = runtimeComputedNames(orderedStates, orderedComputed, runtimeRoots);
|
||||||
const reactive: PageReactive | null =
|
const reactive: PageReactive | null =
|
||||||
reactiveNames.length > 0
|
reactiveNames.length > 0
|
||||||
? { stateNames: new Set(reactiveNames), runtimeStateNames, scope: seedScope }
|
? { stateNames: new Set(reactiveNames), runtimeStateNames, scope: seedScope }
|
||||||
@@ -1187,12 +1351,7 @@ export function generate(ast: PageAst): string {
|
|||||||
);
|
);
|
||||||
staticShellBody = templateEscape(shellHtml);
|
staticShellBody = templateEscape(shellHtml);
|
||||||
}
|
}
|
||||||
const dynamicStateScope = ast.states
|
const dynamicStateInitializer = generateSsrStateInitializer(orderedStates);
|
||||||
.map(
|
|
||||||
(state) =>
|
|
||||||
`${JSON.stringify(state.name)}: (() => { try { return (${state.expr}); } catch { return undefined; } })()`,
|
|
||||||
)
|
|
||||||
.join(", ");
|
|
||||||
const stateType =
|
const stateType =
|
||||||
ast.states.length > 0
|
ast.states.length > 0
|
||||||
? `{ ${ast.states
|
? `{ ${ast.states
|
||||||
@@ -1202,8 +1361,8 @@ export function generate(ast: PageAst): string {
|
|||||||
const hydrationStateNames = JSON.stringify(browserStates.map((state) => state.name));
|
const hydrationStateNames = JSON.stringify(browserStates.map((state) => state.name));
|
||||||
|
|
||||||
const ssrStateAliases = generateSsrStateAliases(ast.states.map((state) => state.name));
|
const ssrStateAliases = generateSsrStateAliases(ast.states.map((state) => state.name));
|
||||||
|
const ssrComputedAliases = generateSsrComputedAliases(orderedComputed);
|
||||||
|
|
||||||
const storeBindings = importedStoreBindings(ast);
|
|
||||||
const storeDeclarations = storeBindings
|
const storeDeclarations = storeBindings
|
||||||
.map(
|
.map(
|
||||||
(entry) => ` const ${entry.local} = await ctx.__wrnexusUseStore(${entry.internal});`,
|
(entry) => ` const ${entry.local} = await ctx.__wrnexusUseStore(${entry.internal});`,
|
||||||
@@ -1214,12 +1373,16 @@ export function generate(ast: PageAst): string {
|
|||||||
.map((load) => ` const ${load.name} = ctx[${JSON.stringify(load.name)}];`)
|
.map((load) => ` const ${load.name} = ctx[${JSON.stringify(load.name)}];`)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
loops.forEach((code, idx) => {
|
// Inner dynamic expressions are registered before the wrapper that scopes
|
||||||
body = body.replace(`\x00WRNEACH${idx}\x00`, () => code);
|
// them (for example an Async branch alias). Resolve from the outside in so a
|
||||||
|
// wrapper sentinel is expanded before its nested sentinels are visited.
|
||||||
|
for (let idx = loops.length - 1; idx >= 0; idx--) {
|
||||||
|
const code = loops[idx]!;
|
||||||
|
body = body.replaceAll(`\x00WRNEACH${idx}\x00`, code);
|
||||||
if (staticShellBody?.includes(`\x00WRNEACH${idx}\x00`)) {
|
if (staticShellBody?.includes(`\x00WRNEACH${idx}\x00`)) {
|
||||||
staticShellBody = staticShellBody.replace(`\x00WRNEACH${idx}\x00`, () => code);
|
staticShellBody = staticShellBody.replaceAll(`\x00WRNEACH${idx}\x00`, code);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Server loops iterate raw SSR data. Declare a named const for every `ssr` data
|
// Server loops iterate raw SSR data. Declare a named const for every `ssr` data
|
||||||
// binding a loop references, so `{#each <name> as …}` can iterate the real value.
|
// binding a loop references, so `{#each <name> as …}` can iterate the real value.
|
||||||
@@ -1235,10 +1398,7 @@ export function generate(ast: PageAst): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const needsSsrRuntime =
|
const needsSsrRuntime = ssrBindings.length > 0 || loops.length > 0 || runtimeStateNames.size > 0;
|
||||||
ssrBindings.length > 0 ||
|
|
||||||
loops.length > 0 ||
|
|
||||||
ast.states.some((state) => /\bctx\b/.test(state.expr));
|
|
||||||
if (needsSsrRuntime) {
|
if (needsSsrRuntime) {
|
||||||
out.push(ssrRuntimeSource());
|
out.push(ssrRuntimeSource());
|
||||||
out.push(`const __wrnexusSsrBindings = ${JSON.stringify(ssrBindings, null, 2)};`);
|
out.push(`const __wrnexusSsrBindings = ${JSON.stringify(ssrBindings, null, 2)};`);
|
||||||
@@ -1248,16 +1408,18 @@ export function generate(ast: PageAst): string {
|
|||||||
${storeDeclarations}
|
${storeDeclarations}
|
||||||
${serverLoadAliases}
|
${serverLoadAliases}
|
||||||
${decls}
|
${decls}
|
||||||
const __state: ${stateType} = { ${dynamicStateScope} };
|
const __state: ${stateType} = ${dynamicStateInitializer};
|
||||||
${ssrStateAliases}
|
${ssrStateAliases}
|
||||||
|
${ssrComputedAliases}
|
||||||
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
|
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
|
||||||
const __scopeValue = Object.entries(__hydrationState)
|
const __scopeValue = Object.entries(__hydrationState)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
const encoded =
|
let encoded: string;
|
||||||
typeof value === "number" || typeof value === "boolean"
|
try {
|
||||||
? String(value)
|
encoded = value === undefined ? "undefined" : JSON.stringify(value);
|
||||||
: JSON.stringify(value == null ? "" : String(value));
|
} catch {
|
||||||
|
encoded = JSON.stringify(String(value));
|
||||||
|
}
|
||||||
return key + ": " + encoded;
|
return key + ": " + encoded;
|
||||||
})
|
})
|
||||||
.join(", ")
|
.join(", ")
|
||||||
@@ -1279,16 +1441,18 @@ export function generate(ast: PageAst): string {
|
|||||||
`export default ${storeBindings.length > 0 ? "async " : ""}function ${ast.name}(ctx: any) {
|
`export default ${storeBindings.length > 0 ? "async " : ""}function ${ast.name}(ctx: any) {
|
||||||
${storeDeclarations}
|
${storeDeclarations}
|
||||||
${serverLoadAliases}
|
${serverLoadAliases}
|
||||||
const __state: ${stateType} = { ${dynamicStateScope} };
|
const __state: ${stateType} = ${dynamicStateInitializer};
|
||||||
${ssrStateAliases}
|
${ssrStateAliases}
|
||||||
|
${ssrComputedAliases}
|
||||||
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
|
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
|
||||||
const __scopeValue = Object.entries(__hydrationState)
|
const __scopeValue = Object.entries(__hydrationState)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
const encoded =
|
let encoded: string;
|
||||||
typeof value === "number" || typeof value === "boolean"
|
try {
|
||||||
? String(value)
|
encoded = value === undefined ? "undefined" : JSON.stringify(value);
|
||||||
: JSON.stringify(value == null ? "" : String(value));
|
} catch {
|
||||||
|
encoded = JSON.stringify(String(value));
|
||||||
|
}
|
||||||
return key + ": " + encoded;
|
return key + ": " + encoded;
|
||||||
})
|
})
|
||||||
.join(", ")
|
.join(", ")
|
||||||
@@ -1311,14 +1475,18 @@ export function generate(ast: PageAst): string {
|
|||||||
${storeDeclarations}
|
${storeDeclarations}
|
||||||
${serverLoadAliases}
|
${serverLoadAliases}
|
||||||
${loopConsts.length > 0 ? loopConsts.join("\n") : ""}
|
${loopConsts.length > 0 ? loopConsts.join("\n") : ""}
|
||||||
const __state: ${stateType} = { ${dynamicStateScope} };
|
const __state: ${stateType} = ${dynamicStateInitializer};
|
||||||
${ssrStateAliases}
|
${ssrStateAliases}
|
||||||
|
${ssrComputedAliases}
|
||||||
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
|
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
|
||||||
const __scopeValue = Object.entries(__hydrationState)
|
const __scopeValue = Object.entries(__hydrationState)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
const encoded = typeof value === "number" || typeof value === "boolean"
|
let encoded: string;
|
||||||
? String(value)
|
try {
|
||||||
: JSON.stringify(value == null ? "" : String(value));
|
encoded = value === undefined ? "undefined" : JSON.stringify(value);
|
||||||
|
} catch {
|
||||||
|
encoded = JSON.stringify(String(value));
|
||||||
|
}
|
||||||
return key + ": " + encoded;
|
return key + ": " + encoded;
|
||||||
})
|
})
|
||||||
.join(", ")
|
.join(", ")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { existsSync, realpathSync } from "node:fs";
|
import { existsSync, realpathSync, statSync } from "node:fs";
|
||||||
import { dirname, extname, join, resolve } from "node:path";
|
import { dirname, extname, join, resolve } from "node:path";
|
||||||
import type { StructuredImportDecl } from "@wrnexus/syntax";
|
import type { StructuredImportDecl } from "@wrnexus/syntax";
|
||||||
|
|
||||||
@@ -33,12 +33,29 @@ export function resolveWrnImport(
|
|||||||
options: ImportResolverOptions,
|
options: ImportResolverOptions,
|
||||||
): ResolvedImport {
|
): ResolvedImport {
|
||||||
const source = declaration.source;
|
const source = declaration.source;
|
||||||
if (!source.startsWith(".") && !source.startsWith("@/")) return { declaration, resolved: source };
|
const aliases: Record<string, string> = {
|
||||||
const aliasRoot = options.aliases?.["@"] ?? "./app";
|
"@": "./app",
|
||||||
const base = source.startsWith("@/")
|
...(options.aliases ?? {}),
|
||||||
? resolve(options.appRoot, aliasRoot, source.slice(2))
|
};
|
||||||
|
const alias = Object.keys(aliases)
|
||||||
|
.filter((key) => key.length > 0 && (source === key || source.startsWith(`${key}/`)))
|
||||||
|
.sort((left, right) => right.length - left.length)[0];
|
||||||
|
if (!source.startsWith(".") && !alias) return { declaration, resolved: source };
|
||||||
|
const base = alias
|
||||||
|
? resolve(
|
||||||
|
options.appRoot,
|
||||||
|
aliases[alias]!,
|
||||||
|
source === alias ? "" : source.slice(alias.length + 1),
|
||||||
|
)
|
||||||
: resolve(dirname(importer), source);
|
: resolve(dirname(importer), source);
|
||||||
const found = candidates(base).find(existsSync);
|
const found = candidates(base).find((candidate) => {
|
||||||
|
if (!existsSync(candidate)) return false;
|
||||||
|
try {
|
||||||
|
return statSync(candidate).isFile();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
if (found) return { declaration, resolved: realpathSync(found) };
|
if (found) return { declaration, resolved: realpathSync(found) };
|
||||||
const severity = (options.mode ?? "compatible") === "explicit" ? "error" : "warning";
|
const severity = (options.mode ?? "compatible") === "explicit" ? "error" : "warning";
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { parse, resolveWrnImports } from "../src/index.ts";
|
||||||
|
|
||||||
|
test("configured import aliases resolve application client modules", () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "wrnexus-import-alias-"));
|
||||||
|
const app = join(root, "app");
|
||||||
|
const page = join(app, "settings.wrn");
|
||||||
|
const helper = join(app, "client", "storage.ts");
|
||||||
|
const clientIndex = join(app, "client", "index.ts");
|
||||||
|
mkdirSync(join(app, "client"), { recursive: true });
|
||||||
|
writeFileSync(helper, "export function persist(value: unknown) { return value; }\n");
|
||||||
|
writeFileSync(clientIndex, "export const storageReady = true;\n");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ast = parse(`import { persist } from "~/client/storage.ts"
|
||||||
|
page Settings { view { <button>Save</button> } }`);
|
||||||
|
const [resolved] = resolveWrnImports(ast.structuredImports, page, {
|
||||||
|
appRoot: root,
|
||||||
|
mode: "explicit",
|
||||||
|
aliases: { "~": "./app" },
|
||||||
|
});
|
||||||
|
expect(resolved?.diagnostic).toBeUndefined();
|
||||||
|
expect(resolved?.resolved).toBe(helper);
|
||||||
|
|
||||||
|
const directoryAst = parse(`import { storageReady } from "~client"
|
||||||
|
page ClientIndex { view { <p>Ready</p> } }`);
|
||||||
|
const [directoryResolved] = resolveWrnImports(directoryAst.structuredImports, page, {
|
||||||
|
appRoot: root,
|
||||||
|
mode: "explicit",
|
||||||
|
aliases: { "~client": "./app/client" },
|
||||||
|
});
|
||||||
|
expect(directoryResolved?.diagnostic).toBeUndefined();
|
||||||
|
expect(directoryResolved?.resolved).toBe(clientIndex);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { generate, generateTargets, parse } from "../src/index.ts";
|
||||||
|
|
||||||
|
test("page computed values derived from request state are declared during SSR", () => {
|
||||||
|
const code = generate(
|
||||||
|
parse(`import UserCard from "./UserCard.wrn"
|
||||||
|
page Chat {
|
||||||
|
state {
|
||||||
|
currentUserId = ctx.url.searchParams.get("as") ?? "asha"
|
||||||
|
users = [{ id: "asha", name: "Asha" }, { id: "rohan", name: "Rohan" }]
|
||||||
|
}
|
||||||
|
computed {
|
||||||
|
currentUser = users.find((user) => user.id === currentUserId) ?? users[0]
|
||||||
|
}
|
||||||
|
view {
|
||||||
|
<h1>{currentUser.name}</h1>
|
||||||
|
{#each users as user}
|
||||||
|
<UserCard user='{user}' />
|
||||||
|
<div data-user='{user.id}'></div>
|
||||||
|
{/each}
|
||||||
|
}
|
||||||
|
}`),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(code).toContain("const currentUser =");
|
||||||
|
expect(code).toContain("__wrnexusEscapeHtml(currentUser.name)");
|
||||||
|
expect(code).toContain("__wrnexusPropAttr(user)");
|
||||||
|
expect(code).toContain("__wrnexusEscapeHtml(user.id)");
|
||||||
|
expect(code).not.toContain("__wrnexusPropAttr(user.id)");
|
||||||
|
expect(code).toContain('encoded = value === undefined ? "undefined" : JSON.stringify(value)');
|
||||||
|
expect(code).not.toContain("String((__state as any)[key])");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("computed values that reference ctx directly stay dynamic during SSR", () => {
|
||||||
|
const code = generate(
|
||||||
|
parse(`page DirectRequestComputed {
|
||||||
|
computed { identity = ctx.url.searchParams.get("as") ?? "asha" }
|
||||||
|
view { <p>{identity}</p> }
|
||||||
|
}`),
|
||||||
|
);
|
||||||
|
expect(code).toContain("const identity =");
|
||||||
|
expect(code).toContain("__wrnexusEscapeHtml(identity)");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("computed values derived from server loads stay dynamic during SSR", () => {
|
||||||
|
const code = generate(
|
||||||
|
parse(`page LoadedComputed {
|
||||||
|
load server overview { return { count: 4 } }
|
||||||
|
computed { total = overview.count + 1 }
|
||||||
|
view { <p>{total}</p> }
|
||||||
|
}`),
|
||||||
|
);
|
||||||
|
expect(code).toContain('const overview = ctx["overview"]');
|
||||||
|
expect(code).toContain("__wrnexusEscapeHtml(total)");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("request-derived state dependencies stay dynamic during SSR", async () => {
|
||||||
|
const code = generate(
|
||||||
|
parse(`page Identity {
|
||||||
|
state {
|
||||||
|
userId = ctx.url.searchParams.get("as") ?? "asha"
|
||||||
|
label = "User: " + userId
|
||||||
|
}
|
||||||
|
view { <p>{label}</p> }
|
||||||
|
}`),
|
||||||
|
);
|
||||||
|
expect(code).toContain("__wrnexusEscapeHtml(label)");
|
||||||
|
expect(code).not.toContain('<span data-text="label"></span>');
|
||||||
|
const javascript = new Bun.Transpiler({ loader: "ts" }).transformSync(code);
|
||||||
|
const moduleUrl = `data:text/javascript;base64,${Buffer.from(javascript).toString("base64")}`;
|
||||||
|
const rendered = await (
|
||||||
|
await import(moduleUrl)
|
||||||
|
).default({
|
||||||
|
url: new URL("http://localhost/?as=rohan"),
|
||||||
|
});
|
||||||
|
expect(rendered).toContain("User: rohan");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Async aliases and invalidation tags are scoped in generated templates", () => {
|
||||||
|
const code = generate(
|
||||||
|
parse(`page Uploads {
|
||||||
|
load client uploads { return { name: "report.pdf" } }
|
||||||
|
view {
|
||||||
|
<Async source="uploads" tags="uploads,files">
|
||||||
|
<Loading>Loading</Loading>
|
||||||
|
<Success data="file"><a href="/files/{file.name}">{file.name}</a></Success>
|
||||||
|
<Error error="problem"><p>{problem.message}</p></Error>
|
||||||
|
</Async>
|
||||||
|
}
|
||||||
|
}`),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(code).toContain('data-wrn-async-tags="uploads,files"');
|
||||||
|
expect(code).toContain('data-wrn-async-alias="file"');
|
||||||
|
expect(code).toContain('data-wrn-async-alias="problem"');
|
||||||
|
expect(code).toContain("const file =");
|
||||||
|
expect(code).toContain("const problem =");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("browser codegen prunes SSR-only UI imports and keeps client dependencies", () => {
|
||||||
|
const browser = generateTargets(
|
||||||
|
parse(`import { Button } from "@wrnexus/ui"
|
||||||
|
import { save } from "./helper.ts"
|
||||||
|
component Settings {
|
||||||
|
state { enabled = true }
|
||||||
|
functions {
|
||||||
|
client function persist(): void { save(enabled) }
|
||||||
|
}
|
||||||
|
view {
|
||||||
|
<Button label="Save" />
|
||||||
|
<button @click='persist()'>Save</button>
|
||||||
|
}
|
||||||
|
}`),
|
||||||
|
).browser;
|
||||||
|
|
||||||
|
expect(browser).not.toContain("@wrnexus/ui");
|
||||||
|
expect(browser).toContain('import { save } from "./helper.ts"');
|
||||||
|
expect(browser).toContain('"persist": function');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reactive view imports trigger a browser module without hydrating static SSR helpers", () => {
|
||||||
|
const reactiveAst = parse(`import { formatName } from "./format.ts"
|
||||||
|
page Profile {
|
||||||
|
state { name = "Asha" }
|
||||||
|
view { <p>{formatName(name)}</p> }
|
||||||
|
}`);
|
||||||
|
const staticAst = parse(`import { readFileSync } from "node:fs"
|
||||||
|
page StaticProfile {
|
||||||
|
runtime = "server"
|
||||||
|
view { <p>{readFileSync("profile.txt", "utf8")}</p> }
|
||||||
|
}`);
|
||||||
|
|
||||||
|
expect(generate(reactiveAst)).toContain('data-wrn-client-module="__WRNEXUS_CLIENT_MODULE__"');
|
||||||
|
expect(generateTargets(reactiveAst).browser).toContain(
|
||||||
|
'import { formatName } from "./format.ts"',
|
||||||
|
);
|
||||||
|
expect(generate(staticAst)).not.toContain("data-wrn-client-module");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("state dependency cycles fail code generation", () => {
|
||||||
|
expect(() =>
|
||||||
|
generate(
|
||||||
|
parse(`page CyclicState {
|
||||||
|
state { first = second + 1 second = first + 1 }
|
||||||
|
view { <p>{first}</p> }
|
||||||
|
}`),
|
||||||
|
),
|
||||||
|
).toThrow("WRN-STATE-CYCLE");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("computed dependency cycles fail code generation", () => {
|
||||||
|
expect(() =>
|
||||||
|
generate(
|
||||||
|
parse(`page Cyclic {
|
||||||
|
computed { first = second + 1 second = first + 1 }
|
||||||
|
view { <p>{first}</p> }
|
||||||
|
}`),
|
||||||
|
),
|
||||||
|
).toThrow("WRN-COMPUTED-CYCLE");
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/content",
|
"name": "@wrnexus/content",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Typed Markdown content collections, loaders, indexes, feeds, and preview workflows for WRNexusJS.",
|
"description": "Typed Markdown content collections, loaders, indexes, feeds, and preview workflows for WRNexusJS.",
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/core",
|
"name": "@wrnexus/core",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -135,7 +135,6 @@ const DEFAULT_PERMISSIONS_POLICY: PermissionsPolicyConfig = {
|
|||||||
payment: [],
|
payment: [],
|
||||||
"picture-in-picture": [],
|
"picture-in-picture": [],
|
||||||
"sync-xhr": [],
|
"sync-xhr": [],
|
||||||
unload: [],
|
|
||||||
usb: [],
|
usb: [],
|
||||||
"xr-spatial-tracking": [],
|
"xr-spatial-tracking": [],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -72,3 +72,8 @@ test("resolveRequestUrl honors standard Forwarded headers behind trusted proxies
|
|||||||
expect(resolveRequestUrl(request, true).href).toBe("https://workroot.in/path");
|
expect(resolveRequestUrl(request, true).href).toBe("https://workroot.in/path");
|
||||||
expect(resolveRequestUrl(request, false).href).toBe("http://127.0.0.1:3000/path");
|
expect(resolveRequestUrl(request, false).href).toBe("http://127.0.0.1:3000/path");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("default permissions policy does not emit unsupported unload", () => {
|
||||||
|
const res = withSecurityHeaders(req(), new Response("x"), "development");
|
||||||
|
expect(res.headers.get("permissions-policy") ?? "").not.toContain("unload=");
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/csr",
|
"name": "@wrnexus/csr",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -702,5 +702,10 @@ export const NAV_RUNTIME = String.raw`
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Re-render the current route without adding a duplicate history entry. */
|
||||||
|
window.__wrnexusRefresh = function () {
|
||||||
|
navigate(location.href, true);
|
||||||
|
};
|
||||||
})();
|
})();
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|||||||
@@ -363,6 +363,26 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function routeSnapshot() {
|
||||||
|
var url = new URL(window.location.href);
|
||||||
|
var params = window.__wrnexusRouteParams || {};
|
||||||
|
var marker = document.querySelector("[data-wrn-route-params]");
|
||||||
|
if (marker) {
|
||||||
|
try { params = JSON.parse(marker.getAttribute("data-wrn-route-params") || "{}"); }
|
||||||
|
catch (_) { params = {}; }
|
||||||
|
window.__wrnexusRouteParams = params;
|
||||||
|
marker.remove();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
url: url.href,
|
||||||
|
pathname: url.pathname,
|
||||||
|
search: url.search,
|
||||||
|
hash: url.hash,
|
||||||
|
searchParams: url.searchParams,
|
||||||
|
params: params,
|
||||||
|
};
|
||||||
|
}
|
||||||
function sanitizeReactiveUrl(value) {
|
function sanitizeReactiveUrl(value) {
|
||||||
var raw = String(value == null ? "" : value);
|
var raw = String(value == null ? "" : value);
|
||||||
var compact = raw.trim().replace(/[\u0000-\u0020]+/g, "").toLowerCase();
|
var compact = raw.trim().replace(/[\u0000-\u0020]+/g, "").toLowerCase();
|
||||||
@@ -755,6 +775,20 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
var anyStateListeners = new Set();
|
var anyStateListeners = new Set();
|
||||||
var cleanupCallbacks = [];
|
var cleanupCallbacks = [];
|
||||||
var disposed = false;
|
var disposed = false;
|
||||||
|
var routeValue = signal(routeSnapshot());
|
||||||
|
|
||||||
|
function updateRouteValue() {
|
||||||
|
routeValue.set(routeSnapshot());
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("wrnexus:navigated", updateRouteValue);
|
||||||
|
window.addEventListener("popstate", updateRouteValue);
|
||||||
|
window.addEventListener("hashchange", updateRouteValue);
|
||||||
|
cleanupCallbacks.push(function () {
|
||||||
|
window.removeEventListener("wrnexus:navigated", updateRouteValue);
|
||||||
|
window.removeEventListener("popstate", updateRouteValue);
|
||||||
|
window.removeEventListener("hashchange", updateRouteValue);
|
||||||
|
});
|
||||||
|
|
||||||
function readGlobal(name) {
|
function readGlobal(name) {
|
||||||
if (declaredEvents.has(name)) {
|
if (declaredEvents.has(name)) {
|
||||||
@@ -785,6 +819,10 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
if (name === "location") return window.location;
|
if (name === "location") return window.location;
|
||||||
if (name === "history") return window.history;
|
if (name === "history") return window.history;
|
||||||
if (name === "navigator") return window.navigator;
|
if (name === "navigator") return window.navigator;
|
||||||
|
if (name === "$route" || name === "route") {
|
||||||
|
if (currentRenderer) routeValue.subscribe(currentRenderer);
|
||||||
|
return routeValue.get();
|
||||||
|
}
|
||||||
if (name === "setTimeout") return window.setTimeout.bind(window);
|
if (name === "setTimeout") return window.setTimeout.bind(window);
|
||||||
if (name === "clearTimeout") return window.clearTimeout.bind(window);
|
if (name === "clearTimeout") return window.clearTimeout.bind(window);
|
||||||
if (name === "setInterval") return window.setInterval.bind(window);
|
if (name === "setInterval") return window.setInterval.bind(window);
|
||||||
@@ -965,6 +1003,12 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
|
|
||||||
function installBehaviorFunctions(source) {
|
function installBehaviorFunctions(source) {
|
||||||
extractBehaviorFunctions(source || "").forEach(function (definition) {
|
extractBehaviorFunctions(source || "").forEach(function (definition) {
|
||||||
|
// A generated browser module contains the real JavaScript implementation
|
||||||
|
// of client/shared functions. Keep that implementation when it has already
|
||||||
|
// been installed instead of replacing it with the small CSP-safe fallback
|
||||||
|
// interpreter. The fallback intentionally supports only a bounded subset of
|
||||||
|
// JavaScript and must never shadow the compiled function.
|
||||||
|
if (typeof behaviorFunctions[definition.name] === "function") return;
|
||||||
behaviorFunctions[
|
behaviorFunctions[
|
||||||
definition.name
|
definition.name
|
||||||
] = function () {
|
] = function () {
|
||||||
@@ -3665,15 +3709,8 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
return special.value;
|
return special.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Template literals are evaluated without eval so they remain compatible
|
function evaluateTemplateLiteral(source) {
|
||||||
// with a strict CSP. Each interpolation uses the same bounded expression
|
var template = source.slice(1, -1);
|
||||||
// evaluator as every other reactive binding.
|
|
||||||
if (
|
|
||||||
expr.length >= 2 &&
|
|
||||||
expr[0] === "\`" &&
|
|
||||||
expr[expr.length - 1] === "\`"
|
|
||||||
) {
|
|
||||||
var template = expr.slice(1, -1);
|
|
||||||
var rendered = "";
|
var rendered = "";
|
||||||
var cursor = 0;
|
var cursor = 0;
|
||||||
|
|
||||||
@@ -3703,7 +3740,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
else if (character === "}" && --depth === 0) break;
|
else if (character === "}" && --depth === 0) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (depth !== 0) throw new Error("Unclosed template interpolation in '" + expr + "'");
|
if (depth !== 0) throw new Error("Unclosed template interpolation in '" + source + "'");
|
||||||
rendered += String(evaluateExpression(template.slice(expressionStart, expressionEnd), read));
|
rendered += String(evaluateExpression(template.slice(expressionStart, expressionEnd), read));
|
||||||
cursor = expressionEnd + 1;
|
cursor = expressionEnd + 1;
|
||||||
continue;
|
continue;
|
||||||
@@ -3715,6 +3752,18 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
return rendered;
|
return rendered;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Template literals are evaluated without eval so they remain compatible
|
||||||
|
// with a strict CSP. Each interpolation uses the same bounded expression
|
||||||
|
// evaluator as every other reactive binding. They are accepted both as a
|
||||||
|
// complete expression and as a primary inside a larger expression.
|
||||||
|
if (
|
||||||
|
expr.length >= 2 &&
|
||||||
|
expr[0] === "\`" &&
|
||||||
|
expr[expr.length - 1] === "\`"
|
||||||
|
) {
|
||||||
|
return evaluateTemplateLiteral(expr);
|
||||||
|
}
|
||||||
|
|
||||||
var tokens =
|
var tokens =
|
||||||
tokenizeExpression(expr);
|
tokenizeExpression(expr);
|
||||||
|
|
||||||
@@ -3731,6 +3780,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
var t = next();
|
var t = next();
|
||||||
if (!t) throw new Error("Unexpected end of expression in '" + expr + "'");
|
if (!t) throw new Error("Unexpected end of expression in '" + expr + "'");
|
||||||
if (t.type === "number" || t.type === "string") return { value: t.value };
|
if (t.type === "number" || t.type === "string") return { value: t.value };
|
||||||
|
if (t.type === "template") return { value: evaluateTemplateLiteral(t.value) };
|
||||||
if (t.type === "ident") {
|
if (t.type === "ident") {
|
||||||
if (t.value === "true") return { value: true };
|
if (t.value === "true") return { value: true };
|
||||||
if (t.value === "false") return { value: false };
|
if (t.value === "false") return { value: false };
|
||||||
@@ -3874,6 +3924,37 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
tokens.push({ type: "string", value: value });
|
tokens.push({ type: "string", value: value });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (ch === "\`") {
|
||||||
|
var templateStart = i++;
|
||||||
|
var templateDepth = 0;
|
||||||
|
var templateQuote = "";
|
||||||
|
while (i < input.length) {
|
||||||
|
ch = input[i++];
|
||||||
|
if (templateQuote) {
|
||||||
|
if (ch === "\\") i++;
|
||||||
|
else if (ch === templateQuote) templateQuote = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === "\\") {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === "\`" && templateDepth === 0) break;
|
||||||
|
if (ch === "$" && input[i] === "{") {
|
||||||
|
templateDepth++;
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (templateDepth > 0) {
|
||||||
|
if (ch === '"' || ch === "'") templateQuote = ch;
|
||||||
|
else if (ch === "{") templateDepth++;
|
||||||
|
else if (ch === "}") templateDepth--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (input[i - 1] !== "\`") throw new Error("Unclosed template literal");
|
||||||
|
tokens.push({ type: "template", value: input.slice(templateStart, i) });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (/[A-Za-z_$]/.test(ch)) {
|
if (/[A-Za-z_$]/.test(ch)) {
|
||||||
var s = i++;
|
var s = i++;
|
||||||
while (i < input.length && /[A-Za-z0-9_$]/.test(input[i])) i++;
|
while (i < input.length && /[A-Za-z0-9_$]/.test(input[i])) i++;
|
||||||
@@ -4047,6 +4128,23 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
Array.from(node.childNodes || []).forEach(visit);
|
Array.from(node.childNodes || []).forEach(visit);
|
||||||
}
|
}
|
||||||
visit(root);
|
visit(root);
|
||||||
|
root.querySelectorAll("*").forEach(function (node) {
|
||||||
|
Array.from(node.attributes || []).forEach(function (attribute) {
|
||||||
|
if (attribute.name.indexOf("data-wrn-async-") === 0) return;
|
||||||
|
if (attribute.value.indexOf("{") === -1) return;
|
||||||
|
var nextValue = String(attribute.value).replace(/\{\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*\}/g, function (whole, path) {
|
||||||
|
var parts = path.split(".");
|
||||||
|
if (parts[0] === prefix) parts.shift();
|
||||||
|
if (prefix === "error" && parts[0] !== "message") return whole;
|
||||||
|
var value = asyncValueAt(model, parts.join("."));
|
||||||
|
return typeof value === "object" ? JSON.stringify(value) : String(value);
|
||||||
|
});
|
||||||
|
if (/^(?:href|src|action|formaction)$/i.test(attribute.name)) {
|
||||||
|
nextValue = sanitizeReactiveUrl(nextValue);
|
||||||
|
}
|
||||||
|
node.setAttribute(attribute.name, nextValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
root.querySelectorAll("[data-wrn-async-value]").forEach(function (node) {
|
root.querySelectorAll("[data-wrn-async-value]").forEach(function (node) {
|
||||||
var path = node.getAttribute("data-wrn-async-value") || "";
|
var path = node.getAttribute("data-wrn-async-value") || "";
|
||||||
var value = asyncValueAt(model, path);
|
var value = asyncValueAt(model, path);
|
||||||
@@ -4058,7 +4156,9 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
var content = boundary.querySelector("[data-wrn-async-content]");
|
var content = boundary.querySelector("[data-wrn-async-content]");
|
||||||
if (!template || !content) return;
|
if (!template || !content) return;
|
||||||
var fragment = template.content.cloneNode(true);
|
var fragment = template.content.cloneNode(true);
|
||||||
interpolateAsync(fragment, model, state === "error" ? "error" : boundary.getAttribute("data-wrn-async"));
|
var alias = template.getAttribute("data-wrn-async-alias") ||
|
||||||
|
(state === "error" ? "error" : boundary.getAttribute("data-wrn-async"));
|
||||||
|
interpolateAsync(fragment, model, alias);
|
||||||
content.replaceChildren(fragment);
|
content.replaceChildren(fragment);
|
||||||
boundary.setAttribute("aria-busy", state === "loading" ? "true" : "false");
|
boundary.setAttribute("aria-busy", state === "loading" ? "true" : "false");
|
||||||
boundary.setAttribute("data-wrn-async-state", state);
|
boundary.setAttribute("data-wrn-async-state", state);
|
||||||
@@ -4073,7 +4173,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
}
|
}
|
||||||
var name = boundary.getAttribute("data-wrn-async") || "";
|
var name = boundary.getAttribute("data-wrn-async") || "";
|
||||||
if (!/^[A-Za-z_$][\w$]{0,63}$/.test(name)) return showAsync(boundary, "error", { message: "Invalid data source" });
|
if (!/^[A-Za-z_$][\w$]{0,63}$/.test(name)) return showAsync(boundary, "error", { message: "Invalid data source" });
|
||||||
var url = "/__wrnexus/client-load?route=" + encodeURIComponent(location.pathname) + "&name=" + encodeURIComponent(name);
|
var url = "/__wrnexus/client-load?route=" + encodeURIComponent(location.pathname) + "&search=" + encodeURIComponent(location.search) + "&name=" + encodeURIComponent(name);
|
||||||
var attempts = Math.min(5, Math.max(0, Number(boundary.getAttribute("data-wrn-async-retries")) || 0));
|
var attempts = Math.min(5, Math.max(0, Number(boundary.getAttribute("data-wrn-async-retries")) || 0));
|
||||||
var controller = new AbortController();
|
var controller = new AbortController();
|
||||||
boundary.__wrnexusAsyncAbort = controller;
|
boundary.__wrnexusAsyncAbort = controller;
|
||||||
@@ -4102,6 +4202,44 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
(root || document).querySelectorAll("[data-wrn-async]").forEach(setupAsyncBoundary);
|
(root || document).querySelectorAll("[data-wrn-async]").forEach(setupAsyncBoundary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var invalidationRefreshPending = false;
|
||||||
|
function asyncBoundaryTags(boundary) {
|
||||||
|
var raw = boundary.getAttribute("data-wrn-async-tags") || boundary.getAttribute("data-wrn-async") || "";
|
||||||
|
return raw.split(",").map(function (tag) { return tag.trim(); }).filter(Boolean);
|
||||||
|
}
|
||||||
|
function refreshAsyncBoundary(boundary) {
|
||||||
|
if (boundary.__wrnexusAsyncAbort) {
|
||||||
|
try { boundary.__wrnexusAsyncAbort.abort(); } catch (_) {}
|
||||||
|
}
|
||||||
|
boundary.__wrnexusAsync = false;
|
||||||
|
boundary.removeAttribute("data-wrn-async-resolved");
|
||||||
|
setupAsyncBoundary(boundary);
|
||||||
|
}
|
||||||
|
function handleCacheInvalidation(event) {
|
||||||
|
var detail = event && event.detail && typeof event.detail === "object" ? event.detail : {};
|
||||||
|
var tags = Array.isArray(detail.tags) ? detail.tags.map(String) : [];
|
||||||
|
if (!tags.length) return;
|
||||||
|
var matchedServerBoundary = false;
|
||||||
|
document.querySelectorAll("[data-wrn-async]").forEach(function (boundary) {
|
||||||
|
var matches = asyncBoundaryTags(boundary).some(function (tag) { return tags.indexOf(tag) !== -1; });
|
||||||
|
if (!matches) return;
|
||||||
|
if (boundary.getAttribute("data-wrn-async-resolved") === "true") {
|
||||||
|
matchedServerBoundary = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
refreshAsyncBoundary(boundary);
|
||||||
|
});
|
||||||
|
if (matchedServerBoundary && !invalidationRefreshPending) {
|
||||||
|
invalidationRefreshPending = true;
|
||||||
|
Promise.resolve().then(function () {
|
||||||
|
invalidationRefreshPending = false;
|
||||||
|
if (typeof window.__wrnexusRefresh === "function") window.__wrnexusRefresh();
|
||||||
|
else window.location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener("wrnexus:cache:invalidate", handleCacheInvalidation);
|
||||||
|
|
||||||
function mountClientRoots(root) {
|
function mountClientRoots(root) {
|
||||||
(root || document).querySelectorAll("template[data-wrn-client-template]").forEach(function (template) {
|
(root || document).querySelectorAll("template[data-wrn-client-template]").forEach(function (template) {
|
||||||
var id = template.getAttribute("data-wrn-client-template");
|
var id = template.getAttribute("data-wrn-client-template");
|
||||||
|
|||||||
@@ -23,10 +23,29 @@ export const REALTIME_RUNTIME = String.raw`
|
|||||||
if (!("WebSocket" in window)) return;
|
if (!("WebSocket" in window)) return;
|
||||||
var wire = (window.wire = window.wire || {});
|
var wire = (window.wire = window.wire || {});
|
||||||
if (wire.room) return; // already installed
|
if (wire.room) return; // already installed
|
||||||
var open = {}; // name -> room connection
|
var open = {}; // normalized room+query key -> room connection
|
||||||
|
|
||||||
|
function normalizedQuery(query) {
|
||||||
|
var params = new URLSearchParams(String(query || "").replace(/^\?/, ""));
|
||||||
|
var pairs = [];
|
||||||
|
params.forEach(function (value, name) { pairs.push([name, value]); });
|
||||||
|
pairs.sort(function (a, b) {
|
||||||
|
return a[0] === b[0] ? String(a[1]).localeCompare(String(b[1])) : String(a[0]).localeCompare(String(b[0]));
|
||||||
|
});
|
||||||
|
var normalized = new URLSearchParams();
|
||||||
|
pairs.forEach(function (pair) { normalized.append(pair[0], pair[1]); });
|
||||||
|
return normalized.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function roomKey(name, query) {
|
||||||
|
var normalized = normalizedQuery(query);
|
||||||
|
return String(name || "") + (normalized ? "?" + normalized : "");
|
||||||
|
}
|
||||||
|
|
||||||
function openRoom(name, query) {
|
function openRoom(name, query) {
|
||||||
if (open[name]) return open[name];
|
query = normalizedQuery(query);
|
||||||
|
var key = roomKey(name, query);
|
||||||
|
if (open[key]) return open[key];
|
||||||
var ws = null, queue = [], listeners = [], attempts = 0, timer = null, closed = false;
|
var ws = null, queue = [], listeners = [], attempts = 0, timer = null, closed = false;
|
||||||
|
|
||||||
function url() {
|
function url() {
|
||||||
@@ -69,6 +88,8 @@ export const REALTIME_RUNTIME = String.raw`
|
|||||||
|
|
||||||
var api = {
|
var api = {
|
||||||
name: name,
|
name: name,
|
||||||
|
key: key,
|
||||||
|
query: query,
|
||||||
send: function (obj) {
|
send: function (obj) {
|
||||||
var payload = typeof obj === "string" ? obj : JSON.stringify(obj);
|
var payload = typeof obj === "string" ? obj : JSON.stringify(obj);
|
||||||
if (ws && ws.readyState === 1) ws.send(payload);
|
if (ws && ws.readyState === 1) ws.send(payload);
|
||||||
@@ -80,9 +101,9 @@ export const REALTIME_RUNTIME = String.raw`
|
|||||||
listeners.push({ type: type, cb: cb });
|
listeners.push({ type: type, cb: cb });
|
||||||
return api;
|
return api;
|
||||||
},
|
},
|
||||||
close: function () { closed = true; clearTimeout(timer); if (ws) try { ws.close(); } catch (_) {} ws = null; delete open[name]; },
|
close: function () { closed = true; clearTimeout(timer); if (ws) try { ws.close(); } catch (_) {} ws = null; delete open[key]; },
|
||||||
};
|
};
|
||||||
open[name] = api;
|
open[key] = api;
|
||||||
connect();
|
connect();
|
||||||
return api;
|
return api;
|
||||||
}
|
}
|
||||||
@@ -114,11 +135,14 @@ export const REALTIME_RUNTIME = String.raw`
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bindContainer(el) {
|
function bindContainer(el) {
|
||||||
if (el.__wireRoomBound) return;
|
|
||||||
el.__wireRoomBound = true;
|
|
||||||
var name = el.getAttribute("data-room");
|
var name = el.getAttribute("data-room");
|
||||||
var user = el.getAttribute("data-room-user");
|
var user = el.getAttribute("data-room-user");
|
||||||
var room = openRoom(name, user ? "user=" + encodeURIComponent(user) : "");
|
var query = el.getAttribute("data-room-query") || (user ? "user=" + encodeURIComponent(user) : "");
|
||||||
|
var key = roomKey(name, query);
|
||||||
|
if (el.__wireRoomBound && el.__wireRoomKey === key) return;
|
||||||
|
el.__wireRoomBound = true;
|
||||||
|
el.__wireRoomKey = key;
|
||||||
|
var room = openRoom(name, query);
|
||||||
el.__wireRoom = room;
|
el.__wireRoom = room;
|
||||||
|
|
||||||
var log = el.querySelector("[data-room-log]");
|
var log = el.querySelector("[data-room-log]");
|
||||||
@@ -161,7 +185,7 @@ export const REALTIME_RUNTIME = String.raw`
|
|||||||
var input = form.elements[i];
|
var input = form.elements[i];
|
||||||
if (input.name) data[input.name] = input.value;
|
if (input.name) data[input.name] = input.value;
|
||||||
}
|
}
|
||||||
room.send(data);
|
if (el.__wireRoom) el.__wireRoom.send(data);
|
||||||
for (var j = 0; j < form.elements.length; j++) {
|
for (var j = 0; j < form.elements.length; j++) {
|
||||||
if (form.elements[j].hasAttribute("data-room-reset")) form.elements[j].value = "";
|
if (form.elements[j].hasAttribute("data-room-reset")) form.elements[j].value = "";
|
||||||
}
|
}
|
||||||
@@ -173,8 +197,8 @@ export const REALTIME_RUNTIME = String.raw`
|
|||||||
var containers = (root || document).querySelectorAll("[data-room]");
|
var containers = (root || document).querySelectorAll("[data-room]");
|
||||||
var present = {};
|
var present = {};
|
||||||
for (var i = 0; i < containers.length; i++) {
|
for (var i = 0; i < containers.length; i++) {
|
||||||
present[containers[i].getAttribute("data-room")] = true;
|
|
||||||
bindContainer(containers[i]);
|
bindContainer(containers[i]);
|
||||||
|
if (containers[i].__wireRoomKey) present[containers[i].__wireRoomKey] = true;
|
||||||
}
|
}
|
||||||
// Close rooms whose container has left the page (client-side navigation).
|
// Close rooms whose container has left the page (client-side navigation).
|
||||||
for (var nm in open) if (!present[nm]) open[nm].close();
|
for (var nm in open) if (!present[nm]) open[nm].close();
|
||||||
|
|||||||
@@ -452,3 +452,124 @@ test("$emit inside component functions does not depend on a global browser event
|
|||||||
expect(win.document.querySelector("span")!.textContent).toBe("true");
|
expect(win.document.querySelector("span")!.textContent).toBe("true");
|
||||||
expect(detail).toEqual({ source: "click" });
|
expect(detail).toEqual({ source: "click" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("template literals work inside browser API call arguments", () => {
|
||||||
|
const behavior = Buffer.from(
|
||||||
|
JSON.stringify({
|
||||||
|
functions: `function save() {
|
||||||
|
window.localStorage.setItem(\`prefs:\${count}\`, JSON.stringify(count))
|
||||||
|
message = \`Saved \${count}\`
|
||||||
|
}`,
|
||||||
|
watches: [],
|
||||||
|
lifecycle: {},
|
||||||
|
}),
|
||||||
|
).toString("base64");
|
||||||
|
const win = mount(
|
||||||
|
`<div data-scope="count: 3, message: 'ready'" data-wrn-behavior="${behavior}">` +
|
||||||
|
`<button data-on-click="save()">{message}</button></div>`,
|
||||||
|
);
|
||||||
|
win.document.querySelector("button")!.click();
|
||||||
|
expect(win.localStorage.getItem("prefs:3")).toBe("3");
|
||||||
|
expect(win.document.querySelector("button")!.textContent).toBe("Saved 3");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("compiled client functions are not overwritten by fallback behavior parsing", () => {
|
||||||
|
const behavior = Buffer.from(
|
||||||
|
JSON.stringify({
|
||||||
|
functions: `function save() { message = \`fallback\` }`,
|
||||||
|
watches: [],
|
||||||
|
lifecycle: {},
|
||||||
|
}),
|
||||||
|
).toString("base64");
|
||||||
|
const win = new Window() as unknown as Window & Record<string, unknown>;
|
||||||
|
win.document.body.innerHTML =
|
||||||
|
`<div id="scope" data-scope="message: 'ready'" data-wrn-behavior="${behavior}">` +
|
||||||
|
`<button data-on-click="save()">{message}</button></div>`;
|
||||||
|
const scope = win.document.getElementById("scope") as unknown as HTMLElement & {
|
||||||
|
__wrnexusClientModule?: unknown;
|
||||||
|
};
|
||||||
|
scope.__wrnexusClientModule = {
|
||||||
|
bindClientScope(context: { state: Record<string, unknown> }) {
|
||||||
|
return {
|
||||||
|
save() {
|
||||||
|
context.state.message = "native";
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
(globalThis as Record<string, unknown>).window = win;
|
||||||
|
(globalThis as Record<string, unknown>).document = win.document;
|
||||||
|
(globalThis as Record<string, unknown>).location = win.location;
|
||||||
|
(globalThis as Record<string, unknown>).NodeFilter = (
|
||||||
|
win as unknown as { NodeFilter: unknown }
|
||||||
|
).NodeFilter;
|
||||||
|
(globalThis as Record<string, unknown>).MutationObserver = (
|
||||||
|
win as unknown as { MutationObserver: unknown }
|
||||||
|
).MutationObserver;
|
||||||
|
(0, eval)(REACTIVE_RUNTIME);
|
||||||
|
(win as unknown as { __wrnexusHydrateScopes?: (root: unknown) => void }).__wrnexusHydrateScopes?.(
|
||||||
|
win.document,
|
||||||
|
);
|
||||||
|
win.document.querySelector("button")!.click();
|
||||||
|
expect(win.document.querySelector("button")!.textContent).toBe("native");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("$route updates after client navigation", () => {
|
||||||
|
const win = new Window({ url: "http://localhost/" }) as unknown as Window &
|
||||||
|
Record<string, unknown>;
|
||||||
|
win.document.body.innerHTML =
|
||||||
|
`<span hidden data-wrn-route-params='{"id":"42"}'></span>` +
|
||||||
|
`<div data-scope=""><strong>{$route.pathname}</strong><em>{$route.params.id}</em></div>`;
|
||||||
|
(globalThis as Record<string, unknown>).window = win;
|
||||||
|
(globalThis as Record<string, unknown>).document = win.document;
|
||||||
|
(globalThis as Record<string, unknown>).location = win.location;
|
||||||
|
(globalThis as Record<string, unknown>).NodeFilter = (
|
||||||
|
win as unknown as { NodeFilter: unknown }
|
||||||
|
).NodeFilter;
|
||||||
|
(globalThis as Record<string, unknown>).MutationObserver = (
|
||||||
|
win as unknown as { MutationObserver: unknown }
|
||||||
|
).MutationObserver;
|
||||||
|
(0, eval)(REACTIVE_RUNTIME);
|
||||||
|
(win as unknown as { __wrnexusHydrateScopes?: (root: unknown) => void }).__wrnexusHydrateScopes?.(
|
||||||
|
win.document,
|
||||||
|
);
|
||||||
|
expect(win.document.querySelector("strong")!.textContent).toBe("/");
|
||||||
|
expect(win.document.querySelector("em")!.textContent).toBe("42");
|
||||||
|
expect(win.document.querySelector("[data-wrn-route-params]")).toBeNull();
|
||||||
|
win.history.pushState({}, "", "/settings?tab=chat");
|
||||||
|
win.dispatchEvent(
|
||||||
|
new (win as unknown as { CustomEvent: typeof CustomEvent }).CustomEvent(
|
||||||
|
"wrnexus:navigated",
|
||||||
|
) as unknown as Parameters<Window["dispatchEvent"]>[0],
|
||||||
|
);
|
||||||
|
expect(win.document.querySelector("strong")!.textContent).toBe("/settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("cache invalidation refetches matching client Async boundaries", async () => {
|
||||||
|
let requests = 0;
|
||||||
|
(globalThis as Record<string, unknown>).fetch = () => {
|
||||||
|
requests++;
|
||||||
|
return Promise.resolve(Response.json({ data: { value: requests } }));
|
||||||
|
};
|
||||||
|
const win = mount(
|
||||||
|
`<section data-wrn-async="uploads" data-wrn-async-tags="uploads" data-wrn-async-retries="0">` +
|
||||||
|
`<div data-wrn-async-content></div>` +
|
||||||
|
`<template data-wrn-async-loading>Loading</template>` +
|
||||||
|
`<template data-wrn-async-success data-wrn-async-alias="uploads"><b>{uploads.value}</b></template>` +
|
||||||
|
`<template data-wrn-async-error data-wrn-async-alias="error">{error.message}</template>` +
|
||||||
|
`</section>`,
|
||||||
|
);
|
||||||
|
const runtime = win as unknown as { __wrnexusHydrateAsyncBoundaries?: (root: unknown) => void };
|
||||||
|
runtime.__wrnexusHydrateAsyncBoundaries?.(win.document);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
expect(win.document.querySelector("b")?.textContent).toBe("1");
|
||||||
|
win.dispatchEvent(
|
||||||
|
new (win as unknown as { CustomEvent: typeof CustomEvent }).CustomEvent(
|
||||||
|
"wrnexus:cache:invalidate",
|
||||||
|
{ detail: { tags: ["uploads"] } },
|
||||||
|
) as unknown as Parameters<Window["dispatchEvent"]>[0],
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
expect(requests).toBe(2);
|
||||||
|
expect(win.document.querySelector("b")?.textContent).toBe("2");
|
||||||
|
});
|
||||||
|
|||||||
@@ -121,3 +121,27 @@ test("programmatic wire.room() sends and receives", () => {
|
|||||||
sockets[0]!.fireMessage({ type: "ping", n: 1 });
|
sockets[0]!.fireMessage({ type: "ping", n: 1 });
|
||||||
expect(got).toEqual([{ type: "ping", n: 1 }]);
|
expect(got).toEqual([{ type: "ping", n: 1 }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("same room with different users opens identity-specific sockets", () => {
|
||||||
|
boot(`
|
||||||
|
<div data-room="teamspace" data-room-user="asha"></div>
|
||||||
|
<div data-room="teamspace" data-room-user="rohan"></div>
|
||||||
|
`);
|
||||||
|
expect(sockets.map((socket) => socket.url).sort()).toEqual([
|
||||||
|
"ws://localhost/realtime/teamspace?user=asha",
|
||||||
|
"ws://localhost/realtime/teamspace?user=rohan",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("declarative room reconnects when its user identity changes", () => {
|
||||||
|
const win = boot(
|
||||||
|
`<div id="room" data-room="teamspace" data-room-user="asha"></div>`,
|
||||||
|
) as unknown as Window & {
|
||||||
|
wire: { bindRooms: (root?: unknown) => void };
|
||||||
|
};
|
||||||
|
expect(sockets[0]?.url).toBe("ws://localhost/realtime/teamspace?user=asha");
|
||||||
|
win.document.getElementById("room")!.setAttribute("data-room-user", "neha");
|
||||||
|
win.wire.bindRooms(win.document);
|
||||||
|
expect(sockets[0]?.readyState).toBe(3);
|
||||||
|
expect(sockets[1]?.url).toBe("ws://localhost/realtime/teamspace?user=neha");
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/db",
|
"name": "@wrnexus/db",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/dev-server",
|
"name": "@wrnexus/dev-server",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import {
|
|||||||
setCompileCacheDir,
|
setCompileCacheDir,
|
||||||
setCompileImportOptions,
|
setCompileImportOptions,
|
||||||
setDevCompilerPipeline,
|
setDevCompilerPipeline,
|
||||||
wrnBrowserArtifactUrl,
|
wrnBrowserArtifactUrlAsync,
|
||||||
} from "./pipeline.ts";
|
} from "./pipeline.ts";
|
||||||
import { createRpcHandler } from "@wrnexus/ssr/rpc";
|
import { createRpcHandler } from "@wrnexus/ssr/rpc";
|
||||||
import { createHandlers, type WsData } from "./runtime.ts";
|
import { createHandlers, type WsData } from "./runtime.ts";
|
||||||
@@ -632,7 +632,7 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
|||||||
storeUpdates.push({
|
storeUpdates.push({
|
||||||
name: declaration[2]!,
|
name: declaration[2]!,
|
||||||
kind: declaration[1]!,
|
kind: declaration[1]!,
|
||||||
url: wrnBrowserArtifactUrl(absolute),
|
url: await wrnBrowserArtifactUrlAsync(absolute),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`[wrnexus] failed to prepare store HMR for ${absolute}`, error);
|
console.warn(`[wrnexus] failed to prepare store HMR for ${absolute}`, error);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
unlinkSync,
|
unlinkSync,
|
||||||
existsSync,
|
existsSync,
|
||||||
} from "node:fs";
|
} from "node:fs";
|
||||||
import { dirname, join, basename, extname, resolve } from "node:path";
|
import { dirname, join, basename, extname, relative, resolve } from "node:path";
|
||||||
import {
|
import {
|
||||||
compile,
|
compile,
|
||||||
generate,
|
generate,
|
||||||
@@ -91,6 +91,16 @@ export function setCompileImportOptions(
|
|||||||
|
|
||||||
const compileInProgress = new Map<string, WrnCompileArtifacts>();
|
const compileInProgress = new Map<string, WrnCompileArtifacts>();
|
||||||
|
|
||||||
|
function isApplicationImportSource(
|
||||||
|
source: string,
|
||||||
|
aliases: Record<string, string> | undefined,
|
||||||
|
): boolean {
|
||||||
|
if (source.startsWith(".")) return true;
|
||||||
|
return Object.keys(aliases ?? {}).some(
|
||||||
|
(alias) => source === alias || source.startsWith(`${alias}/`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function projectRootForFile(file: string): string {
|
function projectRootForFile(file: string): string {
|
||||||
let current = dirname(resolve(file));
|
let current = dirname(resolve(file));
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -101,6 +111,19 @@ function projectRootForFile(file: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function importOptionsHash(file: string): string {
|
||||||
|
const root = resolve(projectRootForFile(file));
|
||||||
|
const options = compileImportOptions.get(root) ?? {
|
||||||
|
mode: "compatible" as const,
|
||||||
|
aliases: { "@": "./app" },
|
||||||
|
autoImport: true,
|
||||||
|
};
|
||||||
|
const aliases = Object.fromEntries(
|
||||||
|
Object.entries(options.aliases).sort(([left], [right]) => left.localeCompare(right)),
|
||||||
|
);
|
||||||
|
return hashPath(JSON.stringify({ ...options, aliases }));
|
||||||
|
}
|
||||||
|
|
||||||
function rewriteArtifactImports(
|
function rewriteArtifactImports(
|
||||||
code: string,
|
code: string,
|
||||||
ast: PageAst,
|
ast: PageAst,
|
||||||
@@ -132,8 +155,7 @@ function rewriteArtifactImports(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!entry.resolved || !entry.declaration.source) continue;
|
if (!entry.resolved || !entry.declaration.source) continue;
|
||||||
if (!entry.declaration.source.startsWith(".") && !entry.declaration.source.startsWith("@/"))
|
if (!isApplicationImportSource(entry.declaration.source, importOptions.aliases)) continue;
|
||||||
continue;
|
|
||||||
let replacement = entry.resolved;
|
let replacement = entry.resolved;
|
||||||
if (entry.resolved.endsWith(".wrn")) {
|
if (entry.resolved.endsWith(".wrn")) {
|
||||||
const dependencySource = readFileSync(entry.resolved, "utf8");
|
const dependencySource = readFileSync(entry.resolved, "utf8");
|
||||||
@@ -148,13 +170,22 @@ function rewriteArtifactImports(
|
|||||||
output = output.replace(entry.declaration.raw, "");
|
output = output.replace(entry.declaration.raw, "");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
replacement = `/__wrnexus/client/${basename(dependency.browser).replace(/\.client\.mjs$/, ".mjs")}`;
|
replacement = dependency.browser;
|
||||||
} else {
|
} else {
|
||||||
replacement = target === "server" ? dependency.server : dependency.main;
|
replacement = target === "server" ? dependency.server : dependency.main;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const escaped = entry.declaration.source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
const escaped = entry.declaration.source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
const specifier = replacement.startsWith("/") ? replacement : pathToFileURL(replacement).href;
|
const specifier =
|
||||||
|
target === "browser"
|
||||||
|
? (() => {
|
||||||
|
const cacheDir = compileCacheDir ?? join(dirname(importer), ".wrnexus");
|
||||||
|
const relativeTarget = relative(cacheDir, replacement).replace(/\\/g, "/");
|
||||||
|
return relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`;
|
||||||
|
})()
|
||||||
|
: replacement.startsWith("/")
|
||||||
|
? replacement
|
||||||
|
: pathToFileURL(replacement).href;
|
||||||
output = output.replace(new RegExp(`(["'])${escaped}\\1`, "g"), JSON.stringify(specifier));
|
output = output.replace(new RegExp(`(["'])${escaped}\\1`, "g"), JSON.stringify(specifier));
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
@@ -169,10 +200,15 @@ async function rewriteArtifactImportsAsync(
|
|||||||
let output = code;
|
let output = code;
|
||||||
for (const [id, replacement] of devCompilerPipeline?.virtualModules ?? []) {
|
for (const [id, replacement] of devCompilerPipeline?.virtualModules ?? []) {
|
||||||
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
output = output.replace(
|
const specifier =
|
||||||
new RegExp(`(["'])${escaped}\\1`, "g"),
|
target === "browser"
|
||||||
JSON.stringify(pathToFileURL(replacement).href),
|
? (() => {
|
||||||
);
|
const cacheDir = compileCacheDir ?? join(dirname(importer), ".wrnexus");
|
||||||
|
const relativeTarget = relative(cacheDir, replacement).replace(/\\/g, "/");
|
||||||
|
return relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`;
|
||||||
|
})()
|
||||||
|
: pathToFileURL(replacement).href;
|
||||||
|
output = output.replace(new RegExp(`(["'])${escaped}\\1`, "g"), JSON.stringify(specifier));
|
||||||
}
|
}
|
||||||
if (!ast.structuredImports.length) return output;
|
if (!ast.structuredImports.length) return output;
|
||||||
const root = projectRootForFile(importer);
|
const root = projectRootForFile(importer);
|
||||||
@@ -189,8 +225,7 @@ async function rewriteArtifactImportsAsync(
|
|||||||
for (const entry of resolved) {
|
for (const entry of resolved) {
|
||||||
if (entry.diagnostic?.severity === "error") throw new Error(entry.diagnostic.message);
|
if (entry.diagnostic?.severity === "error") throw new Error(entry.diagnostic.message);
|
||||||
if (!entry.resolved || !entry.declaration.source) continue;
|
if (!entry.resolved || !entry.declaration.source) continue;
|
||||||
if (!entry.declaration.source.startsWith(".") && !entry.declaration.source.startsWith("@/"))
|
if (!isApplicationImportSource(entry.declaration.source, importOptions.aliases)) continue;
|
||||||
continue;
|
|
||||||
let replacement = entry.resolved;
|
let replacement = entry.resolved;
|
||||||
if (replacement.endsWith(".wrn")) {
|
if (replacement.endsWith(".wrn")) {
|
||||||
const dependencySource = readFileSync(replacement, "utf8");
|
const dependencySource = readFileSync(replacement, "utf8");
|
||||||
@@ -204,11 +239,20 @@ async function rewriteArtifactImportsAsync(
|
|||||||
output = output.replace(entry.declaration.raw, "");
|
output = output.replace(entry.declaration.raw, "");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
replacement = `/__wrnexus/client/${basename(dependency.browser).replace(/\.client\.mjs$/, ".mjs")}`;
|
replacement = dependency.browser;
|
||||||
} else replacement = target === "server" ? dependency.server : dependency.main;
|
} else replacement = target === "server" ? dependency.server : dependency.main;
|
||||||
}
|
}
|
||||||
const escaped = entry.declaration.source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
const escaped = entry.declaration.source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
const specifier = replacement.startsWith("/") ? replacement : pathToFileURL(replacement).href;
|
const specifier =
|
||||||
|
target === "browser"
|
||||||
|
? (() => {
|
||||||
|
const cacheDir = compileCacheDir ?? join(dirname(importer), ".wrnexus");
|
||||||
|
const relativeTarget = relative(cacheDir, replacement).replace(/\\/g, "/");
|
||||||
|
return relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`;
|
||||||
|
})()
|
||||||
|
: replacement.startsWith("/")
|
||||||
|
? replacement
|
||||||
|
: pathToFileURL(replacement).href;
|
||||||
output = output.replace(new RegExp(`(["'])${escaped}\\1`, "g"), JSON.stringify(specifier));
|
output = output.replace(new RegExp(`(["'])${escaped}\\1`, "g"), JSON.stringify(specifier));
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
@@ -257,14 +301,14 @@ export async function loadModule(file: string): Promise<Record<string, unknown>>
|
|||||||
* When unset, compilation falls back to a sibling `.wrnexus/` next to each file.
|
* When unset, compilation falls back to a sibling `.wrnexus/` next to each file.
|
||||||
*/
|
*/
|
||||||
let compileCacheDir: string | null = null;
|
let compileCacheDir: string | null = null;
|
||||||
const WRN_COMPILE_CACHE_VERSION = "v2";
|
const WRN_COMPILE_CACHE_VERSION = "v3";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Point all `.wrn` compilation at ONE cache dir (typically `<appRoot>/.wrnexus`)
|
* Point all `.wrn` compilation at ONE cache dir (typically `<appRoot>/.wrnexus`)
|
||||||
* instead of scattering a `.wrnexus/` folder next to every `.wrn` source. Called
|
* instead of scattering a `.wrnexus/` folder next to every `.wrn` source. Called
|
||||||
* once by the dev server at startup.
|
* once by the dev server at startup.
|
||||||
*/
|
*/
|
||||||
export function setCompileCacheDir(dir: string): void {
|
export function setCompileCacheDir(dir: string | null): void {
|
||||||
compileCacheDir = dir;
|
compileCacheDir = dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,18 +401,68 @@ const compileMetrics: WrnCompileMetrics = {
|
|||||||
};
|
};
|
||||||
const asyncCompileInProgress = new Map<string, Promise<WrnCompileArtifacts>>();
|
const asyncCompileInProgress = new Map<string, Promise<WrnCompileArtifacts>>();
|
||||||
|
|
||||||
|
async function bundleBrowserArtifact(
|
||||||
|
code: string,
|
||||||
|
file: string,
|
||||||
|
cacheDir: string,
|
||||||
|
stem: string,
|
||||||
|
): Promise<string> {
|
||||||
|
const hasModuleImport = /(?:^|\n)\s*import(?:\s|["'])|\bimport\s*\(/m.test(code);
|
||||||
|
if (!hasModuleImport || code.includes("wrnexus-client-bundled")) return code;
|
||||||
|
const bun = (globalThis as any).Bun;
|
||||||
|
if (!bun?.build) {
|
||||||
|
throw new Error(
|
||||||
|
`WRN-CLIENT-BUNDLE: ${file} has browser imports, but the Bun bundler is unavailable.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const entry = join(cacheDir, `${stem}.browser-entry.mjs`);
|
||||||
|
writeFileSync(entry, code, "utf8");
|
||||||
|
try {
|
||||||
|
const result = await bun.build({
|
||||||
|
entrypoints: [entry],
|
||||||
|
target: "browser",
|
||||||
|
format: "esm",
|
||||||
|
splitting: false,
|
||||||
|
minify: false,
|
||||||
|
sourcemap: "inline",
|
||||||
|
});
|
||||||
|
if (!result.success || !result.outputs?.length) {
|
||||||
|
const detail = (result.logs ?? []).map(String).join("\n");
|
||||||
|
throw new Error(`WRN-CLIENT-BUNDLE: failed to bundle ${file}${detail ? `\n${detail}` : ""}`);
|
||||||
|
}
|
||||||
|
return `// wrnexus-client-bundled\n${await result.outputs[0].text()}`;
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
unlinkSync(entry);
|
||||||
|
} catch {
|
||||||
|
// Best-effort cleanup; cache pruning removes stale temporary entries.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function compileWireArtifactsAsync(file: string, version = 0): Promise<WrnCompileArtifacts> {
|
export function compileWireArtifactsAsync(file: string, version = 0): Promise<WrnCompileArtifacts> {
|
||||||
if (!devCompilerPipeline) return Promise.resolve(compileWireArtifacts(file, version));
|
|
||||||
const key = `${file}:${version}`;
|
const key = `${file}:${version}`;
|
||||||
const active = asyncCompileInProgress.get(key);
|
const active = asyncCompileInProgress.get(key);
|
||||||
if (active) return active;
|
if (active) return active;
|
||||||
const task = (async () => {
|
const task = (async () => {
|
||||||
|
if (!devCompilerPipeline) {
|
||||||
|
const artifacts = compileWireArtifacts(file, version);
|
||||||
|
const code = readFileSync(artifacts.browser, "utf8");
|
||||||
|
const bundled = await bundleBrowserArtifact(
|
||||||
|
code,
|
||||||
|
file,
|
||||||
|
dirname(artifacts.browser),
|
||||||
|
basename(artifacts.browser, ".client.mjs"),
|
||||||
|
);
|
||||||
|
if (bundled !== code) writeFileSync(artifacts.browser, bundled, "utf8");
|
||||||
|
return artifacts;
|
||||||
|
}
|
||||||
const cacheDir = compileCacheDir ?? join(dirname(file), ".wrnexus");
|
const cacheDir = compileCacheDir ?? join(dirname(file), ".wrnexus");
|
||||||
const name = basename(file).replace(/\.wrn$/, "");
|
const name = basename(file).replace(/\.wrn$/, "");
|
||||||
const suffix = version ? `-hmr-${version}` : "";
|
const suffix = version ? `-hmr-${version}` : "";
|
||||||
const source = readFileSync(file, "utf8");
|
const source = readFileSync(file, "utf8");
|
||||||
// Plugin output affects the artifact, so use a separate cache generation.
|
// Plugin output affects the artifact, so use a separate cache generation.
|
||||||
const stem = `${name}-${WRN_COMPILE_CACHE_VERSION}-plugin-${hashPath(file)}-${hashPath(source)}${suffix}`;
|
const stem = `${name}-${WRN_COMPILE_CACHE_VERSION}-plugin-${hashPath(file)}-${hashPath(source)}-${importOptionsHash(file)}${suffix}`;
|
||||||
const artifacts: WrnCompileArtifacts = {
|
const artifacts: WrnCompileArtifacts = {
|
||||||
main: join(cacheDir, `${stem}.wrn.ts`),
|
main: join(cacheDir, `${stem}.wrn.ts`),
|
||||||
browser: join(cacheDir, `${stem}.client.mjs`),
|
browser: join(cacheDir, `${stem}.client.mjs`),
|
||||||
@@ -394,11 +488,11 @@ export function compileWireArtifactsAsync(file: string, version = 0): Promise<Wr
|
|||||||
};
|
};
|
||||||
for (const target of ["main", "browser", "server"] as const) {
|
for (const target of ["main", "browser", "server"] as const) {
|
||||||
const rewritten = await rewriteArtifactImportsAsync(outputs[target], ast, file, target);
|
const rewritten = await rewriteArtifactImportsAsync(outputs[target], ast, file, target);
|
||||||
writeFileSync(
|
let transformed = await devCompilerPipeline!.transformCode(rewritten, file);
|
||||||
artifacts[target],
|
if (target === "browser") {
|
||||||
await devCompilerPipeline!.transformCode(rewritten, file),
|
transformed = await bundleBrowserArtifact(transformed, file, cacheDir, stem);
|
||||||
"utf8",
|
}
|
||||||
);
|
writeFileSync(artifacts[target], transformed, "utf8");
|
||||||
}
|
}
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
artifacts.declarations,
|
artifacts.declarations,
|
||||||
@@ -437,7 +531,7 @@ export function compileWireArtifacts(file: string, version = 0): WrnCompileArtif
|
|||||||
const name = basename(file).replace(/\.wrn$/, "");
|
const name = basename(file).replace(/\.wrn$/, "");
|
||||||
const suffix = version ? `-hmr-${version}` : "";
|
const suffix = version ? `-hmr-${version}` : "";
|
||||||
const source = readFileSync(file, "utf8");
|
const source = readFileSync(file, "utf8");
|
||||||
const stem = `${name}-${WRN_COMPILE_CACHE_VERSION}-${hashPath(file)}-${hashPath(source)}${suffix}`;
|
const stem = `${name}-${WRN_COMPILE_CACHE_VERSION}-${hashPath(file)}-${hashPath(source)}-${importOptionsHash(file)}${suffix}`;
|
||||||
const artifacts: WrnCompileArtifacts = {
|
const artifacts: WrnCompileArtifacts = {
|
||||||
main: join(cacheDir, `${stem}.wrn.ts`),
|
main: join(cacheDir, `${stem}.wrn.ts`),
|
||||||
browser: join(cacheDir, `${stem}.client.mjs`),
|
browser: join(cacheDir, `${stem}.client.mjs`),
|
||||||
@@ -516,6 +610,12 @@ export function wrnBrowserArtifactUrl(file: string): string {
|
|||||||
return `/__wrnexus/client/${basename(artifact).replace(/\.client\.mjs$/, ".mjs")}`;
|
return `/__wrnexus/client/${basename(artifact).replace(/\.client\.mjs$/, ".mjs")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Async browser artifact URL used by HMR so imported client modules are bundled before delivery. */
|
||||||
|
export async function wrnBrowserArtifactUrlAsync(file: string): Promise<string> {
|
||||||
|
const artifact = (await compileWireArtifactsAsync(file, moduleVersions.get(file) ?? 0)).browser;
|
||||||
|
return `/__wrnexus/client/${basename(artifact).replace(/\.client\.mjs$/, ".mjs")}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function serveWrnBrowserArtifact(pathname: string): Response | null {
|
export function serveWrnBrowserArtifact(pathname: string): Response | null {
|
||||||
const artifact = browserArtifactPaths.get(pathname);
|
const artifact = browserArtifactPaths.get(pathname);
|
||||||
if (!artifact || !existsSync(artifact)) return null;
|
if (!artifact || !existsSync(artifact)) return null;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
* supervised `dev --production-runtime` mode can explicitly enable it.
|
* supervised `dev --production-runtime` mode can explicitly enable it.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { join } from "node:path";
|
||||||
import type { Middleware, Mode, SecurityConfig, SeoConfig } from "@wrnexus/core";
|
import type { Middleware, Mode, SecurityConfig, SeoConfig } from "@wrnexus/core";
|
||||||
import {
|
import {
|
||||||
compileRoutePattern,
|
compileRoutePattern,
|
||||||
@@ -88,6 +89,8 @@ export interface ProdOptions {
|
|||||||
stylesIncludeFramework?: boolean;
|
stylesIncludeFramework?: boolean;
|
||||||
/** Absolute path to the pre-built reactive runtime. */
|
/** Absolute path to the pre-built reactive runtime. */
|
||||||
reactivePath?: string;
|
reactivePath?: string;
|
||||||
|
/** Absolute directory containing bundled per-WRN browser modules. */
|
||||||
|
clientModulesDir?: string;
|
||||||
/** Absolute path to the pre-built theme stylesheet (`theme.css`). */
|
/** Absolute path to the pre-built theme stylesheet (`theme.css`). */
|
||||||
themePath?: string;
|
themePath?: string;
|
||||||
/** Absolute path to the pre-built theme runtime (`theme.js`). */
|
/** Absolute path to the pre-built theme runtime (`theme.js`). */
|
||||||
@@ -267,6 +270,13 @@ async function serveFile(path: string | undefined, headers: Record<string, strin
|
|||||||
function createProdAssetServer(opts: ProdOptions): AssetServer {
|
function createProdAssetServer(opts: ProdOptions): AssetServer {
|
||||||
return {
|
return {
|
||||||
async serve(pathname: string): Promise<Response | null> {
|
async serve(pathname: string): Promise<Response | null> {
|
||||||
|
if (pathname.startsWith("/__wrnexus/client/")) {
|
||||||
|
const name = pathname.slice("/__wrnexus/client/".length);
|
||||||
|
if (!opts.clientModulesDir || !/^[A-Za-z0-9._-]+\.mjs$/.test(name)) {
|
||||||
|
return new Response("Not Found", { status: 404 });
|
||||||
|
}
|
||||||
|
return serveFile(join(opts.clientModulesDir, name), JS_HEADERS);
|
||||||
|
}
|
||||||
if (pathname === "/__wrnexus/reactive.js") {
|
if (pathname === "/__wrnexus/reactive.js") {
|
||||||
if (opts.reactivePath) {
|
if (opts.reactivePath) {
|
||||||
const file = Bun.file(opts.reactivePath);
|
const file = Bun.file(opts.reactivePath);
|
||||||
|
|||||||
@@ -1279,11 +1279,14 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
|||||||
|
|
||||||
async function handleClientLoad(ctx: Context): Promise<Response> {
|
async function handleClientLoad(ctx: Context): Promise<Response> {
|
||||||
const routePath = ctx.url.searchParams.get("route") ?? "";
|
const routePath = ctx.url.searchParams.get("route") ?? "";
|
||||||
|
const routeSearch = ctx.url.searchParams.get("search") ?? "";
|
||||||
const name = ctx.url.searchParams.get("name") ?? "";
|
const name = ctx.url.searchParams.get("name") ?? "";
|
||||||
if (
|
if (
|
||||||
!routePath.startsWith("/") ||
|
!routePath.startsWith("/") ||
|
||||||
routePath.startsWith("/__wrnexus/") ||
|
routePath.startsWith("/__wrnexus/") ||
|
||||||
!isSafeRequestPath(routePath) ||
|
!isSafeRequestPath(routePath) ||
|
||||||
|
(routeSearch !== "" &&
|
||||||
|
(!routeSearch.startsWith("?") || routeSearch.includes("#") || routeSearch.length > 4096)) ||
|
||||||
!/^[A-Za-z_$][\w$]{0,63}$/.test(name)
|
!/^[A-Za-z_$][\w$]{0,63}$/.test(name)
|
||||||
) {
|
) {
|
||||||
return new Response("Not Found", { status: 404 });
|
return new Response("Not Found", { status: 404 });
|
||||||
@@ -1294,8 +1297,11 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
|||||||
const load = pageModule.__wrnexusClientLoad;
|
const load = pageModule.__wrnexusClientLoad;
|
||||||
if (typeof load !== "function") return new Response("Not Found", { status: 404 });
|
if (typeof load !== "function") return new Response("Not Found", { status: 404 });
|
||||||
try {
|
try {
|
||||||
ctx.params = page.params;
|
const routeUrl = new URL(routePath + routeSearch, ctx.url.origin);
|
||||||
const values = await load(ctx);
|
if (routeUrl.pathname !== routePath) return new Response("Not Found", { status: 404 });
|
||||||
|
const routeRequest = new Request(routeUrl, { method: "GET", headers: ctx.req.headers });
|
||||||
|
const loadCtx: Context = { ...ctx, req: routeRequest, url: routeUrl, params: page.params };
|
||||||
|
const values = await load(loadCtx);
|
||||||
if (!values || typeof values !== "object" || !(name in values)) {
|
if (!values || typeof values !== "object" || !(name in values)) {
|
||||||
return new Response("Not Found", { status: 404 });
|
return new Response("Not Found", { status: 404 });
|
||||||
}
|
}
|
||||||
@@ -1627,6 +1633,10 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
|||||||
if (partial) {
|
if (partial) {
|
||||||
body = typeof precomputedShell === "string" ? precomputedShell : (pagePartial?.shell ?? body);
|
body = typeof precomputedShell === "string" ? precomputedShell : (pagePartial?.shell ?? body);
|
||||||
}
|
}
|
||||||
|
const routeParamsMarker = `<span hidden data-wrn-route-params="${escapeHtml(
|
||||||
|
JSON.stringify(ctx.params ?? {}),
|
||||||
|
)}"></span>`;
|
||||||
|
body = `${routeParamsMarker}${body}`;
|
||||||
if (body.includes("data-wrn-action=")) {
|
if (body.includes("data-wrn-action=")) {
|
||||||
body = body.replace(
|
body = body.replace(
|
||||||
/(<form\b[^>]*\bdata-wrn-action=(?:"[^"]+"|'[^']+')[^>]*>)/gi,
|
/(<form\b[^>]*\bdata-wrn-action=(?:"[^"]+"|'[^']+')[^>]*>)/gi,
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import {
|
||||||
|
compileWireArtifactsAsync,
|
||||||
|
setCompileCacheDir,
|
||||||
|
setCompileImportOptions,
|
||||||
|
} from "../src/pipeline.ts";
|
||||||
|
|
||||||
|
test("development browser artifacts bundle local client imports", async () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "wrnexus-client-bundle-"));
|
||||||
|
const app = join(root, "app");
|
||||||
|
const cache = join(root, ".wrnexus");
|
||||||
|
mkdirSync(app, { recursive: true });
|
||||||
|
writeFileSync(join(app, "helper.ts"), "export function save(value: unknown) { return value; }\n");
|
||||||
|
const page = join(app, "settings.wrn");
|
||||||
|
writeFileSync(
|
||||||
|
page,
|
||||||
|
`import { save } from "./helper.ts"
|
||||||
|
page Settings {
|
||||||
|
state { value = 1 }
|
||||||
|
functions { client function persist() { return save(value) } }
|
||||||
|
view { <button @click='persist()'>Save</button> }
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
setCompileCacheDir(cache);
|
||||||
|
const artifacts = await compileWireArtifactsAsync(page);
|
||||||
|
const browser = readFileSync(artifacts.browser, "utf8");
|
||||||
|
expect(browser).toContain("wrnexus-client-bundled");
|
||||||
|
expect(browser).not.toContain("file://");
|
||||||
|
expect(browser).not.toContain('from "./helper.ts"');
|
||||||
|
} finally {
|
||||||
|
setCompileCacheDir(null);
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("development browser artifacts bundle configured alias imports", async () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "wrnexus-client-alias-bundle-"));
|
||||||
|
const app = join(root, "app");
|
||||||
|
const cache = join(root, ".wrnexus");
|
||||||
|
mkdirSync(join(app, "client"), { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
join(app, "client", "storage.ts"),
|
||||||
|
"export function persist(value: unknown) { return value; }\n",
|
||||||
|
);
|
||||||
|
const page = join(app, "settings.wrn");
|
||||||
|
writeFileSync(
|
||||||
|
page,
|
||||||
|
`import { persist } from "~/client/storage.ts"
|
||||||
|
page Settings {
|
||||||
|
state { value = 1 }
|
||||||
|
functions { client function save() { return persist(value) } }
|
||||||
|
view { <button @click='save()'>Save</button> }
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
setCompileCacheDir(cache);
|
||||||
|
setCompileImportOptions(root, {
|
||||||
|
mode: "explicit",
|
||||||
|
aliases: { "~": "./app" },
|
||||||
|
autoImport: false,
|
||||||
|
});
|
||||||
|
const artifacts = await compileWireArtifactsAsync(page);
|
||||||
|
const browser = readFileSync(artifacts.browser, "utf8");
|
||||||
|
expect(browser).toContain("wrnexus-client-bundled");
|
||||||
|
expect(browser).not.toContain("~/client/storage.ts");
|
||||||
|
expect(browser).not.toContain("file://");
|
||||||
|
|
||||||
|
mkdirSync(join(root, "alternate", "client"), { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
join(root, "alternate", "client", "storage.ts"),
|
||||||
|
"export function persist(value: unknown) { return value; }\n",
|
||||||
|
);
|
||||||
|
setCompileImportOptions(root, {
|
||||||
|
mode: "explicit",
|
||||||
|
aliases: { "~": "./alternate" },
|
||||||
|
autoImport: false,
|
||||||
|
});
|
||||||
|
const changed = await compileWireArtifactsAsync(page);
|
||||||
|
expect(changed.browser).not.toBe(artifacts.browser);
|
||||||
|
} finally {
|
||||||
|
setCompileCacheDir(null);
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -62,7 +62,9 @@ test("production streams request regions into the build-time static shell", asyn
|
|||||||
test("normal production output remains free of development HMR", async () => {
|
test("normal production output remains free of development HMR", async () => {
|
||||||
const handlers = createProductionHandlers(manifest, {});
|
const handlers = createProductionHandlers(manifest, {});
|
||||||
const response = await handlers.fetch(new Request("http://localhost/"), server);
|
const response = await handlers.fetch(new Request("http://localhost/"), server);
|
||||||
expect(await (response as Response).text()).not.toContain("/__wrnexus/hmr");
|
const html = await (response as Response).text();
|
||||||
|
expect(html).not.toContain("/__wrnexus/hmr");
|
||||||
|
expect(html).toContain('data-wrn-route-params="{}"');
|
||||||
});
|
});
|
||||||
|
|
||||||
test("production client-load endpoint returns only the requested named result", async () => {
|
test("production client-load endpoint returns only the requested named result", async () => {
|
||||||
@@ -121,3 +123,43 @@ test("production prefers fast gzip for dynamic HTML and cached Brotli for immuta
|
|||||||
)) as Response;
|
)) as Response;
|
||||||
expect(head.headers.get("content-encoding")).toBeNull();
|
expect(head.headers.get("content-encoding")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("client-load preserves the active route query string", async () => {
|
||||||
|
const queryManifest: ProdManifest = {
|
||||||
|
...manifest,
|
||||||
|
pages: [
|
||||||
|
...manifest.pages,
|
||||||
|
{
|
||||||
|
raw: "/query",
|
||||||
|
mod: {
|
||||||
|
default: () => "<main>Query page</main>",
|
||||||
|
__wrnexusClientLoad: async (ctx: { url: URL }) => ({
|
||||||
|
identity: ctx.url.searchParams.get("as"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const handlers = createProductionHandlers(queryManifest, {});
|
||||||
|
const response = (await handlers.fetch(
|
||||||
|
new Request(
|
||||||
|
"http://localhost/__wrnexus/client-load?route=%2Fquery&search=%3Fas%3Drohan&name=identity",
|
||||||
|
),
|
||||||
|
server,
|
||||||
|
)) as Response;
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(await response.json()).toEqual({ data: "rohan" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("production serves generated WRN browser modules from the client directory", async () => {
|
||||||
|
const directory = mkdtempSync(join(tmpdir(), "wrnexus-client-modules-"));
|
||||||
|
writeFileSync(join(directory, "settings-abc123.mjs"), "export const ready = true;\n");
|
||||||
|
const handlers = createProductionHandlers(manifest, { clientModulesDir: directory });
|
||||||
|
const response = (await handlers.fetch(
|
||||||
|
new Request("http://localhost/__wrnexus/client/settings-abc123.mjs"),
|
||||||
|
server,
|
||||||
|
)) as Response;
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.headers.get("content-type")).toContain("text/javascript");
|
||||||
|
expect(await response.text()).toContain("ready = true");
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/dev-toolbar",
|
"name": "@wrnexus/dev-toolbar",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/encryption",
|
"name": "@wrnexus/encryption",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/graphql",
|
"name": "@wrnexus/graphql",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/helpers",
|
"name": "@wrnexus/helpers",
|
||||||
"version": "0.8.2",
|
"version": "0.8.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
|
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Badge, Card } from "@wrnexus/ui"
|
||||||
|
|
||||||
component LocaleStatus {
|
component LocaleStatus {
|
||||||
props {
|
props {
|
||||||
locale: string = "en"
|
locale: string = "en"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user