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

journey-dsl

v0.8.0

Published

A TypeScript toolkit for defining, compiling, verifying, and running interactive user journeys as state machines.

Readme

journey-dsl

A TypeScript toolkit for defining, compiling, verifying, and running interactive user journeys as state machines.

Author flows in a readable .journey DSL, compile them to typed TypeScript definitions, verify graph correctness and component wiring, and run them with a built-in runtime engine.

Install

npm install journey-dsl

Quick Start

# Scaffold a new project
npx journey init

# Create a journey
npx journey new daily-checkin --personas user,admin

# Edit journeys/dsl/authored/daily-checkin.journey, then:
npx journey compile

# Verify everything
npx journey check

New to journey-dsl? The Build an App guide walks you through building a complete onboarding flow from scratch — from writing the .journey file through component wiring, manifests, verification, and runtime setup. It covers the end-to-end concepts that this README's API reference assumes you already know.

The .journey DSL

Journeys are authored as indentation-based .journey files that describe state machines with nodes (screens, decisions, exits) and edges (transitions between them).

journey daily-checkin
  description "Daily mood check-in with optional journaling"
  persona survivor guardian

  effect save_checkin
    api POST /api/checkins
    req { mood: string!, journalText: string? }
    res { checkinId: string!, streakDays: number! }
    err 400 invalid_mood

  entry mood-screen "How Are You Feeling?"
    story checkin--mood-screen
    produces { mood: string! }

  decision journal-check "Should Journal?"

  view journal-screen "Write a Journal Entry"
    story checkin--journal-screen
    requires { mood: string! }
    produces { journalText: string! }

  exit success "Complete"
  abandon skip "Skip for Now"

  mood-screen -> skip                close
  mood-screen -> journal-check       submit
  journal-check -> journal-screen    @system route    guard "mood !== 'great'" "Bad days"
  journal-check -> success           @system route
  journal-screen -> success          save_checkin

Node Types

| Type | Screen? | Purpose | | ------------ | ------- | --------------------------------------------- | | entry | Yes | Journey start point (exactly one per journey) | | view | Yes | Interactive screen | | decision | No | Routing logic via guards | | action | No | Non-interactive processing | | exit | No | Successful completion (absorbing) | | abandon | No | User cancellation (absorbing) | | error | No | Error state (absorbing) | | subjourney | No | Reference to another journey |

Node Properties

entry welcome "Welcome Screen"
  story my-app--welcome          # Storybook story ID (entry/view only)
  requires { userId: string! }   # Data this node needs
  produces { userName: string! } # Data this node emits

Field types: string, number, boolean, object, array. Append ! for required (default), ? for optional.

Effects

Declare API contracts that edges can reference:

effect create_user
  api POST /api/users
  req { name: string!, email: string! }
  res { userId: string! }
  err 400 validation_error
  err 409 duplicate_email

Edges

source -> target    trigger [+ effect]...    [guard "condition" "description"]

Trigger types:

  • User action (button click): next, submit, close
  • System event (async): @system route, @system data_loaded

Guards add conditions to edges:

decision-node -> path-a    @system route    guard "role === 'admin'" "Admin only"
decision-node -> path-b    @system route

Effects are chained with +:

form -> result    submit + save_checkin + send_notification

Configuration

Create journey.config.ts at your project root:

import { defineConfig } from "journey-dsl";

export default defineConfig({
  journeyDirs: ["journeys/dsl/authored"],
  definitionsDir: "journeys/definitions",
  indexFile: "journeys/definitions/index.ts",
  manifestsFile: "stories/manifests.ts",
  stories: {
    graph: "stories/organisms/JourneyGraph.stories.js",
    walkthrough: "stories/organisms/JourneyWalkthrough.stories.js",
    visualizer: "stories/visualizer/JourneyVisualizer.stories.js",
  },
  specs: {
    openapi: ["specs/api.yaml"],
    graphql: ["specs/schema.graphql"],
  },
  rootJourney: "app-lifecycle",
});

All paths are relative to the config file. Every field has sensible defaults matching the layout above.

CLI

| Command | Description | | --------------------------------------- | ------------------------------------------------------------------ | | journey init | Scaffold folder structure, config, and empty barrel/manifest files | | journey new <name> [--personas p1,p2] | Create .journey template and update barrel + manifest files | | journey compile [--force] [--dry-run] | Compile .journey files to .generated.ts, update barrel + stories | | journey validate [--all] [files...] | Static AST and cross-file validation without writing files | | journey verify [--no-spec] [--strict] [--quiet] [--format=json] | Full graph verification + component wiring checks | | journey gen:stories | Regenerate Storybook story files from compiled definitions | | journey check [--no-spec] [--strict] [--quiet] [--format=json] | All-in-one: validate + compile + verify | | journey explore [file.journey] | Launch interactive visualizer dev server (port 4777) | | journey watch [--verify] | Watch .journey files, validate on change | | journey diff <file.journey> | Semantic diff: source vs compiled output | | journey doctor | Health check for project setup | | journey install-hooks | Install git pre-commit hook for validation |

CI Integration

Use --strict, --quiet, and --format=json flags for CI pipelines:

# Fail on warnings (not just errors)
journey check --strict

# Machine-readable output for CI parsers
journey verify --format=json

# Suppress info-level diagnostics
journey check --strict --quiet

Install a git pre-commit hook that validates staged .journey files before each commit:

journey install-hooks

The hook only validates files in the current commit, so it stays fast.

Explorer

Launch a standalone interactive visualizer without Storybook:

# Explore all journeys in configured directories
journey explore

# Focus on a specific file
journey explore journeys/dsl/authored/daily-checkin.journey

The explorer starts a Vite dev server on port 4777 with:

  • Journey picker sidebar — searchable list of all discovered journeys with node/edge counts and personas
  • Drag-and-drop — drop .journey files onto the explorer to compile and visualize them on the fly
  • Single-file mode — pass a file argument to open directly to that journey
  • No React install needed — React is resolved from the package internals, so your project doesn't need it as a dependency

Programmatic API

Parse & Compile

import { parse, compile, decompile } from "journey-dsl";

const ast = parse(sourceText);
const definition = compile(ast);
const roundTripped = decompile(definition);

Validation

import { validateAST, validateCrossFile } from "journey-dsl";

const diagnostics = validateAST(ast, { filePath: "my-flow.journey" });
const crossFileDiags = validateCrossFile(astMap, {
  rootJourney: "app-lifecycle",
});

Graph Construction

import {
  JourneyGraph,
  entry,
  view,
  exit,
  edge,
  resetEdgeCounter,
} from "journey-dsl";

resetEdgeCounter();
const graph = new JourneyGraph();

graph.addNode(entry({ id: "start", label: "Start", storyId: "app--start" }));
graph.addNode(exit({ id: "done", label: "Done" }));
graph.addEdge(
  edge({
    source: "start",
    target: "done",
    trigger: { type: "user_action", action: "next" },
  }),
);

Verification

import { verify } from "journey-dsl";

const result = verify(graph, {
  knownStoryIds: new Set(["app--start"]),
  knownJourneyNames: new Set(["daily-checkin"]),
  registry: manifestRegistry,
  externalRegistry: apiRegistry,
});

// result.diagnostics: VerificationDiagnostic[]
// result.pass: boolean

Individual checks are also exported: checkReachability, checkDeadEnds, checkCycles, checkAbsorbing, checkGuards, checkDataFlow, checkStoryIds, checkApiContracts, checkFormSchemas, checkSubjourneys, checkCircularSubjourneys, checkComplexity.

Component Wiring Checks

Validate that Storybook components match what the journey graph expects. Each check reads specific manifest properties:

| Check | Reads | Element Types | Severity | Behavior | | --- | --- | --- | --- | --- | | checkActionCoverage | emitsActions + edge triggers | buttons | error + warning | Bidirectional. Error if edge expects action component doesn't emit. Warning if component emits action no edge consumes. | | checkDataFieldCoverage | emitsData + node produces | inputs/toggles/etc. | warning | Warns if declared production has no matching emitted data field. Skips inherited fields (those with source). | | checkInteractiveTracking | interactiveElements[].fieldName | all except buttons | warning | Warns if non-button element lacks fieldName. Buttons skipped — covered by action-coverage. | | checkAbandonPaths | interactiveElements[] (buttons) | buttons | error | Errors if edge targets abandon node but component has no button for that action. | | checkDeadButtons | interactiveElements[] (buttons) | buttons | warning | Warns if button emits action no outgoing edge consumes. | | checkManifestCompleteness | registry entries | — | error + warning | Error if storyId has no registry entry. Warning if entry exists but has no manifest. | | checkRequiresData | requiresData + node data fields | — | error | Errors if a required requiresData field isn't available from the node's dataRequirements or dataProductions. |

import {
  checkActionCoverage,
  checkDataFieldCoverage,
  checkInteractiveTracking,
  checkAbandonPaths,
  checkDeadButtons,
  checkManifestCompleteness,
  checkRequiresData,
} from "journey-dsl";

// All 7 checks are also run automatically by verify() when a registry is provided:
const result = verify(graph, { registry: manifestRegistry, knownStoryIds });

Runtime

journey-dsl provides two runtime APIs:

  • createStateMachine — render-agnostic state machine. Your app owns rendering (React, Svelte, vanilla JS, etc.). This is the recommended approach for most apps.
  • createRuntime — DOM-based runtime that manages rendering via a component registry. Backward-compatible wrapper around createStateMachine.

createStateMachine (render-agnostic)

import { createStateMachine } from "journey-dsl";

const sm = createStateMachine({
  journey: definition,

  journeyMap: new Map([
    ["daily-checkin", dailyCheckin()],
    ["safety-plan", safetyPlan()],
  ]),

  guardResolvers: {
    'mood === "struggling"': (guard, data) => data.mood === "struggling",
  },

  effectHandlers: {
    api_call: async (effect, data) => {
      const res = await fetch(effect.payload.endpoint, {
        method: effect.payload.method,
      });
      return res.json();
    },
  },

  onTransition: (state, from, to, edge) => {
    console.log(`${from.id} -> ${to.id}`);
  },

  onValidationError: (nodeId, fieldErrors) => {
    // Show validation errors in your UI framework
  },

  onUnhandledAction: (nodeId, action) => {
    console.warn(`No edge for action "${action}" at node "${nodeId}"`);
  },
});

// Core API
await sm.dispatch("next");         // Trigger a user_action edge
sm.sendSystemEvent("data_loaded"); // Trigger a system_event edge
sm.mergeData({ name: "Alice" });   // Update journey data store
sm.getState();                     // { currentNodeId, data, history }
sm.getCurrentNode();               // Current JourneyNode
sm.getGraph();                     // Active JourneyGraph (child graph when inside subjourney)
sm.getContextDepth();              // 0 = root, 1+ = inside nested subjourney
sm.getDiagnostics();               // RuntimeDiagnostic[]

// Subscribe for reactive updates (React, Svelte, etc.)
const unsubscribe = sm.subscribe((state) => {
  // Re-render your UI with the new state
  renderMyComponent(state.currentNodeId, state.data);
});

React integration example:

function JourneyScreen({ sm }: { sm: StateMachineHandle }) {
  const [state, setState] = useState(sm.getState());

  useEffect(() => sm.subscribe(setState), [sm]);

  const node = sm.getCurrentNode();
  // Render based on node.id, node.type, state.data, etc.
}

createRuntime (DOM-based)

Wraps createStateMachine with DOM rendering, event listeners, and validation UI. Best for vanilla JS apps where you want the runtime to own rendering.

import { createRuntime } from "journey-dsl";

const runtime = createRuntime({
  journey: definition,
  registry: componentRegistry,
  container: document.getElementById("app"),

  journeyMap: new Map([
    ["daily-checkin", dailyCheckin()],
    ["safety-plan", safetyPlan()],
  ]),

  guardResolvers: {
    'mood === "struggling"': (guard, data) => data.mood === "struggling",
  },

  effectHandlers: {
    api_call: async (effect, data) => {
      const res = await fetch(effect.payload.endpoint, {
        method: effect.payload.method,
      });
      return res.json();
    },
  },

  onTransition: (state, from, to) => {
    console.log(`${from.id} -> ${to.id}`);
  },
});

runtime.dispatch("next"); // Take a user_action edge
runtime.getState(); // { currentNodeId, data, history }
runtime.getContextDepth(); // 0 = root journey, 1+ = inside subjourney
runtime.getDiagnostics(); // RuntimeDiagnostic[] (includes post-render-dom, dead-render)

The DOM runtime automatically:

  • Listens for journey:action and journey:data CustomEvents on the container
  • Renders the active node's component from the registry on each transition
  • Shows/clears validation errors on [data-journey-field] elements
  • Runs post-render DOM validation against component manifests (checks [data-action], button text, [name], [data-journey-field] attributes)
  • Detects dead renders (empty container content when manifest declares interactive elements)

Subjourney resolution

Both runtimes support nested subjourneys via a context stack. When a transition lands on a subjourney node:

  1. The node's journeyRef is looked up in journeyMap
  2. The parent context (graph, node position, exit mapping) is pushed onto a stack
  3. The child journey's entry node becomes the active node
  4. When the child journey reaches an exit or abandon node, the runtime pops the stack, maps the exit node ID through exitMapping to a system event, and dispatches that event in the parent — continuing the parent journey

Data is shared across parent and child (single state.data store). Nesting depth is unlimited. If journeyMap is not provided or the journeyRef can't be resolved, a subjourney-error diagnostic is emitted.

Form Validation Adapters

Wrap Zod or Yup schemas for use as node formSchema:

import { zodSchema, yupSchema } from "journey-dsl";

const schema = zodSchema(
  z.object({
    email: z.string().email(),
    age: z.number().min(18),
  }),
);

const result = schema({ email: "[email protected]", age: 25 });
// { valid: true, fieldErrors: {} }

API Spec Compliance

Check that journey effects match external OpenAPI/GraphQL specs:

import {
  loadOpenApiSpec,
  buildRegistry,
  checkApiSpecCompliance,
} from "journey-dsl";

const registry = await buildRegistry({
  openapi: ["specs/api.yaml"],
  graphql: ["specs/schema.graphql"],
});

const diagnostics = checkApiSpecCompliance(graph, registry);

Formatting

import {
  formatDiagnostic,
  formatJourneyResult,
  formatSummary,
} from "journey-dsl";

Vite Plugin

Import .journey files directly in your app with HMR support:

// vite.config.ts
import { journeyPlugin } from "journey-dsl/vite";

export default {
  plugins: [journeyPlugin()],
};
// In your app
import { journey } from "./daily-checkin.journey";
const definition = journey(); // JourneyDefinition

Component Manifests

The verification system checks components against the graph. Define manifests for each Storybook story:

import type { ComponentManifest } from "journey-dsl";

export const manifestRegistry = new Map<
  string,
  { manifest: ComponentManifest }
>([
  [
    "app--welcome",
    {
      manifest: {
        emitsActions: ["next", "close"],
        emitsData: ["userName"],
        interactiveElements: [
          { type: "button", action: "next", label: "Continue" },
          { type: "input", fieldName: "userName", label: "Your Name" },
        ],
        // Optional: declare inbound data the component needs to render correctly
        requiresData: [
          { name: "greeting", type: "string", required: true },
          { name: "showTips", type: "boolean", required: false },
        ],
      },
    },
  ],
]);

Interactive element types: button, input, toggle, scale, chip-group, selectable-row, grid. Buttons use the action property (maps to edge triggers); all other types use fieldName (maps to data fields the node produces).

The optional requiresData array declares what data the component expects as props. The checkRequiresData wiring check verifies that required fields are available from upstream dataRequirements or dataProductions on the node.

Verification Diagnostics

Every check produces diagnostics with a severity and category:

Severities: error | warning | info

Categories:

| Category | What it checks | | ---------------------- | -------------------------------------------- | | reachability | All nodes reachable from entry | | dead-ends | All nodes can reach an exit/abandon | | cycles | Loops have exit edges | | absorbing | Exit nodes exist | | guards | Decision guards are exhaustive | | data-flow | Required data available at each node | | story-ids | Story IDs exist in registry | | api-contracts | Effect schemas are consistent | | api-spec-compliance | Effects match external OpenAPI/GraphQL specs | | action-coverage | Bidirectional: edge actions match emitsActions, and vice versa (error + warning) | | data-field-coverage | Node produces fields match emitsData; skips inherited fields (warning) | | interactive-tracking | Non-button elements have fieldName; buttons excluded (warning) | | dead-buttons | Button actions in manifest consumed by at least one edge (warning) | | missing-abandon | Abandon-edge actions have matching buttons in component (error) | | component-manifest | All story IDs have registry entries with manifests (error + warning) | | requires-data | Component requiresData fields available on the node | | post-render-dom | DOM elements match manifest after render (runtime only) | | dead-render | Container not empty when manifest declares interactives (runtime only) | | subjourney-refs | Referenced sub-journeys exist | | circular-subjourneys | Circular subjourney reference cycles | | complexity | High branching (>5 edges) or large journeys (>30 nodes) | | unused-journeys | Journeys not reachable from rootJourney |

Visualizer

An interactive React-based journey visualizer with graph layout, simulation, node inspection, and diagnostics. Available as a separate entry point so the core package stays React-free.

# Install peer dependencies (only needed for the visualizer)
npm install react react-dom @xyflow/react dagre

Mount in any container

import { mountVisualizer } from "journey-dsl/visualizer";

const cleanup = mountVisualizer(
  document.getElementById("viz"),
  journeyDefinition,
  storyRegistry,    // optional — enables component preview mode
  journeyMap,       // optional — Map<name, JourneyDefinition> for subjourney expansion
  knownStoryIds,    // optional — Set<string> for diagnostics
);

// Later: cleanup() to unmount

Use individual components

All visualizer internals are exported for custom layouts:

import {
  // Layout
  graphToReactFlow,

  // React components
  JourneyNodeComponent,
  JourneyEdgeComponent,
  DiagnosticsPanel,
  NodeInspector,
  SimulationControls,
  SearchBar,
  ExportMenu,

  // Hooks
  useSimulation,
  useKeyboardShortcuts,

  // Analysis
  analyzeDataFlow,
} from "journey-dsl/visualizer";

import type {
  ViewMode,
  StoryRegistry,
  JourneyMap,
  SimulationState,
} from "journey-dsl/visualizer";

Storybook integration

The journey gen:stories command auto-generates Storybook stories that use the visualizer. Each journey gets its own fullscreen story with interactive graph navigation, simulation controls, and live diagnostics.

Features

  • Summary / Preview modes — toggle between compact node labels and live component previews
  • Simulation — step through journeys manually or auto-play, with accumulated data tracking and form validation status
  • Node inspector — click any node to see data requirements, productions, incoming/outgoing edges
  • Diagnostics panel — live verification results grouped by node or category, with severity filtering
  • Diagnostic badges — red/yellow badges on nodes with verification errors or warnings
  • Subjourney expansion — expand subjourney nodes inline to see the child journey's full graph
  • Dagre auto-layout — automatic left-to-right graph layout with happy path highlighting
  • Search — press / to search nodes by id, label, type, or storyId
  • Keyboard shortcuts — Space (play/pause), Arrow Right (step), R (reset), E (zoom to error), F (fit), ? (help)
  • Export — Mermaid, SVG, JSON to clipboard; JSON file download
  • Coverage overlay — toggle to see visited vs unvisited nodes/edges with color-coded stats
  • Data flow analysis — trace field producers/consumers through the graph

Peer Dependencies

All optional:

  • vite >= 5 — for the Vite plugin
  • @apidevtools/swagger-parser >= 12 — for OpenAPI spec loading
  • graphql >= 16 — for GraphQL schema loading
  • react >= 18 — for the visualizer
  • react-dom >= 18 — for the visualizer
  • @xyflow/react >= 12 — for the visualizer
  • dagre >= 0.8 — for the visualizer

License

Proprietary. See LICENSE for details.