fix(server): resolve socket.io deps via NODE_PATH + polyfill AsyncLocalStorage
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) <noreply@anthropic.com>
This commit is contained in:
16
Dockerfile
16
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
|
# --packages=external) resolves socket.io and its FULL transitive closure
|
||||||
# (engine.io → accepts/ws/cors, @socket.io/redis-adapter, ...) from
|
# (engine.io → accepts/ws/cors, @socket.io/redis-adapter, ...) from
|
||||||
# node_modules at runtime. The Next tracer omits these from
|
# node_modules at runtime. The Next tracer omits these from
|
||||||
# .next/standalone because no Next route imports the socket server, and
|
# .next/standalone because no Next route imports the socket server
|
||||||
# cherry-copying just socket.io/ leaves its deps unresolved
|
# (→ MODULE_NOT_FOUND 'accepts'). Stage the complete hoisted prod tree in
|
||||||
# (MODULE_NOT_FOUND 'accepts'). Overlay the complete prod dependency tree
|
# a SEPARATE dir on NODE_PATH rather than touching the standalone
|
||||||
# (flat/hoisted layout → symlink-safe copy) on top of the traced subset.
|
# node_modules: overlaying real dirs onto its pnpm symlinks (e.g.
|
||||||
COPY --from=prod-deps --chown=nextjs:nodejs /app/node_modules ./node_modules
|
# @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
|
USER nextjs
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||||
|
|||||||
15
src/server-runtime-preamble.ts
Normal file
15
src/server-runtime-preamble.ts
Normal file
@@ -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;
|
||||||
@@ -9,6 +9,10 @@
|
|||||||
* → dist/worker.js) and this file only handles Next.js + Socket.io.
|
* → 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 { createServer, type Server as HttpServer } from 'node:http';
|
||||||
|
|
||||||
import next from 'next';
|
import next from 'next';
|
||||||
|
|||||||
Reference in New Issue
Block a user