Spry LogoOpsfolio
Contributing and Support

JSR Exports

Publishing and consuming Spry modules via JSR (JavaScript Registry).

Introduction

JSR (JavaScript Registry) is the modern package registry for JavaScript and TypeScript. Spry modules can be published to JSR, making them available for use in any Deno, Node.js, Bun, or browser project without requiring the full Spry framework.

What is JSR?

JSR is a modern, TypeScript-first package registry that natively supports ES modules. Unlike npm, JSR publishes TypeScript source code directly and handles transpilation, documentation generation, and type declarations automatically.


Purpose

JSR exports enable:

  • Standalone Usage - Use Spry utilities in any JavaScript/TypeScript project
  • Dependency Management - Leverage JSR's built-in versioning and dependency resolution
  • Cross-Platform - Works seamlessly in Deno, Node.js, Bun, and browsers
  • Tree Shaking - Import only what you need for optimal bundle sizes
  • Type Safety - Full TypeScript definitions included
  • Auto-Generated Docs - API documentation created from TypeScript source

Exportable Modules

Spry consists of multiple modules that can be independently published to JSR for standalone use. Each module serves a specific purpose and can be used independently or in combination.

@spry/universal

Core utilities and cross-cutting functionality

The universal module provides essential infrastructure used throughout Spry, including DAG-based task execution, shell command execution, file system utilities, and terminal UI components.

# Add to your project
deno add jsr:@spry/universal

# Or use directly without install
import { executionPlan } from "jsr:@spry/universal@^1.0.0";
# Install
npx jsr add @spry/universal

# Use in your code
import { executionPlan } from "@spry/universal";
# pnpm
pnpm add jsr:@spry/universal

# yarn
yarn add jsr:@spry/universal

Available Exports:

// Task execution and DAG processing
import {
  executionPlan,
  executionSubplan,
  executeDAG,
  type Task,
} from "@spry/universal/task";

// Shell command execution
import {
  shell,
  verboseInfoShellEventBus,
  errorOnlyShellEventBus,
} from "@spry/universal/shell";

// Event bus for pub/sub messaging
import { eventBus, type EventBus } from "@spry/universal/event-bus";

// File system utilities
import { Resource } from "@spry/universal/resource";
import { PathTree } from "@spry/universal/path-tree";
import { gitignore } from "@spry/universal/gitignore";

// Terminal UI builders
import { ListerBuilder } from "@spry/universal/lister-tabular-tui";
import { TreeLister } from "@spry/universal/lister-tree-tui";

// Text and template utilities
import {
  dedentIfFirstLineBlank,
  indent,
  safeJsonStringify,
} from "@spry/universal/tmpl-literal-aide";

// System diagnostics
import { doctor } from "@spry/universal/doctor";
import { watcher } from "@spry/universal/watcher";
import { computeSemVerSync } from "@spry/universal/version";

// Code parsing
import { parseCodeFenceInfo } from "@spry/universal/cline";

// Zod utilities
import { jsonToZod } from "@spry/universal/zod-aide";

Use Cases:

  • Building CLI tools with rich terminal output
  • Creating task runners and build systems
  • File system manipulation and monitoring
  • Shell script orchestration
  • DAG-based workflow engines

@spry/courier

Typed, protocol-aware data movement library

Courier provides typed data movement capabilities for ETL/ELT, CDC, and streaming workflows, with support for Singer, Airbyte, and native protocols.

# Add to your project
deno add jsr:@spry/courier

# Or use directly
import { dataMovementPipeline } from "jsr:@spry/courier@^1.0.0";
# Install
npx jsr add @spry/courier

# Use in your code
import { dataMovementPipeline } from "@spry/courier";
# pnpm
pnpm add jsr:@spry/courier

# yarn
yarn add jsr:@spry/courier

Available Exports:

// Core protocol and pipeline
import {
  dataMovementPipeline,
  dataMoveSingleStreamMap,
  dataMoveSingleStreamDef,
  type DataMoveTap,
  type DataMoveTarget,
  type DataMoveMessageTransform,
  type StreamSchemaMap,
} from "@spry/courier/protocol";

// Singer protocol support
import {
  singerWireMessageSchema,
  dataMoveTypedRecordToSingerRecordWireSchema,
  dataMoveSingerSchemaWireFromMetaSchema,
} from "@spry/courier/singer";

// Airbyte protocol support
import {
  airbyteWireMessageSchema,
  airbyteRecordMessageSchema,
} from "@spry/courier/airbyte";

Use Cases:

  • Building data pipelines and ETL workflows
  • Creating custom Singer taps and targets
  • Integrating with Airbyte connectors
  • Database synchronization
  • API to warehouse data loading
  • CDC (Change Data Capture) systems

@spry/tap

Test Anything Protocol (TAP) implementation for Deno.

Provides TAP-compliant test output formatting and reporting. This module is designed for generating TAP v14 output, useful for building test runners, reporters, or compliance artifacts.

Add to your project

deno add jsr:@spry/tap

Usage

Generating TAP Output

Use TapContentBuilder to construct a TAP test suite and stringify to generate the TAP output string.

import { TapContentBuilder, stringify } from "@spry/tap";

const builder = new TapContentBuilder();
const bb = builder.bb;

// Add test cases
await bb.ok("Service should be up");
await bb.notOk("Database connection failed", {
  diagnostics: { error: "Connection refused", code: 500 },
});

// Add comments
await bb.comment("Checking critical systems");

// Support for subtests
await bb.okParent("Authentication Module", async (subBB) => {
  await subBB.ok("Login success");
  await subBB.ok("Logout success");
});

// Generate TAP string
const content = builder.tapContent();
console.log(stringify(content));

Generating HTML Report

You can also generate a standalone HTML report from the TAP content.

import { TapContentBuilder } from "@spry/tap";
import { tapContentHTML } from "@spry/tap/report"; // Assuming report is exported or accessible

const builder = new TapContentBuilder();
// ... add tests ...

const content = builder.tapContent();
const html = tapContentHTML(content);
await Deno.writeTextFile("report.html", html);

API

TapContentBuilder

The main class for building TAP content.

  • bb.ok(description, options?): Add a passing test case.
  • bb.notOk(description, options?): Add a failing test case.
  • bb.comment(text): Add a comment.
  • bb.okParent(description, callback): Add a subtest suite.

stringify(content)

Converts the TapContent object into a TAP v14 formatted string.

tapContentHTML(content, options?)

Generates a self-contained HTML report with filtering and search capabilities.


@spry/extend

Plugin and extension system

Provides a flexible, type-safe extension system where "callables" are normal JavaScript functions validated at runtime by Zod. It allows hosts to discover and execute implementations by stable string IDs, decoupling them from symbol names or module paths.

# Add to your project
deno add jsr:@spry/extend

# Or use directly
import { callableDefn } from "jsr:@spry/extend@^1.0.0";
# Install
npx jsr add @spry/extend

# Use in your code

import { callableDefn } from "@spry/extend";
# pnpm
pnpm add jsr:@spry/extend

# yarn
yarn add jsr:@spry/extend

Available Exports:

// Extension system core
import {
  callableDefn,
  callable,
  scanCallables,
  call,
  type CallableDefn,
  type CallableImpl,
} from "@spry/extend";

Use Cases:

  • Creating plugin systems for applications
  • Building extensible CLI tools
  • Decoupling interface definitions from implementations
  • Dynamic feature loading
  • Type-safe runtime execution
  • Modular application architecture

@spry/reflect

Reflection and metadata utilities for Deno

Provides lightweight utilities for object inspection, callable method extraction, and source code provenance capturing for building dynamic, introspective applications.

# Add to your project
deno add jsr:@spry/reflect

# Or use directly
import { reflect } from "jsr:@spry/reflect@^1.0.0";
# Install
npx jsr add @spry/reflect

# Use in your code
import { reflect } from "@spry/reflect";
# pnpm
pnpm add jsr:@spry/reflect

# yarn
yarn add jsr:@spry/reflect

Available Exports:

// Object reflection
import {
  reflect,
  type TypeInfo,
  type ObjectTypeInfo,
  type PropertyInfo,
} from "@spry/reflect";

// Callable extraction
import {
  callables,
  type CallableInfo,
} from "@spry/reflect";

// Source code provenance
import {
  sourceCodeProvenance,
  type ProvenanceInfo,
} from "@spry/reflect";

Use Cases:

  • Runtime type inspection and validation
  • Dynamic API discovery
  • Debugging and logging utilities
  • Metadata-driven frameworks
  • Serialization/deserialization
  • Developer tooling and introspection

@spry/spawn

Process spawning and management

Advanced process spawning utilities with enhanced error handling, streaming I/O, and lifecycle management for robust subprocess execution.

# Add to your project
deno add jsr:@spry/spawn

# Or use directly
import { shell } from "jsr:@spry/spawn@^1.0.0";
# Install
npx jsr add @spry/spawn

# Use in your code

import { shell } from "@spry/spawn";
# pnpm
pnpm add jsr:@spry/spawn

# yarn
yarn add jsr:@spry/spawn

Available Exports:

// Shell execution
import { shell, type ShellBusEvents } from "@spry/spawn";

// Catalog-based execution
import { catalogFromYaml, using, type LanguageInitCatalog } from "@spry/spawn";

Use Cases:

  • Running external commands with robust error handling
  • Process pool management
  • Build tool implementations
  • Test runner process management
  • Parallel command execution
  • Long-running process supervision

Installation Methods

Direct Import (Deno Only)

Deno supports importing JSR packages directly without an install step:

import { executionPlan } from "jsr:@spry/universal@^1.0.0/task";
import { dataMovementPipeline } from "jsr:@spry/courier@^1.0.0/protocol";

// Use immediately
const plan = executionPlan(tasks);
await dataMovementPipeline({ tap, target });

Version Constraints: Use @^1.0.0 for latest 1.x version, @1 for any 1.x, or @1.2.3 for exact version.


With Package Manager

For projects using package.json:

# npm
npx jsr add @spry/universal @spry/courier

# pnpm (10.9+)
pnpm add jsr:@spry/universal jsr:@spry/courier

# yarn (4.9+)
yarn add jsr:@spry/universal jsr:@spry/courier

# bun
bunx jsr add @spry/universal @spry/courier

This adds entries to your package.json:

{
  "dependencies": {
    "@spry/universal": "jsr:@spry/universal@^1.0.0",
    "@spry/courier": "jsr:@spry/courier@^1.0.0"
  }
}

Publishing to JSR

Prerequisites

  1. Create JSR Account: Sign up at jsr.io
  2. Create Scope: Create a scope like @yourname at jsr.io/new
  3. Install Deno: Required for publishing (even for Node.js packages)

Package Structure

Each Spry module should have this structure for JSR publishing:

@spry/universal/
├── deno.json           # Package configuration
├── mod.ts              # Main entrypoint
├── task.ts             # Task execution module
├── shell.ts            # Shell execution module
├── event-bus.ts        # Event bus module
└── README.md           # Package documentation

Configuration File

Create a deno.json or jsr.json file for each module:

Example for @spry/universal:

{
  "name": "@spry/universal",
  "version": "1.0.0",
  "exports": {
    ".": "./mod.ts",
    "./task": "./task.ts",
    "./shell": "./shell.ts",
    "./event-bus": "./event-bus.ts",
    "./resource": "./resource.ts",
    "./gitignore": "./gitignore.ts",
    "./lister-tabular-tui": "./lister-tabular-tui.ts",
    "./lister-tree-tui": "./lister-tree-tui.ts",
    "./doctor": "./doctor.ts",
    "./watcher": "./watcher.ts",
    "./version": "./version.ts",
    "./cline": "./cline.ts",
    "./zod-aide": "./zod-aide.ts",
    "./tmpl-literal-aide": "./tmpl-literal-aide.ts",
    "./path-tree": "./path-tree.ts"
  },
  "publish": {
    "exclude": [
      "**/*_test.ts",
      "**/*.test.ts",
      "test/",
      "examples/"
    ]
  }
}

Example for @spry/courier:

{
  "name": "@spry/courier",
  "version": "1.0.0",
  "exports": {
    ".": "./mod.ts",
    "./protocol": "./protocol.ts",
    "./singer": "./singer.ts",
    "./airbyte": "./airbyte.ts"
  },
  "publish": {
    "exclude": [
      "**/*_test.ts",
      "test/"
    ]
  }
}

Example for @spry/spawn:

{
  "name": "@spry/spawn",
  "version": "1.0.0",
  "exports": {
    ".": "./mod.ts",
    "./manager": "./manager.ts",
    "./streams": "./streams.ts",
    "./errors": "./errors.ts",
    "./utilities": "./utilities.ts"
  },
  "publish": {
    "exclude": [
      "**/*_test.ts",
      "test/"
    ]
  }
}

Exports Field:

The exports field defines which modules users can import. Each key is an import path, each value is the file that provides it. Use subpath exports (like ./manager) to organize your API logically.


Publishing Process

# Dry run to verify package
deno publish --dry-run

# Publish for real
deno publish

# Follow browser authentication prompt
# Your package is now live at jsr.io/@spry/universal

First, link your GitHub repository in package settings on jsr.io.

Then create .github/workflows/publish.yml:

name: Publish to JSR

on:
  push:
    branches:
      - main
    tags:
      - 'v*'

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write  # Required for OIDC auth

    steps:
      - uses: actions/checkout@v4
      
      - uses: denoland/setup-deno@v1
        with:
          deno-version: v1.x
      
      - name: Publish to JSR
        run: deno publish

This automatically publishes on every push to main or when you create a version tag.


Usage Examples

Using Universal Module

import { z } from "@zod/zod";
import {
  executionPlan,
  executeDAG,
  shell,
  verboseInfoShellEventBus,
  ListerBuilder,
} from "@spry/universal";

// Create shell executor
const shellBus = verboseInfoShellEventBus({ style: "rich" });
const sh = shell({ bus: shellBus, cwd: Deno.cwd() });

// Define tasks
const tasks = [
  {
    taskId: () => "clean",
    dependencies: () => [],
    async run() {
      await sh.spawnText("rm -rf dist");
    },
  },
  {
    taskId: () => "build",
    dependencies: () => ["clean"],
    async run() {
      await sh.spawnText("deno bundle src/mod.ts dist/bundle.js");
    },
  },
];

// Execute task DAG
const plan = executionPlan(tasks);
await executeDAG(plan, (task) => task.run());

// Display results in table
const results = [
  { task: "clean", status: "done", duration: 123 },
  { task: "build", status: "done", duration: 4567 },
];

await new ListerBuilder()
  .declareColumns("task", "status", "duration")
  .from(results)
  .field("task", "task", { header: "Task" })
  .field("status", "status", { header: "Status" })
  .field("duration", "duration", { header: "Duration (ms)" })
  .build()
  .ls(true);

Using Courier Module

import { z } from "@zod/zod";
import {
  dataMovementPipeline,
  dataMoveSingleStreamMap,
  dataMoveSingleStreamDef,
  type DataMoveTap,
  type DataMoveTarget,
} from "@spry/courier";

// Define schema
const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
});

const schemas = dataMoveSingleStreamMap("users", UserSchema);

// Create tap (source)
const apiTap: DataMoveTap<typeof schemas> = {
  id: "api-source",
  streams: {
    users: dataMoveSingleStreamDef("users", UserSchema),
  },
  async *read(ctx) {
    const response = await fetch("https://api.example.com/users");
    const users = await response.json();
    
    for (const user of users) {
      yield {
        protocol: "data-move-protocol",
        type: "RECORD",
        stream: "users",
        record: user,
      };
    }
  },
};

// Create target (destination)
const dbTarget: DataMoveTarget<typeof schemas> = {
  id: "database-target",
  handleMessage(msg) {
    if (msg.type === "RECORD") {
      console.log(`Inserting user: ${msg.record.name}`);
      // Insert into database
    }
  },
};

// Run pipeline
await dataMovementPipeline({
  tap: apiTap,
  target: dbTarget,
});

Combining Multiple Modules

import { executionPlan, executeDAG } from "@spry/universal/task";
import { dataMovementPipeline } from "@spry/courier/protocol";
import { spawn } from "@spry/spawn";
import { doctor } from "@spry/universal/doctor";
import { createExtension } from "@spry/extend";
import { getMetadata } from "@spry/reflect/metadata";

// Check prerequisites
const diags = doctor([
  "deno --version",
  "psql --version",
]);

const health = await diags.run();
if (health.some(d => !d.success)) {
  console.error("Missing dependencies!");
  Deno.exit(1);
}

// Define ETL pipeline as tasks
const tasks = [
  {
    taskId: () => "extract",
    dependencies: () => [],
    async run() {
      await dataMovementPipeline({ tap: sourceTap, target: stagingTarget });
    },
  },
  {
    taskId: () => "transform",
    dependencies: () => ["extract"],
    async run() {
      const result = await spawn("dbt", ["run", "--models", "staging"]);
      if (result.code !== 0) {
        throw new Error(`Transform failed: ${result.stderr}`);
      }
    },
  },
  {
    taskId: () => "load",
    dependencies: () => ["transform"],
    async run() {
      await dataMovementPipeline({ tap: stagingTap, target: warehouseTarget });
    },
  },
];

// Execute workflow
const plan = executionPlan(tasks);
await executeDAG(plan, (task) => task.run());

Using TAP Module for Testing

import { createTAPReporter, ok, equal, deepEqual } from "@spry/tap";

const tap = createTAPReporter();

tap.plan(5); // Declare number of tests

// Run tests
ok(true, "basic assertion");
ok(1 + 1 === 2, "math works");
equal("hello", "hello", "string equality");
notEqual(1, 2, "numbers are different");

const obj1 = { a: 1, b: 2 };
const obj2 = { a: 1, b: 2 };
deepEqual(obj1, obj2, "object deep equality");

// Output:
// TAP version 14
// 1..5
// ok 1 - basic assertion
// ok 2 - math works
// ok 3 - string equality
// ok 4 - numbers are different
// ok 5 - object deep equality

Using Extend Module for Plugins

import {
  createExtension,
  registerExtension,
  loadExtensions,
} from "@spry/extend";
import { defineExtensionPoint } from "@spry/extend/extension-points";

// Define extension point
const loggerExtension = defineExtensionPoint<{
  log: (message: string) => void;
}>("logger");

// Create extensions
const consoleLoggerExt = createExtension({
  id: "console-logger",
  extensionPoint: "logger",
  implementation: {
    log: (message: string) => console.log(message),
  },
});

const fileLoggerExt = createExtension({
  id: "file-logger",
  extensionPoint: "logger",
  implementation: {
    log: async (message: string) => {
      await Deno.writeTextFile("app.log", `${message}\n`, { append: true });
    },
  },
});

// Register and use
registerExtension(consoleLoggerExt);
registerExtension(fileLoggerExt);

const extensions = loadExtensions("logger");
extensions.forEach(ext => ext.implementation.log("Application started"));

Using Reflect Module for Metadata

import {
  defineMetadata,
  getMetadata,
  createPropertyDecorator,
} from "@spry/reflect";

// Create custom decorators using reflection
const Required = createPropertyDecorator((target, propertyKey) => {
  defineMetadata("validation:required", true, target, propertyKey);
});

const Email = createPropertyDecorator((target, propertyKey) => {
  defineMetadata("validation:email", true, target, propertyKey);
});

// Use decorators
class User {
  @Required
  name!: string;

  @Required
  @Email
  email!: string;

  age?: number;
}

// Validation using metadata
function validate(instance: any) {
  const constructor = instance.constructor;
  
  for (const key of Object.keys(instance)) {
    const isRequired = getMetadata("validation:required", constructor.prototype, key);
    const isEmail = getMetadata("validation:email", constructor.prototype, key);
    
    if (isRequired && !instance[key]) {
      throw new Error(`${key} is required`);
    }
    
    if (isEmail && instance[key] && !instance[key].includes("@")) {
      throw new Error(`${key} must be a valid email`);
    }
  }
}

// Test validation
const user = new User();
user.name = "Alice";
user.email = "alice@example.com";

validate(user); // Passes

Using Spawn Module for Process Management

import {
  spawn,
  spawnStream,
  withTimeout,
  withRetry,
} from "@spry/spawn";
import { ProcessManager } from "@spry/spawn/manager";

// Simple process spawning
const result = await spawn("git", ["status"]);
console.log(result.stdout);

// Spawn with streaming output
await spawnStream("npm", ["install"], {
  onStdout: (chunk) => process.stdout.write(chunk),
  onStderr: (chunk) => process.stderr.write(chunk),
});

// Spawn with timeout
try {
  await withTimeout(
    spawn("long-running-command"),
    5000 // 5 seconds
  );
} catch (err) {
  console.error("Command timed out!");
}

// Spawn with retry
const result = await withRetry(
  () => spawn("flaky-command"),
  {
    maxAttempts: 3,
    delayMs: 1000,
  }
);

// Process pool management
const manager = new ProcessManager({
  maxConcurrent: 5,
});

// Spawn multiple processes with concurrency limit
const commands = [
  ["task1", "arg1"],
  ["task2", "arg2"],
  ["task3", "arg3"],
  ["task4", "arg4"],
  ["task5", "arg5"],
  ["task6", "arg6"],
];

const results = await Promise.all(
  commands.map(args =>
    manager.spawn(args[0], args.slice(1))
  )
);

// Cleanup
await manager.shutdown();

Package Scores

JSR assigns quality scores to packages based on:

  • Documentation Coverage - JSDoc comments on all exported symbols
  • Type Quality - Avoid "slow types" for better type checking performance
  • Runtime Compatibility - Support for multiple runtimes (Deno, Node, Bun, etc.)
  • Package Metadata - Complete package.json/deno.json configuration

Improving Your Score:

  1. Add comprehensive JSDoc comments
  2. Specify runtime compatibility in package settings
  3. Use proper TypeScript types (avoid any)
  4. Include README with examples
  5. Add tests and CI/CD

How is this guide?

Last updated on

On this page