From cff420a29ea11091c84b3530a817429383c04bd4 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Mon, 13 Jul 2026 14:54:03 +0530 Subject: [PATCH] feat: restore gateway HMR and add WRN formatting --- .publish/ai/package.json | 2 +- .publish/authz/package.json | 2 +- .publish/cli/package.json | 22 +-- .publish/compiler/package.json | 2 +- .publish/core/package.json | 2 +- .publish/csr/package.json | 4 +- .publish/db/package.json | 2 +- .publish/dev-server/README.md | 2 +- .publish/dev-server/package.json | 26 +-- .publish/encryption/package.json | 2 +- .publish/helpers/package.json | 4 +- .publish/i18n/package.json | 4 +- .publish/jwt/package.json | 2 +- .publish/mobile/package.json | 4 +- .publish/native/package.json | 2 +- .publish/oauth/package.json | 2 +- .publish/pubsub/package.json | 2 +- .publish/queue/package.json | 2 +- .publish/reactive/package.json | 2 +- .publish/router/package.json | 6 +- .publish/ssr/package.json | 4 +- .publish/styles/package.json | 4 +- .publish/test/package.json | 2 +- .publish/tracking/package.json | 2 +- .publish/ui/package.json | 4 +- .publish/uploader/package.json | 4 +- .publish/validation/package.json | 2 +- editors/vscode/CHANGELOG.md | 5 + editors/vscode/README.md | 9 +- editors/vscode/package-lock.json | 4 +- editors/vscode/package.json | 12 +- editors/vscode/src/compiler.cjs | 220 ++++++++++++++++++++++- editors/vscode/src/extension.js | 12 ++ editors/vscode/src/formatter.js | 117 ++++++++++++ editors/vscode/test/validate.mjs | 14 ++ editors/vscode/wrnexus-0.2.0.vsix | Bin 0 -> 26944 bytes packages/ai/package.json | 2 +- packages/authz/package.json | 2 +- packages/cli/package.json | 2 +- packages/cli/src/create.ts | 38 +++- packages/cli/src/update.ts | 84 +++++++++ packages/cli/test/create.test.ts | 41 +++++ packages/cli/test/update.test.ts | 32 ++++ packages/compiler/package.json | 2 +- packages/core/package.json | 2 +- packages/csr/package.json | 2 +- packages/db/package.json | 2 +- packages/dev-server/README.md | 2 +- packages/dev-server/package.json | 2 +- packages/dev-server/src/gateway.ts | 71 ++++++-- packages/dev-server/src/index.ts | 4 +- packages/dev-server/src/restart.ts | 2 + packages/dev-server/test/gateway.test.ts | 9 + packages/encryption/package.json | 2 +- packages/helpers/package.json | 2 +- packages/i18n/package.json | 2 +- packages/jwt/package.json | 2 +- packages/mobile/package.json | 2 +- packages/native/package.json | 2 +- packages/oauth/package.json | 2 +- packages/pubsub/package.json | 2 +- packages/queue/package.json | 2 +- packages/reactive/package.json | 2 +- packages/router/package.json | 2 +- packages/ssr/package.json | 2 +- packages/styles/package.json | 2 +- packages/test/package.json | 2 +- packages/tracking/package.json | 2 +- packages/ui/package.json | 2 +- packages/uploader/package.json | 2 +- packages/validation/package.json | 2 +- 71 files changed, 727 insertions(+), 119 deletions(-) create mode 100644 editors/vscode/src/formatter.js create mode 100644 editors/vscode/wrnexus-0.2.0.vsix create mode 100644 packages/dev-server/src/restart.ts diff --git a/.publish/ai/package.json b/.publish/ai/package.json index 78887f5b..3d29a755 100644 --- a/.publish/ai/package.json +++ b/.publish/ai/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ai", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", "license": "MIT", diff --git a/.publish/authz/package.json b/.publish/authz/package.json index 895695b8..21bdce31 100644 --- a/.publish/authz/package.json +++ b/.publish/authz/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/authz", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "@wrnexus/authz — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/cli/package.json b/.publish/cli/package.json index 140a16ac..88a5b48b 100644 --- a/.publish/cli/package.json +++ b/.publish/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "@wrnexus/cli — part of the WrNexus framework.", "license": "MIT", @@ -28,16 +28,16 @@ "wrnexus": "./dist/index.js" }, "dependencies": { - "@wrnexus/core": "^0.2.18", - "@wrnexus/router": "^0.2.18", - "@wrnexus/csr": "^0.2.18", - "@wrnexus/compiler": "^0.2.18", - "@wrnexus/styles": "^0.2.18", - "@wrnexus/dev-server": "^0.2.18", - "@wrnexus/ui": "^0.2.18", - "@wrnexus/validation": "^0.2.18", - "@wrnexus/i18n": "^0.2.18", - "@wrnexus/db": "^0.2.18" + "@wrnexus/core": "^0.2.19", + "@wrnexus/router": "^0.2.19", + "@wrnexus/csr": "^0.2.19", + "@wrnexus/compiler": "^0.2.19", + "@wrnexus/styles": "^0.2.19", + "@wrnexus/dev-server": "^0.2.19", + "@wrnexus/ui": "^0.2.19", + "@wrnexus/validation": "^0.2.19", + "@wrnexus/i18n": "^0.2.19", + "@wrnexus/db": "^0.2.19" }, "files": [ "dist" diff --git a/.publish/compiler/package.json b/.publish/compiler/package.json index f22ab6f5..b259433c 100644 --- a/.publish/compiler/package.json +++ b/.publish/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "@wrnexus/compiler — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/core/package.json b/.publish/core/package.json index c7a20a3a..85e6645a 100644 --- a/.publish/core/package.json +++ b/.publish/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "@wrnexus/core — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/csr/package.json b/.publish/csr/package.json index 2b5ef12d..3e7ec4a0 100644 --- a/.publish/csr/package.json +++ b/.publish/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "@wrnexus/csr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.18" + "@wrnexus/core": "^0.2.19" }, "files": [ "dist" diff --git a/.publish/db/package.json b/.publish/db/package.json index 505b2809..39d6693d 100644 --- a/.publish/db/package.json +++ b/.publish/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "@wrnexus/db — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/dev-server/README.md b/.publish/dev-server/README.md index 105e193f..6e45c204 100644 --- a/.publish/dev-server/README.md +++ b/.publish/dev-server/README.md @@ -142,7 +142,7 @@ interface ProdOptions { ### `startGateway(opts)` — multi-app gateway -Serves several apps behind one port and routes each request to the right app by its `Host` header. Each app runs as its own child process (full isolation); the gateway is a thin host-based reverse proxy for HTTP and WebSocket. Apps communicate at runtime via `@wrnexus/pubsub` (use the Redis driver so messages cross processes). +Serves several apps behind one port and routes each request to the right app by its `Host` header. Each app runs as its own child process (full isolation); the gateway is a thin host-based reverse proxy for HTTP and WebSocket. In development, the gateway supervises every child: editing a page, component, API, or middleware restarts only that app and the HMR client reconnects to display the latest page. Apps communicate at runtime via `@wrnexus/pubsub` (use the Redis driver so messages cross processes). ```ts interface GatewayOptions { diff --git a/.publish/dev-server/package.json b/.publish/dev-server/package.json index 198608d8..706d7fc1 100644 --- a/.publish/dev-server/package.json +++ b/.publish/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "@wrnexus/dev-server — part of the WrNexus framework.", "license": "MIT", @@ -25,18 +25,18 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.18", - "@wrnexus/router": "^0.2.18", - "@wrnexus/ssr": "^0.2.18", - "@wrnexus/csr": "^0.2.18", - "@wrnexus/compiler": "^0.2.18", - "@wrnexus/styles": "^0.2.18", - "@wrnexus/ui": "^0.2.18", - "@wrnexus/validation": "^0.2.18", - "@wrnexus/i18n": "^0.2.18", - "@wrnexus/db": "^0.2.18", - "@wrnexus/pubsub": "^0.2.18", - "@wrnexus/uploader": "^0.2.18" + "@wrnexus/core": "^0.2.19", + "@wrnexus/router": "^0.2.19", + "@wrnexus/ssr": "^0.2.19", + "@wrnexus/csr": "^0.2.19", + "@wrnexus/compiler": "^0.2.19", + "@wrnexus/styles": "^0.2.19", + "@wrnexus/ui": "^0.2.19", + "@wrnexus/validation": "^0.2.19", + "@wrnexus/i18n": "^0.2.19", + "@wrnexus/db": "^0.2.19", + "@wrnexus/pubsub": "^0.2.19", + "@wrnexus/uploader": "^0.2.19" }, "files": [ "dist" diff --git a/.publish/encryption/package.json b/.publish/encryption/package.json index dbbb5487..011c8d61 100644 --- a/.publish/encryption/package.json +++ b/.publish/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "@wrnexus/encryption — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/helpers/package.json b/.publish/helpers/package.json index 04f8ef0f..5e674bad 100644 --- a/.publish/helpers/package.json +++ b/.publish/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.18" + "@wrnexus/core": "^0.2.19" }, "files": [ "dist" diff --git a/.publish/i18n/package.json b/.publish/i18n/package.json index b7c77cca..efccad28 100644 --- a/.publish/i18n/package.json +++ b/.publish/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "@wrnexus/i18n — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.18" + "@wrnexus/core": "^0.2.19" }, "files": [ "dist" diff --git a/.publish/jwt/package.json b/.publish/jwt/package.json index d25533f4..c639446e 100644 --- a/.publish/jwt/package.json +++ b/.publish/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "@wrnexus/jwt — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/mobile/package.json b/.publish/mobile/package.json index fa454978..226d4109 100644 --- a/.publish/mobile/package.json +++ b/.publish/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "@wrnexus/mobile — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/native": "^0.2.18" + "@wrnexus/native": "^0.2.19" }, "files": [ "dist" diff --git a/.publish/native/package.json b/.publish/native/package.json index b203cf6a..a1c40fac 100644 --- a/.publish/native/package.json +++ b/.publish/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "@wrnexus/native — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/oauth/package.json b/.publish/oauth/package.json index e93ef3a0..b797e882 100644 --- a/.publish/oauth/package.json +++ b/.publish/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "@wrnexus/oauth — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/pubsub/package.json b/.publish/pubsub/package.json index fe75fe97..c095f509 100644 --- a/.publish/pubsub/package.json +++ b/.publish/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "@wrnexus/pubsub — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/queue/package.json b/.publish/queue/package.json index 732142e5..2f30fc68 100644 --- a/.publish/queue/package.json +++ b/.publish/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "@wrnexus/queue — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/reactive/package.json b/.publish/reactive/package.json index e9fce405..874523db 100644 --- a/.publish/reactive/package.json +++ b/.publish/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "@wrnexus/reactive — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/router/package.json b/.publish/router/package.json index 886d3de8..9871430e 100644 --- a/.publish/router/package.json +++ b/.publish/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "@wrnexus/router — part of the WrNexus framework.", "license": "MIT", @@ -21,8 +21,8 @@ } }, "dependencies": { - "@wrnexus/compiler": "^0.2.18", - "@wrnexus/core": "^0.2.18" + "@wrnexus/compiler": "^0.2.19", + "@wrnexus/core": "^0.2.19" }, "files": [ "dist" diff --git a/.publish/ssr/package.json b/.publish/ssr/package.json index 99a48ac3..36357c00 100644 --- a/.publish/ssr/package.json +++ b/.publish/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "@wrnexus/ssr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.18" + "@wrnexus/core": "^0.2.19" }, "files": [ "dist" diff --git a/.publish/styles/package.json b/.publish/styles/package.json index 61fac9aa..f1b25262 100644 --- a/.publish/styles/package.json +++ b/.publish/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "@wrnexus/styles — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/uploader": "^0.2.18" + "@wrnexus/uploader": "^0.2.19" }, "files": [ "dist" diff --git a/.publish/test/package.json b/.publish/test/package.json index da25dc1d..9540d59b 100644 --- a/.publish/test/package.json +++ b/.publish/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "@wrnexus/test — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/tracking/package.json b/.publish/tracking/package.json index 2901320c..4918e6d0 100644 --- a/.publish/tracking/package.json +++ b/.publish/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "@wrnexus/tracking — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/ui/package.json b/.publish/ui/package.json index 2a6abc59..5326e5df 100644 --- a/.publish/ui/package.json +++ b/.publish/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "@wrnexus/ui — part of the WrNexus framework.", "license": "MIT", @@ -22,7 +22,7 @@ "./ui.css": "./ui.css" }, "dependencies": { - "@wrnexus/core": "^0.2.18" + "@wrnexus/core": "^0.2.19" }, "files": [ "dist", diff --git a/.publish/uploader/package.json b/.publish/uploader/package.json index 62966d87..1a2f44e0 100644 --- a/.publish/uploader/package.json +++ b/.publish/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "@wrnexus/uploader — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.18" + "@wrnexus/core": "^0.2.19" }, "files": [ "dist" diff --git a/.publish/validation/package.json b/.publish/validation/package.json index cca2e0a0..ff67ddd5 100644 --- a/.publish/validation/package.json +++ b/.publish/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "description": "@wrnexus/validation — part of the WrNexus framework.", "license": "MIT", diff --git a/editors/vscode/CHANGELOG.md b/editors/vscode/CHANGELOG.md index 0a734c54..ac623fbf 100644 --- a/editors/vscode/CHANGELOG.md +++ b/editors/vscode/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.2.0 + +- Added native Format Document and format-on-save support for `.wrn` files. +- Added configurable formatter enablement through `wrnexus.format.enable`. + ## 0.1.0 - Initial Marketplace package for WrNexus `.wrn` language support. diff --git a/editors/vscode/README.md b/editors/vscode/README.md index 5dfafe80..031f6357 100644 --- a/editors/vscode/README.md +++ b/editors/vscode/README.md @@ -21,6 +21,8 @@ Bun-powered full-stack framework. `entity.other.attribute-name.wrn.directive` and `…wire.event`). - **Diagnostics.** Parse errors from the real `@wrnexus/compiler` are shown inline as you type, anchored to the exact offset. +- **Formatting.** Format Document and format-on-save normalize WRN block, HTML, + CSS, and function indentation without rewriting application expressions. - **Snippets.** `page`, `component`, `view`, `state`, `props`, `seo`, `api`, `ssr`, `client`, `realtime`, `functions`, `style`, plus view helpers `mount`, `for`, `show`, `t`. @@ -29,9 +31,10 @@ Bun-powered full-stack framework. ## Settings -| Setting | Default | Description | -| ---------------------------- | ------- | ----------------------------------- | -| `wrnexus.diagnostics.enable` | `true` | Show parse errors for `.wrn` files. | +| Setting | Default | Description | +| ---------------------------- | ------- | ----------------------------------------- | +| `wrnexus.diagnostics.enable` | `true` | Show parse errors for `.wrn` files. | +| `wrnexus.format.enable` | `true` | Enable the built-in formatter for `.wrn`. | ## Building the diagnostics engine diff --git a/editors/vscode/package-lock.json b/editors/vscode/package-lock.json index 9ce29756..463fb864 100644 --- a/editors/vscode/package-lock.json +++ b/editors/vscode/package-lock.json @@ -1,12 +1,12 @@ { "name": "wrnexus", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wrnexus", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "devDependencies": { "@vscode/vsce": "^3.9.2" diff --git a/editors/vscode/package.json b/editors/vscode/package.json index f7c6905a..5c10b7fd 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -1,8 +1,8 @@ { "name": "wrnexus", "displayName": "WrNexus (.wrn)", - "description": "Syntax highlighting, diagnostics, snippets and completions for WrNexus .wrn files.", - "version": "0.1.0", + "description": "Syntax highlighting, formatting, diagnostics, snippets and completions for WrNexus .wrn files.", + "version": "0.2.0", "publisher": "wrnexus", "license": "MIT", "repository": { @@ -25,7 +25,8 @@ "categories": [ "Programming Languages", "Snippets", - "Linters" + "Linters", + "Formatters" ], "keywords": [ "wire", @@ -90,6 +91,11 @@ "type": "boolean", "default": true, "description": "Show parse errors for .wrn files as you type." + }, + "wrnexus.format.enable": { + "type": "boolean", + "default": true, + "description": "Enable the built-in formatter for .wrn files." } } }, diff --git a/editors/vscode/src/compiler.cjs b/editors/vscode/src/compiler.cjs index f32379e2..dd1e430b 100644 --- a/editors/vscode/src/compiler.cjs +++ b/editors/vscode/src/compiler.cjs @@ -40,10 +40,13 @@ var __export = (target, all) => { var exports_src = {}; __export(exports_src, { parse: () => parse, + generateNative: () => generateNative, generate: () => generate, compileWireFile: () => compileWireFile, + compileNativeWireFile: () => compileNativeWireFile, compile: () => compile, ParseError: () => ParseError, + NativeCompileError: () => NativeCompileError, Lexer: () => Lexer, LexError: () => LexError }); @@ -678,7 +681,7 @@ function attrValue(attrs, name) { } function renderAttr(attr) { if (attr.event) - return ` data-on-${attr.name}="${attrEscape(attr.value)}"`; + return ` ${eventAttribute(attr.name)}="${attrEscape(attr.value)}"`; switch (attr.name) { case "api": case "ssrGet": @@ -690,6 +693,13 @@ function renderAttr(attr) { return attr.boolean ? ` ${attr.name}` : ` ${attr.name}="${attrEscape(attr.value)}"`; } } +function eventAttribute(name) { + if (name.startsWith("browser-")) + return `data-on-wrnexus-browser-${name.slice(8)}`; + if (name.startsWith("mobile-")) + return `data-on-wrnexus-mobile-${name.slice(7)}`; + return `data-on-${name}`; +} function renderAttrs(attrs, csrId) { const rendered = attrs.map(renderAttr).join(""); return csrId ? `${rendered} data-wrnexus-csr="${attrEscape(csrId)}"` : rendered; @@ -768,7 +778,7 @@ function renderLoopBody(node) { if (node.type === "if") return compileIfExpr(node); const attrs = node.attrs.map((a) => { - const name = a.event ? `data-on-${a.name}` : a.name; + const name = a.event ? eventAttribute(a.name) : a.name; if (a.boolean) return escLit(` ${name}`); return escLit(` ${name}="`) + bakeLoopAttr(a.value) + escLit(`"`); @@ -1176,7 +1186,7 @@ function renderComponentNode(node, ctx) { if (node.type === "each" || node.type === "if") { throw new Error("Server `{#each}` / `{#if}` blocks are supported in pages, not components. Move them into a page (or use data-for / data-show on the client)."); } - const attrs = node.attrs.map((a) => a.event ? ` data-on-${a.name}="${compileAttrValue(a.value, ctx)}"` : a.boolean ? ` ${a.name}` : ` ${a.name}="${compileAttrValue(a.value, ctx)}"`).join(""); + const attrs = node.attrs.map((a) => a.event ? ` ${eventAttribute(a.name)}="${compileAttrValue(a.value, ctx)}"` : a.boolean ? ` ${a.name}` : ` ${a.name}="${compileAttrValue(a.value, ctx)}"`).join(""); const loops = loopVarsOf(node); const childCtx = loops.length > 0 ? { ...ctx, loopVars: new Set([...ctx.loopVars ?? [], ...loops]) } : ctx; if (VOID_ELEMENTS.has(node.tag.toLowerCase())) @@ -1262,7 +1272,211 @@ function __wireAttr(v: any): string { `; } +// ../../packages/compiler/src/native-codegen.ts +class NativeCompileError extends Error { + constructor(message) { + super(message); + this.name = "NativeCompileError"; + } +} +var tagMap = { + div: "View", + main: "View", + section: "View", + article: "View", + nav: "View", + header: "View", + footer: "View", + aside: "View", + form: "View", + ul: "View", + ol: "View", + li: "View", + p: "Text", + span: "Text", + strong: "Text", + em: "Text", + small: "Text", + label: "Text", + h1: "Text", + h2: "Text", + h3: "Text", + h4: "Text", + h5: "Text", + h6: "Text", + button: "Pressable", + a: "Pressable", + input: "TextInput", + textarea: "TextInput", + img: "Image", + view: "View", + text: "Text", + pressable: "Pressable", + textinput: "TextInput", + image: "Image", + scrollview: "ScrollView", + safeareaview: "SafeAreaView", + flatlist: "FlatList", + activityindicator: "ActivityIndicator" +}; +var attrMap = { + class: "style", + className: "style", + src: "source", + alt: "accessibilityLabel", + placeholder: "placeholder", + disabled: "disabled", + value: "value", + href: "__href", + "aria-label": "accessibilityLabel" +}; +function expression(value) { + const exact = /^\{([\s\S]+)\}$/.exec(value.trim()); + return exact?.[1]?.trim() ?? null; +} +function textJsx(value) { + const pieces = []; + let last = 0; + for (const match of value.matchAll(/\{([^{}]+)\}/g)) { + if (match.index > last) + pieces.push(value.slice(last, match.index)); + const expr = match[1].trim(); + pieces.push(expr.startsWith("t:") ? `{${JSON.stringify(expr.slice(2).trim())}}` : `{${expr}}`); + last = match.index + match[0].length; + } + pieces.push(value.slice(last)); + return pieces.join("").replace(/([<>])/g, (char) => char === "<" ? "<" : ">"); +} +function eventBody(value, states) { + let body = expression(value) ?? value; + for (const state of states) { + const cap = state[0].toUpperCase() + state.slice(1); + body = body.replace(new RegExp(`\\b${state}\\+\\+`, "g"), `set${cap}(value => value + 1)`).replace(new RegExp(`\\b${state}--`, "g"), `set${cap}(value => value - 1)`).replace(new RegExp(`\\b${state}\\s*=\\s*([^;]+)`, "g"), `set${cap}($1)`); + } + return `() => { ${body} }`; +} +function renderAttrs2(attrs, states) { + return attrs.map((attr) => { + if (attr.event) { + if (attr.name.startsWith("browser-")) + return ""; + const eventName = attr.name.startsWith("mobile-") ? attr.name.slice(7) : attr.name; + const event = eventName === "click" || eventName === "press" ? "onPress" : eventName === "input" || eventName === "change" ? "onChangeText" : `on${eventName[0].toUpperCase()}${eventName.slice(1)}`; + return ` ${event}={${eventBody(attr.value, states)}}`; + } + if (attr.name === "data-native-browser" || attr.name.startsWith("data-native-on-browser-")) + return ""; + if (attr.name === "data-native-options" || attr.name === "data-native-only" || attr.name === "data-native-requires" || attr.name === "data-native-unsupported") + return ""; + if (attr.name === "data-native-mobile") { + throw new NativeCompileError(`Declarative native capability "${attr.value}" currently targets browser/Capacitor pages. In Expo output, call the installed Expo package from an @mobile-event handler.`); + } + const name = attrMap[attr.name] ?? attr.name; + if (name === "__href") + return ` onPress={() => router.push(${JSON.stringify(attr.value)})}`; + if (name === "source") { + const expr2 = expression(attr.value); + return ` source={${expr2 ? `{ uri: ${expr2} }` : `{ uri: ${JSON.stringify(attr.value)} }`}}`; + } + if (name === "style" && attr.name !== "style") { + return ` style={[${attr.value.split(/\s+/).filter(Boolean).map((value) => `styles[${JSON.stringify(value)}]`).join(", ")} ]}`; + } + if (name === "style") { + const inlineExpression = expression(attr.value); + if (inlineExpression) + return ` style={${inlineExpression}}`; + throw new NativeCompileError('Inline CSS strings are not portable to native; use class="name" and a page style block'); + } + if (attr.boolean) + return ` ${name}`; + const expr = expression(attr.value); + return expr ? ` ${name}={${expr}}` : ` ${name}=${JSON.stringify(attr.value)}`; + }).join(""); +} +function renderNode2(node, states, key) { + if (node.type === "text") + return textJsx(node.value); + if (node.type === "each") { + const params = node.index ? `${node.item}, ${node.index}` : `${node.item}, __index`; + const body = node.body.map((child, index) => renderNode2(child, states, index === 0 ? node.index ?? "__index" : undefined)).join(""); + const empty = node.empty.map((child) => renderNode2(child, states)).join(""); + return `{(${node.list})?.length ? (${node.list}).map((${params}) => <>${body}) : <>${empty}}`; + } + if (node.type === "if") { + const result = node.branches.reduceRight((fallback, branch) => branch.cond === null ? `<>${branch.body.map((child) => renderNode2(child, states)).join("")}` : `(${branch.cond}) ? <>${branch.body.map((child) => renderNode2(child, states)).join("")} : ${fallback}`, "null"); + return `{${result}}`; + } + const nativeOnly = node.attrs.find((attr) => !attr.event && attr.name === "data-native-only")?.value; + if (nativeOnly === "browser" || nativeOnly === "web") + return ""; + const nativeTag = tagMap[node.tag.toLowerCase()] ?? (/^[A-Z]/.test(node.tag) ? node.tag : undefined); + if (!nativeTag) + throw new NativeCompileError(`HTML element <${node.tag}> has no native equivalent`); + const attrs = renderAttrs2(node.attrs, states) + (key ? ` key={${key}}` : ""); + if (nativeTag === "TextInput" || nativeTag === "Image" || nativeTag === "ActivityIndicator") + return `<${nativeTag}${attrs} />`; + const children = node.children.map((child) => { + if (child.type !== "text") + return renderNode2(child, states); + if (!child.value.trim()) + return ""; + const text = textJsx(child.value); + return nativeTag === "Text" ? text : `${text}`; + }).join(""); + return `<${nativeTag}${attrs}>${children}`; +} +function nativeStyles(blocks) { + const entries = []; + for (const block of blocks) { + for (const match of block.matchAll(/\.([A-Za-z_][\w-]*)\s*\{([^}]*)\}/g)) { + const props = []; + for (const declaration of match[2].split(";")) { + const colon = declaration.indexOf(":"); + if (colon < 0) + continue; + const name = declaration.slice(0, colon).trim().replace(/-([a-z])/g, (_, c) => c.toUpperCase()); + let value = declaration.slice(colon + 1).trim(); + if (/^-?\d+(?:\.\d+)?px$/.test(value)) + value = Number(value.slice(0, -2)); + props.push(`${JSON.stringify(name)}: ${typeof value === "number" ? value : JSON.stringify(value)}`); + } + entries.push(`${JSON.stringify(match[1])}: { ${props.join(", ")} }`); + } + } + return `const styles = StyleSheet.create({ ${entries.join(`, +`)} });`; +} +function generateNative(ast) { + if (ast.kind !== "page") + throw new NativeCompileError("Native route compilation currently accepts page files only"); + if (ast.dataApis.length) + throw new NativeCompileError("Data API blocks are not yet portable to native screens; fetch through the generated native backend helper"); + const states = new Set(ast.states.map((state) => state.name)); + const hooks = ast.states.map((state) => { + const cap = state.name[0].toUpperCase() + state.name.slice(1); + return ` const [${state.name}, set${cap}] = useState(${state.expr});`; + }).join(` +`); + const body = ast.view.map((node) => renderNode2(node, states)).join(""); + return `// generated from .wrn for Expo/React Native +import React, { useState } from "react"; +import { ActivityIndicator, FlatList, Image, Pressable, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View } from "react-native"; +import { useRouter } from "expo-router"; + +export default function ${ast.name}() { + const router = useRouter(); +${hooks} + return <>${body}; +} + +${nativeStyles(ast.styles)} +`; +} + // ../../packages/compiler/src/index.ts +function compileNativeWireFile(source) { + return generateNative(parse(source)); +} function compileWireFile(source) { const ast = parse(source); return `// compiled from .wrn diff --git a/editors/vscode/src/extension.js b/editors/vscode/src/extension.js index 0a3331e7..a85b0af3 100644 --- a/editors/vscode/src/extension.js +++ b/editors/vscode/src/extension.js @@ -2,6 +2,7 @@ "use strict"; const vscode = require("vscode"); +const { formatWrn } = require("./formatter.js"); // The .wrn compiler, bundled to CJS by `bun run build:compiler`. Loaded // defensively so the rest of the extension (highlighting, snippets, completion) @@ -112,6 +113,17 @@ function activate(context) { // Completions. context.subscriptions.push( + vscode.languages.registerDocumentFormattingEditProvider("wrn", { + provideDocumentFormattingEdits(document, options) { + if (!vscode.workspace.getConfiguration("wrnexus").get("format.enable", true)) return []; + const formatted = formatWrn(document.getText(), options); + if (formatted === document.getText()) return []; + const end = document.positionAt(document.getText().length); + return [ + vscode.TextEdit.replace(new vscode.Range(new vscode.Position(0, 0), end), formatted), + ]; + }, + }), vscode.languages.registerCompletionItemProvider( "wrn", { provideCompletionItems: provideCompletions }, diff --git a/editors/vscode/src/formatter.js b/editors/vscode/src/formatter.js new file mode 100644 index 00000000..a8eba438 --- /dev/null +++ b/editors/vscode/src/formatter.js @@ -0,0 +1,117 @@ +"use strict"; + +const VOID_ELEMENTS = new Set([ + "area", + "base", + "br", + "col", + "embed", + "hr", + "img", + "input", + "link", + "meta", + "param", + "source", + "track", + "wbr", +]); + +/** Count structural curly braces while ignoring quoted strings and line comments. */ +function curlyDelta(line, state) { + let delta = 0; + let quote = null; + let escaped = false; + + for (let index = 0; index < line.length; index++) { + const char = line[index]; + const next = line[index + 1]; + + if (state.blockComment) { + if (char === "*" && next === "/") { + state.blockComment = false; + index++; + } + continue; + } + if (state.template) { + if (!escaped && char === "`") state.template = false; + escaped = !escaped && char === "\\"; + if (char !== "\\") escaped = false; + continue; + } + if (quote) { + if (!escaped && char === quote) quote = null; + escaped = !escaped && char === "\\"; + if (char !== "\\") escaped = false; + continue; + } + if (char === "/" && next === "*") { + state.blockComment = true; + index++; + } else if (char === "/" && next === "/") { + break; + } else if (char === "`") { + state.template = true; + } else if (char === '"' || char === "'") { + quote = char; + } else if (char === "{") { + delta++; + } else if (char === "}") { + delta--; + } + } + return delta; +} + +/** Return the net nesting introduced by HTML-like tags on this line. */ +function htmlDelta(line) { + let delta = 0; + const tags = line.matchAll(/<\s*(\/?)\s*([A-Za-z][\w:-]*)\b[^>]*>/g); + for (const match of tags) { + const full = match[0]; + const closing = match[1] === "/"; + const name = match[2].toLowerCase(); + if (closing) delta--; + else if (!VOID_ELEMENTS.has(name) && !/\/\s*>$/.test(full)) delta++; + } + return delta; +} + +function leadingClosers(line) { + let count = /^\s*}/.test(line) ? 1 : 0; + const tags = line.match(/^\s*((?:<\/\s*[A-Za-z][\w:-]*\s*>\s*)+)/); + if (tags) count += [...tags[1].matchAll(/<\//g)].length; + return count; +} + +/** + * Format WRN source conservatively: normalize structural indentation and + * trailing whitespace without rewriting expressions, HTML, CSS, or JS. + */ +function formatWrn(text, options = {}) { + const tabSize = Math.max(1, Number(options.tabSize) || 2); + const unit = options.insertSpaces === false ? "\t" : " ".repeat(tabSize); + const hadFinalNewline = /\r?\n$/.test(text); + const lines = text.replace(/\r\n/g, "\n").split("\n"); + if (hadFinalNewline) lines.pop(); + + let depth = 0; + const state = { blockComment: false, template: false }; + const formatted = lines.map((original) => { + const line = original.trimEnd(); + if (!line.trim()) return ""; + + // Preserve multiline template-string content because its whitespace can be data. + const wasTemplate = state.template; + const trimmed = line.trimStart(); + const indent = Math.max(0, depth - leadingClosers(trimmed)); + const output = wasTemplate ? line : unit.repeat(indent) + trimmed; + depth = Math.max(0, depth + curlyDelta(trimmed, state) + htmlDelta(trimmed)); + return output; + }); + + return formatted.join("\n") + (hadFinalNewline ? "\n" : ""); +} + +module.exports = { formatWrn }; diff --git a/editors/vscode/test/validate.mjs b/editors/vscode/test/validate.mjs index 1166c41d..01e26e41 100644 --- a/editors/vscode/test/validate.mjs +++ b/editors/vscode/test/validate.mjs @@ -58,5 +58,19 @@ try { bad("compiler bundle loads", e.message); } +// 4. Formatter normalizes nested WRN blocks and is idempotent. +try { + const { formatWrn } = require(join(root, "src/formatter.js")); + const source = `page Home {\nview {\n
\n

Hello

\n
\n}\n}\n`; + const expected = `page Home {\n view {\n
\n

Hello

\n
\n }\n}\n`; + const formatted = formatWrn(source, { insertSpaces: true, tabSize: 2 }); + formatted === expected ? ok("formatter indents WRN blocks") : bad("formatter indents WRN blocks"); + formatWrn(formatted, { insertSpaces: true, tabSize: 2 }) === formatted + ? ok("formatter is idempotent") + : bad("formatter is idempotent"); +} catch (e) { + bad("formatter loads", e.message); +} + console.log(failures === 0 ? "\nAll extension checks passed." : `\n${failures} check(s) failed.`); process.exit(failures === 0 ? 0 : 1); diff --git a/editors/vscode/wrnexus-0.2.0.vsix b/editors/vscode/wrnexus-0.2.0.vsix new file mode 100644 index 0000000000000000000000000000000000000000..e34cf5d5adc778cf8f7937746a1c015d511d7f81 GIT binary patch literal 26944 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 = [ + [ + settings, + JSON.stringify( + { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, + "prettier.requireConfig": true, + "[wrn]": { + "editor.defaultFormatter": "wrnexus.wrnexus", + "editor.formatOnSave": true, + }, + }, + null, + 2, + ) + "\n", + ".vscode/settings.json", + ], + [ + extensions, + JSON.stringify( + { + recommendations: [ + "wrnexus.wrnexus", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + ], + }, + null, + 2, + ) + "\n", + ".vscode/extensions.json", + ], + ]; + + for (const [file, contents, label] of files) { + if (existsSync(file)) continue; + ctx.log(`+ ${label}`); + if (!ctx.dryRun) { + mkdirSync(vscode, { recursive: true }); + writeFileSync(file, contents, "utf8"); + } + } + + const prettier = existsSync(prettierIgnore) ? readFileSync(prettierIgnore, "utf8") : ""; + if (!new Set(prettier.split(/\r?\n/).map((line) => line.trim())).has("CLAUDE.md")) { + ctx.log("+ .prettierignore: CLAUDE.md"); + if (!ctx.dryRun) { + writeFileSync(prettierIgnore, `${prettier.replace(/\s*$/, "")}\nCLAUDE.md\n`, "utf8"); + } + } + + if (existsSync(gitignore)) { + const current = readFileSync(gitignore, "utf8"); + const lines = current.split(/\r?\n/); + const vscodeIgnore = lines.findIndex((line) => line.trim() === ".vscode/"); + const needed = [".vscode/*", "!.vscode/settings.json", "!.vscode/extensions.json"]; + if (vscodeIgnore >= 0 || needed.some((entry) => !lines.includes(entry))) { + ctx.log("~ .gitignore: track shared VS Code configuration"); + if (!ctx.dryRun) { + const filtered = lines.filter((line) => line.trim() !== ".vscode/"); + const have = new Set(filtered.map((line) => line.trim())); + for (const entry of needed) if (!have.has(entry)) filtered.push(entry); + writeFileSync(gitignore, filtered.join("\n").replace(/\n*$/, "\n"), "utf8"); + } + } + } + }, + }, ]; /** Release tooling uses this to require an explicit migration entry per version. */ @@ -246,6 +327,9 @@ function backupProjectFiles(appRoot: string, from: string, target: string): stri "wrnexus.config.mjs", "tsconfig.json", ".gitignore", + ".prettierignore", + ".vscode/settings.json", + ".vscode/extensions.json", "CLAUDE.md", "public/llms.txt", ]) { diff --git a/packages/cli/test/create.test.ts b/packages/cli/test/create.test.ts index 4f3fffe7..bead6d81 100644 --- a/packages/cli/test/create.test.ts +++ b/packages/cli/test/create.test.ts @@ -2,6 +2,7 @@ import { expect, test } from "bun:test"; import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { format } from "prettier"; import { scaffoldApp } from "../src/create.ts"; import { currentCliVersion } from "../src/update-notifier.ts"; @@ -69,6 +70,15 @@ test("scaffoldApp pins the current framework and exposes llms.txt publicly", () expect(existsSync(join(root, "public", "llms.txt"))).toBe(true); expect(existsSync(join(root, "llms.txt"))).toBe(false); expect(readFileSync(join(root, ".prettierignore"), "utf8")).toContain("CLAUDE.md"); + expect(readFileSync(join(root, ".vscode", "settings.json"), "utf8")).toContain( + '"editor.formatOnSave": true', + ); + expect(readFileSync(join(root, ".vscode", "settings.json"), "utf8")).toContain( + '"editor.defaultFormatter": "wrnexus.wrnexus"', + ); + expect(readFileSync(join(root, ".vscode", "extensions.json"), "utf8")).toContain( + "wrnexus.wrnexus", + ); expect(readFileSync(join(root, "app", "pages", "index.wrn"), "utf8")).toContain( 'href="/api/hello"', ); @@ -77,3 +87,34 @@ test("scaffoldApp pins the current framework and exposes llms.txt publicly", () rmSync(parent, { recursive: true, force: true }); } }); + +test("scaffoldApp emits Prettier-compliant editor and framework files", async () => { + const parent = mkdtempSync(join(tmpdir(), "wrnexus-create-")); + const root = join(parent, "formatted-app"); + + try { + scaffoldApp(root, "formatted-app"); + for (const relative of [ + "app/styles/global.css", + "eslint.config.js", + ".vscode/settings.json", + ".vscode/extensions.json", + ]) { + const file = join(root, relative); + const source = readFileSync(file, "utf8"); + const formatted = await format(source, { + filepath: file, + printWidth: 100, + tabWidth: 2, + useTabs: false, + semi: true, + singleQuote: false, + trailingComma: "all", + endOfLine: "lf", + }); + expect(formatted).toBe(source); + } + } finally { + rmSync(parent, { recursive: true, force: true }); + } +}); diff --git a/packages/cli/test/update.test.ts b/packages/cli/test/update.test.ts index e9e9bc3b..2274db00 100644 --- a/packages/cli/test/update.test.ts +++ b/packages/cli/test/update.test.ts @@ -61,3 +61,35 @@ test("0.2.18 migration adds helpers to existing apps and is idempotent", () => { rmSync(root, { recursive: true, force: true }); } }); + +test("0.2.19 migration adds VS Code formatting defaults without overwriting settings", () => { + const root = mkdtempSync(join(tmpdir(), "wrnexus-update-")); + mkdirSync(join(root, "app", "pages"), { recursive: true }); + mkdirSync(join(root, ".vscode"), { recursive: true }); + writeFileSync( + join(root, "package.json"), + JSON.stringify({ name: "editor-app", wrnexus: { version: "0.2.18" } }), + ); + writeFileSync(join(root, ".gitignore"), ".vscode/\n"); + writeFileSync(join(root, ".prettierignore"), "node_modules/\n"); + writeFileSync(join(root, ".vscode", "settings.json"), '{"editor.tabSize":4}\n'); + + try { + updateApp(root, "0.2.19", false); + updateApp(root, "0.2.19", false); + + expect(readFileSync(join(root, ".vscode", "settings.json"), "utf8")).toBe( + '{"editor.tabSize":4}\n', + ); + expect(readFileSync(join(root, ".vscode", "extensions.json"), "utf8")).toContain( + "wrnexus.wrnexus", + ); + expect(readFileSync(join(root, ".prettierignore"), "utf8")).toBe("node_modules/\nCLAUDE.md\n"); + const gitignore = readFileSync(join(root, ".gitignore"), "utf8"); + expect(gitignore).not.toContain(".vscode/\n"); + expect(gitignore).toContain("!.vscode/settings.json"); + expect(gitignore).toContain("!.vscode/extensions.json"); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 1eb26f61..f1fff46f 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/core/package.json b/packages/core/package.json index 79ca1ee0..b0a0758d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/csr/package.json b/packages/csr/package.json index 444d96d3..3e17a63d 100644 --- a/packages/csr/package.json +++ b/packages/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/db/package.json b/packages/db/package.json index 541fc3ee..747d1341 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.2.18", + "version": "0.2.19", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/dev-server/README.md b/packages/dev-server/README.md index 105e193f..6e45c204 100644 --- a/packages/dev-server/README.md +++ b/packages/dev-server/README.md @@ -142,7 +142,7 @@ interface ProdOptions { ### `startGateway(opts)` — multi-app gateway -Serves several apps behind one port and routes each request to the right app by its `Host` header. Each app runs as its own child process (full isolation); the gateway is a thin host-based reverse proxy for HTTP and WebSocket. Apps communicate at runtime via `@wrnexus/pubsub` (use the Redis driver so messages cross processes). +Serves several apps behind one port and routes each request to the right app by its `Host` header. Each app runs as its own child process (full isolation); the gateway is a thin host-based reverse proxy for HTTP and WebSocket. In development, the gateway supervises every child: editing a page, component, API, or middleware restarts only that app and the HMR client reconnects to display the latest page. Apps communicate at runtime via `@wrnexus/pubsub` (use the Redis driver so messages cross processes). ```ts interface GatewayOptions { diff --git a/packages/dev-server/package.json b/packages/dev-server/package.json index 42017e87..0ed95623 100644 --- a/packages/dev-server/package.json +++ b/packages/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/dev-server/src/gateway.ts b/packages/dev-server/src/gateway.ts index a40f4bf1..3bd20258 100644 --- a/packages/dev-server/src/gateway.ts +++ b/packages/dev-server/src/gateway.ts @@ -12,6 +12,7 @@ import { spawn, type ChildProcess } from "node:child_process"; import { join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { RESTART_EXIT_CODE } from "./restart.ts"; /** Per-app access control, enforced at the gateway before proxying. */ export interface GatewayAuth { @@ -71,7 +72,7 @@ export interface RunningGateway { interface Target extends GatewayApp { port: number; origin: string; - child: ChildProcess; + child?: ChildProcess; } interface WsBridge { @@ -81,6 +82,18 @@ interface WsBridge { queue: (string | ArrayBufferLike | ArrayBufferView)[]; } +/** Decide whether a gateway child should be relaunched after it exits. */ +export function gatewayRestartDelay( + mode: "development" | "production", + code: number | null, + signal: NodeJS.Signals | null, +): number | null { + if (mode !== "development" || signal) return null; + if (code === RESTART_EXIT_CODE) return 0; + if (code && code !== 0) return 1200; + return null; +} + /** Fixed-window rate limiter keyed by client IP. */ function makeRateLimiter(max: number, windowMs: number) { const hits = new Map(); @@ -196,9 +209,9 @@ function applyEdgeHeaders(res: Response): Response { async function waitReady(target: Target, timeoutMs = 15000): Promise { const deadline = Date.now() + timeoutMs; for (;;) { - if (target.child.exitCode !== null) { + if (!target.child || target.child.exitCode !== null) { throw new Error( - `app '${target.name}' exited with code ${target.child.exitCode} during startup`, + `app '${target.name}' exited with code ${target.child?.exitCode ?? "unknown"} during startup`, ); } try { @@ -245,28 +258,52 @@ export async function startGateway(opts: GatewayOptions): Promise { const appPort = app.port ?? port + 1 + i; const dir = resolve(app.dir); - const child = - mode === "production" - ? spawn(process.execPath, [join(dir, "dist", "server.js")], { - stdio: "inherit", - env: { ...process.env, PORT: String(appPort) }, - }) - : spawn( - process.execPath, - [serveEntry, join(dir, "app"), String(appPort), mode, "127.0.0.1"], - { + const target: Target = { + ...app, + port: appPort, + origin: `http://127.0.0.1:${appPort}`, + }; + + const launch = (): void => { + if (stopping) return; + const child = + mode === "production" + ? spawn(process.execPath, [join(dir, "dist", "server.js")], { stdio: "inherit", - }, - ); - return { ...app, port: appPort, origin: `http://127.0.0.1:${appPort}`, child }; + env: { ...process.env, PORT: String(appPort) }, + }) + : spawn( + process.execPath, + [serveEntry, join(dir, "app"), String(appPort), mode, "127.0.0.1"], + { stdio: "inherit" }, + ); + target.child = child; + + child.once("exit", (code, signal) => { + if (stopping || target.child !== child) return; + const delay = gatewayRestartDelay(mode, code, signal); + if (delay === null) return; + if (delay > 0) { + console.error(`[wrnexus] app '${target.name}' exited (code ${code}); retrying in 1.2s…`); + setTimeout(launch, delay); + } else { + launch(); + } + }); + }; + + launch(); + return target; }); const stopChildren = () => { + stopping = true; for (const target of targets) { - if (target.child.exitCode === null) target.child.kill(); + if (target.child?.exitCode === null) target.child.kill(); } }; diff --git a/packages/dev-server/src/index.ts b/packages/dev-server/src/index.ts index c1ededc6..00855b2f 100644 --- a/packages/dev-server/src/index.ts +++ b/packages/dev-server/src/index.ts @@ -28,9 +28,9 @@ import { createHandlers, type WsData } from "./runtime.ts"; import { createDevAssetServer } from "./assets.ts"; import { HmrHub } from "./hmr.ts"; import { startWatcher } from "./watch.ts"; +import { RESTART_EXIT_CODE } from "./restart.ts"; -/** Exit code the child uses to ask the dev supervisor for a fresh process. */ -export const RESTART_EXIT_CODE = 97; +export { RESTART_EXIT_CODE } from "./restart.ts"; export interface ServeOptions { appDir: string; diff --git a/packages/dev-server/src/restart.ts b/packages/dev-server/src/restart.ts new file mode 100644 index 00000000..5ac73cdc --- /dev/null +++ b/packages/dev-server/src/restart.ts @@ -0,0 +1,2 @@ +/** Exit code a dev-server child uses to request a clean supervisor restart. */ +export const RESTART_EXIT_CODE = 97; diff --git a/packages/dev-server/test/gateway.test.ts b/packages/dev-server/test/gateway.test.ts index 81983401..d8b09f93 100644 --- a/packages/dev-server/test/gateway.test.ts +++ b/packages/dev-server/test/gateway.test.ts @@ -4,6 +4,7 @@ import { forwardAuthFailure, forwardAuthHeaders, gatewayProxyHeaders, + gatewayRestartDelay, } from "../src/gateway.ts"; test("gateway uses platform-safe hostname defaults", () => { @@ -56,3 +57,11 @@ test("forward auth describes the original gateway request", () => { expect(headers.get("cookie")).toBe("session=abc"); expect(headers.get("authorization")).toBe("Bearer token"); }); + +test("gateway respawns development apps after an HMR restart exit", () => { + expect(gatewayRestartDelay("development", 97, null)).toBe(0); + expect(gatewayRestartDelay("development", 1, null)).toBe(1200); + expect(gatewayRestartDelay("development", 0, null)).toBeNull(); + expect(gatewayRestartDelay("development", 97, "SIGTERM")).toBeNull(); + expect(gatewayRestartDelay("production", 97, null)).toBeNull(); +}); diff --git a/packages/encryption/package.json b/packages/encryption/package.json index 5189e521..061a518d 100644 --- a/packages/encryption/package.json +++ b/packages/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.2.18", + "version": "0.2.19", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/helpers/package.json b/packages/helpers/package.json index f549cc34..e0695618 100644 --- a/packages/helpers/package.json +++ b/packages/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.2.18", + "version": "0.2.19", "private": true, "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", diff --git a/packages/i18n/package.json b/packages/i18n/package.json index b56bda94..d4334a2f 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.2.18", + "version": "0.2.19", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/jwt/package.json b/packages/jwt/package.json index 5996535e..85802952 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.2.18", + "version": "0.2.19", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 9d134e45..422a7ba3 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/native/package.json b/packages/native/package.json index 01188db0..e26e722e 100644 --- a/packages/native/package.json +++ b/packages/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/oauth/package.json b/packages/oauth/package.json index c10437c5..f6be6bbc 100644 --- a/packages/oauth/package.json +++ b/packages/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.2.18", + "version": "0.2.19", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/pubsub/package.json b/packages/pubsub/package.json index 341def4e..d5f13b71 100644 --- a/packages/pubsub/package.json +++ b/packages/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.2.18", + "version": "0.2.19", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/queue/package.json b/packages/queue/package.json index 5f793572..3ce85456 100644 --- a/packages/queue/package.json +++ b/packages/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.2.18", + "version": "0.2.19", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/reactive/package.json b/packages/reactive/package.json index f9cfa726..fe5bd702 100644 --- a/packages/reactive/package.json +++ b/packages/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/router/package.json b/packages/router/package.json index c2c4a0f6..7636c0bc 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/ssr/package.json b/packages/ssr/package.json index a4222f1e..f51678b3 100644 --- a/packages/ssr/package.json +++ b/packages/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/styles/package.json b/packages/styles/package.json index e93c06bd..28a655e2 100644 --- a/packages/styles/package.json +++ b/packages/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.2.18", + "version": "0.2.19", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/test/package.json b/packages/test/package.json index a4d9f814..30ac988c 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.2.18", + "version": "0.2.19", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/tracking/package.json b/packages/tracking/package.json index 8e02a8b7..675299ac 100644 --- a/packages/tracking/package.json +++ b/packages/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.2.18", + "version": "0.2.19", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index 9458e237..ad859b63 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.2.18", + "version": "0.2.19", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/uploader/package.json b/packages/uploader/package.json index 6a6e4d2f..6952029c 100644 --- a/packages/uploader/package.json +++ b/packages/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.2.18", + "version": "0.2.19", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/validation/package.json b/packages/validation/package.json index ba991f34..e4dca774 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.2.18", + "version": "0.2.19", "private": true, "type": "module", "main": "src/index.ts",