The Appview for the kipclip.com atproto bookmarking service
at main 126 lines 4.0 kB view raw
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}