Draft
Conversation
Contributor
|
Preview deployment: https://chorexupgrade-next-16.preview.avy-fx.org |
Closed
8 tasks
3 tasks
Major framework upgrade from 15.4.11. Run via @next/codemod@canary upgrade and additional manual fixes for breaking changes. Codemod changes (automated): - next 15.4.11 → 16.2.4, react/react-dom pinned to 19.2.5 - eslint-config-next + @next/eslint-plugin-next 15 → 16.2.4 - src/middleware.ts → src/proxy.ts (Next 16 rename) - tsconfig.json: jsx "preserve" → "react-jsx"; include .next/dev/types Manual fixes: - @sentry/nextjs 9.x → 10.x (peer dep needed for next 16) - eslint-plugin-react-hooks 5.x → 7.x (matches next 16's transitive) - revalidateTag(tag) → revalidateTag(tag, 'default') in 14 callsites: the second `profile` arg is required in Next 16 - src/app/(frontend)/globals.css and (embeds)/a3-globals.css: move @import above @tailwind directives — Turbopack strictly enforces the CSS spec rule that @import must precede other rules - eslint.config.mjs: import eslint-config-next as native flat config (FlatCompat shim no longer works); downgrade three new react-hooks rules (set-state-in-effect, refs, static-components) to warn so pre-existing patterns don't block the upgrade — fix in follow-up - jest.config.mjs: add .claude/worktrees/ to testPathIgnorePatterns so stale worktree copies don't pollute test runs Verified: pnpm tsc, pnpm lint (warnings only), pnpm test (40 suites / 376 tests), pnpm build, dev server boots on Turbopack and serves homepage with 200. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
029a927 to
d5dcbe1
Compare
- images.qualities: [75, 80] — Next 16 requires explicit allowlist
for any non-default Image quality. We use quality={80} on some
components, which was emitting a console warning on every image
request.
- Move Sentry's disableLogger and automaticVercelMonitors under the
webpack: { ... } key. Sentry 10 deprecated the top-level options
(logged a warning on dev startup), with no Turbopack equivalent
yet. The webpack-keyed form is what Sentry 10 wants now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The webpack ignore was added to suppress a "Critical dependency" warning from @opentelemetry/instrumentation that Sentry 9 surfaced through its dynamic require pattern (sentry-javascript#12077). Sentry 10 fixed the underlying pattern upstream. Verified by running `pnpm build` with and without the block — both produce 0 warnings and no OpenTelemetry mentions in the output. Removing the block also unblocks one of the two follow-ups noted in the Next 16 upgrade PR description: there's no longer a webpack-only config we'd need a Turbopack equivalent for. If we ever opt builds into Turbopack via `--turbopack`, there's nothing to port over. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
eslint-plugin-react-hooks 7.x ships with eslint-config-next@16 and flags patterns that don't sit well with React Compiler. Promotes the three downgraded rules (set-state-in-effect, refs, static-components) back to default level by either fixing the patterns or accepting narrowly-scoped suppressions in legitimate external-system-sync code. Real fixes: - VersionDisplay.client.tsx: replace useEffect+setState hydration guard with useSyncExternalStore reading window.next.version. - ColumnLayoutPicker/index.tsx: drop the redundant local state that mirrored Payload's useField value; derive layoutSelection during render instead. - InviteUserDrawer.tsx: hoist RoleAssignmentsArray to module scope (it was being recreated each render — static-components rule). - EventTable/Component.tsx: initialize `loading` from `eventOptions` prop so the early-return setState in the effect goes away. - carousel.tsx: replace the onSelect+useEffect+setState dance with useSyncExternalStore subscribed to embla's reInit/select events. - GenericEmbed/Component.tsx: extract BlobIframe sub-component owning the blob URL via useMemo + cleanup effect. Outer component uses useSyncExternalStore for SSR-safe DOMPurify output. No state sync. - DateRangeFilter.tsx: derive quickFilter from URL state with a separate `customMode` flag for the "user clicked Custom while dates matched a quick filter" case. Removes the initial-mount sync effect. Targeted suppressions: - TenantSelectionProvider/index.client.tsx: the auth-sync effects (login/logout transitions, stale-cookie cleanup, global-entity auto-select) are the canonical "subscribe to an external system, setState in response" pattern the rule explicitly accepts. Kept a single eslint-disable block over the auth-sync effects with a comment explaining the design. eslint.config.mjs: removed the temporary `'warn'` overrides for the three rules now that the codebase is clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@payloadcms/plugin-multi-tenant's TenantSelectionProvider uses `setTenantOptions((prev) => prev.length > 0 ? [] : prev)` in the auth-sync effect, which avoids needing `tenantOptions` in the deps array. Smaller deps = fewer effect runs (the effect now only runs on userID/initialValue/syncTenants/router changes, not whenever tenant options shift). Mirrors: https://github.com/payloadcms/payload/blob/main/packages/plugin-multi-tenant/src/providers/TenantSelectionProvider/index.client.tsx Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The carousel refactor in 534484e diverged src/components/ui/carousel.tsx from upstream shadcn output, which would force us to redo the useSyncExternalStore migration every time we re-pull (e.g. via `pnpm dlx shadcn@latest add carousel`). Restored the pristine shadcn version and added a targeted ESLint override that disables the three new react-hooks rules (set-state-in-effect, refs, static-components) for src/components/ui/** so future shadcn re-pulls don't trip CI. Other src/** files remain at default rule level. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Next 16 added an SSRF guard that blocks the image optimizer from
fetching source URLs that resolve to loopback or private IPs. In dev,
`getMediaURL` produces `http://localhost:3000/api/media/file/...` which
resolves to 127.0.0.1, so every <Image> on a page logs:
⨯ upstream image http://localhost:3000/api/... resolved to private ip
Setting `images.localPatterns = [{ pathname: '/api/**' }]` is the
official Next 16 escape hatch — it tells the optimizer that fetching
same-origin /api/** paths is intentional and should skip the SSRF
check. Same behavior in production (the path matches there too).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Description
Upgrades the framework from Next.js 15.4.11 to Next.js 16.2.4 (LTS). Driven by
@next/codemod@canary upgrade 16.2.4plus manual fixes for breaking changes the codemod doesn't cover, then five follow-up commits to clear all the warnings/deprecations the upgrade surfaced.Per the rationale in #1021, Next 16 was carved out as a dedicated PR so it could be reviewed and reverted independently.
Related Issues
N/A
Key Changes
Codemod-driven (automated by
@next/codemod)next15.4.11 → 16.2.4react/react-dompinned to 19.2.5eslint-config-next+@next/eslint-plugin-next15.x → 16.2.4src/middleware.tsrenamed tosrc/proxy.tsandmiddleware()→proxy()(Next 16 rename)tsconfig.json:jsx: "preserve"→"react-jsx",.next/dev/types/**/*.tsadded toincludeoverridesadded for@types/react/@types/react-domManual fixes for Next 16 breaking changes
@sentry/nextjs9.x → 10.x. The 9.x peer dep excludesnext@16; bumped to 10.51.0.eslint-plugin-react-hooks5.x → 7.x.eslint-config-next@16ships the 7.x version transitively, so we bumped our direct dep to match.revalidateTag(tag)→revalidateTag(tag, 'default')in 14 callsites. Next 16 made the secondprofilearg required.@importmoved above@tailwinddirectives insrc/app/(frontend)/globals.cssandsrc/app/(embeds)/a3-globals.css. Turbopack strictly enforces the CSS spec rule.eslint.config.mjsrewritten to importeslint-config-nextas native flat config (the FlatCompat shim no longer works because the package now exportsLinter.Config[]directly).jest.config.mjs:.claude/worktrees/added totestPathIgnorePatternsso stale Claude worktree copies don't pollute jest runs.Config deprecation cleanups
images.qualities: [75, 80]added tonext.config.js. Next 16 requires an explicit allowlist for any non-default<Image quality={...}>value.disableLoggerandautomaticVercelMonitorsmoved underwebpack: { treeshake: { removeDebugLogging }, automaticVercelMonitors }.ignoreWarningsblock innext.config.js. Sentry 10 fixed the upstream OpenTelemetry pattern that produced the warning, so the workaround is dead code.React 19 /
react-hooks7.x cleanup (8 files)The upgrade pulled in
eslint-plugin-react-hooks7.x with three new rules (set-state-in-effect,refs,static-components) that flagged 15 patterns at first. All resolved:VersionDisplay.client.tsx—useEffect+setStatehydration guard →useSyncExternalStorereadingwindow.next.version.ColumnLayoutPicker/index.tsx— dropped redundant local state mirroring Payload'suseFieldvalue; derivelayoutSelectionduring render.InviteUserDrawer.tsx— hoistedRoleAssignmentsArrayto module scope (was being recreated each render —static-componentsrule).EventTable/Component.tsx— initializeloadingfrom theeventOptionsprop so the early-return setState in the effect goes away.GenericEmbed/Component.tsx— extractedBlobIframesub-component owning the blob URL viauseMemo+ cleanup effect; outer component usesuseSyncExternalStorefor SSR-safe DOMPurify output.DateRangeFilter.tsx— derivequickFilterfrom URL state with a separatecustomModeflag for the "user clicked Custom while dates matched a quick filter" case. Removes the initial-mount sync effect.TenantSelectionProvider/index.client.tsx— moveduserChangedcomputation inside the auth-sync effect (was reading a ref during render); adopted the upstream@payloadcms/plugin-multi-tenantfunctional-setter pattern (setTenantOptions((prev) => prev.length > 0 ? [] : prev)) to droptenantOptionsfrom deps. Auth-state-transitionsetStatecalls are kept under a single targetedeslint-disableblock — they're the canonical "subscribe to an external system, setState in response" pattern the rule explicitly accepts; matches upstream's design.carousel.tsx— kept shadcn-pristine. ESLint config has a folder-scoped override forsrc/components/ui/**that disables the three new react-hooks rules so futurepnpm dlx shadcn@latest add <name>re-pulls don't trip CI.How to test
pnpm tsc— cleanpnpm lint— 0 errors, 0 warningspnpm test— 40/40 suites, 376/376 testspnpm build— production build succeeds (route summary showsProxy (Middleware))pnpm dev— Turbopack boots in ~340ms, no Sentry deprecation warnings, nounconfigured qualitieswarning,GET /returns 200proxy.tsmiddleware (wasmiddleware.ts) still rewrites tenant subdomains correctlyScreenshots / Demo video
N/A — framework upgrade only. CI build summary now shows
Proxy (Middleware)instead of the oldMiddlewarelabel.Migration Explanation
No database migrations needed.
🤖 Generated with Claude Code