release: WRNexusJS 0.2.31
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ai",
|
"name": "@wrnexus/ai",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"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.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/authz — part of the WrNexus framework.",
|
"description": "@wrnexus/authz — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
+11
-11
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/cli",
|
"name": "@wrnexus/cli",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/cli — part of the WrNexus framework.",
|
"description": "@wrnexus/cli — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -28,16 +28,16 @@
|
|||||||
"wrnexus": "./dist/index.js"
|
"wrnexus": "./dist/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.2.30",
|
"@wrnexus/core": "^0.2.31",
|
||||||
"@wrnexus/router": "^0.2.30",
|
"@wrnexus/router": "^0.2.31",
|
||||||
"@wrnexus/csr": "^0.2.30",
|
"@wrnexus/csr": "^0.2.31",
|
||||||
"@wrnexus/compiler": "^0.2.30",
|
"@wrnexus/compiler": "^0.2.31",
|
||||||
"@wrnexus/styles": "^0.2.30",
|
"@wrnexus/styles": "^0.2.31",
|
||||||
"@wrnexus/dev-server": "^0.2.30",
|
"@wrnexus/dev-server": "^0.2.31",
|
||||||
"@wrnexus/ui": "^0.2.30",
|
"@wrnexus/ui": "^0.2.31",
|
||||||
"@wrnexus/validation": "^0.2.30",
|
"@wrnexus/validation": "^0.2.31",
|
||||||
"@wrnexus/i18n": "^0.2.30",
|
"@wrnexus/i18n": "^0.2.31",
|
||||||
"@wrnexus/db": "^0.2.30"
|
"@wrnexus/db": "^0.2.31"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/compiler",
|
"name": "@wrnexus/compiler",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/compiler — part of the WrNexus framework.",
|
"description": "@wrnexus/compiler — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/core",
|
"name": "@wrnexus/core",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"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.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/csr — part of the WrNexus framework.",
|
"description": "@wrnexus/csr — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.2.30"
|
"@wrnexus/core": "^0.2.31"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/db",
|
"name": "@wrnexus/db",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/db — part of the WrNexus framework.",
|
"description": "@wrnexus/db — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/dev-server",
|
"name": "@wrnexus/dev-server",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"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",
|
||||||
@@ -25,18 +25,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.2.30",
|
"@wrnexus/core": "^0.2.31",
|
||||||
"@wrnexus/router": "^0.2.30",
|
"@wrnexus/router": "^0.2.31",
|
||||||
"@wrnexus/ssr": "^0.2.30",
|
"@wrnexus/ssr": "^0.2.31",
|
||||||
"@wrnexus/csr": "^0.2.30",
|
"@wrnexus/csr": "^0.2.31",
|
||||||
"@wrnexus/compiler": "^0.2.30",
|
"@wrnexus/compiler": "^0.2.31",
|
||||||
"@wrnexus/styles": "^0.2.30",
|
"@wrnexus/styles": "^0.2.31",
|
||||||
"@wrnexus/ui": "^0.2.30",
|
"@wrnexus/ui": "^0.2.31",
|
||||||
"@wrnexus/validation": "^0.2.30",
|
"@wrnexus/validation": "^0.2.31",
|
||||||
"@wrnexus/i18n": "^0.2.30",
|
"@wrnexus/i18n": "^0.2.31",
|
||||||
"@wrnexus/db": "^0.2.30",
|
"@wrnexus/db": "^0.2.31",
|
||||||
"@wrnexus/pubsub": "^0.2.30",
|
"@wrnexus/pubsub": "^0.2.31",
|
||||||
"@wrnexus/uploader": "^0.2.30"
|
"@wrnexus/uploader": "^0.2.31"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/encryption",
|
"name": "@wrnexus/encryption",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/encryption — part of the WrNexus framework.",
|
"description": "@wrnexus/encryption — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/helpers",
|
"name": "@wrnexus/helpers",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"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",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.2.30"
|
"@wrnexus/core": "^0.2.31"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/i18n",
|
"name": "@wrnexus/i18n",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/i18n — part of the WrNexus framework.",
|
"description": "@wrnexus/i18n — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.2.30"
|
"@wrnexus/core": "^0.2.31"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/jwt",
|
"name": "@wrnexus/jwt",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/jwt — part of the WrNexus framework.",
|
"description": "@wrnexus/jwt — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/mobile",
|
"name": "@wrnexus/mobile",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/mobile — part of the WrNexus framework.",
|
"description": "@wrnexus/mobile — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/native": "^0.2.30"
|
"@wrnexus/native": "^0.2.31"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/native",
|
"name": "@wrnexus/native",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"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.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/oauth — part of the WrNexus framework.",
|
"description": "@wrnexus/oauth — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/pubsub",
|
"name": "@wrnexus/pubsub",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"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.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/queue — part of the WrNexus framework.",
|
"description": "@wrnexus/queue — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/reactive",
|
"name": "@wrnexus/reactive",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"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.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/router — part of the WrNexus framework.",
|
"description": "@wrnexus/router — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21,8 +21,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/compiler": "^0.2.30",
|
"@wrnexus/compiler": "^0.2.31",
|
||||||
"@wrnexus/core": "^0.2.30"
|
"@wrnexus/core": "^0.2.31"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ssr",
|
"name": "@wrnexus/ssr",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/ssr — part of the WrNexus framework.",
|
"description": "@wrnexus/ssr — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.2.30"
|
"@wrnexus/core": "^0.2.31"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/styles",
|
"name": "@wrnexus/styles",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/styles — part of the WrNexus framework.",
|
"description": "@wrnexus/styles — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/uploader": "^0.2.30"
|
"@wrnexus/uploader": "^0.2.31"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/test",
|
"name": "@wrnexus/test",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"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.2.30",
|
"version": "0.2.31",
|
||||||
"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.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/ui — part of the WrNexus framework.",
|
"description": "@wrnexus/ui — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"./ui.css": "./ui.css"
|
"./ui.css": "./ui.css"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.2.30"
|
"@wrnexus/core": "^0.2.31"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/uploader",
|
"name": "@wrnexus/uploader",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/uploader — part of the WrNexus framework.",
|
"description": "@wrnexus/uploader — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.2.30"
|
"@wrnexus/core": "^0.2.31"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/validation",
|
"name": "@wrnexus/validation",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/validation — part of the WrNexus framework.",
|
"description": "@wrnexus/validation — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ai",
|
"name": "@wrnexus/ai",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/authz",
|
"name": "@wrnexus/authz",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/cli",
|
"name": "@wrnexus/cli",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -348,6 +348,15 @@ const MIGRATIONS: Migration[] = [
|
|||||||
// No generated application files require automatic migration.
|
// No generated application files require automatic migration.
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
version: "0.2.31",
|
||||||
|
id: "lifecycle-improvements",
|
||||||
|
description:
|
||||||
|
"Adds component functions, lifecycle hooks, state watchers, optimized behavior cleanup, and CSR navigation lifecycle support.",
|
||||||
|
apply() {
|
||||||
|
// No generated application files require automatic migration.
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/** 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.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -78,8 +78,22 @@ function renderAttr(attr: Attr): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function eventAttribute(name: string): string {
|
function eventAttribute(name: string): string {
|
||||||
if (name.startsWith("browser-")) return `data-on-wrnexus-browser-${name.slice(8)}`;
|
if (name.startsWith("window:")) {
|
||||||
if (name.startsWith("mobile-")) return `data-on-wrnexus-mobile-${name.slice(7)}`;
|
return `data-on-window-${name.slice("window:".length)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.startsWith("document:")) {
|
||||||
|
return `data-on-document-${name.slice("document:".length)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.startsWith("browser-")) {
|
||||||
|
return `data-on-wrnexus-browser-${name.slice(8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.startsWith("mobile-")) {
|
||||||
|
return `data-on-wrnexus-mobile-${name.slice(7)}`;
|
||||||
|
}
|
||||||
|
|
||||||
return `data-on-${name}`;
|
return `data-on-${name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,7 +451,9 @@ function renderNestedComponentInvocation(
|
|||||||
.filter((attr) => attr.name !== "data-component")
|
.filter((attr) => attr.name !== "data-component")
|
||||||
.map((attr) => {
|
.map((attr) => {
|
||||||
if (attr.event) {
|
if (attr.event) {
|
||||||
return ` ${eventAttribute(attr.name)}="${compileAttrValue(attr.value, ctx)}"`;
|
return (
|
||||||
|
escLit(` ${eventAttribute(attr.name)}="`) + escLit(attrEscape(attr.value)) + escLit(`"`)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attr.boolean) {
|
if (attr.boolean) {
|
||||||
@@ -790,6 +806,19 @@ interface CompCtx {
|
|||||||
loopVars?: Set<string>;
|
loopVars?: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ComponentBehavior {
|
||||||
|
functions: string;
|
||||||
|
lifecycle: {
|
||||||
|
mount?: string;
|
||||||
|
update?: string;
|
||||||
|
unmount?: string;
|
||||||
|
};
|
||||||
|
watches: Array<{
|
||||||
|
state: string;
|
||||||
|
body: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
/** Parse a `data-for="item in list"` / `"item, i in list"` directive value. */
|
/** Parse a `data-for="item in list"` / `"item, i in list"` directive value. */
|
||||||
export function parseForExpr(value: string): { item: string; index?: string; list: string } | null {
|
export function parseForExpr(value: string): { item: string; index?: string; list: string } | null {
|
||||||
const m = /^\s*([A-Za-z_$][\w$]*)\s*(?:,\s*([A-Za-z_$][\w$]*)\s*)?\s+in\s+([\s\S]+?)\s*$/.exec(
|
const m = /^\s*([A-Za-z_$][\w$]*)\s*(?:,\s*([A-Za-z_$][\w$]*)\s*)?\s+in\s+([\s\S]+?)\s*$/.exec(
|
||||||
@@ -861,6 +890,44 @@ function escLit(s: string): string {
|
|||||||
return s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
return s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function componentBehavior(ast: PageAst): ComponentBehavior | null {
|
||||||
|
const functions = ast.functions
|
||||||
|
.map((body) => body.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n\n");
|
||||||
|
|
||||||
|
const lifecycle = {
|
||||||
|
...(ast.lifecycle.mount?.trim() ? { mount: ast.lifecycle.mount.trim() } : {}),
|
||||||
|
|
||||||
|
...(ast.lifecycle.update?.trim() ? { update: ast.lifecycle.update.trim() } : {}),
|
||||||
|
|
||||||
|
...(ast.lifecycle.unmount?.trim() ? { unmount: ast.lifecycle.unmount.trim() } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const watches = ast.watches.map((watch) => ({
|
||||||
|
state: watch.state,
|
||||||
|
body: watch.body.trim(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!functions && Object.keys(lifecycle).length === 0 && watches.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
functions,
|
||||||
|
lifecycle,
|
||||||
|
watches,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function behaviorAttribute(behavior: ComponentBehavior | null): string {
|
||||||
|
if (!behavior) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return ` data-wrn-behavior="${attrEscape(JSON.stringify(behavior))}"`;
|
||||||
|
}
|
||||||
|
|
||||||
const INTERP_RE = /\{([^{}]+)\}/g;
|
const INTERP_RE = /\{([^{}]+)\}/g;
|
||||||
|
|
||||||
function exprRefsState(expr: string, stateNames: Set<string>): boolean {
|
function exprRefsState(expr: string, stateNames: Set<string>): boolean {
|
||||||
@@ -971,7 +1038,7 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
|
|||||||
.filter((a) => a.name !== "class" && !a.name.startsWith("class:"))
|
.filter((a) => a.name !== "class" && !a.name.startsWith("class:"))
|
||||||
.map((a) => {
|
.map((a) => {
|
||||||
if (a.event) {
|
if (a.event) {
|
||||||
return ` ${eventAttribute(a.name)}="${compileAttrValue(a.value, ctx)}"`;
|
return ` ${eventAttribute(a.name)}="${escLit(attrEscape(a.value))}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (a.boolean) {
|
if (a.boolean) {
|
||||||
@@ -1066,12 +1133,17 @@ function generateComponent(ast: PageAst): string {
|
|||||||
// A component needs a reactive scope only when it has state or event handlers.
|
// A component needs a reactive scope only when it has state or event handlers.
|
||||||
// Prop-driven text/attributes are baked server-side, so static components ship
|
// Prop-driven text/attributes are baked server-side, so static components ship
|
||||||
// no JavaScript at all.
|
// no JavaScript at all.
|
||||||
const needsScope = ast.states.length > 0 || viewHasEvents(ast.view);
|
const behavior = componentBehavior(ast);
|
||||||
|
|
||||||
|
const needsScope = ast.states.length > 0 || viewHasEvents(ast.view) || behavior !== null;
|
||||||
|
|
||||||
const scopeKeys = [
|
const scopeKeys = [
|
||||||
...effectiveProps.map((prop) => prop.name),
|
...effectiveProps.map((prop) => prop.name),
|
||||||
...ast.states.map((state) => state.name),
|
...ast.states.map((state) => state.name),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const behaviorAttr = behaviorAttribute(behavior);
|
||||||
|
|
||||||
const decls: string[] = [];
|
const decls: string[] = [];
|
||||||
for (const prop of effectiveProps) {
|
for (const prop of effectiveProps) {
|
||||||
decls.push(
|
decls.push(
|
||||||
@@ -1083,7 +1155,7 @@ function generateComponent(ast: PageAst): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const returnExpr = needsScope
|
const returnExpr = needsScope
|
||||||
? "`" + styleTag + '<div data-scope="${__scope}">' + viewCode + "</div>`"
|
? "`" + styleTag + `<div data-scope="\${__scope}"${behaviorAttr}>` + viewCode + "</div>`"
|
||||||
: "`" + styleTag + viewCode + "`";
|
: "`" + styleTag + viewCode + "`";
|
||||||
|
|
||||||
const scopeLine =
|
const scopeLine =
|
||||||
@@ -1098,6 +1170,11 @@ function generateComponent(ast: PageAst): string {
|
|||||||
} else {
|
} else {
|
||||||
out.push(`export const __wrnexusComponent = ${JSON.stringify(ast.name)};`);
|
out.push(`export const __wrnexusComponent = ${JSON.stringify(ast.name)};`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (behavior) {
|
||||||
|
out.push(`export const __wrnexusBehavior = ${JSON.stringify(behavior, null, 2)};`);
|
||||||
|
}
|
||||||
|
|
||||||
out.push(`function __coerce(v: any, def: any): any {
|
out.push(`function __coerce(v: any, def: any): any {
|
||||||
if (v === undefined || v === null) return def;
|
if (v === undefined || v === null) return def;
|
||||||
if (typeof def === "number") return Number(v);
|
if (typeof def === "number") return Number(v);
|
||||||
|
|||||||
@@ -105,6 +105,19 @@ export interface ModeFunctionsBlock {
|
|||||||
body: string;
|
body: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LifecycleHookName = "mount" | "update" | "unmount";
|
||||||
|
|
||||||
|
export interface LifecycleBlock {
|
||||||
|
mount?: string;
|
||||||
|
update?: string;
|
||||||
|
unmount?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WatchBlock {
|
||||||
|
state: string;
|
||||||
|
body: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RealtimeHandler {
|
export interface RealtimeHandler {
|
||||||
event: string;
|
event: string;
|
||||||
args: string[];
|
args: string[];
|
||||||
@@ -141,6 +154,8 @@ export interface PageAst {
|
|||||||
functions: string[];
|
functions: string[];
|
||||||
dataApis: DataApiBlock[];
|
dataApis: DataApiBlock[];
|
||||||
modeFunctions: ModeFunctionsBlock[];
|
modeFunctions: ModeFunctionsBlock[];
|
||||||
|
lifecycle: LifecycleBlock;
|
||||||
|
watches: WatchBlock[];
|
||||||
apis: ApiBlock[];
|
apis: ApiBlock[];
|
||||||
realtimes: RealtimeBlock[];
|
realtimes: RealtimeBlock[];
|
||||||
}
|
}
|
||||||
@@ -210,6 +225,8 @@ export function parse(source: string): PageAst {
|
|||||||
const functions: string[] = [];
|
const functions: string[] = [];
|
||||||
const dataApis: DataApiBlock[] = [];
|
const dataApis: DataApiBlock[] = [];
|
||||||
const modeFunctions: ModeFunctionsBlock[] = [];
|
const modeFunctions: ModeFunctionsBlock[] = [];
|
||||||
|
const lifecycle: LifecycleBlock = {};
|
||||||
|
const watches: WatchBlock[] = [];
|
||||||
const apis: ApiBlock[] = [];
|
const apis: ApiBlock[] = [];
|
||||||
const realtimes: RealtimeBlock[] = [];
|
const realtimes: RealtimeBlock[] = [];
|
||||||
|
|
||||||
@@ -339,6 +356,52 @@ export function parse(source: string): PageAst {
|
|||||||
styles.push(lx.readBalancedBraces());
|
styles.push(lx.readBalancedBraces());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "lifecycle": {
|
||||||
|
lx.next();
|
||||||
|
expect("lbrace");
|
||||||
|
|
||||||
|
while (lx.peek().type !== "rbrace") {
|
||||||
|
const hook = lx.peek();
|
||||||
|
|
||||||
|
if (hook.type === "eof") {
|
||||||
|
throw new ParseError("Unexpected end of input inside lifecycle block");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hook.type !== "ident") {
|
||||||
|
throw new ParseError(`Expected a lifecycle hook at offset ${hook.pos}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hook.value !== "mount" && hook.value !== "update" && hook.value !== "unmount") {
|
||||||
|
throw new ParseError(`Unknown lifecycle hook '${hook.value}' at offset ${hook.pos}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hookName = hook.value as LifecycleHookName;
|
||||||
|
|
||||||
|
lx.next();
|
||||||
|
|
||||||
|
if (lifecycle[hookName] !== undefined) {
|
||||||
|
throw new ParseError(`Duplicate lifecycle hook '${hookName}' at offset ${hook.pos}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycle[hookName] = lx.readBalancedBraces();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect("rbrace");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "watch": {
|
||||||
|
lx.next();
|
||||||
|
|
||||||
|
const stateName = expect("ident").value;
|
||||||
|
const body = lx.readBalancedBraces();
|
||||||
|
|
||||||
|
watches.push({
|
||||||
|
state: stateName,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "functions": {
|
case "functions": {
|
||||||
lx.next();
|
lx.next();
|
||||||
functions.push(lx.readBalancedBraces());
|
functions.push(lx.readBalancedBraces());
|
||||||
@@ -350,6 +413,14 @@ export function parse(source: string): PageAst {
|
|||||||
}
|
}
|
||||||
expect("rbrace");
|
expect("rbrace");
|
||||||
|
|
||||||
|
const declaredStates = new Set(states.map((state) => state.name));
|
||||||
|
|
||||||
|
for (const watcher of watches) {
|
||||||
|
if (!declaredStates.has(watcher.state)) {
|
||||||
|
throw new ParseError(`Cannot watch undeclared state '${watcher.state}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "page",
|
type: "page",
|
||||||
kind,
|
kind,
|
||||||
@@ -363,6 +434,8 @@ export function parse(source: string): PageAst {
|
|||||||
functions,
|
functions,
|
||||||
dataApis,
|
dataApis,
|
||||||
modeFunctions,
|
modeFunctions,
|
||||||
|
lifecycle,
|
||||||
|
watches,
|
||||||
apis,
|
apis,
|
||||||
realtimes,
|
realtimes,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -243,3 +243,196 @@ layout AppLayout {
|
|||||||
|
|
||||||
expect(output).toContain("export function render");
|
expect(output).toContain("export function render");
|
||||||
});
|
});
|
||||||
|
test("parses component lifecycle hooks", () => {
|
||||||
|
const ast = parse(`
|
||||||
|
component BackToTop {
|
||||||
|
state visible = false
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
mount {
|
||||||
|
visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
update {
|
||||||
|
console.log(visible)
|
||||||
|
}
|
||||||
|
|
||||||
|
unmount {
|
||||||
|
visible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view {
|
||||||
|
<button>Top</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(ast.lifecycle.mount).toContain("visible = true");
|
||||||
|
|
||||||
|
expect(ast.lifecycle.update).toContain("console.log(visible)");
|
||||||
|
|
||||||
|
expect(ast.lifecycle.unmount).toContain("visible = false");
|
||||||
|
});
|
||||||
|
test("parses state watchers", () => {
|
||||||
|
const ast = parse(`
|
||||||
|
component Menu {
|
||||||
|
state open = false
|
||||||
|
|
||||||
|
watch open {
|
||||||
|
console.log("Open changed", open)
|
||||||
|
}
|
||||||
|
|
||||||
|
view {
|
||||||
|
<button>{open}</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(ast.watches).toEqual([
|
||||||
|
{
|
||||||
|
state: "open",
|
||||||
|
body: expect.stringContaining('console.log("Open changed", open)'),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
test("rejects watching an undeclared state", () => {
|
||||||
|
expect(() =>
|
||||||
|
parse(`
|
||||||
|
component Broken {
|
||||||
|
watch missing {
|
||||||
|
console.log(missing)
|
||||||
|
}
|
||||||
|
|
||||||
|
view {
|
||||||
|
<div>Broken</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
).toThrow("Cannot watch undeclared state 'missing'");
|
||||||
|
});
|
||||||
|
test("rejects unknown lifecycle hooks", () => {
|
||||||
|
expect(() =>
|
||||||
|
parse(`
|
||||||
|
component Broken {
|
||||||
|
lifecycle {
|
||||||
|
created {
|
||||||
|
console.log("invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view {
|
||||||
|
<div>Broken</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
).toThrow("Unknown lifecycle hook 'created'");
|
||||||
|
});
|
||||||
|
test("component functions compile into browser behavior metadata", () => {
|
||||||
|
const output = generate(
|
||||||
|
parse(`
|
||||||
|
component Counter {
|
||||||
|
state count = 0
|
||||||
|
|
||||||
|
functions {
|
||||||
|
function increment() {
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view {
|
||||||
|
<button @click="increment()">
|
||||||
|
{count}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(output).toContain("data-wrn-behavior");
|
||||||
|
|
||||||
|
expect(output).toContain("function increment()");
|
||||||
|
|
||||||
|
expect(output).toContain("export const __wrnexusBehavior");
|
||||||
|
});
|
||||||
|
test("component lifecycle hooks compile into behavior metadata", () => {
|
||||||
|
const output = generate(
|
||||||
|
parse(`
|
||||||
|
component LifecycleTest {
|
||||||
|
state ready = false
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
mount {
|
||||||
|
ready = true
|
||||||
|
}
|
||||||
|
|
||||||
|
update {
|
||||||
|
console.log(ready)
|
||||||
|
}
|
||||||
|
|
||||||
|
unmount {
|
||||||
|
ready = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view {
|
||||||
|
<div>{ready}</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(output).toContain(""mount"");
|
||||||
|
|
||||||
|
expect(output).toContain(""update"");
|
||||||
|
|
||||||
|
expect(output).toContain(""unmount"");
|
||||||
|
});
|
||||||
|
test("component watchers compile into behavior metadata", () => {
|
||||||
|
const output = generate(
|
||||||
|
parse(`
|
||||||
|
component WatchTest {
|
||||||
|
state visible = false
|
||||||
|
|
||||||
|
watch visible {
|
||||||
|
console.log("changed", visible)
|
||||||
|
}
|
||||||
|
|
||||||
|
view {
|
||||||
|
<div>{visible}</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(output).toContain(""state":"visible"");
|
||||||
|
|
||||||
|
expect(output).toContain("console.log");
|
||||||
|
});
|
||||||
|
test("behavior-only components still receive a reactive scope", () => {
|
||||||
|
const output = generate(
|
||||||
|
parse(`
|
||||||
|
component BehaviorOnly {
|
||||||
|
functions {
|
||||||
|
function sayHello() {
|
||||||
|
console.log("hello")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
mount {
|
||||||
|
sayHello()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view {
|
||||||
|
<div>Hello</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(output).toContain("data-scope");
|
||||||
|
|
||||||
|
expect(output).toContain("data-wrn-behavior");
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/core",
|
"name": "@wrnexus/core",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/csr",
|
"name": "@wrnexus/csr",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
+375
-81
@@ -4,127 +4,421 @@
|
|||||||
* Progressive enhancement over normal links: intercepts same-origin `<a>`
|
* Progressive enhancement over normal links: intercepts same-origin `<a>`
|
||||||
* clicks, fetches the target page's HTML, swaps the `#app` container in place,
|
* clicks, fetches the target page's HTML, swaps the `#app` container in place,
|
||||||
* updates history/title/scroll, ensures any framework runtimes the new page
|
* updates history/title/scroll, ensures any framework runtimes the new page
|
||||||
* needs are present, and re-hydrates. Anything unexpected (cross-origin,
|
* needs are present, and re-hydrates.
|
||||||
* modified click, non-HTML response, missing `#app`) falls back to a full
|
|
||||||
* browser navigation, so behaviour degrades safely.
|
|
||||||
*
|
*
|
||||||
* Data "loaders": pages load their data on the server (SSR `api` bindings), so
|
* Before replacing the current page, component lifecycle behaviors are
|
||||||
* the fetched HTML already contains fresh data — no separate client loader is
|
* explicitly disposed. This ensures `unmount` hooks and watcher cleanups run
|
||||||
* needed. Client-side (`csr`) bindings and reactive scopes re-hydrate after the
|
* before the old DOM is removed.
|
||||||
* swap. Programmatic navigation is exposed as `window.__wrnexusNavigate(url)`.
|
*
|
||||||
|
* Programmatic navigation is exposed as:
|
||||||
|
*
|
||||||
|
* window.__wrnexusNavigate(url)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const NAV_RUNTIME = String.raw`
|
export const NAV_RUNTIME = String.raw`
|
||||||
(function () {
|
(function () {
|
||||||
if (!window.history || !history.pushState || !window.fetch || !window.DOMParser) return;
|
if (
|
||||||
if (window.__wrnexusNavInstalled) return;
|
!window.history ||
|
||||||
|
!history.pushState ||
|
||||||
|
!window.fetch ||
|
||||||
|
!window.DOMParser
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.__wrnexusNavInstalled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
window.__wrnexusNavInstalled = true;
|
window.__wrnexusNavInstalled = true;
|
||||||
|
|
||||||
var APP_ID = "app";
|
var APP_ID = "app";
|
||||||
|
var inFlight = null;
|
||||||
|
|
||||||
function pathOf(src) { return String(src).split("?")[0]; }
|
function pathOf(src) {
|
||||||
|
return String(src).split("?")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLocalLink(anchor) {
|
||||||
|
if (
|
||||||
|
!anchor ||
|
||||||
|
anchor.hasAttribute("download") ||
|
||||||
|
anchor.hasAttribute("data-no-nav")
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anchor.target && anchor.target !== "_self") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anchor.origin !== location.origin) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var href = anchor.getAttribute("href");
|
||||||
|
|
||||||
|
if (!href || href.charAt(0) === "#") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rel = (
|
||||||
|
anchor.getAttribute("rel") || ""
|
||||||
|
).toLowerCase();
|
||||||
|
|
||||||
function isLocalLink(a) {
|
|
||||||
if (!a || a.hasAttribute("download") || a.hasAttribute("data-no-nav")) return false;
|
|
||||||
if (a.target && a.target !== "_self") return false;
|
|
||||||
if (a.origin !== location.origin) return false;
|
|
||||||
var href = a.getAttribute("href");
|
|
||||||
if (!href || href.charAt(0) === "#") return false;
|
|
||||||
var rel = (a.getAttribute("rel") || "").toLowerCase();
|
|
||||||
return rel.indexOf("external") === -1;
|
return rel.indexOf("external") === -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadedScriptPaths() {
|
function loadedScriptPaths() {
|
||||||
var set = {};
|
var loaded = {};
|
||||||
document.querySelectorAll("script[src]").forEach(function (s) {
|
|
||||||
var src = s.getAttribute("src");
|
document
|
||||||
if (src) set[pathOf(src)] = true;
|
.querySelectorAll("script[src]")
|
||||||
});
|
.forEach(function (script) {
|
||||||
return set;
|
var src = script.getAttribute("src");
|
||||||
|
|
||||||
|
if (src) {
|
||||||
|
loaded[pathOf(src)] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return loaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append any /__wrnexus/* runtime the incoming page declares but the current
|
/**
|
||||||
// document has not loaded yet. Fresh scripts self-initialise on load.
|
* Append framework runtimes declared by the incoming document but not
|
||||||
|
* currently loaded.
|
||||||
|
*/
|
||||||
function ensureScripts(doc) {
|
function ensureScripts(doc) {
|
||||||
var loaded = loadedScriptPaths();
|
var loaded = loadedScriptPaths();
|
||||||
doc.querySelectorAll("script[src]").forEach(function (s) {
|
|
||||||
var src = s.getAttribute("src");
|
doc
|
||||||
if (!src || loaded[pathOf(src)]) return;
|
.querySelectorAll("script[src]")
|
||||||
loaded[pathOf(src)] = true;
|
.forEach(function (script) {
|
||||||
var el = document.createElement("script");
|
var src = script.getAttribute("src");
|
||||||
el.src = src;
|
|
||||||
el.async = false; // preserve execution order (e.g. schemas.js before validate.js)
|
if (!src || loaded[pathOf(src)]) {
|
||||||
document.body.appendChild(el);
|
return;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
loaded[pathOf(src)] = true;
|
||||||
|
|
||||||
|
var element =
|
||||||
|
document.createElement("script");
|
||||||
|
|
||||||
|
element.src = src;
|
||||||
|
element.async = false;
|
||||||
|
|
||||||
|
document.body.appendChild(element);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-hydrate already-loaded runtimes against the swapped-in DOM. Every
|
/**
|
||||||
// entrypoint is idempotent, so this is safe even when a fresh script also
|
* Run explicit component cleanup before removing the existing page.
|
||||||
// self-initialises the same nodes.
|
*
|
||||||
function rehydrate() {
|
* The reactive runtime also observes removed DOM nodes, but explicit
|
||||||
try { if (window.__wrnexusHydrateScopes) window.__wrnexusHydrateScopes(document); } catch (e) {}
|
* disposal here guarantees that unmount hooks run before replacement.
|
||||||
try { if (window.__wrnexusHydrateCsrFetches) window.__wrnexusHydrateCsrFetches(document); } catch (e) {}
|
*/
|
||||||
try { if (window.__wireValidate) window.__wireValidate.init(document); } catch (e) {}
|
function dispose(root) {
|
||||||
try { if (window.wireTheme) window.wireTheme.bind(document); } catch (e) {}
|
try {
|
||||||
|
if (
|
||||||
|
typeof window.__wrnexusDisposeBehaviors ===
|
||||||
|
"function"
|
||||||
|
) {
|
||||||
|
window.__wrnexusDisposeBehaviors(root);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"[wrnexus] failed to dispose page behaviors",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-hydrate already-loaded runtimes against newly inserted DOM.
|
||||||
|
*
|
||||||
|
* All framework hydration functions must remain idempotent.
|
||||||
|
*/
|
||||||
|
function rehydrate(root) {
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
typeof window.__wrnexusHydrateScopes ===
|
||||||
|
"function"
|
||||||
|
) {
|
||||||
|
window.__wrnexusHydrateScopes(root);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"[wrnexus] failed to hydrate reactive scopes",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
typeof window.__wrnexusHydrateCsrFetches ===
|
||||||
|
"function"
|
||||||
|
) {
|
||||||
|
window.__wrnexusHydrateCsrFetches(root);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"[wrnexus] failed to hydrate CSR bindings",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
window.__wireValidate &&
|
||||||
|
typeof window.__wireValidate.init ===
|
||||||
|
"function"
|
||||||
|
) {
|
||||||
|
window.__wireValidate.init(document);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"[wrnexus] failed to hydrate validation",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
window.wireTheme &&
|
||||||
|
typeof window.wireTheme.bind ===
|
||||||
|
"function"
|
||||||
|
) {
|
||||||
|
window.wireTheme.bind(document);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"[wrnexus] failed to hydrate theme bindings",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispatchNavigationEvent(url) {
|
||||||
|
try {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("wrnexus:navigated", {
|
||||||
|
detail: {
|
||||||
|
url: url,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
// CustomEvent may not be available in very old browsers.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hardNavigate(url) {
|
||||||
|
location.href = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(html, url, isPop) {
|
function render(html, url, isPop) {
|
||||||
var doc = new DOMParser().parseFromString(html, "text/html");
|
var doc;
|
||||||
var to = doc.getElementById(APP_ID);
|
|
||||||
var from = document.getElementById(APP_ID);
|
|
||||||
if (!to || !from) { location.href = url; return; } // structure mismatch → hard nav
|
|
||||||
if (doc.title) document.title = doc.title;
|
|
||||||
// Swap #app by importing nodes — NOT innerHTML — so it works under a strict
|
|
||||||
// Trusted-Types CSP (require-trusted-types-for 'script').
|
|
||||||
var imported = [];
|
|
||||||
for (var i = 0; i < to.childNodes.length; i++) imported.push(document.importNode(to.childNodes[i], true));
|
|
||||||
from.replaceChildren.apply(from, imported);
|
|
||||||
ensureScripts(doc);
|
|
||||||
rehydrate();
|
|
||||||
if (!isPop) { history.pushState({ wrnexusNav: true }, "", url); window.scrollTo(0, 0); }
|
|
||||||
try {
|
|
||||||
window.dispatchEvent(new CustomEvent("wrnexus:navigated", { detail: { url: url } }));
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
var inFlight = null;
|
try {
|
||||||
|
doc = new DOMParser().parseFromString(
|
||||||
|
html,
|
||||||
|
"text/html",
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
hardNavigate(url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var incomingApp =
|
||||||
|
doc.getElementById(APP_ID);
|
||||||
|
|
||||||
|
var currentApp =
|
||||||
|
document.getElementById(APP_ID);
|
||||||
|
|
||||||
|
if (!incomingApp || !currentApp) {
|
||||||
|
hardNavigate(url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.title) {
|
||||||
|
document.title = doc.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
var importedNodes = [];
|
||||||
|
|
||||||
|
for (
|
||||||
|
var index = 0;
|
||||||
|
index < incomingApp.childNodes.length;
|
||||||
|
index++
|
||||||
|
) {
|
||||||
|
importedNodes.push(
|
||||||
|
document.importNode(
|
||||||
|
incomingApp.childNodes[index],
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run unmount hooks and watcher cleanup while the current DOM is still
|
||||||
|
* connected. This is important for components that remove window or
|
||||||
|
* document listeners during lifecycle.unmount.
|
||||||
|
*/
|
||||||
|
dispose(currentApp);
|
||||||
|
|
||||||
|
try {
|
||||||
|
/*
|
||||||
|
* Avoid innerHTML so navigation works with strict Trusted Types and CSP
|
||||||
|
* configurations.
|
||||||
|
*/
|
||||||
|
currentApp.replaceChildren.apply(
|
||||||
|
currentApp,
|
||||||
|
importedNodes,
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
hardNavigate(url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Load any runtime required only by the incoming page. Fresh scripts
|
||||||
|
* initialise themselves when loaded.
|
||||||
|
*/
|
||||||
|
ensureScripts(doc);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Hydrate runtimes that are already available. Hydration is scoped to
|
||||||
|
* #app instead of the entire document for less DOM traversal.
|
||||||
|
*/
|
||||||
|
rehydrate(currentApp);
|
||||||
|
|
||||||
|
if (!isPop) {
|
||||||
|
history.pushState(
|
||||||
|
{
|
||||||
|
wrnexusNav: true,
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
url,
|
||||||
|
);
|
||||||
|
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchNavigationEvent(url);
|
||||||
|
}
|
||||||
|
|
||||||
function navigate(url, isPop) {
|
function navigate(url, isPop) {
|
||||||
var token = {};
|
var token = {};
|
||||||
|
|
||||||
inFlight = token;
|
inFlight = token;
|
||||||
fetch(url, { headers: { "x-wrnexus-nav": "1", accept: "text/html" }, credentials: "same-origin" })
|
|
||||||
.then(function (r) {
|
fetch(url, {
|
||||||
if (inFlight !== token) return null; // superseded by a newer navigation
|
headers: {
|
||||||
if (r.redirected && r.url) url = r.url; // follow server redirects (e.g. auth)
|
"x-wrnexus-nav": "1",
|
||||||
var ct = r.headers.get("content-type") || "";
|
accept: "text/html",
|
||||||
if (ct.indexOf("text/html") === -1) { location.href = url; return null; }
|
},
|
||||||
return r.text().then(function (t) {
|
|
||||||
if (inFlight === token) render(t, url, isPop);
|
credentials: "same-origin",
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
if (inFlight !== token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.redirected && response.url) {
|
||||||
|
url = response.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentType =
|
||||||
|
response.headers.get("content-type") ||
|
||||||
|
"";
|
||||||
|
|
||||||
|
if (
|
||||||
|
contentType.indexOf("text/html") === -1
|
||||||
|
) {
|
||||||
|
hardNavigate(url);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentType =
|
||||||
|
response.headers.get("content-type") ||
|
||||||
|
"";
|
||||||
|
|
||||||
|
if (
|
||||||
|
contentType.indexOf("text/html") === -1
|
||||||
|
) {
|
||||||
|
hardNavigate(url);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.text().then(function (text) {
|
||||||
|
if (inFlight !== token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(text, url, isPop);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(function () { location.href = url; });
|
.catch(function () {
|
||||||
|
if (inFlight === token) {
|
||||||
|
hardNavigate(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
"click",
|
"click",
|
||||||
function (e) {
|
function (event) {
|
||||||
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
if (
|
||||||
var a = e.target && e.target.closest ? e.target.closest("a") : null;
|
event.defaultPrevented ||
|
||||||
if (!isLocalLink(a)) return;
|
event.button !== 0 ||
|
||||||
if (a.href === location.href) { e.preventDefault(); return; }
|
event.metaKey ||
|
||||||
e.preventDefault();
|
event.ctrlKey ||
|
||||||
navigate(a.href, false);
|
event.shiftKey ||
|
||||||
|
event.altKey
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var anchor =
|
||||||
|
event.target &&
|
||||||
|
event.target.closest
|
||||||
|
? event.target.closest("a")
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!isLocalLink(anchor)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anchor.href === location.href) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
navigate(anchor.href, false);
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
window.addEventListener("popstate", function () {
|
window.addEventListener(
|
||||||
navigate(location.href, true);
|
"popstate",
|
||||||
});
|
function () {
|
||||||
|
navigate(location.href, true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Programmatic navigation for forms/actions and app code.
|
/**
|
||||||
|
* Programmatic navigation for forms, actions and application code.
|
||||||
|
*/
|
||||||
window.__wrnexusNavigate = function (url) {
|
window.__wrnexusNavigate = function (url) {
|
||||||
navigate(new URL(url, location.href).href, false);
|
navigate(
|
||||||
|
new URL(url, location.href).href,
|
||||||
|
false,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|||||||
@@ -15,8 +15,109 @@
|
|||||||
* Expressions are evaluated by a tiny parser instead of `eval`/`new Function`,
|
* Expressions are evaluated by a tiny parser instead of `eval`/`new Function`,
|
||||||
* so production can use a strong CSP without `unsafe-eval`.
|
* so production can use a strong CSP without `unsafe-eval`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const REACTIVE_RUNTIME = String.raw`
|
export const REACTIVE_RUNTIME = String.raw`
|
||||||
(function () {
|
(function () {
|
||||||
|
var behaviorInstances = new WeakMap();
|
||||||
|
var mountedBehaviorRoots = new Set();
|
||||||
|
var pendingUpdateHooks = new Map();
|
||||||
|
var updateHooksScheduled = false;
|
||||||
|
var behaviorObserver;
|
||||||
|
|
||||||
|
function scheduleUpdateHook(element, callback) {
|
||||||
|
pendingUpdateHooks.set(element, callback);
|
||||||
|
if (updateHooksScheduled) return;
|
||||||
|
updateHooksScheduled = true;
|
||||||
|
queueMicrotask(function () {
|
||||||
|
updateHooksScheduled = false;
|
||||||
|
var pending = Array.from(pendingUpdateHooks.entries());
|
||||||
|
pendingUpdateHooks.clear();
|
||||||
|
pending.forEach(function (entry) {
|
||||||
|
var root = entry[0];
|
||||||
|
var hook = entry[1];
|
||||||
|
if (!root.isConnected) return;
|
||||||
|
try { hook(); } catch (error) {
|
||||||
|
console.error("[wrnexus] component update hook failed", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBehavior(element) {
|
||||||
|
var raw = element.getAttribute("data-wrn-behavior");
|
||||||
|
if (!raw) return null;
|
||||||
|
try {
|
||||||
|
var parsed = JSON.parse(raw);
|
||||||
|
return parsed && typeof parsed === "object" ? parsed : null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[wrnexus] failed to parse component behavior", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractBehaviorFunctions(source) {
|
||||||
|
var functions = [];
|
||||||
|
var re = /(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)\s*\{/g;
|
||||||
|
var match;
|
||||||
|
while ((match = re.exec(source || ""))) {
|
||||||
|
var start = re.lastIndex;
|
||||||
|
var depth = 1;
|
||||||
|
var quote = "";
|
||||||
|
var i = start;
|
||||||
|
for (; i < source.length; i++) {
|
||||||
|
var ch = source[i];
|
||||||
|
if (quote) {
|
||||||
|
if (ch === "\\") i++;
|
||||||
|
else if (ch === quote) quote = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === '"' || ch === "'" || ch === "\`") {
|
||||||
|
quote = ch;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === "{") depth++;
|
||||||
|
else if (ch === "}" && --depth === 0) break;
|
||||||
|
}
|
||||||
|
if (depth !== 0) break;
|
||||||
|
functions.push({
|
||||||
|
name: match[1],
|
||||||
|
args: match[2].split(",").map(function (arg) { return arg.trim(); }).filter(Boolean),
|
||||||
|
body: source.slice(start, i),
|
||||||
|
});
|
||||||
|
re.lastIndex = i + 1;
|
||||||
|
}
|
||||||
|
return functions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitStatements(input) {
|
||||||
|
var parts = [];
|
||||||
|
var start = 0;
|
||||||
|
var depth = 0;
|
||||||
|
var quote = "";
|
||||||
|
for (var i = 0; i < input.length; i++) {
|
||||||
|
var ch = input[i];
|
||||||
|
if (quote) {
|
||||||
|
if (ch === "\\") i++;
|
||||||
|
else if (ch === quote) quote = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === '"' || ch === "'" || ch === "\`") {
|
||||||
|
quote = ch;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
||||||
|
else if (ch === ")" || ch === "]" || ch === "}") depth--;
|
||||||
|
else if ((ch === ";" || ch === "\n" || ch === "\r") && depth === 0) {
|
||||||
|
var part = input.slice(start, i).trim();
|
||||||
|
if (part) parts.push(part);
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var tail = input.slice(start).trim();
|
||||||
|
if (tail) parts.push(tail);
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
function signal(initial) {
|
function signal(initial) {
|
||||||
var value = initial;
|
var value = initial;
|
||||||
var subs = new Set();
|
var subs = new Set();
|
||||||
@@ -24,8 +125,9 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
get: function () { return value; },
|
get: function () { return value; },
|
||||||
set: function (v) {
|
set: function (v) {
|
||||||
if (Object.is(v, value)) return;
|
if (Object.is(v, value)) return;
|
||||||
|
var previous = value;
|
||||||
value = v;
|
value = v;
|
||||||
subs.forEach(function (f) { f(value); });
|
subs.forEach(function (f) { f(value, previous); });
|
||||||
},
|
},
|
||||||
subscribe: function (f) { subs.add(f); return function () { subs.delete(f); }; }
|
subscribe: function (f) { subs.add(f); return function () { subs.delete(f); }; }
|
||||||
};
|
};
|
||||||
@@ -58,29 +160,117 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
}
|
}
|
||||||
function renderAll() { renderers.forEach(function (f) { f(); }); }
|
function renderAll() { renderers.forEach(function (f) { f(); }); }
|
||||||
|
|
||||||
function readScope(name) {
|
var behaviorFunctions = {};
|
||||||
var sig = signals[name];
|
var stateWatchers = {};
|
||||||
if (!sig) return undefined;
|
var anyStateListeners = new Set();
|
||||||
if (currentRenderer) sig.subscribe(currentRenderer); // track dependency
|
var cleanupCallbacks = [];
|
||||||
return sig.get();
|
var disposed = false;
|
||||||
}
|
|
||||||
function peekScope(name) {
|
function readGlobal(name) {
|
||||||
return signals[name] ? signals[name].get() : undefined;
|
if (name === "window") return window;
|
||||||
|
if (name === "document") return document;
|
||||||
|
if (name === "console") return console;
|
||||||
|
if (name === "Math") return Math;
|
||||||
|
if (name === "JSON") return JSON;
|
||||||
|
if (name === "Date") return Date;
|
||||||
|
if (name === "URL") return URL;
|
||||||
|
if (name === "location") return window.location;
|
||||||
|
if (name === "history") return window.history;
|
||||||
|
if (name === "navigator") return window.navigator;
|
||||||
|
if (name === "setTimeout") return window.setTimeout.bind(window);
|
||||||
|
if (name === "clearTimeout") return window.clearTimeout.bind(window);
|
||||||
|
if (name === "requestAnimationFrame") return window.requestAnimationFrame.bind(window);
|
||||||
|
if (name === "cancelAnimationFrame") return window.cancelAnimationFrame.bind(window);
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function evalExpr(expr) {
|
function readScope(name) {
|
||||||
return evaluateExpression(expr, readScope);
|
var sig = signals[name];
|
||||||
|
if (sig) {
|
||||||
|
if (currentRenderer) sig.subscribe(currentRenderer);
|
||||||
|
return sig.get();
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(behaviorFunctions, name)) {
|
||||||
|
return behaviorFunctions[name];
|
||||||
|
}
|
||||||
|
return readGlobal(name);
|
||||||
}
|
}
|
||||||
function runStmt(stmt) {
|
|
||||||
splitTopLevel(stmt, ";").forEach(function (part) {
|
function peekScope(name) {
|
||||||
runStatement(part, function (e) { return evaluateExpression(e, peekScope); }, peekScope, function (name, value) {
|
if (signals[name]) return signals[name].get();
|
||||||
if (!signals[name]) {
|
if (Object.prototype.hasOwnProperty.call(behaviorFunctions, name)) {
|
||||||
signals[name] = signal(value);
|
return behaviorFunctions[name];
|
||||||
renderAll(); // new variable: re-run once so readers pick it up + re-track
|
}
|
||||||
} else {
|
return readGlobal(name);
|
||||||
signals[name].set(value);
|
}
|
||||||
|
|
||||||
|
function notifyState(name, value, previous) {
|
||||||
|
var watchers = stateWatchers[name];
|
||||||
|
if (watchers) {
|
||||||
|
watchers.slice().forEach(function (watcher) {
|
||||||
|
try { watcher(value, previous); } catch (error) {
|
||||||
|
console.error("[wrnexus] watcher for '" + name + "' failed", error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
anyStateListeners.forEach(function (listener) {
|
||||||
|
try { listener(name, value, previous); } catch (error) {
|
||||||
|
console.error("[wrnexus] state subscriber failed", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeScope(name, value) {
|
||||||
|
if (!signals[name]) {
|
||||||
|
signals[name] = signal(value);
|
||||||
|
renderAll();
|
||||||
|
notifyState(name, value, undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var previous = signals[name].get();
|
||||||
|
if (Object.is(previous, value)) return;
|
||||||
|
signals[name].set(value);
|
||||||
|
notifyState(name, value, previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
function evalExpr(expr, locals) {
|
||||||
|
return evaluateExpression(expr, function (name) {
|
||||||
|
if (locals && Object.prototype.hasOwnProperty.call(locals, name)) {
|
||||||
|
return locals[name];
|
||||||
|
}
|
||||||
|
return readScope(name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function runStmt(stmt, locals) {
|
||||||
|
splitStatements(stmt).forEach(function (part) {
|
||||||
|
runStatement(
|
||||||
|
part,
|
||||||
|
function (expression) { return evalExpr(expression, locals); },
|
||||||
|
function (name) {
|
||||||
|
if (locals && Object.prototype.hasOwnProperty.call(locals, name)) return locals[name];
|
||||||
|
return peekScope(name);
|
||||||
|
},
|
||||||
|
function (name, value) {
|
||||||
|
if (locals && Object.prototype.hasOwnProperty.call(locals, name)) {
|
||||||
|
locals[name] = value;
|
||||||
|
} else {
|
||||||
|
writeScope(name, value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function installBehaviorFunctions(source) {
|
||||||
|
extractBehaviorFunctions(source || "").forEach(function (definition) {
|
||||||
|
behaviorFunctions[definition.name] = function () {
|
||||||
|
var locals = {};
|
||||||
|
for (var i = 0; i < definition.args.length; i++) {
|
||||||
|
locals[definition.args[i]] = arguments[i];
|
||||||
|
}
|
||||||
|
runStmt(definition.body, locals);
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,27 +477,146 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
})(textNode, template);
|
})(textNode, template);
|
||||||
}
|
}
|
||||||
|
|
||||||
// data-on-<event> handlers, on the scope element and the descendants it owns.
|
// Event handlers on elements, window, and document.
|
||||||
var nodes = [el].concat(Array.prototype.slice.call(el.querySelectorAll("*")));
|
var nodes = [el].concat(Array.prototype.slice.call(el.querySelectorAll("*")));
|
||||||
nodes.forEach(function (node) {
|
nodes.forEach(function (node) {
|
||||||
if (!owns(node)) return;
|
if (!owns(node)) return;
|
||||||
Array.prototype.slice.call(node.attributes).forEach(function (attr) {
|
Array.prototype.slice.call(node.attributes).forEach(function (attr) {
|
||||||
if (attr.name.indexOf("data-on-") !== 0) return;
|
if (attr.name.indexOf("data-on-") !== 0) return;
|
||||||
var evt = attr.name.slice("data-on-".length);
|
|
||||||
|
var rawName = attr.name.slice("data-on-".length);
|
||||||
|
var target = node;
|
||||||
|
var evt = rawName;
|
||||||
|
|
||||||
|
if (rawName.indexOf("window-") === 0) {
|
||||||
|
target = window;
|
||||||
|
evt = rawName.slice("window-".length);
|
||||||
|
} else if (rawName.indexOf("document-") === 0) {
|
||||||
|
target = document;
|
||||||
|
evt = rawName.slice("document-".length);
|
||||||
|
}
|
||||||
|
|
||||||
var stmt = attr.value;
|
var stmt = attr.value;
|
||||||
node.addEventListener(evt, function () {
|
var listener = function (event) {
|
||||||
try { runStmt(stmt); } catch (e) {
|
try { runStmt(stmt, { event: event, $event: event }); } catch (error) {
|
||||||
console.error("[wrnexus] handler error in '" + stmt + "'", e);
|
console.error("[wrnexus] handler error in '" + stmt + "'", error);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var options =
|
||||||
|
target === window && (evt === "scroll" || evt === "touchstart" || evt === "touchmove")
|
||||||
|
? { passive: true }
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
target.addEventListener(evt, listener, options);
|
||||||
|
cleanupCallbacks.push(function () {
|
||||||
|
target.removeEventListener(evt, listener, options);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var behavior = parseBehavior(el);
|
||||||
|
if (behavior) {
|
||||||
|
installBehaviorFunctions(behavior.functions);
|
||||||
|
|
||||||
|
(behavior.watches || []).forEach(function (watch) {
|
||||||
|
if (!watch || typeof watch.state !== "string" || typeof watch.body !== "string") return;
|
||||||
|
if (!stateWatchers[watch.state]) stateWatchers[watch.state] = [];
|
||||||
|
stateWatchers[watch.state].push(function (value, previous) {
|
||||||
|
runStmt(watch.body, { value: value, previous: previous });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var updateSource =
|
||||||
|
behavior.lifecycle && typeof behavior.lifecycle.update === "string"
|
||||||
|
? behavior.lifecycle.update.trim()
|
||||||
|
: "";
|
||||||
|
|
||||||
|
if (updateSource) {
|
||||||
|
anyStateListeners.add(function () {
|
||||||
|
scheduleUpdateHook(el, function () {
|
||||||
|
if (!disposed) runStmt(updateSource);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var mountSource =
|
||||||
|
behavior.lifecycle && typeof behavior.lifecycle.mount === "string"
|
||||||
|
? behavior.lifecycle.mount.trim()
|
||||||
|
: "";
|
||||||
|
|
||||||
|
var unmountSource =
|
||||||
|
behavior.lifecycle && typeof behavior.lifecycle.unmount === "string"
|
||||||
|
? behavior.lifecycle.unmount.trim()
|
||||||
|
: "";
|
||||||
|
|
||||||
|
var instance = {
|
||||||
|
dispose: function () {
|
||||||
|
if (disposed) return;
|
||||||
|
disposed = true;
|
||||||
|
cleanupCallbacks.splice(0).reverse().forEach(function (cleanup) {
|
||||||
|
try { cleanup(); } catch (_) { /* ignore cleanup errors */ }
|
||||||
|
});
|
||||||
|
pendingUpdateHooks.delete(el);
|
||||||
|
if (unmountSource) {
|
||||||
|
try { runStmt(unmountSource); } catch (error) {
|
||||||
|
console.error("[wrnexus] component unmount hook failed", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mountedBehaviorRoots.delete(el);
|
||||||
|
behaviorInstances.delete(el);
|
||||||
|
el.__wrnexusScope = false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
behaviorInstances.set(el, instance);
|
||||||
|
mountedBehaviorRoots.add(el);
|
||||||
|
|
||||||
|
if (mountSource) {
|
||||||
|
queueMicrotask(function () {
|
||||||
|
if (!disposed && el.isConnected) {
|
||||||
|
try { runStmt(mountSource); } catch (error) {
|
||||||
|
console.error("[wrnexus] component mount hook failed", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderAll();
|
renderAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function disposeBehaviors(root) {
|
||||||
|
var elements = [];
|
||||||
|
if (root && root.nodeType === 1 && root.hasAttribute("data-wrn-behavior")) {
|
||||||
|
elements.push(root);
|
||||||
|
}
|
||||||
|
if (root && root.querySelectorAll) {
|
||||||
|
Array.prototype.push.apply(elements, root.querySelectorAll("[data-wrn-behavior]"));
|
||||||
|
}
|
||||||
|
elements.reverse().forEach(function (element) {
|
||||||
|
var instance = behaviorInstances.get(element);
|
||||||
|
if (instance) instance.dispose();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureBehaviorObserver() {
|
||||||
|
if (behaviorObserver || typeof MutationObserver === "undefined") return;
|
||||||
|
behaviorObserver = new MutationObserver(function (records) {
|
||||||
|
records.forEach(function (record) {
|
||||||
|
Array.prototype.forEach.call(record.removedNodes, function (node) {
|
||||||
|
if (node && node.nodeType === 1) disposeBehaviors(node);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
behaviorObserver.observe(document.documentElement, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
|
||||||
function hydrateScopes(root) {
|
function hydrateScopes(root) {
|
||||||
(root || document).querySelectorAll("[data-scope]").forEach(setupScope);
|
var host = root || document;
|
||||||
|
if (host.nodeType === 1 && host.matches && host.matches("[data-scope]")) setupScope(host);
|
||||||
|
if (host.querySelectorAll) host.querySelectorAll("[data-scope]").forEach(setupScope);
|
||||||
|
ensureBehaviorObserver();
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseScopeDecl(decl) {
|
function parseScopeDecl(decl) {
|
||||||
@@ -635,6 +944,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
|
|
||||||
window.__wrnexusHydrateScopes = hydrateScopes;
|
window.__wrnexusHydrateScopes = hydrateScopes;
|
||||||
window.__wrnexusHydrateCsrFetches = hydrateCsrFetches;
|
window.__wrnexusHydrateCsrFetches = hydrateCsrFetches;
|
||||||
|
window.__wrnexusDisposeBehaviors = disposeBehaviors;
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
hydrateScopes(document);
|
hydrateScopes(document);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/db",
|
"name": "@wrnexus/db",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"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.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/encryption",
|
"name": "@wrnexus/encryption",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/helpers",
|
"name": "@wrnexus/helpers",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/i18n",
|
"name": "@wrnexus/i18n",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/jwt",
|
"name": "@wrnexus/jwt",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/mobile",
|
"name": "@wrnexus/mobile",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/native",
|
"name": "@wrnexus/native",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/oauth",
|
"name": "@wrnexus/oauth",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/pubsub",
|
"name": "@wrnexus/pubsub",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/queue",
|
"name": "@wrnexus/queue",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/reactive",
|
"name": "@wrnexus/reactive",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/router",
|
"name": "@wrnexus/router",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ssr",
|
"name": "@wrnexus/ssr",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/styles",
|
"name": "@wrnexus/styles",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/test",
|
"name": "@wrnexus/test",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/tracking",
|
"name": "@wrnexus/tracking",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ui",
|
"name": "@wrnexus/ui",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/uploader",
|
"name": "@wrnexus/uploader",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/validation",
|
"name": "@wrnexus/validation",
|
||||||
"version": "0.2.30",
|
"version": "0.2.31",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user