Skip to content

Web upgrade: Next 16 + React 19 + MSAL v4 - #114

Merged
okieselbach merged 4 commits into
mainfrom
chore/web-next16-react19-msal4
Jul 30, 2026
Merged

Web upgrade: Next 16 + React 19 + MSAL v4#114
okieselbach merged 4 commits into
mainfrom
chore/web-next16-react19-msal4

Conversation

@okieselbach

Copy link
Copy Markdown
Owner

Summary

Aligns the declared React with what the app router actually runs (Next-vendored React 19) and moves to current majors: next 16.2.12, react/react-dom 19.2.8, @types/react 19, msal-browser 4.30.0, msal-react 3.0.29, eslint-config-next 16.

  • MSAL deliberately stops at v4. msal-react 3.0.29 is the React-19 bridge that only needs msal-browser 4; the only code change is the v4 rename allowNativeBroker -> allowPlatformBroker. msal v5 (COOP redirect bridge, per-origin bridge pages, Entra app-registration redirect-URI changes, storeAuthStateInCookie removal) is a separate future project.
  • The react-leaflet peer overrides hotfix (9739d27) is removed — react 19 satisfies react-leaflet 5 natively.
  • React 19 code fixes: three zero-arg useRef() call sites, MutableRefObject -> RefObject in 5 hooks.
  • data-scroll-behavior="smooth" on <html> — Next 16 no longer suppresses CSS smooth-scroll during navigation; without it every route change would animate to top.
  • next-env.d.ts is now gitignored (Next 16 rewrites it per tool and flips the routes import between .next/types and .next/dev/types, next-env.d.ts toggles between .next/dev/types/routes.d.ts and .next/types/routes.d.ts even with typedRoutes: false vercel/next.js#86001; official docs say untrack). CI runs npx next typegen before tsc so fresh checkouts type-check.
  • next lint was removed in 16: lint script is now eslint . with a minimal flat config; the 264 pre-existing findings are untouched and lint stays out of CI (as before).

Verification

  • Exact CI sequence green from a clean tree: fresh npm ci -> next typegen -> tsc --noEmit (0) -> vitest 611/611.
  • Turbopack production build (out/) has an identical route set vs. the pre-upgrade baseline (161 files; Next 16 additionally emits /_not-found/* — additive).
  • Playwright smoke against next dev, including a full authenticated pass (dev app registration): sign-in request carries x-client-VER=4.30.0, silent completion -> dashboard with live data (token acquisition works); sidebar client-nav without full-page reload (guards the 1593232 fix this deploy also ships); real geo map renders and survives navigate-away-and-back with no "Map container is already initialized"; recharts (/sla), inspector, dark mode OK.

Post-merge checks

  • SWA build log: first Oryx build with Turbopack. Contingency only if it fails there: switch build script to next build --webpack.
  • Prod login on both origins (www <-> portal), geo map, sidebar navigation (/.../index.txt 200 on clicks).

Rollback: single revert — no infra, SWA-config, or app-registration changes involved.

🤖 Generated with Claude Code

okieselbach and others added 4 commits July 30, 2026 23:14
react 19.2 aligns the declared dependency with the React the app router
has been running all along (Next-vendored 19 canary). msal-react 3.0.29
is the React-19-compatible bridge that only needs msal-browser 4 —
deliberately NOT msal v5, whose COOP redirect bridge requires new
bridge pages per origin plus app-registration changes (separate
project). The react-leaflet peer overrides are gone: react 19
satisfies react-leaflet 5 natively.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- msalConfig: allowNativeBroker -> allowPlatformBroker (v4 rename);
  storeAuthStateInCookie stays (deprecated in v4, removed only in v5)
- zero-arg useRef() removed in @types/react 19: GlobalSearch (2x),
  dashboard autoLoadTimerRef
- React.MutableRefObject -> React.RefObject in 5 session/progress hooks
  (all refs are mutable in 19 types)
- layout: data-scroll-behavior="smooth" opt-in so route changes keep
  jumping instead of animating (Next 16 stopped suppressing CSS
  smooth-scroll during navigation)
- next.config: analyzer comment - Turbopack default build skips the
  webpack-only analyzer, use --webpack for ANALYZE runs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Next 16 rewrites next-env.d.ts on every dev/build run and the routes
import inside flips between .next/types and .next/dev/types (vercel/
next.js#86001) - official guidance is to gitignore it. CI now runs
`next typegen` before tsc so the type stubs exist on a fresh checkout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Minimal flat config from eslint-config-next@16 (core-web-vitals +
typescript) with build-output ignores. Lint stays out of CI as before;
the 264 pre-existing findings are untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@okieselbach
okieselbach merged commit cf45911 into main Jul 30, 2026
9 checks passed
okieselbach added a commit that referenced this pull request Jul 30, 2026
#115)

typedRoutes: true (stable in Next 16; `next typegen` already runs in CI
before tsc since #114). A typo'd <Link>/router.push target now fails the
build instead of 404ing at runtime — the failure class behind the recent
RSC/routing incidents.

- lib/routes.ts: registry helpers return `Route`; withQuery validates the
  base path literal. New: deviceBlockUrl (OpsEventsSection previously
  hand-built /admin/security/device-block against the registry rule),
  route() to compile-check dynamic-section literals (nav configs,
  section index redirects), trustedRoute() as the single named seam for
  runtime-data hrefs (backend-emitted notification deep links, persisted
  post-login return URL).
- Nav types (NavItem, ExpandableSubItem, PageSectionItem) carry Route
  hrefs — every sidebar/navbar entry is compile-checked.
- ClientRedirect is generic so dynamic-section literals infer.

Verified: tsc clean; negative test (typo'd static + dynamic routes)
fails compilation as intended; 611/611 vitest; static export builds
with an unchanged route list.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
okieselbach added a commit that referenced this pull request Jul 30, 2026
* feat(web): typed routes — broken internal links are now compile errors

typedRoutes: true (stable in Next 16; `next typegen` already runs in CI
before tsc since #114). A typo'd <Link>/router.push target now fails the
build instead of 404ing at runtime — the failure class behind the recent
RSC/routing incidents.

- lib/routes.ts: registry helpers return `Route`; withQuery validates the
  base path literal. New: deviceBlockUrl (OpsEventsSection previously
  hand-built /admin/security/device-block against the registry rule),
  route() to compile-check dynamic-section literals (nav configs,
  section index redirects), trustedRoute() as the single named seam for
  runtime-data hrefs (backend-emitted notification deep links, persisted
  post-login return URL).
- Nav types (NavItem, ExpandableSubItem, PageSectionItem) carry Route
  hrefs — every sidebar/navbar entry is compile-checked.
- ClientRedirect is generic so dynamic-section literals infer.

Verified: tsc clean; negative test (typo'd static + dynamic routes)
fails compilation as intended; 611/611 vitest; static export builds
with an unchanged route list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(web): lint findings to zero errors + CI lint gate

Package deal — fixes only land together with the gate, otherwise they
drift right back. CI web job now runs `eslint . --max-warnings 163`:
errors fail outright, the warning cap is a ratchet (lower it when fixing
warnings, never raise it).

Fixed to zero (mechanical categories, behavior unchanged):
- no-unused-vars (49): dead imports/locals removed; where a prop is part
  of a live call-site contract, only the destructured binding dropped
- no-explicit-any (29): real structural types for event payloads
  (EnrollmentEvent.data et al -> Record<string, unknown> + per-event
  interfaces), SignalR passthroughs typed via the library's own
  signatures; five consumer files got typing-only follow-through
- no-unescaped-entities (15), prefer-const, jsx-no-comment-textnodes
- no-require-imports (5): scripts/**/*.js override — Node CJS scripts,
  require() is correct there
- one stale eslint-disable directive removed

Deliberately NOT fixed inline: the React-Compiler hooks rules new in
eslint-plugin-react-hooks v6 (set-state-in-effect 93, refs 26,
static-components 14, purity 6, immutability 5,
preserve-manual-memoization 3) are demoted to warn in eslint.config.mjs —
each is a per-site behavioral refactor in auth/SignalR-adjacent code, to
be burned down incrementally under the ratchet.

Verified: eslint 0 errors / 163 warnings (gate command exits 0), tsc
clean, vitest 611/611, static export builds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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