Merge pull request #842 from useautumn/charlie/multi-local-hosting
Charlie/multi local hosting
This commit is contained in:
@@ -59,6 +59,7 @@
|
||||
"vite:build": "bun -F @autumn/vite build:bun",
|
||||
"t": "infisical run --env=dev -- bun scripts/testScripts/testDispatcher.ts",
|
||||
"d": "lsof -ti:8080 -ti:3000 | xargs kill -9 2>/dev/null || true; ENV_FILE=.env infisical run --env=dev -- bun scripts/dev.ts",
|
||||
"dx": "bun scripts/dx.ts",
|
||||
"d:test": "lsof -ti:8080 -ti:3000 | xargs kill -9 2>/dev/null || true; ENV_FILE=.env infisical run --env=test -- bun scripts/dev.ts",
|
||||
"p": "ENV_FILE=.env.prod infisical run --env=prod -- bun scripts/dev.ts",
|
||||
"setup": "node scripts/setup/setup.js",
|
||||
|
||||
@@ -2,9 +2,17 @@ import { existsSync, readFileSync, rmSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const VITE_PORT = 3000;
|
||||
const SERVER_PORT = 8080;
|
||||
const CHECKOUT_PORT = 3001;
|
||||
const worktreeIdx = process.argv.indexOf("--worktree");
|
||||
const worktreeNum =
|
||||
worktreeIdx !== -1 && process.argv[worktreeIdx + 1]
|
||||
? Number.parseInt(process.argv[worktreeIdx + 1], 10)
|
||||
: 1;
|
||||
const portOffset = (worktreeNum - 1) * 100;
|
||||
|
||||
const VITE_PORT = 3000 + portOffset;
|
||||
const SERVER_PORT = 8080 + portOffset;
|
||||
const CHECKOUT_PORT = 3001 + portOffset;
|
||||
const skipWorkers = worktreeNum > 1;
|
||||
|
||||
/**
|
||||
* Read environment variable from .env file
|
||||
@@ -71,7 +79,15 @@ async function startDev() {
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Starting development servers...\n");
|
||||
if (worktreeNum > 1) {
|
||||
console.log(`Starting worktree ${worktreeNum} (no workers)...\n`);
|
||||
} else {
|
||||
console.log("Starting development servers...\n");
|
||||
}
|
||||
|
||||
console.log(` vite: http://localhost:${VITE_PORT}`);
|
||||
console.log(` server: http://localhost:${SERVER_PORT}`);
|
||||
console.log(` checkout: http://localhost:${CHECKOUT_PORT}\n`);
|
||||
|
||||
// Use cmd on Windows, sh on Unix
|
||||
const isWindows = process.platform === "win32";
|
||||
@@ -94,21 +110,40 @@ async function startDev() {
|
||||
`bunx concurrently -n server,workers -c green,yellow "cd server && SERVER_PORT=${SERVER_PORT} bun start" "cd server && bun workers"`,
|
||||
];
|
||||
}
|
||||
} else if (isWindows) {
|
||||
const serverCmd = `cd server && set SERVER_PORT=${SERVER_PORT} && bun dev`;
|
||||
const workersCmd = `cd server && bun workers:dev`;
|
||||
const viteCmd = `cd vite && set VITE_PORT=${VITE_PORT} && bun dev`;
|
||||
const checkoutCmd = `cd apps/checkout && set VITE_PORT=${CHECKOUT_PORT} && bun dev`;
|
||||
shellArgs = [
|
||||
"cmd",
|
||||
"/c",
|
||||
`bunx concurrently -n server,workers,vite,checkout -c green,yellow,blue,magenta "${serverCmd}" "${workersCmd}" "${viteCmd}" "${checkoutCmd}"`,
|
||||
];
|
||||
} else {
|
||||
const names = ["server"];
|
||||
const colors = ["green"];
|
||||
const cmds = [
|
||||
isWindows
|
||||
? `"cd server && set SERVER_PORT=${SERVER_PORT} && bun dev"`
|
||||
: `"cd server && SERVER_PORT=${SERVER_PORT} bun dev"`,
|
||||
];
|
||||
|
||||
if (!skipWorkers) {
|
||||
names.push("workers");
|
||||
colors.push("yellow");
|
||||
cmds.push(
|
||||
isWindows
|
||||
? `"cd server && bun workers:dev"`
|
||||
: `"cd server && bun workers:dev"`,
|
||||
);
|
||||
}
|
||||
|
||||
names.push("vite", "checkout");
|
||||
colors.push("blue", "magenta");
|
||||
cmds.push(
|
||||
isWindows
|
||||
? `"cd vite && set VITE_PORT=${VITE_PORT} && bun dev"`
|
||||
: `"cd vite && VITE_PORT=${VITE_PORT} bun dev"`,
|
||||
isWindows
|
||||
? `"cd apps/checkout && set VITE_PORT=${CHECKOUT_PORT} && bun dev"`
|
||||
: `"cd apps/checkout && VITE_PORT=${CHECKOUT_PORT} bun dev"`,
|
||||
);
|
||||
|
||||
shellArgs = [
|
||||
"sh",
|
||||
"-c",
|
||||
`bunx concurrently -n server,workers,vite,checkout -c green,yellow,blue,magenta "cd server && SERVER_PORT=${SERVER_PORT} bun dev" "cd server && bun workers:dev" "cd vite && VITE_PORT=${VITE_PORT} bun dev" "cd apps/checkout && VITE_PORT=${CHECKOUT_PORT} bun dev"`,
|
||||
isWindows ? "cmd" : "sh",
|
||||
isWindows ? "/c" : "-c",
|
||||
`bunx concurrently -n ${names.join(",")} -c ${colors.join(",")} ${cmds.join(" ")}`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -119,6 +154,12 @@ async function startDev() {
|
||||
VITE_PORT: VITE_PORT.toString(),
|
||||
SERVER_PORT: SERVER_PORT.toString(),
|
||||
CHECKOUT_PORT: CHECKOUT_PORT.toString(),
|
||||
...(worktreeNum > 1 && {
|
||||
CLIENT_URL: `http://localhost:${VITE_PORT}`,
|
||||
BETTER_AUTH_URL: `http://localhost:${SERVER_PORT}`,
|
||||
VITE_BACKEND_URL: `http://localhost:${SERVER_PORT}`,
|
||||
VITE_FRONTEND_URL: `http://localhost:${VITE_PORT}`,
|
||||
}),
|
||||
},
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
|
||||
66
scripts/dx.ts
Normal file
66
scripts/dx.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { createConnection } from "node:net";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
function isPortInUse({ port }: { port: number }): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const socket = createConnection({ port, host: "127.0.0.1" });
|
||||
socket.on("connect", () => {
|
||||
socket.destroy();
|
||||
resolve(true);
|
||||
});
|
||||
socket.on("error", () => {
|
||||
socket.destroy();
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function findFreeWorktree(): Promise<number> {
|
||||
for (let n = 2; n <= 10; n++) {
|
||||
const serverPort = 8080 + (n - 1) * 100;
|
||||
if (!(await isPortInUse({ port: serverPort }))) return n;
|
||||
}
|
||||
console.error("No free worktree slots (2-10). All server ports in use.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Allow explicit override: `bun dx 3`, otherwise auto-detect
|
||||
const explicitArg = Number.parseInt(process.argv[2] || "", 10);
|
||||
const worktreeNum =
|
||||
!Number.isNaN(explicitArg) && explicitArg >= 2
|
||||
? explicitArg
|
||||
: await findFreeWorktree();
|
||||
|
||||
const offset = (worktreeNum - 1) * 100;
|
||||
const vitePort = 3000 + offset;
|
||||
const serverPort = 8080 + offset;
|
||||
const checkoutPort = 3001 + offset;
|
||||
|
||||
console.log(
|
||||
`Worktree ${worktreeNum} -> vite:${vitePort}, server:${serverPort}, checkout:${checkoutPort}\n`,
|
||||
);
|
||||
|
||||
const portArgs = [
|
||||
`-ti:${vitePort}`,
|
||||
`-ti:${serverPort}`,
|
||||
`-ti:${checkoutPort}`,
|
||||
].join(" ");
|
||||
const killCmd = `lsof ${portArgs} | xargs kill -9 2>/dev/null || true`;
|
||||
const devCmd = `ENV_FILE=.env infisical run --env=dev -- bun scripts/dev.ts --worktree ${worktreeNum}`;
|
||||
|
||||
const rootDir = dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = join(rootDir, "..");
|
||||
|
||||
const proc = Bun.spawn(["sh", "-c", `${killCmd}; ${devCmd}`], {
|
||||
cwd: projectRoot,
|
||||
env: process.env,
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => proc.kill("SIGINT"));
|
||||
process.on("SIGTERM", () => proc.kill("SIGTERM"));
|
||||
|
||||
await proc.exited;
|
||||
process.exit(proc.exitCode ?? 0);
|
||||
@@ -22,24 +22,7 @@ import { apiRouter } from "./routers/apiRouter.js";
|
||||
import { internalRouter } from "./routers/internalRouter.js";
|
||||
import { publicRouter } from "./routers/publicRouter.js";
|
||||
import { auth } from "./utils/auth.js";
|
||||
|
||||
const ALLOWED_ORIGINS = [
|
||||
"http://localhost:3000",
|
||||
"http://localhost:3001",
|
||||
"http://localhost:3002",
|
||||
"http://localhost:3003",
|
||||
"http://localhost:3004",
|
||||
"http://localhost:3005",
|
||||
"http://localhost:3006",
|
||||
"http://localhost:3007",
|
||||
"http://localhost:5173",
|
||||
"http://localhost:5174",
|
||||
"https://app.useautumn.com",
|
||||
"https://staging.useautumn.com",
|
||||
"https://dev.useautumn.com",
|
||||
"https://api.staging.useautumn.com",
|
||||
"https://localhost:8080",
|
||||
];
|
||||
import { isAllowedOrigin } from "./utils/corsOrigins.js";
|
||||
|
||||
const ALLOWED_HEADERS = [
|
||||
"app_env",
|
||||
@@ -72,7 +55,7 @@ export const createHonoApp = () => {
|
||||
app.use(
|
||||
"*",
|
||||
cors({
|
||||
origin: ALLOWED_ORIGINS,
|
||||
origin: isAllowedOrigin,
|
||||
allowHeaders: ALLOWED_HEADERS,
|
||||
allowMethods: ["POST", "GET", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
||||
exposeHeaders: ["Content-Length"],
|
||||
|
||||
@@ -75,6 +75,11 @@ export const auth = betterAuth({
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
origins.push(`http://localhost:${3000 + i}`);
|
||||
}
|
||||
|
||||
// Support multi-worktree dev with offset ports (e.g. localhost:3100)
|
||||
if (process.env.CLIENT_URL) {
|
||||
origins.push(process.env.CLIENT_URL);
|
||||
}
|
||||
}
|
||||
|
||||
return origins;
|
||||
|
||||
29
server/src/utils/corsOrigins.ts
Normal file
29
server/src/utils/corsOrigins.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export const ALLOWED_ORIGINS = [
|
||||
"http://localhost:3000",
|
||||
"http://localhost:3001",
|
||||
"http://localhost:3002",
|
||||
"http://localhost:3003",
|
||||
"http://localhost:3004",
|
||||
"http://localhost:3005",
|
||||
"http://localhost:3006",
|
||||
"http://localhost:3007",
|
||||
"http://localhost:5173",
|
||||
"http://localhost:5174",
|
||||
"https://app.useautumn.com",
|
||||
"https://staging.useautumn.com",
|
||||
"https://dev.useautumn.com",
|
||||
"https://api.staging.useautumn.com",
|
||||
"https://localhost:8080",
|
||||
];
|
||||
|
||||
/** Allow any localhost origin in dev for multi-worktree support */
|
||||
export const isAllowedOrigin = (origin: string): string | undefined => {
|
||||
if (ALLOWED_ORIGINS.includes(origin)) return origin;
|
||||
if (
|
||||
process.env.NODE_ENV !== "production" &&
|
||||
/^https?:\/\/localhost:\d+$/.test(origin)
|
||||
) {
|
||||
return origin;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
66
server/tests/unit/corsOrigins.test.ts
Normal file
66
server/tests/unit/corsOrigins.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import { ALLOWED_ORIGINS, isAllowedOrigin } from "@/utils/corsOrigins.js";
|
||||
|
||||
describe("isAllowedOrigin", () => {
|
||||
const originalNodeEnv = process.env.NODE_ENV;
|
||||
|
||||
afterEach(() => {
|
||||
process.env.NODE_ENV = originalNodeEnv;
|
||||
});
|
||||
|
||||
describe("production", () => {
|
||||
test("allows hardcoded production origins", () => {
|
||||
process.env.NODE_ENV = "production";
|
||||
for (const origin of ALLOWED_ORIGINS) {
|
||||
expect(isAllowedOrigin(origin)).toBe(origin);
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects arbitrary localhost ports", () => {
|
||||
process.env.NODE_ENV = "production";
|
||||
expect(isAllowedOrigin("http://localhost:3100")).toBeUndefined();
|
||||
expect(isAllowedOrigin("http://localhost:8180")).toBeUndefined();
|
||||
expect(isAllowedOrigin("http://localhost:9999")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("rejects external origins", () => {
|
||||
process.env.NODE_ENV = "production";
|
||||
expect(isAllowedOrigin("https://evil.com")).toBeUndefined();
|
||||
expect(isAllowedOrigin("https://fake.useautumn.com")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("non-production", () => {
|
||||
test("allows hardcoded origins", () => {
|
||||
process.env.NODE_ENV = "development";
|
||||
for (const origin of ALLOWED_ORIGINS) {
|
||||
expect(isAllowedOrigin(origin)).toBe(origin);
|
||||
}
|
||||
});
|
||||
|
||||
test("allows any localhost port (worktree offsets)", () => {
|
||||
process.env.NODE_ENV = "development";
|
||||
expect(isAllowedOrigin("http://localhost:3100")).toBe(
|
||||
"http://localhost:3100",
|
||||
);
|
||||
expect(isAllowedOrigin("http://localhost:8180")).toBe(
|
||||
"http://localhost:8180",
|
||||
);
|
||||
expect(isAllowedOrigin("http://localhost:3200")).toBe(
|
||||
"http://localhost:3200",
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects external origins", () => {
|
||||
process.env.NODE_ENV = "development";
|
||||
expect(isAllowedOrigin("https://evil.com")).toBeUndefined();
|
||||
expect(isAllowedOrigin("http://evil.com:3000")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("rejects localhost with path or query", () => {
|
||||
process.env.NODE_ENV = "development";
|
||||
expect(isAllowedOrigin("http://localhost:3000/evil")).toBeUndefined();
|
||||
expect(isAllowedOrigin("http://localhost:3000?x=1")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user