Merge pull request #842 from useautumn/charlie/multi-local-hosting

Charlie/multi local hosting
This commit is contained in:
John Yeo
2026-02-26 20:16:31 +00:00
committed by GitHub
7 changed files with 227 additions and 36 deletions

View File

@@ -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",

View File

@@ -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
View 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);

View File

@@ -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"],

View File

@@ -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;

View 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;
};

View 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();
});
});
});