diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..d579653 --- /dev/null +++ b/.npmrc @@ -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 diff --git a/Dockerfile b/Dockerfile index 22f4fff..a2d6ada 100644 --- a/Dockerfile +++ b/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"] diff --git a/server.ts b/server.ts index 4336ef9..401bba3 100644 --- a/server.ts +++ b/server.ts @@ -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