⬆️ chore(frontend): upgrade React Router to 8.3.0 - #425
⬆️ chore(frontend): upgrade React Router to 8.3.0#425quentinlebourles-packmind wants to merge 13 commits into
Conversation
…e timers @testing-library/dom gates fake-timer support on a `jest` global existing, so under Vitest waitFor polled on real timers while the fake clock was installed and 8 tests died at the 5s timeout. Shim only the one method it calls, so a stray jest.fn() reintroduced later still fails loudly.
…onfig Nx infers `test` from vite.config.ts via the @nx/vitest plugin already registered in nx.json, so removing the explicit project.json targets is what flips the runner. Carries testTimeout 15000 over from the retired jest config; Vitest defaults to 5s, which the heavier component suites exceed under load.
The two upload-count assertions sampled the mock immediately after the click, while the batch was still in flight: each skill's files are read through FileReader before its request goes out, so the call landed an event-loop turn or more later. Under full-suite parallelism that turn often came too late and the count read 0. Both now wait for the summary the panel renders once the batch has settled, which also makes them stricter — a spurious extra upload can no longer slip past an early sample. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v8 removes AppLoadContext — the load context is a RouterContextProvider under the middleware API — and v8_middleware became default behaviour, so the future flag no longer exists as an option. Clears GHSA-qwww-vcr4-c8h2; pnpm audit --prod now reports no known vulnerabilities. The upgrade was blocked until now because v8 ships ESM only and ts-jest emitted CJS, which failed on import.meta.hot in 27 of 82 suites.
Vitest's precise Mock<Procedure> rejected the partial mutation stubs that jest's loose jest.Mock had accepted, surfacing 20 TS2352 errors in the GitHub App specs. Construct real members of the UseMutationResult discriminated union so the casts hiding the mismatch are unnecessary. - add createIdleMutationResult/createFailedMutationResult test helpers - drop the 20 `as ReturnType<typeof useX>` assertions they make redundant Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`to` was declared on IPMLinkProps but destructured away and never forwarded, so `<PMLink to="/x">` rendered an anchor with no href. The prop invited callers to nest a router link inside PMLink instead, which emits invalid `<a>` inside `<a>` markup. Removing it makes the compiler point callers at `asChild`, the supported way to render as a router link. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The "Sign up" link wrapped a react-router `Link` in a plain `PMLink`, so Chakra's own `<a>` and the router's `<a>` both rendered, tripping React's "<a> cannot be a descendant of <a>" hydration warning. Nested anchors also make screen readers and keyboard navigation behave unpredictably. `asChild` renders the Chakra link *as* the router link instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
React rejects camelCase custom props on DOM elements, so every render of the sign-up form logged "React does not recognize the `data-testId` prop" twice. The ids themselves were fine — setAttribute lowercases attribute names on HTML elements, so the DOM got `data-testid` either way and the e2e page object kept working — but the warning was pure noise. Renamed the five props in SignUpWithOrganizationForm and the latent one in PMPageSection, whose `headerDataTestId` has no caller yet and would have warned the moment one arrived. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The nested-anchor fix was branched from main, which predates the Vitest migration, so its new spec arrived using jest.mock. Rename it and drop the __esModule marker, which is a CJS interop artifact with no meaning under Vitest.
Greptile SummaryThe PR upgrades the frontend to React Router 8 and migrates its Jest suite to Vitest so the ESM-only router can be tested natively.
Confidence Score: 5/5The PR appears safe to merge; no concrete changed-code defect remains after reviewing the router upgrade, test-runner integration, and shared-link API change. The frontend test target is discoverable from the Vitest-enabled Vite configuration, active test locations are included, React Router 8 compatibility changes match the new API, and all current PMLink callers preserve valid navigation semantics. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart LR
Nx["Nx test target"] --> Vitest["Vitest 4"]
Vitest --> Vite["Frontend Vite config"]
Vite --> Router["React Router 8 ESM modules"]
Vitest --> Setup["jsdom test setup"]
Setup --> Tests["src/** and app/** suites"]
App["Frontend application"] --> Router
App --> UI["@packmind/ui"]
UI --> PMLink["PMLink asChild composition"]
Reviews (1): Last reviewed commit: "✅ test(frontend): run the new sign-in ro..." | Re-trigger Greptile |
|



What
Upgrades
react-routerto 8.3.0, which required first migratingapps/frontendfrom Jest to Vitest 4.1.10.pnpm audit --prodnow reports no known vulnerabilities (GHSA-qwww-vcr4-c8h2 cleared).Why the runner migration came first
react-router 8 ships ESM only —
"type": "module", no.cjsanywhere indist/.apps/frontendran Jest through ts-jest, which emits CommonJS, so it died onimport.meta.hotindist/production/lib/dom/ssr/routeModules.js: 27 of 82 suites failed as collection errors, zero assertion failures.@swc/jestdoes not help — verified by direct probe that SWC's commonjs transform leavesimport.metaintact.Vitest was chosen over Jest's ESM mode because
vi.mockhoists exactly likejest.mock. Jest ESM has no mock hoisting, so all 113jest.mock()calls across 45 files would have needed restructuring intojest.unstable_mockModuleplus dynamicawait import(). Vitest also resolves the ESM-only dependency natively and reuses thevite.config.tsaliases, letting thepathsToModuleNameMapperduplication injest.config.tsbe deleted outright.Scope:
apps/frontendonly. Everypackages/*project and every other app stays on Jest 30.The v8 code changes
Only two were needed:
AppLoadContext→RouterContextProviderinapp/entry.server.tsx(v8 removes the type; under the middleware API the load context is aRouterContextProvider)v8_middlewarefromreact-router.config.ts(became default behaviour; no longer exists as an option)Verification
apps/frontend(Vitest)packages/ui(still Jest)apps/apipnpm audit --prodThe Jest baseline before migration was 82 files / 1066 tests. Nothing was dropped, skipped, or weakened — the delta is new tests added by the bundled fixes below.
Also verified by hand in a browser against the dockerised stack: app boots under v8, deep-linked routes resolve directly, zero console errors.
Bundled fixes
Two related fixes were folded in rather than left dangling:
PMLinknested anchors (packages/ui) —towas a dead prop: typed, accepted, silently discarded. Callers passed it believing it navigated, then wrapped a routerLinkinside to compensate, producing invalid<a>inside<a>and a React hydration error on/sign-in. Removing the prop forcesasChild. Confirmed fixed in the live DOM (document.querySelectorAll('a a').length === 0) with the sign-up link still resolving correctly. Note: this is technically a breaking change to@packmind/ui, but no other caller passedto— frontend typecheck, ui build, and all 164 ui tests pass.Mock<Procedure>correctly rejects partial mutation stubs that jest's looseMock<any, any>accepted. Fixed by constructing real members of theUseMutationResultunion rather than casting the mismatch away.Gotchas worth knowing (documented in the plugin knowledge banks)
viis global underglobals: true, but the types are not —import type { Mock, Mocked, MockedFunction } from 'vitest'.vi.importActualis async; a synchronous factory is a parse error and fails the whole file.reactRouter()is gated behind!process.env.VITEST— its Fast Refresh preamble needs a real browser document and killed 54 component suites under jsdom.src/test-setup.tsshimsglobalThis.jest = { advanceTimersByTime }.@testing-library/domgates fake-timer support on ajestglobal existing, so without itwaitForpolls on real timers and times out. Deliberately one method, not an alias ofvi, so a strayjest.fn()still fails loudly.src/**+app/**.tests/and the gitignored.react-router/are deliberately excluded.Known gaps (pre-existing, not introduced here)
file:lineon react-router 7 and 8 with everything else held constant. They have no passing e2e locally, so v8's behaviour on those specific flows is unverified — CI with credentials would close that.apps/frontend/tsconfig.spec.jsonis compiled by no nx target (nx typecheck frontendonly runstsc -p tsconfig.app.json). It reports 91 errors nothing checks. Down from 699 before this migration, but still an unguarded surface.Reviewer notes
The
✅ test(frontend):commits are a mechanical codemod and are best reviewed as a sequence rather than as a squashed diff. Two of them fix defects found mid-migration and are worth a closer look:await the import batch before counting skill uploads— fixes a ~50% flake where two tests sampled a mock while the batch was still in flight (files are read throughFileReaderbefore the request goes out). Root-caused by instrumentation, not guessed.shim advanceTimersByTime…— see the@testing-library/domnote above.If you pull this branch and
apps/apifails withCannot find module '@packmind/marketplaces', that is stale local state, not this PR — runnode scripts/select-tsconfig.mjsto regeneratetsconfig.base.effective.json.🤖 Generated with Claude Code