first commit

This commit is contained in:
2026-07-12 15:55:18 +05:30
commit ee98026cc5
404 changed files with 44522 additions and 0 deletions
+218
View File
@@ -0,0 +1,218 @@
/**
* Monorepo support:
* - `wrnexus workspace <name>` scaffolds a multi-app workspace (apps/* + shared packages/*)
* - `wrnexus gateway [--port]` serves every app behind one port, routed by domain
*
* A workspace holds several WrNexus apps under `apps/*` and shared libraries under
* `packages/*`. Apps share code by importing a workspace package (e.g. `@app/shared`),
* share databases, and talk at runtime via @wrnexus/pubsub (Redis driver for
* cross-process). `wrnexus.workspace.ts` maps each app to the domains it serves.
*/
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { scaffoldApp } from "./create.ts";
import type { GatewayAuth, GatewaySecurity } from "@wrnexus/dev-server";
export interface WorkspaceApp {
name: string;
dir: string;
domains: string[];
port?: number;
/** Per-app access control enforced at the gateway (basic auth, IP allowlist, forward-auth). */
auth?: GatewayAuth;
}
export interface WorkspaceConfig {
apps: WorkspaceApp[];
/** Gateway-wide security (trusted hosts, rate limit, headers, access log). */
security?: GatewaySecurity;
}
const files = (name: string): Record<string, string> => ({
"package.json": `{
"name": "${name}",
"private": true,
"type": "module",
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"dev": "wrnexus gateway",
"gateway": "wrnexus gateway"
},
"devDependencies": {
"@wrnexus/cli": "^0.2.0"
}
}
`,
"wrnexus.workspace.ts": `import type { WorkspaceConfig } from "@wrnexus/cli/workspace";
// Map each app to the domains it serves. \`wrnexus gateway\` runs them all behind
// one port and routes by Host header (add these hosts to your /etc/hosts).
const config: WorkspaceConfig = {
// Gateway-wide security (all optional):
security: {
trustedHostsOnly: true, // reject requests for unknown domains
rateLimit: { max: 300, windowMs: 60_000 }, // per client IP
headers: true, // baseline security headers at the edge
accessLog: true, // log host → app, method, path, status
},
apps: [
{ name: "web", dir: "apps/web", domains: ["localhost", "web.localhost"] },
{
name: "admin",
dir: "apps/admin",
domains: ["admin.localhost"],
// Lock the admin app down at the edge (pick one):
auth: { basic: { user: "admin", pass: "change-me" } },
// auth: { allowIps: ["127.0.0.1", "::1"] },
// auth: { forward: { url: "http://localhost:4001/api/verify" } }, // SSO
},
],
};
export default config;
`,
".gitignore": `node_modules/
dist/
.wrnexus/
*.log
*.db
`,
"packages/shared/package.json": `{
"name": "@app/shared",
"version": "0.0.0",
"private": true,
"type": "module",
"main": "src/index.ts",
"exports": { ".": "./src/index.ts" },
"dependencies": {
"@wrnexus/pubsub": "^0.2.0"
}
}
`,
"packages/shared/src/index.ts": `/**
* Shared code for every app in this workspace. Import it anywhere: \`@app/shared\`.
* The cross-app event bus uses Redis so messages reach every app process/domain.
*/
import { createPubSub } from "@wrnexus/pubsub";
import { redisDriver } from "@wrnexus/pubsub/redis";
// One bus per process, backed by Redis (set REDIS_URL, defaults to localhost:6379).
export const bus = createPubSub(redisDriver(process.env.REDIS_URL));
// Shared domain types can live here and be imported by every app.
export interface Tenant {
id: string;
name: string;
}
`,
"README.md": `# ${name}
A WrNexus **workspace** — multiple apps, one gateway, interconnected.
\`\`\`
${name}/
wrnexus.workspace.ts # apps ↔ domains map (used by \`wrnexus gateway\`)
apps/
web/ # a WrNexus app → localhost, web.localhost
admin/ # a WrNexus app → admin.localhost
packages/
shared/ # @app/shared — shared code + cross-app pubsub bus
\`\`\`
## Run everything (one port, routed by domain)
\`\`\`bash
bun install
bun run dev # = wrnexus gateway → http://localhost:3000
\`\`\`
Add the hosts to your machine (e.g. /etc/hosts):
\`\`\`
127.0.0.1 web.localhost admin.localhost
\`\`\`
## Interconnect
- **Shared code:** import \`@app/shared\` in any app.
- **Runtime messaging:** \`import { bus } from "@app/shared"\` then
\`bus.publish("tenant:created", {...})\` in one app and
\`bus.subscribe("tenant:*", fn)\` in another (needs Redis).
- **Databases:** point apps at the same \`db\`/\`databases\` in their config.
## Add another app
\`\`\`bash
wrnexus create apps/reports
# then add it to wrnexus.workspace.ts with its domains
\`\`\`
`,
});
/** Scaffold a monorepo workspace with two starter apps + a shared package. */
export function createWorkspace(name: string): void {
if (!name) {
console.error("Usage: wrnexus workspace <name>");
process.exit(1);
}
const root = resolve(process.cwd(), name);
if (existsSync(root)) {
console.error(`Refusing to overwrite existing directory: ${root}`);
process.exit(1);
}
for (const [rel, contents] of Object.entries(files(name))) {
const target = join(root, rel);
mkdirSync(dirname(target), { recursive: true });
writeFileSync(target, contents, "utf8");
}
// Two starter apps under apps/.
scaffoldApp(join(root, "apps", "web"), "web");
scaffoldApp(join(root, "apps", "admin"), "admin");
console.log(`✓ Created workspace ${name}`);
console.log(`\nNext steps:`);
console.log(` cd ${name}`);
console.log(` bun install`);
console.log(` bun run dev # wrnexus gateway (web + admin, routed by domain)`);
}
/** Load `wrnexus.workspace.ts` from a directory. */
export async function loadWorkspaceConfig(root: string): Promise<WorkspaceConfig> {
for (const file of ["wrnexus.workspace.ts", "wrnexus.workspace.js", "wrnexus.workspace.mjs"]) {
const path = join(root, file);
if (existsSync(path)) {
const mod = (await import(pathToFileURL(path).href)) as { default?: WorkspaceConfig };
if (!mod.default?.apps?.length)
throw new Error(`${file} must default-export { apps: [...] }`);
return mod.default;
}
}
throw new Error("No wrnexus.workspace.ts found. Run `wrnexus workspace <name>` to scaffold one.");
}
/** Run the multi-app gateway from `wrnexus.workspace.ts`. */
export async function runGateway(root: string, args: string[]): Promise<void> {
const config = await loadWorkspaceConfig(resolve(root));
const portArg = args.find((a) => a.startsWith("--port="));
const hostArg = args.find((a) => a.startsWith("--host="));
const port = portArg ? Number(portArg.split("=")[1]) : 3000;
const hostname = hostArg?.split("=")[1] || "::";
const mode = args.includes("--prod") ? "production" : "development";
const { startGateway } = await import("@wrnexus/dev-server");
await startGateway({
port,
hostname,
mode,
security: config.security,
apps: config.apps.map((a) => ({
name: a.name,
dir: resolve(root, a.dir),
domains: a.domains,
port: a.port,
auth: a.auth,
})),
});
}