diff --git a/.github/.release-please-manifest.json b/.github/.release-please-manifest.json index df2552e0a..f325cbc23 100644 --- a/.github/.release-please-manifest.json +++ b/.github/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.15.1" + ".": "1.15.2" } diff --git a/CHANGELOG.md b/CHANGELOG.md index f48d022f6..89936f101 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.15.2](https://github.com/trycompai/crm/compare/v1.15.1...v1.15.2) (2026-08-20) + + +### Documentation + +* **api:** explain runtime openapi document and vendoring rules ([#170](https://github.com/trycompai/crm/issues/170)) ([630f2c9](https://github.com/trycompai/crm/commit/630f2c9f375b5ae084ed708d21bcdb563c774a4f)) + ## [1.15.1](https://github.com/trycompai/crm/compare/v1.15.0...v1.15.1) (2026-08-20) diff --git a/apps/api/scripts/build-func.mjs b/apps/api/scripts/build-func.mjs index 7c10a9c1a..b06f0c017 100644 --- a/apps/api/scripts/build-func.mjs +++ b/apps/api/scripts/build-func.mjs @@ -30,7 +30,15 @@ const EXTERNALS = [ "cardinal", ]; -const VENDOR_ROOTS = ["express", "@nestjs/swagger", "reflect-metadata"]; +const VENDOR_ROOTS = [ + "express", + "@nestjs/swagger", + "reflect-metadata", + "@nestjs/common", + "@nestjs/core", + "class-transformer", + "class-validator", +]; rmSync(outDir, { recursive: true, force: true }); mkdirSync(funcDir, { recursive: true }); @@ -102,7 +110,18 @@ function vendorDeps(realDir, name, placedDir) { } catch { return; } - for (const depName of Object.keys(pj.dependencies || {})) { + const optionalPeers = new Set( + Object.entries(pj.peerDependenciesMeta || {}) + .filter(([, meta]) => meta.optional) + .map(([depName]) => depName), + ); + const depNames = new Set([ + ...Object.keys(pj.dependencies || {}), + ...Object.keys(pj.peerDependencies || {}).filter( + (depName) => !optionalPeers.has(depName), + ), + ]); + for (const depName of depNames) { const depReal = resolveDep(realDir, name, depName); if (!depReal) { console.warn(` ! not found: ${depName} (needed by ${name})`); diff --git a/docs/api.md b/docs/api.md index 99f1d009a..93f4df63f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -141,6 +141,28 @@ self-hoster's admin cannot redeploy. Only `check-types` and `dev` run it. If the app cannot see a new procedure, it has not run. +## The OpenAPI document is built at runtime, not committed + +`GET /openapi.json` serves one document: Nest's own controllers plus a REST bridge +under `/rest` generated from every tRPC procedure. Swagger UI renders it at `/`. +`createApp` builds both halves and merges them, so nothing is generated at build +time and no file is checked in — the document is whatever the routers are. + +`SwaggerModule.setup` runs **before** `app.init()`, because it registers its Express +routes synchronously and Nest's own routing would otherwise shadow them. The factory +form defers building the document to the first request, which is what lets it read +the tRPC router that only exists after init. + +Two rules follow for the serverless build: + +- `@nestjs/swagger` stays in `EXTERNALS` in `apps/api/scripts/build-func.mjs`, because + `swagger-ui-dist` resolves its assets from disk at runtime and cannot be bundled. +- Anything external must be **vendored**, and vendoring follows non-optional + `peerDependencies`, not only `dependencies`. Nest packages declare their runtime + needs as peers, so following `dependencies` alone ships a function that throws + `MODULE_NOT_FOUND` on the first request. Adding a name to `EXTERNALS` without + checking it lands in `.vercel/output` breaks production, and the build stays green. + ## Two mail providers, one pipeline `apps/api/src/mailbox` is everything neither Google nor Microsoft owns: diff --git a/package.json b/package.json index c99f9db19..9ec54c190 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "crm", "private": true, "license": "MIT", - "version": "1.15.1", + "version": "1.15.2", "scripts": { "prepare": "git config core.hooksPath .githooks 2>/dev/null || true", "build": "turbo run build",