feat: keep dev server alive during HMR updates
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ai",
|
"name": "@wrnexus/ai",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
|
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/authz",
|
"name": "@wrnexus/authz",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/authz — part of the WrNexus framework.",
|
"description": "@wrnexus/authz — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
+11
-11
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/cli",
|
"name": "@wrnexus/cli",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/cli — part of the WrNexus framework.",
|
"description": "@wrnexus/cli — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -28,16 +28,16 @@
|
|||||||
"wrnexus": "./dist/index.js"
|
"wrnexus": "./dist/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.2.20",
|
"@wrnexus/core": "^0.2.21",
|
||||||
"@wrnexus/router": "^0.2.20",
|
"@wrnexus/router": "^0.2.21",
|
||||||
"@wrnexus/csr": "^0.2.20",
|
"@wrnexus/csr": "^0.2.21",
|
||||||
"@wrnexus/compiler": "^0.2.20",
|
"@wrnexus/compiler": "^0.2.21",
|
||||||
"@wrnexus/styles": "^0.2.20",
|
"@wrnexus/styles": "^0.2.21",
|
||||||
"@wrnexus/dev-server": "^0.2.20",
|
"@wrnexus/dev-server": "^0.2.21",
|
||||||
"@wrnexus/ui": "^0.2.20",
|
"@wrnexus/ui": "^0.2.21",
|
||||||
"@wrnexus/validation": "^0.2.20",
|
"@wrnexus/validation": "^0.2.21",
|
||||||
"@wrnexus/i18n": "^0.2.20",
|
"@wrnexus/i18n": "^0.2.21",
|
||||||
"@wrnexus/db": "^0.2.20"
|
"@wrnexus/db": "^0.2.21"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/compiler",
|
"name": "@wrnexus/compiler",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/compiler — part of the WrNexus framework.",
|
"description": "@wrnexus/compiler — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/core",
|
"name": "@wrnexus/core",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/core — part of the WrNexus framework.",
|
"description": "@wrnexus/core — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/csr",
|
"name": "@wrnexus/csr",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/csr — part of the WrNexus framework.",
|
"description": "@wrnexus/csr — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.2.20"
|
"@wrnexus/core": "^0.2.21"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/db",
|
"name": "@wrnexus/db",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/db — part of the WrNexus framework.",
|
"description": "@wrnexus/db — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ interface RunningServer {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
In development, `startServer` also connects `app/db/migrations` (and `app/db/<name>/migrations`) and auto-applies migrations, then starts an in-process file watcher. CSS edits hot-swap live; any other server change triggers `process.exit(RESTART_EXIT_CODE)` so the dev supervisor (`@wrnexus/cli`) respawns the process with fresh modules.
|
In development, `startServer` also connects `app/db/migrations` (and `app/db/<name>/migrations`) and auto-applies migrations, then starts an in-process file watcher. CSS edits hot-swap live. Page, component, layout, API, middleware, realtime, schema, locale, and public-asset edits invalidate only their cached modules, rescan routes where necessary, and morph fresh HTML through the existing HMR WebSocket. The server process and active gateway stay running.
|
||||||
|
|
||||||
### `createHandlers(deps)`
|
### `createHandlers(deps)`
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ interface ProdOptions {
|
|||||||
|
|
||||||
### `startGateway(opts)` — multi-app gateway
|
### `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. 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).
|
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, normal application edits are applied inside the existing child and sent through its existing HMR connection. The child supervisor remains as crash recovery rather than the normal update path. Apps communicate at runtime via `@wrnexus/pubsub` (use the Redis driver so messages cross processes).
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
interface GatewayOptions {
|
interface GatewayOptions {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/dev-server",
|
"name": "@wrnexus/dev-server",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/dev-server — part of the WrNexus framework.",
|
"description": "@wrnexus/dev-server — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -25,18 +25,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.2.20",
|
"@wrnexus/core": "^0.2.21",
|
||||||
"@wrnexus/router": "^0.2.20",
|
"@wrnexus/router": "^0.2.21",
|
||||||
"@wrnexus/ssr": "^0.2.20",
|
"@wrnexus/ssr": "^0.2.21",
|
||||||
"@wrnexus/csr": "^0.2.20",
|
"@wrnexus/csr": "^0.2.21",
|
||||||
"@wrnexus/compiler": "^0.2.20",
|
"@wrnexus/compiler": "^0.2.21",
|
||||||
"@wrnexus/styles": "^0.2.20",
|
"@wrnexus/styles": "^0.2.21",
|
||||||
"@wrnexus/ui": "^0.2.20",
|
"@wrnexus/ui": "^0.2.21",
|
||||||
"@wrnexus/validation": "^0.2.20",
|
"@wrnexus/validation": "^0.2.21",
|
||||||
"@wrnexus/i18n": "^0.2.20",
|
"@wrnexus/i18n": "^0.2.21",
|
||||||
"@wrnexus/db": "^0.2.20",
|
"@wrnexus/db": "^0.2.21",
|
||||||
"@wrnexus/pubsub": "^0.2.20",
|
"@wrnexus/pubsub": "^0.2.21",
|
||||||
"@wrnexus/uploader": "^0.2.20"
|
"@wrnexus/uploader": "^0.2.21"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/encryption",
|
"name": "@wrnexus/encryption",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/encryption — part of the WrNexus framework.",
|
"description": "@wrnexus/encryption — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/helpers",
|
"name": "@wrnexus/helpers",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
|
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.2.20"
|
"@wrnexus/core": "^0.2.21"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/i18n",
|
"name": "@wrnexus/i18n",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/i18n — part of the WrNexus framework.",
|
"description": "@wrnexus/i18n — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.2.20"
|
"@wrnexus/core": "^0.2.21"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/jwt",
|
"name": "@wrnexus/jwt",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/jwt — part of the WrNexus framework.",
|
"description": "@wrnexus/jwt — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/mobile",
|
"name": "@wrnexus/mobile",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/mobile — part of the WrNexus framework.",
|
"description": "@wrnexus/mobile — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/native": "^0.2.20"
|
"@wrnexus/native": "^0.2.21"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/native",
|
"name": "@wrnexus/native",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/native — part of the WrNexus framework.",
|
"description": "@wrnexus/native — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/oauth",
|
"name": "@wrnexus/oauth",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/oauth — part of the WrNexus framework.",
|
"description": "@wrnexus/oauth — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/pubsub",
|
"name": "@wrnexus/pubsub",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/pubsub — part of the WrNexus framework.",
|
"description": "@wrnexus/pubsub — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/queue",
|
"name": "@wrnexus/queue",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/queue — part of the WrNexus framework.",
|
"description": "@wrnexus/queue — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/reactive",
|
"name": "@wrnexus/reactive",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/reactive — part of the WrNexus framework.",
|
"description": "@wrnexus/reactive — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/router",
|
"name": "@wrnexus/router",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/router — part of the WrNexus framework.",
|
"description": "@wrnexus/router — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21,8 +21,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/compiler": "^0.2.20",
|
"@wrnexus/compiler": "^0.2.21",
|
||||||
"@wrnexus/core": "^0.2.20"
|
"@wrnexus/core": "^0.2.21"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ssr",
|
"name": "@wrnexus/ssr",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/ssr — part of the WrNexus framework.",
|
"description": "@wrnexus/ssr — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.2.20"
|
"@wrnexus/core": "^0.2.21"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/styles",
|
"name": "@wrnexus/styles",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/styles — part of the WrNexus framework.",
|
"description": "@wrnexus/styles — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/uploader": "^0.2.20"
|
"@wrnexus/uploader": "^0.2.21"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/test",
|
"name": "@wrnexus/test",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/test — part of the WrNexus framework.",
|
"description": "@wrnexus/test — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/tracking",
|
"name": "@wrnexus/tracking",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/tracking — part of the WrNexus framework.",
|
"description": "@wrnexus/tracking — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ui",
|
"name": "@wrnexus/ui",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/ui — part of the WrNexus framework.",
|
"description": "@wrnexus/ui — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"./ui.css": "./ui.css"
|
"./ui.css": "./ui.css"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.2.20"
|
"@wrnexus/core": "^0.2.21"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/uploader",
|
"name": "@wrnexus/uploader",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/uploader — part of the WrNexus framework.",
|
"description": "@wrnexus/uploader — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "^0.2.20"
|
"@wrnexus/core": "^0.2.21"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/validation",
|
"name": "@wrnexus/validation",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/validation — part of the WrNexus framework.",
|
"description": "@wrnexus/validation — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ai",
|
"name": "@wrnexus/ai",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
|
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/authz",
|
"name": "@wrnexus/authz",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/cli",
|
"name": "@wrnexus/cli",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -264,6 +264,14 @@ const MIGRATIONS: Migration[] = [
|
|||||||
if (!ctx.dryRun) writeFileSync(file, VSCODE_EXTENSIONS, "utf8");
|
if (!ctx.dryRun) writeFileSync(file, VSCODE_EXTENSIONS, "utf8");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
version: "0.2.21",
|
||||||
|
id: "in-process-hmr-runtime",
|
||||||
|
description: "No project-file changes; the in-process HMR behavior is runtime-only",
|
||||||
|
apply() {
|
||||||
|
// Explicit no-op: updating @wrnexus/dev-server is sufficient.
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Release tooling uses this to require an explicit migration entry per version. */
|
/** Release tooling uses this to require an explicit migration entry per version. */
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/compiler",
|
"name": "@wrnexus/compiler",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/core",
|
"name": "@wrnexus/core",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/csr",
|
"name": "@wrnexus/csr",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/db",
|
"name": "@wrnexus/db",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ interface RunningServer {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
In development, `startServer` also connects `app/db/migrations` (and `app/db/<name>/migrations`) and auto-applies migrations, then starts an in-process file watcher. CSS edits hot-swap live; any other server change triggers `process.exit(RESTART_EXIT_CODE)` so the dev supervisor (`@wrnexus/cli`) respawns the process with fresh modules.
|
In development, `startServer` also connects `app/db/migrations` (and `app/db/<name>/migrations`) and auto-applies migrations, then starts an in-process file watcher. CSS edits hot-swap live. Page, component, layout, API, middleware, realtime, schema, locale, and public-asset edits invalidate only their cached modules, rescan routes where necessary, and morph fresh HTML through the existing HMR WebSocket. The server process and active gateway stay running.
|
||||||
|
|
||||||
### `createHandlers(deps)`
|
### `createHandlers(deps)`
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ interface ProdOptions {
|
|||||||
|
|
||||||
### `startGateway(opts)` — multi-app gateway
|
### `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. 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).
|
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, normal application edits are applied inside the existing child and sent through its existing HMR connection. The child supervisor remains as crash recovery rather than the normal update path. Apps communicate at runtime via `@wrnexus/pubsub` (use the Redis driver so messages cross processes).
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
interface GatewayOptions {
|
interface GatewayOptions {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/dev-server",
|
"name": "@wrnexus/dev-server",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export interface DevStyles {
|
|||||||
/** A dev asset server also supports invalidating its caches in-process. */
|
/** A dev asset server also supports invalidating its caches in-process. */
|
||||||
export interface DevAssetServer extends AssetServer {
|
export interface DevAssetServer extends AssetServer {
|
||||||
invalidateCss(): void;
|
invalidateCss(): void;
|
||||||
|
updateSchemas(code: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function jsResponse(code: string): Response {
|
function jsResponse(code: string): Response {
|
||||||
@@ -65,12 +66,17 @@ export function createDevAssetServer(
|
|||||||
schemasJs?: string,
|
schemasJs?: string,
|
||||||
): DevAssetServer {
|
): DevAssetServer {
|
||||||
let cssCache: string | null = null;
|
let cssCache: string | null = null;
|
||||||
|
let schemasCode = schemasJs ?? "window.__wireSchemas={};";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
invalidateCss() {
|
invalidateCss() {
|
||||||
cssCache = null;
|
cssCache = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateSchemas(code) {
|
||||||
|
schemasCode = code;
|
||||||
|
},
|
||||||
|
|
||||||
async serve(pathname: string): Promise<Response | null> {
|
async serve(pathname: string): Promise<Response | null> {
|
||||||
if (pathname === "/__wrnexus/reactive.js") return jsResponse(getReactiveRuntime());
|
if (pathname === "/__wrnexus/reactive.js") return jsResponse(getReactiveRuntime());
|
||||||
if (pathname === "/__wrnexus/nav.js") return jsResponse(getNavRuntime());
|
if (pathname === "/__wrnexus/nav.js") return jsResponse(getNavRuntime());
|
||||||
@@ -83,8 +89,7 @@ export function createDevAssetServer(
|
|||||||
if (pathname.startsWith(UPLOADS_PREFIX)) {
|
if (pathname.startsWith(UPLOADS_PREFIX)) {
|
||||||
return (await serveStoredFile(pathname)) ?? new Response("Not Found", { status: 404 });
|
return (await serveStoredFile(pathname)) ?? new Response("Not Found", { status: 404 });
|
||||||
}
|
}
|
||||||
if (pathname === "/__wrnexus/schemas.js")
|
if (pathname === "/__wrnexus/schemas.js") return jsResponse(schemasCode);
|
||||||
return jsResponse(schemasJs ?? "window.__wireSchemas={};");
|
|
||||||
|
|
||||||
if (pathname === "/__wrnexus/ui.css") {
|
if (pathname === "/__wrnexus/ui.css") {
|
||||||
return uiCss ? cssResponse(uiCss) : new Response("Not Found", { status: 404 });
|
return uiCss ? cssResponse(uiCss) : new Response("Not Found", { status: 404 });
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
* { type: "css" } -> the browser hot-swaps the stylesheet (no reload)
|
* { type: "css" } -> the browser hot-swaps the stylesheet (no reload)
|
||||||
* { type: "reload" } -> the browser asks for fresh HTML over the HMR socket
|
* { type: "reload" } -> the browser asks for fresh HTML over the HMR socket
|
||||||
*
|
*
|
||||||
* Server-logic changes (pages/api/middleware/realtime) are NOT broadcast here:
|
* Page/component/API/middleware/realtime changes invalidate their modules and
|
||||||
* they require a fresh process, so the child exits and the supervisor respawns
|
* broadcast `reload` without closing the server or WebSocket. The browser asks
|
||||||
* it. The browser then reconnects and performs a soft DOM morph automatically.
|
* the same process for fresh HTML and performs a soft DOM morph.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type HmrMessage = { type: "css"; version: number } | { type: "reload"; version: number };
|
export type HmrMessage = { type: "css"; version: number } | { type: "reload"; version: number };
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
* @wrnexus/dev-server — the development HTTP + WebSocket server.
|
* @wrnexus/dev-server — the development HTTP + WebSocket server.
|
||||||
*
|
*
|
||||||
* Thin Bun.serve wrapper around the shared runtime (runtime.ts). Dynamic module
|
* Thin Bun.serve wrapper around the shared runtime (runtime.ts). Dynamic module
|
||||||
* loading makes it fast to iterate; the dev supervisor (see @wrnexus/cli)
|
* loading and targeted cache invalidation keep page/component/API edits inside
|
||||||
* restarts this process on file changes.
|
* the running process while the HMR socket morphs fresh HTML into the browser.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { resolve, dirname, join } from "node:path";
|
import { resolve, dirname, join } from "node:path";
|
||||||
@@ -23,13 +23,11 @@ import { migrate, setDb, registerDb } from "@wrnexus/db";
|
|||||||
import { connectFromConfig } from "@wrnexus/db/connect";
|
import { connectFromConfig } from "@wrnexus/db/connect";
|
||||||
import { configureStorage, type StorageConfig } from "@wrnexus/uploader";
|
import { configureStorage, type StorageConfig } from "@wrnexus/uploader";
|
||||||
import { realtimeBusFromConfig } from "./realtime-bus.ts";
|
import { realtimeBusFromConfig } from "./realtime-bus.ts";
|
||||||
import { loadModule, setCompileCacheDir } from "./pipeline.ts";
|
import { invalidateModule, loadModule, setCompileCacheDir } from "./pipeline.ts";
|
||||||
import { createHandlers, type WsData } from "./runtime.ts";
|
import { createHandlers, type WsData } from "./runtime.ts";
|
||||||
import { createDevAssetServer } from "./assets.ts";
|
import { createDevAssetServer } from "./assets.ts";
|
||||||
import { HmrHub } from "./hmr.ts";
|
import { HmrHub } from "./hmr.ts";
|
||||||
import { startWatcher } from "./watch.ts";
|
import { startWatcher } from "./watch.ts";
|
||||||
import { RESTART_EXIT_CODE } from "./restart.ts";
|
|
||||||
|
|
||||||
export { RESTART_EXIT_CODE } from "./restart.ts";
|
export { RESTART_EXIT_CODE } from "./restart.ts";
|
||||||
|
|
||||||
export interface ServeOptions {
|
export interface ServeOptions {
|
||||||
@@ -73,22 +71,46 @@ export interface RunningServer {
|
|||||||
stop(): void;
|
stop(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Build a cached middleware loader for a router. */
|
/** Build an invalidatable middleware loader for in-process hot updates. */
|
||||||
function middlewareLoader(router: Router): () => Promise<Middleware[]> {
|
function middlewareLoader(router: Router): {
|
||||||
|
load: () => Promise<Middleware[]>;
|
||||||
|
invalidate: () => void;
|
||||||
|
} {
|
||||||
let cache: Middleware[] | null = null;
|
let cache: Middleware[] | null = null;
|
||||||
return async () => {
|
return {
|
||||||
if (cache) return cache;
|
async load() {
|
||||||
const out: Middleware[] = [];
|
if (cache) return cache;
|
||||||
for (const file of router.middlewareFiles) {
|
const out: Middleware[] = [];
|
||||||
const mod = await loadModule(file);
|
for (const file of router.middlewareFiles) {
|
||||||
if (typeof mod.default === "function") out.push(mod.default as Middleware);
|
const mod = await loadModule(file);
|
||||||
else console.warn(`[wrnexus] middleware ${file} has no default export; skipped`);
|
if (typeof mod.default === "function") out.push(mod.default as Middleware);
|
||||||
}
|
else console.warn(`[wrnexus] middleware ${file} has no default export; skipped`);
|
||||||
cache = out;
|
}
|
||||||
return out;
|
cache = out;
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
invalidate() {
|
||||||
|
cache = null;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function schemaRuntime(router: Router): Promise<string> {
|
||||||
|
const descriptors: Record<string, SchemaDescriptor> = {};
|
||||||
|
for (const schemaRef of router.schemas) {
|
||||||
|
try {
|
||||||
|
const mod = await loadModule(schemaRef.file);
|
||||||
|
const schema = mod.default as ObjectSchema | undefined;
|
||||||
|
if (schema && typeof schema.describe === "function") {
|
||||||
|
descriptors[schemaRef.name] = schema.describe();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[wrnexus] schema '${schemaRef.name}' failed to load`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return renderSchemasScript(descriptors);
|
||||||
|
}
|
||||||
|
|
||||||
export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||||
const appDir = resolve(opts.appDir);
|
const appDir = resolve(opts.appDir);
|
||||||
const mode: Mode = opts.mode ?? "development";
|
const mode: Mode = opts.mode ?? "development";
|
||||||
@@ -106,19 +128,7 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
|||||||
const theme = resolveThemeConfig(opts.theme);
|
const theme = resolveThemeConfig(opts.theme);
|
||||||
const uiStyles = uiCss();
|
const uiStyles = uiCss();
|
||||||
|
|
||||||
// Load validation schemas once at startup and bake their descriptors into the
|
const schemasJs = await schemaRuntime(router);
|
||||||
// client script (schemas change → the dev supervisor restarts this process).
|
|
||||||
const descriptors: Record<string, SchemaDescriptor> = {};
|
|
||||||
for (const s of router.schemas) {
|
|
||||||
try {
|
|
||||||
const mod = await loadModule(s.file);
|
|
||||||
const schema = mod.default as ObjectSchema | undefined;
|
|
||||||
if (schema && typeof schema.describe === "function") descriptors[s.name] = schema.describe();
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(`[wrnexus] schema '${s.name}' failed to load`, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const schemasJs = renderSchemasScript(descriptors);
|
|
||||||
|
|
||||||
// i18n is opt-in by the presence of app/locales/*.json.
|
// i18n is opt-in by the presence of app/locales/*.json.
|
||||||
const localeMessages = loadLocales(join(appDir, "locales"));
|
const localeMessages = loadLocales(join(appDir, "locales"));
|
||||||
@@ -170,13 +180,14 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const hub = hmr ? new HmrHub() : undefined;
|
const hub = hmr ? new HmrHub() : undefined;
|
||||||
|
const middleware = middlewareLoader(router);
|
||||||
|
|
||||||
const handlers = createHandlers({
|
const runtimeDeps = {
|
||||||
mode,
|
mode,
|
||||||
hmr,
|
hmr,
|
||||||
router,
|
router,
|
||||||
loadModule,
|
loadModule,
|
||||||
getMiddleware: middlewareLoader(router),
|
getMiddleware: middleware.load,
|
||||||
assets,
|
assets,
|
||||||
hasStyles: !!styleEntry,
|
hasStyles: !!styleEntry,
|
||||||
hasUi: true,
|
hasUi: true,
|
||||||
@@ -189,7 +200,8 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
|||||||
security: opts.security,
|
security: opts.security,
|
||||||
hub,
|
hub,
|
||||||
realtimeBus: realtimeBusFromConfig(opts.realtime),
|
realtimeBus: realtimeBusFromConfig(opts.realtime),
|
||||||
});
|
};
|
||||||
|
const handlers = createHandlers(runtimeDeps);
|
||||||
|
|
||||||
const server = Bun.serve<WsData>({
|
const server = Bun.serve<WsData>({
|
||||||
port,
|
port,
|
||||||
@@ -200,34 +212,31 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
|||||||
websocket: handlers.websocket,
|
websocket: handlers.websocket,
|
||||||
});
|
});
|
||||||
|
|
||||||
// In-process HMR: CSS edits update live; server edits (pages/components/api)
|
let watcher: ReturnType<typeof startWatcher>;
|
||||||
// request a restart.
|
|
||||||
|
// In-process HMR: keep the server and socket alive, invalidate only changed
|
||||||
|
// modules, rescan file routes, and ask browsers to morph in fresh HTML.
|
||||||
if (hmr && hub) {
|
if (hmr && hub) {
|
||||||
let restarting = false;
|
const hotUpdate = async (files: string[]): Promise<void> => {
|
||||||
const requestRestart = (): void => {
|
for (const relative of files) invalidateModule(resolve(appDir, relative));
|
||||||
if (restarting) return;
|
|
||||||
restarting = true;
|
Object.assign(router, buildRouter(appDir, { componentDirs: [uiComponentsDir()] }));
|
||||||
console.log("[wrnexus] server change — restarting…");
|
middleware.invalidate();
|
||||||
// Close the watcher and stop the server FIRST. On Windows a live recursive
|
|
||||||
// fs.watch handle can hang `process.exit`, and stopping the server frees the
|
if (files.some((file) => file === "schemas" || file.startsWith("schemas/"))) {
|
||||||
// port so the freshly-spawned child can rebind immediately (no EADDRINUSE).
|
assets.updateSchemas(await schemaRuntime(router));
|
||||||
// Without this the child would print "restarting…" but never actually exit.
|
|
||||||
try {
|
|
||||||
watcher?.close();
|
|
||||||
} catch {
|
|
||||||
/* already closed */
|
|
||||||
}
|
}
|
||||||
try {
|
if (files.some((file) => file === "locales" || file.startsWith("locales/"))) {
|
||||||
server.stop(true); // true = close active connections now, release the socket
|
const messages = loadLocales(join(appDir, "locales"));
|
||||||
} catch {
|
runtimeDeps.i18n = Object.keys(messages).length
|
||||||
/* already stopping */
|
? resolveI18n(messages, opts.i18n)
|
||||||
|
: undefined;
|
||||||
}
|
}
|
||||||
// Let close callbacks and stdio flush, then force the exit if any handle
|
|
||||||
// remains alive. This is especially important on Windows file watching.
|
console.log(`[wrnexus] hot update — ${files.join(", ")}`);
|
||||||
process.exitCode = RESTART_EXIT_CODE;
|
hub.reload();
|
||||||
setTimeout(() => process.exit(RESTART_EXIT_CODE), 250).unref();
|
|
||||||
};
|
};
|
||||||
const watcher = startWatcher({ appDir, hub, assets, onServerChange: requestRestart });
|
watcher = startWatcher({ appDir, hub, assets, onHotChange: hotUpdate });
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundPort = server.port ?? port;
|
const boundPort = server.port ?? port;
|
||||||
@@ -236,7 +245,10 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
|||||||
hostname,
|
hostname,
|
||||||
url: `http://${displayHost}:${boundPort}`,
|
url: `http://${displayHost}:${boundPort}`,
|
||||||
router,
|
router,
|
||||||
stop: () => server.stop(),
|
stop: () => {
|
||||||
|
watcher?.close();
|
||||||
|
server.stop();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { pathToFileURL } from "node:url";
|
import { pathToFileURL } from "node:url";
|
||||||
import { readFileSync, writeFileSync, mkdirSync, statSync } from "node:fs";
|
import {
|
||||||
import { dirname, join, basename } from "node:path";
|
copyFileSync,
|
||||||
|
readFileSync,
|
||||||
|
writeFileSync,
|
||||||
|
mkdirSync,
|
||||||
|
statSync,
|
||||||
|
unlinkSync,
|
||||||
|
} from "node:fs";
|
||||||
|
import { dirname, join, basename, extname } from "node:path";
|
||||||
import { compileWireFile } from "@wrnexus/compiler";
|
import { compileWireFile } from "@wrnexus/compiler";
|
||||||
import type { Context, Middleware } from "@wrnexus/core";
|
import type { Context, Middleware } from "@wrnexus/core";
|
||||||
|
|
||||||
@@ -40,14 +47,36 @@ export function runMiddleware(
|
|||||||
* paths discovered during the startup scan — never from request input.
|
* paths discovered during the startup scan — never from request input.
|
||||||
*/
|
*/
|
||||||
const moduleCache = new Map<string, Promise<Record<string, unknown>>>();
|
const moduleCache = new Map<string, Promise<Record<string, unknown>>>();
|
||||||
|
const moduleVersions = new Map<string, number>();
|
||||||
|
|
||||||
export function loadModule(file: string): Promise<Record<string, unknown>> {
|
export function loadModule(file: string): Promise<Record<string, unknown>> {
|
||||||
let mod = moduleCache.get(file);
|
let mod = moduleCache.get(file);
|
||||||
if (!mod) {
|
if (!mod) {
|
||||||
|
const version = moduleVersions.get(file) ?? 0;
|
||||||
// `.wrn` files are compiled to TypeScript first, then imported.
|
// `.wrn` files are compiled to TypeScript first, then imported.
|
||||||
const target = file.endsWith(".wrn") ? compileWireToTs(file) : file;
|
let target = file.endsWith(".wrn") ? compileWireToTs(file, version) : file;
|
||||||
|
let temporary = false;
|
||||||
|
// Bun intentionally caches local TS/JS modules by filesystem path and ignores
|
||||||
|
// URL query strings. A short-lived versioned sibling keeps relative imports
|
||||||
|
// correct while giving the changed module a genuinely new import identity.
|
||||||
|
if (version && !file.endsWith(".wrn")) {
|
||||||
|
const extension = extname(file);
|
||||||
|
const stem = basename(file, extension);
|
||||||
|
target = join(dirname(file), `${stem}.wrnexus-hmr-${version}${extension}`);
|
||||||
|
copyFileSync(file, target);
|
||||||
|
temporary = true;
|
||||||
|
}
|
||||||
// pathToFileURL handles Windows drive letters and spaces correctly.
|
// pathToFileURL handles Windows drive letters and spaces correctly.
|
||||||
mod = import(pathToFileURL(target).href) as Promise<Record<string, unknown>>;
|
mod = import(pathToFileURL(target).href) as Promise<Record<string, unknown>>;
|
||||||
|
if (temporary) {
|
||||||
|
mod = mod.finally(() => {
|
||||||
|
try {
|
||||||
|
unlinkSync(target);
|
||||||
|
} catch {
|
||||||
|
/* best-effort cleanup after Bun has loaded the module */
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
moduleCache.set(file, mod);
|
moduleCache.set(file, mod);
|
||||||
}
|
}
|
||||||
return mod;
|
return mod;
|
||||||
@@ -86,10 +115,11 @@ function hashPath(s: string): string {
|
|||||||
* components) share one cache dir without colliding. Generated modules are
|
* components) share one cache dir without colliding. Generated modules are
|
||||||
* self-contained (no relative imports), so the cache location doesn't affect them.
|
* self-contained (no relative imports), so the cache location doesn't affect them.
|
||||||
*/
|
*/
|
||||||
function compileWireToTs(file: string): string {
|
function compileWireToTs(file: string, version = 0): string {
|
||||||
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 out = join(cacheDir, `${name}-${hashPath(file)}.wrn.ts`);
|
const suffix = version ? `-hmr-${version}` : "";
|
||||||
|
const out = join(cacheDir, `${name}-${hashPath(file)}${suffix}.wrn.ts`);
|
||||||
|
|
||||||
// Skip recompiling when the on-disk cache is already newer than the source
|
// Skip recompiling when the on-disk cache is already newer than the source
|
||||||
// (e.g. reused across dev restarts) — avoids a read + compile + write.
|
// (e.g. reused across dev restarts) — avoids a read + compile + write.
|
||||||
@@ -105,7 +135,16 @@ function compileWireToTs(file: string): string {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Forget one module and force its next dynamic import to bypass Bun's import cache. */
|
||||||
|
export function invalidateModule(file: string): void {
|
||||||
|
moduleCache.delete(file);
|
||||||
|
moduleVersions.set(file, (moduleVersions.get(file) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
/** Forget cached modules (used by build/dev tooling if needed). */
|
/** Forget cached modules (used by build/dev tooling if needed). */
|
||||||
export function clearModuleCache(): void {
|
export function clearModuleCache(): void {
|
||||||
|
for (const file of moduleCache.keys()) {
|
||||||
|
moduleVersions.set(file, (moduleVersions.get(file) ?? 0) + 1);
|
||||||
|
}
|
||||||
moduleCache.clear();
|
moduleCache.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -410,8 +410,8 @@ export const STYLES_HREF = "/__wrnexus/styles.css";
|
|||||||
/**
|
/**
|
||||||
* Inline HMR client (WebSocket). Goals:
|
* Inline HMR client (WebSocket). Goals:
|
||||||
* - CSS change -> hot-swap the stylesheet, zero reload, zero flash.
|
* - CSS change -> hot-swap the stylesheet, zero reload, zero flash.
|
||||||
* - markup/page -> after the server restarts, ask over the HMR WebSocket for
|
* - markup/page -> ask over the existing HMR WebSocket for fresh HTML and
|
||||||
* fresh HTML and MORPH the live DOM in place.
|
* MORPH the live DOM in place, without restarting the server.
|
||||||
* - island code -> same WebSocket sync path; no location.reload().
|
* - island code -> same WebSocket sync path; no location.reload().
|
||||||
*/
|
*/
|
||||||
export const HMR_CLIENT_JS = `
|
export const HMR_CLIENT_JS = `
|
||||||
|
|||||||
@@ -3,10 +3,9 @@
|
|||||||
* cheapest update that still shows the latest page:
|
* cheapest update that still shows the latest page:
|
||||||
*
|
*
|
||||||
* *.css / styles/ -> invalidate CSS cache, push { type: "css" } (instant swap)
|
* *.css / styles/ -> invalidate CSS cache, push { type: "css" } (instant swap)
|
||||||
* anything else -> a server module changed (pages, components, api, …):
|
* anything else -> invalidate changed modules, rescan routes, and ask
|
||||||
* it can't be re-imported in process, so request a
|
* the connected browser to sync fresh HTML. The server
|
||||||
* restart (the supervisor respawns us; the browser then
|
* process and HMR socket stay alive.
|
||||||
* morphs in the new HTML).
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { watch, type FSWatcher } from "node:fs";
|
import { watch, type FSWatcher } from "node:fs";
|
||||||
@@ -17,14 +16,15 @@ export interface WatchOptions {
|
|||||||
appDir: string;
|
appDir: string;
|
||||||
hub: HmrHub;
|
hub: HmrHub;
|
||||||
assets: DevAssetServer;
|
assets: DevAssetServer;
|
||||||
/** Called when a change requires a fresh process. */
|
/** Called with changed app-relative files that need an in-process hot update. */
|
||||||
onServerChange: () => void;
|
onHotChange: (files: string[]) => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isIgnored(rel: string): boolean {
|
function isIgnored(rel: string): boolean {
|
||||||
return (
|
return (
|
||||||
rel.includes("node_modules/") ||
|
rel.includes("node_modules/") ||
|
||||||
rel.includes(".wrnexus/") ||
|
rel.includes(".wrnexus/") ||
|
||||||
|
rel.includes(".wrnexus-hmr-") ||
|
||||||
rel.startsWith("dist/") ||
|
rel.startsWith("dist/") ||
|
||||||
rel.includes("/dist/")
|
rel.includes("/dist/")
|
||||||
);
|
);
|
||||||
@@ -37,19 +37,22 @@ function classify(rel: string): Kind {
|
|||||||
return "server";
|
return "server";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the watcher so the caller can close it before a restart (important on
|
/** Returns the watcher so the running server can close it during shutdown. */
|
||||||
* Windows, where a live recursive fs.watch handle can block `process.exit`). */
|
|
||||||
export function startWatcher(opts: WatchOptions): FSWatcher | undefined {
|
export function startWatcher(opts: WatchOptions): FSWatcher | undefined {
|
||||||
const { appDir, hub, assets, onServerChange } = opts;
|
const { appDir, hub, assets } = opts;
|
||||||
const pending = new Set<Kind>();
|
const pending = new Set<Kind>();
|
||||||
|
const pendingFiles = new Set<string>();
|
||||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
const flush = (): void => {
|
const flush = (): void => {
|
||||||
timer = null;
|
timer = null;
|
||||||
// A server change always wins (needs a restart).
|
|
||||||
if (pending.has("server")) {
|
if (pending.has("server")) {
|
||||||
pending.clear();
|
pending.clear();
|
||||||
onServerChange();
|
const files = [...pendingFiles];
|
||||||
|
pendingFiles.clear();
|
||||||
|
void Promise.resolve(opts.onHotChange(files)).catch((error) => {
|
||||||
|
console.error("[wrnexus] hot update failed", error);
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (pending.has("css")) {
|
if (pending.has("css")) {
|
||||||
@@ -57,6 +60,7 @@ export function startWatcher(opts: WatchOptions): FSWatcher | undefined {
|
|||||||
hub.css();
|
hub.css();
|
||||||
}
|
}
|
||||||
pending.clear();
|
pending.clear();
|
||||||
|
pendingFiles.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -64,6 +68,7 @@ export function startWatcher(opts: WatchOptions): FSWatcher | undefined {
|
|||||||
if (!filename) return;
|
if (!filename) return;
|
||||||
const rel = filename.toString().replace(/\\/g, "/");
|
const rel = filename.toString().replace(/\\/g, "/");
|
||||||
if (isIgnored(rel)) return;
|
if (isIgnored(rel)) return;
|
||||||
|
pendingFiles.add(rel);
|
||||||
pending.add(classify(rel));
|
pending.add(classify(rel));
|
||||||
if (timer) clearTimeout(timer);
|
if (timer) clearTimeout(timer);
|
||||||
timer = setTimeout(flush, 60); // debounce editor write bursts
|
timer = setTimeout(flush, 60); // debounce editor write bursts
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { invalidateModule, loadModule, setCompileCacheDir } from "../src/pipeline.ts";
|
||||||
|
|
||||||
|
test("invalidateModule loads changed server modules without restarting the process", async () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "wrnexus-hmr-"));
|
||||||
|
const file = join(root, "page.ts");
|
||||||
|
setCompileCacheDir(join(root, ".wrnexus"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
writeFileSync(file, "export const value = 1;\n");
|
||||||
|
expect((await loadModule(file)).value).toBe(1);
|
||||||
|
|
||||||
|
writeFileSync(file, "export const value = 2;\n");
|
||||||
|
expect((await loadModule(file)).value).toBe(1);
|
||||||
|
|
||||||
|
invalidateModule(file);
|
||||||
|
expect((await loadModule(file)).value).toBe(2);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("invalidateModule recompiles changed WRN files in-process", async () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "wrnexus-hmr-"));
|
||||||
|
const file = join(root, "page.wrn");
|
||||||
|
setCompileCacheDir(join(root, ".wrnexus"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
writeFileSync(file, "page Home { view { <h1>First</h1> } }\n");
|
||||||
|
const first = (await loadModule(file)).default as (ctx: unknown) => unknown;
|
||||||
|
expect(String(first({}))).toContain("First");
|
||||||
|
|
||||||
|
writeFileSync(file, "page Home { view { <h1>Second</h1> } }\n");
|
||||||
|
invalidateModule(file);
|
||||||
|
const second = (await loadModule(file)).default as (ctx: unknown) => unknown;
|
||||||
|
expect(String(second({}))).toContain("Second");
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/encryption",
|
"name": "@wrnexus/encryption",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/helpers",
|
"name": "@wrnexus/helpers",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
|
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/i18n",
|
"name": "@wrnexus/i18n",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/jwt",
|
"name": "@wrnexus/jwt",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/mobile",
|
"name": "@wrnexus/mobile",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/native",
|
"name": "@wrnexus/native",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/oauth",
|
"name": "@wrnexus/oauth",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/pubsub",
|
"name": "@wrnexus/pubsub",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/queue",
|
"name": "@wrnexus/queue",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/reactive",
|
"name": "@wrnexus/reactive",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/router",
|
"name": "@wrnexus/router",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ssr",
|
"name": "@wrnexus/ssr",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/styles",
|
"name": "@wrnexus/styles",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/test",
|
"name": "@wrnexus/test",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/tracking",
|
"name": "@wrnexus/tracking",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ui",
|
"name": "@wrnexus/ui",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/uploader",
|
"name": "@wrnexus/uploader",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/validation",
|
"name": "@wrnexus/validation",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user