@liively/swc-jest-coverage-nestjs-plugin
v0.4.1
Published
SWC plugin to transform NestJS decorator metadata for accurate Jest coverage
Downloads
26
Maintainers
Readme
@liively/swc-jest-coverage-nestjs-plugin
SWC plugin that eliminates phantom coverage targets generated by NestJS decorator transpilation, giving you accurate function and branch coverage when using @swc/jest.
Problem
When SWC transpiles TypeScript decorators it generates wrapper code — arrow functions for type parameters, _ts_metadata calls for reflection — that Istanbul/Jest counts as coverable targets. These targets can never be "called" by your tests, so your coverage reports show artificially low numbers (e.g. 64% function coverage on a fully-tested resolver).
This plugin transforms the generated code at compile time so that Istanbul sees only the targets that correspond to real application logic.
Installation
npm install --save-dev @liively/swc-jest-coverage-nestjs-plugin
# or
pnpm add --save-dev @liively/swc-jest-coverage-nestjs-plugin
# or
yarn add --dev @liively/swc-jest-coverage-nestjs-pluginConfiguration
Add the plugin to your Jest SWC config (usually in jest.config.js or jest.config.ts):
// jest.config.js
module.exports = {
transform: {
'^.+\\.tsx?$': [
'@swc/jest',
{
jsc: {
experimental: {
plugins: [
['@liively/swc-jest-coverage-nestjs-plugin', {
// All options shown with their defaults:
// unwrapTypeArrows: true,
// unwrapDecoratorArrows: true,
// simplifyMetadataTypeofs: true,
// simplifyDesignTypeTypeofs: false,
// stripMetadata: false,
}],
],
},
},
},
],
},
};Important: After installing or updating the plugin, clear the Jest cache:
npx jest --clearCache
Options
| Option | Type | Default | Description |
|---|---|---|---|
| unwrapTypeArrows | boolean | true | Unwrap type: () => String → type: String in decorator option objects |
| unwrapDecoratorArrows | boolean | true | Unwrap ResolveField(() => String) → ResolveField(String) in decorator call arguments |
| simplifyMetadataTypeofs | boolean | true | Simplify typeof guard conditionals in design:paramtypes metadata to Object |
| simplifyDesignTypeTypeofs | boolean | false | Simplify typeof guard conditionals in design:type metadata to Object |
| stripMetadata | boolean | false | Remove _ts_metadata("design:type", ...) calls from _ts_decorate arrays |
Note:
stripMetadatadefaults tofalsebecause@nestjs/mongoosedepends ondesign:typemetadata at runtime for schema type inference. Only enable it if your project does not use Mongoose (or any other library that readsdesign:typemetadata).
Note:
simplifyDesignTypeTypeofsdefaults tofalsebecause@nestjs/mongoose@Prop()usesdesign:typeto infer schema types at runtime. Enable it only if yourdesign:typemetadata contains member-expression types (e.g.mongoose.Types.ObjectId) that generate phantom branch coverage from always-true typeof guards. For mixed codebases, use per-file overrides to enable this selectively.
Per-File Overrides
You can apply different options to different files using overrides, similar to ESLint's override syntax. Each override specifies glob patterns and config options to apply when a file matches:
// jest.config.js
module.exports = {
transform: {
'^.+\\.tsx?$': [
'@swc/jest',
{
jsc: {
experimental: {
plugins: [
['@liively/swc-jest-coverage-nestjs-plugin', {
simplifyDesignTypeTypeofs: false,
overrides: [
{
files: [
'**/venue.model*',
'**/user.model*',
'**/role.model*',
],
config: {
simplifyDesignTypeTypeofs: true,
},
},
],
}],
],
},
},
},
],
},
};Behavior:
- Glob syntax supports
*,**,{a,b}, and?patterns - Later overrides take precedence when multiple rules match the same file
- Only specified fields in an override are applied; unspecified fields inherit from the base config
- If SWC doesn't provide a filename (unlikely in practice), overrides are skipped and the base config is used
- Windows backslash paths are normalized to forward slashes before matching
How It Works
The plugin applies up to four transforms on _ts_decorate([ ... ]) call sites:
1. Unwrap decorator arrow arguments (unwrapDecoratorArrows)
// Before — Istanbul counts the arrow as a coverable function
(0, _graphql.Query)(() => Menu)
// After — no phantom function target
(0, _graphql.Query)(Menu)2. Unwrap type property arrows (unwrapTypeArrows)
// Before
_ts_param(0, (0, _graphql.Args)('id', { type: () => String }))
// After
_ts_param(0, (0, _graphql.Args)('id', { type: String }))3. Simplify design:paramtypes typeof guards (simplifyMetadataTypeofs)
// Before — Istanbul counts the ternary as 2 branches, only 1 is covered
_ts_metadata("design:paramtypes", [
typeof Express === "undefined" || typeof Express.Multer === "undefined" || typeof Express.Multer.File === "undefined" ? Object : Express.Multer.File
])
// After — no phantom branch target
_ts_metadata("design:paramtypes", [Object])4. Simplify design:type typeof guards (simplifyDesignTypeTypeofs)
// Before — same phantom branch problem for member-expression types
_ts_metadata("design:type", typeof mongoose === "undefined" || typeof mongoose.Types === "undefined" || typeof mongoose.Types.ObjectId === "undefined" ? Object : mongoose.Types.ObjectId)
// After
_ts_metadata("design:type", Object)Warning: This replaces the runtime type with
Object, which may break libraries that readdesign:typemetadata (e.g.@nestjs/mongoose@Prop()). Only enable this if you know yourdesign:typevalues are not used at runtime, or if the affected properties already specify the type explicitly in the decorator options.
5. Strip metadata calls (stripMetadata)
// Before
_ts_decorate([
(0, _graphql.Query)(),
_ts_metadata("design:type", Function),
_ts_metadata("design:paramtypes", [Object]),
_ts_metadata("design:returntype", Promise)
], Resolver.prototype, "method", null);
// After
_ts_decorate([
(0, _graphql.Query)()
], Resolver.prototype, "method", null);Only "simple" arrows are unwrapped — the body must be an identifier (String), member expression (SomeModule.Type), or array expression ([String]). Complex arrows with block bodies are left untouched to avoid changing runtime behavior.
Compatibility
| Dependency | Version |
|---|---|
| @swc/core | >= 1.15.0 (swc_core v50 plugin ABI) |
| Node.js | >= 18 |
| Jest | >= 29 with @swc/jest |
swc_core version compatibility
| Plugin version | swc_core | @swc/core |
|---|---|---|
| 0.1.x | 50 | >= 1.15.0 |
Development
# Run tests
cargo test
# Build WASM (debug)
cargo build --target wasm32-wasip1
# Build WASM (release)
bash build.shAdding test fixtures
Each fixture is a directory under tests/fixture/ containing:
input.js— the SWC-transpiled code to transformoutput.js— the expected output after transformationconfig.json(optional) — plugin options to use instead of defaults
