# 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 test:postgres # PostgreSQL tests against real servers in Docker (postgres:16, then postgres:17) 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. - A few tests open a real SQLite file in a temporary directory (the chunked id lookups, the slug backfill and the `?seo=` measurement). They never touch the real database. - The tests that need a real MySQL, MariaDB or PostgreSQL server skip themselves when `RENEWCMS_TEST_MYSQL_DSN` or `RENEWCMS_TEST_POSTGRES_DSN` is unset. No CI exists, so run them by hand before a release that touches the repositories: - `task test:mysql` runs `mysql:8.4` then `mariadb:11` in Docker, and `task test:postgres` runs `postgres:16` then `postgres:17`. Docker is required; the tasks start and remove the containers. Extra arguments go after `--`, for example `task test:postgres -- -run Postgres.*Chunked -v`. - To use your own server on a scratch database: `RENEWCMS_TEST_MYSQL_DSN='user:pass@tcp(host:3306)/db' go test -tags nofrontend ./test/... -run MySQL -count=1`, or `RENEWCMS_TEST_POSTGRES_DSN='postgres://user:pass@host:5432/db?sslmode=disable' go test -tags nofrontend ./test/... -run Postgres -count=1`. - `TestMySQLChunkedLookups` and `TestPostgresChunkedLookups` check that the batched `IN` lookups of the article repository (500 ids per batch) return the same rows as SQLite. ## Visual check against the design `task visual:check -- [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///`, and a summary to `tmp/visual//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.