fix(docker): single container (caddy+node), ESM adapters fix blank page

Docker: moved all docker files to docker/ tree (was conflated with
upstream Dockerfile at root + server/Dockerfile). Single container now:
caddy (front, serves static + proxies /api /ws) + node backend (internal
:4001). Node never exposed. entrypoint.sh runs both. Compose: one service.

Blank page root cause: storage adapters had inconsistent module systems.
firebase.js = ESM (export). ws.js + memory.js = CJS (module.exports).
CRA prod build = ESM strict -> CJS runtime crash, blank root. Dev mode
lenient, masked bug. First ws prod build (docker) = first exposure.
Never dev/prod split intended; just inconsistency from M2 era.

Fix: all adapters ESM. ws.js lazy-loads 'ws' pkg via dynamic import()
(Node/jest only; browser uses global WebSocket). index.js static
imports. server jest: added babel.config.js (preset-env, node target)
to transform ESM for jest.

Test: src/tests/StorageEsm.test.js — 4 tests grep all adapters for
module.exports / require(). Regression guard catches CJS leak.

Verified: docker page renders (root 4534 chars, UI visible).
server 24 green, shared 90 green, FE ESM 4 green.
This commit is contained in:
david raistrick
2026-07-01 19:03:59 -04:00
parent c1d982b4a4
commit da25f46e3e
14 changed files with 145 additions and 157 deletions
+4 -4
View File
@@ -11,6 +11,8 @@ import {
onSnapshot, updateDoc, deleteDoc, query, orderBy, limit, writeBatch,
} from 'firebase/firestore';
import { initFirebase, createFirebaseStorage } from './firebase';
import { createWsStorage } from './ws';
import { createMemoryStorage } from './memory';
let storageInstance = null;
@@ -23,13 +25,11 @@ export function getStorage() {
if (!ok) throw new Error('Firebase config missing. Check REACT_APP_FIREBASE_* env.');
storageInstance = createFirebaseStorage();
} else if (mode === 'ws') {
const { createWsStorage } = require('./ws');
storageInstance = createWsStorage({
baseUrl: process.env.REACT_APP_BACKEND_URL || 'http://127.0.0.1:4001',
wsUrl: process.env.REACT_APP_BACKEND_WS || 'ws://127.0.0.1:4001/ws',
baseUrl: process.env.REACT_APP_BACKEND_URL || '',
wsUrl: process.env.REACT_APP_BACKEND_WS || '',
});
} else {
const { createMemoryStorage } = require('./memory');
storageInstance = createMemoryStorage();
}
return storageInstance;
+2 -2
View File
@@ -4,7 +4,7 @@
'use strict';
const { EventEmitter } = require('events');
import { EventEmitter } from 'events';
function createMemoryStorage() {
const docs = new Map(); // path -> data obj
@@ -137,4 +137,4 @@ function deepClone(v) {
return JSON.parse(JSON.stringify(v));
}
module.exports = { createMemoryStorage };
export { createMemoryStorage };
+12 -4
View File
@@ -5,11 +5,10 @@
'use strict';
// Native browser WebSocket if present, else ws pkg (Node/jest).
// Lazy load ws pkg so CRA prod build (ESM) doesn't choke on require().
let WebSocketImpl;
if (typeof WebSocket !== 'undefined') {
WebSocketImpl = WebSocket;
} else {
WebSocketImpl = require('ws').WebSocket;
}
function createWsStorage({ baseUrl, wsUrl } = {}) {
@@ -47,7 +46,15 @@ function createWsStorage({ baseUrl, wsUrl } = {}) {
function ensureWs() {
if (wsReady) return wsReady;
wsReady = new Promise((resolve, reject) => {
ws = new WebSocketImpl(WS);
(async () => {
// Node/jest only: load ws pkg via dynamic import. Browser uses global
// WebSocket. Avoids require() in CRA prod ESM bundle (webpack crash).
let WsClass = WebSocketImpl;
if (!WsClass) {
const wsPkg = await import('ws');
WsClass = wsPkg.WebSocket;
}
ws = new WsClass(WS);
const onOpen = () => {
const isReconnect = everConnected;
everConnected = true;
@@ -99,6 +106,7 @@ function createWsStorage({ baseUrl, wsUrl } = {}) {
ws.addEventListener('close', onClose);
ws.addEventListener('message', onMessage);
}
})();
});
return wsReady;
}
@@ -226,4 +234,4 @@ function createWsStorage({ baseUrl, wsUrl } = {}) {
return storage;
}
module.exports = { createWsStorage };
export { createWsStorage };