first commit
This commit is contained in:
@@ -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,
|
||||
})),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user