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/core

v0.6.0

Published

The Venn compiler: grammar, parser, type checker, expression compiler, decorator expansion, and the Problem model.

Downloads

1,387

Readme

@venn-lang/core

The Venn compiler: grammar, parser, type checker, expression compiler, decorator expansion, and the Problem model.

Everything that turns .vn text into something another package can run, check or draw lives here. The kernel is deliberately small: it knows flows, steps, expressions, types and decorators, and it knows nothing about HTTP, browsers or files. Protocols arrive as plugins built on @venn-lang/sdk; the core only ever sees the tree they decorate.

@venn-lang/core never imports node:*. It is built with platform: "neutral" so the same bundle runs in Node, in the CLI, and inside the Web Worker that hosts the language server. Its runtime dependencies are Langium (parsing) and @venn-lang/types (the published type vocabulary).

Usage

import { checkTypes, parse, toGraph } from "@venn-lang/core";

const uri = "file:///hello.vn";
const source = `flow "Hello" {
  step "Ping" {
    http.get "https://example.com/health"
    expect res.status == 200
  }
}`;

const { ast, problems } = parse(source, { uri });
const checked = checkTypes(ast, { uri });

for (const problem of [...problems, ...checked.problems]) {
  console.log(`${problem.code} ${problem.title} (${problem.span.line}:${problem.span.column})`);
}

const graph = toGraph(ast); // flow -> step -> action/expect, for the node editor

parse is synchronous and touches no filesystem. Error recovery keeps a partial AST, so a file with a syntax error still produces a tree the editor can work with.

Module map

| Module | Responsibility | | --- | --- | | grammar/ | venn.langium, the whole grammar of the language. The single source the parser is generated from. | | generated/ | Langium output: the AST interfaces, the isX type guards, the grammar object and the services module. Regenerated by pnpm --filter @venn-lang/core langium:generate. | | lang/ | The Langium services parse uses, plus VennLexer, which makes newlines significant between statements and suppresses them inside ( ) and [ ]. | | parse/ | parse and parseExpression. Turns text into a Document plus VN1xxx problems. | | ast/ | Small readers over the generated tree: walkAst, dottedPath, callArgs, splitCall, isRunnable. | | problem/ | The Problem shape shared by compile diagnostics and runtime failures, with Span, Diff, Severity, ProblemError and buildDiff. | | codes/ | CODES, the catalog of every VNxxxx the kernel can raise, and buildProblem. | | expand/ | The expansion phase. Runs every @decorator over the tree once, before anything else reads it. | | typecheck/ | Hindley-Milner inference with generics, the Type algebra, the plugin TypeCatalog, cross-file importedTypes, and the prose the editor shows on hover. | | compile/ | Compiles each Expr into a Thunk, a closure over the environment. Memoised per node, so the tree is walked once and not once per evaluation. | | expr/ | The evaluator surface: evaluate, closures and frames, invoke, memberValue, the built-in method tables, and the prelude values. | | value/ | What a runtime value is, and the three questions asked of one: truthy, strictEquals, isNumeric. | | units/ | Unit-typed values (300ms, 2mb, 50%, ISO instants) and the arithmetic that keeps units honest. | | interpolation/ | ${…} placeholders, described once for both the evaluator and the editor. | | format/ | formatText, shared by venn fmt and the language server so both agree on the result. | | graph/ | toGraph: a pure AST transform into nodes and edges for the visual editor. | | events/ | The event envelope the runner emits and the UI consumes. Types only, no runtime. | | module/ | Which of the three kinds an import specifier is: relative, alias or package. |

API

Parsing

| Export | What it gives you | | --- | --- | | parse(text, { uri? }) | ParseOutput: the Document AST and the VN1xxx problems. | | parseExpression(source) | An Expr for a standalone expression, or undefined when it is not one. | | EXPRESSION_OFFSET | How far parseExpression shifts CST offsets, so a caller can map back onto the ${…} it came from. | | vennServices() / createVennServices() | The Langium services, cached and fresh respectively. | | VennLexer | The lexer that suppresses newlines inside brackets. | | VennGeneratedModule, VennGeneratedSharedModule, VennLanguageMetaData | The generated Langium modules, for a host that builds its own services. |

Everything from generated/ast.ts is re-exported: Document, FlowDecl, StepDecl, ActionCall, ExpectStmt, FnDecl, DecoDecl, Expr and the rest, each with its isX guard.

The AST readers exist because the grammar spells some things two ways:

| Export | What it answers | | --- | --- | | callArgs(call) | The arguments of a call as one list, whether it was written http.get "url" or conn.close(). | | splitCall(args, takes) | Which trailing { … } is an options map and which is a real argument. | | dottedPath(expr) | The dotted name an expression spells (http.get), or undefined when it is not a plain chain. | | isRunnable(decl) | Whether a top-level node runs or merely defines. | | walkAst(root) | Every descendant, depth first. |

Problems and codes

Every failure, at compile time or at run time, is a Problem: a stable code, a severity, a one-line title in the user's vocabulary, a span, and optional help, related, diff, note and docs. One shape means one renderer serves the terminal, the editor and the UI.

import { buildProblem, CODES, ProblemError } from "@venn-lang/core";

throw new ProblemError(
  buildProblem({
    spec: CODES.VN6001_ASSERTION_FAILED,
    span: { uri, offset: 0, length: 0, line: 1, column: 1 },
    title: "The response did not arrive before the deadline",
  }),
);

| Family | Covers | | --- | --- | | VN1xxx | Lexical and syntactic. | | VN2xxx | Name resolution, imports, decorators, capability negotiation. | | VN3xxx | Types and units. | | VN4xxx | Concurrency and isolation. | | VN5xxx | Lint. | | VN6xxx | Assertions, at run time. | | VN7xxx | Actions and protocols, at run time. | | VN8xxx | Timeouts and resource limits. |

CODES holds the codes the kernel itself raises, from VN1001_LEX to VN8002_LOOP_LIMIT. Plugins add their own in the same families. buildDiff turns two compared values into a structured Diff, walking them field by field when they line up, so a failure names the field that moved instead of printing two renderings side by side. formatValue renders a single value for a report.

Type checking

import { checkTypes, showType } from "@venn-lang/core";

const { problems, types, slots } = checkTypes(ast, { uri, catalog, decos, imports });

checkTypes runs Hindley-Milner inference over the whole document. Functions are generalised, so one fn id(x) => x serves every type; dynamic unifies with everything and never errors, so a plugin result or a parsed HTTP response places no annotation burden on the pure parts of a file. It returns the problems, every expression's inferred type keyed by node (for hover), and the expressions parsed out of each string literal's ${…} slots.

CheckTypesOptions is how the checker learns about the world outside the file: catalog (what the loaded plugins publish), decos (the pub decos the imports reach) and imports (what each imported name turned out to be).

| Export | Purpose | | --- | --- | | Type, FnType, RecordType, UnionType, OpaqueType, LiteralType | The type algebra. | | DYNAMIC, literal, opaque, union | Constructors for the ones a caller builds by hand. | | prune | Follow a type variable to what it has been solved to. | | showType, showTypes | Render types for a hover or a diagnostic. showTypes names variables across a group, so two unrelated parameters are not both called a. | | TypeCatalog | The two questions the core asks about plugins: typeOf(name) and signatureOf(target). | | specToType, ResolveRef | Read a published TypeSpec from @venn-lang/types into the checker's own type. | | importedTypes, ImportedTypes | The types of the names a document imports, worked out from the files it names. Cycles answer with nothing rather than looping. | | memberType, resolveMember | The type of xs.map, s.length, p.name. | | createContext | Fresh per-check inference state, so two checks never share variable ids. | | BUILTIN_TYPES, isBuiltinType | string, number, bool, duration, size, percent, instant and friends, each with a line of documentation and an example. | | PRELUDE_SPECS, isPrelude, PreludeSpec, PreludeArg | The prelude, described once: types for the checker, prose for the editor. | | MEMBER_DOCS, MemberDoc, memberKind | What each built-in member does, for completion and hover. | | KIND_SPECS, KIND_TYPES | What a decorator target handle offers, published as types. |

Compiling and evaluating expressions

An expression is compiled once into a Thunk, a function of the environment. Everything the source settles (which operator, which literal, which slot a name lives in) is decided at compile time.

import { evaluate, type EvalEnv } from "@venn-lang/core";

const env: EvalEnv = { lookup: (name) => (name === "res" ? { status: 200 } : undefined) };
evaluate(subject, env); // true, for `res.status == 200`

| Export | Purpose | | --- | --- | | compileExpr(expr) | The compiled Thunk. Memoised per node. | | closureOfDecl(decl, env) | A top-level fn as a callable value bound into an environment. | | evaluate(expr, env) | Compile if needed, then run. The seam every caller uses. | | EvalEnv | One method, lookup(name). Actions and matchers are not here: they belong to the runtime registry. | | childEnv(parent, bindings) | A nested scope. | | Cell, CellEnv, hasCells | Bindings addressed by cell, which is what lets a recursive fn capture itself. | | invoke, invoke1, callClosure, isCallable | Calling a Venn callable, with fixed arities for the hot paths. | | Closure, isClosure, NativeFn, nativeFn, isNativeFn | The two kinds of callable. | | memberValue(receiver, member) | What .x means on any value: a map's own data first, then the built-in member tables. | | namespaceValue, isNamespaceValue | A plugin namespace as a value, so fmt.table(rows) works inside any expression. | | PRELUDE_VALUES | range, str, typeOf, pretty, spawn. | | display(value), typeName(value) | How print renders a value, and what the language calls its type. |

Value (from value/) is the shape a runtime value can take, with truthy, strictEquals and isNumeric alongside it. There is no coercion: "99.00" never equals 99.

Decorator expansion

expand is the phase between parsing and everything else. It is the only place a @name means anything. The kernel does not know what @retry is; it knows that a name written with an @ is looked up in a DecoratorSource, handed the node it sits on, and allowed to rewrite it. Decorators written in TypeScript by a plugin and decorators written in Venn with deco go through the same door.

import { expand } from "@venn-lang/core";

const { problems } = expand({ document: ast, decorators, uri, imported });

Applied innermost first, so a decorator that rewrites a body finds a body its own decorators have already finished with. An ExpandContext gives a decorator the node, its evaluated args, the same arguments as syntax in written, the parent, and four verbs: replace, remove, meta and reject.

| Export | Purpose | | --- | --- | | expand, ExpandResult, ExpandContext, DecoratorDefinition, DecoratorSource, DecoratedNode, NodeMeta | The mechanism and its contracts. | | TargetKind, TARGET_KINDS, isTargetKind, kindOf | The words a deco uses about what it decorates: Fn, Flow, Step, Binding, Type, Node. kindOf is the one place that vocabulary meets the compiler's $type names. | | makeHandle, handleSurface, TargetHandle, HandleSurface | The value a deco body holds for its target, and what it publishes. | | readSignature, DecoSignature, SignatureResult, acceptedKinds, decoTarget | Reading what a deco decorates off the type of its first parameter. | | decoDecorator, withDocumentDecos, ImportedDeco, DocumentDecoArgs, decoCannotCall, DecoBodyArgs | Turning a deco declaration, local or imported, into a definition expansion can run. | | metaOf, readMeta, writeMeta | Facts a decorator leaves on a node for the runtime. Non-enumerable, so they never land in a serialised AST. | | readDecorations, addDecoration, AROUND_KEYS, Decorations, decorateCallable | Where .wrap, .before and .after leave their closures, and how a callable picks them up. | | swapNode, spanOf | Replacing a node in its container, and locating one. | | wrongKind, wrongKindTitle, wrongTargetTitle, kindWords, nodeWord, everyKindWritten | The prose behind VN2014, phrased in the author's words rather than in node type names. |

Units

A number literal may carry a unit, and the unit survives arithmetic.

expect res.duration < 300ms
expect res.size <= 2mb
const rate = 99.9%

parseNumber turns a NUMBER lexeme into a plain number or a UnitValue; parseInstant turns an ISO-8601 lexeme into an Instant that keeps its source text. combine({ op, left, right }) does the arithmetic and comparison: 300ms + 1s succeeds, 300ms + 2mb returns the mismatch behind VN3012. isUnitValue and isInstant are the guards. Canonical bases are milliseconds, bytes, a ratio from 0 to 1, and epoch milliseconds.

Interpolation

scanInterpolations(text) locates every ${…} precisely enough to highlight, hover and jump to. compileTemplate(text) splits a literal once into its constant chunks and its holes, each hole carrying both its source and its parsed Expr. There is always exactly one more chunk than holes, so rejoining them cannot drop text. Both are cached by text, because a .vn file holds a fixed set of string literals.

Formatting

import { DEFAULT_FORMAT, formatOptionsFrom, formatText } from "@venn-lang/core";

formatText(source, { indentWidth: 4 });
formatText(source, formatOptionsFrom(manifest.format)); // the [format] table of venn.toml

formatText re-indents by bracket depth and, by default, moves every use above every import. It never joins or splits lines, and it is idempotent. organizeHeader and reindent are the two steps on their own.

Graph and events

toGraph(document) derives the node graph from the AST: a flow contains its steps, a step contains its actions and expectations, and sequential edges run between siblings. Pure, with no runtime state.

The events/ module is types only: Envelope is the single contract between runner, host and UI, carrying a monotonic seq, a timestamp, a RunId, a kind and its payload. EventKind is derived from the keys of EventData, so adding an event is one edit. RunPlan is what the UI draws in grey before anything executes.

Modules

specifierKind(spec) classifies an import specifier the way Node does: ./util.vn is relative, #shared/auth.vn is an alias resolved through [paths], and a bare name is an installed package. isPackageSpecifier is the common question asked of it.

Grammar

The grammar lives in src/grammar/venn.langium and is the fixed part of the language. It knows flow, step, group, fragment, fn, deco, type, config, matrix, control flow, lifecycle hooks, expect, and one rule that absorbs every protocol present and future:

ActionCall:
    target=QualifiedName (called?='(' (call=ArgList)? ')')?
    (args+=ActionArg)* (opts=MapLit)?;

That is why http.get "url" { headers } needs no grammar change to exist. Whether a dotted target is a plugin verb or a method on something in scope is decided by what the name resolves to, not by how the call is written.

Statements are terminated by a newline or ;. The lexer drops those inside ( ) and [ ], so an argument list or a list literal may still span lines, while blocks and maps keep their newlines.

Regenerate the parser after editing the grammar:

pnpm --filter @venn-lang/core langium:generate
pnpm --filter @venn-lang/core test

See also