1import { serve, file } from "bun";
2import path from "node:path";
3import { fileURLToPath } from "node:url";
4import homepage from "../public/index.html";
5import cheatsheet from "../public/cheatsheet.html";
6import gallery from "../public/gallery.html";
7import privacy from "../public/privacy.html";
8import {
9 getSkills,
10 getCommands,
11 getCommandSource,
12 getPatterns,
13 handleFileDownload,
14 handleBundleDownload
15} from "./lib/api-handlers.js";
16import { generateSubPages } from "../scripts/build-sub-pages.js";
17
18const __filename = fileURLToPath(import.meta.url);
19const __dirname = path.dirname(__filename);
20const ROOT_DIR = path.resolve(__dirname, "..");
21
22// Pre-generate sub-pages so dev + prod share the same output shape.
23console.log("📝 Generating sub-pages for dev server...");
24const { files: subPageFiles } = await generateSubPages(ROOT_DIR);
25console.log(`✓ Generated ${subPageFiles.length} sub-page(s)`);
26
27// Helper: serve a generated HTML file by absolute path, 404 if missing.
28async function serveGenerated(pagePath) {
29 const f = file(pagePath);
30 if (!(await f.exists())) return new Response("Not Found", { status: 404 });
31 return new Response(f, {
32 headers: {
33 "Content-Type": "text/html;charset=utf-8",
34 "X-Content-Type-Options": "nosniff",
35 "X-Frame-Options": "DENY",
36 },
37 });
38}
39
40const server = serve({
41 port: process.env.PORT || 3000,
42
43 routes: {
44 "/": homepage,
45 "/cheatsheet": cheatsheet,
46 "/gallery": gallery,
47 "/privacy": privacy,
48
49 // Generated sub-pages — served directly from the pre-generated files
50 "/skills": () => serveGenerated(path.join(ROOT_DIR, "public/skills/index.html")),
51 "/skills/:id": (req) => {
52 const id = req.params.id.replace(/[^a-z0-9-]/gi, "");
53 return serveGenerated(path.join(ROOT_DIR, `public/skills/${id}.html`));
54 },
55 "/anti-patterns": () => serveGenerated(path.join(ROOT_DIR, "public/anti-patterns/index.html")),
56 "/visual-mode": () => serveGenerated(path.join(ROOT_DIR, "public/visual-mode/index.html")),
57 "/tutorials": () => serveGenerated(path.join(ROOT_DIR, "public/tutorials/index.html")),
58 "/tutorials/:slug": (req) => {
59 const slug = req.params.slug.replace(/[^a-z0-9-]/gi, "");
60 return serveGenerated(path.join(ROOT_DIR, `public/tutorials/${slug}.html`));
61 },
62
63 // Static assets - all public subdirectories
64 "/assets/*": async (req) => {
65 const url = new URL(req.url);
66 if (url.pathname.includes('..')) return new Response("Bad Request", { status: 400 });
67 const filePath = `./public${url.pathname}`;
68 const assetFile = file(filePath);
69 if (await assetFile.exists()) {
70 return new Response(assetFile, {
71 headers: { "X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY" }
72 });
73 }
74 return new Response("Not Found", { status: 404 });
75 },
76 "/css/*": async (req) => {
77 const url = new URL(req.url);
78 if (url.pathname.includes('..')) return new Response("Bad Request", { status: 400 });
79 const filePath = `./public${url.pathname}`;
80 const assetFile = file(filePath);
81 if (await assetFile.exists()) {
82 return new Response(assetFile, {
83 headers: { "Content-Type": "text/css", "X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY" }
84 });
85 }
86 return new Response("Not Found", { status: 404 });
87 },
88 "/js/*": async (req) => {
89 const url = new URL(req.url);
90 if (url.pathname.includes('..')) return new Response("Bad Request", { status: 400 });
91 // Check public/js/ first, then fall back to built artifacts
92 const headers = { "Content-Type": "application/javascript", "X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY" };
93 const publicFile = file(`./public${url.pathname}`);
94 if (await publicFile.exists()) return new Response(publicFile, { headers });
95 // Browser detector served from impeccable package
96 if (url.pathname === '/js/detect-antipatterns-browser.js') {
97 const pkgFile = file('./src/detect-antipatterns-browser.js');
98 if (await pkgFile.exists()) return new Response(pkgFile, { headers });
99 }
100 return new Response("Not Found", { status: 404 });
101 },
102 // Test fixtures (for browser visual testing)
103 "/fixtures/*": async (req) => {
104 const url = new URL(req.url);
105 if (url.pathname.includes('..')) return new Response("Bad Request", { status: 400 });
106 const filePath = `./tests${url.pathname}`;
107 const assetFile = file(filePath);
108 if (await assetFile.exists()) {
109 const ext = url.pathname.split('.').pop();
110 const types = { html: 'text/html', css: 'text/css', js: 'application/javascript' };
111 return new Response(assetFile, {
112 headers: { "Content-Type": types[ext] || "application/octet-stream", "X-Content-Type-Options": "nosniff" }
113 });
114 }
115 return new Response("Not Found", { status: 404 });
116 },
117 "/antipattern-images/*": async (req) => {
118 const url = new URL(req.url);
119 if (url.pathname.includes('..')) return new Response("Bad Request", { status: 400 });
120 const filePath = `./public${url.pathname}`;
121 const assetFile = file(filePath);
122 if (await assetFile.exists()) {
123 return new Response(assetFile, {
124 headers: { "X-Content-Type-Options": "nosniff" }
125 });
126 }
127 return new Response("Not Found", { status: 404 });
128 },
129 "/antipattern-examples/*": async (req) => {
130 const url = new URL(req.url);
131 if (url.pathname.includes('..')) return new Response("Bad Request", { status: 400 });
132 const filePath = `./public${url.pathname}`;
133 const assetFile = file(filePath);
134 if (await assetFile.exists()) {
135 return new Response(assetFile, {
136 headers: { "Content-Type": "text/html", "X-Content-Type-Options": "nosniff", "X-Frame-Options": "SAMEORIGIN" }
137 });
138 }
139 return new Response("Not Found", { status: 404 });
140 },
141
142 // API: Get all skills
143 "/api/skills": {
144 async GET() {
145 const skills = await getSkills();
146 return Response.json(skills);
147 },
148 },
149
150 // API: Get all commands
151 "/api/commands": {
152 async GET() {
153 const commands = await getCommands();
154 return Response.json(commands);
155 },
156 },
157
158 // API: Get patterns and antipatterns
159 "/api/patterns": {
160 async GET() {
161 const patterns = await getPatterns();
162 return Response.json(patterns);
163 },
164 },
165
166 // API: Get command source content
167 "/api/command-source/:id": async (req) => {
168 const { id } = req.params;
169 const result = await getCommandSource(id);
170 if (result && result.error) {
171 return Response.json({ error: result.error }, { status: result.status });
172 }
173 if (!result) {
174 return Response.json({ error: "Command not found" }, { status: 404 });
175 }
176 return Response.json({ content: result });
177 },
178
179 // API: Download individual file
180 "/api/download/:type/:provider/:id": async (req) => {
181 const { type, provider, id } = req.params;
182 return handleFileDownload(type, provider, id);
183 },
184
185 // API: Download provider bundle ZIP
186 "/api/download/bundle/:provider": async (req) => {
187 const { provider } = req.params;
188 return handleBundleDownload(provider);
189 },
190 },
191
192 // Serve root-level static files (og-image.png, favicon, robots.txt, etc.)
193 fetch(req) {
194 const url = new URL(req.url);
195 if (url.pathname.includes('..')) {
196 return new Response("Bad Request", { status: 400 });
197 }
198 const filePath = `./public${url.pathname}`;
199 const staticFile = file(filePath);
200 if (staticFile.size > 0) {
201 return new Response(staticFile);
202 }
203 return new Response("Not Found", { status: 404 });
204 },
205
206 development: process.env.NODE_ENV !== "production",
207});
208
209console.log(`🎨 impeccable.style running at ${server.url}`);
210