Crust logoCrust

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/create

Pair it with any CLI and prompt libraries needed by your create tool.

Quick example

src/index.ts
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 valueResolves fromUse for
new URL("../templates/base", import.meta.url)The module that contains the callTemplates shipped inside your generator package
"./templates/base"The user's current working directoryTemplates in the project where the command runs
"/srv/templates/base"That exact filesystem pathA 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

templates/base/package.json
{
  "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

StepBehavior
installDetect the package manager and install dependencies
git-initInitialize Git and optionally create a commit
open-editorOpen $EDITOR or VS Code; warn instead of failing when unavailable
commandRun 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.

On this page