Coding agent guide for the mlack repository — a real-time Slack-like chat app built with Hono, TypeScript, and Cloudflare D1 (SQLite).
Package manager is pnpm (v10.12.4). Node 24.
pnpm install # Install dependencies
pnpm build # Full build (client + server)
pnpm build:check # Type-check only (no emit)
pnpm lint # Biome lint + format check
pnpm lint:fix # Biome auto-fix
# Unit tests (Vitest)
pnpm test # Watch mode
pnpm test:run # Single run (CI)
npx vitest run hono/routes/health.test.ts # Run a single test file
npx vitest run -t "should return 401" # Run tests matching a name
# E2E tests (Playwright — requires running Postgres)
pnpm test:e2e # Run all E2E tests
npx playwright test e2e/app.spec.ts # Run a single E2E file
# Database (Drizzle ORM)
pnpm db:generate # Generate migration from schema changes
pnpm db:migrate # Apply migrationsCI runs: pnpm test:run then pnpm lint then pnpm build. All three must pass.
Task completion order: pnpm test:run → pnpm lint → pnpm build → Copilot CLI review → commit.
hono/
index.ts # Server entry point
app.tsx # App factory, middleware, route registration
types.ts # Shared types (User, Variables)
testApp.ts # Test helper — creates app with mock session
auth/ # Auth utilities (password hashing)
components/ # Server-rendered JSX pages + CSS
db/ # Drizzle schema, connection, migrations
routes/ # Route handlers
static/ # Client-side TypeScript (compiled to JS)
e2e/ # Playwright E2E tests
- 2-space indentation, 120-character line width.
- Run
pnpm lint:fixto auto-format. No ESLint or Prettier. - Biome organizes imports automatically — do not manually sort them.
- Always use
.jsextensions in local imports (ESM requirement):import { authRoute } from "./routes/auth.js"; - Use
import typefor type-only imports (verbatimModuleSyntaxis enforced):import type { User, Variables } from "../types.js"; - Exception: E2E tests in
e2e/do not use.jsextensions (Playwright resolves them).
- Named exports only — no default exports.
- Strict mode enabled. Avoid
any. - Use
typealiases (notinterface) for data structures. - Hono app instances are typed with
Hono<{ Variables: Variables }>. - Session user is accessed via
session.get("user") as User | undefined.
| Kind | Convention | Examples |
|---|---|---|
| Files (components) | PascalCase | ChatPage.tsx, LoginPage.tsx |
| Files (other) | camelCase | emailAuth.ts, password.ts |
| E2E specs | kebab-case | app.spec.ts, database-persistence.spec.ts |
| Unit tests | .test.ts |
health.test.ts, ChatPage.test.ts |
| Variables/functions | camelCase | createApp, hashPassword, wsUrl |
| Constants | UPPER_SNAKE | SALT_LENGTH, KEY_LENGTH |
| Types | PascalCase | User, Variables, AppOptions |
| JSX components | PascalCase | ChatPage(), LoginPage() |
| Route instances | camelCase + Route suffix |
const authRoute = new Hono(...) exported as { authRoute } |
Avoid code comments. Use descriptive variable and function names so the code is self-documenting.
Route handlers follow this pattern:
routeHandler.get("/path", async (c) => {
try {
if (!input) {
return c.json({ error: "Bad request" }, 400);
}
// logic
} catch (error) {
console.error("Descriptive context:", error);
return c.json({ error: "User-facing message" }, 500);
// or for HTML routes:
// return c.html(`<!DOCTYPE html>${await Page("Error message")}`, 500);
}
});- API routes return
c.json({ error: "..." }, statusCode). - HTML form routes re-render the page with an inline error message.
- Auth guards: check
session.get("user"), return 401 or redirect to/auth/login. - WebSocket errors are logged but do not stop message broadcast.
- Server-side only (Hono JSX, not React). No client-side JSX.
- Components are async functions returning full HTML documents (
<html>,<head>,<body>). - Render with:
c.html(\${await PageComponent(args)}`)`. - Use
className(notclass),htmlFor(notfor),charSet(notcharset).
- Plain CSS files co-located with components in
hono/components/. - Served as static assets via
/components/*.css.
-
Co-locate test files next to implementation:
health.ts/health.test.ts. -
Import from vitest explicitly:
import { describe, expect, it, vi } from "vitest". -
Use
describe/itblocks (nottest). -
Use
createTestApp()fromhono/testApp.tsfor route tests — it injects mock session middleware:const { app } = createTestApp({ authenticatedUser: { email: "test@example.com", name: "Test", picture: "pic.jpg" }, }); const response = await app.request("/path");
-
Test HTTP via Hono's built-in
app.request()— no supertest.
- Files in
e2e/with.spec.tsextension. - Use
loginWithMock(page)helper for authentication. - Selectors: CSS classes (
.user-email) and IDs (#messageInput).
DO NOT commit or consider a task complete without running Copilot reviews first.
After completing a task (not after every individual file edit), run the following for each file that was created or modified:
copilot -p 'Review @path/to/file for security, efficiency, and readability. Suggest specific improvements.' --model gpt-5.4 -sReplace path/to/file with the actual file path (for example, hono/routes/health.ts).
- Run reviews after tests and lint pass, but before committing.
- If Copilot finds issues, refactor the code, re-run tests, and repeat the review for affected files.
- Do not skip this step. Do not assume the tool is unavailable — verify by running it.
- Conventional Commits:
feat:,fix:,chore:,docs:,refactor:,test:, etc. - Do not commit unless tests pass (both unit and E2E). Write tests alongside implementation.
- Do not commit unless: tests pass, lint passes, build passes, and Copilot CLI reviews have been run for all modified files.
- Delete code and files that become unnecessary after changes.