fix(infra): Dockerfile compiles TS, SPA fallback works on Express 4
Rebuilding the pediatric-ai-scribe container surfaced three long-
standing bugs that pre-date today's work but only showed up when
the image was actually rebuilt for the first time since April.
Fixing them in one commit so the live app reflects the React
changes shipped earlier today.
Dockerfile (compiles TypeScript now):
• COPY package.json → COPY package.json package-lock.json .npmrc
so npm ci can do a deterministic install.
• Drop --omit=dev from the install step (tsc is a devDep and we
need it to compile); prune devDeps AFTER the build runs.
• RUN npm run build — compiles server.ts + src/ to dist/.
server.ts was renamed from server.js in commit d60a29f but no
one updated the Dockerfile; CMD has been "node server.js"
against a file that didn't exist. The running e2e container
survived because it was built before the rename.
• cp package.json dist/package.json — server.ts does
require('./package.json') which resolves relative to the
compiled file; keeping a copy next to it is simpler than
making the source path-aware.
• ln -s /app/public /app/dist/public — express.static /
sendFile calls use path.join(__dirname, 'public', …) and
__dirname at runtime is /app/dist.
• CMD ["node", "dist/server.js"] (was "server.js").
.npmrc:
• legacy-peer-deps=true. npm 10+ hard-rejects the transitive
peerOptional mismatch between openai@4.x (peerOptional zod@^3)
and our direct dep zod@^4.3.6. Host, CI, and Docker now all
share one resolution policy so the lock file stays reproducible.
server.ts:
• `app.get('/app/*splat', …)` → `app.get('/app/*', …)`
• `app.get('*splat', …)` → `app.get('*', …)`
Express 5 named-splat syntax doesn't match anything on Express
4.21 (which we ship). Hard refresh on /wellvisit, /encounter,
/cms etc. was falling through to the static 404 instead of
serving the React index. Verified: all 7 clinical routes now
return HTTP 200 from the SPA fallback.
Smoke-tested against 127.0.0.1:3552 after rebuild:
/ HTTP 200
/wellvisit /encounter /cms HTTP 200
/auth /api/health HTTP 200
/app/assets/index-C9wKWOAN.js HTTP 200
/does-not-exist.jpg HTTP 404 (static 404 preserved)
This commit is contained in:
parent
ae4c36ff8e
commit
fb609f6672
3 changed files with 35 additions and 6 deletions
8
.npmrc
Normal file
8
.npmrc
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# npm 10+ rejects transitive peerOptional mismatches by default. The
|
||||
# repo has one: openai@4.x declares peerOptional zod@^3.x, but zod@4.x
|
||||
# is a direct dependency here (used by the request-schema layer in
|
||||
# shared/clinical/). Earlier npm versions ignored peerOptional clashes
|
||||
# automatically; since npm 10 we have to opt-in to that behaviour.
|
||||
# Pinning it here so host installs, CI, and Docker all share one
|
||||
# resolution policy and the lock file stays reproducible.
|
||||
legacy-peer-deps=true
|
||||
25
Dockerfile
25
Dockerfile
|
|
@ -17,14 +17,31 @@ RUN apk add --no-cache ffmpeg curl jq
|
|||
COPY --from=bao-src /bin/bao /usr/local/bin/bao
|
||||
RUN /usr/local/bin/bao version
|
||||
|
||||
COPY package.json ./
|
||||
# argon2 compiles native code via node-gyp — needs python3/make/g++ at build time
|
||||
COPY package.json package-lock.json .npmrc ./
|
||||
# argon2 compiles native code via node-gyp — needs python3/make/g++ at build time.
|
||||
# npm ci installs the exact versions from package-lock.json so a transitive
|
||||
# peer-dep conflict (e.g. zod v4 vs openai's peerOptional zod@^3.x) can't
|
||||
# break the image build — the lock file has already resolved it once.
|
||||
# Install with devDeps so tsc is available for the compile step below.
|
||||
RUN apk add --no-cache --virtual .build-deps python3 make g++ \
|
||||
&& npm install --omit=dev \
|
||||
&& npm ci \
|
||||
&& apk del .build-deps
|
||||
|
||||
COPY . .
|
||||
|
||||
# Compile TypeScript → dist/, then prune devDeps from node_modules to keep
|
||||
# the final image slim. `server.ts` is the entry point (renamed from
|
||||
# server.js in commit d60a29f); tsconfig emits to ./dist.
|
||||
#
|
||||
# Also copy package.json into dist/ — server.ts does `require('./package.json')`
|
||||
# to read the app version at boot, which resolves relative to dist/server.js
|
||||
# at runtime rather than the repo root. Keeping a copy alongside the compiled
|
||||
# entry is simpler than making the source path-aware.
|
||||
RUN npm run build \
|
||||
&& cp package.json dist/package.json \
|
||||
&& ln -s /app/public /app/dist/public \
|
||||
&& npm prune --omit=dev
|
||||
|
||||
# Ensure the entrypoint is executable regardless of host file permissions
|
||||
RUN chmod +x /app/docker-entrypoint.sh
|
||||
|
||||
|
|
@ -55,5 +72,5 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=20s \
|
|||
# See docker-entrypoint.sh for the logic — it is a no-op if OPENBAO_ADDR is
|
||||
# unset, so legacy .env-only deployments continue to work unchanged.
|
||||
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||
CMD ["node", "server.js"]
|
||||
CMD ["node", "dist/server.js"]
|
||||
|
||||
|
|
|
|||
|
|
@ -196,7 +196,11 @@ app.get('/index.html', sendReactIndex);
|
|||
// SPA. React Router treats /app/encounter the same as /encounter
|
||||
// because there's no basename — but we still set a proper 200 rather
|
||||
// than 404 so the one-page bundle can client-side-redirect if needed.
|
||||
app.get('/app/*splat', sendReactIndex);
|
||||
// Express 4 syntax (package.json pins ^4.21.0). Express 5's named-
|
||||
// param splat (*splat) is parsed as a literal and matches nothing on
|
||||
// v4 — see commit history; this was the reason /wellvisit, /encounter,
|
||||
// and other React-Router paths 404'd on hard refresh.
|
||||
app.get('/app/*', sendReactIndex);
|
||||
// Email landing pages — served by the SPA so the React router picks
|
||||
// up ?token=xxx and renders the reset / verify component.
|
||||
app.get('/reset-password', sendReactIndex);
|
||||
|
|
@ -309,7 +313,7 @@ app.use('/api/admin/learning', require('./src/routes/learningAI'));
|
|||
// route (not an API, not a known static file extension). Runs AFTER
|
||||
// express.static so real files still win. Without this, a hard
|
||||
// refresh on /encounter or /settings would 404.
|
||||
app.get('*splat', function(req, res, next) {
|
||||
app.get('*', function(req, res, next) {
|
||||
if (req.method !== 'GET') return next();
|
||||
if (req.path.startsWith('/api/')) return next();
|
||||
// Let 404 handler deal with anything that looks like a static asset
|
||||
|
|
|
|||
Loading…
Reference in a new issue