diff --git a/CLAUDE.md b/CLAUDE.md index f8d575a..b9190d8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 `
`, puts the theme from its cookie and an admin's link from the last visit +`web/boot.ts`, in ``, 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 diff --git a/Dockerfile.directory b/Dockerfile.directory index 4c89e80..3dbe3f8 100644 --- a/Dockerfile.directory +++ b/Dockerfile.directory @@ -1,5 +1,5 @@ # ipx-directory (#204). Built from the repo's root: docker buildx build -f Dockerfile.directory . -# No node here: only ipx's own build runs web/build.mjs. +# No node here: only ipx's own build runs web/build.mts. FROM rust:1-slim-trixie AS build # SQLite is compiled from source for sqlx, which needs a C toolchain. RUN apt-get update && apt-get install -y --no-install-recommends build-essential \ diff --git a/Dockerfile.web b/Dockerfile.web index b84d0a4..c1187ce 100644 --- a/Dockerfile.web +++ b/Dockerfile.web @@ -1,11 +1,11 @@ # ipx-web (#203): the frontend, built from web/ and served by Caddy, which passes the API to ipx. # Built from the repo's root: docker buildx build -f Dockerfile.web . -FROM node:22-slim AS build +FROM node:24-slim AS build WORKDIR /src COPY package.json package-lock.json ./ RUN npm ci --omit=dev COPY web ./web -RUN node web/build.mjs /dist +RUN node web/build.mts /dist FROM caddy:2 # caddy:2's Alpine ships curl, which nothing here runs (no healthcheck since #218): out, so a diff --git a/README.md b/README.md index 92d6e1f..1787d42 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,8 @@ The UI is plain HTTP, so put TLS in front of it if it is reachable from outside ```sh cargo test # the engine: parsing, filters, retention, schedules, SQL, per-user state -node tests/page-smoke.js # the page script loads without throwing -node tests/native-bridge.js # the page hands playback to a native shell +node tests/page-smoke.ts # the page script loads without throwing +node tests/native-bridge.ts # the page hands playback to a native shell npx playwright test # a real browser against a real daemon on fixture feeds ``` diff --git a/crates/directory/src/site.js b/crates/directory/src/site.js index 88037d8..8f900c8 100644 --- a/crates/directory/src/site.js +++ b/crates/directory/src/site.js @@ -1,10 +1,17 @@ +// Made from site.ts by `node web/build.mts --directory-script`: edit that, not this. // The one thing the directory's pages script (#215): Copy puts a feed's address on the clipboard. // Without it, the address is in a field of its own to select by hand. -document.addEventListener('click', ev => { - const b = ev.target.closest('[data-copy]'); - if (!b || !navigator.clipboard) return; - navigator.clipboard.writeText(b.dataset.copy).then(() => { - b.textContent = 'Copied'; - setTimeout(() => { b.textContent = 'Copy'; }, 1500); - }); +// +// The source: the directory serves site.js, made from this by `node web/build.mts --directory-script` +// (#392), since its build has no Node to make it; tests/directory-script.ts checks the two agree. +document.addEventListener('click', (ev)=>{ + const b = ev.target?.closest('[data-copy]'); + // Only in a secure context; plain HTTP has no clipboard. + if (!b || !navigator.clipboard) return; + void navigator.clipboard.writeText(b.dataset.copy ?? '').then(()=>{ + b.textContent = 'Copied'; + setTimeout(()=>{ + b.textContent = 'Copy'; + }, 1500); + }); }); diff --git a/crates/directory/src/site.ts b/crates/directory/src/site.ts new file mode 100644 index 0000000..0f27117 --- /dev/null +++ b/crates/directory/src/site.ts @@ -0,0 +1,14 @@ +// The one thing the directory's pages script (#215): Copy puts a feed's address on the clipboard. +// Without it, the address is in a field of its own to select by hand. +// +// The source: the directory serves site.js, made from this by `node web/build.mts --directory-script` +// (#392), since its build has no Node to make it; tests/directory-script.ts checks the two agree. +document.addEventListener('click', (ev) => { + const b = (ev.target as Element | null)?.closest