94 lines
6.3 KiB
Markdown
94 lines
6.3 KiB
Markdown
# Development
|
||
|
||
## Prerequisites
|
||
|
||
| Tool | Version | Install |
|
||
|-------------------------------|----------------------------|----------------------------------------------------------|
|
||
| [Go](https://go.dev/dl/) | 1.27+ (see `go.mod`) | |
|
||
| [Node.js](https://nodejs.org) | 20.19+ or 22.12+ (22 LTS recommended) | `nvm install 22` — Vite and vue-tsc run on Node |
|
||
| [Bun](https://bun.sh) (package manager and script runner) | 1.x | `powershell -c "irm bun.sh/install.ps1 \| iex"` or `curl -fsSL https://bun.sh/install \| bash` |
|
||
| [Task](https://taskfile.dev) | 3.43+ | `go install github.com/go-task/task/v3/cmd/task@latest` |
|
||
| Git | any | |
|
||
|
||
`task setup` installs the rest: [air](https://github.com/air-verse/air) for hot reload, [gremlins](https://github.com/go-gremlins/gremlins) for mutation testing, Go modules and frontend dependencies. Make sure `$(go env GOPATH)/bin` is on your `PATH`.
|
||
|
||
No C compiler is needed. The SQLite driver (`glebarez/sqlite`, backed by `modernc.org/sqlite`) is pure Go, and every build runs with `CGO_ENABLED=0`.
|
||
|
||
## First run
|
||
|
||
```sh
|
||
cp .env.example .env # set at least JWT_SECRET; SMTP is only needed to actually send mail
|
||
task setup
|
||
task dev
|
||
```
|
||
|
||
Open **http://localhost:5173**.
|
||
|
||
## How the dev setup works
|
||
|
||
```
|
||
browser ──▶ http://localhost:5173 Vite dev server (HMR) task dev:web
|
||
│ /v1/*, /healthz
|
||
└──proxy──▶ http://localhost:8080 Go API via air task dev:api
|
||
```
|
||
|
||
- **Always use the Vite URL (5173).** The Vite dev server proxies API calls to Go (see `server.proxy` in [`web/vite.config.ts`](../web/vite.config.ts)). The browser therefore sees a single origin, so the `jwt` cookie works and no CORS setup is needed. In the frontend, call the API with relative URLs (`/v1/...`) and never hardcode `localhost:8080`.
|
||
- **air** rebuilds and restarts the API when a `.go` or `.html` file (such as a mail template) changes. Its config is in [`.air.toml`](../.air.toml).
|
||
- Run the two halves separately with `task dev:api` and `task dev:web` if you want their logs in separate terminals.
|
||
|
||
### The `nofrontend` build tag
|
||
|
||
The production binary embeds the built SPA from `internal/infrastructure/assets/dist/` (the `//go:embed` in `assets_embed.go`). That directory only exists after `task build:web`.
|
||
|
||
To keep Go work independent of the frontend toolchain, all dev and quality tasks build with **`-tags nofrontend`**. That tag compiles `assets_stub.go` instead of the embed. The resulting binary serves the API normally and answers `/` with a notice that points to Vite.
|
||
|
||
| Command | Tag | Needs the frontend built? |
|
||
|------------------------------------|----------------|---------------------------|
|
||
| `task dev:api`, `test`, `cover`, `mutate`, `lint:go` | `nofrontend` | no |
|
||
| `task build`, `release`, `run` | *(none)* | yes (the task builds it first) |
|
||
| plain `go build ./...`, `go vet ./...` | *(none)* | yes, so add `-tags nofrontend` |
|
||
|
||
If your editor reports `pattern all:dist: no matching files found`, either run `task build:web` once or set the gopls build flags to `-tags=nofrontend`. In VS Code, add this to `settings.json`: `"gopls": { "buildFlags": ["-tags=nofrontend"] }`.
|
||
|
||
## Everyday commands
|
||
|
||
```sh
|
||
task --list # everything available
|
||
task test # Go tests; extra args pass through: task test -- -run TestCreateArticle -v
|
||
task test:mysql # MySQL tests against real servers in Docker (mysql:8.4, then mariadb:11)
|
||
task cover # coverage of the use cases → tmp/coverage.out
|
||
task mutate # gremlins mutation testing → tmp/gremlins.json
|
||
task test:web # SPA component tests (Vitest)
|
||
task visual:check -- log-in # screenshot design vs SPA for a screen → tmp/visual/log-in/
|
||
task lint # gofmt check, go vet, eslint --fix, vue-tsc
|
||
task fmt # gofmt -w + prettier
|
||
task run # build the production binary and run it (SPA embedded) on :8080
|
||
task clean # remove bin/, tmp/ and the built SPA
|
||
```
|
||
|
||
## Tests
|
||
|
||
- All Go tests live in [`test/`](../test) (package `test`, black-box). They use in-memory fakes of the domain `Repository` interfaces, so they need no database, SMTP server or network.
|
||
- HTTP-level tests use `net/http/httptest` against a chi router. Call `validator.InitValidator()` before invoking a handler.
|
||
- SPA component tests use Vitest + Vue Test Utils (`task test:web`). `task lint:web` runs eslint and a type check.
|
||
|
||
## Visual check against the design
|
||
|
||
`task visual:check -- <screen> [state]` compares the SPA with the Claude Design boards in [`docs/design/source`](design/source). The screen is a file name from [`docs/design/screens`](design/screens) (`log-in`, `home`, …), and the state is one of its headings.
|
||
|
||
- It builds the SPA into `tmp/visual/dist` and serves both the build and the boards locally, so no API or dev server is needed.
|
||
- For every state it screenshots:
|
||
- the board's frame for that state;
|
||
- the SPA in the same state, at 1440×900 and 390×844, in light and dark.
|
||
- It saves `design-*.png`, `app-*.png` and `diff-*.png` (differing pixels in red) to `tmp/visual/<screen>/<state>/`, and a summary to `tmp/visual/<screen>/report.md`.
|
||
- [`web/visual/states.ts`](../web/visual/states.ts) says how to reach each state in the SPA: the route, the mocked `/v1` responses (keyed `'METHOD /v1/path'`) and any user steps. A state without an entry is captured on the design side only, and marked "design only" in the report.
|
||
- Narrow a run with `--viewport=desktop|mobile` and `--theme=light|dark`.
|
||
- Lucide icons are served from the local `@iconify-json/lucide` set, for both the boards and the SPA. api.iconify.design rate-limits repeated runs.
|
||
- The first time on a machine, run `bunx playwright install chromium` in `web/`.
|
||
|
||
The pixel percentage only points at where to look, because font anti-aliasing always differs a little. Judge by comparing the image pairs.
|
||
|
||
## Data
|
||
|
||
With the default configuration, local state lives in `./data/`: `renewcms.db` and `uploads/`. The directory is gitignored. Delete it to start from scratch.
|