Tests, tooling and the served scripts are TypeScript (#392)

The page and the service worker were strict TypeScript; the tests, the
build, the home page's generator, the load tests, boot.js and the
directory's site.js were plain JavaScript that nothing checked. All of
it is TypeScript now, in five programs, each with its own globals, all
checked by npm run typecheck: the page (tsconfig.json, with boot.ts and
the directory's site.ts), the worker, Node's (tsconfig.tools.json: the
Node tests, the browser suite, the build, the site generator, the load
harness), and k6's (tsconfig.k6.json, with @types/k6).

Node 24 runs the .ts files by stripping their types, so they hold only
TypeScript that erases (erasableSyntaxOnly) and import each other by
.ts name; package.json says "type": "module" for it, and Dockerfile.web
moves to Node 24 to run build.mts. k6 2.3 strips its scripts' types
itself.

The browser suite's page.evaluate calls reach the page's globals; the
page's tsc run now also writes its declarations to target/page-types,
which the suite's program includes, so those calls are typed by the
page's own code rather than by hand-written copies.

boot.ts is compiled by build.mts with swc, as the worker is, and named
by the hash of what is served. The directory's build has no Node, so
site.js is compiled from site.ts and committed (node web/build.mts
--directory-script), and tests/directory-script.ts fails if they drift.

Checked: the site generator's 139 pages are byte for byte what the
JavaScript made; the page build's app.js is identical and only boot.js
(now minified) and its hash differ; contrast, page-smoke, page-unit,
native-bridge, the browser suite (96 passed, 2 skipped) and the
directory's and ipx's Rust tests pass; k6 parses each load script. The
load tests themselves did not run: they build a release binary outside
the capped builder. .agents/skills/security-audit is an installed
skill's, not ours, and is left as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 02:23:22 +00:00
parent b509ea4b35
commit f8408a3d26
45 changed files with 692 additions and 531 deletions

View File

@@ -128,8 +128,8 @@ running the command and kills the session (exit 144), which has happened more th
There are three pages: the app (`web/index.html`), the admin page (`web/admin.html`, sent to
admins only) and sign-in (`web/login.html`). The app and admin pages share one stylesheet,
`web/app.css`, and their script is TypeScript in `web/src/`; `web/build.mjs` lists which files
make up each page's script. The page is its own image, **`ipx-web`** (#203): `web/build.mjs` uses
`web/app.css`, and their script is TypeScript in `web/src/`; `web/build.mts` lists which files
make up each page's script. The page is its own image, **`ipx-web`** (#203): `web/build.mts` uses
swc to strip the types and minify each script (`app.js`, `admin.js`, `login.js`), minifies the
pages, and puts them with the icons and typeface in `web/dist`, which Caddy serves
(`web/Caddyfile`, `Dockerfile.web`), passing `/api` and `/media` to ipx on the same origin. ipx
@@ -143,23 +143,35 @@ not signed in to `/login` and anyone but an admin away from `/admin` (and refuse
`/admin.js`); `/api/login-check` sends someone Cloudflare Access vouches for past the form; a page
asked for with `?token=` goes to `/api/signin`, which sets the cookie. The page is the same file
for everyone, so what ipx used to write into it per person is the page's own job now:
`web/boot.js`, in `<head>`, puts the theme from its cookie and an admin's link from the last visit
`web/boot.ts`, in `<head>`, puts the theme from its cookie and an admin's link from the last visit
on the page before the first frame, and `/api/me` brings the rest (version, the account's old
theme).
The files in `web/src` are not modules. They are one script split up, concatenated in the order
`web/build.mjs` lists them, sharing one top-level scope as the single inline script did; a new
`web/build.mts` lists them, sharing one top-level scope as the single inline script did; a new
file goes into that list. Top-level names are kept as they are, because markup calls some by
name (`onclick="closeModal()"`) and the browser tests reach others through `page.evaluate`.
After any edit to it:
```sh
npx tsc -p . && npx tsc -p tsconfig.sw.json
node tests/page-smoke.js
node --test tests/page-unit.js
npm run -s typecheck
node tests/page-smoke.ts
node --test tests/page-unit.ts
```
**All the code here is TypeScript** (#392): the page, the service worker, the tests, the build, the
home page's generator and the load tests. Node runs a `.ts` file as it is, by stripping the types,
so it takes only TypeScript that erases (`erasableSyntaxOnly`: no enums, namespaces or constructor
parameter properties) and imports name their `.ts` files; k6 strips its own. What a browser loads
on its own is compiled by `web/build.mts` with swc (`boot.ts`, the worker). The directory's
`site.js` is compiled from `crates/directory/src/site.ts` and committed, since its build has no
Node: after editing `site.ts`, `node web/build.mts --directory-script`, which
`node tests/directory-script.ts` checks. `npm run -s typecheck` checks five programs, each with its
own globals: the page (`tsconfig.json`, which also writes the page's declarations to
`target/page-types` for the browser tests' `page.evaluate` calls), the worker, Node's
(`tsconfig.tools.json`), and k6's (`tsconfig.k6.json`).
The service worker that keeps iPX readable offline (#389) is TypeScript too, `web/sw/sw.ts`,
a script of its own built to `/sw.js`, and checked by `tsconfig.sw.json` against a worker's
globals, which cannot share a program with the page's. It goes to the network first for
@@ -188,13 +200,14 @@ cargo clippy --workspace --all-targets -- -D warnings # no warnings, ever
cargo deny check # known vulnerabilities, licences, sources (deny.toml); reads Cargo.lock, builds nothing
cargo machete # dependencies a crate lists and does not use; now and then, it can be wrong
cargo test # parsing, filters, retention, schedules, SQL, per-user state
npm run -s typecheck # web/src, the service worker, and the tests' and build's TypeScript
node tests/page-smoke.js
node --test tests/page-unit.js # the page's own functions, called directly, in a second
node tests/native-bridge.js # the page hands playback to a native shell
node tests/contrast.js # every theme's palette against WCAG AA
npm run -s typecheck # the page, the service worker, the tests', build's and load tests' TypeScript
node tests/page-smoke.ts
node tests/directory-script.ts # the directory's site.js is its site.ts as it stands
node --test tests/page-unit.ts # the page's own functions, called directly, in a second
node tests/native-bridge.ts # the page hands playback to a native shell
node tests/contrast.ts # every theme's palette against WCAG AA
npx playwright test # the browser tests, against a real daemon on fixture feeds
node tests/load/run.js # k6: many people at once against a scratch daemon with 1,500 feeds
node tests/load/run.ts # k6: many people at once against a scratch daemon with 1,500 feeds
sh scripts/coverage.sh # which Rust lines the cargo tests reach, capped like a build (#390)
npm run coverage:page # which lines of web/src the browser tests reach (#390)
```
@@ -264,7 +277,7 @@ seeking through files, a flood of wrong passwords. Things about them:
* Each threshold is a budget well above what the request takes now: it is there to catch a query
that has started asking once per feed, or writers queueing on a lock, not a slow minute.
* `ipx status` runs every second throughout, and a run fails if one takes 5s.
* One at a time: `node tests/load/run.js signin`. All four take about six minutes.
* One at a time: `node tests/load/run.ts signin`. All four take about six minutes.
* The daemon is on **SQLite** unless `--postgres`, which puts it in `IPX_TEST_DATABASE_URL`, in
a schema of its own (`ipx_load`) dropped and made again each run. That URL, in `/src/.envrc`, is
`ipodderx_test` on the `databases` project's server, production's, as `ipodderx`, taken from