Skip to content

Migrate the web app to Next 16, eslint-config-next 16, and TypeScript 6 - #332

Merged
samrusani merged 1 commit into
mainfrom
codex/next16-web-migration
Jul 28, 2026
Merged

Migrate the web app to Next 16, eslint-config-next 16, and TypeScript 6#332
samrusani merged 1 commit into
mainfrom
codex/next16-web-migration

Conversation

@samrusani

Copy link
Copy Markdown
Owner

Replaces #300, #239 and #214, which cannot be merged separately.

Why one PR

  • TypeScript 6 fails under Next 15: app/layout.tsx raises TS2882 on the side-effect import of globals.css. It only typechecks once Next 16 is present.
  • Next 16 requires eslint-config-next 16, which ships native flat config. The current FlatCompat bridge dies in the eslintrc validator with a circular structure error. eslint.config.mjs now imports eslint-config-next/core-web-vitals directly.
  • That enables react-hooks/set-state-in-effect, which surfaces 13 real violations.

The 13 hook fixes

Fixed on their merits with lazy useState initializers or guarded render-time state adjustment, across vnext-brain-workspace (7), calendar-event-ingest-form (2), gmail-message-ingest-form (2), approval-detail (1) and workflow-memory-writeback-form (1).

Zero suppressions: no disable comments, no rule downgrade, no lint-glob narrowing. The rule is enforced and pnpm lint is clean.

Mount-path equivalence was checked per fix, because useEffect fires on mount and a render-time guard does not. In each case the initializer already holds the value the effect would have set.

Behaviour

One deliberate difference. On initial live mount the status line stays "Loading live vNext workspace from the trusted API." for the whole request instead of flipping to "Refreshing live vNext workspace...". The spinner, aria-busy and disabled states are unchanged because isRefreshing initializes from liveModeReady. Reporting a first load as a refresh was the weaker prior behaviour.

No existing test assertion was deleted, softened or retargeted. One assertion was extracted to a variable and is now asserted three times rather than once, proving the browser-clipper capability survives clip edits and URL changes.

Bundle budget

Next 16 adds about 43.7 KB gzip of framework weight to every route. Budgets are raised by a uniform 44,000 bytes so the gate keeps its previous headroom rather than concealing the growth. Measured by building both the base commit and this branch, in gzip bytes:

Route Before After Growth Old headroom New headroom
/ 106247 149940 +43693 13753 14060
/continuity 113660 156942 +43282 16340 17058
/vnext 137901 181747 +43846 17099 17253

check-bundle-budget.mjs reads either the legacy app build manifest or Next 16's .next/diagnostics/route-bundle-stats.json.

Generated config

The tsconfig.json switch to "jsx": "react-jsx" and the .next/dev/types/**/*.ts include are written by Next 16 itself during build. Verified by reverting both and rebuilding, which restores them. Kept rather than fought.

Verification

All eight web gates run individually: test, test:coverage:core, test:coverage:vnext, typecheck, lint, build, test:budget, test:advisory-audit. Both advisory audit invocations pass (93 production packages clean; 553 full, one live allowlisted exception, zero unexcepted at high). Full Python unit suite passes at 4102 passed and 2 skipped.

Application versions stay at 0.14.0.

Known and out of scope

Next 16 warns that the middleware file convention is deprecated in favour of proxy. The build passes. Renaming it is not required by this migration and is left for a separate change.

These three cannot land separately. TypeScript 6 fails under Next 15 because
app/layout.tsx reports TS2882 on the side-effect import of globals.css, and it
only typechecks once Next 16 is present. Next 16 in turn requires
eslint-config-next 16, which ships native flat config, so routing it through
FlatCompat now dies inside the eslintrc validator with a circular structure
error. eslint.config.mjs imports eslint-config-next/core-web-vitals directly
instead.

That combination enables react-hooks/set-state-in-effect, which found 13 real
violations across five components. All 13 are fixed on their merits using lazy
useState initializers or guarded render-time state adjustment. No disable
comment, rule downgrade, or lint-glob narrowing was added. Mount-path
equivalence was checked per fix, since useEffect runs on mount and a
render-time guard does not; in each case the initializer already holds the
value the effect would have set.

One behaviour difference remains and is deliberate. On the initial live mount
the status line now reads "Loading live vNext workspace from the trusted API."
for the whole request, where it previously flipped to "Refreshing live vNext
workspace...". The spinner, aria-busy, and disabled states are unchanged
because isRefreshing initializes from liveModeReady. Reporting a first load as
a refresh was the older, weaker behaviour.

Next 16 grows first-load JS by about 43.7 KB gzip on every route, which is
framework weight rather than application code. The route budgets are raised by
a uniform 44,000 bytes so the gate keeps its previous headroom instead of
hiding the growth. Measured before and after, in gzip bytes:

  /             106247 -> 149940, headroom 13753 -> 14060
  /continuity   113660 -> 156942, headroom 16340 -> 17058
  /vnext        137901 -> 181747, headroom 17099 -> 17253

check-bundle-budget.mjs now reads either the legacy app build manifest or Next
16's .next/diagnostics/route-bundle-stats.json, so it works across both.

The tsconfig jsx switch to react-jsx and the .next/dev/types include are both
written by Next 16 itself during build, verified by reverting them and
rebuilding, so they are kept rather than fought.

Closes #300
Closes #239
Closes #214

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@samrusani
samrusani merged commit 25043b4 into main Jul 28, 2026
15 checks passed
@samrusani
samrusani deleted the codex/next16-web-migration branch July 28, 2026 13:30
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