* refactor(companion): extract AvailabilityDetailScreen with AppPressable migration Apply Phases 2, 3, 4 to availability-detail.tsx: - Phase 2: Extract screen logic (~995 lines) to components/screens/AvailabilityDetailScreen.tsx - Route file is now a minimal ~35 line wrapper with Stack.Screen config - Phase 3: ScrollView+map pattern kept (7 days is a fixed small list, FlatList not needed) - Phase 4: Migrate all TouchableOpacity to AppPressable (63 instances) Follows the same pattern as EventTypeDetailScreen extraction. * fix(companion): add GlassView header with Save button to AvailabilityDetailScreen (#26107) * refactor(companion): extract BookingDetailScreen with AppPressable migration (#26108) * refactor(companion): extract AvailabilityListScreen with AppPressable migration (#26109) * fix(companion): add GlassView header with Save button to AvailabilityDetailScreen * refactor(companion): extract BookingDetailScreen with AppPressable migration * refactor(companion): extract AvailabilityListScreen with AppPressable migration * refactor(companion): Standardized error handling with dev-only debug logs (#26110) * address cubics comments * address cubics comments * addressed typecheck erros * removed localhost from wxt.config for chrome extension * declare FullScreenModal FullScreenModalProps --------- Co-authored-by: Peer Richelsen <peer@cal.com> * add loggers for dev env --------- Co-authored-by: Peer Richelsen <peer@cal.com> --------- Co-authored-by: Peer Richelsen <peer@cal.com>
94 lines
3.1 KiB
TypeScript
94 lines
3.1 KiB
TypeScript
import { createReadStream, existsSync } from "fs";
|
|
import { promises as fs } from "fs";
|
|
import { IncomingMessage, ServerResponse } from "http";
|
|
import { extname, join, normalize } from "path";
|
|
|
|
const distDir = join(process.cwd(), "dist");
|
|
|
|
const mimeTypes: Record<string, string> = {
|
|
".html": "text/html; charset=utf-8",
|
|
".js": "application/javascript; charset=utf-8",
|
|
".css": "text/css; charset=utf-8",
|
|
".json": "application/json; charset=utf-8",
|
|
".png": "image/png",
|
|
".jpg": "image/jpeg",
|
|
".jpeg": "image/jpeg",
|
|
".svg": "image/svg+xml",
|
|
".ico": "image/x-icon",
|
|
".woff": "font/woff",
|
|
".woff2": "font/woff2",
|
|
".ttf": "font/ttf",
|
|
".map": "application/json; charset=utf-8",
|
|
};
|
|
|
|
const getSafePath = (urlPath: string): string => {
|
|
const decodedPath = decodeURIComponent(urlPath.split("?")[0]);
|
|
const normalized = normalize(decodedPath).replace(/^(\.\.(\/|\\|$))+/, "");
|
|
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
};
|
|
|
|
const resolveFilePath = async (safePath: string): Promise<string> => {
|
|
let candidate = join(distDir, safePath);
|
|
try {
|
|
const stats = await fs.stat(candidate);
|
|
if (stats.isDirectory()) {
|
|
candidate = join(candidate, "index.html");
|
|
}
|
|
return candidate;
|
|
} catch {
|
|
const fallback = join(distDir, "index.html");
|
|
if (existsSync(fallback)) {
|
|
return fallback;
|
|
}
|
|
throw new Error("Dist folder missing. Run `npm run build` before deploying.");
|
|
}
|
|
};
|
|
|
|
const sendFile = (res: ServerResponse, filePath: string) => {
|
|
const ext = extname(filePath).toLowerCase();
|
|
const contentType = mimeTypes[ext] || "application/octet-stream";
|
|
res.setHeader("Content-Type", contentType);
|
|
res.setHeader(
|
|
"Cache-Control",
|
|
ext === ".html" ? "no-cache, no-store, must-revalidate" : "public, max-age=31536000, immutable"
|
|
);
|
|
|
|
const stream = createReadStream(filePath);
|
|
stream.on("error", (error) => {
|
|
console.error("Failed to read file");
|
|
if (process.env.NODE_ENV !== "production") {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
const stack = error instanceof Error ? error.stack : undefined;
|
|
console.debug("[companion/api] Failed to read file", { message, stack });
|
|
}
|
|
res.statusCode = 500;
|
|
res.end("Internal Server Error");
|
|
});
|
|
stream.pipe(res);
|
|
};
|
|
|
|
export default async function handler(req: IncomingMessage, res: ServerResponse) {
|
|
if (req.method && !["GET", "HEAD"].includes(req.method)) {
|
|
res.statusCode = 405;
|
|
res.setHeader("Allow", "GET, HEAD");
|
|
res.end("Method Not Allowed");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const urlPath = req.url ?? "/";
|
|
const safePath = getSafePath(urlPath);
|
|
const filePath = await resolveFilePath(safePath);
|
|
sendFile(res, filePath);
|
|
} catch (error) {
|
|
console.error("Server render error");
|
|
if (process.env.NODE_ENV !== "production") {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
const stack = error instanceof Error ? error.stack : undefined;
|
|
console.debug("[companion/api] Server render error", { message, stack });
|
|
}
|
|
res.statusCode = 500;
|
|
res.end("Internal Server Error");
|
|
}
|
|
}
|