Skip to content

Commit eccebff

Browse files
authored
Companion build (calcom#25648)
1 parent 0a36dd7 commit eccebff

File tree

2 files changed

+94
-0
lines changed

2 files changed

+94
-0
lines changed

companion/api/server.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { createReadStream, existsSync } from "fs";
2+
import { promises as fs } from "fs";
3+
import { IncomingMessage, ServerResponse } from "http";
4+
import { extname, join, normalize } from "path";
5+
6+
const distDir = join(process.cwd(), "dist");
7+
8+
const mimeTypes: Record<string, string> = {
9+
".html": "text/html; charset=utf-8",
10+
".js": "application/javascript; charset=utf-8",
11+
".css": "text/css; charset=utf-8",
12+
".json": "application/json; charset=utf-8",
13+
".png": "image/png",
14+
".jpg": "image/jpeg",
15+
".jpeg": "image/jpeg",
16+
".svg": "image/svg+xml",
17+
".ico": "image/x-icon",
18+
".woff": "font/woff",
19+
".woff2": "font/woff2",
20+
".ttf": "font/ttf",
21+
".map": "application/json; charset=utf-8",
22+
};
23+
24+
const getSafePath = (urlPath: string): string => {
25+
const decodedPath = decodeURIComponent(urlPath.split("?")[0]);
26+
const normalized = normalize(decodedPath).replace(/^(\.\.(\/|\\|$))+/, "");
27+
return normalized.startsWith("/") ? normalized : `/${normalized}`;
28+
};
29+
30+
const resolveFilePath = async (safePath: string): Promise<string> => {
31+
let candidate = join(distDir, safePath);
32+
try {
33+
const stats = await fs.stat(candidate);
34+
if (stats.isDirectory()) {
35+
candidate = join(candidate, "index.html");
36+
}
37+
return candidate;
38+
} catch {
39+
const fallback = join(distDir, "index.html");
40+
if (existsSync(fallback)) {
41+
return fallback;
42+
}
43+
throw new Error("Dist folder missing. Run `npm run build` before deploying.");
44+
}
45+
};
46+
47+
const sendFile = (res: ServerResponse, filePath: string) => {
48+
const ext = extname(filePath).toLowerCase();
49+
const contentType = mimeTypes[ext] || "application/octet-stream";
50+
res.setHeader("Content-Type", contentType);
51+
res.setHeader(
52+
"Cache-Control",
53+
ext === ".html" ? "no-cache, no-store, must-revalidate" : "public, max-age=31536000, immutable"
54+
);
55+
56+
const stream = createReadStream(filePath);
57+
stream.on("error", (error) => {
58+
console.error("Failed to read file:", error);
59+
res.statusCode = 500;
60+
res.end("Internal Server Error");
61+
});
62+
stream.pipe(res);
63+
};
64+
65+
export default async function handler(req: IncomingMessage, res: ServerResponse) {
66+
if (req.method && !["GET", "HEAD"].includes(req.method)) {
67+
res.statusCode = 405;
68+
res.setHeader("Allow", "GET, HEAD");
69+
res.end("Method Not Allowed");
70+
return;
71+
}
72+
73+
try {
74+
const urlPath = req.url ?? "/";
75+
const safePath = getSafePath(urlPath);
76+
const filePath = await resolveFilePath(safePath);
77+
sendFile(res, filePath);
78+
} catch (error) {
79+
console.error("Server render error:", error);
80+
res.statusCode = 500;
81+
res.end("Internal Server Error");
82+
}
83+
}

companion/vercel.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"routes": [
3+
{
4+
"handle": "filesystem"
5+
},
6+
{
7+
"src": "/.*",
8+
"dest": "/index.html"
9+
}
10+
]
11+
}

0 commit comments

Comments
 (0)