Backend / staging cleanup, performance substrate, and create-flow polish #60
@@ -1,20 +1,22 @@
|
|||||||
import type { ReactNode } from "react";
|
import { Suspense, type ReactNode } from "react";
|
||||||
import ConditionalNavigation from "../components/navigation/ConditionalNavigation";
|
import ConditionalNavigation from "../components/navigation/ConditionalNavigation";
|
||||||
import { MessagesProvider } from "../contexts/MessagesContext";
|
import { MessagesProvider } from "../contexts/MessagesContext";
|
||||||
import { AuthModalProvider } from "../contexts/AuthModalContext";
|
import { AuthModalProvider } from "../contexts/AuthModalContext";
|
||||||
import messages from "../../messages/en/index";
|
import messages from "../../messages/en/index";
|
||||||
|
|
||||||
// Reads the session for admin chrome (matches the HttpOnly cookie on first
|
// `force-dynamic` removed in favor of `experimental.cacheComponents` (Next 16).
|
||||||
// HTML response). Scoped here so `(marketing)` can render statically.
|
// See `(app)/layout.tsx` for the matching `<Suspense fallback={null}>` rationale
|
||||||
export const dynamic = "force-dynamic";
|
// — the fallback can't access `usePathname()` since it sits in the static shell.
|
||||||
|
//
|
||||||
// Operator/admin dashboards (e.g. `/monitor`) intentionally render without the
|
// Operator/admin dashboards (e.g. `/monitor`) intentionally render without the
|
||||||
// public marketing footer. Auth/access is enforced upstream.
|
// public marketing footer. Auth/access is enforced upstream.
|
||||||
export default function AdminLayout({ children }: { children: ReactNode }) {
|
export default function AdminLayout({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<MessagesProvider messages={messages}>
|
<MessagesProvider messages={messages}>
|
||||||
<AuthModalProvider>
|
<AuthModalProvider>
|
||||||
<ConditionalNavigation />
|
<Suspense fallback={null}>
|
||||||
|
<ConditionalNavigation />
|
||||||
|
</Suspense>
|
||||||
<main className="flex-1">{children}</main>
|
<main className="flex-1">{children}</main>
|
||||||
</AuthModalProvider>
|
</AuthModalProvider>
|
||||||
</MessagesProvider>
|
</MessagesProvider>
|
||||||
|
|||||||
+14
-8
@@ -1,15 +1,19 @@
|
|||||||
import type { ReactNode } from "react";
|
import { Suspense, type ReactNode } from "react";
|
||||||
import ConditionalNavigation from "../components/navigation/ConditionalNavigation";
|
import ConditionalNavigation from "../components/navigation/ConditionalNavigation";
|
||||||
import { MessagesProvider } from "../contexts/MessagesContext";
|
import { MessagesProvider } from "../contexts/MessagesContext";
|
||||||
import { AuthModalProvider } from "../contexts/AuthModalContext";
|
import { AuthModalProvider } from "../contexts/AuthModalContext";
|
||||||
import messages from "../../messages/en/index";
|
import messages from "../../messages/en/index";
|
||||||
|
|
||||||
// Reads `cr_session` via Server Components on every navigation so the header
|
// `force-dynamic` removed in favor of `experimental.cacheComponents` (Next 16).
|
||||||
// matches the HttpOnly cookie on the first HTML response (no "Log in" flash
|
// `ConditionalNavigation` reads `cr_session` server-side (and `usePathname()`
|
||||||
// before `/api/auth/session`). Scoped here instead of the root layout so
|
// transitively in `ConditionalNavigationClient`) — both are uncached, so it
|
||||||
// `(marketing)` can render statically.
|
// lives behind a `<Suspense>` boundary so the rest of the layout stays in the
|
||||||
export const dynamic = "force-dynamic";
|
// static shell while the session/pathname-aware nav streams in. The fallback
|
||||||
|
// is `null` because any non-null fallback would also need to live in the
|
||||||
|
// static shell, and the nav's chromeless decision depends on the pathname
|
||||||
|
// (e.g. `/create/*` and `/login` render no top-nav). Brief blank-nav while
|
||||||
|
// the dynamic island resolves is acceptable on signed-in product surfaces.
|
||||||
|
//
|
||||||
// Signed-in product surfaces (`/create/*`, `/login`) run without the marketing
|
// Signed-in product surfaces (`/create/*`, `/login`) run without the marketing
|
||||||
// footer. `/profile` adds it via `profile/layout.tsx`. Per-route chrome (e.g.
|
// footer. `/profile` adds it via `profile/layout.tsx`. Per-route chrome (e.g.
|
||||||
// CreateFlow) is composed in nested layouts.
|
// CreateFlow) is composed in nested layouts.
|
||||||
@@ -17,7 +21,9 @@ export default function AppLayout({ children }: { children: ReactNode }) {
|
|||||||
return (
|
return (
|
||||||
<MessagesProvider messages={messages}>
|
<MessagesProvider messages={messages}>
|
||||||
<AuthModalProvider>
|
<AuthModalProvider>
|
||||||
<ConditionalNavigation />
|
<Suspense fallback={null}>
|
||||||
|
<ConditionalNavigation />
|
||||||
|
</Suspense>
|
||||||
<main className="flex-1">{children}</main>
|
<main className="flex-1">{children}</main>
|
||||||
</AuthModalProvider>
|
</AuthModalProvider>
|
||||||
</MessagesProvider>
|
</MessagesProvider>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import type { ReactNode } from "react";
|
import { Suspense, type ReactNode } from "react";
|
||||||
import MarketingNavigation from "../components/navigation/MarketingNavigation";
|
import MarketingNavigation from "../components/navigation/MarketingNavigation";
|
||||||
import { MessagesProvider } from "../contexts/MessagesContext";
|
import { MessagesProvider } from "../contexts/MessagesContext";
|
||||||
import { AuthModalProvider } from "../contexts/AuthModalContext";
|
import { AuthModalProvider } from "../contexts/AuthModalContext";
|
||||||
@@ -19,7 +19,14 @@ export default function MarketingLayout({ children }: { children: ReactNode }) {
|
|||||||
return (
|
return (
|
||||||
<MessagesProvider messages={marketingMessages}>
|
<MessagesProvider messages={marketingMessages}>
|
||||||
<AuthModalProvider>
|
<AuthModalProvider>
|
||||||
<MarketingNavigation />
|
{/*
|
||||||
|
* MarketingNavigation reads `usePathname()` to decide chromeless paths
|
||||||
|
* (uncached data under `cacheComponents`). Suspense lets the static
|
||||||
|
* shell prerender; the nav streams in with the correct visibility.
|
||||||
|
*/}
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<MarketingNavigation />
|
||||||
|
</Suspense>
|
||||||
<main className="flex-1">{children}</main>
|
<main className="flex-1">{children}</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
</AuthModalProvider>
|
</AuthModalProvider>
|
||||||
|
|||||||
+107
-59
@@ -1,18 +1,19 @@
|
|||||||
# Next 16 substrate evaluation (Phase 3)
|
# Next 16 substrate evaluation (Phase 3)
|
||||||
|
|
||||||
Evaluation of `experimental.cacheComponents` (formerly `experimental.ppr`)
|
Evaluation of `experimental.cacheComponents` (formerly `experimental.ppr`)
|
||||||
and React Compiler against this repo on Next.js 16.2.6. Performed as a
|
and React Compiler against this repo on Next.js 16.2.6. Originally written
|
||||||
canary build pass without committing either flag to `main`.
|
as a canary report when both flags were deferred; updated when both shipped
|
||||||
|
as follow-up work (see "Outcome" sections below).
|
||||||
|
|
||||||
## TL;DR
|
## TL;DR
|
||||||
|
|
||||||
| Flag | Recommendation | Why |
|
| Flag | Recommendation | Status |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `cacheComponents` (PPR successor) | **Defer** — requires a follow-up refactor before it can ship | Renamed from `ppr` in Next 16; now a boolean global toggle, no per-route `experimental_ppr` opt-in. Requires removing `force-dynamic` from `(app)` and `(admin)` layouts and re-expressing session-aware dynamism via Suspense + cache primitives. |
|
| `cacheComponents` (PPR successor) | **Ship** | **Shipped.** `force-dynamic` removed from `(app)` and `(admin)` layouts; `<ConditionalNavigation />` (and `<MarketingNavigation />`) wrapped in `<Suspense fallback={null}>`. `(app)`/`(admin)` routes are now `◐ Partial Prerender` instead of `ƒ Dynamic`. `/` static shell dropped from 45 KB → 11.7 KB gzipped. |
|
||||||
| React Compiler | **Defer** — config surface moved + missing dep | Moved out of `experimental` to the top-level `reactCompiler` key in Next 16. Requires installing `babel-plugin-react-compiler`. No blocking codebase incompatibilities found in the canary surface, but the install + eslint plugin setup is its own follow-up task. |
|
| React Compiler | **Ship (annotation mode)** | **Shipped (plumbing only).** `babel-plugin-react-compiler` + `eslint-plugin-react-compiler` installed. `reactCompiler: { compilationMode: "annotation" }` enabled in `next.config.mjs`. ESLint rule wired in at "warn" — found 31 latent warnings across 8 files (none introduced by this change). Migrating containers to `"use memo"` is a future task. |
|
||||||
|
|
||||||
Neither flag was shippable as a pure config flip in this audit. The findings
|
Both flags now ship in `main`. The findings below describe what changed in
|
||||||
below describe what changed in Next 16 and the work each would require.
|
Next 16, what work each required, and the outcomes.
|
||||||
|
|
||||||
## Repo baseline (Next 16.2.6, Turbopack, no experimental flags)
|
## Repo baseline (Next 16.2.6, Turbopack, no experimental flags)
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ Note: Next 16 with Turbopack no longer prints per-route first-load JS sizes
|
|||||||
in the build summary. Bundle analyzer (`ANALYZE=true`) is the canonical
|
in the build summary. Bundle analyzer (`ANALYZE=true`) is the canonical
|
||||||
source for size data — see Phase 4a.
|
source for size data — see Phase 4a.
|
||||||
|
|
||||||
## 3a. `cacheComponents` (PPR) — DEFER
|
## 3a. `cacheComponents` (PPR) — SHIPPED
|
||||||
|
|
||||||
### What changed in Next 16
|
### What changed in Next 16
|
||||||
|
|
||||||
@@ -67,30 +68,46 @@ session-aware chrome (set in Phase 4b of the prior plan). `cacheComponents`
|
|||||||
requires expressing that dynamism via `<Suspense>` boundaries plus
|
requires expressing that dynamism via `<Suspense>` boundaries plus
|
||||||
`unstable_noStore()`/`unstable_cache()` instead of route-segment `dynamic`.
|
`unstable_noStore()`/`unstable_cache()` instead of route-segment `dynamic`.
|
||||||
|
|
||||||
### Estimated work to ship
|
### Work performed
|
||||||
|
|
||||||
1. Refactor [app/(app)/layout.tsx](../../app/(app)/layout.tsx) and
|
1. Removed `export const dynamic = "force-dynamic"` from
|
||||||
[app/(admin)/layout.tsx](../../app/(admin)/layout.tsx) so the
|
[app/(app)/layout.tsx](../../app/(app)/layout.tsx) and
|
||||||
`ConditionalNavigation` session fetch sits inside a `<Suspense>` boundary
|
[app/(admin)/layout.tsx](../../app/(admin)/layout.tsx).
|
||||||
with a fallback that matches the generic chrome.
|
2. Wrapped `<ConditionalNavigation />` (server component reading
|
||||||
2. Mark the session-reading components with `unstable_noStore()` (or the
|
`getNavAuthSignedIn()` → `cookies()`) in `<Suspense fallback={null}>` in
|
||||||
stable equivalent in Next 16) so they opt out of the static cache.
|
both layouts.
|
||||||
3. Verify the existing static routes (`/`, `/about`, `/blog`, etc.) still
|
3. Same change for `<MarketingNavigation />` in
|
||||||
prerender; add `<Suspense>` boundaries around any future dynamic islands.
|
[app/(marketing)/layout.tsx](../../app/(marketing)/layout.tsx) — the
|
||||||
4. Confirm `(marketing)` routes still serve from CDN with the static shell
|
marketing nav reads `usePathname()` (uncached per request) and would
|
||||||
while the personalized nav island streams.
|
otherwise block the static shell at routes like `/rules/[id]`.
|
||||||
|
4. Enabled `experimental.cacheComponents: true` in
|
||||||
|
[next.config.mjs](../../next.config.mjs).
|
||||||
|
|
||||||
This is the natural next step after Phase 4b made marketing static, but
|
`unstable_noStore()` already sits inside `getNavAuthSignedIn()`; no
|
||||||
it's not a config-only change. Ticket separately.
|
additional cache primitives were needed.
|
||||||
|
|
||||||
### Verification (when shipping)
|
### Why `fallback={null}` and not a placeholder
|
||||||
|
|
||||||
- `(marketing)` routes still appear as `○ Static` in build output.
|
Any non-null fallback would also need to live in the static shell. The
|
||||||
- `(app)`/`(admin)` routes' static shell prerenders; the personalized nav
|
existing `ConditionalNavigationClient` reads `usePathname()` to decide
|
||||||
streams (visible in `curl` of the HTML — partial shell first, then nav).
|
chromeless paths (`/create/*`, `/login`), which is uncached data —
|
||||||
- TTFB on `(marketing)` unchanged or improved.
|
disallowed in the static shell under `cacheComponents`. A truly static
|
||||||
|
placeholder is possible but would cause layout shift on routes that
|
||||||
|
ultimately render no nav. Trade-off accepted: brief blank-nav while the
|
||||||
|
dynamic island streams in.
|
||||||
|
|
||||||
## 3b. React Compiler — DEFER
|
### Outcome (measured against `npx next build`)
|
||||||
|
|
||||||
|
| Route group | Before | After |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `/`, `/about`, `/blog`, `/components-preview`, `/how-it-works`, `/learn` | `○ Static` | `○ Static` |
|
||||||
|
| `/create`, `/create/[screenId]`, `/profile`, `/monitor`, `/login`, `/rules/[id]` | `ƒ Dynamic` | `◐ Partial Prerender` |
|
||||||
|
| `/templates`, `/use-cases`, `/use-cases/[slug]`, `/blog/[slug]` | `○ Static` / `◐ SSG` | `◐ Partial Prerender` |
|
||||||
|
|
||||||
|
`/` static shell: 45 KB gzipped → 11.7 KB gzipped (74% reduction). All
|
||||||
|
196 test files / 1251 tests pass.
|
||||||
|
|
||||||
|
## 3b. React Compiler — SHIPPED (annotation mode, plumbing only)
|
||||||
|
|
||||||
### What changed in Next 16
|
### What changed in Next 16
|
||||||
|
|
||||||
@@ -110,51 +127,82 @@ to the next package. Is babel-plugin-react-compiler installed in your
|
|||||||
node_modules directory?
|
node_modules directory?
|
||||||
```
|
```
|
||||||
|
|
||||||
### Estimated work to ship
|
### Work performed
|
||||||
|
|
||||||
1. `npm install --save-dev babel-plugin-react-compiler eslint-plugin-react-compiler`.
|
1. `npm install --save-dev babel-plugin-react-compiler eslint-plugin-react-compiler`.
|
||||||
2. Add `reactCompiler: { compilationMode: "annotation" }` to `next.config.mjs`
|
2. Added top-level `reactCompiler: { compilationMode: "annotation" }` to
|
||||||
(top-level, not under `experimental`).
|
[next.config.mjs](../../next.config.mjs).
|
||||||
3. Enable `eslint-plugin-react-compiler` and run it against the repo to
|
3. Wired `react-compiler/react-compiler` as `"warn"` in
|
||||||
surface components that would bail (refs mutated during render, reads
|
[eslint.config.mjs](../../eslint.config.mjs) (both JS and TS plugin blocks).
|
||||||
of non-reactive globals inline, etc.).
|
4. No `"use memo"` directives added in this pass — the goal is plumbing,
|
||||||
4. Incrementally add `"use memo"` directives to high-render-frequency
|
not migration. The compiler is a no-op until components opt in.
|
||||||
containers (`CreateFlowProvider`, `AuthModalProvider`, list-heavy views).
|
|
||||||
5. Once stable, flip `compilationMode: "all"` and remove hand-written
|
|
||||||
`useMemo`/`useCallback` where the compiler subsumes them.
|
|
||||||
|
|
||||||
### Why annotation mode first
|
### Why annotation mode first (unchanged from original plan)
|
||||||
|
|
||||||
We have many hand-rolled memoized containers. The risk of `compilationMode: "all"`
|
We have many hand-rolled memoized containers. The risk of `compilationMode: "all"`
|
||||||
on day one is that the compiler bails on a critical component in a way that
|
on day one is that the compiler bails on a critical component in a way that
|
||||||
changes render counts. Annotation mode lets us migrate one component at a
|
changes render counts. Annotation mode lets us migrate one component at a
|
||||||
time with eslint enforcement.
|
time with eslint enforcement.
|
||||||
|
|
||||||
### Verification (when shipping)
|
### ESLint audit results
|
||||||
|
|
||||||
- Bundle size before/after `next build` with the runtime added.
|
`npx eslint app lib` after wiring the rule found **31 react-compiler warnings
|
||||||
- Test suite green (`npx vitest run` — 196 files / 1251 tests today).
|
across 8 files**:
|
||||||
- Component render counts unchanged or reduced on key surfaces (use the
|
|
||||||
React DevTools profiler on `/create/informational` and `/`).
|
| Category | Count |
|
||||||
|
| --- | --- |
|
||||||
|
| "Hooks may not be referenced as normal values" (passing hook references as values) | 25 |
|
||||||
|
| "Writing to a variable defined outside a component or hook" (module-level mutation) | 2 |
|
||||||
|
| "Hooks must always be called in a consistent order" (conditional hooks) | 2 |
|
||||||
|
| "React Compiler skipped optimizing" (file has React rules disabled) | 2 |
|
||||||
|
|
||||||
|
Files flagged:
|
||||||
|
- `app/(app)/create/context/CreateFlowContext.tsx`
|
||||||
|
- `app/(app)/create/hooks/useCompletedRuleShareExport.ts`
|
||||||
|
- `app/(marketing)/use-cases/[slug]/page.tsx`
|
||||||
|
- `app/(marketing)/use-cases/page.tsx`
|
||||||
|
- `app/(marketing-case-study)/use-cases/[slug]/rule/_components/useUseCaseCompletedRuleActions.ts`
|
||||||
|
- `app/(marketing-case-study)/use-cases/[slug]/rule/page.tsx`
|
||||||
|
- `app/components/controls/SelectInput/SelectInput.container.tsx`
|
||||||
|
- `app/components/sections/RelatedArticles/RelatedArticles.view.tsx`
|
||||||
|
|
||||||
|
All warnings are latent (not introduced by this change). The compiler runs
|
||||||
|
in annotation mode, so these files are not affected at runtime until they
|
||||||
|
opt in. Not fixed in this commit — these are the migration targets to
|
||||||
|
address before flipping to `compilationMode: "all"`.
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
- Test suite green: 196 files / 1251 tests pass.
|
||||||
|
- Build green: `npx next build` clean with the new config.
|
||||||
|
- TSC clean.
|
||||||
|
- Bundle size delta minimal — no `"use memo"` annotations means no compiler
|
||||||
|
runtime calls are emitted in user code yet.
|
||||||
|
|
||||||
## Impact on Phase 4 (MessagesProvider)
|
## Impact on Phase 4 (MessagesProvider)
|
||||||
|
|
||||||
If we later ship `cacheComponents`, the MessagesProvider refactor's win
|
Phase 4 (route-scoped `MessagesProvider`) shipped before this work. With
|
||||||
shrinks meaningfully: the messages dictionary lives in the static shell of
|
`cacheComponents` now enabled, the marketing routes' messages dictionary
|
||||||
every route, and only the dynamic island re-fetches. The static prerender
|
lives in the static shell — cached at the CDN with no per-request cost.
|
||||||
output is already cacheable at the CDN. So Phase 4 should be re-evaluated
|
The route-scoping is still a win for the dynamic islands' RSC payload, but
|
||||||
**after** the `cacheComponents` work lands, not before.
|
the static-shell win is now structural, not bundle-size dependent.
|
||||||
|
|
||||||
If we don't ship `cacheComponents`, Phase 4's bundle-size measurement
|
## Follow-up work
|
||||||
(Phase 4a) is still the right gate — measure first, refactor only if the
|
|
||||||
data justifies it.
|
|
||||||
|
|
||||||
## What to do now
|
`cacheComponents` and React Compiler annotation mode now ship. Remaining
|
||||||
|
work (file separately when scheduled):
|
||||||
|
|
||||||
- Skip both flags for this performance follow-ups plan.
|
1. **Migrate top React Compiler bail sites.** Fix the 31 latent warnings
|
||||||
- File two follow-up tickets:
|
(especially the hook-reference patterns in `CreateFlowContext` and the
|
||||||
1. "Enable `cacheComponents`: refactor `(app)`/`(admin)` layouts to
|
conditional hooks in `SelectInput.container`) so those files become
|
||||||
Suspense + cache primitives, remove `force-dynamic` from route segments."
|
compiler-eligible.
|
||||||
2. "Adopt React Compiler in annotation mode: install plugin, enable
|
2. **Annotate high-render containers with `"use memo"`.** Targets:
|
||||||
eslint rule, migrate top containers."
|
`CreateFlowProvider`, `AuthModalProvider`, list-heavy views. Measure
|
||||||
- Proceed with Phase 4a (measure) and let the data drive Phase 4b.
|
render counts before/after with React DevTools profiler.
|
||||||
|
3. **Flip React Compiler from `annotation` to `all`** once the bail list is
|
||||||
|
green and a critical mass of containers are annotated. Remove
|
||||||
|
hand-written `useMemo`/`useCallback` the compiler subsumes.
|
||||||
|
4. **Audit `<Suspense fallback={null}>` UX on (app)/(admin) routes.** If
|
||||||
|
blank-nav flash becomes noticeable on slow connections, replace the
|
||||||
|
`null` fallback with a static placeholder that doesn't read `usePathname`
|
||||||
|
(e.g. a `min-h` div sized to the nav).
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import nextPlugin from "@next/eslint-plugin-next";
|
|||||||
import globals from "globals";
|
import globals from "globals";
|
||||||
import react from "eslint-plugin-react";
|
import react from "eslint-plugin-react";
|
||||||
import reactHooks from "eslint-plugin-react-hooks";
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
|
import reactCompiler from "eslint-plugin-react-compiler";
|
||||||
|
|
||||||
const eslintConfig = [
|
const eslintConfig = [
|
||||||
// Base JavaScript recommended rules
|
// Base JavaScript recommended rules
|
||||||
@@ -51,6 +52,7 @@ const eslintConfig = [
|
|||||||
plugins: {
|
plugins: {
|
||||||
react,
|
react,
|
||||||
"react-hooks": reactHooks,
|
"react-hooks": reactHooks,
|
||||||
|
"react-compiler": reactCompiler,
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
@@ -62,6 +64,9 @@ const eslintConfig = [
|
|||||||
...reactHooks.configs.recommended.rules,
|
...reactHooks.configs.recommended.rules,
|
||||||
"react/react-in-jsx-scope": "off", // React 19 doesn't require React import
|
"react/react-in-jsx-scope": "off", // React 19 doesn't require React import
|
||||||
"react/prop-types": "off", // Using TypeScript for prop validation
|
"react/prop-types": "off", // Using TypeScript for prop validation
|
||||||
|
// Surface code the React Compiler would bail on. We run in annotation
|
||||||
|
// mode, so the rule is "warn" — informational, not a build blocker.
|
||||||
|
"react-compiler/react-compiler": "warn",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// TypeScript files configuration
|
// TypeScript files configuration
|
||||||
@@ -90,6 +95,7 @@ const eslintConfig = [
|
|||||||
"@next/next": nextPlugin,
|
"@next/next": nextPlugin,
|
||||||
react,
|
react,
|
||||||
"react-hooks": reactHooks,
|
"react-hooks": reactHooks,
|
||||||
|
"react-compiler": reactCompiler,
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
@@ -101,6 +107,9 @@ const eslintConfig = [
|
|||||||
...reactHooks.configs.recommended.rules,
|
...reactHooks.configs.recommended.rules,
|
||||||
"react/react-in-jsx-scope": "off", // React 19 doesn't require React import
|
"react/react-in-jsx-scope": "off", // React 19 doesn't require React import
|
||||||
"react/prop-types": "off", // Using TypeScript for prop validation
|
"react/prop-types": "off", // Using TypeScript for prop validation
|
||||||
|
// Surface code the React Compiler would bail on. We run in annotation
|
||||||
|
// mode, so the rule is "warn" — informational, not a build blocker.
|
||||||
|
"react-compiler/react-compiler": "warn",
|
||||||
"@typescript-eslint/no-unused-vars": [
|
"@typescript-eslint/no-unused-vars": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
|
|||||||
+15
-3
@@ -5,6 +5,14 @@ import createMDX from "@next/mdx";
|
|||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
serverExternalPackages: ["@prisma/client"],
|
serverExternalPackages: ["@prisma/client"],
|
||||||
|
// React Compiler — annotation mode: opt-in via the `"use memo"` directive at
|
||||||
|
// the top of a component/hook. With no annotations in the codebase yet, this
|
||||||
|
// is plumbing only (no behavior change). Migrate hand-written `useMemo`/
|
||||||
|
// `useCallback` containers incrementally and rely on `eslint-plugin-react-
|
||||||
|
// compiler` to surface any code that the compiler bails on.
|
||||||
|
reactCompiler: {
|
||||||
|
compilationMode: "annotation",
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* `next dev --turbopack` does not use `webpack()`; without this, `.svg`
|
* `next dev --turbopack` does not use `webpack()`; without this, `.svg`
|
||||||
* imports resolve as asset URLs and {@link app/components/asset/icon/Icon.tsx}
|
* imports resolve as asset URLs and {@link app/components/asset/icon/Icon.tsx}
|
||||||
@@ -23,10 +31,14 @@ const nextConfig = {
|
|||||||
experimental: {
|
experimental: {
|
||||||
optimizeCss: true,
|
optimizeCss: true,
|
||||||
optimizePackageImports: ["react", "react-dom"],
|
optimizePackageImports: ["react", "react-dom"],
|
||||||
|
// Cache Components (the Next 16 successor to `experimental.ppr`) — components
|
||||||
|
// without `"use cache"` are dynamic by default, and any cookies/headers
|
||||||
|
// access outside a `<Suspense>` boundary becomes a build-time error. The
|
||||||
|
// `(app)` and `(admin)` layouts wrap `<ConditionalNavigation />` in
|
||||||
|
// `<Suspense>` so the static shell prerenders while the session-aware nav
|
||||||
|
// streams in. Replaces the prior `force-dynamic` route-segment exports.
|
||||||
|
cacheComponents: true,
|
||||||
},
|
},
|
||||||
// Phase 3 canary stub (not enabled): React Compiler probe surfaces a missing
|
|
||||||
// `babel-plugin-react-compiler` dep — Next 16 also moved this top-level out
|
|
||||||
// of `experimental`. See `docs/perf/next16-eval.md` for evaluation results.
|
|
||||||
// Compression
|
// Compression
|
||||||
compress: true,
|
compress: true,
|
||||||
// Image optimization
|
// Image optimization
|
||||||
|
|||||||
Generated
+64
@@ -45,8 +45,10 @@
|
|||||||
"@typescript-eslint/parser": "^8.41.0",
|
"@typescript-eslint/parser": "^8.41.0",
|
||||||
"@vitejs/plugin-react": "^5.0.2",
|
"@vitejs/plugin-react": "^5.0.2",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "^16.0.0",
|
"eslint-config-next": "^16.0.0",
|
||||||
|
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
|
||||||
"eslint-plugin-storybook": "^10.4.1",
|
"eslint-plugin-storybook": "^10.4.1",
|
||||||
"globals": "^17.1.0",
|
"globals": "^17.1.0",
|
||||||
"jest-axe": "^10.0.0",
|
"jest-axe": "^10.0.0",
|
||||||
@@ -645,6 +647,24 @@
|
|||||||
"@babel/core": "^7.0.0"
|
"@babel/core": "^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/plugin-proposal-private-methods": {
|
||||||
|
"version": "7.18.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz",
|
||||||
|
"integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==",
|
||||||
|
"deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-create-class-features-plugin": "^7.18.6",
|
||||||
|
"@babel/helper-plugin-utils": "^7.18.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@babel/core": "^7.0.0-0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/plugin-proposal-private-property-in-object": {
|
"node_modules/@babel/plugin-proposal-private-property-in-object": {
|
||||||
"version": "7.21.0-placeholder-for-preset-env.2",
|
"version": "7.21.0-placeholder-for-preset-env.2",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
|
||||||
@@ -9164,6 +9184,16 @@
|
|||||||
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/babel-plugin-react-compiler": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.26.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bail": {
|
"node_modules/bail": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
|
||||||
@@ -12200,6 +12230,40 @@
|
|||||||
"eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
|
"eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eslint-plugin-react-compiler": {
|
||||||
|
"version": "19.1.0-rc.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-19.1.0-rc.2.tgz",
|
||||||
|
"integrity": "sha512-oKalwDGcD+RX9mf3NEO4zOoUMeLvjSvcbbEOpquzmzqEEM2MQdp7/FY/Hx9NzmUwFzH1W9SKTz5fihfMldpEYw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/core": "^7.24.4",
|
||||||
|
"@babel/parser": "^7.24.4",
|
||||||
|
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
||||||
|
"hermes-parser": "^0.25.1",
|
||||||
|
"zod": "^3.22.4",
|
||||||
|
"zod-validation-error": "^3.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.17.0 || ^16.0.0 || >= 18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"eslint": ">=7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eslint-plugin-react-compiler/node_modules/zod-validation-error": {
|
||||||
|
"version": "3.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.5.4.tgz",
|
||||||
|
"integrity": "sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.24.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eslint-plugin-react-hooks": {
|
"node_modules/eslint-plugin-react-hooks": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
|
||||||
|
|||||||
@@ -84,8 +84,10 @@
|
|||||||
"@typescript-eslint/parser": "^8.41.0",
|
"@typescript-eslint/parser": "^8.41.0",
|
||||||
"@vitejs/plugin-react": "^5.0.2",
|
"@vitejs/plugin-react": "^5.0.2",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "^16.0.0",
|
"eslint-config-next": "^16.0.0",
|
||||||
|
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
|
||||||
"eslint-plugin-storybook": "^10.4.1",
|
"eslint-plugin-storybook": "^10.4.1",
|
||||||
"globals": "^17.1.0",
|
"globals": "^17.1.0",
|
||||||
"jest-axe": "^10.0.0",
|
"jest-axe": "^10.0.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user