SpuntoSpunto/@spunto/build

@spunto/build

The parts of Spunto's Build pillar that are the same whoever runs the control plane — shared by Spunto Cloud and Spunto Lite.

@spunto/build holds the parts of Spunto's Build pillar that do not depend on who runs the control plane. Spunto Cloud and Spunto Lite both ship them, from the same published version on npm.

Build is the pillar that turns a project (an image, some devcontainer features, some VS Code extensions, repositories, lifecycle commands) into a running worker: an isolated development container used by a human or an AI agent. Two products do this. They differ in almost everything around it, such as tenancy, database, Docker topology and UI. But a lot of what happens between a project and a worker has to come out identical on both sides:

  • the shell script that bakes the image and the one that sets up the container,
  • the markers that script prints so a UI can follow the build,
  • the names of the containers, volumes and images it creates,
  • the file a worker writes its progress to,
  • the JSON file a project is exported to.

Before this package, each of those existed twice, typed by hand on each side, and the two copies had already started to drift in both directions. Now there is one copy, and it lives here.

"Looks" vs. "agrees on"

This package follows the same idea as @spunto/design-system, one layer lower:

PackageOwnsIts types are
@spunto/design-systemwhat the two products look like: tokens, primitives, componentsstructural and lax: everything optional, so a card degrades instead of throwing on a payload it has never seen
@spunto/buildwhat the two products agree on: protocols, names, file formats, generated scriptscanonical and strict: a shell-script generator must refuse a shape it does not know

The two rules are opposite on purpose, and that is why this package cannot be folded into the design system. See Design rules.

A quick tour

Each entry point is a subpath import, and each one does a single job:

Several of these modules define wire formats: something one program writes and another reads, sometimes minutes later, sometimes in another language. Those formats are documented byte by byte in the Formats tab, because changing one of them is a migration, not an edit.

Quick start

npm install @spunto/build

The package ships TypeScript source, so your toolchain has to transpile it (Next.js needs one line of config). See Installation.

Generate an image build script, then follow its progress in the log:

build.ts
import { buildImageScript } from "@spunto/build/script"
import { BuildStepTracker } from "@spunto/build/steps"
import { projectImageRef } from "@spunto/build/naming"

const { script, steps } = buildImageScript({
  features: [{ id: "node" }],
  vscodeExtensions: ["esbenp.prettier-vscode"],
})

const ref = projectImageRef("my-app", 7) // "mp-proj-my-app:v7"

// …run `script` inside a docker build tagged `ref`, and for every chunk of its log:
const tracker = new BuildStepTracker(steps)
const { text, changed } = tracker.ingest(chunk) // markers become banners, steps light up

Export a project as a file, and read one back:

export.ts
import { buildProjectSpec, parseProjectSpec, projectSpecFilename } from "@spunto/build/spec"

const file = JSON.stringify(buildProjectSpec(project), null, 2) // kind: "spunto/project"
const name = projectSpecFilename(project.name)                  // "my-app.spunto-project.json"
const spec = parseProjectSpec(await uploaded.text())            // throws a toast-safe message

Nothing in this package reads a database, opens a socket, or renders anything. It produces strings and parses strings. The I/O (running the script, forwarding the log, storing the file) stays with the product that owns the Docker socket and the database.

Edit on GitHub

On this page