npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

nextcov

v0.7.5

Published

Collect test coverage for Playwright E2E tests in Next.js applications

Readme

nextcov

npm version License: MIT

V8 code coverage for Next.js applications with Playwright E2E tests.

Merge your Playwright E2E coverage with Vitest unit test coverage for complete coverage reports.

Why nextcov?

The React Server Components Testing Gap

Next.js App Router introduced React Server Components (RSC) and async server components. These are notoriously difficult to unit test because:

  • Server Components run only on the server - They can't be rendered in jsdom or similar test environments
  • Async components fetch data directly - Mocking becomes complex and often unreliable
  • Tight coupling with Next.js runtime - Server actions, cookies, headers require the full framework

The practical solution? Test server components through E2E tests with Playwright, where they run in their natural environment. But until now, there was no good way to get coverage for these tests.

The Coverage Problem

But this creates a coverage gap:

  • Unit tests (Vitest) cover client components, utilities, and hooks
  • E2E tests (Playwright) cover server components, pages, and user flows
  • No unified coverage - You're missing the full picture

Getting accurate combined coverage is challenging because:

  • Playwright runs against production builds (bundled, minified code)
  • Source maps are needed to map back to original TypeScript/JSX
  • Different coverage formats need to be merged correctly

The Solution

nextcov is the first tool to bridge this gap by:

  • Collecting V8 coverage from both client and server during E2E tests
  • Using source maps to map bundled code back to original sources
  • Producing Istanbul-compatible output that merges seamlessly with Vitest coverage

Now you can finally see the complete coverage picture for your Next.js application.

Features

  • Next.js focused - Built specifically for Next.js applications
  • Client + Server coverage - Collects coverage from both browser and Node.js server
  • Dev mode support - Works with next dev (no build required), auto-detected
  • Production mode support - Works with next build && next start using external source maps
  • Auto-detection - Automatically detects dev vs production mode, no configuration needed
  • V8 native coverage - Uses Node.js built-in NODE_V8_COVERAGE for accurate server coverage
  • Source map support - Maps bundled code back to original TypeScript/JSX
  • Vitest compatible - Output merges seamlessly with Vitest coverage reports
  • Playwright integration - Simple fixtures for automatic coverage collection
  • Istanbul format - Generates standard coverage-final.json for tooling compatibility
  • Multiple reporters - HTML, LCOV, JSON, text-summary, and more

Inspiration

This project is inspired by and builds upon:

Installation

npm install nextcov --save-dev

Requirements

  • Node.js >= 20
  • Next.js 14+ (including Next.js 15)
  • Playwright 1.40+

Peer Dependencies

npm install @playwright/test --save-dev

Example Projects

nextcov-example

See nextcov-example for a simple Next.js App Router application demonstrating nextcov with Playwright E2E tests.

Highlights:

  • Simple todo CRUD application
  • 100% branch coverage achieved with E2E tests
  • Demonstrates coverage for client components with conditional rendering

| Metric | Coverage | |--------|----------| | Statements | 100% | | Branches | 100% | | Functions | 100% | | Lines | 100% |

restaurant-reviews-platform

See restaurant-reviews-platform for a complete working example of nextcov integrated with a Next.js App Router application using Playwright E2E tests and Vitest unit tests.

Highlights:

  • Full-stack Next.js application with authentication
  • Combines unit tests (Vitest) with E2E tests (Playwright)
  • Demonstrates merging coverage from multiple sources

| Coverage Type | Lines | Description | |---------------|-------|-------------| | Unit Tests (Vitest) | ~80% | Client components, utilities, API routes | | E2E Tests (Playwright + nextcov) | ~46% | Server components, pages, user flows | | Merged | ~88% | Complete picture of your application |

Key Files (restaurant-reviews-platform)

Quick Start

1. Configure Next.js for Source Maps

In your next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  // Enable source maps for production builds (required for coverage)
  productionBrowserSourceMaps: true,

  // Optional: Configure webpack for E2E mode
  webpack: (config, { dev }) => {
    if (process.env.E2E_MODE) {
      // Use full source maps for accurate coverage
      config.devtool = 'source-map'

      // Disable minification to preserve readable code
      config.optimization = {
        ...config.optimization,
        minimize: false,
      }
    }
    return config
  },
}

module.exports = nextConfig

2. Configure Playwright with nextcov

In your playwright.config.ts:

import { defineConfig, devices } from '@playwright/test'
import type { NextcovConfig } from 'nextcov'

// Extend Playwright config type to include nextcov
type PlaywrightConfigWithNextcov = Parameters<typeof defineConfig>[0] & {
  nextcov?: NextcovConfig
}

// Export nextcov config separately for use in global-teardown
export const nextcov: NextcovConfig = {
  cdpPort: 9230,
  buildDir: '.next',           // Next.js build output directory (use 'dist' if customized)
  outputDir: 'coverage/e2e',
  sourceRoot: './src',
  include: ['src/**/*.{ts,tsx,js,jsx}'],
  exclude: [
    'src/**/__tests__/**',
    'src/**/*.test.{ts,tsx}',
    'src/**/*.spec.{ts,tsx}',
  ],
  reporters: ['html', 'lcov', 'json', 'text-summary'],
  log: true,                   // Enable verbose logging (default: false)
}

const config: PlaywrightConfigWithNextcov = {
  testDir: './e2e',
  globalSetup: './e2e/global-setup.ts',
  globalTeardown: './e2e/global-teardown.ts',
  use: {
    baseURL: 'http://localhost:3000',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
  nextcov,
}

export default defineConfig(config)

3. Add Coverage Fixture

Create e2e/fixtures.ts:

import { test as base, expect } from '@playwright/test'
import { collectClientCoverage } from 'nextcov/playwright'

export const test = base.extend({
  // Auto-collect v8 coverage for each test
  coverage: [
    async ({ page }, use, testInfo) => {
      await collectClientCoverage(page, testInfo, use)
    },
    { scope: 'test', auto: true },
  ],
})

export { expect }

4. Add Global Setup and Teardown

Create e2e/global-setup.ts:

import * as path from 'path'
import { startServerCoverage, loadNextcovConfig } from 'nextcov/playwright'

export default async function globalSetup() {
  // Load config from playwright.config.ts
  const config = await loadNextcovConfig(
    path.join(process.cwd(), 'playwright.config.ts')
  )
  // Start server coverage collection (auto-detects dev vs production mode)
  await startServerCoverage(config)
}

Create e2e/global-teardown.ts:

import * as path from 'path'
import { finalizeCoverage } from 'nextcov/playwright'
import { loadNextcovConfig } from 'nextcov'

export default async function globalTeardown() {
  // Load config from playwright.config.ts
  const config = await loadNextcovConfig(
    path.join(process.cwd(), 'playwright.config.ts')
  )
  await finalizeCoverage(config)
}

5. Write Tests Using the Fixture

In your test files (e2e/example.spec.ts):

import { test, expect } from './fixtures'

test('should load home page', async ({ page }) => {
  await page.goto('/')
  await expect(page.getByRole('heading')).toBeVisible()
})

6. Run Tests

# Build Next.js with source maps (use E2E_MODE for optimal coverage)
E2E_MODE=true npm run build

# Start the server with V8 coverage enabled and run tests
NODE_V8_COVERAGE=.v8-coverage NODE_OPTIONS='--inspect=9230' npm run start &
npx playwright test

# Or use start-server-and-test for better cross-platform support
npx start-server-and-test 'NODE_V8_COVERAGE=.v8-coverage NODE_OPTIONS=--inspect=9230 npm start' http://localhost:3000 'npx playwright test'

The key environment variables:

  • NODE_V8_COVERAGE=.v8-coverage - Enables Node.js to collect V8 coverage data
  • NODE_OPTIONS='--inspect=9230' - Enables CDP connection for triggering coverage flush

Development Mode Coverage

nextcov supports collecting coverage directly from next dev without requiring a production build. This is useful for faster iteration during development.

Auto-Detection

nextcov automatically detects whether you're running in dev mode or production mode. You don't need to configure anything - just use the same globalSetup and globalTeardown for both modes.

How it works:

  • Dev mode (next dev --inspect=9230): Next.js spawns a worker process on port 9231 (inspect port + 1). nextcov connects to the worker via CDP and uses Profiler.startPreciseCoverage() to collect coverage.
  • Production mode (next start --inspect=9230): Next.js runs on port 9230 directly. nextcov uses NODE_V8_COVERAGE env var to collect coverage, triggered via CDP.

The auto-detection output looks like:

📊 Auto-detecting server mode...
  Trying dev mode (worker port 9231)...
  ✓ Dev mode detected (webpack eval scripts found)
  ✓ Server coverage collection started

Or for production mode:

📊 Auto-detecting server mode...
  Trying dev mode (worker port 9231)...
  ⚠️ Failed to connect to CDP (dev mode): Error: connect ECONNREFUSED
  ℹ️ Production mode will be used (NODE_V8_COVERAGE + port 9230)

Running Tests Against Dev Server

# Start Next.js dev server with V8 coverage and inspector enabled
NODE_V8_COVERAGE=.v8-coverage NODE_OPTIONS='--inspect=9230' npm run dev &

# Run Playwright tests
npx playwright test

Dev Mode vs Production Mode

| Aspect | Dev Mode | Production Mode | |--------|----------|-----------------| | Server Command | next dev | next build && next start | | Source Maps | Inline (base64 in JS) | External (.map files) | | Build Required | No | Yes | | Hot Reload | Yes | No | | Build Directory | Not used (inline source maps) | Configurable (buildDir) | | CDP Port | cdpPort + 1 (e.g., 9231) | cdpPort (e.g., 9230) | | Performance | Slower | Faster | | Recommended For | Development iteration | CI/CD, final coverage |

When to Use Each Mode

  • Dev Mode: Quick feedback during development, testing new features
  • Production Mode: CI pipelines, accurate production-like coverage, final reports

Both modes produce identical Istanbul-compatible output that can be merged with Vitest coverage.

Merging with Vitest Coverage

The main power of nextcov is combining E2E coverage with unit test coverage.

Configure Vitest for Coverage

In your vitest.config.ts:

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['json', 'html'],
      reportsDirectory: './coverage/unit',
    },
  },
})

Using the CLI (Recommended)

The simplest way to merge coverage is using the nextcov CLI:

# Merge unit and E2E coverage
npx nextcov merge coverage/unit coverage/e2e -o coverage/merged

# Merge multiple coverage directories
npx nextcov merge coverage/unit coverage/e2e coverage/browser -o coverage/all

# Customize reporters
npx nextcov merge coverage/unit coverage/e2e --reporters html,lcov,json

Add to your package.json:

{
  "scripts": {
    "coverage:merge": "npx nextcov merge coverage/unit coverage/integration -o coverage/merged"
  }
}

CLI Reference

Usage: npx nextcov merge <dirs...> [options]

Merge multiple coverage directories into a single report.

Arguments:
  dirs                  Coverage directories to merge (must contain coverage-final.json)

Options:
  -o, --output <dir>    Output directory for merged report (default: ./coverage/merged)
  --reporters <list>    Comma-separated reporters: html,lcov,json,text-summary (default: html,lcov,json,text-summary)
  --help                Show this help message

Examples:
  npx nextcov merge coverage/unit coverage/integration
  npx nextcov merge coverage/unit coverage/e2e coverage/browser -o coverage/merged
  npx nextcov merge coverage/unit coverage/integration --reporters html,lcov

Using the API (Advanced)

For more control, you can use the programmatic API:

import * as path from 'path'
import {
  mergeCoverage,
  printCoverageSummary,
  printCoverageComparison,
} from 'nextcov'

const projectRoot = process.cwd()

async function main() {
  console.log('Merging coverage reports...\n')

  const result = await mergeCoverage({
    unitCoveragePath: path.join(projectRoot, 'coverage/unit/coverage-final.json'),
    e2eCoveragePath: path.join(projectRoot, 'coverage/e2e/coverage-final.json'),
    outputDir: path.join(projectRoot, 'coverage/merged'),
    projectRoot,
    verbose: true,
  })

  if (!result) {
    console.error('Failed to merge coverage')
    process.exit(1)
  }

  // Print merged summary
  printCoverageSummary(result.summary, 'Merged Coverage Summary')

  // Print comparison
  if (result.unitSummary) {
    printCoverageComparison(result.unitSummary, result.e2eSummary, result.summary)
  }

  // List E2E-only files
  if (result.e2eOnlyFiles.length > 0) {
    console.log(`\nE2E-only files (${result.e2eOnlyFiles.length}):`)
    for (const file of result.e2eOnlyFiles) {
      console.log(`  - ${file}`)
    }
  }
}

main().catch(console.error)

Run with:

npx ts-node --esm scripts/merge-coverage.ts

API Reference

Playwright Integration (nextcov/playwright)

startServerCoverage(config?)

Starts server-side coverage collection. Call in globalSetup. Auto-detects dev mode vs production mode.

import { startServerCoverage, loadNextcovConfig } from 'nextcov/playwright'

const config = await loadNextcovConfig('./playwright.config.ts')
await startServerCoverage(config)

Returns true if dev mode was detected, false for production mode.

collectClientCoverage(page, testInfo, use)

Collects V8 coverage for a single test. Use in a Playwright fixture.

await collectClientCoverage(page, testInfo, use)

finalizeCoverage(options?)

Finalizes coverage collection and generates reports. Call in globalTeardown.

| Option | Type | Default | Description | |--------|------|---------|-------------| | projectRoot | string | process.cwd() | Project root directory | | buildDir | string | '.next' | Next.js build output directory | | outputDir | string | 'coverage/e2e' | Output directory for reports | | sourceRoot | string | './src' | Source root relative to project | | include | string[] | ['src/**/*'] | Glob patterns to include | | exclude | string[] | ['node_modules/**'] | Glob patterns to exclude | | reporters | string[] | ['html', 'lcov', 'json'] | Report formats | | collectServer | boolean | true | Collect server-side coverage | | collectClient | boolean | true | Collect client-side coverage | | cleanup | boolean | true | Clean up temp files | | cdpPort | number | 9230 | CDP port for triggering v8.takeCoverage() | | log | boolean | false | Enable verbose logging output |

Main API (nextcov)

loadNextcovConfig(configPath?)

Loads nextcov configuration from playwright.config.ts.

import { loadNextcovConfig } from 'nextcov'

const config = await loadNextcovConfig('./e2e/playwright.config.ts')

V8ServerCoverageCollector

Collector for server-side V8 coverage using NODE_V8_COVERAGE + CDP trigger.

import { V8ServerCoverageCollector } from 'nextcov'

const collector = new V8ServerCoverageCollector({
  cdpPort: 9230,
  buildDir: '.next',
  sourceRoot: './src',
})

const connected = await collector.connect()
if (connected) {
  const coverage = await collector.collect()
  console.log(`Collected ${coverage.length} entries`)
}

mergeCoverage(options)

Merges unit and E2E coverage reports.

const result = await mergeCoverage({
  unitCoveragePath: './coverage/unit/coverage-final.json',
  e2eCoveragePath: './coverage/e2e/coverage-final.json',
  outputDir: './coverage/merged',
  reporters: ['html', 'lcov', 'json'],
  verbose: false,
  projectRoot: process.cwd(),
})

CoverageProcessor

Low-level class for processing V8 coverage.

import { CoverageProcessor } from 'nextcov'

const processor = new CoverageProcessor(projectRoot, {
  outputDir: './coverage',
  sourceRoot: './src',
  include: ['src/**/*.{ts,tsx}'],
  exclude: ['**/*.test.*'],
  reporters: ['html', 'json'],
})

const result = await processor.processAllCoverage(v8CoverageEntries)

CoverageMerger

Class for merging coverage maps with different strategies.

import { CoverageMerger } from 'nextcov'

const merger = new CoverageMerger({
  strategy: 'max',        // 'max' | 'add' | 'prefer-first' | 'prefer-last'
  applyFixes: true,       // Apply coverage fixes
})

const merged = await merger.merge(map1, map2, map3)

How It Works

  1. Coverage Collection

    • Client: Uses Playwright's CDP integration to collect V8 coverage from the browser
    • Server: Uses Node.js NODE_V8_COVERAGE env var to collect coverage, triggered via CDP v8.takeCoverage()
  2. Source Mapping

    • Loads source maps from Next.js build output (.next/)
    • Handles inline source maps and external .map files
    • Maps bundled JavaScript back to original TypeScript/JSX
  3. Format Conversion

    • Converts V8 coverage format to Istanbul format using AST analysis
    • Preserves accurate line, function, and branch coverage
  4. Merging

    • Merges coverage from multiple sources (unit tests, E2E tests)
    • Uses intelligent strategies to combine coverage data
    • Handles different instrumentation structures
  5. Report Generation

    • Generates Istanbul-compatible reports (HTML, LCOV, JSON, etc.)
    • Compatible with standard coverage tools and CI integrations

Troubleshooting

0% Coverage

  • Ensure productionBrowserSourceMaps: true is set in next.config.js
  • Verify source maps exist in .next/static/chunks/*.map
  • Check that E2E_MODE=true is set when building

Server Coverage Not Working

  • Ensure Next.js is started with NODE_V8_COVERAGE=.v8-coverage and NODE_OPTIONS='--inspect=9230'
  • Verify the CDP port matches your config
  • Check that globalTeardown calls finalizeCoverage()

Source Maps Not Found

  • Run npm run build with E2E_MODE=true
  • Check .next/static/chunks/ for .map files
  • Ensure webpack devtool is set to 'source-map'

Dev Mode Coverage Not Working

  • Ensure Next.js dev server is started with NODE_V8_COVERAGE=.v8-coverage and NODE_OPTIONS='--inspect=9230'
  • Check that your source files are in the sourceRoot directory (default: src)

Slow Coverage Processing

If coverage processing takes a very long time (30+ seconds), you may have large bundled dependencies. V8 coverage works on the bundled output, so large libraries bundled into your app will slow down source map processing.

Common culprits:

  • react-icons - Barrel exports bundle entire icon sets even when importing a few icons
  • Large UI component libraries
  • Unoptimized imports from lodash, @mui/icons-material, etc.

Solutions:

  1. Use direct imports instead of barrel imports:

    // Bad - bundles entire icon set
    import { FiEdit } from 'react-icons/fi'
    
    // Good - import only what you need
    import FiEdit from 'react-icons/fi/FiEdit'
  2. Use inline SVGs for icons you use frequently:

    // Best for small icon sets - zero runtime cost
    export const EditIcon = ({ size = 24 }) => (
      <svg width={size} height={size} viewBox="0 0 24 24">
        <path d="..." />
      </svg>
    )
  3. Enable optimizePackageImports in Next.js config:

    experimental: {
      optimizePackageImports: ['react-icons', 'lodash'],
    }
  4. Check your bundle size: If .next/server/app/page.js is several MB, you likely have bundle bloat. A lean app should have page bundles under 500KB.

License

MIT