diff --git a/.docker/.manifests/apps__worker__package.json b/.docker/.manifests/apps__worker__package.json index 59924d4..874f85a 100644 --- a/.docker/.manifests/apps__worker__package.json +++ b/.docker/.manifests/apps__worker__package.json @@ -15,6 +15,8 @@ "@cv/core": "workspace:*", "@cv/file-storage": "*", "@cv/handlers": "workspace:*", + "isomorphic-dompurify": "^2.16.0", + "jsdom": "^28.1.0", "@nestjs/common": "^11.1.18", "@nestjs/config": "^4.0.4", "@nestjs/core": "^11.1.18", diff --git a/.docker/api.Dockerfile b/.docker/api.Dockerfile index 3bd6b90..f1e4f8c 100644 --- a/.docker/api.Dockerfile +++ b/.docker/api.Dockerfile @@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libpixman-1-dev libcairo2-dev libpango1.0-dev \ libjpeg-dev libgif-dev librsvg2-dev \ && rm -rf /var/lib/apt/lists/* -RUN corepack enable && corepack prepare pnpm@10 --activate +RUN corepack enable && corepack prepare pnpm@10.29.1 --activate WORKDIR /app @@ -33,8 +33,8 @@ COPY packages/ ./packages/ COPY apps/api/ ./apps/api/ # Generate Prisma client -WORKDIR /app/packages/core -RUN pnpm exec prisma generate + +RUN pnpm --filter @cv/core run prisma:generate EXPOSE 3000 @@ -48,7 +48,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libpixman-1-dev libcairo2-dev libpango1.0-dev \ libjpeg-dev libgif-dev librsvg2-dev \ && rm -rf /var/lib/apt/lists/* -RUN corepack enable && corepack prepare pnpm@10 --activate +RUN corepack enable && corepack prepare pnpm@10.29.1 --activate WORKDIR /app @@ -66,9 +66,10 @@ RUN --mount=type=secret,id=github_token,required=true \ COPY packages/ ./packages/ COPY apps/api/ ./apps/api/ -WORKDIR /app/packages/core -RUN pnpm exec prisma generate -RUN pnpm build + +RUN pnpm --filter @cv/core run prisma:generate +# Filter to api only — docs/client/worker aren't COPYed into this image. +RUN pnpm --filter @cv/api build # ---- Production runtime ---- FROM node:24-bookworm-slim AS production @@ -79,7 +80,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libcairo2 libpango-1.0-0 libpangocairo-1.0-0 \ libjpeg62-turbo libgif7 librsvg2-2 libpixman-1-0 \ && rm -rf /var/lib/apt/lists/* -RUN corepack enable && corepack prepare pnpm@10 --activate +RUN corepack enable && corepack prepare pnpm@10.29.1 --activate WORKDIR /app @@ -97,7 +98,7 @@ RUN --mount=type=secret,id=github_token,required=true \ # Copy Prisma schema + config, generate client in prod node_modules context COPY packages/core/prisma/ ./packages/core/prisma/ COPY packages/core/prisma.config.ts ./packages/core/prisma.config.ts -RUN cd packages/core && pnpm exec prisma generate +RUN pnpm --filter @cv/core run prisma:generate # Copy bundled output (single file, @cv/* packages inlined) COPY --from=builder /app/apps/api/dist/ ./apps/api/dist/ diff --git a/.docker/worker.Dockerfile b/.docker/worker.Dockerfile index 7842311..80a95b8 100644 --- a/.docker/worker.Dockerfile +++ b/.docker/worker.Dockerfile @@ -2,7 +2,7 @@ FROM node:24-bookworm-slim AS development RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/* -RUN corepack enable && corepack prepare pnpm@10 --activate +RUN corepack enable && corepack prepare pnpm@10.29.1 --activate WORKDIR /app @@ -22,18 +22,14 @@ RUN --mount=type=secret,id=github_token,required=true \ NPM_AUTH_TOKEN=$(cat /run/secrets/github_token) \ pnpm install --frozen-lockfile -# Layer 4: source code -COPY packages/tsconfig/ ./packages/tsconfig/ -COPY packages/utils/ ./packages/utils/ -COPY packages/cv-renderer/ ./packages/cv-renderer/ -COPY packages/file-storage/ ./packages/file-storage/ -COPY packages/core/ ./packages/core/ -COPY packages/handlers/ ./packages/handlers/ -COPY packages/core/prisma/ ./packages/core/prisma/ +# Layer 4: source code. tsup with noExternal: [/^@cv\//] inlines all @cv/* +# packages, so the entire workspace's package sources need to be present +# even if the worker's runtime path doesn't touch them all directly. +COPY packages/ ./packages/ COPY apps/worker/ ./apps/worker/ # Generate Prisma client (@cv/core depends on @prisma/client) -RUN cd packages/core && pnpm exec prisma generate +RUN pnpm --filter @cv/core run prisma:generate # Install Chromium + system deps via Playwright RUN cd apps/worker && pnpm exec playwright install --with-deps chromium @@ -45,7 +41,7 @@ CMD ["pnpm", "dev"] # ---- Production build ---- FROM node:24-bookworm-slim AS builder -RUN corepack enable && corepack prepare pnpm@10 --activate +RUN corepack enable && corepack prepare pnpm@10.29.1 --activate WORKDIR /app @@ -60,24 +56,16 @@ RUN --mount=type=secret,id=github_token,required=true \ NPM_AUTH_TOKEN=$(cat /run/secrets/github_token) \ pnpm install --frozen-lockfile -COPY packages/tsconfig/ ./packages/tsconfig/ -COPY packages/utils/ ./packages/utils/ -COPY packages/cv-renderer/ ./packages/cv-renderer/ -COPY packages/file-storage/ ./packages/file-storage/ -COPY packages/core/ ./packages/core/ -COPY packages/handlers/ ./packages/handlers/ -COPY packages/core/prisma/ ./packages/core/prisma/ +COPY packages/ ./packages/ COPY apps/worker/ ./apps/worker/ -RUN cd packages/core && pnpm exec prisma generate - -WORKDIR /app/apps/worker -RUN pnpm build +RUN pnpm --filter @cv/core run prisma:generate +RUN pnpm --filter @cv/worker build # ---- Production runtime ---- FROM node:24-bookworm-slim AS production -RUN corepack enable && corepack prepare pnpm@10 --activate +RUN corepack enable && corepack prepare pnpm@10.29.1 --activate WORKDIR /app diff --git a/.dockerignore b/.dockerignore index fcc43f8..250d5f9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,6 @@ -# Dependencies -node_modules/ +# Dependencies — exclude node_modules at every depth so a host-side +# pnpm install doesn't shadow the container's fresh install. +**/node_modules # Keep packages directory for workspace dependencies !packages/ @@ -70,11 +71,12 @@ apps/client/dist/ apps/server/dist/ packages/**/dist/ -# Compiled TypeScript files +# Compiled TypeScript files. Don't re-include node_modules contents +# here — the **/node_modules exclusion at the top must hold so the +# container's fresh pnpm install isn't shadowed by host symlinks. **/*.js **/*.d.ts **/*.tsbuildinfo -!**/node_modules/** !**/dist/** # Lerna diff --git a/.npmrc b/.npmrc index 1b3abc9..d280273 100644 --- a/.npmrc +++ b/.npmrc @@ -1,5 +1,11 @@ link-workspace-packages=true prefer-workspace-packages=true public-hoist-pattern[]=@prisma/* +# tsup keeps jsdom + isomorphic-dompurify external (asset path joins +# via __dirname don't survive bundling). Shamefully-hoist makes their +# packages visible in /app/node_modules so node CJS resolution from +# apps/api/dist/ finds them without depending on workspace-internal +# nesting. +shamefully-hoist=true @riotbyte-com:registry=https://npm.pkg.github.com //npm.pkg.github.com/:_authToken=${NPM_AUTH_TOKEN} diff --git a/apps/api/package.json b/apps/api/package.json index 883b72e..15f659c 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -41,6 +41,8 @@ "@cv/file-storage": "*", "@cv/file-upload": "*", "@cv/utils": "*", + "isomorphic-dompurify": "^2.16.0", + "jsdom": "^28.1.0", "@nestjs/apollo": "^13.2.5", "@nestjs/common": "^11.1.18", "@nestjs/config": "^4.0.4", diff --git a/apps/api/scripts/post-build.sh b/apps/api/scripts/post-build.sh index 8f139be..e200b7e 100755 --- a/apps/api/scripts/post-build.sh +++ b/apps/api/scripts/post-build.sh @@ -2,6 +2,12 @@ set -eu DIST_DIR="dist" +SRC="../../packages/core/src/modules/cv-template/templates" -mkdir -p "${DIST_DIR}/templates" -cp ../../packages/core/src/modules/cv-template/templates/* "${DIST_DIR}/templates/" +# tsup bundles main.ts to dist/main.js (cwd-relative __dirname = dist/) and +# scripts/seed.ts to dist/scripts/seed.js (__dirname = dist/scripts/). The +# CVTemplateSeedService reads templates from __dirname/templates/, so both +# locations need a copy. +mkdir -p "${DIST_DIR}/templates" "${DIST_DIR}/scripts/templates" +cp "$SRC"/* "${DIST_DIR}/templates/" +cp "$SRC"/* "${DIST_DIR}/scripts/templates/" diff --git a/apps/api/src/config/env.validation.ts b/apps/api/src/config/env.validation.ts index c3b274d..98e6c11 100644 --- a/apps/api/src/config/env.validation.ts +++ b/apps/api/src/config/env.validation.ts @@ -146,14 +146,15 @@ export const envSchema = z // hatch (ALLOWED_DB_PLAINTEXT=true) is for providers that terminate TLS // at a sidecar; default-deny otherwise. if (data.NODE_ENV === "production" && !data.ALLOWED_DB_PLAINTEXT) { - const hasTls = /[?&]sslmode=(require|verify-ca|verify-full)\b/.test( - data.DATABASE_URL, - ); + const hasTls = + /[?&]sslmode=(require|verify-ca|verify-full|no-verify)\b/.test( + data.DATABASE_URL, + ); if (!hasTls) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: - "DATABASE_URL must include sslmode=require (or verify-ca / verify-full) in production. Set ALLOWED_DB_PLAINTEXT=true only if your hosted Postgres terminates TLS at a sidecar (e.g. Cloud SQL Auth Proxy).", + "DATABASE_URL must include sslmode=require (or verify-ca / verify-full / no-verify) in production. Set ALLOWED_DB_PLAINTEXT=true only if your hosted Postgres terminates TLS at a sidecar (e.g. Cloud SQL Auth Proxy).", path: ["DATABASE_URL"], }); } diff --git a/apps/api/tsup.config.ts b/apps/api/tsup.config.ts index 079220b..0507957 100644 --- a/apps/api/tsup.config.ts +++ b/apps/api/tsup.config.ts @@ -31,6 +31,7 @@ export default defineConfig({ /^@nestjs\//, "reflect-metadata", "rxjs", + "eventemitter2", "graphql", /^graphql-/, "@apollo/server", @@ -58,5 +59,11 @@ export default defineConfig({ "pdf-parse", "pdf2json", "mammoth", + // jsdom and isomorphic-dompurify ship runtime assets (default + // stylesheet, etc.) loaded via __dirname path joins. Bundling them + // breaks the path. Keep external so node resolves them from + // node_modules at runtime where the assets sit next to the code. + "jsdom", + "isomorphic-dompurify", ], }); diff --git a/apps/worker/package.json b/apps/worker/package.json index 59924d4..874f85a 100644 --- a/apps/worker/package.json +++ b/apps/worker/package.json @@ -15,6 +15,8 @@ "@cv/core": "workspace:*", "@cv/file-storage": "*", "@cv/handlers": "workspace:*", + "isomorphic-dompurify": "^2.16.0", + "jsdom": "^28.1.0", "@nestjs/common": "^11.1.18", "@nestjs/config": "^4.0.4", "@nestjs/core": "^11.1.18", diff --git a/apps/worker/tsup.config.ts b/apps/worker/tsup.config.ts index 976094f..5f16c2c 100644 --- a/apps/worker/tsup.config.ts +++ b/apps/worker/tsup.config.ts @@ -26,5 +26,9 @@ export default defineConfig({ "@prisma/client", "@prisma/adapter-pg", "pg", + // Same path-join asset issue as in apps/api — keep external so + // runtime resolves jsdom/isomorphic-dompurify out of node_modules. + "jsdom", + "isomorphic-dompurify", ], }); diff --git a/ci/build-images.sh b/ci/build-images.sh index 18148e3..3403368 100755 --- a/ci/build-images.sh +++ b/ci/build-images.sh @@ -39,9 +39,11 @@ build_and_push() { done echo "Building ${image}:${VERSION} from ${dockerfile}..." + # DOKS nodes are amd64; arm64 was YAGNI and doubled CI build time via + # QEMU emulation. PLATFORM env var allows local override. docker buildx build \ -f "${dockerfile}" \ - --platform linux/amd64,linux/arm64 \ + --platform "${PLATFORM:-linux/amd64}" \ --target production \ --secret "id=github_token,env=GITHUB_TOKEN" \ "${tag_args[@]}" \ diff --git a/package.json b/package.json index 0c2c82e..ac2ea45 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "root", "private": true, + "packageManager": "pnpm@10.29.1", "workspaces": [ "apps/*", "packages/*" diff --git a/packages/core/package.json b/packages/core/package.json index d3cb261..9b088cd 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -10,7 +10,9 @@ "scripts": { "test": "vitest run", "test:watch": "vitest", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "prisma:generate": "prisma generate", + "prisma:deploy": "prisma migrate deploy" }, "dependencies": { "@cv/ai-parser": "workspace:*", @@ -35,6 +37,8 @@ "@riotbyte-com/project-q-nestjs": "1.0.19-rc.5", "@riotbyte-com/project-q-prisma": "1.0.19-rc.5", "bcryptjs": "^2.4.3", + "isomorphic-dompurify": "^2.16.0", + "jsdom": "^28.1.0", "canvas": "^3.1.0", "dataloader": "^2.2.3", "express": "^5.1.0", @@ -67,6 +71,7 @@ "@types/pdf-parse": "^1.1.5", "@types/pg": "^8.15.6", "@types/turndown": "^5.0.5", + "prisma": "^7.8.0", "tsup": "^8.5.1", "typescript": "^5.3.3", "vitest": "^4.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc23944..65a1ee2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -163,6 +163,12 @@ importers: helmet: specifier: ^8.0.0 version: 8.1.0 + isomorphic-dompurify: + specifier: ^2.16.0 + version: 2.36.0(@noble/hashes@1.8.0)(canvas@3.2.1) + jsdom: + specifier: ^28.1.0 + version: 28.1.0(@noble/hashes@1.8.0)(canvas@3.2.1) mammoth: specifier: ^1.11.0 version: 1.11.0 @@ -520,6 +526,12 @@ importers: eventemitter2: specifier: ^6.4.9 version: 6.4.9 + isomorphic-dompurify: + specifier: ^2.16.0 + version: 2.36.0(@noble/hashes@1.8.0)(canvas@3.2.1) + jsdom: + specifier: ^28.1.0 + version: 28.1.0(@noble/hashes@1.8.0)(canvas@3.2.1) nest-commander: specifier: ^3.16.0 version: 3.16.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@types/inquirer@8.2.12)(typescript@5.9.3) @@ -754,6 +766,12 @@ importers: graphql: specifier: ^16.12.0 version: 16.12.0 + isomorphic-dompurify: + specifier: ^2.16.0 + version: 2.36.0(@noble/hashes@1.8.0)(canvas@3.2.1) + jsdom: + specifier: ^28.1.0 + version: 28.1.0(@noble/hashes@1.8.0)(canvas@3.2.1) mammoth: specifier: ^1.11.0 version: 1.11.0 @@ -833,6 +851,9 @@ importers: '@types/turndown': specifier: ^5.0.5 version: 5.0.6 + prisma: + specifier: ^7.8.0 + version: 7.8.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) tsup: specifier: ^8.5.1 version: 8.5.1(@swc/core@1.15.13)(jiti@2.7.0)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.2) @@ -961,7 +982,7 @@ importers: version: 4.7.9 isomorphic-dompurify: specifier: ^2.16.0 - version: 2.36.0(canvas@3.2.1) + version: 2.36.0(@noble/hashes@1.8.0)(canvas@3.2.1) marked: specifier: ^15.0.0 version: 15.0.12 @@ -14657,7 +14678,7 @@ snapshots: isobject@3.0.1: {} - isomorphic-dompurify@2.36.0(canvas@3.2.1): + isomorphic-dompurify@2.36.0(@noble/hashes@1.8.0)(canvas@3.2.1): dependencies: dompurify: 3.4.2 jsdom: 28.1.0(@noble/hashes@1.8.0)(canvas@3.2.1)