From 319fd7fd1a48c5f89016b3f9321135826c5d1ea4 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 2 Jun 2026 23:34:28 +0200 Subject: [PATCH] fix(server): resolve socket.io deps via NODE_PATH + polyfill AsyncLocalStorage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two runtime defects in the crm-app prod image (never exercised before this deploy; CI only builds + pushes): 1. Replacing the standalone node_modules wholesale to add socket.io's deps swapped out Next's standalone-tuned `next` and broke its runtime ("Invariant: AsyncLocalStorage accessed in runtime where it is not available"). Instead, stage the complete hoisted prod tree in a separate dir on NODE_PATH: the standalone node_modules (and its `next`) stay intact, and only the socket server's otherwise-missing deps (engine.io→accepts/ws/cors, @socket.io/redis-adapter) fall through to it. 2. Defensively set globalThis.AsyncLocalStorage before Next's app-render modules load, via a preamble that is the first import in server.ts. Next's node-environment-baseline normally sets it during the standalone bootstrap, but the custom server can load app-render storage first. Verified in the esbuild bundle that the assignment runs before require("next"). Co-Authored-By: Claude Opus 4.8 (1M context) --- Dockerfile | 16 +++++++++++----- src/server-runtime-preamble.ts | 15 +++++++++++++++ src/server.ts | 4 ++++ 3 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 src/server-runtime-preamble.ts diff --git a/Dockerfile b/Dockerfile index 27e41342..93b8714c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,11 +45,17 @@ COPY --from=builder --chown=nextjs:nodejs /app/dist/server.js ./server-custom.js # --packages=external) resolves socket.io and its FULL transitive closure # (engine.io → accepts/ws/cors, @socket.io/redis-adapter, ...) from # node_modules at runtime. The Next tracer omits these from -# .next/standalone because no Next route imports the socket server, and -# cherry-copying just socket.io/ leaves its deps unresolved -# (MODULE_NOT_FOUND 'accepts'). Overlay the complete prod dependency tree -# (flat/hoisted layout → symlink-safe copy) on top of the traced subset. -COPY --from=prod-deps --chown=nextjs:nodejs /app/node_modules ./node_modules +# .next/standalone because no Next route imports the socket server +# (→ MODULE_NOT_FOUND 'accepts'). Stage the complete hoisted prod tree in +# a SEPARATE dir on NODE_PATH rather than touching the standalone +# node_modules: overlaying real dirs onto its pnpm symlinks (e.g. +# @react-pdf/renderer) fails the COPY, and replacing it wholesale swaps +# out the standalone-tuned `next` and breaks Next's runtime +# (AsyncLocalStorage invariant). NODE_PATH is searched AFTER the local +# walk, so Next still resolves its own deps from ./node_modules; only the +# socket server's otherwise-missing deps fall through to here. +COPY --from=prod-deps --chown=nextjs:nodejs /app/node_modules ./_server_deps +ENV NODE_PATH=/app/_server_deps USER nextjs EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ diff --git a/src/server-runtime-preamble.ts b/src/server-runtime-preamble.ts new file mode 100644 index 00000000..6938ba9f --- /dev/null +++ b/src/server-runtime-preamble.ts @@ -0,0 +1,15 @@ +/** + * Runtime preamble — MUST stay the first import in server.ts. + * + * Next's app-render calls createAsyncLocalStorage(), which falls back to a + * throwing FakeAsyncLocalStorage when `globalThis.AsyncLocalStorage` is unset + * ("Invariant: AsyncLocalStorage accessed in runtime where it is not + * available", error E504). next/dist/server/node-environment-baseline sets it + * during Next's own standalone-server bootstrap, but our custom server + * (dist/server.js → server-custom.js) can load app-render storage before that + * runs. Set it up-front, idempotently, so the invariant can never fire. + */ +import { AsyncLocalStorage } from 'node:async_hooks'; + +const g = globalThis as typeof globalThis & { AsyncLocalStorage?: unknown }; +g.AsyncLocalStorage ??= AsyncLocalStorage; diff --git a/src/server.ts b/src/server.ts index 1775b737..7e2437fc 100644 --- a/src/server.ts +++ b/src/server.ts @@ -9,6 +9,10 @@ * → dist/worker.js) and this file only handles Next.js + Socket.io. */ +// Must be first: sets globalThis.AsyncLocalStorage before Next's app-render +// modules load. See src/server-runtime-preamble.ts. +import './server-runtime-preamble'; + import { createServer, type Server as HttpServer } from 'node:http'; import next from 'next';