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
17 changes: 17 additions & 0 deletions .fallowrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,23 @@
"zod"
],
"rules": {
"boundary-violation": "error",
"unused-dependencies": "warn"
},
"boundaries": {
"zones": [
{ "name": "domain", "patterns": ["src/domain/**"] },
{ "name": "infra", "patterns": ["src/infra/**"] },
{ "name": "api", "patterns": ["src/api/**"] },
{ "name": "webapp", "patterns": ["src/webapp/**"] },
{ "name": "server", "patterns": ["src/server.ts"] }
],
"rules": [
{ "from": "domain", "allow": [] },
{ "from": "infra", "allow": ["domain"] },
{ "from": "api", "allow": ["domain", "infra"] },
{ "from": "webapp", "allow": ["domain", "api"] },
{ "from": "server", "allow": ["domain", "infra", "api", "webapp"] }
]
}
}
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ This project uses **Vite 8**, **Vitest 4**, **Stryker**, **oxlint**, **oxfmt**,

- `pnpm lint` - Lint and auto-fix with oxlint (includes local `starter/*` JS plugin rules in `tools/oxlint-plugins/`)
- `pnpm format` - Format code with oxfmt
- `pnpm run ci` - Full CI check: lint, format, and typecheck
- `pnpm run ci` - Full CI check: lint, format, typecheck, and Fallow audit

### Dependencies

Expand All @@ -46,6 +46,7 @@ This project uses **Vite 8**, **Vitest 4**, **Stryker**, **oxlint**, **oxfmt**,
- **Stryker config**: `stryker.config.mjs` (mutation scope, reporters, 80% break threshold)
- **oxlint config**: `oxlint.config.ts` (type-aware linting, ignore patterns)
- **oxfmt config**: `oxfmt.config.ts` (formatting: no semi, single quotes)
- **Fallow config**: `.fallowrc.json` (dead code analysis, dependency checks, custom architecture boundary zones)
- **TypeScript**: `tsconfig.json` and layer-specific configs

## Imports
Expand All @@ -60,7 +61,7 @@ This project uses **Vite 8**, **Vitest 4**, **Stryker**, **oxlint**, **oxfmt**,

Git hooks are managed by [lefthook](https://lefthook.dev), configured in `lefthook.yml`.

- **pre-commit** runs `oxlint --fix` and `oxfmt --write` on staged files (auto-restaged), then runs `vitest related` over the staged files — only unit tests that import a staged file execute.
- **pre-commit** runs `oxlint --fix` and `oxfmt --write` on staged files (auto-restaged), then runs `vitest related` over the staged files — only unit tests that import a staged file execute. It also runs `pnpm fallow:ci` to enforce Fallow checks before each commit.
- **pre-push** runs `vitest run --changed origin/master --project unit`, executing only the unit tests affected by files changed against `origin/master`.

Mutation testing is intentionally not a git hook because it is slower than the commit/push feedback loop. Run `pnpm test:mutate` before merging changes to `src/domain/**`, `src/api/**`, or unit-test behaviour; CI enforces the same command for those paths.
Expand All @@ -71,4 +72,5 @@ Hooks install automatically via the `prepare` script (`lefthook install`). To sk

- [ ] Run `pnpm install` after pulling remote changes and before getting started.
- [ ] Run `pnpm run ci` and `pnpm test` to validate changes.
- [ ] Run `pnpm fallow list --boundaries` and `pnpm fallow dead-code --boundary-violations` after changing architecture boundary config.
- [ ] Run `pnpm test:mutate` when changing mutation-scoped logic or unit tests.
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,14 @@ Business logic lives in pure TypeScript. Frameworks, databases, and external ser
- `src/api/` — HTTP composition root (Hono). Wires concrete adapters into use cases and exposes them as routes.
- `src/webapp/` — React + TanStack Start UI. Calls into the API; composes its own adapters where needed.

**The boundary is enforced three times, on purpose:**
**The boundary is enforced four times, on purpose:**

1. **Path aliases** — `@domain/*` and `@infra/*` in `tsconfig.json` make the layer of every import readable at a glance.
2. **Layered typechecks** — `tsconfig.domain.json` compiles the domain alone; `tsconfig.infra.json` compiles domain + infra. If domain code reaches into infra, the domain typecheck fails before anything else runs. `pnpm typecheck:layers` runs both.
3. **Custom oxlint rule** — `starter/domain-no-infra-imports` (in `tools/oxlint-plugins/rules/`) blocks both static and dynamic imports from `src/infra/**` inside `src/domain/**` files.
4. **Fallow custom boundaries** — `.fallowrc.json` defines explicit zones for `domain`, `infra`, `api`, `webapp`, and `server`, then fails `boundary-violation` issues in CI.

Any one of the three would catch most mistakes; all three together make the violation loud and local.
Any one of the four would catch most mistakes; all four together make the violation loud and local.

## Directory layout

Expand Down Expand Up @@ -147,7 +148,7 @@ Rule of thumb: if a module contains only declarations, use `*.schema.ts` or `*.t

Every stage has a specific job. Understanding the _why_ matters as much as the commands.

- **Pre-commit (lefthook)** — runs `oxlint --fix` and `oxfmt --write` on staged files (auto-restaged), then `vitest related --run --project unit` over the staged files. _Why_: keeps git history clean and readable (no "fix lint" commits), and ensures every commit is **independently releasable** — no commit silently breaks the behaviour of code near the change.
- **Pre-commit (lefthook)** — runs `oxlint --fix` and `oxfmt --write` on staged files (auto-restaged), then `vitest related --run --project unit` over the staged files and `pnpm fallow:ci` for Fallow checks. _Why_: keeps git history clean and readable (no "fix lint" commits), and ensures every commit is **independently releasable** — no commit silently breaks the behaviour or architecture of code near the change.
- **Pre-push (lefthook)** — `vitest run --changed origin/master --project unit`. Catches regressions across the whole change set before they leave the machine.
- **`pnpm run ci`** (local + CI) — `oxlint && oxfmt --check . && pnpm typecheck:layers && pnpm fallow:ci`. Read-only quality gate; no fixes, no writes. The source of truth for "is this branch green?"
- **GitHub Actions** — runs the same gate plus the test matrix (unit, browser, integration).
Expand Down Expand Up @@ -178,10 +179,12 @@ pnpm fallow # full analysis (dead code + dupes + health)
pnpm fallow dead-code # unused code + circular deps only
pnpm fallow dupes # duplication scan
pnpm fallow health # complexity + maintainability
pnpm fallow list --boundaries
pnpm fallow dead-code --boundary-violations
pnpm fallow fix --dry-run # preview auto-fixes for unused exports/deps
```

`pnpm run ci` runs `pnpm fallow:ci` (`fallow audit`) as a quality gate — it scopes analysis to files changed against the base branch and returns a pass/warn/fail verdict. Configuration lives in `.fallowrc.json`. For the full feature set, see the [fallow docs](https://docs.fallow.tools).
`pnpm run ci` runs `pnpm fallow:ci` (`fallow audit`) as a quality gate — it scopes analysis to files changed against the base branch and returns a pass/warn/fail verdict. Configuration lives in `.fallowrc.json`, including custom boundary zones and rules for the hexagonal architecture. For the full feature set, see the [fallow docs](https://docs.fallow.tools).

## Reproducing this setup in your own repo

Expand All @@ -191,7 +194,7 @@ A short checklist for applying these conventions to a greenfield project:
2. Create `src/domain`, `src/infra`, `src/utils` on day one, even if empty. Directory shape is a commitment.
3. Copy the `tsconfig.json` + `tsconfig.domain.json` + `tsconfig.infra.json` trio; adjust the includes/aliases.
4. Set up `oxlint.config.ts` and `oxfmt.config.ts`. Copy `tools/oxlint-plugins/` and rename the `starter` namespace.
5. Add `lefthook.yml` with pre-commit (lint + format + `vitest related --project unit`) and pre-push (`vitest run --changed origin/master --project unit`).
5. Add `lefthook.yml` with pre-commit (lint + format + `vitest related --project unit` + `pnpm fallow:ci`) and pre-push (`vitest run --changed origin/master --project unit`).
6. Add the `pnpm ci` script: lint → format check → layered typecheck → fallow audit.
7. Add Stryker (`@stryker-mutator/core`, `@stryker-mutator/vitest-runner`), `stryker.config.mjs`, `vitest.mutation.config.ts`, and the `pnpm test:mutate` script with an 80% `thresholds.break` floor.
8. Adopt the four test-type filename suffixes (`.unit`, `.browser`, `.integration`, `.e2e`) before writing any tests.
Expand Down
2 changes: 2 additions & 0 deletions lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ pre-commit:
- name: vitest
glob: '*.{js,ts,tsx,jsx,mjs,cjs}'
run: pnpm vitest related --run --project unit --passWithNoTests {staged_files}
- name: fallow
run: pnpm fallow:ci

pre-push:
jobs:
Expand Down
Loading