Skip to content

Commit 9906b4f

Browse files
committed
Upgrade to Next.js@15 and React@19
This upgrades Next.js by running `npx @next/codemod@canary upgrade latest`, which also runs `npx codemod@latest react/19/migration-recipe`. Additional manual changes I had to do: 1. Disable Turbopack in dev mode. It wasn't clear to me how to use raw-loader to load .ftl files, so I postponed that for later. 2. Turned `getExperimentationId` into an async function. I did this because it calls `headers()`, which is async now. All its invocations where in async functions anyway. 3. Server components should now pass the accept language to get l10n In most environments, getL10n() and getL10nBundles() can be synchronous functions, and just read the language preferences directly. However, on the server-side, reading the headers is now asynchronous. Thus, I've modified the l10n generation functions to require the accept language argument if no sync function can be provided that returns it. Not great, but better than forcing the same functions in e.g. client components to be async. An alternative would have been to simply make the getL10n() and getL10nBundles() functions asynchronous for the server, but I imagine that would be even more confusing than just making an optional argument required. 4. Dynamically import react-dom/server(.edge) In `/src/emails/renderEmail`, we _want_ to render HTML to a string, rather than generating a server component that the server serves, but Next.js throws an error if we do that directly. Hence, I had to replace that with a dynamic import as a workaround, turning `renderEmail` into an async function. Additionally, I got errors in the tests when not importing it from .edge. 5. Move `serverExternalPackages` out of `experimental`
1 parent bfde08d commit 9906b4f

File tree

79 files changed

+3545
-4560
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+3545
-4560
lines changed

jest.setup.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,22 @@ global.IntersectionObserver = jest.fn();
4040
defaultFallbackInView(false);
4141
beforeEach(() => {
4242
setupIntersectionMocking(jest.fn);
43+
44+
// react-dom/server.edge is apparently needed instead of react-dom/server
45+
// to avoid this error:
46+
// > Uncaught ReferenceError: MessageChannel is not defined
47+
// See https://github.com/jsdom/jsdom/issues/2448#issuecomment-1581009331
48+
window.MessageChannel = jest.fn().mockImplementation(() => {
49+
return {
50+
port1: {
51+
postMessage: jest.fn(),
52+
},
53+
port2: {
54+
addEventListener: jest.fn(),
55+
removeEventListener: jest.fn(),
56+
},
57+
};
58+
});
4359
});
4460
afterEach(() => {
4561
resetIntersectionMocking();

next-env.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
/// <reference types="next/image-types/global" />
33

44
// NOTE: This file should not be edited
5-
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
5+
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

next.config.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,9 @@ const nextConfig = {
175175

176176
return config;
177177
},
178-
experimental: {
179-
// Without this setting, Next.js has Webpack trying and failing to load
180-
// uglify-js when compiling MJML email templates to HTML in `renderEmail.ts`:
181-
serverComponentsExternalPackages: ["mjml"],
182-
// Sentry 8.x requires `instrumentation.ts` vs. it's previous custom approach.
183-
instrumentationHook: true,
184-
},
178+
// Without this setting, Next.js has Webpack trying and failing to load
179+
// uglify-js when compiling MJML email templates to HTML in `renderEmail.ts`:
180+
serverExternalPackages: ["mjml"],
185181
};
186182

187183
const sentryOptions = {

package-lock.json

Lines changed: 3124 additions & 4309 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,32 +80,32 @@
8080
"@grpc/grpc-js": "1.12.2",
8181
"@leeoniya/ufuzzy": "^1.0.17",
8282
"@mozilla/glean": "^5.0.3",
83-
"@next/third-parties": "^14.2.15",
83+
"@next/third-parties": "15.1.4",
8484
"@sentry/nextjs": "^8.48.0",
8585
"@sentry/node": "^8.0.0",
8686
"@sentry/utils": "^8.48.0",
8787
"@stripe/stripe-js": "^5.5.0",
8888
"@types/jsdom": "^21.1.7",
8989
"@types/node": "^22.10.5",
90-
"@types/react": "^18.3.12",
91-
"@types/react-dom": "^18.3.1",
90+
"@types/react": "19.0.3",
91+
"@types/react-dom": "19.0.2",
9292
"canvas-confetti": "^1.9.3",
9393
"dotenv-flow": "^4.1.0",
94-
"eslint-config-next": "^14.2.15",
94+
"eslint-config-next": "15.1.4",
9595
"ioredis": "^5.4.2",
9696
"jsdom": "^25.0.1",
9797
"jsonwebtoken": "^9.0.2",
9898
"jwk-to-pem": "^2.0.7",
9999
"knex": "^3.1.0",
100100
"mjml": "^4.15.3",
101-
"next": "^14.2.22",
101+
"next": "15.1.4",
102102
"next-auth": "^4.24.11",
103103
"nodemailer": "^6.9.16",
104104
"pg": "^8.13.1",
105-
"react": "^18.3.1",
105+
"react": "19.0.0",
106106
"react-aria": "^3.36.0",
107107
"react-cookie": "^7.2.2",
108-
"react-dom": "^18.3.1",
108+
"react-dom": "19.0.0",
109109
"react-intersection-observer": "^9.15.1",
110110
"react-stately": "^3.34.0",
111111
"react-toastify": "^11.0.2",
@@ -166,5 +166,9 @@
166166
"tsx": "^4.19.2",
167167
"typescript": "^5.7.3",
168168
"yaml": "^2.7.0"
169+
},
170+
"overrides": {
171+
"@types/react": "19.0.3",
172+
"@types/react-dom": "19.0.2"
169173
}
170174
}

src/app/(proper_react)/(redesign)/(authenticated)/admin/emails/actions.tsx

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import { renderEmail } from "../../../../../../emails/renderEmail";
1010
import { VerifyEmailAddressEmail } from "../../../../../../emails/templates/verifyEmailAddress/VerifyEmailAddressEmail";
1111
import { sanitizeSubscriberRow } from "../../../../../functions/server/sanitize";
1212
import { getServerSession } from "../../../../../functions/server/getServerSession";
13-
import { getL10n } from "../../../../../functions/l10n/serverComponents";
13+
import {
14+
getAcceptLangHeaderInServerComponents,
15+
getL10n,
16+
} from "../../../../../functions/l10n/serverComponents";
1417
import { getSubscriberByFxaUid } from "../../../../../../db/tables/subscribers";
1518
import { ReactNode } from "react";
1619
import { SubscriberRow } from "knex/types/tables";
@@ -80,7 +83,7 @@ async function send(
8083
return sendEmail(
8184
emailAddress,
8285
"Test email: " + subject,
83-
renderEmail(template),
86+
await renderEmail(template),
8487
);
8588
}
8689

@@ -90,7 +93,8 @@ export async function triggerSignupReportEmail(emailAddress: string) {
9093
return false;
9194
}
9295

93-
const l10n = getL10n();
96+
const acceptLangHeader = await getAcceptLangHeaderInServerComponents();
97+
const l10n = getL10n(acceptLangHeader);
9498
const breaches = await getBreachesForEmail(
9599
getSha1(emailAddress),
96100
await getBreaches(),
@@ -115,7 +119,8 @@ export async function triggerVerificationEmail(emailAddress: string) {
115119
return false;
116120
}
117121

118-
const l10n = getL10n();
122+
const acceptLangHeader = await getAcceptLangHeaderInServerComponents();
123+
const l10n = getL10n(acceptLangHeader);
119124
await send(
120125
emailAddress,
121126
l10n.getString("email-subject-verify"),
@@ -135,7 +140,8 @@ export async function triggerMonthlyActivityFree(emailAddress: string) {
135140
return false;
136141
}
137142

138-
const l10n = getL10n();
143+
const acceptLangHeader = await getAcceptLangHeaderInServerComponents();
144+
const l10n = getL10n(acceptLangHeader);
139145

140146
if (typeof subscriber.onerep_profile_id === "number") {
141147
await refreshStoredScanResults(subscriber.onerep_profile_id);
@@ -148,7 +154,7 @@ export async function triggerMonthlyActivityFree(emailAddress: string) {
148154
latestScan.results,
149155
await getSubscriberBreaches({
150156
fxaUid: session.user.subscriber?.fxa_uid,
151-
countryCode: getCountryCode(headers()),
157+
countryCode: getCountryCode(await headers()),
152158
}),
153159
);
154160

@@ -174,7 +180,8 @@ export async function triggerMonthlyActivityPlus(emailAddress: string) {
174180
return false;
175181
}
176182

177-
const l10n = getL10n();
183+
const acceptLangHeader = await getAcceptLangHeaderInServerComponents();
184+
const l10n = getL10n(acceptLangHeader);
178185

179186
if (typeof subscriber.onerep_profile_id === "number") {
180187
await refreshStoredScanResults(subscriber.onerep_profile_id);
@@ -187,7 +194,7 @@ export async function triggerMonthlyActivityPlus(emailAddress: string) {
187194
latestScan.results,
188195
await getSubscriberBreaches({
189196
fxaUid: session.user.subscriber?.fxa_uid,
190-
countryCode: getCountryCode(headers()),
197+
countryCode: getCountryCode(await headers()),
191198
}),
192199
);
193200

@@ -212,7 +219,8 @@ export async function triggerBreachAlert(
212219
return false;
213220
}
214221

215-
const l10n = getL10n();
222+
const acceptLangHeader = await getAcceptLangHeaderInServerComponents();
223+
const l10n = getL10n(acceptLangHeader);
216224

217225
const assumedCountryCode = getSignupLocaleCountry(subscriber);
218226

@@ -260,7 +268,8 @@ export async function triggerBreachAlert(
260268
}
261269

262270
export async function triggerFirstDataBrokerRemovalFixed(emailAddress: string) {
263-
const l10n = getL10n();
271+
const acceptLangHeader = await getAcceptLangHeaderInServerComponents();
272+
const l10n = getL10n(acceptLangHeader);
264273
const randomScanResult = createRandomScanResult({ status: "removed" });
265274

266275
await send(

src/app/(proper_react)/(redesign)/(authenticated)/admin/removals/page.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,18 @@ import { isAdmin } from "../../../../../api/utils/auth";
88
import { NoResults, Removals } from "./Removals";
99
import { getStuckRemovals } from "../../../../../functions/server/getStuckRemovals";
1010

11-
export default async function Page({
12-
searchParams: { page = "1", days = "30", perPage = "100" },
13-
}: {
14-
searchParams: {
11+
export default async function Page(props: {
12+
searchParams: Promise<{
1513
query?: string;
1614
page: string;
1715
days: string;
1816
perPage: string;
19-
};
17+
}>;
2018
}) {
19+
const searchParams = await props.searchParams;
20+
21+
const { page = "1", days = "30", perPage = "100" } = searchParams;
22+
2123
const session = await getServerSession();
2224

2325
if (!isAdmin(session?.user?.email || "")) {

src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/[[...slug]]/page.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,27 @@ import { getExperimentationId } from "../../../../../../../functions/server/getE
4141
import { getElapsedTimeInDaysSinceInitialScan } from "../../../../../../../functions/server/getElapsedTimeInDaysSinceInitialScan";
4242
import { getExperiments } from "../../../../../../../functions/server/getExperiments";
4343
import { getLocale } from "../../../../../../../functions/universal/getLocale";
44-
import { getL10n } from "../../../../../../../functions/l10n/serverComponents";
44+
import {
45+
getAcceptLangHeaderInServerComponents,
46+
getL10n,
47+
} from "../../../../../../../functions/l10n/serverComponents";
4548
import { getDataBrokerRemovalTimeEstimates } from "../../../../../../../functions/server/getDataBrokerRemovalTimeEstimates";
4649

4750
const dashboardTabSlugs = ["action-needed", "fixed"];
4851

4952
type Props = {
50-
params: {
53+
params: Promise<{
5154
slug: string[] | undefined;
52-
};
53-
searchParams: {
55+
}>;
56+
searchParams: Promise<{
5457
nimbus_preview?: string;
5558
dialog?: "subscriptions";
56-
};
59+
}>;
5760
};
5861

59-
export default async function DashboardPage({ params, searchParams }: Props) {
62+
export default async function DashboardPage(props: Props) {
63+
const searchParams = await props.searchParams;
64+
const params = await props.params;
6065
const session = await getServerSession();
6166
if (!checkSession(session) || !session?.user?.subscriber?.id) {
6267
return redirect("/auth/logout");
@@ -74,7 +79,7 @@ export default async function DashboardPage({ params, searchParams }: Props) {
7479
return redirect(`/user/dashboard/${defaultTab}`);
7580
}
7681

77-
const headersList = headers();
82+
const headersList = await headers();
7883
const countryCode = getCountryCode(headersList);
7984

8085
const profileId = await getOnerepProfileId(session.user.subscriber.id);
@@ -125,11 +130,11 @@ export default async function DashboardPage({ params, searchParams }: Props) {
125130
});
126131
const userIsEligibleForPremium = isEligibleForPremium(countryCode);
127132

128-
const experimentationId = getExperimentationId(session.user);
133+
const experimentationId = await getExperimentationId(session.user);
129134
const experimentData = await getExperiments({
130135
experimentationId: experimentationId,
131136
countryCode: countryCode,
132-
locale: getLocale(getL10n()),
137+
locale: getLocale(getL10n(await getAcceptLangHeaderInServerComponents())),
133138
previewMode: searchParams.nimbus_preview === "true",
134139
});
135140

src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/automatic-remove/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export default async function AutomaticRemovePage() {
4242
email: session.user.email,
4343
});
4444

45-
const countryCode = getCountryCode(headers());
45+
const countryCode = getCountryCode(await headers());
4646
const profileId = await getOnerepProfileId(session.user.subscriber.id);
4747
const scanData = await getScanResultsWithBroker(
4848
profileId,

src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/data-broker-profiles/manual-remove/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default async function ManualRemovePage() {
2626
email: session.user.email,
2727
});
2828

29-
const countryCode = getCountryCode(headers());
29+
const countryCode = getCountryCode(await headers());
3030
const profileId = await getOnerepProfileId(session.user.subscriber.id);
3131
const scanData = await getScanResultsWithBroker(
3232
profileId,

0 commit comments

Comments
 (0)