Fix memory leak

This commit is contained in:
Konstantin Wohlwend 2026-04-18 22:20:45 -07:00
parent d568ad5149
commit 560ee4c16e
5 changed files with 356 additions and 31 deletions

View File

@ -11,7 +11,7 @@
"with-env:dev": "dotenv -c development --",
"with-env:prod": "dotenv -c production --",
"with-env:test": "dotenv -c test --",
"dev": "BACKEND_PORT=${STACK_DEV_FALLBACK_BACKEND:+${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}10} && BACKEND_PORT=${BACKEND_PORT:-${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02} && concurrently -n \"dev,codegen,prisma-studio,email-queue,cron-jobs,bulldozer-studio\" -k \"next dev --port $BACKEND_PORT ${STACK_BACKEND_DEV_EXTRA_ARGS:-}\" \"pnpm run codegen:watch\" \"pnpm run prisma-studio\" \"pnpm run run-email-queue\" \"pnpm run run-cron-jobs\" \"pnpm run run-bulldozer-studio\"",
"dev": "BACKEND_PORT=${STACK_DEV_FALLBACK_BACKEND:+${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}10} && BACKEND_PORT=${BACKEND_PORT:-${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02} && concurrently -n \"dev,codegen,prisma-studio,email-queue,cron-jobs,bulldozer-studio\" -k \"STACK_DISABLE_REACT_ASYNC_DEBUG_INFO=${STACK_DISABLE_REACT_ASYNC_DEBUG_INFO:-true} next dev --port $BACKEND_PORT ${STACK_BACKEND_DEV_EXTRA_ARGS:-}\" \"pnpm run codegen:watch\" \"pnpm run prisma-studio\" \"pnpm run run-email-queue\" \"pnpm run run-cron-jobs\" \"pnpm run run-bulldozer-studio\"",
"dev:inspect": "STACK_BACKEND_DEV_EXTRA_ARGS=\"--inspect\" pnpm run dev",
"dev:profile": "STACK_BACKEND_DEV_EXTRA_ARGS=\"--experimental-cpu-prof\" pnpm run dev",
"build": "pnpm run codegen && next build",

View File

@ -377,3 +377,9 @@ A: Any trigger callbacks written as `(changesTable) => ...` can fail against the
Q: How should `x-stack-override-error-status` behave in backend smart responses?
A: In `apps/backend/src/route-handlers/smart-response.tsx`, only override `4xx` responses to `200` with `x-stack-actual-status`. Do not override `5xx`, so infrastructure/runtime failures still surface as real server errors.
Q: Why can `email-queue-step` heap growth still point at `app-page-turbo.runtime.dev.js` after disabling React async debug info in `react-server-dom-*` bundles?
A: Next.js dev app-page runtimes (`app-page*.runtime.dev.js`) include their own inlined async debug hook (`async_hooks.createHook`) with `pendingOperations` nodes plus stack-frame arrays from `collectStackTracePrivate`. Patching only `react-server-dom-*` is not enough. Gate the app-page runtime hook with `STACK_DISABLE_REACT_ASYNC_DEBUG_INFO` too; once gated, inspector allocation samples stop showing `init`/`collectStackTracePrivate` in `app-page-turbo.runtime.dev.js` and per-burst retained deltas drop from multi-MB to near-baseline noise.
Q: How can we replace the huge `next@16.1.7` patch file with a resilient install-time rewrite?
A: Use a strict root `postinstall` script that rewrites only Next `>=16` app-page dev runtime bundles (`app-page*.runtime.dev.js`) from `doNotLimit=new WeakSet;async_hooks.createHook(` to the guarded `STACK_DISABLE_REACT_ASYNC_DEBUG_INFO` form. Guardrails should fail loud on marker mismatches, mixed guarded/unguarded states, replacement counts not equal to one, or missing runtime fingerprints; the script should also be idempotent (`patched=0, alreadyPatched>0` on second run).

View File

@ -8,6 +8,7 @@
"pre-preinstall": "pnpx only-allow@1.2.2 pnpm && node -e \"if(process.env.STACK_SKIP_TEMPLATE_GENERATION !== 'true') require('child_process').execSync('pnpm exec tsx ./scripts/generate-sdks.ts', {stdio: 'inherit'})\"",
"pre": "pnpm pre-preinstall",
"preinstall": "pnpm pre-preinstall",
"postinstall": "node ./scripts/postinstall-patch-next-async-debug-info.mjs",
"typecheck": "pnpm pre && turbo typecheck --",
"build:dev": "pnpm pre && NODE_ENV=development pnpm run build",
"build": "pnpm pre && turbo build",

View File

@ -1020,7 +1020,7 @@ importers:
devDependencies:
mint:
specifier: ^4.2.487
version: 4.2.487(@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/node@20.17.6)(@types/react@18.3.12)(encoding@0.1.13)(react-dom@19.2.3(react@19.2.3))(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0)
version: 4.2.487(@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/node@24.9.2)(@types/react@18.3.12)(encoding@0.1.13)(react-dom@19.2.3(react@19.2.3))(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0)
examples/cjs-test:
dependencies:
@ -22672,6 +22672,16 @@ snapshots:
optionalDependencies:
'@types/node': 20.17.6
'@inquirer/checkbox@4.3.2(@types/node@24.9.2)':
dependencies:
'@inquirer/ansi': 1.0.2
'@inquirer/core': 10.3.2(@types/node@24.9.2)
'@inquirer/figures': 1.0.15
'@inquirer/type': 3.0.10(@types/node@24.9.2)
yoctocolors-cjs: 2.1.3
optionalDependencies:
'@types/node': 24.9.2
'@inquirer/confirm@5.1.21(@types/node@20.17.6)':
dependencies:
'@inquirer/core': 10.3.2(@types/node@20.17.6)
@ -22679,6 +22689,13 @@ snapshots:
optionalDependencies:
'@types/node': 20.17.6
'@inquirer/confirm@5.1.21(@types/node@24.9.2)':
dependencies:
'@inquirer/core': 10.3.2(@types/node@24.9.2)
'@inquirer/type': 3.0.10(@types/node@24.9.2)
optionalDependencies:
'@types/node': 24.9.2
'@inquirer/core@10.3.2(@types/node@20.17.6)':
dependencies:
'@inquirer/ansi': 1.0.2
@ -22692,6 +22709,19 @@ snapshots:
optionalDependencies:
'@types/node': 20.17.6
'@inquirer/core@10.3.2(@types/node@24.9.2)':
dependencies:
'@inquirer/ansi': 1.0.2
'@inquirer/figures': 1.0.15
'@inquirer/type': 3.0.10(@types/node@24.9.2)
cli-width: 4.1.0
mute-stream: 2.0.0
signal-exit: 4.1.0
wrap-ansi: 6.2.0
yoctocolors-cjs: 2.1.3
optionalDependencies:
'@types/node': 24.9.2
'@inquirer/editor@4.2.23(@types/node@20.17.6)':
dependencies:
'@inquirer/core': 10.3.2(@types/node@20.17.6)
@ -22700,6 +22730,14 @@ snapshots:
optionalDependencies:
'@types/node': 20.17.6
'@inquirer/editor@4.2.23(@types/node@24.9.2)':
dependencies:
'@inquirer/core': 10.3.2(@types/node@24.9.2)
'@inquirer/external-editor': 1.0.3(@types/node@24.9.2)
'@inquirer/type': 3.0.10(@types/node@24.9.2)
optionalDependencies:
'@types/node': 24.9.2
'@inquirer/expand@4.0.23(@types/node@20.17.6)':
dependencies:
'@inquirer/core': 10.3.2(@types/node@20.17.6)
@ -22708,6 +22746,14 @@ snapshots:
optionalDependencies:
'@types/node': 20.17.6
'@inquirer/expand@4.0.23(@types/node@24.9.2)':
dependencies:
'@inquirer/core': 10.3.2(@types/node@24.9.2)
'@inquirer/type': 3.0.10(@types/node@24.9.2)
yoctocolors-cjs: 2.1.3
optionalDependencies:
'@types/node': 24.9.2
'@inquirer/external-editor@1.0.3(@types/node@20.17.6)':
dependencies:
chardet: 2.1.1
@ -22715,6 +22761,13 @@ snapshots:
optionalDependencies:
'@types/node': 20.17.6
'@inquirer/external-editor@1.0.3(@types/node@24.9.2)':
dependencies:
chardet: 2.1.1
iconv-lite: 0.7.0
optionalDependencies:
'@types/node': 24.9.2
'@inquirer/figures@1.0.15': {}
'@inquirer/figures@1.0.3': {}
@ -22726,6 +22779,13 @@ snapshots:
optionalDependencies:
'@types/node': 20.17.6
'@inquirer/input@4.3.1(@types/node@24.9.2)':
dependencies:
'@inquirer/core': 10.3.2(@types/node@24.9.2)
'@inquirer/type': 3.0.10(@types/node@24.9.2)
optionalDependencies:
'@types/node': 24.9.2
'@inquirer/number@3.0.23(@types/node@20.17.6)':
dependencies:
'@inquirer/core': 10.3.2(@types/node@20.17.6)
@ -22733,6 +22793,13 @@ snapshots:
optionalDependencies:
'@types/node': 20.17.6
'@inquirer/number@3.0.23(@types/node@24.9.2)':
dependencies:
'@inquirer/core': 10.3.2(@types/node@24.9.2)
'@inquirer/type': 3.0.10(@types/node@24.9.2)
optionalDependencies:
'@types/node': 24.9.2
'@inquirer/password@4.0.23(@types/node@20.17.6)':
dependencies:
'@inquirer/ansi': 1.0.2
@ -22741,6 +22808,14 @@ snapshots:
optionalDependencies:
'@types/node': 20.17.6
'@inquirer/password@4.0.23(@types/node@24.9.2)':
dependencies:
'@inquirer/ansi': 1.0.2
'@inquirer/core': 10.3.2(@types/node@24.9.2)
'@inquirer/type': 3.0.10(@types/node@24.9.2)
optionalDependencies:
'@types/node': 24.9.2
'@inquirer/prompts@7.10.1(@types/node@20.17.6)':
dependencies:
'@inquirer/checkbox': 4.3.2(@types/node@20.17.6)
@ -22756,20 +22831,35 @@ snapshots:
optionalDependencies:
'@types/node': 20.17.6
'@inquirer/prompts@7.9.0(@types/node@20.17.6)':
'@inquirer/prompts@7.10.1(@types/node@24.9.2)':
dependencies:
'@inquirer/checkbox': 4.3.2(@types/node@20.17.6)
'@inquirer/confirm': 5.1.21(@types/node@20.17.6)
'@inquirer/editor': 4.2.23(@types/node@20.17.6)
'@inquirer/expand': 4.0.23(@types/node@20.17.6)
'@inquirer/input': 4.3.1(@types/node@20.17.6)
'@inquirer/number': 3.0.23(@types/node@20.17.6)
'@inquirer/password': 4.0.23(@types/node@20.17.6)
'@inquirer/rawlist': 4.1.11(@types/node@20.17.6)
'@inquirer/search': 3.2.2(@types/node@20.17.6)
'@inquirer/select': 4.4.2(@types/node@20.17.6)
'@inquirer/checkbox': 4.3.2(@types/node@24.9.2)
'@inquirer/confirm': 5.1.21(@types/node@24.9.2)
'@inquirer/editor': 4.2.23(@types/node@24.9.2)
'@inquirer/expand': 4.0.23(@types/node@24.9.2)
'@inquirer/input': 4.3.1(@types/node@24.9.2)
'@inquirer/number': 3.0.23(@types/node@24.9.2)
'@inquirer/password': 4.0.23(@types/node@24.9.2)
'@inquirer/rawlist': 4.1.11(@types/node@24.9.2)
'@inquirer/search': 3.2.2(@types/node@24.9.2)
'@inquirer/select': 4.4.2(@types/node@24.9.2)
optionalDependencies:
'@types/node': 20.17.6
'@types/node': 24.9.2
'@inquirer/prompts@7.9.0(@types/node@24.9.2)':
dependencies:
'@inquirer/checkbox': 4.3.2(@types/node@24.9.2)
'@inquirer/confirm': 5.1.21(@types/node@24.9.2)
'@inquirer/editor': 4.2.23(@types/node@24.9.2)
'@inquirer/expand': 4.0.23(@types/node@24.9.2)
'@inquirer/input': 4.3.1(@types/node@24.9.2)
'@inquirer/number': 3.0.23(@types/node@24.9.2)
'@inquirer/password': 4.0.23(@types/node@24.9.2)
'@inquirer/rawlist': 4.1.11(@types/node@24.9.2)
'@inquirer/search': 3.2.2(@types/node@24.9.2)
'@inquirer/select': 4.4.2(@types/node@24.9.2)
optionalDependencies:
'@types/node': 24.9.2
'@inquirer/rawlist@4.1.11(@types/node@20.17.6)':
dependencies:
@ -22779,6 +22869,14 @@ snapshots:
optionalDependencies:
'@types/node': 20.17.6
'@inquirer/rawlist@4.1.11(@types/node@24.9.2)':
dependencies:
'@inquirer/core': 10.3.2(@types/node@24.9.2)
'@inquirer/type': 3.0.10(@types/node@24.9.2)
yoctocolors-cjs: 2.1.3
optionalDependencies:
'@types/node': 24.9.2
'@inquirer/search@3.2.2(@types/node@20.17.6)':
dependencies:
'@inquirer/core': 10.3.2(@types/node@20.17.6)
@ -22788,6 +22886,15 @@ snapshots:
optionalDependencies:
'@types/node': 20.17.6
'@inquirer/search@3.2.2(@types/node@24.9.2)':
dependencies:
'@inquirer/core': 10.3.2(@types/node@24.9.2)
'@inquirer/figures': 1.0.15
'@inquirer/type': 3.0.10(@types/node@24.9.2)
yoctocolors-cjs: 2.1.3
optionalDependencies:
'@types/node': 24.9.2
'@inquirer/select@4.4.2(@types/node@20.17.6)':
dependencies:
'@inquirer/ansi': 1.0.2
@ -22798,10 +22905,24 @@ snapshots:
optionalDependencies:
'@types/node': 20.17.6
'@inquirer/select@4.4.2(@types/node@24.9.2)':
dependencies:
'@inquirer/ansi': 1.0.2
'@inquirer/core': 10.3.2(@types/node@24.9.2)
'@inquirer/figures': 1.0.15
'@inquirer/type': 3.0.10(@types/node@24.9.2)
yoctocolors-cjs: 2.1.3
optionalDependencies:
'@types/node': 24.9.2
'@inquirer/type@3.0.10(@types/node@20.17.6)':
optionalDependencies:
'@types/node': 20.17.6
'@inquirer/type@3.0.10(@types/node@24.9.2)':
optionalDependencies:
'@types/node': 24.9.2
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
@ -22939,9 +23060,9 @@ snapshots:
dependencies:
langium: 3.3.1
'@mintlify/cli@4.0.1090(@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/node@20.17.6)(@types/react@18.3.12)(encoding@0.1.13)(react-dom@19.2.3(react@19.2.3))(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0)':
'@mintlify/cli@4.0.1090(@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/node@24.9.2)(@types/react@18.3.12)(encoding@0.1.13)(react-dom@19.2.3(react@19.2.3))(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0)':
dependencies:
'@inquirer/prompts': 7.9.0(@types/node@20.17.6)
'@inquirer/prompts': 7.9.0(@types/node@24.9.2)
'@mintlify/common': 1.0.835(@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@18.3.12)(encoding@0.1.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0)
'@mintlify/link-rot': 3.0.1010(@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@18.3.12)(encoding@0.1.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0)
'@mintlify/prebuild': 1.0.977(@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@18.3.12)(encoding@0.1.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0)
@ -22954,7 +23075,7 @@ snapshots:
front-matter: 4.0.2
fs-extra: 11.2.0
ink: 6.3.0(@types/react@18.3.12)(react@19.2.3)
inquirer: 12.3.0(@types/node@20.17.6)
inquirer: 12.3.0(@types/node@24.9.2)
js-yaml: 4.1.0
mdast-util-mdx-jsx: 3.2.0
open: 8.4.2
@ -30122,7 +30243,6 @@ snapshots:
'@types/node@24.9.2':
dependencies:
undici-types: 7.16.0
optional: true
'@types/nodemailer@6.4.15':
dependencies:
@ -33200,7 +33320,7 @@ snapshots:
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
eslint-plugin-react: 7.37.2(eslint@8.57.1)
eslint-plugin-react-hooks: 5.1.0(eslint@8.57.1)
@ -33242,7 +33362,7 @@ snapshots:
debug: 4.4.3
enhanced-resolve: 5.17.1
eslint: 8.57.1
eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
fast-glob: 3.3.3
get-tsconfig: 4.8.1
is-bun-module: 1.2.1
@ -33285,7 +33405,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1):
dependencies:
debug: 3.2.7
optionalDependencies:
@ -33363,7 +33483,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
hasown: 2.0.2
is-core-module: 2.15.1
is-glob: 4.0.3
@ -35269,12 +35389,12 @@ snapshots:
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
inquirer@12.3.0(@types/node@20.17.6):
inquirer@12.3.0(@types/node@24.9.2):
dependencies:
'@inquirer/core': 10.3.2(@types/node@20.17.6)
'@inquirer/prompts': 7.10.1(@types/node@20.17.6)
'@inquirer/type': 3.0.10(@types/node@20.17.6)
'@types/node': 20.17.6
'@inquirer/core': 10.3.2(@types/node@24.9.2)
'@inquirer/prompts': 7.10.1(@types/node@24.9.2)
'@inquirer/type': 3.0.10(@types/node@24.9.2)
'@types/node': 24.9.2
ansi-escapes: 4.3.2
mute-stream: 2.0.0
run-async: 3.0.0
@ -36757,9 +36877,9 @@ snapshots:
dependencies:
minipass: 7.1.2
mint@4.2.487(@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/node@20.17.6)(@types/react@18.3.12)(encoding@0.1.13)(react-dom@19.2.3(react@19.2.3))(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0):
mint@4.2.487(@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/node@24.9.2)(@types/react@18.3.12)(encoding@0.1.13)(react-dom@19.2.3(react@19.2.3))(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0):
dependencies:
'@mintlify/cli': 4.0.1090(@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/node@20.17.6)(@types/react@18.3.12)(encoding@0.1.13)(react-dom@19.2.3(react@19.2.3))(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0)
'@mintlify/cli': 4.0.1090(@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/node@24.9.2)(@types/react@18.3.12)(encoding@0.1.13)(react-dom@19.2.3(react@19.2.3))(tsx@4.19.3)(typescript@5.9.3)(yaml@2.6.0)
transitivePeerDependencies:
- '@radix-ui/react-popover'
- '@types/node'
@ -40923,8 +41043,7 @@ snapshots:
undici-types@6.21.0: {}
undici-types@7.16.0:
optional: true
undici-types@7.16.0: {}
undici@6.19.8: {}

View File

@ -0,0 +1,199 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
/**
* Why this script exists:
* - Next.js dev app-page runtimes (app-page*.runtime.dev.js) include an async debug hook
* (`async_hooks.createHook`) that captures async stack-trace metadata in hot paths.
* - In our backend dev workload (notably repeated email-queue-step requests), this hook
* causes measurable heap growth/retention in dev mode.
* - We disable that hook when STACK_DISABLE_REACT_ASYNC_DEBUG_INFO=true.
*
* Why we do this in postinstall:
* - The equivalent pnpm patch touched minified one-line bundles and produced a multi-MB
* patch file that is hard to review and noisy in diffs.
* - A strict install-time rewrite keeps the repo clean while still being deterministic:
* if assumptions no longer hold, we fail loudly instead of silently continuing.
*/
const LOG_PREFIX = "[patch-next-async-debug-info]";
const MIN_TARGET_NEXT_MAJOR = 16;
// We only patch app-page dev runtimes where this hook is present and relevant.
const APP_PAGE_RUNTIME_FILE_REGEX = /^app-page(?:-turbo)?(?:-experimental)?\.runtime\.dev\.js$/;
const HOOK_NEEDLE = "doNotLimit=new WeakSet;async_hooks.createHook(";
const GUARDED_HOOK =
"doNotLimit=new WeakSet,shouldEnableAsyncDebugInfo=\"true\"!==process.env.STACK_DISABLE_REACT_ASYNC_DEBUG_INFO;shouldEnableAsyncDebugInfo&&async_hooks.createHook(";
// Extra fingerprints reduce the chance of accidentally patching unrelated files.
const RUNTIME_FINGERPRINTS = [
"collectStackTracePrivate(",
"pendingOperations",
];
function fail(message) {
throw new Error(`${LOG_PREFIX} ${message}`);
}
function hasAllRuntimeFingerprints(content) {
return RUNTIME_FINGERPRINTS.every((fingerprint) => content.includes(fingerprint));
}
function patchRuntimeFile(filePath) {
const content = fs.readFileSync(filePath, "utf8");
const hasNeedle = content.includes(HOOK_NEEDLE);
const hasGuard = content.includes(GUARDED_HOOK);
if (!hasAllRuntimeFingerprints(content)) {
return { status: "ignored" };
}
if (hasNeedle && hasGuard) {
fail(`File ${filePath} contains both guarded and unguarded markers; refusing to continue.`);
}
if (!hasNeedle && !hasGuard) {
fail(`File ${filePath} no longer contains the expected async debug marker. Next.js internals likely changed.`);
}
// Already guarded => idempotent no-op.
if (hasGuard) {
return { status: "already" };
}
const needleCount = content.split(HOOK_NEEDLE).length - 1;
if (needleCount !== 1) {
fail(`File ${filePath} matched ${needleCount} unguarded markers (expected exactly 1).`);
}
const patchedContent = content.replace(HOOK_NEEDLE, GUARDED_HOOK);
if (patchedContent === content) {
fail(`File ${filePath} did not change after replacement.`);
}
if (patchedContent.includes(HOOK_NEEDLE)) {
fail(`File ${filePath} still contains unguarded marker after patch.`);
}
if (!patchedContent.includes(GUARDED_HOOK)) {
fail(`File ${filePath} is missing guarded marker after patch.`);
}
fs.writeFileSync(filePath, patchedContent);
return { status: "patched" };
}
function listInstalledNextServerDirs(repoRoot) {
const pnpmVirtualStoreDir = path.join(repoRoot, "node_modules", ".pnpm");
if (!fs.existsSync(pnpmVirtualStoreDir)) {
fail(`Missing ${pnpmVirtualStoreDir}. Run pnpm install before applying this patch.`);
}
const dirEntries = fs.readdirSync(pnpmVirtualStoreDir, { withFileTypes: true });
const nextServerDirs = dirEntries
.filter((entry) => entry.isDirectory() && entry.name.startsWith("next@"))
.map((entry) => {
const versionMatch = entry.name.match(/^next@(\d+)\./);
if (!versionMatch) {
return null;
}
const majorVersion = Number(versionMatch[1]);
// This guard targets current Next 16 dev runtimes only; older installed versions
// (e.g. transitive Next 14) may not contain the same runtime structure.
if (majorVersion < MIN_TARGET_NEXT_MAJOR) {
return null;
}
const nextServerDir = path.join(
pnpmVirtualStoreDir,
entry.name,
"node_modules",
"next",
"dist",
"compiled",
"next-server",
);
return fs.existsSync(nextServerDir) ? nextServerDir : null;
})
.filter((nextServerDir) => nextServerDir !== null);
if (nextServerDirs.length === 0) {
fail(`No installed Next.js runtimes with major >= ${MIN_TARGET_NEXT_MAJOR} found in node_modules/.pnpm.`);
}
return nextServerDirs;
}
function patchAllNextRuntimeDirs(repoRoot) {
const nextServerDirs = listInstalledNextServerDirs(repoRoot);
const summary = {
nextServerDirs: nextServerDirs.length,
candidateFiles: 0,
fingerprintedFiles: 0,
patchedFiles: 0,
alreadyPatchedFiles: 0,
};
for (const nextServerDir of nextServerDirs) {
const runtimeFiles = fs.readdirSync(nextServerDir)
.filter((fileName) => APP_PAGE_RUNTIME_FILE_REGEX.test(fileName))
.map((fileName) => path.join(nextServerDir, fileName));
if (runtimeFiles.length === 0) {
fail(`No app-page*.runtime.dev.js files found in ${nextServerDir}.`);
}
summary.candidateFiles += runtimeFiles.length;
let touchedFingerprintFileInDir = 0;
for (const runtimeFile of runtimeFiles) {
const result = patchRuntimeFile(runtimeFile);
if (result.status === "ignored") {
continue;
}
touchedFingerprintFileInDir += 1;
summary.fingerprintedFiles += 1;
if (result.status === "patched") {
summary.patchedFiles += 1;
} else if (result.status === "already") {
summary.alreadyPatchedFiles += 1;
} else {
fail(`Unexpected patch status "${result.status}" for ${runtimeFile}.`);
}
}
if (touchedFingerprintFileInDir === 0) {
fail(`Found app-page runtimes in ${nextServerDir}, but none matched expected async debug fingerprints.`);
}
}
if (summary.fingerprintedFiles === 0) {
fail("No runtime files matched expected async debug fingerprints.");
}
if (summary.patchedFiles === 0 && summary.alreadyPatchedFiles === 0) {
fail("Patch script completed without touching any files.");
}
return summary;
}
function main() {
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, "..");
const summary = patchAllNextRuntimeDirs(repoRoot);
// Emit a compact machine-readable summary for local debugging and CI logs.
console.log(
`${LOG_PREFIX} patched=${summary.patchedFiles} alreadyPatched=${summary.alreadyPatchedFiles} ` +
`fingerprinted=${summary.fingerprintedFiles} candidates=${summary.candidateFiles} nextDirs=${summary.nextServerDirs}`,
);
}
main();