From da82ea4909bdc3f6ef74dd4e2e17f2e7c4a7f016 Mon Sep 17 00:00:00 2001 From: April Arcus Date: Mon, 13 Apr 2026 19:17:41 -0700 Subject: [PATCH 1/3] pass __GRAPHILE_APP__ payload through req instead of query params Previously, we were packing the properties used by __GRAPHILE_APP__ into the query params of a parsed URL object received by Next.js. This exploited a loophole by which the `search` field of the URL object remained the source of truth for navigation, but the `query` field would be written into `__NEXT_DATA__` whence it could be retrieved by @app/client/src/pages/_app.tsx upon bootstrap and written into the window global. Starting in Next.js 14, if both `search` and `query` are present, search will take precedence. Injecting these extra properties into the search segment will cause rehydration errors during navigation due to a mismatch between the browser history and what Next thinks the search segment should contain. In this commit, we change strategies and instead pack the extra props into the `req` object in @app/server/src/middleware/installSSR.ts, read them out of `ctx.req` in `MyApp.getInitialProps()` in @app/client/src/pages/_app.tsx, and return them as part of the props object. This causes Next to serializes them into the __NEXT_DATA__ bootstrap payload under the `props` key, whence they can be attached to `window.__GRAPHILE_APP__` as before. We also add some abstractions in @app/lib to centralize reading and writing to this global object in a type-safe manner. unblocks next 14.x --- @app/client/src/pages/_app.tsx | 25 ++++++++---------- @app/lib/src/graphileApp.ts | 32 ++++++++++++++++++++++++ @app/lib/src/index.tsx | 1 + @app/lib/src/withApollo.tsx | 9 +++---- @app/server/package.json | 1 + @app/server/src/middleware/installSSR.ts | 22 ++++++++-------- @app/server/tsconfig.json | 2 +- package.json | 2 +- yarn.lock | 1 + 9 files changed, 61 insertions(+), 34 deletions(-) create mode 100644 @app/lib/src/graphileApp.ts diff --git a/@app/client/src/pages/_app.tsx b/@app/client/src/pages/_app.tsx index a77be754..d511ffeb 100644 --- a/@app/client/src/pages/_app.tsx +++ b/@app/client/src/pages/_app.tsx @@ -3,22 +3,13 @@ import "nprogress/nprogress.css"; import "../styles.css"; import { ApolloClient, ApolloProvider } from "@apollo/client"; -import { withApollo } from "@app/lib"; +import { setGraphileApp, withApollo } from "@app/lib"; import { ConfigProvider, notification } from "antd"; import App from "next/app"; import Router from "next/router"; import NProgress from "nprogress"; import * as React from "react"; -declare global { - interface Window { - __GRAPHILE_APP__: { - ROOT_URL?: string; - T_AND_C_URL?: string; - }; - } -} - NProgress.configure({ showSpinner: false, }); @@ -29,10 +20,12 @@ if (typeof window !== "undefined") { throw new Error("Cannot read from __NEXT_DATA__ element"); } const data = JSON.parse(nextDataEl.textContent); - window.__GRAPHILE_APP__ = { - ROOT_URL: data.query.ROOT_URL, - T_AND_C_URL: data.query.T_AND_C_URL, - }; + if (!data.props?.graphileApp) { + throw new Error( + "Cannot find property props.graphileApp in __NEXT_DATA__. Was it returned correctly from MyApp.getInitialProps()?" + ); + } + setGraphileApp(data.props.graphileApp); Router.events.on("routeChangeStart", () => { NProgress.start(); @@ -65,7 +58,9 @@ class MyApp extends App<{ apollo: ApolloClient }> { pageProps = await Component.getInitialProps(ctx); } - return { pageProps }; + const graphileApp = ctx.req?.graphileApp; + + return { pageProps, graphileApp }; } render() { diff --git a/@app/lib/src/graphileApp.ts b/@app/lib/src/graphileApp.ts new file mode 100644 index 00000000..7ca420c3 --- /dev/null +++ b/@app/lib/src/graphileApp.ts @@ -0,0 +1,32 @@ +export interface GraphileApp { + CSRF_TOKEN: string; + ROOT_URL: string; + T_AND_C_URL?: string; +} + +type GraphileAppWindow = Window & { + __GRAPHILE_APP__?: GraphileApp; +}; + +export function setGraphileApp(incoming: GraphileApp): void { + const existing = (window as GraphileAppWindow).__GRAPHILE_APP__; + if (existing) { + if ( + existing.CSRF_TOKEN === incoming.CSRF_TOKEN && + existing.ROOT_URL === incoming.ROOT_URL && + existing.T_AND_C_URL === incoming.T_AND_C_URL + ) { + return; + } else { + throw new Error("window.__GRAPHILE_APP__ has already been set."); + } + } + (window as GraphileAppWindow).__GRAPHILE_APP__ = incoming; +} + +export function getGraphileApp(): GraphileApp { + const graphileApp = (window as GraphileAppWindow).__GRAPHILE_APP__; + if (!graphileApp) + throw new Error("window.__GRAPHILE_APP__ has not been set."); + return graphileApp; +} diff --git a/@app/lib/src/index.tsx b/@app/lib/src/index.tsx index 7e51630b..3358bab9 100644 --- a/@app/lib/src/index.tsx +++ b/@app/lib/src/index.tsx @@ -1,4 +1,5 @@ export * from "./errors"; export * from "./forms"; +export * from "./graphileApp"; export * from "./passwords"; export * from "./withApollo"; diff --git a/@app/lib/src/withApollo.tsx b/@app/lib/src/withApollo.tsx index 804f3a1d..76fdd1ab 100644 --- a/@app/lib/src/withApollo.tsx +++ b/@app/lib/src/withApollo.tsx @@ -15,6 +15,7 @@ import { Client, createClient } from "graphql-ws"; import { withApollo as withApolloBase } from "next-with-apollo"; import { GraphileApolloLink } from "./GraphileApolloLink"; +import { getGraphileApp } from "./graphileApp"; let wsClient: Client | null = null; @@ -98,12 +99,10 @@ function makeClientSideLink(ROOT_URL: string) { throw new Error("Must only makeClientSideLink once"); } _rootURL = ROOT_URL; - const nextDataEl = document.getElementById("__NEXT_DATA__"); - if (!nextDataEl || !nextDataEl.textContent) { - throw new Error("Cannot read from __NEXT_DATA__ element"); + const { CSRF_TOKEN } = getGraphileApp(); + if (!CSRF_TOKEN) { + throw new Error("Cannot read CSRF_TOKEN"); } - const data = JSON.parse(nextDataEl.textContent); - const CSRF_TOKEN = data.query.CSRF_TOKEN; const httpLink = new HttpLink({ uri: `${ROOT_URL}/graphql`, credentials: "same-origin", diff --git a/@app/server/package.json b/@app/server/package.json index b84ecd4f..9fa90ecb 100644 --- a/@app/server/package.json +++ b/@app/server/package.json @@ -48,6 +48,7 @@ "tslib": "^2.5.0" }, "devDependencies": { + "@app/lib": "0.0.0", "@types/node": "^18.14.2", "cross-env": "^7.0.3", "graphql": "^16.9.0", diff --git a/@app/server/src/middleware/installSSR.ts b/@app/server/src/middleware/installSSR.ts index 60298a60..5316eabe 100644 --- a/@app/server/src/middleware/installSSR.ts +++ b/@app/server/src/middleware/installSSR.ts @@ -1,7 +1,9 @@ +import type { GraphileApp } from "@app/lib" with { + "resolution-mode": "import", +}; import { Express } from "express"; import { createServer } from "http"; import next from "next"; -import { parse } from "url"; import { getUpgradeHandlers } from "../app"; @@ -35,17 +37,13 @@ export default async function installSSR(app: Express) { }); app.get("*", async (req, res) => { const handler = await handlerPromise; - const parsedUrl = parse(req.url, true); - handler(req, res, { - ...parsedUrl, - query: { - ...parsedUrl.query, - CSRF_TOKEN: req.csrfToken(), - // See 'next.config.js': - ROOT_URL: process.env.ROOT_URL || "http://localhost:5678", - T_AND_C_URL: process.env.T_AND_C_URL, - }, - }); + const graphileApp: GraphileApp = { + CSRF_TOKEN: req.csrfToken(), + ROOT_URL: process.env.ROOT_URL || "http://localhost:5678", + ...(process.env.T_AND_C_URL && { T_AND_C_URL: process.env.T_AND_C_URL }), + }; + (req as any).graphileApp = graphileApp; + handler(req, res); }); // Now handle websockets diff --git a/@app/server/tsconfig.json b/@app/server/tsconfig.json index 458803c9..7f48c3cb 100644 --- a/@app/server/tsconfig.json +++ b/@app/server/tsconfig.json @@ -10,7 +10,7 @@ "target": "es2018" }, "include": ["src"], - "references": [{ "path": "../config" }], + "references": [{ "path": "../config" }, { "path": "../lib" }], "ts-node": { "compilerOptions": { "rootDir": null diff --git a/package.json b/package.json index bb20d38a..c237e88f 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "lint:fix": "yarn eslint --fix . && yarn prettier:all --write", "eslint": "eslint --ext .js,.jsx,.ts,.tsx,.graphql", "prettier:all": "prettier --ignore-path .eslintignore \"**/*.{js,jsx,ts,tsx,graphql,md}\"", - "depcheck": "yarn workspaces foreach --verbose --topological --exclude ROOT --exclude docker-helpers exec depcheck --ignores=\"@app/config,@app/client,tslib,webpack,babel-plugin-import,source-map-support,@graphql-codegen/*,*eslint*,@typescript-eslint/*,graphql-toolkit,net,tls,dayjs,@types/jest,babel-jest,jest,mock-req,mock-res,nodemon,ts-jest,ts-loader,ts-node,update-dotenv,mkdirp,@types/helmet,helmet\" --ignore-dirs=\".next\"", + "depcheck": "yarn workspaces foreach --verbose --topological --exclude ROOT --exclude docker-helpers exec depcheck --ignores=\"@app/config,@app/client,@app/lib,tslib,webpack,babel-plugin-import,source-map-support,@graphql-codegen/*,*eslint*,@typescript-eslint/*,graphql-toolkit,net,tls,dayjs,@types/jest,babel-jest,jest,mock-req,mock-res,nodemon,ts-jest,ts-loader,ts-node,update-dotenv,mkdirp,@types/helmet,helmet\" --ignore-dirs=\".next\"", "dev": "yarn && yarn workspaces foreach --verbose --topological --exclude ROOT --exclude docker-helpers run codegen && tsc -b && concurrently --kill-others --names \"TSC,WATCH,RUN,TEST\" --prefix \"({name})\" --prefix-colors \"yellow.bold,yellow.bold,cyan.bold,greenBright.bold\" \"tsc -b --watch --preserveWatchOutput\" \"yarn workspaces foreach --verbose --parallel --interlaced --exclude ROOT --exclude docker-helpers run watch\" \"yarn workspaces foreach --verbose --parallel --interlaced --exclude ROOT --exclude docker-helpers run dev\" \"yarn test:watch --delay 10\"", "build": "yarn workspaces foreach --verbose --topological --exclude ROOT --exclude docker-helpers run build", "clean": "node ./scripts/clean.js", diff --git a/yarn.lock b/yarn.lock index 7a04a8a5..c726b121 100644 --- a/yarn.lock +++ b/yarn.lock @@ -346,6 +346,7 @@ __metadata: dependencies: "@app/client": 0.0.0 "@app/config": 0.0.0 + "@app/lib": 0.0.0 "@dataplan/json": ^1.0.0 "@dataplan/pg": ^1.0.0 "@graphile/simplify-inflection": ^8.0.0 From f82b68602f40cf03b9cd04d4af6751b6fe8b831f Mon Sep 17 00:00:00 2001 From: April Arcus Date: Thu, 9 Apr 2026 09:35:41 -0700 Subject: [PATCH 2/3] next -> 14.x --- @app/client/package.json | 2 +- @app/client/src/next.config.js | 42 +++++++ @app/components/package.json | 2 +- @app/lib/package.json | 2 +- @app/server/package.json | 2 +- yarn.lock | 196 +++++++++++++++------------------ 6 files changed, 132 insertions(+), 114 deletions(-) diff --git a/@app/client/package.json b/@app/client/package.json index a3e0c3c5..3a9a1b59 100644 --- a/@app/client/package.json +++ b/@app/client/package.json @@ -23,7 +23,7 @@ "graphql": "^16.9.0", "lodash": "^4.17.21", "net": "^1.0.2", - "next": "^13.2.3", + "next": "^14.2.35", "nprogress": "^0.2.0", "rc-field-form": "~1.27.4", "react": "^18.2.0", diff --git a/@app/client/src/next.config.js b/@app/client/src/next.config.js index c0761291..55531301 100644 --- a/@app/client/src/next.config.js +++ b/@app/client/src/next.config.js @@ -16,6 +16,48 @@ if (!process.env.ROOT_URL) { return { poweredByHeader: false, distDir: `../.next`, + // more @ant-design/* and rc-* may need to be added here depending on + // usage. `bundlePagesRouterDependencies` may be able to help with this + // config bloat in next.js 15 + // https://nextjs.org/docs/15/pages/api-reference/config/next-config-js/bundlePagesRouterDependencies + transpilePackages: [ + "@ant-design/icons", + "@ant-design/icons-svg", + "rc-cascader", + "rc-checkbox", + "rc-collapse", + "rc-dialog", + "rc-drawer", + "rc-dropdown", + "rc-field-form", + "rc-image", + "rc-input", + "rc-input-number", + "rc-mentions", + "rc-menu", + "rc-motion", + "rc-notification", + "rc-overflow", + "rc-pagination", + "rc-picker", + "rc-progress", + "rc-rate", + "rc-resize-observer", + "rc-segmented", + "rc-select", + "rc-slider", + "rc-steps", + "rc-switch", + "rc-table", + "rc-tabs", + "rc-textarea", + "rc-tooltip", + "rc-tree", + "rc-tree-select", + "rc-upload", + "rc-util", + "rc-virtual-list", + ], trailingSlash: false, webpack(config, { webpack, dev, isServer }) { const makeSafe = (externals) => { diff --git a/@app/components/package.json b/@app/components/package.json index be758ea1..08ad0d76 100644 --- a/@app/components/package.json +++ b/@app/components/package.json @@ -12,7 +12,7 @@ "@apollo/client": "3.6.10", "@app/graphql": "0.0.0", "antd": "5.3.3", - "next": "^13.2.3", + "next": "^14.2.35", "react": "^18.2.0", "tslib": "^2.5.0" }, diff --git a/@app/lib/package.json b/@app/lib/package.json index 6d6b1cd4..4bd84d5b 100644 --- a/@app/lib/package.json +++ b/@app/lib/package.json @@ -13,7 +13,7 @@ "grafast": "^1.0.0", "graphql": "^16.9.0", "graphql-ws": "^5.11.3", - "next": "^13.2.3", + "next": "^14.2.35", "next-with-apollo": "^5.3.0", "rc-field-form": "^1.27.4", "react": "^18.2.0", diff --git a/@app/server/package.json b/@app/server/package.json index 9fa90ecb..3c5ff574 100644 --- a/@app/server/package.json +++ b/@app/server/package.json @@ -37,7 +37,7 @@ "helmet": "^6.0.1", "lodash": "^4.17.21", "morgan": "^1.10.0", - "next": "^13.2.3", + "next": "^14.2.35", "passport": "^0.6.0", "passport-github2": "^0.1.12", "pg": "^8.9.0", diff --git a/yarn.lock b/yarn.lock index c726b121..c81fa0f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -235,7 +235,7 @@ __metadata: jest: ^29.7.0 lodash: ^4.17.21 net: ^1.0.2 - next: ^13.2.3 + next: ^14.2.35 nprogress: ^0.2.0 rc-field-form: ~1.27.4 react: ^18.2.0 @@ -256,7 +256,7 @@ __metadata: antd: 5.3.3 cross-env: ^7.0.3 jest: ^29.7.0 - next: ^13.2.3 + next: ^14.2.35 react: ^18.2.0 tslib: ^2.5.0 typescript: ~5.7.3 @@ -328,7 +328,7 @@ __metadata: graphql: ^16.9.0 graphql-ws: ^5.11.3 jest: ^29.7.0 - next: ^13.2.3 + next: ^14.2.35 next-with-apollo: ^5.3.0 postgraphile: ^5.0.0 rc-field-form: ^1.27.4 @@ -377,7 +377,7 @@ __metadata: lodash: ^4.17.21 mock-req: ^0.2.0 morgan: ^1.10.0 - next: ^13.2.3 + next: ^14.2.35 passport: ^0.6.0 passport-github2: ^0.1.12 pg: ^8.9.0 @@ -3212,10 +3212,10 @@ __metadata: languageName: node linkType: hard -"@next/env@npm:13.2.3": - version: 13.2.3 - resolution: "@next/env@npm:13.2.3" - checksum: e3d59b888c0e57ead895ec0c96ce92b6929684a1af8b3435e0142d21e6af21b1193f421f9d6b9b36ce7711a0379fb85033e25670fef3ba9c923cc77752be6c10 +"@next/env@npm:14.2.35": + version: 14.2.35 + resolution: "@next/env@npm:14.2.35" + checksum: d484b5f98abe2862bacb804a5ff70030f1453ec10788136b5f923286d0644c420f695cdca265c74ccdaaee45f0fc8507bf9bb84b5084545485b21543906ad230 languageName: node linkType: hard @@ -3228,93 +3228,65 @@ __metadata: languageName: node linkType: hard -"@next/swc-android-arm-eabi@npm:13.2.3": - version: 13.2.3 - resolution: "@next/swc-android-arm-eabi@npm:13.2.3" - conditions: os=android & cpu=arm - languageName: node - linkType: hard - -"@next/swc-android-arm64@npm:13.2.3": - version: 13.2.3 - resolution: "@next/swc-android-arm64@npm:13.2.3" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - -"@next/swc-darwin-arm64@npm:13.2.3": - version: 13.2.3 - resolution: "@next/swc-darwin-arm64@npm:13.2.3" +"@next/swc-darwin-arm64@npm:14.2.33": + version: 14.2.33 + resolution: "@next/swc-darwin-arm64@npm:14.2.33" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@next/swc-darwin-x64@npm:13.2.3": - version: 13.2.3 - resolution: "@next/swc-darwin-x64@npm:13.2.3" +"@next/swc-darwin-x64@npm:14.2.33": + version: 14.2.33 + resolution: "@next/swc-darwin-x64@npm:14.2.33" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@next/swc-freebsd-x64@npm:13.2.3": - version: 13.2.3 - resolution: "@next/swc-freebsd-x64@npm:13.2.3" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - -"@next/swc-linux-arm-gnueabihf@npm:13.2.3": - version: 13.2.3 - resolution: "@next/swc-linux-arm-gnueabihf@npm:13.2.3" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"@next/swc-linux-arm64-gnu@npm:13.2.3": - version: 13.2.3 - resolution: "@next/swc-linux-arm64-gnu@npm:13.2.3" +"@next/swc-linux-arm64-gnu@npm:14.2.33": + version: 14.2.33 + resolution: "@next/swc-linux-arm64-gnu@npm:14.2.33" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-arm64-musl@npm:13.2.3": - version: 13.2.3 - resolution: "@next/swc-linux-arm64-musl@npm:13.2.3" +"@next/swc-linux-arm64-musl@npm:14.2.33": + version: 14.2.33 + resolution: "@next/swc-linux-arm64-musl@npm:14.2.33" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@next/swc-linux-x64-gnu@npm:13.2.3": - version: 13.2.3 - resolution: "@next/swc-linux-x64-gnu@npm:13.2.3" +"@next/swc-linux-x64-gnu@npm:14.2.33": + version: 14.2.33 + resolution: "@next/swc-linux-x64-gnu@npm:14.2.33" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-x64-musl@npm:13.2.3": - version: 13.2.3 - resolution: "@next/swc-linux-x64-musl@npm:13.2.3" +"@next/swc-linux-x64-musl@npm:14.2.33": + version: 14.2.33 + resolution: "@next/swc-linux-x64-musl@npm:14.2.33" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@next/swc-win32-arm64-msvc@npm:13.2.3": - version: 13.2.3 - resolution: "@next/swc-win32-arm64-msvc@npm:13.2.3" +"@next/swc-win32-arm64-msvc@npm:14.2.33": + version: 14.2.33 + resolution: "@next/swc-win32-arm64-msvc@npm:14.2.33" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@next/swc-win32-ia32-msvc@npm:13.2.3": - version: 13.2.3 - resolution: "@next/swc-win32-ia32-msvc@npm:13.2.3" +"@next/swc-win32-ia32-msvc@npm:14.2.33": + version: 14.2.33 + resolution: "@next/swc-win32-ia32-msvc@npm:14.2.33" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@next/swc-win32-x64-msvc@npm:13.2.3": - version: 13.2.3 - resolution: "@next/swc-win32-x64-msvc@npm:13.2.3" +"@next/swc-win32-x64-msvc@npm:14.2.33": + version: 14.2.33 + resolution: "@next/swc-win32-x64-msvc@npm:14.2.33" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -4323,12 +4295,20 @@ __metadata: languageName: node linkType: hard -"@swc/helpers@npm:0.4.14": - version: 0.4.14 - resolution: "@swc/helpers@npm:0.4.14" +"@swc/counter@npm:^0.1.3": + version: 0.1.3 + resolution: "@swc/counter@npm:0.1.3" + checksum: df8f9cfba9904d3d60f511664c70d23bb323b3a0803ec9890f60133954173047ba9bdeabce28cd70ba89ccd3fd6c71c7b0bd58be85f611e1ffbe5d5c18616598 + languageName: node + linkType: hard + +"@swc/helpers@npm:0.5.5": + version: 0.5.5 + resolution: "@swc/helpers@npm:0.5.5" dependencies: + "@swc/counter": ^0.1.3 tslib: ^2.4.0 - checksum: 273fd3f3fc461a92f3790cc551ea054745c6d6959afbe1232e6d7aa1c722bbc114d308aab96bef5c78fc0303c85c7b472ef00e2253251cc89737f3b1af56e5a5 + checksum: d4f207b191e54b29460804ddf2984ba6ece1d679a0b2f6a9c765dcf27bba92c5769e7965668a4546fb9f1021eaf0ff9be4bf5c235ce12adcd65acdfe77187d11 languageName: node linkType: hard @@ -6778,7 +6758,7 @@ __metadata: languageName: node linkType: hard -"busboy@npm:^1.6.0": +"busboy@npm:1.6.0, busboy@npm:^1.6.0": version: 1.6.0 resolution: "busboy@npm:1.6.0" dependencies: @@ -6900,7 +6880,7 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001406, caniuse-lite@npm:^1.0.30001782": +"caniuse-lite@npm:^1.0.30001579, caniuse-lite@npm:^1.0.30001782": version: 1.0.30001787 resolution: "caniuse-lite@npm:1.0.30001787" checksum: 00f548869c36ae5db59975c94f38de73a410c90568ec76fa6c55936e9c88d296593efe771cdf10e3a233b4ae3c069f2271d0b1a4dfae5ee4baec4bb6aaaf4764 @@ -13302,6 +13282,15 @@ __metadata: languageName: node linkType: hard +"nanoid@npm:^3.3.6": + version: 3.3.11 + resolution: "nanoid@npm:3.3.11" + bin: + nanoid: bin/nanoid.cjs + checksum: 3be20d8866a57a6b6d218e82549711c8352ed969f9ab3c45379da28f405363ad4c9aeb0b39e9abc101a529ca65a72ff9502b00bf74a912c4b64a9d62dfd26c29 + languageName: node + linkType: hard + "nanolru@npm:^1.0.0": version: 1.0.0 resolution: "nanolru@npm:1.0.0" @@ -13359,48 +13348,37 @@ __metadata: languageName: node linkType: hard -"next@npm:^13.2.3": - version: 13.2.3 - resolution: "next@npm:13.2.3" - dependencies: - "@next/env": 13.2.3 - "@next/swc-android-arm-eabi": 13.2.3 - "@next/swc-android-arm64": 13.2.3 - "@next/swc-darwin-arm64": 13.2.3 - "@next/swc-darwin-x64": 13.2.3 - "@next/swc-freebsd-x64": 13.2.3 - "@next/swc-linux-arm-gnueabihf": 13.2.3 - "@next/swc-linux-arm64-gnu": 13.2.3 - "@next/swc-linux-arm64-musl": 13.2.3 - "@next/swc-linux-x64-gnu": 13.2.3 - "@next/swc-linux-x64-musl": 13.2.3 - "@next/swc-win32-arm64-msvc": 13.2.3 - "@next/swc-win32-ia32-msvc": 13.2.3 - "@next/swc-win32-x64-msvc": 13.2.3 - "@swc/helpers": 0.4.14 - caniuse-lite: ^1.0.30001406 - postcss: 8.4.14 +"next@npm:^14.2.35": + version: 14.2.35 + resolution: "next@npm:14.2.35" + dependencies: + "@next/env": 14.2.35 + "@next/swc-darwin-arm64": 14.2.33 + "@next/swc-darwin-x64": 14.2.33 + "@next/swc-linux-arm64-gnu": 14.2.33 + "@next/swc-linux-arm64-musl": 14.2.33 + "@next/swc-linux-x64-gnu": 14.2.33 + "@next/swc-linux-x64-musl": 14.2.33 + "@next/swc-win32-arm64-msvc": 14.2.33 + "@next/swc-win32-ia32-msvc": 14.2.33 + "@next/swc-win32-x64-msvc": 14.2.33 + "@swc/helpers": 0.5.5 + busboy: 1.6.0 + caniuse-lite: ^1.0.30001579 + graceful-fs: ^4.2.11 + postcss: 8.4.31 styled-jsx: 5.1.1 peerDependencies: - "@opentelemetry/api": ^1.4.0 - fibers: ">= 3.1.0" - node-sass: ^6.0.0 || ^7.0.0 + "@opentelemetry/api": ^1.1.0 + "@playwright/test": ^1.41.2 react: ^18.2.0 react-dom: ^18.2.0 sass: ^1.3.0 dependenciesMeta: - "@next/swc-android-arm-eabi": - optional: true - "@next/swc-android-arm64": - optional: true "@next/swc-darwin-arm64": optional: true "@next/swc-darwin-x64": optional: true - "@next/swc-freebsd-x64": - optional: true - "@next/swc-linux-arm-gnueabihf": - optional: true "@next/swc-linux-arm64-gnu": optional: true "@next/swc-linux-arm64-musl": @@ -13418,15 +13396,13 @@ __metadata: peerDependenciesMeta: "@opentelemetry/api": optional: true - fibers: - optional: true - node-sass: + "@playwright/test": optional: true sass: optional: true bin: next: dist/bin/next - checksum: 5147078cebf6156acd1dc54208b02852d23c50fe9fae9de444d9982090b63ac1975284d6e8d1647b78c98508c6d4be90a2debc3a12e383db423188724d38e6a9 + checksum: a5c25e1387a066720273045cd15222e1c1556d394a15624b148be389cf4e9b876d1dc22f50b97d3c7a97f765560a90eae56585b7a80d74dacd25496da09dc96d languageName: node linkType: hard @@ -14402,14 +14378,14 @@ __metadata: languageName: node linkType: hard -"postcss@npm:8.4.14": - version: 8.4.14 - resolution: "postcss@npm:8.4.14" +"postcss@npm:8.4.31": + version: 8.4.31 + resolution: "postcss@npm:8.4.31" dependencies: - nanoid: ^3.3.4 + nanoid: ^3.3.6 picocolors: ^1.0.0 source-map-js: ^1.0.2 - checksum: fe58766ff32e4becf65a7d57678995cfd239df6deed2fe0557f038b47c94e4132e7e5f68b5aa820c13adfec32e523b693efaeb65798efb995ce49ccd83953816 + checksum: 1d8611341b073143ad90486fcdfeab49edd243377b1f51834dc4f6d028e82ce5190e4f11bb2633276864503654fb7cab28e67abdc0fbf9d1f88cad4a0ff0beea languageName: node linkType: hard From 35cda3acb56f7b96835ad0352dcef3e48a0b3dda Mon Sep 17 00:00:00 2001 From: April Arcus Date: Tue, 14 Apr 2026 10:10:19 -0700 Subject: [PATCH 3/3] fix next hmr upgrades in custom server Next 14 wires its custom-server websocket listener lazily from the request handler path, so reading the fake server's upgrade listener during SSR middleware installation can miss it. That leaves the central upgrade dispatcher without a Next handler and causes /_next/webpack-hmr connections to be destroyed. Register the Next upgrade dispatcher entry up front, but resolve the listener from the fake server when a matching upgrade arrives. This keeps the existing single-server websocket dispatch model without reaching into Next's private custom-server fields. --- @app/server/src/middleware/installSSR.ts | 48 +++++++++++++----------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/@app/server/src/middleware/installSSR.ts b/@app/server/src/middleware/installSSR.ts index 5316eabe..8a8e4c93 100644 --- a/@app/server/src/middleware/installSSR.ts +++ b/@app/server/src/middleware/installSSR.ts @@ -22,8 +22,10 @@ export default async function installSSR(app: Express) { // Don't specify 'conf' key // Trick Next.js into adding its upgrade handler here, so we can extract - // it. Calling `getUpgradeHandler()` is insufficient because that doesn't - // handle the assets. + // it. Calling `getUpgradeHandler()` is insufficient here: in Next + // custom-server mode it follows the inherited server handleUpgrade path, + // while dev HMR uses the upgrade handler returned by getRequestHandlers() + // and wired onto httpServer. httpServer: fakeHttpServer, }); const handlerPromise = (async () => { @@ -46,23 +48,27 @@ export default async function installSSR(app: Express) { handler(req, res); }); - // Now handle websockets - if (!(nextApp as any).getServer) { - console.warn( - `Our Next.js workaround for getting the upgrade handler without giving Next.js dominion over all websockets might no longer work - nextApp.getServer (private API) is no more.` - ); - } else { - await (nextApp as any).getServer(); - } - const nextJsUpgradeHandler = fakeHttpServer.listeners("upgrade")[0] as any; - if (nextJsUpgradeHandler) { - const upgradeHandlers = getUpgradeHandlers(app); - upgradeHandlers.push({ - name: "Next.js", - check(req) { - return req.url?.includes("/_next/") ?? false; - }, - upgrade: nextJsUpgradeHandler, - }); - } + // Next.js wires its custom-server websocket listener lazily the first time + // its request handler runs. Register our dispatcher entry now, then read the + // listener from the fake server when a matching upgrade arrives. + let nextJsUpgradeHandler: ReturnType[number]; + const upgradeHandlers = getUpgradeHandlers(app); + upgradeHandlers.push({ + name: "Next.js", + check(req) { + if (req.url == null) return false; + return req.url.includes("/_next/"); + }, + upgrade(req, socket, head) { + nextJsUpgradeHandler ??= fakeHttpServer.listeners("upgrade")[0]; + if (typeof nextJsUpgradeHandler === "function") { + nextJsUpgradeHandler(req, socket, head); + } else { + console.error( + `Next.js websocket upgrade handler was not installed before an upgrade request for ${req.url ?? ""}.` + ); + socket.destroy(); + } + }, + }); }