Repository navigation
build(deps): migrate toolchain to vite 8, vitest 5, TS 6, eslint 10; CI to Node 24 - #239
Merged
obvious-autobuild-staging[bot] merged 2 commits intoNov 1, 2026
Conversation
Node 24 in CI (vitest 5 requires ^22.12 || ^24 || >=26), dev toolchain majors per the approved dependency-update blueprint, React 18 unchanged (glide-data-grid peer cap). Conversion deps stay pinned; postinstall patches re-verified idempotent on the fresh install. Deviation from the PR split: tailwindcss + @tailwindcss/vite move to ^4.3.3 here instead of PR 2 — @tailwindcss/vite@4.1.0 peers vite ^5.2 || ^6 || ^7 and cannot resolve against vite 8, so the pair is version-locked to this migration. PR 2 keeps the runtime minors. Also: react-hooks v7's flat-native configs.*.flat["recommended-latest"] replaces the eslintrc-shaped recommended-latest; its two new compiler-era rules (refs, set-state-in-effect) flag the app's deliberate latest-value-mirror refs and derived-state-resync effects — off with a config comment until React 19 unlocks useEffectEvent. useMediaQuery is rewritten on useSyncExternalStore (the canonical set-state-in-effect fix); TS 6 drops deprecated baseUrl (paths is tsconfig-relative) and declares types: ["node"] explicitly. Gates: eslint clean, tsc -b clean, 612/612 tests green, vite 8 build + prerender committed to app/dist, verify-seo.sh green, php -l + verify-shim.sh green. .obvious/obvious.md corrected: React 18 (not 19) with the glide-data-grid peer reason. Co-authored-by: David Boskovic <david@obvious.ai>
The vite 8 / rolldown compressor preserves the side-effect-free Intl.Segmenter member-access check that @codemirror/state emits for the browser grapheme API; the vite 5 / esbuild minifier dropped it. The zero-tolerance remnant grep matched "Segment" and failed the shim job. Same strip-and-regrep discipline as the sanctioned banner URL: a real Segment analytics integration still trips the scan. Co-authored-by: David Boskovic <david@obvious.ai>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Every dev-toolchain dependency is at least one major behind, and the gap compounds: Dependabot re-raises the vitest major migration every few weeks (most recently #238, which now drags vite 5→8 across two majors) and each closure buys time, not headroom. This PR is the approved deliberate migration (blueprint art_5caGrGsI) — all toolchain majors in one review, after which Dependabot bumps return to patch/minor size.
What
Versions — the version-locked toolchain block from the blueprint, moved together:
CI moves to Node 24 (
actions/setup-nodein thecijob) — vitest 5 requires^22.12.0 \|\| ^24 \|\| >=26. React stays 18 (glide-data-grid peer cap; blueprint locked decision). Conversion deps stay pinned; postinstall patches re-verified idempotent on the fresh install. Runtime dependency minors stay with PR 2.Deviations from the blueprint's PR split, both forced:
@tailwindcss/vite@4.1.0peersvite ^5.2 \|\| ^6 \|\| ^7— npm cannot resolve the tree against vite 8 at all (no--legacy-peer-depsshortcuts). The pair is version-locked to this migration; PR 2 drops its tailwind line. This makes tailwind 4.1→4.3 the one CSS-affecting bump in PR 1 — layout was verified in the live preview (see evidence).refs,set-state-in-effect) are off, with a documented comment ineslint.config.js. The codebase passes the rest of the compiler suite cleanly (purity, immutability, set-state-in-render, …). The two rules flag the deliberate "latest-value mirror" refs (CsvTable's latestRef/viewRef, App's converterStateRef — event handlers must see the last completed render's props) and derived-state-resync effects. The sanctioned replacement (useEffectEvent) is a React 19-era API and React is pinned to 18 here; adopting these rules is its own future refactor, not a side effect of a toolchain bump.Migration fixes the majors required:
eslint.config.js:reactHooks.configs["recommended-latest"]→reactHooks.configs.flat["recommended-latest"]— under ESM, v7's top-level configs are eslintrc-shaped (plugins: ["react-hooks"]), which eslint 10 rejects in flat config.useMediaQueryrewritten onuseSyncExternalStore— the canonical fix for the newset-state-in-effectrule, behavior-preserving (matchMedia-less environments still getfalse, never re-render; the server snapshot keeps prerender on thefalsebranch).tsconfig.app.json: dropped deprecatedbaseUrl(TS 6 flags it;pathsis already tsconfig-relative) and added"types": ["node"]— the app project's Node ambient types previously rode in via baseUrl-based inclusion; now declared explicitly.convert.ts: rethrown conversion errors attach{ cause: e }(newpreserve-caught-errorrule), and a deadrecordsinitializer removed (newno-useless-assignmentrule)..obvious/obvious.md: corrected the stale "React 19" claim to React 18 with the glide-data-grid peer reason.Rebuilt
app/distcommitted — vite 8 (rolldown core) changes asset hashes; verified byte-deterministic across consecutive builds before committing.How to Review
app/scripts/prerender.mjsneeded no changes — vite 8's middleware-modessrLoadModulepath still works; the 40k-char prerender output is byte-identical across builds.eslint.config.js's rules block comment for the two deferred compiler rules — that's a judgment call worth a second opinion, not a mechanical migration step.Test Evidence
Upvote confirms end-to-end — Changelog Vote event fires after Postgres write (row: entry 24, vote 1), vote state persists

Changelog popout opens with entries rendered — Changelog Open manual event fires

Paste a CSV and convert — data grid renders under the vite 8 bundle, layout unchanged

All gates run locally under Node 24.22.2 before push:
npm run lint— clean (eslint 10.14.0)npx tsc -b --noEmit— clean (TS 6.0.3)npm test— 35 files, 612 tests passed, 0 failures (vitest 5.0.5 + jsdom 30.1.3)npm run build+ dist hash-guard simulated locally: committedapp/distdiffed byte-identical against a fresh buildverify-seo.sh— green (45,480 bytes prerendered).phpfile touched):php -lover all six files +verify-shim.shgreen locally (PHP 8.4.27)Dogfood preview: paste CSV → convert → copy output, pane flip, and the four Plausible manual events (changelog_open, changelog_vote, Input, Export) verified firing in manual mode — evidence attached to the task.
Human author: David Boskovic (david@obvious.ai)
🔗 Obvious Project · 🧵 Obvious Thread · 🔀 Obvious PR Artifact