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

@beamhop/builder

v0.1.2

Published

Build OCI images from a typed, immutable spec — layer assembly, caching, and a pluggable RUN executor.

Readme

@beamhop/builder

The beambox build engine: stages, layer assembly, caching, and the executor interface that RUN steps plug into.

Most people want beambox, which wraps this with a fluent API and sensible defaults. Reach for this package directly when you are generating build plans programmatically or writing your own executor.

bun add @beamhop/builder

A build plan

A plan is data. Nothing here is fluent, and nothing is hidden.

import { BlobStore } from "@beamhop/oci"
import { build } from "@beamhop/builder"

const store = new BlobStore("./cache/blobs")

const image = await build(
  {
    stages: [
      {
        base: { kind: "registry", reference: "alpine:3.20" },
        ops: [
          { kind: "copy", sources: ["dist"], destination: "/app" },
          { kind: "env", values: { NODE_ENV: "production" } },
          { kind: "workdir", path: "/app" },
          { kind: "cmd", command: { form: "exec", argv: ["/app/server"] } },
        ],
      },
    ],
  },
  { store, context: "." },
)

No RUN steps means no executor is needed and no VM is booted. build returns a plain ImageArtifact from @beamhop/oci, ready for the archive writers or the registry pusher.

Stages

const plan = {
  stages: [
    {
      name: "builder",
      base: { kind: "registry", reference: "node:22" },
      ops: [
        { kind: "copy", sources: ["."], destination: "/src" },
        { kind: "run", command: { form: "shell", command: "npm ci && npm run build" } },
      ],
    },
    {
      base: { kind: "registry", reference: "node:22-slim" },
      ops: [{ kind: "copy", from: "builder", sources: ["/src/dist"], destination: "/app" }],
    },
  ],
  target: undefined, // or a stage name, as `docker build --target` does
}

base can be { kind: "scratch" }, { kind: "registry", reference }, or { kind: "stage", stage }. COPY --from reads a finished stage's filesystem by replaying its layers and applying whiteouts — see StageFilesystem.

Writing an executor

The engine never imports microsandbox. It talks to this interface, which is why a RUN-free build has no runtime dependency and why the engine is testable without booting anything.

import type { Executor, ExecutorSession } from "@beamhop/builder"

export const myExecutor: Executor = {
  name: "my-executor",

  supports(platform) {
    return platform.os === "linux" ? { supported: true } : { supported: false, reason: "linux only" }
  },

  async open(context): Promise<ExecutorSession> {
    // context.base is the image so far; context.mounts is every mount this stage declares.
    return {
      async apply(step) {
        if (step.kind === "run") {
          // Execute step.argv, then return a layer of what changed — or undefined if nothing did.
        }
        // step.kind === "materialize": write step.entries so later RUN steps can see them.
        return undefined
      },
      async close() {},
    }
  },
}

One session is opened per stage, at the first RUN, and closed when the stage ends — including when a step throws. A COPY between two RUN steps is routed through the session so the next command actually sees the files; a trailing COPY with no RUN after it closes the session first and builds on the host, which is far cheaper.

Caching

Only RUN is cached, keyed on the parent image digest plus the exact step. COPY and ADD build their layer on the host anyway, and the blob store already deduplicates identical content by digest, so a cache entry would save nothing.

import { LayerCache, defaultCacheDirectory } from "@beamhop/builder"

const cache = new LayerCache(store, `${defaultCacheDirectory()}/run-cache.json`)
await build(plan, { store, executor, cache })
await build(plan, { store, executor, cache: false }) // ignore it

A cache index that outlives its blobs is a miss, not a failure.

Progress

await build(plan, {
  store,
  executor,
  onProgress: (event) => {
    switch (event.kind) {
      case "stage":   return console.log(`stage ${event.index + 1}/${event.total}`)
      case "step":    return console.log(`  ${event.instruction}`)
      case "cached":  return console.log(`  (cached)`)
      case "pull":    return console.log(`  pulling ${event.reference}`)
      case "output":  return process.stderr.write(event.text)
      case "warning": return console.warn(event.message)
    }
  },
})

Build context

resolveCopy implements Dockerfile's destination rules — copying a directory contributes its contents, and a destination ending in / or any copy with several sources is treated as a directory. .dockerignore is order-sensitive, so a later !pattern re-includes.

import { loadDockerignore, parseDockerignore, resolveCopy } from "@beamhop/builder"

const ignore = parseDockerignore("node_modules\n*.log\n!keep.log\n")
ignore.ignores("node_modules/x") // true
ignore.ignores("keep.log")       // false