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 🙏

© 2026 – Pkg Stats / Ryan Hefner

runstack

v1.2.2

Published

End-to-end testing framework for container-based integration tests

Readme

RunStack

Write integration tests that run inside Docker containers with real dependencies.

RunStack lets you define test scenarios as TypeScript code, automatically builds Docker images, orchestrates dependent services (databases, caches, APIs), and runs your tests in isolated environments. It's designed to work with Node.js's built-in test runner.

Why RunStack?

Integration testing often requires external services—databases, message queues, or third-party APIs. Setting these up manually is tedious and error-prone. RunStack automates this by:

  • Defining infrastructure as code — Your test setup lives in the same file as your tests
  • Managing container lifecycles — Services start before tests, clean up after
  • Handling dependencies — Peer containers start in order with health checks
  • Integrating with node:test — Familiar test syntax, no new test runner to learn

Runtime Support

runstack works with both Node.js (≥20) and Bun (≥1.0). The library automatically detects your runtime and uses the native test runner:

  • Node.js: Uses the built-in node:test module
  • Bun: Uses the built-in bun:test module

All features work identically across both runtimes.

Installation

Node.js

npm install runstack --save-dev
# or
yarn add runstack --dev
# or
pnpm add runstack --save-dev

Bun

bun add runstack --dev

Requirements:

  • Node.js >= 24
  • Docker installed and running

Quick Start

Create a test file and use the describeDockerTests function:

import { describeDockerTests, PeerContainer } from 'runstack';

function createPostgresPeer(): PeerContainer {
  const host = 'postgres';
  const user = 'testuser';
  const password = 'testpass';
  const database = 'testdb';

  return {
    id: host,
    service: {
      image: 'postgres:16-alpine',
      environment: {
        POSTGRES_USER: user,
        POSTGRES_PASSWORD: password,
        POSTGRES_DB: database
      },
      healthcheck: {
        test: ['CMD-SHELL', `pg_isready -U ${user}`],
        interval: '5s',
        timeout: '5s',
        retries: 5
      }
    },
    injectEnvironment: {
      PGHOST: host,
      PGUSER: user,
      PGPASSWORD: password,
      PGDATABASE: database
    }
  };
}

describeDockerTests('Database Integration', (it) => {
  it('can query the database', {
    baseImage: 'postgres:16-alpine',
    testScript: `
      psql -c "SELECT version();"
    `,
    peerContainers: [createPostgresPeer()]
  }, async (result) => {
    if (!result.ok) {
      throw new Error(`Test failed: ${result.error.message}`);
    }
    if (!result.output.includes('PostgreSQL')) {
      throw new Error('Expected PostgreSQL version in output');
    }
  });
});

The injectEnvironment option passes configuration from the peer container into your test container as environment variables. Here, psql automatically uses PGHOST, PGUSER, PGPASSWORD, and PGDATABASE—no command-line flags needed.

Run with Node's test runner:

node --test your-test-file.test.ts

How It Works

Each test scenario defines:

  1. A base image — The Docker image your test runs in
  2. A test script — Shell commands to execute
  3. Optional peer containers — Services your test depends on
  4. Optional files — Configuration or code to inject

RunStack generates Dockerfiles, builds images, starts services in dependency order, waits for health checks, runs your test, and cleans up afterward.

Core Concepts

Test Scenarios

A TestScenario configures the test environment:

interface TestScenario {
  baseImage: string;           // Docker image for the test container
  testScript: string;          // Shell script to execute
  addlSteps?: string;          // Extra Dockerfile instructions
  files?: Files;               // Files to inject into the container
  peerContainers?: PeerContainer[];  // Supporting services
  containerExtensions?: TestContainerExtension[];  // Build modifications
}

Peer Containers

Peer containers are services your test interacts with—databases, caches, message queues, etc. The injectEnvironment option exposes connection details to your test script as environment variables.

function createRedisPeer(): PeerContainer {
  const host = 'redis';
  const port = '6379';

  return {
    id: host,
    service: {
      image: 'redis:7-alpine',
      healthcheck: {
        test: ['CMD', 'redis-cli', 'ping'],
        interval: '5s',
        timeout: '3s',
        retries: 3
      }
    },
    injectEnvironment: {
      REDIS_HOST: host,
      REDIS_PORT: port
    }
  };
}

Containers start in the order they appear in the array. Each container waits for previous ones to be healthy before starting.

File Injection

Inject files at build time:

{
  baseImage: 'alpine',
  files: {
    'schema.sql': 'CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT);',
    'scripts/setup.sh': {
      content: '#!/bin/bash\npsql -f /schema.sql',
      chmod: '755'
    }
  },
  testScript: './scripts/setup.sh'
}

Custom Build Steps

Add Dockerfile instructions with addlSteps:

{
  baseImage: 'node:22-alpine',
  addlSteps: `
    RUN apk add --no-cache postgresql-client
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci
  `,
  testScript: 'npm test'
}

Global Dockerfile Steps

Configure steps that apply to all test containers using setGlobalDockerfileSteps. This is useful for setup that should be consistent across your entire test suite, such as installing CA certificates:

import { setGlobalDockerfileSteps, describeDockerTests } from 'runstack';

// Configure global steps once, typically in a test setup file
setGlobalDockerfileSteps(`
  COPY certs/ca.crt /usr/local/share/ca-certificates/ca.crt
  RUN update-ca-certificates && \\
      openssl verify -CAfile /usr/local/share/ca-certificates/ca.crt /usr/local/share/ca-certificates/ca.crt
`);

// All tests will now include these steps
describeDockerTests('My Tests', (it) => {
  it('connects via TLS', {
    baseImage: 'node:20',
    testScript: 'node test-tls-connection.js',
  }, async (result) => {
    // ...
  });
});

Global steps are inserted after the base debugging tools and before any per-test addlSteps. To reset global steps, pass an empty string:

setGlobalDockerfileSteps('');

Container Extensions

For more advanced builds, container extensions let you add multi-stage build logic or modify the final image:

const testHelperExtension = {
  id: 'test-helper',
  stages: `
    FROM node:22-alpine AS test-helper
    RUN npm install -g jest
  `,
  finalSteps: `
    COPY --from=test-helper /usr/local/bin/jest /usr/local/bin/jest
  `,
  environment: {
    NODE_ENV: 'test'
  }
};

Examples

Testing Database Migrations

import { describeDockerTests, PeerContainer } from 'runstack';

function createPostgresPeer(): PeerContainer {
  const host = 'postgres';
  const user = 'testuser';
  const password = 'testpass';
  const database = 'testdb';

  return {
    id: host,
    service: {
      image: 'postgres:16-alpine',
      environment: {
        POSTGRES_USER: user,
        POSTGRES_PASSWORD: password,
        POSTGRES_DB: database
      },
      healthcheck: {
        test: ['CMD-SHELL', `pg_isready -U ${user}`],
        interval: '5s',
        timeout: '5s',
        retries: 5
      }
    },
    injectEnvironment: {
      PGHOST: host,
      PGUSER: user,
      PGPASSWORD: password,
      PGDATABASE: database
    }
  };
}

describeDockerTests('Database Migrations', (it) => {
  it('applies schema and seeds data', {
    baseImage: 'postgres:16-alpine',
    files: {
      'schema.sql': `
        CREATE TABLE users (
          id SERIAL PRIMARY KEY,
          email TEXT UNIQUE NOT NULL,
          created_at TIMESTAMP DEFAULT NOW()
        );
      `,
      'seed.sql': `
        INSERT INTO users (email) VALUES ('[email protected]'), ('[email protected]');
      `
    },
    testScript: `
      psql -f /schema.sql
      psql -f /seed.sql
      psql -c "SELECT COUNT(*) FROM users;" | grep -q "2"
    `,
    peerContainers: [createPostgresPeer()]
  }, async (result) => {
    if (!result.ok) throw new Error(result.error.message);
  });
});

Testing with Multiple Services

import { describeDockerTests, PeerContainer } from 'runstack';

function createRedisPeer(): PeerContainer {
  const host = 'redis';
  const port = '6379';

  return {
    id: host,
    service: {
      image: 'redis:7-alpine',
      healthcheck: {
        test: ['CMD', 'redis-cli', 'ping'],
        interval: '3s',
        timeout: '2s',
        retries: 3
      }
    },
    injectEnvironment: {
      REDIS_HOST: host,
      REDIS_PORT: port
    }
  };
}

function createPostgresPeer(): PeerContainer {
  const host = 'postgres';
  const user = 'testuser';
  const password = 'testpass';
  const database = 'testdb';

  return {
    id: host,
    service: {
      image: 'postgres:16-alpine',
      environment: {
        POSTGRES_USER: user,
        POSTGRES_PASSWORD: password,
        POSTGRES_DB: database
      },
      healthcheck: {
        test: ['CMD-SHELL', `pg_isready -U ${user}`],
        interval: '5s',
        timeout: '5s',
        retries: 5
      }
    },
    injectEnvironment: {
      PGHOST: host,
      PGUSER: user,
      PGPASSWORD: password,
      PGDATABASE: database
    }
  };
}

describeDockerTests('Multi-Service Integration', (it) => {
  it('verifies Redis and PostgreSQL are both accessible', {
    baseImage: 'alpine',
    addlSteps: `
      RUN apk add --no-cache postgresql-client redis
    `,
    testScript: `
      # Test Redis using injected environment variables
      redis-cli -h $REDIS_HOST -p $REDIS_PORT PING | grep -q PONG
      redis-cli -h $REDIS_HOST -p $REDIS_PORT SET test:key "hello"
      redis-cli -h $REDIS_HOST -p $REDIS_PORT GET test:key | grep -q hello

      # Test PostgreSQL (psql uses PG* env vars automatically)
      psql -c "SELECT 1;" | grep -q "1"
      
      echo "Both services are working"
    `,
    peerContainers: [createRedisPeer(), createPostgresPeer()]
  }, async (result) => {
    if (!result.ok) throw new Error(result.error.message);
    if (!result.output.includes('Both services are working')) {
      throw new Error('Expected success message');
    }
  });
});

Skipping and Focusing Tests

Use .only and .skip just like you would with node:test:

describeDockerTests('Feature Tests', (it) => {
  it.only('runs only this test', { /* ... */ }, async (result) => {});
  it.skip('skips this test', { /* ... */ }, async (result) => {});
  it('runs normally', { /* ... */ }, async (result) => {});
});

Test Results

The assertion callback receives either an ExecResult (success) or ExecFailure (failure):

// Success
interface ExecResult {
  ok: true;
  output: string;              // stdout and stderr interleaved
  exitCode: number;
  signal: NodeJS.Signals | null;
  timedOut: false;
  maxBufferExceeded: false;
}

// Failure
interface ExecFailure {
  ok: false;
  output: string;              // captured output before failure
  exitCode: number | null;
  signal: NodeJS.Signals | null;
  timedOut: boolean;
  maxBufferExceeded: boolean;
  error: Error;
}

Debugging

Enable verbose output to see Docker Compose commands:

DEBUG=1 node --test your-test-file.test.ts

Clean up Docker images after tests complete:

DOCKER_IMAGE_CLEANUP=1 node --test your-test-file.test.ts

Tear down all containers after each individual test for full isolation (by default, containers are only torn down once at the end of the suite):

RUNSTACK_TEST_TEARDOWN=1 node --test your-test-file.test.ts

Markdown Test Loader

In addition to the TypeScript API, RunStack supports writing tests directly in Markdown files. This allows you to document your tests in a readable format while still executing them in Docker containers.

Why Markdown Tests?

  • Documentation as tests — Tests serve as living documentation
  • Readable format — Tests are easy to understand for non-developers
  • Version control friendly — Diff-friendly format for code reviews
  • IDE support — Full IntelliSense via test-types.d.ts

Writing Markdown Tests

Markdown test files use the .test.md or .spec.md extension.

File Structure

---
baseImage: 'debian:bookworm-slim'
---

# Suite Title

## Describe Block (H2)

### Test Case (H3)

Optional description text here.

```bash
echo "Test script to execute"
expect(result.ok).to.be.true;
expect(result.output).to.include('Test script');

#### Document-Level Frontmatter (YAML)

Frontmatter at the top of the file applies to all tests:

```markdown
---
baseImage: 'node:20-alpine'
addlSteps: 'RUN apk add --no-cache postgresql-client'
files:
  '/config/app.json': '{"env": "test"}'
peers:
  - id: 'postgres'
    service:
      image: 'postgres:16-alpine'
      healthcheck:
        test: ['CMD-SHELL', 'pg_isready -U testuser']
        interval: '5s'
        timeout: '5s'
        retries: 5
    injectEnvironment:
      PGHOST: 'postgres'
      PGUSER: 'testuser'
---

Test-Level Frontmatter (YAML)

Each test case can override settings from the document-level frontmatter:

---
addlSteps: 'RUN apt-get install -y redis-tools'
files:
  '/data/test.txt': 'test content'
---

### specific test case

```bash
cat /data/test.txt
```

```typescript
expect(result.output).to.include('test content');
```

Supported Frontmatter Fields

| Field | Type | Description | |-------|------|-------------| | baseImage | string | Docker image for the test container (required at document level) | | testScript | string | Inline test script (alternative to bash code block) | | addlSteps | string | Additional Dockerfile instructions | | files | object | Files to inject into the container | | peers | PeerContainer[] | Peer container definitions | | environment | object | Environment variables for the test container | | containerExtensions | array | Container extension configurations | | only | boolean | Run only this test (.only modifier) | | skip | boolean | Skip this test (.skip modifier) |

Code Blocks

Each test case contains two types of code blocks:

  1. Bash code block (```bash) — The test script to execute in the container
  2. TypeScript code block (```typescript) — Assertions using Chai's expect

Both blocks are optional (though at least one is required), but TypeScript assertions are recommended for proper validation.

Complete Markdown Test Example

---
baseImage: 'debian:bookworm-slim'
addlSteps: 'RUN apt-get update && apt-get install -y redis-tools'
peers:
  - id: 'redis'
    service:
      image: 'redis:7-alpine'
      healthcheck:
        test: ['CMD', 'redis-cli', 'ping']
        interval: '3s'
        timeout: '2s'
        retries: 5
    injectEnvironment:
      REDIS_HOST: 'redis'
      REDIS_PORT: '6379'
---

# Redis Integration Tests

## Connection Tests

### connects to Redis peer container

This test verifies basic connectivity to the Redis peer.

```bash
redis-cli -h $REDIS_HOST -p $REDIS_PORT PING
```

```typescript
expect(result.ok).to.be.true;
expect(result.exitCode).to.equal(0);
expect(result.output).to.include('PONG');
```

### sets and retrieves values

Tests basic key-value operations.

---
files:
  '/scripts/test.sh':
    content: |-
      #!/bin/bash
      redis-cli -h $REDIS_HOST SET mykey "hello"
      redis-cli -h $REDIS_HOST GET mykey
    chmod: '755'
---

```bash
/scripts/test.sh
```

```typescript
expect(result.ok).to.be.true;
expect(result.output).to.include('hello');
```

## Environment Tests

### injects environment variables

Verifies that environment variables from peer containers are available.

```bash
echo "Redis host: $REDIS_HOST"
echo "Redis port: $REDIS_PORT"
```

```typescript
expect(result.output).to.include('Redis host: redis');
expect(result.output).to.include('Redis port: 6379');
```

Running Markdown Tests

Node.js

Use the --import flag with the runstack/register module:

# Run all markdown tests
node --import runstack/register --test tests/*.test.md

# Run a specific markdown test file
node --import runstack/register --test tests/integration.test.md

# Run with pattern matching
node --import runstack/register --test --test-name-pattern="connection" tests/*.test.md

Bun

Running markdown tests with Bun requires two flags:

  • --preload registers the markdown loader plugin before the test runner starts
  • --loader .md:js tells Bun's test discovery to treat .md files as valid test files
# Run all markdown tests
bun test --preload ./node_modules/runstack/register-bun.mjs --loader .md:js tests/*.test.md

# Run a specific markdown test file
bun test --preload ./node_modules/runstack/register-bun.mjs --loader .md:js tests/integration.test.md

# Run with pattern matching
bun test --preload ./node_modules/runstack/register-bun.mjs --loader .md:js --test-name-pattern="connection" tests/*.test.md

Note: Both flags must come after bun test. Placing flags before test (e.g. bun --import ... test) causes Bun to interpret test as a package.json script name rather than the built-in test runner.

TypeScript IntelliSense

RunStack provides TypeScript type definitions via test-types.d.ts. Reference this file in your project for IntelliSense support:

tsconfig.json:

{
  "include": ["test-types.d.ts", "tests/**/*.ts", "tests/**/*.test.md"]
}

This provides:

  • Type definitions for expect (Chai)
  • Type definitions for result (ExecResult | ExecFailure)
  • Auto-completion and type checking in your TypeScript assertions

Markdown vs TypeScript API

| Feature | TypeScript API | Markdown | |---------|---------------|----------| | Flexibility | Full JavaScript/TypeScript | Declarative YAML + code blocks | | IDE Support | Excellent | Good (via test-types.d.ts) | | Documentation | Comments | Native Markdown | | Peer containers | Full PeerContainer objects | YAML configuration | | File injection | Full Files type | YAML configuration | | Nested suites | describe() blocks | H2/H3 headers |

Choose Markdown for documentation-heavy tests and TypeScript for complex test logic.

API Reference

describeDockerTests(description, callback)

Creates a test suite with automatic container lifecycle management. The callback receives it and describe functions that work like their node:test counterparts, but accept a TestScenario as the second argument.

describeDockerTests('My Tests', (it, describe) => {
  it('test name', scenario, assertions);
  
  describe('nested suite', () => {
    it('nested test', scenario, assertions);
  });
});

Low-Level Utilities

For advanced use cases, these functions are exported:

| Function | Description | |----------|-------------| | prepare(scenarios) | Build all test containers | | runTestScript(scenarios, testName) | Execute a specific test | | teardown(scenarios) | Clean up containers | | makeDockerComposeConfig(scenarios, testName?) | Generate docker-compose YAML | | getInlineDockerfile(...) | Generate Dockerfile content | | setGlobalDockerfileSteps(steps) | Configure Dockerfile steps for all test containers | | dedent(...), stripEmptyLines(...) | String utilities |

License

MIT