Skip to content

Type generation

Every schematic keeps an Input type in schema.generated.ts, derived from its schema.json. pbuilder-codegen — a binary shipped inside @pbuilder/sdk — writes that file. When you change schemas across several collections, one run regenerates all of them from what project-builder.json registers; there is no second list of directories to keep in sync.

pbuilder-codegen is a Node.js script, so any package runner works: npx, pnpm exec, yarn, or bunx. The examples use bunx, like the rest of these docs.

You want to regenerate… Run What it selects
Every registered schematic in the project you are in bunx pbuilder-codegen The nearest project-builder.json, walking up from the current directory
Every registered schematic in a specific project bunx pbuilder-codegen --project <directory> Exactly that directory’s project-builder.json, from any working directory — no upward search
One schematic bunx pbuilder-codegen <schematic-directory> Only that directory

Use the project modes after editing several schemas, in CI, or as your generate:types script. Use the single-directory mode while iterating on one schematic — it is what Your first schematic teaches, and what builder new schematic runs for you automatically.

Project modes process all collections, not only default.

A workspace registers one schematic by directory and a second collection through a manifest whose factory is a compiled module:

workspace/
├── package.json
├── project-builder.json
└── schematics/
├── collection.json
├── hello/
│ ├── factory.ts
│ └── schema.json
└── dist/
├── widget.js
└── schema.json
project-builder.json
{
"collections": {
"local": { "hello": { "path": "./schematics/hello" } },
"shared": { "path": "./schematics/collection.json" }
}
}
schematics/collection.json
{
"schematics": {
"widget": { "factory": "./dist/widget.js#createWidget" }
}
}
schematics/hello/schema.json
{
"properties": {
"name": { "type": "string", "label": "Service name", "required": true }
}
}
schematics/dist/schema.json
{
"properties": {
"label": { "type": "string", "label": "Widget label", "required": true }
}
}
  1. Run codegen from anywhere inside the workspace — here, from a schematic’s own folder:

    Terminal window
    cd schematics/hello
    bunx pbuilder-codegen
    pbuilder-codegen: generated 2, failed 0, duplicates 0

    The exit status is 0.

  2. Check the outputs. Each schema gets a schema.generated.ts beside it — for the manifest entry, beside the compiled dist/widget.js that the pointer names:

    schematics/
    ├── dist/
    │ ├── schema.generated.ts ← new
    │ ├── schema.json
    │ └── widget.js
    └── hello/
    ├── factory.ts
    ├── schema.generated.ts ← new
    └── schema.json
    schematics/dist/schema.generated.ts
    // AUTO-GENERATED by pbuilder-codegen — do not edit. Regenerate: pbuilder-codegen <package-dir>
    // @schema-digest sha256:…
    export type Input = {
    /** Widget label */
    label: string;
    };
  3. Or target the project from outside it, for example from a monorepo root or a CI script:

    Terminal window
    bunx pbuilder-codegen --project ./workspace

    The result is the same.

  • Without arguments, the search walks up from the current directory, and the first project-builder.json found owns the run. If that file is malformed, unreadable, a directory, or a dangling link, the run fails — it never falls back to a valid file further up.
  • With --project <directory>, exactly that directory is used. Pass a directory, not the path to the configuration file.
  • A missing or malformed configuration fails before anything is generated. A configuration with no collections, or an empty one, succeeds with zero work.
  • The selected project’s own directory is the write boundary. If project-builder.json is a symbolic link, its target’s directory does not gain write permission.

Where the SDK is installed does not matter for selection: the working directory and --project decide which project runs.

Two registration forms have files to generate:

Registration Example Rules
Schematic directory "hello": { "path": "./schematics/hello" } The directory must contain exactly one regular factory.ts or factory.js — neither or both is an error. The schema is schema.json in that directory.
Collection manifest "shared": { "path": "./schematics/collection.json" } Each factory pointer needs an explicit export: module#export, split at the last #, with a non-empty module and an identifier-shaped export (default is valid). The module path is relative to the manifest and must be a real file. The schema is the schema.json beside that module.

Both path values resolve against the selected project’s directory; absolute paths are accepted as long as the result stays inside the project. Factories are never imported or run — codegen only reads files. JSON files with a leading byte-order mark are accepted.

Not supported:

  • Inline registrations — schematics embedded in project-builder.json, such as those created by builder new schematic --inline — have no file to write. Each one counts as a failure. A name registered both inline and as a directory also fails.
  • Package lookup, extension inference, inheritance, URL-encoded paths, and schematics that are on disk but not registered. Codegen does not scan folders.

Every project run ends with one summary line on stdout:

pbuilder-codegen: generated G, failed F, duplicates D

Warnings about individual entries go to stderr. The exit status is 1 if any entry failed, and 0 otherwise — including a run with zero work.

A run keeps going after an entry fails. In this example, drafts/broken has a property without a label, and legacy/broken-copy registers the same directory again:

project-builder.json
{
"collections": {
"local": { "hello": { "path": "./schematics/hello" } },
"drafts": { "broken": { "path": "./schematics/broken" } },
"legacy": { "broken-copy": { "path": "./schematics/broken" } }
}
}
$ bunx pbuilder-codegen
pbuilder-codegen: "drafts/broken" ("<workspace>/schematics/broken"): "pbuilder-codegen: <workspace>/schematics/broken/schema.json: property \"title\" is missing a label"
pbuilder-codegen: generated 1, failed 1, duplicates 1
$ echo $?
1

How entries are counted:

  • Duplicates — registrations that resolve to a destination already attempted in this run count once in duplicates, even when the first attempt failed; they do not add another failure. Aliases through directory symlinks are recognised as the same destination. Distinct hard-linked files are not.
  • Failures — each unresolvable, invalid or refused registration counts on its own.
  • Not a transaction — files generated successfully stay written when another entry fails. An entry that fails validation keeps its previous schema.generated.ts.

Fix the reported schema or registration and run again. Never edit schema.generated.ts by hand.

Let the exit status fail the job. Files on disk are not proof that the whole run succeeded:

Terminal window
bunx pbuilder-codegen

If you pipe the output, keep the status — for example with set -o pipefail in Bash. To also catch schemas changed without regenerating, check for differences afterwards:

Terminal window
bunx pbuilder-codegen
git diff --exit-code -- '*schema.generated.ts'

With sdk.root, node_modules/@pbuilder/sdk is a link the CLI created, not a package your package manager installed — so there is no node_modules/.bin/pbuilder-codegen shim. Package runners then misbehave in one of two ways:

  • bunx pbuilder-codegen and npx pbuilder-codegen report the binary as missing, even though the SDK is present and builder execute works.
  • If an SDK is also installed globally, npx can run that global copy without telling you — possibly a different version from the one sdk.root names.
  • Recent pnpm versions run an install before pnpm exec. That makes the command work, but it writes a pnpm-lock.yaml and a shim into your project — the kind of change sdk.root exists to avoid.

The generate:types script that builder init adds calls pbuilder-codegen by name, so it is affected too.

Run the script directly with Node instead. It accepts the same forms — no argument, --project <directory>, or a schematic directory:

Terminal window
node node_modules/@pbuilder/sdk/dist/bin/pbuilder-codegen.js
node node_modules/@pbuilder/sdk/dist/bin/pbuilder-codegen.js schematics/hello

The link exists once a committing builder execute has run. Before that, use the configured root itself: node <sdk.root>/dist/bin/pbuilder-codegen.js.

The same applies when the link was created from --sdk-root or BUILDER_SDK_ROOT instead of sdk.root. Before the link exists, run the script from that value: node $BUILDER_SDK_ROOT/dist/bin/pbuilder-codegen.js.

builder new schematic needs none of this with sdk.root: it resolves the configured root on its own and regenerates the new schematic’s types. It does not read BUILDER_SDK_ROOT: when the variable is your only SDK source, it warns that @pbuilder/sdk was not found and skips schema.generated.ts — run the script as above.

Codegen only writes where the run is allowed to:

Mode Write boundary
Project (pbuilder-codegen, --project) The selected project’s directory
Single directory (pbuilder-codegen <dir>) The nearest ancestor of the working directory that has a package.json, or the working directory itself if there is none

Both the schematic directory and the output file — after resolving symbolic links — must stay inside that boundary. A destination outside it is not written: in a project run it is reported as refusing to write outside the selected project root and counts as a failure. Fix the registration or run codegen from the project that owns the schematic.

On Linux and macOS, schema.generated.ts must be either absent or a regular file. An output that is a symbolic link is refused — even if its target is inside the project — and so is any other non-regular entry:

$ bunx pbuilder-codegen schematics/hello
pbuilder-codegen: schematics/hello/schema.generated.ts: refusing symbolic-link output

In a project run the same entry reports cannot write a regular, non-symbolic-link output. builder new schematic surfaces it as new_codegen_failed. Reinstalling the SDK does not help — the file layout is the cause.

To migrate:

  1. Confirm the entry is a link and note where it points:

    Terminal window
    ls -l schematics/hello/schema.generated.ts
  2. Remove only the link. Leave its target alone:

    Terminal window
    rm schematics/hello/schema.generated.ts
  3. Regenerate an ordinary file in place:

    Terminal window
    bunx pbuilder-codegen schematics/hello

Directory links are different: a schematic directory reached through a symbolic link keeps working, as long as the resolved directory and its output stay inside the write boundary. Creating a missing output and replacing an existing regular one — including with shorter content — work as always.