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

@xevion/tempo

v0.2.1

Published

A task graph for a project's checks, formatters, and dev servers

Readme

@xevion/tempo

A task graph for the scripts a project accumulates: checks, formatters, dev servers, one-off commands. Works with Bun, Node.js (22+), and Deno.

Install

bun add -d @xevion/tempo    # or npm/pnpm/yarn

Quick Start

Create a tempo.config.ts in your project root:

import { defineConfig, presets, task } from "@xevion/tempo";

export default defineConfig({
  tasks: [
    ...presets.biome({ name: "web" }),

    task({ name: "web:types", body: "bunx tsc --noEmit", tags: ["check"] }),

    task({
      name: "web:dev",
      body: "bun run --hot src/index.ts",
      tags: ["dev"],
      persistent: true,
    }),
  ],

  commands: {
    check: { description: "Run every check in parallel", tags: ["check"] },
    fmt: { description: "Apply every formatter", tags: ["format"], concurrency: 1 },
    dev: { description: "Run the dev server", tags: ["dev"] },
  },
});
tempo check          # everything tagged "check", in parallel
tempo check web      # narrowed to one component, dependencies preserved
tempo fmt            # everything tagged "format", one at a time
tempo list           # every task with its tags and dependencies
tempo run web:types  # specific tasks by name

A target names a task, a tag, or a namespace (web covers web:*). One that matches nothing in the command's selection is an error listing what does, never a quietly smaller run. requireTargets: true on a command makes a bare invocation the same error, for a workspace where "everything" is no longer a sensible default.

The model

A task is the atom: a name, a body, tags, requirements, and edges. A command is a selector over tasks rather than a runner of its own — check means "everything tagged check". Adding a task to a tag adds it to the command; there is no second place to register it.

A body is a shell string, an argv array, or a TypeScript function, and all three are peers. A function body gets the same row, timing, cancellation, and JSON record as a spawned process:

task({
  name: "api:bindings",
  tags: ["check"],
  body: async (ctx) => {
    const { stdout } = await ctx.capture(["cargo", "metadata", "--format-version", "1"]);
    if (!stdout.includes("ts-rs")) ctx.fail("ts-rs is not in the dependency tree");
    await ctx.run(["cargo", "test", "export_bindings"]);
  },
});

needs and after are orthogonal. needs(x) requires x to succeed and pulls it into the run; after(x) orders only, and is inert unless x was already selected. Use after for a reporting step that should still run when the thing it reports on failed.

Caching

Declare what a task reads and writes, and it is skipped while its inputs are unchanged:

task({
  name: "pkg:build",
  body: "bun run build",
  tags: ["build"],
  inputs: ["src/**/*.ts", "package.json"],
  outputs: ["dist/**/*.mjs"],
});

Inputs are fingerprinted by content, not timestamps. Inside a git work tree they are the files git ls-files -co --exclude-standard lists, so ignored trees such as target/ are never walked; a pattern with no wildcard names one file and counts even when it is ignored. Digests are remembered in .tempo/hashes.json by size and timestamps, so a cached task re-reads only what changed. Outputs are checked by size and mtime, so a hand edit to a generated file reruns its task; they are found by walking below each pattern's literal prefix, so ignored build artifacts work. cacheKey adds anything else the result depends on, such as a compiler version. --no-cache recomputes everything for one run. Fingerprints live in .tempo/, which ignores itself, so nothing reaches your diff. A cache hit satisfies dependents: anything that needs this task proceeds immediately.

Dev sessions

A persistent task with a watch spec runs under a supervisor that restarts it when its files change, re-running its needs first:

task({
  name: "api:dev",
  body: ["cargo", "run", "-p", "server"],
  tags: ["dev"],
  persistent: true,
  needs: ["api:build"],
  // interrupt: the rebuild rewrites the running binary, so stop before rebuilding.
  watch: { paths: ["src"], exts: [".rs"], interrupt: true },
  readyWhen: async (ctx) => (await ctx.run("curl -sf localhost:8080/health")) === 0,
});

readyWhen gates dependents on serving rather than on spawning. interrupt decides ordering: stop-then-rebuild when the rebuild would overwrite the running binary, otherwise rebuild-then-swap so a failed build costs nothing.

Requirements

A task can declare what its environment must provide. Missing requirements skip the task locally and fail it under CI, so a machine without a tool loses a check rather than pretending:

task({
  name: "ci:actionlint",
  body: "actionlint",
  tags: ["check"],
  requires: [{ tool: "actionlint", hint: "brew install actionlint" }],
});

{ tool }, { env }, and { file } are all accepted.

Locking

lock: true serializes a task against every other tempo process in the project — for a tool whose incremental cache corrupts when two invocations race. A string names its own lock file, so tasks contending for one cache can share a lock without taking the rest. A blocked task waits rather than failing, and the wait is reported beside the work:

✓ mod:verify (41.2s, waited 12.8s)

Presets

Presets generate the tasks for a toolchain. override replaces a body, or drops the task with false, and naming a task the preset does not produce is an error rather than a silent no-op:

...presets.rust({
  name: "api",
  override: {
    lint: "cargo clippy --all-targets --all-features -- -D warnings",
    build: false,
  },
})

| Preset | Tasks it generates | |--------|--------------------| | presets.rust | format, format-fix, lint, test, build | | presets.biome | format, format-fix, lint, build, and type-check/test on request | | presets.go | format, format-fix, lint, build, test | | presets.gradle | format, format-fix, lint, compile, test |

Throttling

A run takes the whole machine by default. throttle hands most of it back while something else needs it, which is what makes a full gate survivable during a game or another build:

export default defineConfig({
  throttle: {
    whileRunning: ["bf4.exe"],  // omit to throttle unconditionally
    cores: 2,                   // whole physical cores, SMT siblings included
    concurrency: 2,             // a cap, never a raise
    env: { CARGO_BUILD_JOBS: "2" },
  },
  tasks: [...],
})

Each child is spawned behind taskset -c <cpus> chrt -i 0 ionice -c3, so every tool downstream sizes its own thread pool from the affinity mask without a per-tool flag. Linux only. whileRunning is checked again as tasks spawn, so a game launched mid-session throttles the next rebuild; the concurrency cap is fixed when the run starts. throttle: false on a command exempts it, and on a task keeps that one process (a dev server, say) at full priority.

CLI

Global flags are parsed before the command, so -- and everything after it survives:

| Flag | Effect | |------|--------| | --config <path> | Use a specific config | | --dry-run | Print the layers that would run | | --json | Emit the raw engine record stream as JSON Lines on stdout | | -c, --concurrency <n> | Cap parallel tasks | | --no-cache | Ignore fingerprints | | --no-throttle | Ignore config.throttle for this run |

--json is the engine's own record stream, not a rendering of it: stdout carries records and nothing else, so it stays parseable while a task is writing to the terminal.

Exports

import { defineConfig, presets, task } from "@xevion/tempo";
import { Graph, run, supervise } from "@xevion/tempo/engine";
import { c, elapsed } from "@xevion/tempo/fmt";

@xevion/tempo/engine is the graph and scheduler on their own, for driving the engine without the CLI.

Global installs

A config can import from @xevion/tempo even when the package is not in the project's node_modules — the CLI registers itself as a virtual module, so a globally installed tempo works in a project with no package.json at all. Type information still needs a real devDependency, since the compiler cannot see a module that only exists at runtime.

License

LGPL-3.0-or-later