The Appview for the kipclip.com atproto bookmarking service
1/**
2 * File server utilities for serving static files.
3 * Frontend is now pre-built, so no runtime transpilation needed.
4 * Includes cache headers for optimal performance.
5 */
6
7import { dirname, fromFileUrl, join } from "@std/path";
8import { contentType } from "@std/media-types";
9
10/** Cache durations in seconds */
11const CACHE_IMMUTABLE = 31536000; // 1 year
12const CACHE_MEDIUM = 3600; // 1 hour
13const CACHE_SHORT = 60; // 1 minute
14
15/**
16 * Determine cache control header based on file path.
17 * Content-hashed files get immutable caching, others get shorter TTLs.
18 */
19function getCacheControl(path: string): string {
20 const filename = path.split("/").pop() || "";
21
22 // Content-hashed bundle files (bundle.xxxxxxxx.js) - immutable
23 if (/^bundle\.[a-f0-9]{8}\.js(\.map)?$/.test(filename)) {
24 return `public, max-age=${CACHE_IMMUTABLE}, immutable`;
25 }
26
27 // Other JS/CSS files - medium cache with revalidation
28 if (filename.endsWith(".js") || filename.endsWith(".css")) {
29 return `public, max-age=${CACHE_MEDIUM}`;
30 }
31
32 // HTML files - short cache, must revalidate
33 if (filename.endsWith(".html")) {
34 return `public, max-age=${CACHE_SHORT}, must-revalidate`;
35 }
36
37 // Default for other static assets
38 return `public, max-age=${CACHE_MEDIUM}`;
39}
40
41/**
42 * Resolve a path relative to the project root.
43 * Works on both local development (file://) and Deno Deploy (app://).
44 */
45function resolveProjectPath(path: string, baseUrl: string): string {
46 // Remove leading slash from path
47 const cleanPath = path.startsWith("/") ? path.slice(1) : path;
48
49 // Get the directory of the base URL (main.ts location)
50 const baseUrlObj = new URL(baseUrl);
51
52 if (baseUrlObj.protocol === "file:") {
53 // Local development - resolve from filesystem
54 const baseDir = dirname(fromFileUrl(baseUrl));
55 return join(baseDir, cleanPath);
56 } else {
57 // Deno Deploy (app://) - resolve relative to base
58 // import.meta.url is like "app:///main.ts", so dirname gives "app:///"
59 const basePath = dirname(baseUrlObj.pathname);
60 return join(basePath, cleanPath);
61 }
62}
63
64/**
65 * Read a file from the project relative to the given base URL.
66 *
67 * @param path - Path to file (e.g., "/frontend/index.html")
68 * @param baseUrl - import.meta.url of the calling module
69 * @returns File contents as string
70 */
71export async function readFile(path: string, baseUrl: string): Promise<string> {
72 const filePath = resolveProjectPath(path, baseUrl);
73
74 try {
75 const content = await Deno.readTextFile(filePath);
76 return content;
77 } catch (error) {
78 console.error(`Failed to read file: ${filePath}`, error);
79 throw error;
80 }
81}
82
83/**
84 * Serve a file from the project with appropriate content-type and cache headers.
85 *
86 * @param path - Path to file (e.g., "/frontend/style.css")
87 * @param baseUrl - import.meta.url of the calling module
88 * @returns Response with file contents and caching headers
89 */
90export async function serveFile(
91 path: string,
92 baseUrl: string,
93): Promise<Response> {
94 try {
95 const ext = path.split(".").pop() || "";
96 const content = await readFile(path, baseUrl);
97 const mimeType = contentType(ext) || "application/octet-stream";
98 const cacheControl = getCacheControl(path);
99
100 return new Response(content, {
101 headers: {
102 "Content-Type": mimeType,
103 "Cache-Control": cacheControl,
104 },
105 });
106 } catch {
107 return new Response("File not found", { status: 404 });
108 }
109}
110
111/**
112 * Read and parse the bundle manifest to get the current hashed bundle filename.
113 *
114 * @param baseUrl - import.meta.url of the calling module
115 * @returns The current bundle filename (e.g., "bundle.abc12345.js")
116 */
117export async function getBundleFileName(baseUrl: string): Promise<string> {
118 try {
119 const manifestContent = await readFile("/static/manifest.json", baseUrl);
120 const manifest = JSON.parse(manifestContent);
121 return manifest["bundle.js"] || "bundle.js";
122 } catch {
123 // Fallback if manifest doesn't exist (development)
124 return "bundle.js";
125 }
126}