From 90fb22b8d658776501ac4a79b3b903d52ecb21a0 Mon Sep 17 00:00:00 2001 From: Abdikhani Abdirashid Date: Sat, 31 Oct 2026 15:02:46 +0100 Subject: [PATCH] docs: record the dashboard toolchain (npm, Node 24.22.1, oxlint) --- docs/CONTEXT.md | 2 +- docs/decisions/0005-dashboard-toolchain.md | 75 ++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 docs/decisions/0005-dashboard-toolchain.md diff --git a/docs/CONTEXT.md b/docs/CONTEXT.md index 45c1a0d..8638475 100644 --- a/docs/CONTEXT.md +++ b/docs/CONTEXT.md @@ -92,7 +92,7 @@ Both the API and the dashboard follow it; changes to it go through a PR first. | Area | Choice | Not chosen | |---|---|---| -| Runtime | Bun **1.4.4**, pinned everywhere (laptop, CI, Docker). 1.4 is a new rewrite in Rust | Node (more common, but we know Bun; it stays our fallback) | +| Runtime | **API and worker:** Bun **1.4.4**, pinned on the laptops, in CI and in Docker (1.4 is a new rewrite in Rust). **Dashboard build and e2e:** Node **24.22.1** ([decision 0005](decisions/0005-dashboard-toolchain.md)) | *For the API:* Node (more common, but we know Bun; it stays our fallback). *For the dashboard:* Bun (Vite and Playwright are built for and released against Node) | | Web framework | **Fastify 5** ([decision 0001](decisions/0001-web-framework-fastify.md)): `buildApp()` + `listen({ host: "0.0.0.0" })`, pino logs, Zod validation, `@fastify/cors`, `inject()` tests | Hono (native on Bun, but new to us), Express, Elysia | | Database library | postgres.js 3.4.9 behind one file `src/db.ts`: pool 5 per pod, 5 s `statement_timeout`; the API waits for the database before it listens, and answers 503 `service_unavailable` while it can't reach it | `Bun.sql` (ties the data layer to the new runtime), Drizzle | | API code structure | **Zod 4** for bodies, queries and env vars ([decision 0003](decisions/0003-api-code-structure.md)); layers routes → services → repositories; one Fastify error handler with one error shape; small files, one job each | Fastify JSON Schema, `fastify-type-provider-zod`, logic in route handlers | diff --git a/docs/decisions/0005-dashboard-toolchain.md b/docs/decisions/0005-dashboard-toolchain.md new file mode 100644 index 0000000..b1bc560 --- /dev/null +++ b/docs/decisions/0005-dashboard-toolchain.md @@ -0,0 +1,75 @@ +# 0005. Dashboard toolchain: npm, Node 24.22.1, oxlint + +- **Status:** Accepted +- **Date:** 2026-10-31 +- **Decided by:** Devinder Singh and Abdikhani Mohamed + +## Context + +[Decision 0002](0002-dashboard-stack.md) records what the dashboard is built *from* (Vite, +React, TypeScript, Tailwind, React Router). It says nothing about what *builds and checks* it, +and that gap showed up in the review of #9: the choices were already made in the code without +being written down anywhere, so neither of us could point at a reason for them. + +The rest of the project runs on **Bun 1.4.4**, pinned on the laptops, in CI and in Docker. The +dashboard is the one part that does not. It is a small single-page app, built once per deploy +into static files that nginx serves; Node never runs in the finished container. Its Playwright +e2e tests also live in this folder and run in CI. + +Everything else in the project is pinned to an exact version (Bun 1.4.4, `postgres:18.6`, +k3s v1.36.5), so the toolchain should be too. + +## Decision + +| Part | Choice | Not chosen, and why | +|---|---|---| +| Package manager | **npm** (`npm ci`, `package-lock.json`) | npm is what this folder already has: `create-vite` generated the `package-lock.json`, `npm ci` installs exactly what that file pins, and it is the workflow we both already know. The Node *version* row below is a separate decision — Vite's and Playwright's requirements are about Node, not about npm. *Bun:* one runtime for the whole project would be tidier, and it is what we use everywhere else. But this folder's tools are built for Node: Vite's own `engines` field names Node versions (`^20.19.0 \|\| >=22.12.0`), and Playwright is documented and released against Node. Running them on Bun means relying on its Node compatibility layer for the two tools that decide whether the dashboard builds and whether the e2e tests are trustworthy. That is a real risk for no gain: `npm ci` plus a 330 ms Vite build is not what slows us down. | +| Node version | **24.22.1**, pinned in `dashboard/.nvmrc` and `node:24.22.1-alpine` | *Node 26:* became the active LTS on 28 Oct 2026, three days before this record. We stay on the Node 24 line the dashboard is already built and tested on: a major Node version is the kind of change that moves Vite's and Playwright's own supported ranges, and those two tools decide whether the dashboard builds and whether the e2e tests mean anything. The week of a defense is the wrong time to find that out, so 26 is a move for after the exam, not before it. *Node 22:* in maintenance a year longer and end-of-life Apr 2027, so it buys nothing and expires sooner. 24 is in maintenance — security and bug fixes, no new features — until Apr 2028, and it is the version on Devinder's laptop, which is what we agreed so that we are not debugging a version difference between us. | +| Linter | **oxlint 1.89.0** | *ESLint:* a much larger rule and plugin ecosystem, and many of its rules work without a typed-lint setup. We have not measured it against oxlint, so speed is not our argument. The reason is narrower: oxlint is the linter `create-vite` scaffolds, it covers the checks we want with a small config, and we have not yet needed a rule that only ESLint has. If we do, adding ESLint is its own decision rather than a reason to start there. | + +Node's release lines, for the version row above (from `nodejs/Release`, checked 31 Oct 2026): + +| Node | Status on 31 Oct 2026 | Supported until | +|---|---|---| +| 22 | maintenance since 21 Oct 2025 | 30 Apr 2027 | +| **24** | **maintenance since 20 Oct 2026** | **30 Apr 2028** | +| 26 | active LTS since 28 Oct 2026 | 30 Apr 2029 | + +## Consequences + +- The project has **two runtimes**: Bun for `api/` and the worker, Node for `dashboard/`. Each + folder says which, so neither of us has to remember: `api/.bun-version` and + `dashboard/.nvmrc`. +- `dashboard/` has its own lockfile (`package-lock.json`) and its own CI cache, separate from + the API's `bun.lock`. +- The dashboard image is a two-stage build: `node:24.22.1-alpine` compiles, and the result is + copied into `nginxinc/nginx-unprivileged`. **Node never runs in production.** A Node CVE is + therefore a build-time concern rather than a running-container one — but not a harmless one: + a compromised build step could change the files it produces, and those files are exactly what + users are served. +- `nvm use` inside `dashboard/` picks up `.nvmrc`. Worth setting `nvm alias default 24.22.1` + as well: without a default, a new shell falls back to whatever Node the system has, and a + system Node older than Vite's `^20.19.0 || >=22.12.0` fails in a way that looks like a + broken checkout rather than a wrong Node. That happened on 31 Oct 2026. +- `package.json` has **no `engines` field** today, so npm installs happily on a Node version + Vite will then refuse. Adding `engines` on its own would not change that: npm's + `engine-strict` is `false` by default, so `engines` only prints a warning. Turning it into a + real failure takes `engines` **plus** `engine-strict=true` in `dashboard/.npmrc`. We have + not done either, on purpose: CI pins Node from `.nvmrc`, which is the guard that decides + whether the dashboard builds, and `engine-strict` would only protect a local `npm install` + on the wrong Node. +- Node 24 leaves support in **April 2028**, well past the exam, so there is no *major* upgrade + to make before then; patch releases on the 24 line still apply as normal. The choice is + worth revisiting if the repo outlives the course. + +## In CI + +The `dashboard-test` job runs on every pull request. It reads the Node version from +`dashboard/.nvmrc`, so CI, Docker and both laptops take it from one file, then runs `npm ci`, +`npm run lint`, `npm run build` and the Playwright e2e tests. The build step is there on +purpose: Playwright drives Vite's dev server, so only `npm run build` proves the production +build still works. + +The `dashboard-image` job then builds the two-stage image, smoke-tests it, and on `main` +pushes `ghcr.io/devinder-dev/relay-dashboard:`, so the tag exists for every commit the +deploy might apply. Neither laptop is the authority on whether the dashboard builds.