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

@venn-lang/sdk

v0.6.0

Published

The plugin authoring API: small typed builders that return plain definition objects.

Readme

@venn-lang/sdk

The plugin authoring API: small typed builders that return plain definition objects.

The Venn kernel knows no protocols. Every verb a .vn file can call (http.get, crypto.hash, db.query) arrives from a plugin, and a plugin is just an object built with the functions here. One definition feeds the runtime, the language server and the node graph at once, so a verb describes itself in exactly one place.

The package depends on @venn-lang/contracts (types only), @venn-lang/types and zod. It never imports node:*: a plugin has to load in a Web Worker like everything else, so its I/O goes through a port.

Install

Nothing is published to npm yet. Inside this workspace:

{ "dependencies": { "@venn-lang/sdk": "workspace:*", "@venn-lang/types": "workspace:*" } }

Usage

A complete plugin: one namespace, one verb, one matcher.

// src/plugin.ts
import { HttpClientPort } from "@venn-lang/http";
import { arg, defineAction, defineMatcher, definePlugin, type PluginDefinition, z } from "@venn-lang/sdk";
import { t } from "@venn-lang/types";

interface Probe {
  url: string;
  up: boolean;
  ms: number;
}

const probeParams = z.object({
  status: z.number().default(200).describe("Which status counts as up."),
});

export const uptimePlugin: PluginDefinition = definePlugin({
  name: "@acme/uptime",
  version: "0.1.0",
  namespace: "uptime",
  requires: ["net"],
  typeDefs: { Probe: t.record({ url: t.string, up: t.bool, ms: t.number }) },
  actions: [
    defineAction({
      name: "probe",
      doc: "Ask a URL whether it is alive.",
      args: [arg("url", t.string, "Where to knock.")],
      params: probeParams,
      result: t.ref("uptime.Probe"),
      run: async (ctx, input): Promise<Probe> => {
        const url = String(input.args[0]);
        const client = ctx.port(HttpClientPort);
        const res = await client.request({ method: "GET", url, signal: ctx.signal });
        return { url, up: res.status === input.params.status, ms: res.time };
      },
    }),
  ],
  matchers: [
    defineMatcher({
      name: "up",
      appliesTo: "Probe",
      test: ({ subject }) => (subject as Probe).up,
      message: ({ subject }) => `expected ${(subject as Probe).url} to answer`,
      detail: ({ subject }) => ({ expected: true, actual: (subject as Probe).up }),
    }),
  ],
});

export default uptimePlugin;

The namespace, the verb and the matcher are then ordinary Venn:

import { uptime } from "@acme/uptime"

flow "the API answers" {
  step "probe the health endpoint" {
    const probe = uptime.probe "https://api.example.com/health" { status: 204 }
    expect probe up
  }
}

venn verify-plugin <path> loads a built module (its default export, or the first export that looks like a plugin) and prints the namespace with the counts of actions, matchers and resources.

API

| Export | What it does | | --- | --- | | definePlugin(def) | Returns the PluginDefinition unchanged. This is the shape the registry ingests. | | defineAction(def) | One verb. Derives signature from args and result when you do not write one. | | defineMatcher(def) | One word usable after expect, with its own failure message and diff. | | defineDecorator(def) | One @name, applied to the program's tree before anything reads it. | | defineResource(def) | A handle with a runner-managed lifecycle. | | Duration | A ZodType<number>: reads "30s", "2m", 1500, yields milliseconds. | | arg, optionalArg, restArg | Build an ArgSpec, one named positional argument. | | signatureOf(args, result) | The FnSpec those arguments describe. defineAction calls it for you. | | paramSpecs(schema), paramNames(schema) | Read a Zod options schema into ParamSpec[] (or just the key names). | | z, ZodType | Zod 4, re-exported so a plugin depends on @venn-lang/sdk alone. |

Types are exported alongside: PluginDefinition, ActionDefinition, MatcherDefinition, MatcherDetail, ResourceDefinition, ResourceScope, DecoratorDefinition, DecoratedNode, ExpandContext, ActionContext, ActionInput, MatcherArgs, MatcherContext, ArgSpec, ParamSpec.

The plugin object

| Field | Meaning | | --- | --- | | name, version | The package identity. name is what a file writes in import { … } from "…". | | namespace | The prefix every verb gets: namespace uptime plus action probe is uptime.probe. | | requires | Host capabilities: fs, process, net, clock, random, secrets, log, io. | | actions, matchers, decorators, resources | The contributions. All optional. | | types | Zod schemas for the nominal data shapes the plugin publishes. | | typeDefs | The same shapes as TypeSpec data, by short name. Probe is reachable from a flow as uptime.Probe. |

Capabilities are negotiated when the registry is built, before a single line runs. A plugin that requires net on a host that does not offer it fails with VN2010 naming the plugin and the missing capability, never with a TypeError halfway through a test.

A plugin with no verbs at all is legitimate. @venn-lang/env contributes only its namespace, so that a file reading configuration still has to declare import { env } from "venn/env".

Actions

defineAction splits what a call site carries in two.

  • args are the positional arguments, named and in order. The name is the point: a type alone tells the editor that a verb wants two values, never which. restArg says this argument and every one after it, optionalArg says the call still means something without it.
  • params is a Zod schema for the trailing options map, { status: 204 }. Describe a key with .describe() and the editor shows that text; there is no second list to drift.

result is what the call evaluates to. Between them, args and result produce the signature the checker reads, via signatureOf. Pass signature yourself for a shape args cannot describe, and it wins. Write neither and the call stays dynamic: a plugin that says nothing about types is still a working plugin.

run(ctx, input) receives input.args (the evaluated positional values) and input.params (the options, already parsed by the schema). Validation happens before run is entered, and it fails in the user's words:

crypto.hash "abc" { algorithmm: "sha512" }   # VN3001, did you mean "algorithm"
crypto.hash "abc" { algorithm: "sha5" }      # VN3010, "algorithm" must be one of the four it knows

A schema that declares no keys (z.record(...)) or welcomes unnamed ones (z.looseObject) accepts a free-form map, and nothing in it is unknown.

ActionContext is the whole of what run may reach for:

| Member | Meaning | | --- | --- | | port(port) | The implementation bound to a port. This is how a plugin does I/O. | | secrets | The host's SecretProvider: get(name), has(name). | | config | The document's evaluated config { … } block, for example baseUrl. | | signal | Aborted when a race this action runs inside has already been won. | | log(message) | One line into the host log. | | invoke(fn, args) | Call a function the flow passed in. The only way to run a language closure. | | redact(value) | Marks a string as secret. Present on the interface; the current runner binds it to a no-op. |

invoke is what makes a handler argument work. http.on takes a fn and calls it per request:

server.onRequest((request) => ctx.invoke(handler, [request]));

Matchers

A matcher is registered by bare name, not under the namespace, so expect probe up needs no prefix. test returns the verdict, message gives the one-line failure, and detail gives the two sides so the failure carries a structured diff instead of prose. Set aligned: false when the two sides were never compared field by field, as a membership check holds one needle against every item. appliesTo is documentation: the editor shows it on hover.

export const contains: MatcherDefinition = defineMatcher({
  name: "contains",
  args: [arg("value", t.dynamic, "What to look for: a substring, or an item of the list.")],
  test: ({ subject, args }) => includes(subject, args[0]),
  message: ({ subject, args }) => failureLine({ subject, relation: "to contain", other: args[0] }),
  detail: ({ subject, args }) => ({ expected: args[0], actual: subject, aligned: false }),
});

Matcher options are validated exactly like an action's, with the same codes and the same words.

Decorators

defineDecorator contributes a @name that runs over the program once, before anything else reads it. expand(ctx) gets the node it was written on and may leave a fact behind with ctx.meta, put a different node in its place with ctx.replace, take it out with ctx.remove, or refuse the program with ctx.reject({ code, title }). targets lists the node types it accepts; writing it elsewhere is refused with VN2014, and an unknown @name is VN2013.

const drop = defineDecorator({ name: "drop", expand: (ctx) => ctx.remove() });

The built-ins (@skip, @only, @serial, @tags, @timeout, @retry, @lock, @flaky) have this exact shape, and a plugin decorator of the same name replaces one. A project is entitled to its own @retry.

Resources

defineResource declares a handle the runner opens and closes for you: name, a scope of "suite", "worker", "flow" or "step", an open(ctx) and an optional close(instance).

export const browserResource: ResourceDefinition = defineResource({
  name: "Browser",
  scope: "worker",
  open: () => ({ id: "browser-1", engine: "chromium" }),
  close: () => undefined,
});

The grammar has no resource declaration yet, so nothing in a .vn file opens one today. The definition shape is settled and @venn-lang/browser already declares against it.

Durations

Duration is a Zod schema, so it composes into an options schema like any other:

const rampParams = z.object({ over: Duration.optional(), hold: Duration.optional() });
load.ramp 0 200 { over: "30s", hold: "5m" }

It accepts a unit string (ms, s, m, h) or a plain millisecond count, and always yields a number of milliseconds. It reads the string "30s", not the language's own 30s duration value.

Reaching I/O

A plugin never calls fetch, fs or a driver directly. It asks the context for a port and calls the interface:

run: (ctx, input) => ctx.port(HttpClientPort).request({ method: "GET", url: input.args[0] }),

The port descriptor (id, version, requires, methods) lives in @venn-lang/contracts, and the implementation is bound at startup. That is what lets the same plugin run against a real client in production and a fake one in tests. Every port ships with both, plus a conformance suite they both pass.

Running a plugin

@venn-lang/runtime takes the definitions and a host, and nothing else:

import { createTestHost } from "@venn-lang/contracts";
import { parse } from "@venn-lang/core";
import { createFakeClient, HttpClientPort } from "@venn-lang/http";
import { createMemorySink, createRunner } from "@venn-lang/runtime";
import { uptimePlugin } from "./plugin.js";

const runner = createRunner({
  host: createTestHost(),
  plugins: [uptimePlugin],
  ports: [{ port: HttpClientPort, impl: createFakeClient() }],
  sink: createMemorySink(),
});

const result = await runner.run(parse(source).ast);

See also