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

@nagatatz/rescript-vitest

v0.2.0

Published

Type-safe ReScript bindings for Vitest — describe/test/expect matchers, vi mocks, spies, snapshots, fake timers.

Readme

@nagatatz/rescript-vitest

Docs CI Sponsor

Type-safe ReScript bindings for Vitest.

The bindings are faithful: matchers are side-effecting and throw on failure, exactly like Vitest itself. The expect(value) wrapper carries the type of the value under test, so matchers such as toBe stay honest at compile time.

  • describe / test / it (+ .only / .skip / .todo / .each / .concurrent / .sequential / .shuffle / .skipIf / .runIf / .fails / .for)
  • ✅ Lifecycle hooks (beforeEach / afterEach / beforeAll / afterAll / aroundEach / aroundAll / onTestFailed / onTestFinished, sync & async)
  • ✅ The full expect matcher set (equality, numbers, strings, collections, objects, type/predicate, exceptions, snapshots — inline and file, mock call/return/resolve matchers)
  • ✅ Asymmetric matchers and negations (Expect.anything / arrayContaining / objectContaining / … and Expect.Not.*), plus assertion guards (assertions / hasAssertions / soft / poll)
  • ✅ Negation (not_) and async assertions (resolves / rejects)
  • Vi — mock functions, spies (incl. getter/setter), module mocking, global/env stubs, and fake timers (sync & async) with waitFor / waitUntil
  • VitestConfig — minimal vitest/config bindings (defineConfig / mergeConfig / defineProject + the common test fields)
  • ✅ Closed-set arguments are polymorphic variants, rejecting invalid values at compile time (toBeTypeOf(#string), spyOnAccessor(…, #get), coverage.provider: #v8)

Why another binding?

rescript-vitest (cometkim) and @greenfinity/rescript-vitest both target Vitest 2/3 and pin the dependency tree to Vite 5. Vitest 4.1+ requires vite/module-runner (Vite 6+), so those bindings block the upgrade. This package targets Vitest 4 (Vite 6/7) directly.

Requirements

| Tool | Version | |------|---------| | ReScript (rescript) | ^12.0.0-0 (12.x, prereleases allowed) | | @rescript/runtime | ^12.0.0-0 (same major as ReScript) | | Vitest | ^4.0.0 | | Vite | ^6 or ^7 (Vitest 4 peer) |

rescript, @rescript/runtime and vitest are peer dependencies — install them alongside the bindings.

Install

pnpm add -D @nagatatz/rescript-vitest rescript @rescript/runtime vitest vite

Add the package to your rescript.json dependencies so ReScript compiles the bindings:

{
  "dependencies": ["@nagatatz/rescript-vitest"]
}

Point Vitest at the compiled test files (*.res.js):

// vitest.config.js
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    include: ['__tests__/**/*_test.res.js'],
  },
})

Usage

open Vitest

describe("Math", () => {
  test("adds", () => {
    expect(1 + 1)->toBe(2)
  })

  test("negation", () => {
    expect([1, 2])->not_->toContain(3)
  })

  testAsync("works with promises", async () => {
    await expect(Promise.resolve(42))->resolves->Async.toBe(42)
  })
})

Mocks, spies and timers

open Vitest

test("counts calls", () => {
  let mock = Vi.fn1()
  let fn = mock->Vi.MockFn.asFn   // use the mock where a function is expected
  fn("a")->ignore
  mock->Vi.MockFn.asAssertion->toHaveBeenCalledOnce
})

test("fake timers", () => {
  Vi.useFakeTimers()
  // ... schedule a timer ...
  Vi.advanceTimersByTime(1000)
  Vi.useRealTimers()
})

API cheat sheet

Test structure (Vitest)

describe, describeAsync, describeOnly, describeSkip, describeEach (+ …2 / …3 for tuple cases), describeTodo, describeConcurrent, describeSequential, describeShuffle, describeSkipIf, describeRunIf, describeFor, test, testAsync, testOnly, testOnlyAsync, testSkip, testTodo, testConcurrent, testEach (+ …2 / …3), testFor, testSkipIf, testSkipIfAsync, testRunIf, testRunIfAsync, testFails, testFailsAsync, testSequential, testSequentialAsync, it, itAsync, itOnly, itSkip, itTodo, itConcurrent, itEach (+ …2 / …3), itFails, itSequential, itSkipIf, itRunIf.

Lifecycle

beforeAll, afterAll, beforeEach, afterEach, the suite/test wrappers aroundAll, aroundEach (each receives a runSuite / runTest thunk to await), plus the per-test hooks onTestFailed, onTestFinished (the before*/after*/onTest* hooks each have an …Async variant).

expect matchers

  • Equality: toBe, toEqual, toStrictEqual
  • Truthiness: toBeTruthy, toBeFalsy, toBeNull, toBeUndefined, toBeDefined, toBeNaN
  • Numbers: toBeGreaterThan, toBeGreaterThanOrEqual, toBeLessThan, toBeLessThanOrEqual, toBeCloseTo, toBeCloseToWithDigits
  • Strings: toMatch, toMatchRegExp, toContainString
  • Collections: toContain, toContainEqual, toHaveLength
  • Objects: toMatchObject, toHaveProperty, toHavePropertyValue
  • Type & predicate: toBeTypeOf, toBeInstanceOf, toBeOneOf, toSatisfy
  • Exceptions: toThrow, toThrowWithMessage, toThrowRegExp
  • Snapshots: toMatchSnapshot, toMatchSnapshotWithName, toMatchInlineSnapshot, toMatchFileSnapshot, toThrowErrorMatchingSnapshot, toThrowErrorMatchingInlineSnapshot
  • Mocks (calls): toHaveBeenCalled, toHaveBeenCalledOnce, toHaveBeenCalledTimes, toHaveBeenCalledWith (+ …With2), toHaveBeenLastCalledWith (+ …With2), toHaveBeenNthCalledWith (+ …With2), toHaveBeenCalledExactlyOnceWith (+ …With2)
  • Mocks (returns): toHaveReturned, toHaveReturnedTimes, toHaveReturnedWith, toHaveLastReturnedWith, toHaveNthReturnedWith
  • Mocks (resolves): toHaveResolved, toHaveResolvedTimes, toHaveResolvedWith, toHaveLastResolvedWith, toHaveNthResolvedWith
  • Asymmetric (Expect module): anything, any, arrayContaining, objectContaining, stringContaining, stringMatching (+ …RegExp), closeTo (+ …WithPrecision) — embed in the expected position of toEqual / toMatchObject / toHaveBeenCalledWith; negated forms live in Expect.Not
  • Guards & special (Expect module): assertions, hasAssertions, soft, poll, unreachable (+ …WithMessage)
  • Modifiers: not_, resolves / rejects (+ the Async matcher module)

Not yet bound

test.extend (fixtures) and expect.extend (custom matchers) are intentionally left unbound: their JavaScript shapes — a fixtures object injected via use callbacks, and matchers added dynamically onto every assertion — cannot be expressed faithfully or type-safely in ReScript without per-matcher manual bindings. Use ReScript helper functions instead of fixtures, and a dedicated @send external if you must call a project-specific custom matcher.

Vi

  • Create: fn, fnWith, fn0, fn1, fn2, spyOn, spyOnAccessor (#get / #set), spyOnGetter, spyOnSetter
  • MockFn: asFn, asAssertion, calls (one argument list per call), results ({type_, value} records), mockClear, mockReset, mockRestore, mockImplementation, mockImplementationOnce, mockReturnValue, mockReturnValueOnce, mockResolvedValue, mockResolvedValueOnce, mockRejectedValue, mockRejectedValueOnce, mockReturnThis, getMockName, mockName, getMockImplementation, withImplementation
  • Inspection / hoisting: mocked, isMockFunction, hoisted
  • Modules: mock, mockWithFactory, unmock, doMock, doUnmock, resetModules, importActual, importMock, mockObject, dynamicImportSettled
  • Global / env stubs: stubGlobal, stubEnv, unstubAllGlobals, unstubAllEnvs
  • Global state: clearAllMocks, resetAllMocks, restoreAllMocks
  • Timers: useFakeTimers (+ useFakeTimersWith for FakeTimerInstallOpts), useRealTimers, runAllTimers, runAllTicks, runOnlyPendingTimers, advanceTimersByTime, advanceTimersToNextTimer, setSystemTime (+ …Ms), clearAllTimers
  • Async timers: advanceTimersByTimeAsync, runAllTimersAsync, runOnlyPendingTimersAsync, advanceTimersToNextTimerAsync, advanceTimersToNextFrame
  • Timer inspection: isFakeTimers, getTimerCount, getMockedSystemTime, getRealSystemTime, setTimerTickMode (+ …WithInterval)
  • Waiting: waitFor, waitUntil (each with a …With variant taking {interval?, timeout?})

VitestConfig (vitest/config)

Minimal config-side bindings: defineConfig, defineConfigFn (function form receiving {mode, command}), mergeConfig, defineProject. Configs are typed with optional-record types covering the common test fields — globals, environment, include_, exclude, setupFiles, coverage, pool, testTimeout, hookTimeout, reporters, watch, projects — and coverage (provider, enabled, reporter, include_, exclude). (include is a ReScript keyword, so the field is named include_ and maps to JS "include".) coverage.provider is a polymorphic variant (#istanbul / #v8 / #custom); pool and environment stay string because Vitest treats them as extensible unions, so custom pools / environment packages remain expressible.

Scope (intentional): Vite-level config (plugins, resolve, server, build) and the long tail of test options are not bound — write those in a plain JS config file. Config objects are write-once data with a large, churning surface, so full coverage is not worth the maintenance cost.

ReScript does not emit export default, so wire the typed config into the real vitest.config through a thin JS shim:

// MyConfig.res
let config = VitestConfig.defineConfig({
  test: {globals: true, environment: "node", include_: ["__tests__/**/*_test.res.js"]},
})
// vitest.config.js
import { config } from "./MyConfig.res.js"
export default config

Development

pnpm install
pnpm build        # compile ReScript bindings + tests
pnpm test         # run the dogfood test suite under Vitest 4
pnpm format       # format hand-written JS/JSON with oxfmt
pnpm lint         # lint hand-written JS with oxlint

Non-ReScript hand-written files (vitest.config.js, JSON configs) are formatted with oxfmt and linted with oxlint; generated *.res.js output is excluded via .gitignore. A pre-commit hook (activated by the prepare script setting core.hooksPath) runs format:check + lint.

Binding correctness is verified by compilation (the bindings type-check) and the dogfood test suite (each binding is called against a real Vitest). Code coverage is not used as a gate: ReScript externals are erased at compile time and leave almost no instrumentable code, so statement/line coverage cannot detect an untested binding. pnpm test:coverage remains available for local inspection.

License

MIT