From bf616f679031b86ae21b1ff08126e68ef5b923ca Mon Sep 17 00:00:00 2001 From: asepharyana Date: Sat, 1 Aug 2026 13:15:10 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20root=20page=20500=20=E2=80=94=20resolve?= =?UTF-8?q?=20home.html=20for=20dev=20&=20prod=20layouts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit handleHome looked up `${import.meta.dir}/home.html` which exists in neither layout: dev (src/interfaces/http/controllers/) nor prod bundle ($out/share/teleuploader/dist/ — flake copies home.html beside dist/). Add resolveHomeHtml(): walk up from import.meta.dir (bounded) to find home.html. Works for dev (src/home.html, 4 levels up) and prod (../home.html, 1 level up). Fails fast with a clear error instead of a bare ENOENT 500. Tests cover both layouts + not-found fallback. --- .../http/controllers/home-controller.ts | 51 +++++++++++++++- test/home.test.ts | 60 +++++++++++++++++++ 2 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 test/home.test.ts diff --git a/src/interfaces/http/controllers/home-controller.ts b/src/interfaces/http/controllers/home-controller.ts index 75b7f69..1fdafad 100644 --- a/src/interfaces/http/controllers/home-controller.ts +++ b/src/interfaces/http/controllers/home-controller.ts @@ -1,15 +1,60 @@ +import { existsSync } from 'node:fs'; +import { dirname, join } from 'node:path'; import type { BunFile } from 'bun'; +/** + * Maximum number of parent directories to walk up when locating home.html. + * Deep enough for the dev layout (controllers/ -> src/ = 4 levels) with margin. + */ +const MAX_PARENT_WALK = 6; + +/** + * Resolves the absolute path to `home.html` by walking up from `startDir`. + * + * The file lives at different depths depending on how the app is run: + * - Dev (`bun --hot src/index.ts`): `import.meta.dir` is + * `src/interfaces/http/controllers/`, home.html lives at `src/home.html` + * (4 levels up). + * - Prod (bundled `dist/index.js`): `import.meta.dir` is + * `$out/share/teleuploader/dist/`, home.html lives next to dist/ + * (1 level up, per flake.nix installPhase). + * + * Returns the first existing candidate, or `null` if none is found within + * the walk bound. + * + * @param startDir - Directory to start the search from (typically `import.meta.dir`). + * @param maxDepth - Maximum number of parent directories to walk (default: 6). + * @returns Absolute path to home.html, or `null` if not found. + */ +export const resolveHomeHtml = (startDir: string, maxDepth = MAX_PARENT_WALK): string | null => { + let dir = startDir; + for (let depth = 0; depth <= maxDepth; depth++) { + const candidate = join(dir, 'home.html'); + if (existsSync(candidate)) return candidate; + const parent = dirname(dir); + if (parent === dir) break; + dir = parent; + } + return null; +}; + /** * Handles the home/dashboard page request. * - * Reads the `home.html` file from the adjacent directory and serves it as - * an HTML response with UTF-8 charset. + * Reads the `home.html` file and serves it as an HTML response with UTF-8 + * charset. Fails fast with a clear error when the file cannot be located + * instead of letting Bun.serve swallow the ENOENT into a bare 500. * * @returns An HTML response containing the home page content. */ export const handleHome = async (): Promise => { - const html = await (Bun.file(`${import.meta.dir}/home.html`) as BunFile).text(); + const homeHtml = resolveHomeHtml(import.meta.dir); + if (!homeHtml) { + throw new Error( + `home.html not found — looked up from ${import.meta.dir} and ${MAX_PARENT_WALK} parent dirs`, + ); + } + const html = await (Bun.file(homeHtml) as BunFile).text(); return new Response(html, { status: 200, headers: { diff --git a/test/home.test.ts b/test/home.test.ts new file mode 100644 index 0000000..8fccfec --- /dev/null +++ b/test/home.test.ts @@ -0,0 +1,60 @@ +import { afterEach, describe, expect, it } from 'bun:test'; +import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { handleHome, resolveHomeHtml } from '../src/interfaces/http/controllers/home-controller'; + +describe('resolveHomeHtml', () => { + let root: string; + + const makeLayout = (tree: Record) => { + root = mkdtempSync(join(tmpdir(), 'home-resolver-')); + for (const [rel, content] of Object.entries(tree)) { + const full = join(root, rel); + mkdirSync(dirname(full), { recursive: true }); + writeFileSync(full, content); + } + return root; + }; + + afterEach(() => { + if (root) rmSync(root, { recursive: true, force: true }); + }); + + it('resolves home.html beside dist/ — prod Nix layout', () => { + makeLayout({ + 'dist/index.js': 'x', + 'home.html': 'prod', + }); + expect(resolveHomeHtml(join(root, 'dist'))).toBe(join(root, 'home.html')); + }); + + it('resolves src/home.html from controllers dir — dev layout', () => { + makeLayout({ + 'src/home.html': 'dev', + 'src/interfaces/http/controllers/home-controller.ts': 'x', + }); + expect(resolveHomeHtml(join(root, 'src/interfaces/http/controllers'))).toBe( + join(root, 'src/home.html'), + ); + }); + + it('returns null when home.html is not found within the walk bound', () => { + makeLayout({ 'dist/index.js': 'x' }); + expect(resolveHomeHtml(join(root, 'dist'))).toBeNull(); + }); +}); + +describe('handleHome', () => { + it('serves the dashboard HTML with 200 and text/html', async () => { + // handleHome resolves from the real source tree: src/home.html must exist. + const srcHome = join(import.meta.dir, '..', 'src', 'home.html'); + expect(existsSync(srcHome)).toBe(true); + + const res = await handleHome(); + expect(res.status).toBe(200); + expect(res.headers.get('content-type')).toContain('text/html'); + const body = await res.text(); + expect(body).toContain('FileDrop · S3 File Manager'); + }); +});