npm package: @liively/swc-jest-coverage-nestjs-plugin
Description: SWC plugin to transform NestJS decorator metadata for accurate Jest coverage
Author: Liively
Latest version: 0.4.1

# @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

```bash
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-plugin
```

## Configuration

Add the plugin to your Jest SWC config (usually in `jest.config.js` or `jest.config.ts`):

```js
// 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:
> ```bash
> 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:** `stripMetadata` defaults to `false` because `@nestjs/mongoose` depends on `design:type` metadata at runtime for schema type inference. Only enable it if your project does not use Mongoose (or any other library that reads `design:type` metadata).

> **Note:** `simplifyDesignTypeTypeofs` defaults to `false` because `@nestjs/mongoose` `@Prop()` uses `design:type` to infer schema types at runtime. Enable it only if your `design:type` metadata 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](#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:

```js
// 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`)

```js
// 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`)

```js
// 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`)

```js
// 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`)

```js
// 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 read `design:type` metadata (e.g. `@nestjs/mongoose` `@Prop()`). Only enable this if you know your `design:type` values are not used at runtime, or if the affected properties already specify the type explicitly in the decorator options.

### 5. Strip metadata calls (`stripMetadata`)

```js
// 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

```bash
# Run tests
cargo test

# Build WASM (debug)
cargo build --target wasm32-wasip1

# Build WASM (release)
bash build.sh
```

### Adding test fixtures

Each fixture is a directory under `tests/fixture/` containing:

- `input.js` — the SWC-transpiled code to transform
- `output.js` — the expected output after transformation
- `config.json` (optional) — plugin options to use instead of defaults

## License

[MIT](LICENSE)