|
| 1 | +# Migration Plan: Update Deps, App Router, Sentry CLI |
| 2 | + |
| 3 | +## Overview |
| 4 | +Incremental migration in 3 phases. Each phase verified with tests before proceeding. |
| 5 | + |
| 6 | +**Success criteria:** All Playwright e2e tests and Vitest unit tests pass. |
| 7 | + |
| 8 | +--- |
| 9 | + |
| 10 | +## Phase 1: Update All Dependencies to Latest |
| 11 | + |
| 12 | +### 1.1 Update Next.js 12 → 16.2.0 (and React 18 → 19) |
| 13 | +Next.js 16 requires React 19. Key breaking changes across versions: |
| 14 | + |
| 15 | +- **Next 13**: `<Link>` no longer needs `<a>` child, `next/image` removes `layout` prop, App Router introduced |
| 16 | +- **Next 14**: Turbopack stabilized, Server Actions stable |
| 17 | +- **Next 15**: `fetch` no longer cached by default, `params`/`searchParams` are async, React 19 |
| 18 | +- **Next 16**: React 19 required, continued App Router improvements |
| 19 | + |
| 20 | +**Changes needed before update:** |
| 21 | +1. **`next/link`** — Remove nested `<a>` tags in 8 files (about, contact, faq, get_involved, join/index, press, scholarship/index, terms, Nav component) |
| 22 | +2. **`next/image`** — Remove deprecated `layout` prop in 4 files (project_rebuild, scholarship/index, get_involved). Replace with CSS equivalents: |
| 23 | + - `layout="fill"` → `fill` boolean prop + parent `position: relative` |
| 24 | + - `layout="fixed"` → explicit `width`/`height` (already present) |
| 25 | + - `layout="intrinsic"` → explicit `width`/`height` |
| 26 | +3. **`images.domains`** → `images.remotePatterns` in next.config.js |
| 27 | +4. **`@next/bundle-analyzer`** — Update to match Next.js version |
| 28 | +5. **`next.config.js`** — Convert to `next.config.mjs` (ESM), remove `excludeDefaultMomentLocales` (removed), update `devIndicators` |
| 29 | + |
| 30 | +### 1.2 Update React 18 → 19 |
| 31 | +- `react`, `react-dom` → ^19.x |
| 32 | +- `@types/react`, `@types/react-dom` → ^19.x |
| 33 | +- `react-test-renderer` → removed (deprecated in React 19, use `@testing-library/react` instead) |
| 34 | +- `react-is` → ^19.x |
| 35 | +- Check for removed APIs: `ReactDOM.render`, `findDOMNode` etc. |
| 36 | + |
| 37 | +### 1.3 Update @sentry/nextjs 7 → 9 (latest) |
| 38 | +- Massive API changes — handled in Phase 3 detail |
| 39 | +- For now, just bump version |
| 40 | + |
| 41 | +### 1.4 Update All Other Dependencies |
| 42 | +| Package | Current | Action | |
| 43 | +|---------|---------|--------| |
| 44 | +| `@radix-ui/react-dialog` | 1.1.15 | Update to latest | |
| 45 | +| `@radix-ui/react-tabs` | 1.1.13 | Update to latest | |
| 46 | +| `axios` | ^1.12.2 | Update to latest | |
| 47 | +| `cva` | 1.0.0-beta.4 | Update to latest stable | |
| 48 | +| `fast-xml-parser` | ^3.21.1 | Update to v5 (breaking: API changes) | |
| 49 | +| `formik` | ^2.4.6 | Update to latest | |
| 50 | +| `logrocket` | ^10.1.0 | Update to latest | |
| 51 | +| `react-select` | ^4.0.2 | Update to v5 (breaking: React 18+ only, type changes) | |
| 52 | +| `react-player` | ^2.16.0 | Update to latest | |
| 53 | +| `tailwind-merge` | ^3.3.1 | Update to latest | |
| 54 | +| `yup` | ^1.7.1 | Update to latest | |
| 55 | +| `next-sitemap` | ^4.2.3 | Update to latest | |
| 56 | +| `@svgr/webpack` | ^6.3.1 | Update to latest (v8) | |
| 57 | +| `eslint` | ^8.56.0 | Update to v9 (flat config) | |
| 58 | +| `@typescript-eslint/*` | ^6.21.0 | Update to latest (v8) | |
| 59 | +| `storybook` | ^7.4.1 | Update to v8 | |
| 60 | +| `@testing-library/react` | ^12.1.5 | Update to v16 | |
| 61 | +| `@testing-library/jest-dom` | ^6.9.1 | Update to latest | |
| 62 | +| `chromatic` | ^6.8.0 | Update to latest | |
| 63 | +| `playwright` | ^1.56.1 | Update to latest | |
| 64 | +| `lint-staged` | 13.0.3 | Update to latest | |
| 65 | +| `postcss` | ^8.5.6 | Update to latest | |
| 66 | +| `typescript` | ^5.9.3 | Update to latest | |
| 67 | +| `vitest` / `@vitest/*` | ^3.1.2 | Update to latest | |
| 68 | + |
| 69 | +### 1.5 Remove Obsolete Dependencies |
| 70 | +- `fingerprintjs2` — discontinued, replace or remove |
| 71 | +- `next-cookies` — not needed with App Router (use `cookies()` from `next/headers`) |
| 72 | +- `path` — Node.js built-in, no need for polyfill |
| 73 | +- `intersection-observer` — polyfill no longer needed (widely supported) |
| 74 | +- `css-loader`, `style-loader`, `file-loader`, `url-loader` — webpack loaders not needed with Next.js |
| 75 | +- `require-context.macro` — CRA-specific, not used |
| 76 | +- `identity-obj-proxy` — replaced by vitest CSS handling |
| 77 | +- `process` — polyfill not needed |
| 78 | +- `express` — check if actually used, likely removable |
| 79 | +- `react-test-renderer` — deprecated in React 19 |
| 80 | + |
| 81 | +### 1.6 Fix Breaking Changes in Dependencies |
| 82 | +- **`fast-xml-parser` v3→v5**: API changed. Update `pages/podcast.tsx` parser usage |
| 83 | +- **`react-select` v4→v5**: Type imports changed, `styles` API slightly different |
| 84 | +- **`@testing-library/react` v12→v16**: `render` import stable, but `renderHook` moved |
| 85 | +- **ESLint v8→v9**: Requires flat config migration (`.eslintrc.js` → `eslint.config.mjs`) |
| 86 | +- **Storybook v7→v8**: Config format may change slightly |
| 87 | + |
| 88 | +### 1.7 Verify Phase 1 |
| 89 | +- Run `pnpm test` (vitest unit tests) |
| 90 | +- Run `pnpm test:e2e:headless` (playwright) |
| 91 | +- Fix any snapshot mismatches with `pnpm test:update-snaps` |
| 92 | + |
| 93 | +--- |
| 94 | + |
| 95 | +## Phase 2: Migrate Pages Router → App Router (Full Migration) |
| 96 | + |
| 97 | +### 2.1 Create App Directory Structure |
| 98 | +``` |
| 99 | +app/ |
| 100 | +├── layout.tsx ← from _app.tsx + _document.tsx |
| 101 | +├── page.tsx ← from pages/index.tsx |
| 102 | +├── not-found.tsx ← from pages/404.tsx |
| 103 | +├── error.tsx ← from pages/_error.tsx |
| 104 | +├── about/page.tsx |
| 105 | +├── blog/page.tsx |
| 106 | +├── branding/page.tsx |
| 107 | +├── challenge/page.tsx |
| 108 | +├── chapters/page.tsx |
| 109 | +├── chapter_leader/page.tsx |
| 110 | +├── contact/page.tsx |
| 111 | +├── corporate-training/page.tsx |
| 112 | +├── donate/page.tsx |
| 113 | +├── faq/page.tsx |
| 114 | +├── get_involved/page.tsx |
| 115 | +├── history/page.tsx |
| 116 | +├── jobs/page.tsx |
| 117 | +├── podcast/page.tsx |
| 118 | +├── policy/page.tsx |
| 119 | +├── press/page.tsx |
| 120 | +├── project_rebuild/page.tsx |
| 121 | +├── scholarship/ |
| 122 | +│ ├── page.tsx |
| 123 | +│ └── code_platoon/page.tsx |
| 124 | +├── services/page.tsx |
| 125 | +├── slack_guide/page.tsx |
| 126 | +├── sponsorship/page.tsx |
| 127 | +├── team/page.tsx |
| 128 | +├── terms/page.tsx |
| 129 | +├── thank_you/page.tsx |
| 130 | +├── join/ |
| 131 | +│ ├── page.tsx |
| 132 | +│ ├── form/page.tsx |
| 133 | +│ └── success/page.tsx |
| 134 | +└── api/ |
| 135 | + └── registration/ |
| 136 | + ├── new/route.ts |
| 137 | + └── update/route.ts |
| 138 | +``` |
| 139 | + |
| 140 | +### 2.2 Convert _app.tsx + _document.tsx → app/layout.tsx |
| 141 | +The root layout combines both: |
| 142 | +- **From _document.tsx**: `<html lang="en">`, meta tags, GA scripts, favicon |
| 143 | +- **From _app.tsx**: Layout wrapper (Nav, Footer, ScrollToTopButton), analytics initialization, font loading |
| 144 | +- Metadata API replaces `<Head>` for static meta (title, OG, Twitter card) |
| 145 | +- Google Analytics moves to `<Script>` from `next/script` |
| 146 | +- Font loading moves to `next/font/google` |
| 147 | +- CSS import stays in layout |
| 148 | + |
| 149 | +### 2.3 Convert Custom Head Component → Metadata API |
| 150 | +- `components/head.tsx` uses `next/head` — replace with: |
| 151 | + - Per-route `export const metadata` for static pages |
| 152 | + - `generateMetadata()` function for dynamic pages |
| 153 | +- Remove `next/head` import from all files |
| 154 | + |
| 155 | +### 2.4 Convert Page Components |
| 156 | +For each page in `pages/*.tsx`: |
| 157 | +1. Move to `app/*/page.tsx` |
| 158 | +2. Remove `<Head>` usage, add `metadata` export |
| 159 | +3. Mark as `'use client'` if it uses hooks, state, or browser APIs |
| 160 | +4. Pages that are purely presentational can be Server Components (no directive needed) |
| 161 | + |
| 162 | +**Pages requiring `'use client'`** (use hooks/state/effects): |
| 163 | +- join/index (useRouter, useEffect, useState) |
| 164 | +- join/form (useEffect, formik) |
| 165 | +- blog/index (useRouter) |
| 166 | +- podcast (if using client-side player) |
| 167 | +- faq (accordion state) |
| 168 | + |
| 169 | +**Pages that can be Server Components:** |
| 170 | +- about, branding, challenge, chapters, chapter_leader, contact, corporate-training, donate, get_involved, history, jobs, policy, press, project_rebuild, services, slack_guide, sponsorship, team, terms, thank_you, scholarship/*, join/success |
| 171 | + |
| 172 | +### 2.5 Convert Data Fetching |
| 173 | +- **`pages/podcast.tsx` `getStaticProps`** → async Server Component with `fetch()` + `revalidate` |
| 174 | +- **`pages/join/form.tsx` `getInitialProps`** (cookie check + redirect) → `middleware.ts` or Server Component with `cookies()` from `next/headers` |
| 175 | + |
| 176 | +### 2.6 Convert API Routes |
| 177 | +- `pages/api/registration/new.ts` → `app/api/registration/new/route.ts` |
| 178 | + - `export default handler` → `export async function POST(request: Request)` |
| 179 | + - `req.body` → `await request.json()` |
| 180 | + - `res.status().json()` → `return NextResponse.json()` |
| 181 | + - Cookie setting via `NextResponse` headers |
| 182 | +- Same for `update.ts` → `route.ts` with `PATCH` export |
| 183 | + |
| 184 | +### 2.7 Convert Router Usage |
| 185 | +5 files use `next/router`: |
| 186 | +- `useRouter()` → `useRouter()` from `next/navigation` (different API: no `query`, use `useSearchParams()`) |
| 187 | +- `Router.events` → removed in App Router. Use `usePathname()` + `useEffect` for route change detection |
| 188 | +- `router.push()` → `router.push()` (compatible) |
| 189 | +- `router.prefetch()` → `router.prefetch()` (compatible) |
| 190 | +- `router.query` → `useSearchParams()` or page `params` prop |
| 191 | + |
| 192 | +### 2.8 Update next.config |
| 193 | +- Remove Pages Router-specific config |
| 194 | +- Ensure App Router compatibility |
| 195 | +- Update cache headers source patterns for app router static paths |
| 196 | + |
| 197 | +### 2.9 Delete pages/ Directory |
| 198 | +After all pages are migrated and verified, delete `pages/` entirely. |
| 199 | + |
| 200 | +### 2.10 Update tsconfig.json Path Aliases |
| 201 | +- Remove `pages/*` alias (no longer needed) |
| 202 | +- Ensure `app/*` works if needed |
| 203 | + |
| 204 | +### 2.11 Update Tests |
| 205 | +- **Vitest**: Update `vitest.setup.tsx` if needed for App Router mocking |
| 206 | +- **Playwright**: Tests should mostly work as-is since they test the running app via URL |
| 207 | +- **Snapshots**: Regenerate all snapshots after migration |
| 208 | +- **next-router-mock**: May need updates for `next/navigation` mocking |
| 209 | + |
| 210 | +### 2.12 Verify Phase 2 |
| 211 | +- `pnpm build` (ensure production build succeeds) |
| 212 | +- `pnpm test` (unit tests) |
| 213 | +- `pnpm test:e2e:headless` (e2e tests) |
| 214 | +- Fix any failures |
| 215 | + |
| 216 | +--- |
| 217 | + |
| 218 | +## Phase 3: Reconfigure Sentry via sentry-cli (Full Setup) |
| 219 | + |
| 220 | +### 3.1 Update @sentry/nextjs to v9 (Latest) |
| 221 | +Already bumped in Phase 1, but now configure properly: |
| 222 | + |
| 223 | +**New initialization pattern (v8+):** |
| 224 | +- `sentry.client.config.js` → `sentry.client.config.ts` (updated API) |
| 225 | +- `sentry.server.config.js` → `sentry.server.config.ts` (updated API) |
| 226 | +- Add `sentry.edge.config.ts` (new in v8) |
| 227 | +- Add `instrumentation.ts` in project root (Next.js instrumentation hook): |
| 228 | + ```ts |
| 229 | + export async function register() { |
| 230 | + if (process.env.NEXT_RUNTIME === 'nodejs') { |
| 231 | + await import('./sentry.server.config'); |
| 232 | + } |
| 233 | + if (process.env.NEXT_RUNTIME === 'edge') { |
| 234 | + await import('./sentry.edge.config'); |
| 235 | + } |
| 236 | + } |
| 237 | + ``` |
| 238 | + |
| 239 | +**Config changes:** |
| 240 | +- `Sentry.configureScope()` → removed, use `Sentry.setExtra()`, `Sentry.setTag()` |
| 241 | +- `Sentry.flush()` → still available |
| 242 | +- `withSentryConfig` API updated — new options format |
| 243 | + |
| 244 | +### 3.2 Install and Configure sentry-cli |
| 245 | +```bash |
| 246 | +pnpm add -D @sentry/cli |
| 247 | +``` |
| 248 | + |
| 249 | +**Update `sentry.properties`:** |
| 250 | +``` |
| 251 | +defaults.url=https://sentry.io/ |
| 252 | +defaults.org=operation-code |
| 253 | +defaults.project=front-end |
| 254 | +cli.executable=node_modules/.bin/sentry-cli |
| 255 | +``` |
| 256 | + |
| 257 | +### 3.3 Configure Source Map Uploads |
| 258 | +In `next.config.mjs`, update `withSentryConfig` options: |
| 259 | +```js |
| 260 | +withSentryConfig(nextConfig, { |
| 261 | + org: "operation-code", |
| 262 | + project: "front-end", |
| 263 | + silent: !process.env.CI, |
| 264 | + widenClientFileUpload: true, |
| 265 | + sourcemaps: { |
| 266 | + deleteSourcemapsAfterUpload: true, |
| 267 | + }, |
| 268 | + // Release management |
| 269 | + release: { |
| 270 | + name: process.env.SENTRY_RELEASE || `front-end@${pkg.version}`, |
| 271 | + create: true, |
| 272 | + finalize: true, |
| 273 | + // Commit integration |
| 274 | + setCommits: { |
| 275 | + auto: true, |
| 276 | + }, |
| 277 | + // Deploy tracking |
| 278 | + deploy: { |
| 279 | + env: process.env.VERCEL_ENV || 'development', |
| 280 | + }, |
| 281 | + }, |
| 282 | +}); |
| 283 | +``` |
| 284 | + |
| 285 | +### 3.4 Update Sentry Client Config |
| 286 | +```ts |
| 287 | +import * as Sentry from '@sentry/nextjs'; |
| 288 | + |
| 289 | +Sentry.init({ |
| 290 | + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN || '...', |
| 291 | + tracesSampleRate: 1.0, |
| 292 | + replaysSessionSampleRate: 0.1, |
| 293 | + replaysOnErrorSampleRate: 1.0, |
| 294 | + integrations: [ |
| 295 | + Sentry.replayIntegration(), |
| 296 | + Sentry.browserTracingIntegration(), |
| 297 | + ], |
| 298 | + enabled: !!process.env.NEXT_PUBLIC_SENTRY_DSN, |
| 299 | +}); |
| 300 | +``` |
| 301 | + |
| 302 | +### 3.5 Update Sentry Server Config |
| 303 | +```ts |
| 304 | +import * as Sentry from '@sentry/nextjs'; |
| 305 | + |
| 306 | +Sentry.init({ |
| 307 | + dsn: process.env.SENTRY_DSN || '...', |
| 308 | + tracesSampleRate: 1.0, |
| 309 | + enabled: !!process.env.SENTRY_DSN, |
| 310 | +}); |
| 311 | +``` |
| 312 | + |
| 313 | +### 3.6 Update Error Handling |
| 314 | +- `pages/_error.tsx` → `app/error.tsx` (already done in Phase 2) |
| 315 | +- App Router error boundary uses `'use client'` directive |
| 316 | +- `app/global-error.tsx` for root-level errors |
| 317 | +- Sentry auto-captures via `withSentryConfig` |
| 318 | + |
| 319 | +### 3.7 Update Code Using Sentry APIs |
| 320 | +- `_app.tsx` (now `layout.tsx`): Replace `Sentry.configureScope()` with `Sentry.setExtra()` |
| 321 | +- `_error.tsx` (now `error.tsx`): Update error capture pattern |
| 322 | + |
| 323 | +### 3.8 Add sentry-cli to CI/CD |
| 324 | +Update `.github/workflows/ci.yml`: |
| 325 | +- Add `SENTRY_AUTH_TOKEN` secret |
| 326 | +- Add `SENTRY_ORG` and `SENTRY_PROJECT` env vars |
| 327 | +- sentry-cli runs automatically via `withSentryConfig` during `next build` |
| 328 | + |
| 329 | +### 3.9 Verify Phase 3 |
| 330 | +- `pnpm build` (verify source maps upload) |
| 331 | +- `pnpm test` (unit tests) |
| 332 | +- `pnpm test:e2e:headless` (e2e tests) |
| 333 | + |
| 334 | +--- |
| 335 | + |
| 336 | +## Risk Mitigation |
| 337 | + |
| 338 | +1. **Commit after each sub-step** so we can bisect failures |
| 339 | +2. **Update snapshots** after visual changes (`pnpm test:update-snaps`) |
| 340 | +3. **ESLint flat config** migration is complex — may need to keep v8 compatibility if v9 causes issues with plugins |
| 341 | +4. **Storybook v8** may need separate verification (not in test criteria) |
| 342 | +5. **`fingerprintjs2`** removal may break LogRocket fingerprinting — assess if still needed |
| 343 | +6. **React 19** removes `react-test-renderer` — must migrate snapshot tests to `@testing-library/react` |
| 344 | + |
| 345 | +## Estimated File Changes |
| 346 | +- ~50 page/component files modified |
| 347 | +- ~10 config files modified |
| 348 | +- ~5 new files created (instrumentation.ts, error.tsx, global-error.tsx, etc.) |
| 349 | +- ~40 files moved (pages/ → app/) |
| 350 | +- ~77 test files may need snapshot updates |
0 commit comments