Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 22 additions & 19 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ name: CI
# workflow would repeat every check the first had already run. Chaining them
# means a broken type never costs a build, and nothing is run twice.
#
# A pull request runs check, build and security. `bun run build` is what
# A pull request runs check, build and security. `npm run build` is what
# catches a build error there; the container image is only built on main and
# on release tags, where it is also published.
on:
Expand All @@ -35,55 +35,58 @@ jobs:
steps:
- uses: actions/checkout@v4

- uses: oven-sh/setup-bun@v2
- uses: actions/setup-node@v4
with:
bun-version: latest
node-version: 22
cache: npm

# --frozen-lockfile, so a dependency added without committing bun.lock
# fails here rather than resolving to something different in CI.
# npm ci installs exactly the lockfile and fails if package.json and
# package-lock.json disagree, so a dependency added without committing the
# lockfile fails here rather than resolving to something else in CI.
- name: Install
run: bun install --frozen-lockfile
run: npm ci

- name: Lint
run: bun run lint
run: npm run lint

- name: Typecheck
run: bun run typecheck
run: npm run typecheck

# Dead files and dependency drift. Unused exports are deliberately not
# gated — see knip.json.
- name: Dead code
run: bun run knip
run: npm run knip

# Coverage is printed here rather than uploaded anywhere. There is no
# threshold yet: the suite is small, and a number invented now would
# either be trivially met or fail on the first honest commit.
- name: Test
run: bunx vitest run --coverage
run: npm run test:coverage

build:
needs: check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: oven-sh/setup-bun@v2
- uses: actions/setup-node@v4
with:
bun-version: latest
node-version: 22
cache: npm

- name: Install
run: bun install --frozen-lockfile
run: npm ci

- name: Build
run: bun run build
run: npm run build

# The Docker build cannot regenerate this file — oven/bun:1-alpine has no
# Node for the TanStack Router plugin to run under — so a stale committed
# copy would ship a wrong route tree with nothing to catch it.
# The build regenerates this file, so a diff here means the committed copy
# had drifted from src/routes.ts. Committing it keeps the tree readable
# and reviewable; this guard keeps it honest.
- name: Route tree is current
run: |
if ! git diff --quiet -- src/routeTree.gen.ts; then
echo "::error file=src/routeTree.gen.ts::Route tree is stale. Run 'bun run build' and commit the result."
echo "::error file=src/routeTree.gen.ts::Route tree is stale. Run 'npm run build' and commit the result."
git diff -- src/routeTree.gen.ts
exit 1
fi
Expand Down Expand Up @@ -111,7 +114,7 @@ jobs:
exit 1
fi

# Scans bun.lock for known CVEs. Runs alongside build rather than after it:
# Scans package-lock.json for known CVEs. Runs alongside build rather than after it:
# a vulnerable dependency and a broken bundle are independent failures and
# you want to see both, but neither is worth running if the code does not
# compile.
Expand Down
12 changes: 6 additions & 6 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ set -e
# Fast feedback only. CI runs the same checks and is the actual gate, because
# a local hook can be skipped with --no-verify.

if ! command -v bun >/dev/null 2>&1; then
echo "husky: bun not found, skipping checks"
if ! command -v npm >/dev/null 2>&1; then
echo "husky: npm not found, skipping checks"
exit 0
fi

bun run lint
bun run typecheck
bun run knip
bun run test
npm run lint
npm run typecheck
npm run knip
npm test
39 changes: 23 additions & 16 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,22 @@ a feature flag flips, update it in the same change.
- Routing: TanStack Router, virtual file routes (`src/routes.ts`)
- State: Zustand
- Styling: Tailwind CSS v4, shadcn/ui components in `src/components/ui`
- Package manager: **Bun only.** npm cannot resolve this dependency tree without
`--legacy-peer-deps`; Bun resolves it with no flags
- Runtime and package manager: **Node 22 and npm.** One lockfile,
`package-lock.json`. Nothing runs a JavaScript runtime in production — the
deployed artifact is static files served by nginx
- Tests: Vitest, with happy-dom where a test needs a real `document`
- Dead code: knip
- Serving: nginx in a container (`Dockerfile`, `nginx.conf`)

## Build & Run
```
bun install # also activates git hooks, via the prepare script
bun dev # http://localhost:3000
bun run lint # eslint
bun run typecheck # tsc -b
bun run knip # dead files and dependency drift
bun run test # vitest
bun run build # tsc -b && vite build
npm install # also activates git hooks, via the prepare script
npm run dev # http://localhost:3000
npm run lint # eslint
npm run typecheck # tsc -b
npm run knip # dead files and dependency drift
npm test # vitest
npm run build # tsc -b && vite build
docker compose up --build # the deployable image, http://localhost:8080
```

Expand Down Expand Up @@ -114,9 +115,9 @@ src/
- **`src/routes.ts` is loaded by `vite.config.ts` as a string path.** No static
analysis reaches it. It is declared as a knip entry point for this reason;
without that, knip calls it and `@tanstack/virtual-file-routes` dead.
- **`routeTree.gen.ts` is generated and committed, and must stay committed.**
The Docker build cannot regenerate it — `oven/bun:1-alpine` has no Node for
the router plugin. CI has a guard.
- **`routeTree.gen.ts` is generated and committed.** The build regenerates it,
so a diff after `npm run build` means it had drifted from `src/routes.ts`. CI
guards this.
- **Configuration is read at module-init time**, so `main.tsx` loads config and
then *dynamically* imports `bootstrap.tsx`. A static import would evaluate the
whole graph first and `getConfig()` would throw. The `.then()` chain is not
Expand All @@ -127,11 +128,17 @@ src/
- **`विस्तार` / `વિસ્તાર` is the ordinary word for *area*** in Hindi, Marathi and
Gujarati. Find-and-replace on brand names corrupts legitimate copy. Read every
occurrence.
- **npm cannot install this repo** without `--legacy-peer-deps`; it crashes with
`Cannot read properties of null (reading 'edgesOut')`. Bun is the only
supported package manager, and `bun.lock` is the only lockfile.
- **Never reach for `--legacy-peer-deps`.** It hides real version conflicts —
one was hiding behind it here, `@vitest/coverage-v8` pinned a minor behind
`vitest`. If npm refuses to resolve something, the answer is an upgrade or
pnpm, not the flag.
- **The router plugin needs Node.** `vite.config.ts` passes
`virtualRouteConfig` as a path, so the generator loads that TypeScript file
through `tsx`, a Node loader. Under Bun it failed with
`Cannot find package 'tsx:'`, and only sometimes — depending on whether the
generator's cache decided to re-run.
- **knip's unused-export list is not a task list.** `src/components/ui` is a
vendored component kit kept whole. The gate covers files and dependencies only.
- **Verify with `bun dev` as well as `bun run build`.** A Vite rule about
- **Verify with `npm run dev` as well as `npm run build`.** A Vite rule about
`public/` imports once shipped in a PR that built and served correctly from
`dist/` but broke the dev server.
12 changes: 7 additions & 5 deletions CONVENTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,15 @@ work" are different claims and a reviewer cannot tell them apart afterwards.
a dependency vulnerability scan, and on main and release tags the container
image. Its jobs are chained — `check` first, then `build` and `security`,
then `image` — so a broken type never costs a build and no check runs twice.
A pull request stops after `build`: `bun run build` is what catches a build
A pull request stops after `build`: `npm run build` is what catches a build
error there, and an image nobody will publish is not worth the minutes.

## Dependencies
- **Bun only.** One lockfile, `bun.lock`. npm cannot resolve this tree.
- `bun install --frozen-lockfile` in CI, so a dependency added without
committing the lockfile fails there rather than silently resolving to
something else.
- **Node and npm.** One lockfile, `package-lock.json`.
- `npm ci` in CI, which installs exactly the lockfile and fails when it and
`package.json` disagree, so a dependency added without committing the lockfile
fails there rather than silently resolving to something else.
- **Never `--legacy-peer-deps`.** A peer conflict npm reports is usually real.
Fix the versions.
- A new dependency needs a reason in the commit body. This repo is a reference
implementation; every dependency is one an adopter inherits.
14 changes: 7 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# syntax=docker/dockerfile:1

# ---- Build ------------------------------------------------------------------
# The TanStack Router plugin would regenerate src/routeTree.gen.ts during the
# build, and this image has no Node for it to run under. That file is committed,
# so the build uses the checked-in copy. Keep it committed.
FROM oven/bun:1-alpine AS build
# Node rather than Bun. The TanStack Router plugin loads src/routes.ts through
# tsx, which is a Node loader, so a Bun builder needed a Node installed beside
# it to work at all — and failed unpredictably when it did not.
FROM node:22-alpine AS build

WORKDIR /app

COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY package.json package-lock.json ./
RUN npm ci

COPY . .
RUN bun run build
RUN npm run build

# ---- Serve ------------------------------------------------------------------
FROM nginx:alpine AS serve
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ API contract needs better documentation — please open an issue instead.

## Quick start

Requires [Bun](https://bun.sh).
Requires [Node](https://nodejs.org) 20 or newer.

```bash
bun install
bun dev # http://localhost:3000
npm install
npm run dev # http://localhost:3000
```

No environment variables are required to boot. The app calls `/api/*` on its
Expand All @@ -71,7 +71,7 @@ The hooks live in `.husky/` and are activated by a single local git setting,
`husky` from its `prepare` script:

```bash
bun install
npm install
```

Check that yours are actually active:
Expand All @@ -80,7 +80,7 @@ Check that yours are actually active:
git config core.hooksPath # should print .husky/_
```

If that prints nothing, the hooks are not running. Fix it with `bunx husky`.
If that prints nothing, the hooks are not running. Fix it with `npx husky`.
This is worth checking rather than assuming: `core.hooksPath` is local to your
clone and is not committed, so anyone who installed dependencies before a hook
existed silently has no hooks.
Expand Down
Loading
Loading