Skip to content

Scaffolding

For anything bigger than a couple of files, don’t write one create() per file — keep a folder of template files inside your schematic package and mirror it with scaffold. It is the workhorse for multi-file generators.

schematics/service/
├── schema.json
├── schema.generated.ts
├── factory.ts
└── files/ # your template folder
├── __name@dasherize__.service.ts.template
├── __name@dasherize__.spec.ts.template
└── config/
└── settings.json
schematics/service/factory.ts
import { scaffold } from "@pbuilder/sdk/commons";
import type { Input } from "./schema.generated.ts";
export default (input: Input) => {
scaffold({
from: "files",
to: "src/services/__name@dasherize__",
options: { name: input.name },
});
};

With name: "userProfile" this emits src/services/user-profile/user-profile.service.ts, …/user-profile.spec.ts, and …/config/settings.json — contents rendered against the same options, using the template language.

from is package-local — resolved relative to your schematic’s folder, like copyIn and create({ templateFile }). The CLI passes the package location automatically; in tests you pass packageDir yourself (typically import.meta.dir).

File and folder names carry dynamic values through filename tokens:

  • __name__ becomes {= .name =} — the name option, rendered into the path.
  • __name@pipe__ becomes {= .name | pipe =} — same 7 pipes as templates, so __name@dasherize__.service.ts with name: "userProfile" lands as user-profile.service.ts.
  • The to destination is translated the same way — that’s how one scaffold call fans out into a per-option directory (to: "src/services/__name@dasherize__" above).

scaffold only rewrites the marker syntax; the rendering itself happens in the engine, exactly as for create templates.

Three more controls shape each entry’s destination, applied in a pinned order: rename first, token translation second, .template strip last.

A trailing .template is stripped from the destination name. Use it to keep template files from looking like real source to your editor and tooling: user.service.ts.templateuser.service.ts.

rename is a static remap table matched against the original source-relative path — for the odd file whose destination doesn’t follow the pattern. Because it runs first in the pinned order, the remapped name still goes through token translation and .template stripping afterwards.

include and exclude are glob filters over the original paths: * matches within a path segment, ** matches across segments, and exclude wins when both match a file.

Each surviving file is classified automatically: valid, in-budget text renders as a template; binary or over-budget files travel verbatim (as copyIn). You don’t opt files in or out of rendering by hand.

Like every verb that writes to a new path, scaffold is fail-closed on collision: if a destination file already exists, the run rejects and nothing is written. Pass force: true inside the args object for a deliberate overwrite.

  • Nested symlinked directories inside from are skipped silently; a symlinked from root rejects outright.
  • A single scaffold call caps at 10,000 entries.

Preview what a scaffold call is about to emit with dry-run, and see mutation verbs for the full edge-and-error semantics of scaffold and its siblings.