Installation
Install @spunto/build, let your toolchain transpile its TypeScript source, and choose between subpath and root imports.
Install
npm install @spunto/buildIf you import @spunto/build/spec, also install zod v4 (see The optional zod peer
below):
npm install zod@^4It ships raw TypeScript
There is no dist/. Every entry in exports points straight at a .ts file under src/:
{
"type": "module",
"sideEffects": false,
"exports": {
".": { "types": "./src/index.ts", "import": "./src/index.ts" },
"./steps": { "types": "./src/steps/index.ts", "import": "./src/steps/index.ts" },
"./naming": { "types": "./src/naming/index.ts", "import": "./src/naming/index.ts" },
"./spec": { "types": "./src/spec/index.ts", "import": "./src/spec/index.ts" }
}
}The published tarball contains src (minus *.test.ts and snapshot fixtures) and the README.
That means your toolchain transpiles it, the same way it transpiles your own code. Plain
node will not run it; anything that already compiles TypeScript on the fly will.
Next.js does not compile files inside node_modules unless you tell it to. Add the package
to transpilePackages. Both Spunto Cloud's apps/next and Spunto Lite do exactly this, next to
the design system:
const nextConfig = {
transpilePackages: ["@spunto/design-system", "@spunto/build"],
}
export default nextConfigWhy source, not a build
Both consumers already compile TypeScript, so a build step in the package would only add an artifact
that can disagree with the source. Shipping source also keeps the types exact. Generic signatures,
as const tuples like WORKER_STATUSES and the zod-inferred ProjectSpec reach you as written,
not as a .d.ts approximation.
ESM only
The package is "type": "module" and its exports only declare an import condition. There is no
CommonJS build, so require("@spunto/build") will not resolve. Use import.
Subpath imports vs. the root import
Subpath imports are the normal way in:
import { workerContainerName } from "@spunto/build/naming"
import { BuildStepTracker } from "@spunto/build/steps"The root (@spunto/build) re-exports every module for callers that would rather have one import:
import { workerContainerName, BuildStepTracker } from "@spunto/build"The root re-exports ./spec, which imports zod. Importing from the root without zod installed
fails to resolve, even if you only wanted a name from ./naming. Prefer subpaths. They load only
what you use, and they keep the optional peer optional.
| Subpath | Needs zod |
|---|---|
@spunto/build/steps | no |
@spunto/build/extensions | no |
@spunto/build/naming | no |
@spunto/build/types | no |
@spunto/build/catalogs | no |
@spunto/build/script | no |
@spunto/build/terminal | no |
@spunto/build/agent-stream | no |
@spunto/build/spec | yes |
@spunto/build (root) | yes, because it re-exports ./spec |
The optional zod peer
{
"peerDependencies": { "zod": "^4" },
"peerDependenciesMeta": { "zod": { "optional": true } }
}zod is the package's only runtime dependency. Each part of that declaration has a reason:
- A dependency at all, because
./specdescribes a file format that two products have to validate identically. If each side re-derived the schema, that would be the exact duplication this package exists to remove. - A peer, so that the zod instance in use is the consumer's own. That matters because apps extend these schemas with their own.
- Optional, because a peer applies to the whole package while only
./specneeds it. Spunto Cloud's node agent imports./namingand nothing else, and it used to get zod installed for nothing.
Import ./spec without zod and module resolution fails loudly. That is the intended trade-off. It
is better than taxing every other entry point.
The schemas use zod 4 APIs (z.prettifyError, for instance), so zod 3 will not do.
Runtime and TypeScript expectations
The package declares no engines field. What it actually relies on is set in its own
tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
// No "dom.iterable", and above all no @types/node: this package must typecheck
// against nothing but the platform.
"lib": ["ES2022", "DOM"],
"module": "esnext",
"moduleResolution": "bundler",
"strict": true,
"isolatedModules": true,
"verbatimModuleSyntax": true
}
}- No Node APIs. The source typechecks without
@types/node. It uses only what every modern runtime has:fetch,URL,AbortSignal.timeout,TextEncoder/TextDecoder,console. For example,splitCompleteLinestakes aUint8Arrayrather than aBuffer(aBufferis one, so Node callers compile unchanged). In practice that means a recent Node (the publish workflow uses Node 24), Bun, Deno, or a browser bundle.parseProjectSpecdoes run in the browser in Spunto Cloud's import flow. - ES2022 syntax in the source. Your transpiler handles that, but your target runtime has to provide ES2022 built-ins.
- TypeScript 5 or later with
isolatedModules-compatible settings. The source usesimport type/export typethroughout (verbatimModuleSyntax), so it is safe for single-file transpilers such as esbuild and SWC. - Bundler-style resolution. Internal imports are extensionless (
./steps/index), which works undermoduleResolution: "bundler"and under the transpilers above.