feat: add safe project upgrades and production performance fixes
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ai",
|
"name": "@wrnexus/ai",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/authz — part of the WrNexus framework.",
|
"description": "@wrnexus/authz — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -76,6 +76,12 @@ bun dist/server.js # PORT env var optional
|
|||||||
|
|
||||||
Scaffolds a new app from an inline (dependency-free) template — `package.json`, `.gitignore`, config, and starter `app/` files. Use `npm run dev` during development, `npm run build && npm start` for production, or `npm run production` to build and start in one command. The generated production server currently requires Bun even when npm is used to manage packages and scripts.
|
Scaffolds a new app from an inline (dependency-free) template — `package.json`, `.gitignore`, config, and starter `app/` files. Use `npm run dev` during development, `npm run build && npm start` for production, or `npm run production` to build and start in one command. The generated production server currently requires Bun even when npm is used to manage packages and scripts.
|
||||||
|
|
||||||
|
### `wrnexus update`
|
||||||
|
|
||||||
|
`wrnexus update --latest` performs a complete project upgrade. It hands control to the exact target CLI, backs up important project files under `.wrnexus/update-backups/`, updates every `@wrnexus/*` dependency, refreshes framework-owned references, and applies every versioned syntax/config/file migration between the project version and target version. After installation it runs the project's `check` and `build` scripts; the new version is recorded only after verification succeeds.
|
||||||
|
|
||||||
|
Use `--dry-run` to preview an upgrade or `--no-verify` when verification is intentionally handled elsewhere. Migrations never overwrite user-owned configuration wholesale: each release must provide a focused, idempotent transformation for any changed syntax or config contract.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wrnexus create my-app
|
wrnexus create my-app
|
||||||
```
|
```
|
||||||
|
|||||||
+11
-11
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/cli",
|
"name": "@wrnexus/cli",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"@wrnexus/core": "^0.2.14",
|
||||||
"@wrnexus/router": "^0.2.13",
|
"@wrnexus/router": "^0.2.14",
|
||||||
"@wrnexus/csr": "^0.2.13",
|
"@wrnexus/csr": "^0.2.14",
|
||||||
"@wrnexus/compiler": "^0.2.13",
|
"@wrnexus/compiler": "^0.2.14",
|
||||||
"@wrnexus/styles": "^0.2.13",
|
"@wrnexus/styles": "^0.2.14",
|
||||||
"@wrnexus/dev-server": "^0.2.13",
|
"@wrnexus/dev-server": "^0.2.14",
|
||||||
"@wrnexus/ui": "^0.2.13",
|
"@wrnexus/ui": "^0.2.14",
|
||||||
"@wrnexus/validation": "^0.2.13",
|
"@wrnexus/validation": "^0.2.14",
|
||||||
"@wrnexus/i18n": "^0.2.13",
|
"@wrnexus/i18n": "^0.2.14",
|
||||||
"@wrnexus/db": "^0.2.13"
|
"@wrnexus/db": "^0.2.14"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/compiler",
|
"name": "@wrnexus/compiler",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"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.13"
|
"@wrnexus/core": "^0.2.14"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/db",
|
"name": "@wrnexus/db",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "@wrnexus/db — part of the WrNexus framework.",
|
"description": "@wrnexus/db — part of the WrNexus framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/dev-server",
|
"name": "@wrnexus/dev-server",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"@wrnexus/core": "^0.2.14",
|
||||||
"@wrnexus/router": "^0.2.13",
|
"@wrnexus/router": "^0.2.14",
|
||||||
"@wrnexus/ssr": "^0.2.13",
|
"@wrnexus/ssr": "^0.2.14",
|
||||||
"@wrnexus/csr": "^0.2.13",
|
"@wrnexus/csr": "^0.2.14",
|
||||||
"@wrnexus/compiler": "^0.2.13",
|
"@wrnexus/compiler": "^0.2.14",
|
||||||
"@wrnexus/styles": "^0.2.13",
|
"@wrnexus/styles": "^0.2.14",
|
||||||
"@wrnexus/ui": "^0.2.13",
|
"@wrnexus/ui": "^0.2.14",
|
||||||
"@wrnexus/validation": "^0.2.13",
|
"@wrnexus/validation": "^0.2.14",
|
||||||
"@wrnexus/i18n": "^0.2.13",
|
"@wrnexus/i18n": "^0.2.14",
|
||||||
"@wrnexus/db": "^0.2.13",
|
"@wrnexus/db": "^0.2.14",
|
||||||
"@wrnexus/pubsub": "^0.2.13",
|
"@wrnexus/pubsub": "^0.2.14",
|
||||||
"@wrnexus/uploader": "^0.2.13"
|
"@wrnexus/uploader": "^0.2.14"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/encryption",
|
"name": "@wrnexus/encryption",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"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/i18n",
|
"name": "@wrnexus/i18n",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"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.13"
|
"@wrnexus/core": "^0.2.14"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/jwt",
|
"name": "@wrnexus/jwt",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"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.13"
|
"@wrnexus/native": "^0.2.14"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/native",
|
"name": "@wrnexus/native",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"@wrnexus/compiler": "^0.2.14",
|
||||||
"@wrnexus/core": "^0.2.13"
|
"@wrnexus/core": "^0.2.14"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ssr",
|
"name": "@wrnexus/ssr",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"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.13"
|
"@wrnexus/core": "^0.2.14"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/styles",
|
"name": "@wrnexus/styles",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"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.13"
|
"@wrnexus/uploader": "^0.2.14"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/test",
|
"name": "@wrnexus/test",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"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.13"
|
"@wrnexus/core": "^0.2.14"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/uploader",
|
"name": "@wrnexus/uploader",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"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.13"
|
"@wrnexus/core": "^0.2.14"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/validation",
|
"name": "@wrnexus/validation",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -76,6 +76,12 @@ bun dist/server.js # PORT env var optional
|
|||||||
|
|
||||||
Scaffolds a new app from an inline (dependency-free) template — `package.json`, `.gitignore`, config, and starter `app/` files. Use `npm run dev` during development, `npm run build && npm start` for production, or `npm run production` to build and start in one command. The generated production server currently requires Bun even when npm is used to manage packages and scripts.
|
Scaffolds a new app from an inline (dependency-free) template — `package.json`, `.gitignore`, config, and starter `app/` files. Use `npm run dev` during development, `npm run build && npm start` for production, or `npm run production` to build and start in one command. The generated production server currently requires Bun even when npm is used to manage packages and scripts.
|
||||||
|
|
||||||
|
### `wrnexus update`
|
||||||
|
|
||||||
|
`wrnexus update --latest` performs a complete project upgrade. It hands control to the exact target CLI, backs up important project files under `.wrnexus/update-backups/`, updates every `@wrnexus/*` dependency, refreshes framework-owned references, and applies every versioned syntax/config/file migration between the project version and target version. After installation it runs the project's `check` and `build` scripts; the new version is recorded only after verification succeeds.
|
||||||
|
|
||||||
|
Use `--dry-run` to preview an upgrade or `--no-verify` when verification is intentionally handled elsewhere. Migrations never overwrite user-owned configuration wholesale: each release must provide a focused, idempotent transformation for any changed syntax or config contract.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wrnexus create my-app
|
wrnexus create my-app
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/cli",
|
"name": "@wrnexus/cli",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ wrnexus dev . # dev server + HMR
|
|||||||
wrnexus build . # production build → dist/server.js
|
wrnexus build . # production build → dist/server.js
|
||||||
bun dist/server.js # run the production server (or npm start)
|
bun dist/server.js # run the production server (or npm start)
|
||||||
wrnexus create <name> # scaffold a new app
|
wrnexus create <name> # scaffold a new app
|
||||||
|
wrnexus update --latest # deps + syntax/config migrations + verification
|
||||||
wrnexus generate page <Name> # scaffold a page (aliases: g p)
|
wrnexus generate page <Name> # scaffold a page (aliases: g p)
|
||||||
wrnexus generate component <name> | api <path> | schema <name>
|
wrnexus generate component <name> | api <path> | schema <name>
|
||||||
wrnexus db migrate | rollback | status | new [--from-models] | generate | seed
|
wrnexus db migrate | rollback | status | new [--from-models] | generate | seed
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { compileWireFile } from "@wrnexus/compiler";
|
|||||||
import {
|
import {
|
||||||
loadAppConfig,
|
loadAppConfig,
|
||||||
headToString,
|
headToString,
|
||||||
renderFontHead,
|
renderProductionFontHead,
|
||||||
findStyleEntry,
|
findStyleEntry,
|
||||||
renderStyles,
|
renderStyles,
|
||||||
resolveThemeConfig,
|
resolveThemeConfig,
|
||||||
@@ -124,8 +124,10 @@ export async function runBuild(appRoot: string): Promise<void> {
|
|||||||
// 1a2) Wire UI stylesheet (all component classes, themed via tokens).
|
// 1a2) Wire UI stylesheet (all component classes, themed via tokens).
|
||||||
const uiStyles = uiCss();
|
const uiStyles = uiCss();
|
||||||
writeFileSync(join(distDir, "ui.css"), uiStyles, "utf8");
|
writeFileSync(join(distDir, "ui.css"), uiStyles, "utf8");
|
||||||
|
const frameworkStyles = `${themeCss}\n${uiStyles}`;
|
||||||
|
writeFileSync(join(distDir, "framework.css"), frameworkStyles, "utf8");
|
||||||
assetHash.update(uiStyles);
|
assetHash.update(uiStyles);
|
||||||
console.log(`✓ UI: dist/ui.css`);
|
console.log(`✓ UI: dist/ui.css + framework.css`);
|
||||||
|
|
||||||
// 1a3) Validation: bake schema descriptors into the client script.
|
// 1a3) Validation: bake schema descriptors into the client script.
|
||||||
const descriptors: Record<string, SchemaDescriptor> = {};
|
const descriptors: Record<string, SchemaDescriptor> = {};
|
||||||
@@ -164,7 +166,7 @@ export async function runBuild(appRoot: string): Promise<void> {
|
|||||||
console.log(`✓ Styles: ${join(distDir, "styles.css")}`);
|
console.log(`✓ Styles: ${join(distDir, "styles.css")}`);
|
||||||
}
|
}
|
||||||
const assetVersion = assetHash.digest("hex").slice(0, 12);
|
const assetVersion = assetHash.digest("hex").slice(0, 12);
|
||||||
const headStr = [renderFontHead(config.fonts), headToString(config.head)]
|
const headStr = [await renderProductionFontHead(config.fonts), headToString(config.head)]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n ");
|
.join("\n ");
|
||||||
|
|
||||||
@@ -232,6 +234,7 @@ await createProductionServer(
|
|||||||
themeJsPath: join(import.meta.dir, "theme.js"),
|
themeJsPath: join(import.meta.dir, "theme.js"),
|
||||||
theme: ${JSON.stringify(theme)},
|
theme: ${JSON.stringify(theme)},
|
||||||
uiCssPath: join(import.meta.dir, "ui.css"),
|
uiCssPath: join(import.meta.dir, "ui.css"),
|
||||||
|
frameworkCssPath: join(import.meta.dir, "framework.css"),
|
||||||
schemasJs: ${JSON.stringify(schemasJs)},
|
schemasJs: ${JSON.stringify(schemasJs)},
|
||||||
i18n: ${i18n ? JSON.stringify(i18n) : "undefined"},
|
i18n: ${i18n ? JSON.stringify(i18n) : "undefined"},
|
||||||
db: ${config.db ? JSON.stringify(config.db) : "undefined"},
|
db: ${config.db ? JSON.stringify(config.db) : "undefined"},
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { join, resolve } from "node:path";
|
|||||||
import { resolveProfile, loadEnv } from "@wrnexus/styles";
|
import { resolveProfile, loadEnv } from "@wrnexus/styles";
|
||||||
import { runDev } from "./dev.ts";
|
import { runDev } from "./dev.ts";
|
||||||
import { createApp } from "./create.ts";
|
import { createApp } from "./create.ts";
|
||||||
|
import { currentCliVersion, notifyIfUpdateAvailable } from "./update-notifier.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the active profile from `--profile=<name>` (or WRNEXUS_PROFILE / mode),
|
* Resolve the active profile from `--profile=<name>` (or WRNEXUS_PROFILE / mode),
|
||||||
@@ -51,7 +52,7 @@ Usage:
|
|||||||
wrnexus native list List cross-platform native capabilities
|
wrnexus native list List cross-platform native capabilities
|
||||||
wrnexus native add <capability...> Install capability packages for the configured mobile mode
|
wrnexus native add <capability...> Install capability packages for the configured mobile mode
|
||||||
wrnexus eject <name...> Copy a Wire UI component into app/components
|
wrnexus eject <name...> Copy a Wire UI component into app/components
|
||||||
wrnexus update [dir] [--latest] Upgrade @wrnexus/* deps + apply config/file migrations
|
wrnexus update [dir] [--latest] Upgrade deps, migrate project files, and verify the app
|
||||||
wrnexus db <cmd> Migrations: migrate | rollback | status | seed | generate | new
|
wrnexus db <cmd> Migrations: migrate | rollback | status | seed | generate | new
|
||||||
wrnexus test [app-dir] [--watch] Run the app's tests (bun test, 'test' profile)
|
wrnexus test [app-dir] [--watch] Run the app's tests (bun test, 'test' profile)
|
||||||
wrnexus profiles [app-dir] List config profiles (dev/prod/uat/…) and their env files
|
wrnexus profiles [app-dir] List config profiles (dev/prod/uat/…) and their env files
|
||||||
@@ -59,12 +60,16 @@ Usage:
|
|||||||
|
|
||||||
Profiles: pass --profile=<name> to dev/build/db (or set WRNEXUS_PROFILE) to load
|
Profiles: pass --profile=<name> to dev/build/db (or set WRNEXUS_PROFILE) to load
|
||||||
that profile's .env cascade and config overrides. e.g. wrnexus dev --profile=uat
|
that profile's .env cascade and config overrides. e.g. wrnexus dev --profile=uat
|
||||||
|
|
||||||
|
Update options: --dry-run previews changes; --no-verify skips post-update check/build.
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
const [command, ...rest] = process.argv.slice(2);
|
const [command, ...rest] = process.argv.slice(2);
|
||||||
|
|
||||||
|
notifyIfUpdateAvailable(currentCliVersion(), command);
|
||||||
|
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case "dev": {
|
case "dev": {
|
||||||
const appRoot = rest.find((a) => !a.startsWith("--")) ?? ".";
|
const appRoot = rest.find((a) => !a.startsWith("--")) ?? ".";
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { homedir } from "node:os";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
|
||||||
|
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
interface UpdateCache {
|
||||||
|
checkedAt: number;
|
||||||
|
latest: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function currentCliVersion(): string {
|
||||||
|
try {
|
||||||
|
return JSON.parse(readFileSync(join(import.meta.dir, "..", "package.json"), "utf8")).version;
|
||||||
|
} catch {
|
||||||
|
return "0.0.0";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compareVersions(a: string, b: string): number {
|
||||||
|
const left = a.split("-")[0]!.split(".").map(Number);
|
||||||
|
const right = b.split("-")[0]!.split(".").map(Number);
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const difference = (left[i] || 0) - (right[i] || 0);
|
||||||
|
if (difference) return difference > 0 ? 1 : -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cachePath(): string {
|
||||||
|
return process.env.WRNEXUS_UPDATE_CACHE ?? join(homedir(), ".wrnexus", "update-check.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
function cachedLatest(now: number): string | null {
|
||||||
|
try {
|
||||||
|
const cached = JSON.parse(readFileSync(cachePath(), "utf8")) as UpdateCache;
|
||||||
|
return now - cached.checkedAt < CHECK_INTERVAL_MS ? cached.latest : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registryLatest(): string | null {
|
||||||
|
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||||
|
const result = spawnSync(npm, ["view", "@wrnexus/cli", "version"], {
|
||||||
|
encoding: "utf8",
|
||||||
|
timeout: 5000,
|
||||||
|
windowsHide: true,
|
||||||
|
});
|
||||||
|
const version = (result.stdout ?? "").trim();
|
||||||
|
return result.status === 0 && /^\d+\.\d+\.\d+/.test(version) ? version : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check at most once per day and print a non-blocking upgrade suggestion. */
|
||||||
|
export function notifyIfUpdateAvailable(current: string, command?: string): void {
|
||||||
|
if (
|
||||||
|
process.env.WRNEXUS_NO_UPDATE_CHECK === "1" ||
|
||||||
|
command === "update" ||
|
||||||
|
command === "upgrade"
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const now = Date.now();
|
||||||
|
let latest = cachedLatest(now);
|
||||||
|
if (!latest) {
|
||||||
|
latest = registryLatest();
|
||||||
|
if (!latest) return;
|
||||||
|
try {
|
||||||
|
const file = cachePath();
|
||||||
|
if (!existsSync(dirname(file))) mkdirSync(dirname(file), { recursive: true });
|
||||||
|
writeFileSync(file, JSON.stringify({ checkedAt: now, latest }) + "\n", "utf8");
|
||||||
|
} catch {
|
||||||
|
// A read-only home directory must never break the CLI.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (compareVersions(latest, current) > 0) {
|
||||||
|
console.log(
|
||||||
|
`\n Update available: WRNexusJS ${current} → ${latest}\n` +
|
||||||
|
` Run: wrnexus update --latest\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+169
-10
@@ -17,11 +17,12 @@
|
|||||||
* as the framework evolves — that is how "new things" reach existing apps.
|
* as the framework evolves — that is how "new things" reach existing apps.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
import { join, resolve } from "node:path";
|
import { join, resolve } from "node:path";
|
||||||
import { pathToFileURL } from "node:url";
|
import { pathToFileURL } from "node:url";
|
||||||
import { AI_GUIDE, CLAUDE_MD } from "./ai-guide.ts";
|
import { AI_GUIDE, CLAUDE_MD } from "./ai-guide.ts";
|
||||||
|
import { inspectProject } from "./doctor.ts";
|
||||||
|
|
||||||
/** The version of the CLI currently running (its own package.json). */
|
/** The version of the CLI currently running (its own package.json). */
|
||||||
function cliVersion(): string {
|
function cliVersion(): string {
|
||||||
@@ -35,7 +36,8 @@ function cliVersion(): string {
|
|||||||
/** Query the registry for the latest published `@wrnexus/cli` version. */
|
/** Query the registry for the latest published `@wrnexus/cli` version. */
|
||||||
function latestPublished(): string | null {
|
function latestPublished(): string | null {
|
||||||
try {
|
try {
|
||||||
const out = spawnSync("npm", ["view", "@wrnexus/cli", "version"], { encoding: "utf8" });
|
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||||
|
const out = spawnSync(npm, ["view", "@wrnexus/cli", "version"], { encoding: "utf8" });
|
||||||
const v = (out.stdout ?? "").trim();
|
const v = (out.stdout ?? "").trim();
|
||||||
return /^\d+\.\d+\.\d+/.test(v) ? v : null;
|
return /^\d+\.\d+\.\d+/.test(v) ? v : null;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -92,6 +94,68 @@ const MIGRATIONS: Migration[] = [
|
|||||||
writeFileSync(file, next.replace(/^\n+/, ""), "utf8");
|
writeFileSync(file, next.replace(/^\n+/, ""), "utf8");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
version: "0.2.14",
|
||||||
|
id: "complete-gitignore",
|
||||||
|
description: "Add the complete WRNexusJS generated-project ignore set",
|
||||||
|
apply(ctx) {
|
||||||
|
const file = join(ctx.appRoot, ".gitignore");
|
||||||
|
const want = [
|
||||||
|
"node_modules/",
|
||||||
|
"dist/",
|
||||||
|
".wrnexus/",
|
||||||
|
"**/.wrnexus/",
|
||||||
|
"coverage/",
|
||||||
|
".env",
|
||||||
|
".env.*",
|
||||||
|
"!.env.example",
|
||||||
|
"*.log",
|
||||||
|
"*.db",
|
||||||
|
"*.db-shm",
|
||||||
|
"*.db-wal",
|
||||||
|
"*.sqlite",
|
||||||
|
"*.sqlite3",
|
||||||
|
"mobile/android/",
|
||||||
|
"mobile/ios/",
|
||||||
|
"mobile/.expo/",
|
||||||
|
".idea/",
|
||||||
|
".vscode/",
|
||||||
|
".DS_Store",
|
||||||
|
"Thumbs.db",
|
||||||
|
"*.tsbuildinfo",
|
||||||
|
".eslintcache",
|
||||||
|
];
|
||||||
|
const current = existsSync(file) ? readFileSync(file, "utf8") : "";
|
||||||
|
const have = new Set(current.split(/\r?\n/).map((line) => line.trim()));
|
||||||
|
const missing = want.filter((entry) => !have.has(entry));
|
||||||
|
if (!missing.length) return;
|
||||||
|
ctx.log(`+ .gitignore: ${missing.join(", ")}`);
|
||||||
|
if (!ctx.dryRun) {
|
||||||
|
const next = `${current.replace(/\s*$/, "")}\n${missing.join("\n")}\n`.replace(/^\n+/, "");
|
||||||
|
writeFileSync(file, next, "utf8");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: "0.2.14",
|
||||||
|
id: "production-scripts",
|
||||||
|
description: "Add standard production build and start scripts",
|
||||||
|
apply(ctx) {
|
||||||
|
// Workspace roots are manifests, not runnable applications.
|
||||||
|
if (!existsSync(join(ctx.appRoot, "app", "pages"))) return;
|
||||||
|
const file = join(ctx.appRoot, "package.json");
|
||||||
|
const pkg = JSON.parse(readFileSync(file, "utf8")) as Record<string, any>;
|
||||||
|
const scripts = (pkg.scripts ??= {});
|
||||||
|
const additions: Record<string, string> = {};
|
||||||
|
if (!scripts.build) additions.build = "wrnexus build .";
|
||||||
|
if (!scripts.start) additions.start = "bun dist/server.js";
|
||||||
|
if (!scripts.production) additions.production = "bun run build && bun run start";
|
||||||
|
if (!Object.keys(additions).length) return;
|
||||||
|
Object.assign(scripts, additions);
|
||||||
|
ctx.log(`+ package scripts: ${Object.keys(additions).join(", ")}`);
|
||||||
|
if (!ctx.dryRun) writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Bump every `@wrnexus/*` range to `^target`. Returns the human-readable changes. */
|
/** Bump every `@wrnexus/*` range to `^target`. Returns the human-readable changes. */
|
||||||
@@ -141,17 +205,46 @@ function refreshFrameworkFiles(appRoot: string, dryRun: boolean, log: (m: string
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Update a single app dir (bump its package.json, refresh files, run migrations). */
|
/** Update a single app dir (bump its package.json, refresh files, run migrations). */
|
||||||
function updateApp(appRoot: string, target: string, dryRun: boolean): boolean {
|
interface UpdatedApp {
|
||||||
|
root: string;
|
||||||
|
packagePath: string;
|
||||||
|
pkg: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function backupProjectFiles(appRoot: string, from: string, target: string): string {
|
||||||
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||||
|
const backup = join(appRoot, ".wrnexus", "update-backups", `${stamp}-${from}-to-${target}`);
|
||||||
|
mkdirSync(backup, { recursive: true });
|
||||||
|
for (const name of [
|
||||||
|
"package.json",
|
||||||
|
"wrnexus.config.ts",
|
||||||
|
"wrnexus.config.js",
|
||||||
|
"wrnexus.config.mjs",
|
||||||
|
"tsconfig.json",
|
||||||
|
".gitignore",
|
||||||
|
"CLAUDE.md",
|
||||||
|
"llms.txt",
|
||||||
|
]) {
|
||||||
|
const source = join(appRoot, name);
|
||||||
|
if (existsSync(source)) cpSync(source, join(backup, name));
|
||||||
|
}
|
||||||
|
return backup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update one app without marking success until install and verification pass. */
|
||||||
|
export function updateApp(appRoot: string, target: string, dryRun: boolean): UpdatedApp | null {
|
||||||
const pkgPath = join(appRoot, "package.json");
|
const pkgPath = join(appRoot, "package.json");
|
||||||
if (!existsSync(pkgPath)) {
|
if (!existsSync(pkgPath)) {
|
||||||
console.log(` ⚠ ${appRoot}: no package.json — skipped`);
|
console.log(` ⚠ ${appRoot}: no package.json — skipped`);
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, unknown>;
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, unknown>;
|
||||||
const from = appVersion(appRoot, pkg);
|
const from = appVersion(appRoot, pkg);
|
||||||
const log = (m: string) => console.log(` ${m}`);
|
const log = (m: string) => console.log(` ${m}`);
|
||||||
console.log(` ▸ ${pkg.name ?? appRoot} (${from} → ${target})`);
|
console.log(` ▸ ${pkg.name ?? appRoot} (${from} → ${target})`);
|
||||||
|
|
||||||
|
if (!dryRun) log(`backup: ${backupProjectFiles(appRoot, from, target)}`);
|
||||||
|
|
||||||
const changes = bumpDeps(pkg, target);
|
const changes = bumpDeps(pkg, target);
|
||||||
changes.forEach((c) => log(c));
|
changes.forEach((c) => log(c));
|
||||||
if (!changes.length) log("dependencies already current");
|
if (!changes.length) log("dependencies already current");
|
||||||
@@ -164,9 +257,32 @@ function updateApp(appRoot: string, target: string, dryRun: boolean): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record the applied version so the next update knows where it started.
|
// A migration may have conservatively edited package.json. Merge those
|
||||||
pkg.wrnexus = { ...(pkg.wrnexus as object), version: target };
|
// changes back before writing the dependency bump so neither side is lost.
|
||||||
|
if (!dryRun) {
|
||||||
|
Object.assign(pkg, JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, unknown>);
|
||||||
|
bumpDeps(pkg, target);
|
||||||
|
}
|
||||||
if (!dryRun) writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
if (!dryRun) writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
||||||
|
return { root: appRoot, packagePath: pkgPath, pkg };
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyApp(app: UpdatedApp): boolean {
|
||||||
|
const health = inspectProject(app.root);
|
||||||
|
const failed = health.filter((check) => !check.ok && check.name !== "configuration");
|
||||||
|
if (failed.length) {
|
||||||
|
for (const check of failed) console.error(` ✗ ${check.name}: ${check.detail}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const scripts = (app.pkg.scripts ?? {}) as Record<string, string>;
|
||||||
|
const commands: string[][] = [];
|
||||||
|
if (scripts.check) commands.push(["run", "check"]);
|
||||||
|
if (scripts.build) commands.push(["run", "build"]);
|
||||||
|
for (const args of commands) {
|
||||||
|
console.log(`\n Verifying ${app.pkg.name ?? app.root}: bun ${args.join(" ")}…`);
|
||||||
|
const result = spawnSync(process.execPath, args, { cwd: app.root, stdio: "inherit" });
|
||||||
|
if (result.status !== 0) return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,17 +302,42 @@ async function workspaceApps(root: string): Promise<string[] | null> {
|
|||||||
export async function runUpdate(dir: string, args: string[]): Promise<void> {
|
export async function runUpdate(dir: string, args: string[]): Promise<void> {
|
||||||
const root = resolve(dir);
|
const root = resolve(dir);
|
||||||
const dryRun = args.includes("--dry-run");
|
const dryRun = args.includes("--dry-run");
|
||||||
|
const verify = !args.includes("--no-verify");
|
||||||
const versionArg = args.find((a) => a.startsWith("--version="))?.split("=")[1];
|
const versionArg = args.find((a) => a.startsWith("--version="))?.split("=")[1];
|
||||||
const target =
|
const target =
|
||||||
versionArg ?? (args.includes("--latest") ? latestPublished() : null) ?? cliVersion();
|
versionArg ?? (args.includes("--latest") ? latestPublished() : null) ?? cliVersion();
|
||||||
|
|
||||||
|
// Always let the target CLI apply its own migrations. Without this handoff,
|
||||||
|
// an older installed CLI could bump dependency versions but would not know
|
||||||
|
// about syntax/config migrations shipped by the newer release.
|
||||||
|
if (!args.includes("--delegated") && cmp(target, cliVersion()) > 0) {
|
||||||
|
console.log(`\n Fetching WRNexusJS CLI ${target} to run its project migrations…`);
|
||||||
|
const forwarded = args.filter(
|
||||||
|
(arg) => !arg.startsWith("--version=") && arg !== "--latest" && arg !== "--delegated",
|
||||||
|
);
|
||||||
|
const result = spawnSync(
|
||||||
|
process.execPath,
|
||||||
|
[
|
||||||
|
"x",
|
||||||
|
`@wrnexus/cli@${target}`,
|
||||||
|
"update",
|
||||||
|
dir,
|
||||||
|
`--version=${target}`,
|
||||||
|
"--delegated",
|
||||||
|
...forwarded,
|
||||||
|
],
|
||||||
|
{ cwd: root, stdio: "inherit" },
|
||||||
|
);
|
||||||
|
if (result.status !== 0) process.exitCode = result.status ?? 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`\n ⚡ wrnexus update → ${target}${dryRun ? " (dry run)" : ""}\n`);
|
console.log(`\n ⚡ wrnexus update → ${target}${dryRun ? " (dry run)" : ""}\n`);
|
||||||
|
|
||||||
// Workspace → update the root manifest + every app; else just this app.
|
// Workspace → update the root manifest + every app; else just this app.
|
||||||
const apps = await workspaceApps(root);
|
const apps = await workspaceApps(root);
|
||||||
const targets = apps ? [root, ...apps] : [root];
|
const targets = apps ? [root, ...apps] : [root];
|
||||||
let updated = 0;
|
const updated = targets.map((t) => updateApp(t, target, dryRun)).filter(Boolean) as UpdatedApp[];
|
||||||
for (const t of targets) if (updateApp(t, target, dryRun)) updated++;
|
|
||||||
|
|
||||||
if (dryRun) {
|
if (dryRun) {
|
||||||
console.log(`\n Dry run — no files written. Re-run without --dry-run to apply.\n`);
|
console.log(`\n Dry run — no files written. Re-run without --dry-run to apply.\n`);
|
||||||
@@ -211,6 +352,24 @@ export async function runUpdate(dir: string, args: string[]): Promise<void> {
|
|||||||
process.exit(res.status ?? 1);
|
process.exit(res.status ?? 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\n ✓ Updated ${updated} package(s) to ${target}.`);
|
if (verify) {
|
||||||
console.log(` Review the changes, then rebuild/redeploy (wrnexus build).\n`);
|
const appsToVerify = apps ? updated.filter((app) => app.root !== root) : updated;
|
||||||
|
for (const app of appsToVerify) {
|
||||||
|
if (!verifyApp(app)) {
|
||||||
|
console.error(
|
||||||
|
`\n ✗ Update files were applied, but verification failed. Fix the reported error and re-run wrnexus update.`,
|
||||||
|
);
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const app of updated) {
|
||||||
|
app.pkg.wrnexus = { ...(app.pkg.wrnexus as object), version: target };
|
||||||
|
writeFileSync(app.packagePath, JSON.stringify(app.pkg, null, 2) + "\n", "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n ✓ Updated and verified ${updated.length} package(s) at ${target}.`);
|
||||||
|
console.log(` Backups are under .wrnexus/update-backups/. Review and redeploy.\n`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { afterEach, expect, test } from "bun:test";
|
||||||
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { compareVersions, notifyIfUpdateAvailable } from "../src/update-notifier.ts";
|
||||||
|
|
||||||
|
const originalCache = process.env.WRNEXUS_UPDATE_CACHE;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalCache === undefined) delete process.env.WRNEXUS_UPDATE_CACHE;
|
||||||
|
else process.env.WRNEXUS_UPDATE_CACHE = originalCache;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("compareVersions compares stable semantic versions", () => {
|
||||||
|
expect(compareVersions("0.2.14", "0.2.13")).toBe(1);
|
||||||
|
expect(compareVersions("0.2.13", "0.2.13")).toBe(0);
|
||||||
|
expect(compareVersions("0.2.12", "0.2.13")).toBe(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("cached update checks suggest the exact upgrade command", () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "wrnexus-notify-"));
|
||||||
|
const cache = join(root, "update.json");
|
||||||
|
process.env.WRNEXUS_UPDATE_CACHE = cache;
|
||||||
|
writeFileSync(cache, JSON.stringify({ checkedAt: Date.now(), latest: "0.2.14" }));
|
||||||
|
const messages: string[] = [];
|
||||||
|
const original = console.log;
|
||||||
|
console.log = (...parts) => messages.push(parts.join(" "));
|
||||||
|
|
||||||
|
try {
|
||||||
|
notifyIfUpdateAvailable("0.2.13", "dev");
|
||||||
|
expect(messages.join("\n")).toContain("WRNexusJS 0.2.13 → 0.2.14");
|
||||||
|
expect(messages.join("\n")).toContain("wrnexus update --latest");
|
||||||
|
} finally {
|
||||||
|
console.log = original;
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { updateApp } from "../src/update.ts";
|
||||||
|
|
||||||
|
test("updateApp migrates project files without marking an unverified update complete", () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "wrnexus-update-"));
|
||||||
|
mkdirSync(join(root, "app", "pages"), { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
join(root, "package.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
name: "legacy-app",
|
||||||
|
scripts: { dev: "wrnexus dev ." },
|
||||||
|
dependencies: { "@wrnexus/core": "0.2.13" },
|
||||||
|
wrnexus: { version: "0.2.13" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
writeFileSync(join(root, ".gitignore"), "node_modules/\n");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = updateApp(root, "0.2.14", false);
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
||||||
|
expect(pkg.dependencies["@wrnexus/core"]).toBe("^0.2.14");
|
||||||
|
expect(pkg.scripts.build).toBe("wrnexus build .");
|
||||||
|
expect(pkg.scripts.start).toBe("bun dist/server.js");
|
||||||
|
expect(pkg.scripts.production).toBe("bun run build && bun run start");
|
||||||
|
expect(pkg.wrnexus.version).toBe("0.2.13");
|
||||||
|
|
||||||
|
const gitignore = readFileSync(join(root, ".gitignore"), "utf8");
|
||||||
|
expect(gitignore).toContain("!.env.example");
|
||||||
|
expect(gitignore).toContain("mobile/android/");
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/compiler",
|
"name": "@wrnexus/compiler",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export type Middleware = (ctx: Context, next: Next) => Promise<Response> | Respo
|
|||||||
|
|
||||||
/** SEO metadata rendered into the document `<head>`. */
|
/** SEO metadata rendered into the document `<head>`. */
|
||||||
export type SeoConfig = {
|
export type SeoConfig = {
|
||||||
|
/** BCP 47 document language used on `<html lang>` (default: `en`). */
|
||||||
|
lang?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
titleTemplate?: string;
|
titleTemplate?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/csr",
|
"name": "@wrnexus/csr",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/dev-server",
|
"name": "@wrnexus/dev-server",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ export interface ProdOptions {
|
|||||||
theme?: ResolvedTheme;
|
theme?: ResolvedTheme;
|
||||||
/** Absolute path to the pre-built Wire UI stylesheet (`ui.css`). */
|
/** Absolute path to the pre-built Wire UI stylesheet (`ui.css`). */
|
||||||
uiCssPath?: string;
|
uiCssPath?: string;
|
||||||
|
/** Combined production theme + Wire UI stylesheet. */
|
||||||
|
frameworkCssPath?: string;
|
||||||
/** Pre-built `window.__wireSchemas = {...}` script for client validation. */
|
/** Pre-built `window.__wireSchemas = {...}` script for client validation. */
|
||||||
schemasJs?: string;
|
schemasJs?: string;
|
||||||
/** Resolved i18n bundle (default lang + locale messages). */
|
/** Resolved i18n bundle (default lang + locale messages). */
|
||||||
@@ -216,6 +218,8 @@ function createProdAssetServer(opts: ProdOptions): AssetServer {
|
|||||||
if (pathname === "/__wrnexus/theme.css") return serveFile(opts.themePath, CSS_HEADERS);
|
if (pathname === "/__wrnexus/theme.css") return serveFile(opts.themePath, CSS_HEADERS);
|
||||||
if (pathname === "/__wrnexus/theme.js") return serveFile(opts.themeJsPath, JS_HEADERS);
|
if (pathname === "/__wrnexus/theme.js") return serveFile(opts.themeJsPath, JS_HEADERS);
|
||||||
if (pathname === "/__wrnexus/ui.css") return serveFile(opts.uiCssPath, CSS_HEADERS);
|
if (pathname === "/__wrnexus/ui.css") return serveFile(opts.uiCssPath, CSS_HEADERS);
|
||||||
|
if (pathname === "/__wrnexus/framework.css")
|
||||||
|
return serveFile(opts.frameworkCssPath, CSS_HEADERS);
|
||||||
if (pathname === "/__wrnexus/styles.css") return serveFile(opts.stylesPath, CSS_HEADERS);
|
if (pathname === "/__wrnexus/styles.css") return serveFile(opts.stylesPath, CSS_HEADERS);
|
||||||
return servePublicAsset(opts.publicDir, pathname, MODE);
|
return servePublicAsset(opts.publicDir, pathname, MODE);
|
||||||
},
|
},
|
||||||
@@ -279,6 +283,7 @@ export function createProductionHandlers(
|
|||||||
assets,
|
assets,
|
||||||
hasStyles: !!opts.stylesPath,
|
hasStyles: !!opts.stylesPath,
|
||||||
hasUi: !!opts.uiCssPath,
|
hasUi: !!opts.uiCssPath,
|
||||||
|
hasFrameworkStyles: !!opts.frameworkCssPath,
|
||||||
theme: opts.theme,
|
theme: opts.theme,
|
||||||
i18n: opts.i18n,
|
i18n: opts.i18n,
|
||||||
inlineStyles: opts.inlineStyles,
|
inlineStyles: opts.inlineStyles,
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ export interface RuntimeDeps {
|
|||||||
hasStyles?: boolean;
|
hasStyles?: boolean;
|
||||||
/** When true, inject the Wire UI stylesheet link (`/__wrnexus/ui.css`). */
|
/** When true, inject the Wire UI stylesheet link (`/__wrnexus/ui.css`). */
|
||||||
hasUi?: boolean;
|
hasUi?: boolean;
|
||||||
|
/** Production build combined theme + UI stylesheet. */
|
||||||
|
hasFrameworkStyles?: boolean;
|
||||||
/** Resolved theme config: enables `/__wrnexus/theme.css` + `<html data-theme>`. */
|
/** Resolved theme config: enables `/__wrnexus/theme.css` + `<html data-theme>`. */
|
||||||
theme?: ResolvedTheme;
|
theme?: ResolvedTheme;
|
||||||
/** Resolved i18n bundle: enables `ctx.t`, `<html lang>`, and `{t:key}` markers. */
|
/** Resolved i18n bundle: enables `ctx.t`, `<html lang>`, and `{t:key}` markers. */
|
||||||
@@ -129,8 +131,14 @@ export interface RuntimeDeps {
|
|||||||
realtimeBus?: RealtimeBus;
|
realtimeBus?: RealtimeBus;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PWA_CLIENT = `if ("serviceWorker" in navigator) {
|
export const PWA_CLIENT = `if ("serviceWorker" in navigator) {
|
||||||
addEventListener("load", () => navigator.serviceWorker.register("/sw.js").catch(() => {}));
|
var swUrl = "/sw.js";
|
||||||
|
if (window.trustedTypes) {
|
||||||
|
try {
|
||||||
|
swUrl = window.trustedTypes.createPolicy("wrnexus-pwa", { createScriptURL: function(value) { return value; } }).createScriptURL(swUrl);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
addEventListener("load", () => navigator.serviceWorker.register(swUrl).catch(() => {}));
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
function renderPwaServiceWorker(pwa: PwaConfig): string {
|
function renderPwaServiceWorker(pwa: PwaConfig): string {
|
||||||
@@ -561,11 +569,15 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
|||||||
// Order: theme tokens, then Wire UI, then the app stylesheet — so the app's
|
// Order: theme tokens, then Wire UI, then the app stylesheet — so the app's
|
||||||
// own CSS (loaded last) can override both the tokens and the UI classes.
|
// own CSS (loaded last) can override both the tokens and the UI classes.
|
||||||
const headParts: string[] = [];
|
const headParts: string[] = [];
|
||||||
if (deps.theme) {
|
if (deps.hasFrameworkStyles) {
|
||||||
|
headParts.push(
|
||||||
|
`<link rel="stylesheet" href="${versionAssetUrl("/__wrnexus/framework.css", deps.assetVersion)}" />`,
|
||||||
|
);
|
||||||
|
} else if (deps.theme) {
|
||||||
const themeHref = versionAssetUrl(THEME_CSS_HREF, deps.assetVersion);
|
const themeHref = versionAssetUrl(THEME_CSS_HREF, deps.assetVersion);
|
||||||
headParts.push(`<link rel="stylesheet" href="${themeHref}" />`);
|
headParts.push(`<link rel="stylesheet" href="${themeHref}" />`);
|
||||||
}
|
}
|
||||||
if (deps.hasUi) {
|
if (deps.hasUi && !deps.hasFrameworkStyles) {
|
||||||
headParts.push(
|
headParts.push(
|
||||||
`<link rel="stylesheet" href="${versionAssetUrl("/__wrnexus/ui.css", deps.assetVersion)}" />`,
|
`<link rel="stylesheet" href="${versionAssetUrl("/__wrnexus/ui.css", deps.assetVersion)}" />`,
|
||||||
);
|
);
|
||||||
@@ -677,7 +689,9 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
|||||||
new Response(PWA_CLIENT, {
|
new Response(PWA_CLIENT, {
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "text/javascript; charset=utf-8",
|
"content-type": "text/javascript; charset=utf-8",
|
||||||
"cache-control": "no-cache",
|
"cache-control": url.searchParams.has("v")
|
||||||
|
? "public, max-age=31536000, immutable"
|
||||||
|
: "no-cache",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -687,7 +701,9 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
|||||||
new Response(MOBILE_CLIENT, {
|
new Response(MOBILE_CLIENT, {
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "text/javascript; charset=utf-8",
|
"content-type": "text/javascript; charset=utf-8",
|
||||||
"cache-control": "no-cache",
|
"cache-control": url.searchParams.has("v")
|
||||||
|
? "public, max-age=31536000, immutable"
|
||||||
|
: "no-cache",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -1046,14 +1062,17 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
|||||||
|
|
||||||
// Point 3: only ship the JS this page actually uses.
|
// Point 3: only ship the JS this page actually uses.
|
||||||
const scripts = collectScripts(body).map((src) => versionAssetUrl(src, deps.assetVersion));
|
const scripts = collectScripts(body).map((src) => versionAssetUrl(src, deps.assetVersion));
|
||||||
if (pwaServiceWorkerEnabled) scripts.push("/__wrnexus/pwa.js");
|
if (pwaServiceWorkerEnabled)
|
||||||
if (deps.mobile?.enabled !== false) scripts.push("/__wrnexus/mobile.js");
|
scripts.push(versionAssetUrl("/__wrnexus/pwa.js", deps.assetVersion));
|
||||||
|
if (deps.mobile?.enabled !== false && usesMobileRuntime(body))
|
||||||
|
scripts.push(versionAssetUrl("/__wrnexus/mobile.js", deps.assetVersion));
|
||||||
|
|
||||||
// <html> attributes: no-flash theme (from cookie, validated) + active lang.
|
// <html> attributes: no-flash theme (from cookie, validated) + active lang.
|
||||||
const attrs: string[] = [];
|
const attrs: string[] = [];
|
||||||
if (deps.theme)
|
if (deps.theme)
|
||||||
attrs.push(`data-theme="${resolveThemeName(ctx.cookies.get(THEME_COOKIE), deps.theme)}"`);
|
attrs.push(`data-theme="${resolveThemeName(ctx.cookies.get(THEME_COOKIE), deps.theme)}"`);
|
||||||
if (deps.i18n && ctx.lang) attrs.push(`lang="${ctx.lang}"`);
|
const language = deps.i18n && ctx.lang ? ctx.lang : (meta.lang ?? deps.seo?.lang ?? "en");
|
||||||
|
attrs.push(`lang="${safeLanguageTag(language)}"`);
|
||||||
const htmlAttrs = attrs.length ? ` ${attrs.join(" ")}` : undefined;
|
const htmlAttrs = attrs.length ? ` ${attrs.join(" ")}` : undefined;
|
||||||
|
|
||||||
const html = renderDocument({
|
const html = renderDocument({
|
||||||
@@ -1412,6 +1431,17 @@ export function collectScripts(body: string): string[] {
|
|||||||
return scripts;
|
return scripts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether rendered markup needs the Capacitor/native browser bridge. */
|
||||||
|
export function usesMobileRuntime(body: string): boolean {
|
||||||
|
return /\b(?:data-mobile-[\w-]+|data-native-(?:browser|mobile|only|requires|unsupported|options)|data-on-wrnexus-(?:browser|mobile)-[\w-]+)\b/.test(
|
||||||
|
body,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeLanguageTag(value: string): string {
|
||||||
|
return /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(value) ? value : "en";
|
||||||
|
}
|
||||||
|
|
||||||
function versionAssetUrl(src: string, version?: string): string {
|
function versionAssetUrl(src: string, version?: string): string {
|
||||||
if (!version) return src;
|
if (!version) return src;
|
||||||
return `${src}${src.includes("?") ? "&" : "?"}v=${encodeURIComponent(version)}`;
|
return `${src}${src.includes("?") ? "&" : "?"}v=${encodeURIComponent(version)}`;
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { PWA_CLIENT, usesMobileRuntime } from "../src/runtime.ts";
|
||||||
|
|
||||||
|
test("PWA registration is valid JavaScript and Trusted Types compatible", () => {
|
||||||
|
expect(() => new Bun.Transpiler({ loader: "js" }).transformSync(PWA_CLIENT)).not.toThrow();
|
||||||
|
expect(PWA_CLIENT).toContain('createPolicy("wrnexus-pwa"');
|
||||||
|
expect(PWA_CLIENT).toContain("createScriptURL(swUrl)");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mobile runtime is shipped only for pages using mobile or native directives", () => {
|
||||||
|
expect(usesMobileRuntime('<main class="page">Docs</main>')).toBe(false);
|
||||||
|
expect(usesMobileRuntime('<button data-native-mobile="share">Share</button>')).toBe(true);
|
||||||
|
expect(usesMobileRuntime("<div data-mobile-only>App</div>")).toBe(true);
|
||||||
|
expect(usesMobileRuntime('<button data-on-wrnexus-browser-click="open">Open</button>')).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/encryption",
|
"name": "@wrnexus/encryption",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/i18n",
|
"name": "@wrnexus/i18n",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -55,8 +55,12 @@ export function renderDocument(opts: RenderOptions): string {
|
|||||||
const extraHead = opts.extraHead ? `\n ${opts.extraHead}` : "";
|
const extraHead = opts.extraHead ? `\n ${opts.extraHead}` : "";
|
||||||
const extraBody = opts.extraBody ? `\n ${opts.extraBody}` : "";
|
const extraBody = opts.extraBody ? `\n ${opts.extraBody}` : "";
|
||||||
|
|
||||||
|
const htmlAttrs = /(?:^|\s)lang\s*=/.test(opts.htmlAttrs ?? "")
|
||||||
|
? (opts.htmlAttrs ?? "")
|
||||||
|
: `${opts.htmlAttrs ?? ""} lang="en"`;
|
||||||
|
|
||||||
return `<!doctype html>
|
return `<!doctype html>
|
||||||
<html${opts.htmlAttrs ?? ""}>
|
<html${htmlAttrs}>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|||||||
@@ -22,3 +22,16 @@ test("deduplicates runtime scripts and module preloads", () => {
|
|||||||
expect(html.match(/rel="modulepreload"/g)).toHaveLength(1);
|
expect(html.match(/rel="modulepreload"/g)).toHaveLength(1);
|
||||||
expect(html.match(/src="\/app\.js"/g)).toHaveLength(1);
|
expect(html.match(/src="\/app\.js"/g)).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("always emits a document language and preserves an explicit language", () => {
|
||||||
|
const fallback = renderDocument({ meta: {}, body: "" });
|
||||||
|
expect(fallback).toContain('<html lang="en">');
|
||||||
|
|
||||||
|
const explicit = renderDocument({
|
||||||
|
meta: {},
|
||||||
|
body: "",
|
||||||
|
htmlAttrs: ' data-theme="dark" lang="fr"',
|
||||||
|
});
|
||||||
|
expect(explicit).toContain('<html data-theme="dark" lang="fr">');
|
||||||
|
expect(explicit).not.toContain('lang="en"');
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/styles",
|
"name": "@wrnexus/styles",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -175,6 +175,36 @@ export function renderFontHead(fonts?: FontConfig): string {
|
|||||||
return out.join("\n ");
|
return out.join("\n ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Production variant that inlines the small Google Fonts stylesheet at build
|
||||||
|
* time. This removes a render-blocking CSS round trip while retaining the same
|
||||||
|
* font files, `font-display`, CSP sources, and offline-safe fallback markup.
|
||||||
|
*/
|
||||||
|
export async function renderProductionFontHead(
|
||||||
|
fonts?: FontConfig,
|
||||||
|
fetcher: (input: string, init?: RequestInit) => Promise<Response> = fetch,
|
||||||
|
): Promise<string> {
|
||||||
|
const fallback = renderFontHead(fonts);
|
||||||
|
if (!fonts?.google?.length) return fallback;
|
||||||
|
const url = googleFontsUrl(fonts.google, fonts.display ?? "swap");
|
||||||
|
try {
|
||||||
|
const response = await fetcher(url, {
|
||||||
|
headers: {
|
||||||
|
// Google returns compact WOFF2 rules to modern browser user agents.
|
||||||
|
"user-agent": "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) return fallback;
|
||||||
|
const css = (await response.text()).replace(/<\/style/gi, "<\\/style");
|
||||||
|
const stylesheet = `<link rel="stylesheet" href="${escAttr(url)}">`;
|
||||||
|
return fallback
|
||||||
|
.replace(`<link rel="preconnect" href="${GOOGLE_CSS}">\n `, "")
|
||||||
|
.replace(stylesheet, `<style data-wrnexus-google-fonts>\n${css}\n</style>`);
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CSP source hosts required by the configured fonts, so the policy can be
|
* CSP source hosts required by the configured fonts, so the policy can be
|
||||||
* auto-extended (Google Fonts need their CSS + static hosts allow-listed).
|
* auto-extended (Google Fonts need their CSS + static hosts allow-listed).
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export type {
|
|||||||
export { loadAppConfig, loadRawConfig, headToString, resolveProfile, loadEnv } from "./config.ts";
|
export { loadAppConfig, loadRawConfig, headToString, resolveProfile, loadEnv } from "./config.ts";
|
||||||
export { findStyleEntry, bundleCss } from "./styles.ts";
|
export { findStyleEntry, bundleCss } from "./styles.ts";
|
||||||
export type { FontConfig, GoogleFont, LocalFontFace, FontDisplay } from "./fonts.ts";
|
export type { FontConfig, GoogleFont, LocalFontFace, FontDisplay } from "./fonts.ts";
|
||||||
export { renderFontHead, fontCspSources } from "./fonts.ts";
|
export { renderFontHead, renderProductionFontHead, fontCspSources } from "./fonts.ts";
|
||||||
export type { ThemeConfig, ThemeTokens, ResolvedTheme } from "./theme.ts";
|
export type { ThemeConfig, ThemeTokens, ResolvedTheme } from "./theme.ts";
|
||||||
export {
|
export {
|
||||||
DEFAULT_THEMES,
|
DEFAULT_THEMES,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { expect, test } from "bun:test";
|
import { expect, test } from "bun:test";
|
||||||
import { fontCspSources, renderFontHead } from "../src/index.ts";
|
import { fontCspSources, renderFontHead, renderProductionFontHead } from "../src/index.ts";
|
||||||
|
|
||||||
test("renders subsetted Google and escaped local font markup", () => {
|
test("renders subsetted Google and escaped local font markup", () => {
|
||||||
const html = renderFontHead({
|
const html = renderFontHead({
|
||||||
@@ -20,3 +20,13 @@ test("renders subsetted Google and escaped local font markup", () => {
|
|||||||
font: ["https://fonts.gstatic.com"],
|
font: ["https://fonts.gstatic.com"],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("production font head inlines Google CSS and removes its blocking stylesheet", async () => {
|
||||||
|
const html = await renderProductionFontHead(
|
||||||
|
{ google: [{ family: "Plus Jakarta Sans", weights: [400, 700] }] },
|
||||||
|
() => Promise.resolve(new Response('@font-face{font-family:"Plus Jakarta Sans"}')),
|
||||||
|
);
|
||||||
|
expect(html).toContain("data-wrnexus-google-fonts");
|
||||||
|
expect(html).toContain("@font-face");
|
||||||
|
expect(html).not.toContain('<link rel="stylesheet" href="https://fonts.googleapis.com');
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/test",
|
"name": "@wrnexus/test",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"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.13",
|
"version": "0.2.14",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user