feat(cli): add production workspace command

This commit is contained in:
2026-07-19 21:57:55 +05:30
parent dcdb5e766d
commit 5ce45b4973
58 changed files with 235 additions and 90 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ai", "name": "@wrnexus/ai",
"version": "0.2.63", "version": "0.2.64",
"type": "module", "type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/authz", "name": "@wrnexus/authz",
"version": "0.2.63", "version": "0.2.64",
"type": "module", "type": "module",
"description": "@wrnexus/authz — part of the WrNexus framework.", "description": "@wrnexus/authz — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+12
View File
@@ -36,6 +36,7 @@ Every command accepts an optional `[app-dir]` (defaults to `.`). Commands that r
| `wrnexus workspace <name>` | Scaffold a monorepo (`apps/*` + shared `packages/*`). | | `wrnexus workspace <name>` | Scaffold a monorepo (`apps/*` + shared `packages/*`). |
| `wrnexus workspace add <name>` | Add and register an app in the current workspace. | | `wrnexus workspace add <name>` | Add and register an app in the current workspace. |
| `wrnexus gateway [--port=3000]` | Serve every workspace app behind one port, routed by domain. | | `wrnexus gateway [--port=3000]` | Serve every workspace app behind one port, routed by domain. |
| `wrnexus production [workspace-dir]` | Build, migrate, and serve every workspace app in production. |
| `wrnexus generate <type> <name>` | Scaffold a `page` \| `component` \| `api` \| `schema`. | | `wrnexus generate <type> <name>` | Scaffold a `page` \| `component` \| `api` \| `schema`. |
| `wrnexus generate routes` | Regenerate the typed routes file (`app/routes.gen.ts`). | | `wrnexus generate routes` | Regenerate the typed routes file (`app/routes.gen.ts`). |
| `wrnexus generate docker` | Scaffold `Dockerfile`, `.dockerignore`, and `docker-compose.yml`. | | `wrnexus generate docker` | Scaffold `Dockerfile`, `.dockerignore`, and `docker-compose.yml`. |
@@ -168,6 +169,17 @@ wrnexus workspace acme
wrnexus gateway --port=3000 wrnexus gateway --port=3000
``` ```
For a complete production start, use the first-class workspace orchestrator:
```bash
wrnexus production --host=0.0.0.0 --port=3000
```
It builds every registered app, applies default and named-database SQL migrations
when present, and starts the production gateway only after preparation succeeds.
Use `--prepare-only`, `--no-build`, or `--no-migrate` when deployment stages are
managed separately; `--environment=<name>` selects another workspace environment.
From a workspace root, add and register another app in one command: From a workspace root, add and register another app in one command:
```bash ```bash
+11 -11
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.2.63", "version": "0.2.64",
"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.63", "@wrnexus/core": "^0.2.64",
"@wrnexus/router": "^0.2.63", "@wrnexus/router": "^0.2.64",
"@wrnexus/csr": "^0.2.63", "@wrnexus/csr": "^0.2.64",
"@wrnexus/compiler": "^0.2.63", "@wrnexus/compiler": "^0.2.64",
"@wrnexus/styles": "^0.2.63", "@wrnexus/styles": "^0.2.64",
"@wrnexus/dev-server": "^0.2.63", "@wrnexus/dev-server": "^0.2.64",
"@wrnexus/ui": "^0.2.63", "@wrnexus/ui": "^0.2.64",
"@wrnexus/validation": "^0.2.63", "@wrnexus/validation": "^0.2.64",
"@wrnexus/i18n": "^0.2.63", "@wrnexus/i18n": "^0.2.64",
"@wrnexus/db": "^0.2.63" "@wrnexus/db": "^0.2.64"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/compiler", "name": "@wrnexus/compiler",
"version": "0.2.63", "version": "0.2.64",
"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 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/core", "name": "@wrnexus/core",
"version": "0.2.63", "version": "0.2.64",
"type": "module", "type": "module",
"description": "@wrnexus/core — part of the WrNexus framework.", "description": "@wrnexus/core — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/csr", "name": "@wrnexus/csr",
"version": "0.2.63", "version": "0.2.64",
"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.63" "@wrnexus/core": "^0.2.64"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/db", "name": "@wrnexus/db",
"version": "0.2.63", "version": "0.2.64",
"type": "module", "type": "module",
"description": "@wrnexus/db — part of the WrNexus framework.", "description": "@wrnexus/db — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+13 -13
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.2.63", "version": "0.2.64",
"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.63", "@wrnexus/core": "^0.2.64",
"@wrnexus/router": "^0.2.63", "@wrnexus/router": "^0.2.64",
"@wrnexus/ssr": "^0.2.63", "@wrnexus/ssr": "^0.2.64",
"@wrnexus/csr": "^0.2.63", "@wrnexus/csr": "^0.2.64",
"@wrnexus/compiler": "^0.2.63", "@wrnexus/compiler": "^0.2.64",
"@wrnexus/styles": "^0.2.63", "@wrnexus/styles": "^0.2.64",
"@wrnexus/ui": "^0.2.63", "@wrnexus/ui": "^0.2.64",
"@wrnexus/validation": "^0.2.63", "@wrnexus/validation": "^0.2.64",
"@wrnexus/i18n": "^0.2.63", "@wrnexus/i18n": "^0.2.64",
"@wrnexus/db": "^0.2.63", "@wrnexus/db": "^0.2.64",
"@wrnexus/pubsub": "^0.2.63", "@wrnexus/pubsub": "^0.2.64",
"@wrnexus/uploader": "^0.2.63" "@wrnexus/uploader": "^0.2.64"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/encryption", "name": "@wrnexus/encryption",
"version": "0.2.63", "version": "0.2.64",
"type": "module", "type": "module",
"description": "@wrnexus/encryption — part of the WrNexus framework.", "description": "@wrnexus/encryption — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/helpers", "name": "@wrnexus/helpers",
"version": "0.2.63", "version": "0.2.64",
"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.63" "@wrnexus/core": "^0.2.64"
}, },
"files": [ "files": [
"dist" "dist"
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/i18n", "name": "@wrnexus/i18n",
"version": "0.2.63", "version": "0.2.64",
"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.63" "@wrnexus/core": "^0.2.64"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/jwt", "name": "@wrnexus/jwt",
"version": "0.2.63", "version": "0.2.64",
"type": "module", "type": "module",
"description": "@wrnexus/jwt — part of the WrNexus framework.", "description": "@wrnexus/jwt — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/mobile", "name": "@wrnexus/mobile",
"version": "0.2.63", "version": "0.2.64",
"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.63" "@wrnexus/native": "^0.2.64"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/native", "name": "@wrnexus/native",
"version": "0.2.63", "version": "0.2.64",
"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 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/oauth", "name": "@wrnexus/oauth",
"version": "0.2.63", "version": "0.2.64",
"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 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/pubsub", "name": "@wrnexus/pubsub",
"version": "0.2.63", "version": "0.2.64",
"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 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/queue", "name": "@wrnexus/queue",
"version": "0.2.63", "version": "0.2.64",
"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 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/reactive", "name": "@wrnexus/reactive",
"version": "0.2.63", "version": "0.2.64",
"type": "module", "type": "module",
"description": "@wrnexus/reactive — part of the WrNexus framework.", "description": "@wrnexus/reactive — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/router", "name": "@wrnexus/router",
"version": "0.2.63", "version": "0.2.64",
"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.63", "@wrnexus/compiler": "^0.2.64",
"@wrnexus/core": "^0.2.63" "@wrnexus/core": "^0.2.64"
}, },
"files": [ "files": [
"dist" "dist"
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ssr", "name": "@wrnexus/ssr",
"version": "0.2.63", "version": "0.2.64",
"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.63" "@wrnexus/core": "^0.2.64"
}, },
"files": [ "files": [
"dist" "dist"
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/styles", "name": "@wrnexus/styles",
"version": "0.2.63", "version": "0.2.64",
"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.63" "@wrnexus/uploader": "^0.2.64"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/test", "name": "@wrnexus/test",
"version": "0.2.63", "version": "0.2.64",
"type": "module", "type": "module",
"description": "@wrnexus/test — part of the WrNexus framework.", "description": "@wrnexus/test — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/tracking", "name": "@wrnexus/tracking",
"version": "0.2.63", "version": "0.2.64",
"type": "module", "type": "module",
"description": "@wrnexus/tracking — part of the WrNexus framework.", "description": "@wrnexus/tracking — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ui", "name": "@wrnexus/ui",
"version": "0.2.63", "version": "0.2.64",
"type": "module", "type": "module",
"description": "@wrnexus/ui — part of the WrNexus framework.", "description": "@wrnexus/ui — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -25,7 +25,7 @@
"./ui.css": "./ui.css" "./ui.css": "./ui.css"
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.63" "@wrnexus/core": "^0.2.64"
}, },
"files": [ "files": [
"dist", "dist",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/uploader", "name": "@wrnexus/uploader",
"version": "0.2.63", "version": "0.2.64",
"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.63" "@wrnexus/core": "^0.2.64"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/validation", "name": "@wrnexus/validation",
"version": "0.2.63", "version": "0.2.64",
"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 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ai", "name": "@wrnexus/ai",
"version": "0.2.63", "version": "0.2.64",
"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 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/authz", "name": "@wrnexus/authz",
"version": "0.2.63", "version": "0.2.64",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+12
View File
@@ -36,6 +36,7 @@ Every command accepts an optional `[app-dir]` (defaults to `.`). Commands that r
| `wrnexus workspace <name>` | Scaffold a monorepo (`apps/*` + shared `packages/*`). | | `wrnexus workspace <name>` | Scaffold a monorepo (`apps/*` + shared `packages/*`). |
| `wrnexus workspace add <name>` | Add and register an app in the current workspace. | | `wrnexus workspace add <name>` | Add and register an app in the current workspace. |
| `wrnexus gateway [--port=3000]` | Serve every workspace app behind one port, routed by domain. | | `wrnexus gateway [--port=3000]` | Serve every workspace app behind one port, routed by domain. |
| `wrnexus production [workspace-dir]` | Build, migrate, and serve every workspace app in production. |
| `wrnexus generate <type> <name>` | Scaffold a `page` \| `component` \| `api` \| `schema`. | | `wrnexus generate <type> <name>` | Scaffold a `page` \| `component` \| `api` \| `schema`. |
| `wrnexus generate routes` | Regenerate the typed routes file (`app/routes.gen.ts`). | | `wrnexus generate routes` | Regenerate the typed routes file (`app/routes.gen.ts`). |
| `wrnexus generate docker` | Scaffold `Dockerfile`, `.dockerignore`, and `docker-compose.yml`. | | `wrnexus generate docker` | Scaffold `Dockerfile`, `.dockerignore`, and `docker-compose.yml`. |
@@ -168,6 +169,17 @@ wrnexus workspace acme
wrnexus gateway --port=3000 wrnexus gateway --port=3000
``` ```
For a complete production start, use the first-class workspace orchestrator:
```bash
wrnexus production --host=0.0.0.0 --port=3000
```
It builds every registered app, applies default and named-database SQL migrations
when present, and starts the production gateway only after preparation succeeds.
Use `--prepare-only`, `--no-build`, or `--no-migrate` when deployment stages are
managed separately; `--environment=<name>` selects another workspace environment.
From a workspace root, add and register another app in one command: From a workspace root, add and register another app in one command:
```bash ```bash
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.2.63", "version": "0.2.64",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+7
View File
@@ -46,6 +46,7 @@ Usage:
wrnexus workspace add <name> [--domain=name.localhost] wrnexus workspace add <name> [--domain=name.localhost]
Add an app to the current workspace Add an app to the current workspace
wrnexus gateway [--port=3000] Serve every workspace app behind one port, routed by domain wrnexus gateway [--port=3000] Serve every workspace app behind one port, routed by domain
wrnexus production [workspace-dir] Build, migrate, and serve the complete production workspace
wrnexus generate <type> <name> Scaffold a page | component | api | schema wrnexus generate <type> <name> Scaffold a page | component | api | schema
wrnexus generate routes | docker | mobile wrnexus generate routes | docker | mobile
Generate routes or scaffold deployment targets Generate routes or scaffold deployment targets
@@ -108,6 +109,12 @@ async function main(): Promise<void> {
await runGateway(appRoot, rest); await runGateway(appRoot, rest);
break; break;
} }
case "production": {
const workspaceRoot = rest.find((a) => !a.startsWith("--")) ?? ".";
const { runProduction } = await import("./workspace.ts");
await runProduction(workspaceRoot, rest);
break;
}
case "generate": case "generate":
case "g": { case "g": {
if (rest[0] === "routes") { if (rest[0] === "routes") {
+9
View File
@@ -637,6 +637,15 @@ const MIGRATIONS: Migration[] = [
// The shared UI stylesheet update applies automatically. // The shared UI stylesheet update applies automatically.
}, },
}, },
{
version: "0.2.64",
id: "production-workspace-orchestrator",
description:
"Adds a first-class production command that builds, migrates, and serves every registered workspace app.",
apply() {
// Existing workspaces can invoke `wrnexus production` without generated-file changes.
},
},
]; ];
/** Release tooling uses this to require an explicit migration entry per version. */ /** Release tooling uses this to require an explicit migration entry per version. */
+89 -4
View File
@@ -10,7 +10,7 @@
* cross-process). `wrnexus.workspace.ts` maps each app to the domains it serves. * cross-process). `wrnexus.workspace.ts` maps each app to the domains it serves.
*/ */
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path"; import { dirname, join, resolve } from "node:path";
import { pathToFileURL } from "node:url"; import { pathToFileURL } from "node:url";
import { scaffoldApp } from "./create.ts"; import { scaffoldApp } from "./create.ts";
@@ -68,7 +68,8 @@ export const workspaceFiles = (name: string): Record<string, string> => ({
"workspaces": ["apps/*", "packages/*"], "workspaces": ["apps/*", "packages/*"],
"scripts": { "scripts": {
"dev": "wrnexus gateway", "dev": "wrnexus gateway",
"gateway": "wrnexus gateway" "gateway": "wrnexus gateway",
"production": "wrnexus production"
}, },
"devDependencies": { "devDependencies": {
"@wrnexus/cli": "${frameworkVersion}" "@wrnexus/cli": "${frameworkVersion}"
@@ -394,8 +395,7 @@ export async function runGateway(root: string, args: string[]): Promise<void> {
profileArg?.split("=")[1] || profileArg?.split("=")[1] ||
process.env.WRNEXUS_ENV || process.env.WRNEXUS_ENV ||
process.env.WRNEXUS_PROFILE || process.env.WRNEXUS_PROFILE ||
config.defaultEnvironment || (args.includes("--prod") ? "production" : config.defaultEnvironment || "development");
(args.includes("--prod") ? "production" : "development");
const resolved = resolveWorkspaceConfig(config, environment); const resolved = resolveWorkspaceConfig(config, environment);
@@ -426,6 +426,91 @@ export async function runGateway(root: string, args: string[]): Promise<void> {
}); });
} }
/** Return default/named database targets that contain SQL migrations. */
export function workspaceMigrationTargets(appRoot: string): Array<string | null> {
const dbRoot = join(resolve(appRoot), "app", "db");
if (!existsSync(dbRoot)) return [];
const targets: Array<string | null> = [];
const hasSql = (dir: string) =>
existsSync(dir) &&
readdirSync(dir, { withFileTypes: true }).some(
(entry) => entry.isFile() && entry.name.endsWith(".sql"),
);
if (hasSql(join(dbRoot, "migrations"))) targets.push(null);
for (const entry of readdirSync(dbRoot, { withFileTypes: true })) {
if (
entry.isDirectory() &&
entry.name !== "migrations" &&
hasSql(join(dbRoot, entry.name, "migrations"))
) {
targets.push(entry.name);
}
}
return targets;
}
/** Build, migrate, then serve every registered workspace app in production mode. */
export async function runProduction(root: string, args: string[]): Promise<void> {
const workspaceRoot = resolve(root);
const config = await loadWorkspaceConfig(workspaceRoot);
const shouldBuild = !args.includes("--no-build");
const shouldMigrate = !args.includes("--no-migrate");
const prepareOnly = args.includes("--prepare-only");
const environmentArg = args.find((arg) => arg.startsWith("--environment="));
const profileArg = args.find((arg) => arg.startsWith("--profile="));
const environment = environmentArg?.split("=")[1] || profileArg?.split("=")[1] || "production";
process.env.NODE_ENV = "production";
process.env.WRNEXUS_ENV = environment;
process.env.WRNEXUS_PROFILE = environment;
console.log(`\n ⚡ Preparing WRNexus workspace (${environment})\n`);
if (shouldBuild) {
const { runBuild } = await import("./build.ts");
for (const app of config.apps) {
const appRoot = resolve(workspaceRoot, app.dir);
if (!existsSync(join(appRoot, "package.json"))) {
throw new Error(`Workspace app '${app.name}' has no package.json at ${appRoot}.`);
}
console.log(`\n ▸ Building ${app.name}`);
await runBuild(appRoot);
}
} else {
console.log(" = builds skipped (--no-build)");
}
if (shouldMigrate) {
const { runDbCommand } = await import("./db.ts");
for (const app of config.apps) {
const appRoot = resolve(workspaceRoot, app.dir);
const targets = workspaceMigrationTargets(appRoot);
if (!targets.length) {
console.log(` = ${app.name}: no SQL migrations`);
continue;
}
for (const target of targets) {
console.log(`\n ▸ Migrating ${app.name}${target ? ` (${target})` : ""}`);
await runDbCommand(appRoot, "migrate", target ? [`--db=${target}`] : []);
}
}
} else {
console.log(" = migrations skipped (--no-migrate)");
}
console.log("\n ✓ Workspace builds and migrations are ready.\n");
if (prepareOnly) return;
const gatewayArgs = args.filter(
(arg) => arg !== "--prepare-only" && arg !== "--no-build" && arg !== "--no-migrate",
);
if (!gatewayArgs.some((arg) => arg.startsWith("--environment="))) {
gatewayArgs.push(`--environment=${environment}`);
}
if (!gatewayArgs.some((arg) => arg.startsWith("--host="))) gatewayArgs.push("--host=0.0.0.0");
if (!gatewayArgs.some((arg) => arg.startsWith("--port="))) gatewayArgs.push("--port=3000");
await runGateway(workspaceRoot, gatewayArgs);
}
function environmentVariableName(appName: string): string { function environmentVariableName(appName: string): string {
return `WRNEXUS_APP_${appName.replace(/[^A-Za-z0-9]/g, "_").toUpperCase()}_URL`; return `WRNEXUS_APP_${appName.replace(/[^A-Za-z0-9]/g, "_").toUpperCase()}_URL`;
} }
+22 -2
View File
@@ -1,9 +1,14 @@
import { expect, test } from "bun:test"; import { expect, test } from "bun:test";
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { currentCliVersion } from "../src/update-notifier.ts"; import { currentCliVersion } from "../src/update-notifier.ts";
import { addWorkspaceApp, insertWorkspaceApp, workspaceFiles } from "../src/workspace.ts"; import {
addWorkspaceApp,
insertWorkspaceApp,
workspaceFiles,
workspaceMigrationTargets,
} from "../src/workspace.ts";
test("workspace templates pin the running framework release", () => { test("workspace templates pin the running framework release", () => {
const files = workspaceFiles("acme"); const files = workspaceFiles("acme");
@@ -15,6 +20,21 @@ test("workspace templates pin the running framework release", () => {
expect(sharedPackage.dependencies["@wrnexus/pubsub"]).toBe(version); expect(sharedPackage.dependencies["@wrnexus/pubsub"]).toBe(version);
expect(files["README.md"]).toContain("http://127.0.0.1:3000"); expect(files["README.md"]).toContain("http://127.0.0.1:3000");
expect(files["README.md"]).toContain("internal gateway targets"); expect(files["README.md"]).toContain("internal gateway targets");
expect(JSON.parse(files["package.json"]!).scripts.production).toBe("wrnexus production");
});
test("production workspace detects default and named SQL migrations", () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-production-migrations-"));
try {
mkdirSync(join(root, "app", "db", "migrations"), { recursive: true });
mkdirSync(join(root, "app", "db", "analytics", "migrations"), { recursive: true });
mkdirSync(join(root, "app", "db", "empty", "migrations"), { recursive: true });
writeFileSync(join(root, "app", "db", "migrations", "0001_init.sql"), "select 1;");
writeFileSync(join(root, "app", "db", "analytics", "migrations", "0001_init.sql"), "select 1;");
expect(workspaceMigrationTargets(root)).toEqual([null, "analytics"]);
} finally {
rmSync(root, { recursive: true, force: true });
}
}); });
test("insertWorkspaceApp handles nested arrays and comments", () => { test("insertWorkspaceApp handles nested arrays and comments", () => {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/compiler", "name": "@wrnexus/compiler",
"version": "0.2.63", "version": "0.2.64",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/core", "name": "@wrnexus/core",
"version": "0.2.63", "version": "0.2.64",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/csr", "name": "@wrnexus/csr",
"version": "0.2.63", "version": "0.2.64",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/db", "name": "@wrnexus/db",
"version": "0.2.63", "version": "0.2.64",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.2.63", "version": "0.2.64",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/encryption", "name": "@wrnexus/encryption",
"version": "0.2.63", "version": "0.2.64",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/helpers", "name": "@wrnexus/helpers",
"version": "0.2.63", "version": "0.2.64",
"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 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/i18n", "name": "@wrnexus/i18n",
"version": "0.2.63", "version": "0.2.64",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/jwt", "name": "@wrnexus/jwt",
"version": "0.2.63", "version": "0.2.64",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/mobile", "name": "@wrnexus/mobile",
"version": "0.2.63", "version": "0.2.64",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/native", "name": "@wrnexus/native",
"version": "0.2.63", "version": "0.2.64",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/oauth", "name": "@wrnexus/oauth",
"version": "0.2.63", "version": "0.2.64",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/pubsub", "name": "@wrnexus/pubsub",
"version": "0.2.63", "version": "0.2.64",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/queue", "name": "@wrnexus/queue",
"version": "0.2.63", "version": "0.2.64",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/reactive", "name": "@wrnexus/reactive",
"version": "0.2.63", "version": "0.2.64",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/router", "name": "@wrnexus/router",
"version": "0.2.63", "version": "0.2.64",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ssr", "name": "@wrnexus/ssr",
"version": "0.2.63", "version": "0.2.64",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/styles", "name": "@wrnexus/styles",
"version": "0.2.63", "version": "0.2.64",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/test", "name": "@wrnexus/test",
"version": "0.2.63", "version": "0.2.64",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/tracking", "name": "@wrnexus/tracking",
"version": "0.2.63", "version": "0.2.64",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ui", "name": "@wrnexus/ui",
"version": "0.2.63", "version": "0.2.64",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/uploader", "name": "@wrnexus/uploader",
"version": "0.2.63", "version": "0.2.64",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/validation", "name": "@wrnexus/validation",
"version": "0.2.63", "version": "0.2.64",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",