Template
The starting point for an app with a React frontend. Same server, env, database and API skeleton as template-htmx — the two differ only in what is served at / — with a React 19 SPA, Tailwind v4 and shadcn/ui on top. Carried from a working project rather than designed in the abstract, and carrying the lessons that cost it time: - The dev script states NODE_ENV=development rather than inheriting it. React resolves its runtime against NODE_ENV: Bun emits jsxDEV() in development and React's production jsx-dev-runtime defines jsxDEV as void 0. Inherit production while transforming for development and the app dies at first paint. Verified: the dev bundle resolves the development runtime and contains no void-0 stub. - Asset URLs in components are relative and <base href="/"> is in the HTML, so a path-prefix proxy can rewrite one attribute and every asset follows. A test enforces it. - Routing is in the hash, because the app may be mounted under a prefix it only learns at runtime and a hash means the same thing at either mount. - index.html holds __PUBLIC_URL__ and gen-index.ts substitutes it, because Bun's HTML bundler fails the build on a root-relative favicon href. - APP_ENV failing closed, env files before the ambient environment, the database enforced by name, one Postgres client per process — as in the htmx template. The database has no tables. The App is a shell with one tab and no features, named as a placeholder. Renaming `app` is the first step and the README lists every place it appears. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
52 lines
2.3 KiB
Markdown
52 lines
2.3 KiB
Markdown
# app
|
|
|
|
A project started from `template-react`. **Rename `app` first** — README step 1.
|
|
|
|
**Bun · Hono · React 19 · Tailwind v4 · shadcn/ui · Postgres with Drizzle.**
|
|
|
|
## Read these before writing code
|
|
|
|
| | |
|
|
|---|---|
|
|
| `CONVENTIONS.md` | React patterns — feature folders, manager hooks, colocated state — and layout |
|
|
| `docs/DATABASE.md` | Drizzle structure, `push` not migrations, `check()` vs open `text` |
|
|
| `docs/SURFACES.md` | The JSON API and the app: one process, one query layer |
|
|
| `README.md` | Setup, scripts, configuration |
|
|
|
|
## Rules that are load-bearing
|
|
|
|
**`APP_ENV`, never `NODE_ENV`.** Must be `production` or `development`; throws when unset. Hosts export
|
|
`NODE_ENV=production` into everything they spawn, so anything keyed on it treats development as
|
|
production silently. `NODE_ENV` is still set explicitly by both scripts because React's runtime
|
|
resolves against it — Bun emits `jsxDEV()` in development and React's production runtime defines it
|
|
as `void 0`.
|
|
|
|
**Env files are read with the ambient environment LAST**: `.env.production.local` (production only)
|
|
→ `.env.local` → `.env` → `process.env`.
|
|
|
|
**Asset URLs in components are relative — never start with `/`.** `<base href="/">` in `index.html`
|
|
is what a path-prefix proxy rewrites; a leading slash bypasses it. `asset-urls.test.ts` enforces this.
|
|
`fetch('/api/...')` is the exception.
|
|
|
|
**Routing is in the hash.** The app may be mounted under a prefix it only learns at runtime, and a
|
|
hash means the same thing at either mount point. `lib/routes.ts`.
|
|
|
|
**One feature folder per screen, one manager hook per feature.** State lives in the hook, components
|
|
receive the manager. `CONVENTIONS.md`.
|
|
|
|
**Serializers live in `src/servers/serialize.ts` only.** Its test walks the module's exports for
|
|
camelCase keys and for serializers without a case.
|
|
|
|
## Bun
|
|
|
|
- `bun <file>` not `node`, `bun test` not jest/vitest, `bun install`, `bunx`
|
|
- `Bun.serve()` — not express. `Bun.file` over `node:fs`. `Bun.$` over execa
|
|
- Bun loads `.env` automatically; do not add dotenv
|
|
- `bun dev` and `bun start` set the env markers. Running `src/server.ts` directly throws, by design
|
|
|
|
## Before you finish
|
|
|
|
- `bunx tsc --noEmit` and `bun test`
|
|
- Verify claims rather than asserting them — start the server and request the thing
|
|
- Commit with a message that says *why*, not what the diff already shows
|