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

@ripplo/testing

v0.15.2

Published

Typed test DSL for Ripplo — declare state and user flows, compile to a lockfile

Readme

@ripplo/testing

Ripplo’s typed workflow DSL.

An app defines one Zod state schema. Each top-level key is a source, and each source is read as one validated JSON value. Workflows use handles derived from that schema, so state paths and record fields do not use string keys.

npx ripplo init installs @ripplo/testing and its exact Zod 4 version. For manual setup:

pnpm add -D --save-exact @ripplo/testing [email protected]
import {
  createRipplo,
  defineState,
  setup,
  source,
} from "@ripplo/testing";
import { z } from "zod/v4";

export const state = defineState(
  z.object({
    core: source.http(
      z.object({
        organizations: setup.record(
          z.object({
            id: setup.generated(z.string()),
            name: setup.value(z.string()),
          }),
        ),
        users: setup.record(
          z.object({
            email: setup.value(z.email()),
            token: setup.generated(z.string()),
          }),
        ),
      ),
    ),
    frontend: source.browser(
      z.object({
        editor: z.object({
          dirty: setup.value(z.boolean()),
        }),
      }),
    ),
  }),
);

export const ripplo = createRipplo({
  facts: undefined,
  state,
  viewports: ["desktop", "mobile"],
  workflows,
});

Apps that must keep Zod 3 install Zod 4 under an alias:

pnpm add -D --save-exact @ripplo/testing zod4@npm:[email protected]

Their state schema imports z from zod4 instead of zod/v4. Do not pass Zod 3 schemas to Ripplo.

setup.value(schema) marks a value that workflows may provide. setup.generated(schema) marks a value returned by setup, such as an ID. setup.record(rowSchema) marks a keyed record collection that can be synthesized.

Workflows

One workflow models one critical user intent through its natural multi-step path.

  • given describes the widest starting-state constraints valid for the whole journey.
  • arbitrary(field) supplies schema-valid workflow inputs. Reuse each binding in later actions and effects.
  • exact(value) is only for concrete values that drive behavior.
  • optional() plus named when branches cover state-dependent outcomes when intent and path stay the same.
  • Every mutation declares its visible outcome and complete typed state effects, including cascades.
  • Every effect must provably change the state known at that step.

Every source implements one aggregate read():

import { createStateSourceEngine } from "@ripplo/testing/engine";
import { coreStateSchema, state } from "../../../.ripplo/state.js";

export const coreStateEngine = createStateSourceEngine(state.core, {
  setup: {
    records: {
      organizations: ({ input, runId }) => insertOrganization({ input, runId }),
      users: ({ input, runId }) => insertUser({ input, runId }),
    },
  },
  teardown: ({ runId }) => clearRunData(runId),
  read: ({ runId }) => readCoreState(runId).then((value) => coreStateSchema.parse(value)),
});

Browser-owned sources use the same fields and records setup contract. Connect before rendering so state setup finishes before Ripplo captures the app:

import { connect } from "@ripplo/testing/browser";
import { createStateSourceEngine } from "@ripplo/testing/engine";

const frontendStateEngine = createStateSourceEngine(state.frontend, frontendStateImpl);
const enabled = import.meta.env.VITE_ENABLE_RIPPLO_TESTING === "true";
const connection = enabled ? await connect(frontendStateEngine) : null;

if (connection != null) {
  const stopReadySignal = router.subscribe("onResolved", () => {
    connection.ready();
    stopReadySignal();
  });
}

renderApp();

connect(engine) mounts and gates browser state setup. connection.ready() separately marks the page interactive. Ripplo sets up HTTP state and signs in before it invokes the browser engine. Browser setup may depend on HTTP-created records and may read signed-in application state. Persist its result in storage that survives navigation. Capture starts from a blank page, and the first workflow goto performs a hard navigation. Use connect() without an engine when the browser owns no modeled state.

Server adapters

Mount each HTTP-backed state source and the optional authentication engine behind signed endpoints. For Express:

import { createAuthenticationHandler, createStateSourceHandler } from "@ripplo/testing/express";

const enabled = process.env.ENABLE_RIPPLO_TESTING === "true";

app.use("/ripplo", createStateSourceHandler({ enabled, engine: coreStateEngine }));
app.use("/ripplo", createAuthenticationHandler({ enabled, engines: [userAuthenticationEngine] }));

The same createStateSourceHandler() and createAuthenticationHandler() functions are available from the hono, nextjs, nestjs, koa, and elysia entry points. Fastify exports registerStateSourceHandler() and registerAuthenticationHandler(). Vite exports ripploStateSourcePlugin() and ripploAuthenticationPlugin().

See DSL.md for the complete state and workflow surface.