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

@structure-ai/bdd

v0.0.9

Published

Gherkin feature testing on bun test: Effect-native step definitions, per-scenario worlds with typed dispatch/query, schema-decoded data tables, and owned eventual consistency.

Readme

@structure-ai/bdd

Gherkin feature testing on bun test: business-readable scenarios (written by PMs, business specialists, developers — in any Gherkin dialect) compile into ordinary bun test cases. No second runner, no CLI, no generated code. Step definitions are typed Effect functions over a per-scenario world; the framework owns eventual consistency and failure capture so scenarios state business outcomes, not plumbing.

Usage

apps/my-api/
├── test/
│   ├── features/            # .feature files — the business writes these
│   │   └── booking/
│   │       └── quotation.feature
│   ├── steps/               # step definitions — developers write these
│   │   └── booking.steps.ts
│   ├── composition.ts       # buildTestWorld: in-memory composition
│   └── features.test.ts     # the suite — one file, three lines of wiring
// test/features.test.ts
import { defineFeatureSuite } from "@structure-ai/bdd";
import { buildTestWorld, runWorkers } from "./composition.ts";
import { bookingSteps } from "./steps/booking.steps.ts";

defineFeatureSuite({
  features: "test/features/**/*.feature",
  makeWorld: buildTestWorld,
  steps: bookingSteps,
  drain: (world) => world.use(runWorkers),
});

bun test now runs the scenarios; bun test --test-name-pattern "@booking" filters by tag. Scenarios tagged @wip are registered as todo (and exempt from wiring checks).

Feature files

Standard Gherkin — Feature, Background, Scenario, Scenario Outline + Examples, data tables, doc strings, tags — in any dialect (# language: fr gives Fonctionnalité, Soit, Plan du Scénario, …). Business specialists write and read these:

# language: fr
Fonctionnalité: Tarifs
  Contexte:
    Soit un tarif de 300 € par nuit pour la villa "savanne"

  Plan du Scénario: Total pour <nuits> nuits
    Quand le client demande une réservation de <nuits> nuits à partir du "2026-07-01"
    Alors le total est de "<total>"
    Exemples:
      | nuits | total      |
      | 7     | 2 100,00 € |

Step definitions

Typed Effect functions; parameters are inferred from the cucumber expression ({string}string, {int}/{float}number, {bigint}bigint, anything else → string); data tables decode through Effect Schema:

import { Effect, Schema } from "effect";
import { Given, Then, When } from "@structure-ai/bdd";

const customerRow = Schema.Struct({ email: Schema.String });

export const bookingSteps = [
  Given("registered customers:", ({ world, table }) =>
    world.use(Effect.gen(function* () {
      const rows = table !== undefined ? yield* table.rows(customerRow) : [];
      for (const row of rows) world.signIn(row.email, `user-${row.email}`);
    }))),

  When("the customer submits the QuotationRequest", ({ world }) =>
    world.use(submitCurrentQuotation(world))),

  Then('an exception {string} should be thrown with message {string}', ({ world, params }) => {
    const [tag, message] = params as readonly [string, string];
    world.expectFailure(tag, message);
  }),
];

The world

One fresh world per scenario, built inside a suite-owned Scope (in-memory stores, buses, doubles — mirror your serveTest composition) and torn down afterwards. Subclass ScenarioWorld<R> with typed scenario state; the base class provides the machinery:

| Member | What it does | | --- | --- | | dispatch(command, payload, { actor, idempotencyKey }) | Dispatches on the CommandBus and records the exit — business failures never throw, Then steps assert them. Requires CommandBus in R. | | query(queryDef, payload) | Same for queries (QueryBus in R). | | attempt(effect) | The exit-capturing twin for effects that do not travel the bus (auth-service calls, direct port access): runs any app effect, records its outcome — expectFailure/expectSuccess work uniformly. | | expectSuccess() / expectFailure(tag, message?) | Assert the last outcome; throw (fail the scenario) on mismatch. | | failureTags() | _tags of every recorded failure, in order. | | events() | Every stored event, in global order (EventStore in R). | | signIn(name, id) / actorNamed(name) / currentActor | The scenario's principal registry — dispatch steps run as the current actor. | | use(effect) | Provides the world's services to any app effect. |

Eventual consistency is the framework's problem: a drain hook (outbox relay, projection catch-up) runs after every step by default, so Then steps always observe converged state. Disable per suite (drainAfterStep: false) and drain manually from steps if a scenario needs finer control.

The auth kit

Registration/sign-in flows are the same in every @structure-ai/auth app, so the framework ships them ready-wired over in-memory doubles:

import { TestAuth, registerVerifiedCustomer } from "@structure-ai/bdd";

// one per world — the real auth service, in-memory store, recorded e-mails
const testAuth = TestAuth.make({ tenantId: "my-app", baseUrl: new URL("http://localhost:3000") });

// register → capture verification e-mail → verify → userId (dies on test bugs)
const userId = yield* registerVerifiedCustomer({ testAuth, email, password });

TestAuth exposes the real AuthService + AuthHandler with emails — every sent e-mail with its unwrapped one-time token — so Given ... is logged in steps become three lines instead of fifty. signInPassword({ testAuth, email, password }) records-style sign-in for fixture paths; raw service calls through world.attempt keep tagged errors (InvalidCredentials, …) assertable.

Text conventions

Feature tables carry business formatting; two helpers keep steps honest:

  • norm(s) — whitespace-normalized comparison (fr-FR money emits narrow no-break spaces).
  • ddMmYyyyToIso("05/03/2022")"2022-03-05" — the dd/MM/yyyy convention of business tables, validated.
  • table.rows(schema, { nullLiteral: "NULL" }) — table cells express absence for Schema.NullOr(...) fields.

Loud wiring

A feature file is a contract: every step must match exactly one definition. Suites with undefined or ambiguous steps fail at load time with a full report (file:line, step text, candidate expressions) — silent skips are impossible.

Exports

| Export | What it is | | --- | --- | | defineFeatureSuite(options) | Compiles .feature files into bun test cases. | | Given / When / Then | Register step definitions; params typed from the expression literal. | | ScenarioWorld<R> | Base world: typed dispatch/query with exit capture, actors, events, use. | | DataTable | hashes() raw rows; rows(Schema.Struct) typed decode. | | ExpressionParams<S> | The parameter tuple derived from an expression literal (type-level). | | TestAuth / registerVerifiedCustomer / signInPassword | The auth test kit: real service over in-memory store, recording sender, verified registration. | | norm / ddMmYyyyToIso | Feature-text conventions: locale-safe comparison, dd/MM/yyyy → ISO. |

Dependencies: @cucumber/gherkin (parser + pickles, dialects), @cucumber/cucumber-expressions (matching) — libraries, not a runner; @structure-ai/auth for the test kit. The fixture app under test/ is a full working example (event store, outbox-driven mails, projection-backed query, business failures, a French feature with an outline); its features are the package's own test suite.