Create
Headless scaffolding engine for building create-xx tools.
@crustjs/create copies template directories, interpolates variables, renames dotfiles, resolves conflicts, and runs post-scaffold steps. It does not provide prompts or terminal UI.
Install
npm install @crustjs/createPair it with any CLI and prompt libraries needed by your create tool.
Quick example
import { runSteps, scaffold } from "@crustjs/create";
await scaffold({
template: new URL("../templates/base", import.meta.url),
dest: "./my-project",
context: { name: "my-app" },
});
await runSteps([{ type: "install" }, { type: "git-init" }], "./my-project");
new URL("../templates/base", import.meta.url) points at the templates/base directory next to this module's parent folder, so the template stays with the generator wherever it is installed or run from. scaffold() returns every written path relative to the destination; the exact list depends on the files in that directory.
Locating templates
template value | Resolves from | Use for |
|---|---|---|
new URL("../templates/base", import.meta.url) | The module that contains the call | Templates shipped inside your generator package |
"./templates/base" | The user's current working directory | Templates in the project where the command runs |
"/srv/templates/base" | That exact filesystem path | A location already resolved by your own code or configuration |
A generator's templates belong to its package, but the package rarely runs from its own directory: npx, bun x, and global installs run it from a cache or install location while the user sits in their project. A relative string cannot express "next to my code" because scaffold() receives only the path text, not the file that wrote it. import.meta.url supplies that file, and new URL() resolves the path against it.
String paths behave like dest: they resolve from the working directory, which is right when the template lives in the user's project or was supplied by the user. Convert an existing filesystem path to a URL with pathToFileURL() rather than concatenating file://.
Template conventions
{
"name": "{{name}}",
"description": "{{description}}"
}Text files replace {{identifier}} placeholders from context; missing values remain unchanged. Files with a null byte in their first 8 KiB are copied without interpolation.
templates/base/_gitignore -> my-project/.gitignore
templates/base/__tests__/ -> my-project/__tests__/A single leading _ on a filename becomes ., while __ stays unchanged. Call scaffold() more than once with conflict: "overwrite" to layer templates.
scaffold(options)
function scaffold(options: ScaffoldOptions): Promise<ScaffoldResult>;Prop
Type
Prop
Type
See Locating templates for how template resolves. Resolution throws when the URL scheme is not file: or the resolved template is not a directory.
The default conflict: "abort" throws for a non-empty destination. "overwrite" replaces conflicting files and leaves unrelated destination files in place.
Writes stay inside the destination. dest itself may be a symlink (its target is the destination), but an existing destination file or ancestor directory that is a symlink resolving outside that directory, or to a missing target, throws before any file is written. Symlinks that stay inside the destination are followed.
runSteps(steps, cwd)
await runSteps(
[{ type: "install" }, { type: "git-init", commit: "Initial commit" }],
"./my-project",
);Steps run in order and stop at the first error.
Prop
Type
| Step | Behavior |
|---|---|
install | Detect the package manager and install dependencies |
git-init | Initialize Git and optionally create a commit |
open-editor | Open $EDITOR or VS Code; warn instead of failing when unavailable |
command | Run cmd through the platform shell, optionally in a different cwd |
Utilities
interpolate("Hello, {{name}}!", { name: "world" }); // => "Hello, world!"interpolate() replaces placeholders in a string. detectPackageManager(cwd?) checks lockfiles, then npm_config_user_agent, and defaults to npm; isInGitRepo(cwd?) reports whether the path is inside a Git work tree.
Exports
import {
detectPackageManager,
interpolate,
isInGitRepo,
runSteps,
scaffold,
} from "@crustjs/create";
import type {
PackageManager,
PostScaffoldStep,
ScaffoldOptions,
ScaffoldResult,
} from "@crustjs/create";The package root exports only the listed functions and types.