Skip to content

fix: upgrade Next.js to 16.3.4, fixing 3 high-severity Snyk findings - #1766

Open
magic-peach wants to merge 1 commit into
mainfrom
chore/upgrade-nextjs-16
Open

fix: upgrade Next.js to 16.3.4, fixing 3 high-severity Snyk findings#1766
magic-peach wants to merge 1 commit into
mainfrom
chore/upgrade-nextjs-16

Conversation

@magic-peach

Copy link
Copy Markdown
Collaborator

Summary

Snyk flagged 3 high-severity CVEs on next@15.5.25: two Directory Traversal issues in its postcss@8.4.31 dependency, and one Allocation of Resources Without Limits or Throttling in next itself. All fixed by upgrading to next@16.3.4.

The real risk, and how it was handled

Next 16 defaults to Turbopack, which silently ignores this repo's custom webpack() config in next.config.ts — the fs: false fallback FFmpeg.wasm relies on to load correctly. Confirmed non-hypothetical: an unmodified next build fails outright with an explicit Turbopack/webpack config conflict error. Rather than translate the fix into Turbopack's config surface and hope it behaves identically for WASM loading, dev/build now pin --webpack explicitly — same proven bundler, still gets every other Next 16 fix.

Verified in a real headless browser, not just a clean build exit code: uploaded a synthetic video, triggered export, confirmed via network trace that the FFmpeg worker chunk, ffmpeg-core.js, and ffmpeg-core.wasm all load and export completes with zero console errors.

Also in this PR

  • ESLint pinned to 9.39.5, not the codemod's default 10.9.1eslint-plugin-jsx-a11y has no ESLint-10-compatible release yet, and eslint-plugin-react's react/display-name rule crashes outright under ESLint 10's context API. 9.x fully satisfies eslint-config-next's >=9.0.0 peer requirement.
  • eslint.config.mjs rewritten, not patched — the old FlatCompat bridge for jsx-a11y crashes on ESLint 10's minimatch resolution and is unnecessary; jsx-a11y ships its own native flat-config export. Also added explicit ignores for build output, since migrating off next lint (removed in Next 16) to plain eslint . lost that automatic exclusion — it was linting into storybook-static's bundled JS as if it were source.
  • 18 pre-existing lint findings, fixed individuallyeslint-config-next 16 pulls in a major eslint-plugin-react-hooks bump with new React-Compiler-era rules that caught real (if usually benign) patterns across 16 files. Each was reviewed on its own merits: genuine effect-appropriate cases (SSR-unsafe browser APIs, timer/object-URL lifecycles) got a scoped disable + comment explaining why; prop-mirrored-into-state cases were rewritten using React's own documented pattern; a couple were genuine bugs — see commit message for the full breakdown, including two real (not just lint-shaped) bugs this surfaced and fixed:
    • ThemeProvider had a redundant duplicate setThemeState call.
    • FileUpload.tsx had two <input type="file"> elements bound to the same ref (only the last-rendered one actually worked) — verified fixed via a real browser click test.

Test plan

  • bunx tsc --noEmit clean
  • bun run lint clean (was 18 errors + 6 warnings)
  • bun run build succeeds, static export intact
  • Real browser test: video upload → export → FFmpeg worker + WASM load confirmed via network trace, zero console errors
  • Real browser test: FileUpload "Change" button correctly triggers the (now single) file input

🤖 Generated with Claude Code

https://claude.ai/code/session_01L5gqyU5QKQtLAza6hL6dXv

Snyk flagged 3 high-severity CVEs traced to next@15.5.25: two Directory
Traversal issues in its postcss@8.4.31 dependency, and one Allocation of
Resources Without Limits or Throttling in next itself. All three are
fixed by next@16.3.0+; this goes to the latest 16.3.4.

## The real risk: Turbopack

Next 16 defaults to Turbopack, which silently ignores next.config.ts's
custom webpack() config — the fs:false fallback this app relies on for
FFmpeg.wasm to load correctly. Confirmed this isn't hypothetical: an
unmodified `next build` fails outright ("This build is using Turbopack,
with a webpack config and no turbopack config"). Rather than translate
the fix into Turbopack's different config surface and hope it behaves
identically for WASM loading, dev/build scripts now pin --webpack
explicitly, preserving the exact proven bundler behavior.

Verified end-to-end in a real browser (not just a clean build exit
code): uploaded a synthetic video, triggered export, confirmed via
network trace that the FFmpeg worker chunk, ffmpeg-core.js, and
ffmpeg-core.wasm all load and the export completes with zero console
errors.

## ESLint: pinned to 9.x, not 10.x

The upgrade codemod defaulted eslint to 10.9.1, which cascades into
real breakage unrelated to the Next.js CVE fix: eslint-plugin-jsx-a11y
tops out at eslint ^9 (no compatible release exists yet), and
eslint-plugin-react's react/display-name rule crashes outright under
ESLint 10's context API. Pinned to 9.39.5, the latest 9.x, which
eslint-config-next's peerDependency (>=9.0.0) is fully satisfied by.

## eslint.config.mjs rewritten, not patched

The existing config used a FlatCompat bridge (compat.extends("plugin:
jsx-a11y/recommended")) to pull in jsx-a11y's legacy .eslintrc-style
config. That bridge crashes on ESLint 10's minimatch resolution, and is
unnecessary: jsx-a11y ships a native flat-config export
(flatConfigs.recommended, 34 rules) that eslint-config-next's own
core-web-vitals preset already partially registers. Fixed to merge just
the rules (avoiding the plugin double-registration this causes) with no
compat layer.

Also fixes: `next lint` is removed in Next 16 (migrated to plain
`eslint .` per Next's own codemod), and the migration didn't carry over
next lint's automatic exclusion of build output — eslint.config.mjs now
explicitly ignores .next/out/storybook-static/coverage, since eslint .
was otherwise linting into storybook-static's bundled/minified JS as if
it were source. Removed the now-dead legacy .eslintrc.json.

## 18 pre-existing lint findings, now fixed

eslint-config-next 16 pulls in eslint-plugin-react-hooks@^7 (from an
older major under Next 15), which adds new React-Compiler-era rules
that caught 18 pre-existing issues across 16 files, mostly
react-hooks/set-state-in-effect (calling a setState setter synchronously
in a useEffect body). None of these are caused by this upgrade — the
patterns were always there; the stricter linter just started checking
for them. Each was reviewed individually rather than blanket-suppressed:

- Genuine external-system synchronization (browser-only APIs unavailable
  during SSR, object-URL/timer/DOM lifecycles, video-element imperative
  control) kept as effects with a scoped eslint-disable and a comment
  explaining why: PrivacyBanner, ThemeToggle, ThemeProvider,
  NativeShareButton, VideoEditor, ExportOverlay, ImageOverlay,
  ThumbnailStrip, VideoPreview, OnboardingTour (dismiss), useVideoEditor.
- Prop-mirrored-into-local-state cases (AudioSpeedControl, TrimControl)
  rewritten using React's own documented "adjust state during render"
  pattern instead of an effect — https://react.dev/learn/you-might-not-
  need-an-effect#adjusting-some-state-when-a-prop-changes.
- ThemeProvider had a genuinely redundant duplicate setThemeState call
  immediately superseded by applyTheme() two lines later — removed
  rather than suppressed.
- OnboardingTour's react-hooks/refs finding (reading tooltipRef.current
  during render to self-position a tooltip) fixed properly: the pure
  positioning math now takes plain width/height instead of a ref, with
  the real DOM measurement moved into useLayoutEffect and the result
  stored in state. This also improves actual behavior — previously the
  tooltip always used hardcoded 320x140 fallback dimensions since the
  ref was read during the same render it was attached in, never
  reflecting the tooltip's real measured size.
- FileUpload.tsx restructured: `handleFile` was referenced (in the
  drag-and-drop effect) before its declaration further down the file.
  That effect had `[]` deps with exhaustive-deps silenced, so it
  captured whatever handleFile was on mount and would never pick up a
  new one if onFileSelect's identity ever changed — a real stale-closure
  bug, not just an ordering nitpick. Reordered handleFile before its
  use and added it to the effect's deps. Hoisting FileInfo/DropZone out
  of the component body (react-hooks/static-components: they were
  recreated every render, forcing React to remount their subtree) also
  surfaced and fixed a second latent bug: both FileInfo and the outer
  return rendered their own <input type="file"> bound to the same
  inputRef, so only the last-rendered one actually got the ref —
  verified via a real browser test that the "Change" button's click
  now reliably reaches the single remaining input.
- Handful of stale eslint-disable comments removed (ComparisonPreview,
  LottiePlayer, VideoPreview) where the underlying issue no longer
  applied.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L5gqyU5QKQtLAza6hL6dXv
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

👋 Thanks for your PR, @magic-peach!

Welcome to Reframe — a browser-based video editor built for everyone 🎬

What happens next

  1. 🤖 Automated checks — build & TypeScript typecheck will run automatically
  2. Vercel preview — a preview deployment will be created (requires maintainer authorization for fork PRs)
  3. 👀 Code review — a maintainer will review your changes
  4. 🚀 Merge — once approved, your PR will be merged!

Quick checklist

  • PR title follows Conventional Commits (e.g. feat: add dark mode)
  • Linked the issue this PR closes (e.g. Closes #123)
  • Tested the changes locally (bun run dev)
  • Build passes (bun run build)

Useful links

Happy coding! 🎉

@netlify

netlify Bot commented Sep 2, 2026

Copy link
Copy Markdown

Deploy Preview for reframe-os ready!

Name Link
🔨 Latest commit b5ebf28
🔍 Latest deploy log https://app.netlify.com/projects/reframe-os/deploys/6a9848c3b248ff00082b8c24
😎 Deploy Preview https://deploy-preview-1766--reframe-os.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions github-actions Bot added level:advanced Advanced level - 55 pts type:bug Bug fix type:testing Testing labels Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

⚠️ PR Format Issues — @magic-peach

Please fix the following before your PR can be reviewed:

  • ⚠️ No linked issue found. Add Closes #<issue-number> to your PR description.

Push new commits after fixing — this comment will update automatically.

📖 CONTRIBUTING.md

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Claude encountered an error —— View job


I'll analyze this and get back to you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

level:advanced Advanced level - 55 pts type:bug Bug fix type:testing Testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant