Skip to content

feat: require NestJS 12, reflect-metadata 0.2.2 and Vitest 5 - #80

Merged
joshunrau merged 2 commits into
mainfrom
feat/nest-12
Oct 6, 2026
Merged

joshunrau merged 2 commits into
mainfrom
feat/nest-12

Conversation

@joshunrau

Copy link
Copy Markdown
Collaborator

Summary

This prepares libnest 10.0.0, which requires NestJS 12 and Vitest 5. It unblocks Open Data Capture's move to both. The commit carries a BREAKING CHANGE: footer, so semantic-release will cut a major. I checked this locally with @semantic-release/commit-analyzer and the conventionalcommits preset: the result is major.

What changed

Peer and dependency ranges

Package Before After Why
@nestjs/common, core, platform-fastify, testing (peer) ^11 ^12.0.0 Nest 11 is dropped
reflect-metadata (peer) ~0.1.13 ^0.2.2 0.2.0 overflows the stack in isProviderFor, or silently loses metadata, when a second copy is loaded. 0.2.1 fixed this with a shared registry.
vitest (peer) 4.x 5.x
fastify (peer) ^5.6.1 ^5.12.1 @nestjs/platform-fastify pins fastify exactly: 5.12.1 (12.0.0–12.0.1), 5.12.4 (12.0.2–12.0.3), 5.12.5 (12.0.4–12.1.2). ^5.12.1 accepts every one of those, and the lockfile resolves a single fastify 5.12.5.
@nestjs/throttler ^6.5.0 ^6.7.1 6.5.0's peers stop at Nest 11
@nestjs/swagger ^11.2.5 removed Nothing has imported it since v9's zod-native OpenAPI generator. It was also, by accident, the only thing installing @nestjs/platform-express here (see below).
vitest, @vitest/coverage-v8 (dev) ^4.0.17 ^5.0.3
vite (dev, new) – ^7.3.6 Vitest 5 declares vite only as a required peer. Without it, pnpm reused a stale vite 6.3.3, which does not satisfy ^6.4.0. This matches ODC's catalog.
engines.node 22.x || 24.x ^22.12.0 || 24.x Vitest 5, now a required peer, needs Node ^22.12.0 on the 22 line

The lockfile has exactly one reflect-metadata (0.2.2), one each of @nestjs/common, core, platform-fastify and testing (12.1.2), and one fastify (5.12.5).

libnest build: externals for Nest 12's lazy imports

Nest 12 ships as ESM. It loads its optional packages with literal import() calls such as tryLoadPackage('@nestjs/platform-express', () => import('@nestjs/platform-express')). esbuild resolves these at build time, and the build fails when a package is absent. The externals list moves to an exported OPTIONAL_EXTERNALS and gains three entries:

  • @nestjs/platform-express. NestFactory.create now always preloads it.
  • @fastify/multipart. This is a new lazy import in platform-fastify 12.
  • @nestjs/websockets/socket-module.js. Nest now imports the .js subpath, so the old @nestjs/websockets/socket-module entry no longer matched.

@nestjs/microservices still covers @nestjs/microservices/microservices-module.js, because esbuild treats a bare package name as a prefix.

A new build test bundles the example app, checks that every optional specifier is left as a runtime import(), then imports the bundle and starts the app with createApplicationInstance() and init(). This proves Nest runs without those packages installed.

LoggingService redeclares @Optional()

Nest 12 no longer applies a parent's @Optional() flags to a subclass that declares its own constructor. LoggingService extends JSONLogger, so under Nest 11 its options parameter was optional by inheritance. It now redeclares @Optional() on that parameter, so it stays resolvable without LOGGING_MODULE_OPTIONS_TOKEN. A new test fails without it. INQUIRER always resolves under Nest 12, so it needs no flag. AppModule and JSONLogger themselves are not subclasses, so they are unaffected.

The e2e testing helper runs on Fastify

e2e() from @douglasneuroinformatics/libnest/testing called createNestApplication() with no adapter, so it ran on Nest's default Express adapter, which needs @nestjs/platform-express. It now passes new FastifyAdapter(appContainer.fastifyOptions) and waits for Fastify's ready(), so e2e tests run on the adapter production uses. The current-user and app.factory tests were moved off Express the same way.

Vitest 5

  • clearMocks now defaults to true, which cleared calls recorded in beforeAll. The app.factory CryptoService tests now assert on the provided instance. The prisma.extensions test snapshots the defineExtension calls inside beforeAll.
  • The vitest plugin (testing/plugin), MockFactory/MockedInstance, the Prisma mocks and src/testing/setup.ts needed no changes. They use no removed entry points (vitest/coverage, vitest/reporters, …), no non-top-level vi.mock/vi.hoisted, and no worker IDs. clearMocks leaves mock implementations intact.
  • The existing /* v8 ignore if */ in build.ts is still needed under Vitest 5. Removing it drops branch coverage to 99.66%, so it stays, with its comment updated.

Checked, no change needed

  • PipeTransform. ArgumentMetadata is now generic and gains schema. ValidationPipe, ValidObjectIdPipe and ParseSchemaPipe type-check unchanged and their tests pass.
  • Lifecycle hook ordering. libnest implements no lifecycle hooks.

Migration notes for consumers

  • Upgrade @nestjs/common, core, platform-fastify and testing to ^12, reflect-metadata to ^0.2.2, vitest and @vitest/* to 5.x, and fastify to the version your @nestjs/platform-fastify pins. Install vite explicitly if your package manager does not auto-install peers.
  • Use one reflect-metadata 0.2.2 across the whole install. A leftover 0.1.x copy alongside 0.2.x can still drop decorator metadata.
  • e2e() now runs your app on Fastify. Express-only middleware, or tests that relied on Express semantics, may need updating. Its body limit is Fastify's 1 MiB default unless fastifyOptions sets one. Production keeps libnest's 50 MiB default.
  • libnest build no longer bundles @nestjs/platform-express, @fastify/multipart or @nestjs/websockets/socket-module.js, even when they are installed. If your own code imports any of them, the bundle will import it from node_modules at runtime.
  • Vitest 5 clears mock call history before every test (clearMocks: true). Assertions on calls made in beforeAll must move into the test, or you can set clearMocks: false.

Verification

Run from the repo root on the committed tree:

  • pnpm install --frozen-lockfile: lockfile up to date
  • pnpm prisma:generate && pnpm lint: pass
  • pnpm test:coverage: 50 files and 230 tests pass, with 100% statements, branches, functions and lines
  • pnpm build: pass
  • pnpm example:build && pnpm example:start: the bundled example serves /docs/spec.json (200) and /docs (200, HTML). POST /cats returns 201, GET /cats returns the created cat, and an invalid body returns 400.
  • pnpm example:dev --no-watch (libnest dev on Nest 12's ESM packages): POST /cats returns 201 and /docs/spec.json returns 200.

Against Open Data Capture

An npm-packed tarball of this branch was installed into a scratch worktree of Open Data Capture, with Nest 12.1.1, reflect-metadata 0.2.2 and Vitest 5.0.2 (12.1.2 and 5.0.3 were still inside ODC's 7-day release-age window):

  • One copy of each. The lockfile has a single reflect-metadata (0.2.2), a single version of each Nest core package and a single fastify. The production bundle contains one reflect-metadata registry.
  • pnpm lint: all 34 tasks pass.
  • Unit tests: all 4,314 pass, including every api test. That covers MockFactory, the vitest plugin and the SWC transform. ODC needs three Vitest 5 changes of its own, none involving libnest: its project configs inherit the root projects, two tests assert on calls cleared by clearMocks, and V8 now counts ?? branches.
  • libnest build of the API, run without node_modules, as in ODC's Docker image: the app boots and every route maps. /spec.json returns 200 with 39 paths, and a bad body returns 400. Setup, login, and group and user CRUD work, and a missing token returns 401.
  • Playwright e2e, with the API on libnest dev: 276 of 277 pass. The one failure is a subject-graph test that then passed 5 out of 5 times on repeat.

Note for reviewers

The commit header is feat: rather than feat!:. The repo's commit-msg hook rejects ! because commitlint 19.8 resolves conventional-changelog-conventionalcommits 9.3.1 and gets parserOpts: undefined, then falls back to a parser without ! support. This is a pre-existing tooling issue. semantic-release still reads the BREAKING CHANGE: footer as a major.

🤖 Generated with Claude Code

joshunrau and others added 2 commits October 6, 2026 13:42
Move every NestJS peer to ^12.0.0, reflect-metadata to ^0.2.2 and vitest to
5.x. reflect-metadata 0.2.0 overflows the stack or loses decorator metadata
when a second copy is loaded; 0.2.1 fixed that with a shared registry.

Accept the fastify versions @nestjs/platform-fastify 12 pins (5.12.1 to
5.12.5), require @nestjs/throttler 6.7.1, the first release to accept Nest 12,
and drop @nestjs/swagger, which nothing has imported since the zod-native
OpenAPI generator replaced it. vite becomes a dev dependency, as Vitest 5 only
declares it as a peer.

Nest 12 loads its optional packages with literal import() calls, which esbuild
resolves at build time, so `libnest build` now also leaves out
@nestjs/platform-express, @fastify/multipart and the .js-suffixed
@nestjs/websockets/socket-module.js. A new build test starts the bundled
example app, proving Nest runs without them.

Nest 12 no longer applies a parent's @optional() flags to a subclass that
declares its own constructor, so LoggingService redeclares @optional() on its
options and stays resolvable without LOGGING_MODULE_OPTIONS_TOKEN.

The e2e testing helper now runs the app on the Fastify adapter, as production
does, instead of Nest's default Express adapter, which needed
@nestjs/platform-express installed and was only present here through
@nestjs/swagger.

Tests no longer rely on mock calls recorded in beforeAll, which Vitest 5 clears
before each test now that clearMocks defaults to true.

BREAKING CHANGE: requires @nestjs/common, @nestjs/core, @nestjs/platform-fastify
and @nestjs/testing ^12.0.0, reflect-metadata ^0.2.2, fastify ^5.12.1 and
vitest 5.x. The e2e helper from @douglasneuroinformatics/libnest/testing now
runs the app on the Fastify adapter instead of Express.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vitest 5, now a required peer, supports Node ^22.12.0 || ^24.0.0 || >=26.0.0,
so the engines range no longer claims the 22.x releases before 22.12.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (ce20531) to head (3bec8df).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main       #80   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           64        64           
  Lines          838       836    -2     
  Branches       147       147           
=========================================
- Hits           838       836    -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@joshunrau
joshunrau merged commit 42e1102 into main Oct 6, 2026
3 checks passed
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 10.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant