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.
Mirroring a template folder
Section titled “Mirroring a template folder”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.jsonimport { 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).
Dynamic file names
Section titled “Dynamic file names”File and folder names carry dynamic values through filename tokens:
__name__becomes{= .name =}— thenameoption, rendered into the path.__name@pipe__becomes{= .name | pipe =}— same 7 pipes as templates, so__name@dasherize__.service.tswithname: "userProfile"lands asuser-profile.service.ts.- The
todestination is translated the same way — that’s how onescaffoldcall 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.
Per-entry controls
Section titled “Per-entry controls”Three more controls shape each entry’s destination, applied in a pinned order: rename
first, token translation second, .template strip last.
The .template suffix
Section titled “The .template suffix”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.template → user.service.ts.
rename tables
Section titled “rename tables”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 / exclude glob filters
Section titled “include / exclude glob filters”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.
Filters that eliminate every entry reject loudly — but an empty from folder is a silent
no-op. And npm packaging commonly drops empty directories, so if a folder’s presence matters,
ship a placeholder file in it.
How each file travels
Section titled “How each file travels”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.
Collisions and force
Section titled “Collisions and force”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.
Edge cases
Section titled “Edge cases”- Nested symlinked directories inside
fromare skipped silently; a symlinkedfromroot rejects outright. - A single
scaffoldcall caps at 10,000 entries.
Next steps
Section titled “Next steps”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.