Skip to content

feat: the port to React, TypeScript and Vite, with its checks - #7

Merged
vlad-salone merged 8 commits into
mainfrom
feat/port
Oct 5, 2026
Merged

vlad-salone merged 8 commits into
mainfrom
feat/port

Conversation

@vlad-salone

@vlad-salone vlad-salone commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

What and why

All of ADR-0002's action items. site/ becomes the Vite project of D7, the page behaves as it did, the checks of D6 run in the browser job, and the docs describe the project as it is now.

  • React 19 and TypeScript under Vite 8, with the Rust React Compiler.
  • sqzer 0.3.0 from npm, bundled into the worker, the wasm served from the page's own origin. The footer's version comes from the package at build time.
  • The policy is written once in vite.config.ts and lands after the charset in the built index.html. jsDelivr is gone from it.
  • The worker, its client and the message types are in src/shared/api/sqzer/. The search is an XState machine in src/pages/compress/model/, with the worker as a callback actor.
  • pages.yml builds and uploads dist/. Dependabot watches the root, with sqzer on its own and the development tooling grouped.

It also adds ADR-0004, accepted, for two things that differ from ADR-0002.

The worker outlives a search. D10 ends the worker with every search. I built that first and measured it against main:

# a 4000x2250 JPEG, format `jpeg`, fixed quality, width 1600. the time from a change of
# quality to the result on the page, the 250 ms the controls rest included
                              Chromium   Firefox
the page on `main`            0.37 s     0.57 s
a worker per search, D10      0.95 s     1.15 s
this PR                       0.37 s     0.55 s

The worker now lives as long as the machine's open state and keeps the decoded image. Cancelling is re-entering open, which ends the worker, and that's still the only way an encode is cancelled.

Slices are merged until something is reused. Steiger's fsd/insignificant-slice flags every slice with one user, and with one route that's all of them. So the code sits in src/pages/compress/ under ui, model and lib.

The checks

pnpm typecheck      # two projects, `strict` plus `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess` and the rest of `tsconfig.base.json`
pnpm lint           # oxlint: correctness, suspicious, perf and pedantic as errors, type-aware, with the React Compiler rules of `eslint-plugin-react-hooks`
pnpm format:check   # oxfmt
pnpm fsd            # Steiger, default rules
pnpm test           # Vitest in browser mode, Chromium and Firefox: 18 tests in each
pnpm test:e2e       # Playwright against `vite build` served by `vite preview`, Chromium and Firefox: 5 tests in each
pnpm check          # all of the above
  • /selftest/ is src/shared/api/sqzer/decode-any.test.ts, on the page and in a worker. .github/check.mjs is tests/e2e/page.spec.ts. The fixtures moved to tests/fixtures/.
  • Every Playwright test fails on a page error, a console error, a securitypolicyviolation or a request to another origin.
  • Three rules are off in .oxlintrc.json, each with its reason next to it.

Note: oxfmt is not in ADR-0002 D6, which names no formatter. It's the formatter of the same project as oxlint, and Prettier is the fallback. The browser-check rule in .greptile/config.json names it, and it gets no record of its own.

Changes in behaviour

  • A worker that failed to start or to load is replaced on the next search. On main that case stays at "Reading".
  • The alt texts of the two pictures lose the word "image": jsx-a11y/img-redundant-alt.
  • The footer links licenses.txt, which the build writes: the licences of everything the bundle carries, sqzer in the worker included (ADR-0003 D7 and its item 5).
  • The status line is an <output>, whose implicit role is status, where it was a <p role="status" aria-live="polite">. It looks the same.

Docs

  • CLAUDE.md, README.md, CONTRIBUTING.md and this template describe pnpm, the layout under src/, pnpm check and the policy in vite.config.ts. The README's "Checking a browser" section goes with /selftest/.
  • SECURITY.md drops jsDelivr from the scope. The bug form asks for the version in the footer where it asked for the self-test.
  • Item 6, the note under ADR-0011 item 5, is docs(adr): note under ADR-0011 item 5 that the page has its own stack record sqzer#53. It merges after this one.

Browsers tried

Headless Chromium and Firefox from Playwright 1.63.0, locally and in CI. Before the suites existed, one script ran against the page on main and against the built port, and both printed the same statuses, summaries and output sizes for:

a JPEG with the defaults, with a width, to PNG, at a fixed quality
a quality the package refuses
an SVG at its own size and at width 480
a TIFF, shown through the worker's preview
a HEIC and a file that is no image
a change of format while a search runs
a drop

Safari isn't tried.

New dependencies

react, react-dom               19.3.0     MIT
xstate                         5.33.2     MIT
@xstate/react                  6.1.0      MIT
vite                           8.3.2      MIT          dev
@vitejs/plugin-react           6.1.1      MIT          dev
oxc-transform-react            0.145.0    MIT          dev
typescript                     7.0.2      Apache-2.0   dev
@types/react, @types/react-dom 19.3.0     MIT          dev
@types/node                    24.19.1    MIT          dev
oxlint                         1.86.0     MIT          dev
oxlint-tsgolint                7.0.2003   MIT          dev   the type-aware rules of oxlint
eslint-plugin-react-hooks      7.1.1      MIT          dev
oxfmt                          0.71.0     MIT          dev   not in ADR-0002
steiger                        0.7.0      MIT          dev
vitest                         5.0.3      MIT          dev
@vitest/browser-playwright     5.0.3      MIT          dev
vitest-browser-react           2.3.0      MIT          dev
@playwright/test, playwright   1.63.0     Apache-2.0   dev

All at exact versions, with pnpm-lock.yaml from pnpm 12.8.1.

Checklist

  • .github/check.mjs passes locally, or CI shows it green: it's pnpm check now
  • a path that depends on the browser got a line in /selftest/, if one changed: the checks moved to Vitest unchanged
  • a new ADR has its row in docs/adr/README.md and its entry in .greptile/files.json, if there is one
  • the Content-Security-Policy is unchanged, or the PR says why it had to change: cdn.jsdelivr.net leaves script-src and connect-src, as ADR-0002 D2 decided

🤖 Generated with Claude Code

ADR-0002 item 1. `site/` becomes the Vite project of D7: React 19 with the
Rust React Compiler, `sqzer` 0.3.0 from npm served from the page's own
origin, the Content-Security-Policy written once in `vite.config.ts` with
jsDelivr gone. The worker, its client and the message types are in
`src/shared/api/sqzer/`, and the search is an XState machine with the
worker as a callback actor. The page behaves as it did.

ADR-0004, proposed: the worker outlives a search, as on `main`, and slices
are merged into `src/pages/compress/` until something is reused. A worker
per search, as ADR-0002 D10 had it, made a change of quality on a held
image take 0.95 s where `main` takes 0.37 s.

`check.yml`, `pages.yml` and `dependabot.yml` still name `site/`: items 2
to 4 of ADR-0002.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[High risk] Rebuilds the entire build system and toolchain.

The PR appears safe to merge; no outstanding finding or new actionable defect was established.

Summary

The PR ports the page to React, TypeScript and Vite, moves browser checks into Vitest and Playwright, and builds the GitHub Pages artifact. Since the previous review, it also generates and links bundled-dependency licence notices.

Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A["Vite build"] --> B["Page bundle and generated licences"]
  A --> C["Worker bundle: sqzer"]
  C --> D["Append sqzer notice"]
  B --> D
  D --> E["dist/licenses.txt"]
  E --> F["Footer link"]
Loading

Reviews (7) · Last reviewed commit: "feat: the build ships `licenses.txt`, li..."

Comment thread src/pages/compress/model/machine.ts Resolved
Comment thread package.json Resolved
@greptile-apps

greptile-apps Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Comments Outside Diff

These findings could not be posted inline.

  • P1 Browser check targets deleted self-test site/selftest/index.html:1 ▶

    The pull-request browser job still serves site/, opens /selftest/, and reads a fixture from that directory. This change removes those files without wiring in the replacement browser suites, so the required check cannot pass. The browser-check requirement calls for a green browser job that covers these browser-dependent paths.

  • P1 Deployment still uploads deleted site site/index.html:1 ▶

    On the next merge, pages.yml will still upload site/ without running a build. This change deletes that page and puts the Vite entry at the repository root, so the workflow never publishes the new page from dist/ and deployment breaks.

ADR-0002 items 2 to 4. The `browser` job runs `tsc` over two strict
projects, oxlint with its type-aware rules and the React Compiler rules
of `eslint-plugin-react-hooks`, `oxfmt --check`, Steiger, Vitest in
browser mode and the Playwright suite, the last two in Chromium and
Firefox.

`/selftest/` is `decode-any.test.ts`, on the page and in a worker, and
`.github/check.mjs` is `tests/e2e/page.spec.ts`: one JPEG through the
built page, a change of format during a search, the policy, and every
request on the page's own origin. The search machine and the controls
have tests of their own.

`pages.yml` builds and uploads `dist/`. Dependabot watches the root,
with the development tooling grouped. `.vscode/` has the settings for
the same tools.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vlad-salone vlad-salone changed the title feat: the port to React, TypeScript and Vite feat: the port to React, TypeScript and Vite, with its checks Oct 4, 2026
vlad-salone and others added 2 commits October 4, 2026 21:01
Its implicit role is `status`, so the explicit `role` and `aria-live`
go, and `jsx-a11y/prefer-tag-over-role` is back on. The stylesheet gives
it the block and the margin the paragraph had.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ADR-0002 item 5. `CLAUDE.md`, `README.md`, `CONTRIBUTING.md` and the PR
template describe the project as it is now: pnpm, the layout under
`src/`, `pnpm check`, the policy in `vite.config.ts`. The README's
"Checking a browser" section goes with `/selftest/`.

`SECURITY.md` drops jsDelivr from the scope, and the bug form asks for
the version in the footer where it asked for the self-test. Items 5 and
7 of ADR-0002 are ticked.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
vlad-salone and others added 3 commits October 4, 2026 21:11
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…r own

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread package.json Resolved
ADR-0003 D7 and its item 5. Vite lists the licences of what the page's
bundle carries, React and XState among them, and a small plugin adds
`sqzer`, which is in the worker's bundle and so not on that list.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vlad-salone
vlad-salone merged commit 3a41c3f into main Oct 5, 2026
2 checks passed
@vlad-salone
vlad-salone deleted the feat/port branch October 5, 2026 13:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant