release: WRNexusJS 0.8.3
Quality / quality (ubuntu-latest) (push) Failing after 12m9s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-03 19:47:30 +05:30
parent e8f630f12d
commit 4cebacadfe
156 changed files with 2608 additions and 473 deletions
+1 -1
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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": [
+5 -5
View File
@@ -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 -1
View File
@@ -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",
+2 -2
View File
@@ -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 -1
View File
@@ -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",
+20 -20
View File
@@ -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 -1
View File
@@ -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",
+2 -2
View File
@@ -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",
+2 -2
View File
@@ -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",
+4 -4
View File
@@ -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": {
+2 -2
View File
@@ -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",
+2 -2
View File
@@ -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 -1
View File
@@ -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",
+2 -2
View File
@@ -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",
+2 -2
View File
@@ -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 -1
View File
@@ -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",
+2 -2
View File
@@ -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 -1
View File
@@ -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",
+3 -3
View File
@@ -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",
+4 -4
View File
@@ -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",
+4 -4
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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",
+2 -2
View File
@@ -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",
+4 -4
View File
@@ -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": {
+4 -4
View File
@@ -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": {
+20
View File
@@ -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.
+46 -46
View File
@@ -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:*",
+59
View File
@@ -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.
+73
View File
@@ -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.
+9 -9
View File
@@ -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",
+6
View File
@@ -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.
+2 -2
View File
@@ -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"
+2 -2
View File
@@ -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
View File
@@ -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";
+277 -51
View File
@@ -2348,7 +2348,7 @@ var require_connection = __commonJS((exports2) => {
} }
if (responseMessage.id === null) { if (responseMessage.id === null) {
if (responseMessage.error) { if (responseMessage.error) {
logger.error(`Received response message without id: Error is: logger.error(`Received response message without id: Error is:
${JSON.stringify(responseMessage.error, undefined, 4)}`); ${JSON.stringify(responseMessage.error, undefined, 4)}`);
} else { } else {
logger.error(`Received response message without id. No further error information provided.`); logger.error(`Received response message without id. No further error information provided.`);
@@ -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";
+2 -2
View File
@@ -40,12 +40,12 @@ var require_typescript = __commonJS((exports2, module2) => {
Licensed under the Apache License, Version 2.0 (the "License"); you may not use Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0 License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT. MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License. and limitations under the License.
***************************************************************************** */ ***************************************************************************** */
+5 -4
View File
@@ -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 -1
View File
@@ -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"
+2
View File
@@ -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"
+2
View File
@@ -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"
+3
View File
@@ -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"
+2
View File
@@ -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"
+2
View File
@@ -1,3 +1,5 @@
import { Button, Input } from "@wrnexus/ui"
component VerifyEmail { component VerifyEmail {
props { props {
kind = "email" kind = "email"
+2
View File
@@ -1,3 +1,5 @@
import { Button, PinInput } from "@wrnexus/ui"
component VerifyPhone { component VerifyPhone {
props { props {
kind = "phone" kind = "phone"
+1 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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",
+1 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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": {
+108 -20
View File
@@ -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,23 +248,74 @@ 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;
let target = imported.resolved;
if (target.endsWith(".wrn")) {
await compileWrn(target);
target = compiledFiles.get(target)!;
}
const sourcePattern = imported.declaration.source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const sourcePattern = imported.declaration.source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const relativeTarget = relative(dirname(out), target).replace(/\\/g, "/");
const specifier = relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`; const importAliases = { "@": "./app", ...(config.imports?.aliases ?? {}) };
code = code.replace( const isApplicationImport =
new RegExp(`(["'])${sourcePattern}\\1`, "g"), imported.declaration.source.startsWith(".") ||
JSON.stringify(specifier), Object.keys(importAliases).some(
(alias) =>
imported.declaration.source === alias ||
imported.declaration.source.startsWith(`${alias}/`),
);
if (isApplicationImport) {
let target = imported.resolved;
if (target.endsWith(".wrn")) {
await compileWrn(target);
target = compiledFiles.get(target)!;
}
const relativeTarget = relative(dirname(out), target).replace(/\\/g, "/");
const specifier = relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`;
code = code.replace(
new RegExp(`(["'])${sourcePattern}\\1`, "g"),
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(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(out, code, "utf8"); 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)},
+44 -3
View File
@@ -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> {
+10
View File
@@ -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 -1
View File
@@ -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": {
+115 -17
View File
@@ -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 = {
+220 -52
View File
@@ -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(", ")
+23 -6
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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": {
-1
View File
@@ -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": [],
}; };
+5
View File
@@ -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 -1
View File
@@ -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": {
+5
View File
@@ -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();
+150 -12
View File
@@ -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");
+33 -9
View File
@@ -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();
+121
View File
@@ -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");
});
+24
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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": {
+2 -2
View File
@@ -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);
+123 -23
View File
@@ -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;
+10
View File
@@ -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);
+12 -2
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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.",

Some files were not shown because too many files have changed in this diff Show More