fix: root page 500 — resolve home.html for dev & prod layouts
Build & Deploy (Nix) / build-and-deploy (push) Successful in 54s
Build & Deploy (Nix) / build-and-deploy (push) Successful in 54s
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.
This commit is contained in:
@@ -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<Response> => {
|
||||
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: {
|
||||
|
||||
@@ -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<string, string>) => {
|
||||
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': '<html>prod</html>',
|
||||
});
|
||||
expect(resolveHomeHtml(join(root, 'dist'))).toBe(join(root, 'home.html'));
|
||||
});
|
||||
|
||||
it('resolves src/home.html from controllers dir — dev layout', () => {
|
||||
makeLayout({
|
||||
'src/home.html': '<html>dev</html>',
|
||||
'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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user