Skip to content

Actions guide

Actions transform your files after the repo is scaffolded. Each is a function you import from the package and add to your config’s actions array — so every action and its options are fully typed.

bluprint.config.ts
import {
defineConfig,
prompt,
render,
execute,
} from '@reuters-graphics/bluprint';
export default defineConfig({
name: 'My bluprint',
files: ['**/*'],
ignores: [],
actions: [
prompt({ name: 'name', type: 'text', message: 'Project name?' }), // runs first
render({ files: ['README.md'] }), // then this
execute('pnpm install'), // then this
],
});

Actions share a context object that flows through the run. It starts with some defaults and grows as actions (like prompt) contribute to it:

Key Value
year / month / day Date parts at run time ("2026", "07", "02")
dirname Basename of the directory being scaffolded
bluprintPart The selected part, if any

A prompt action adds its answer under its name, and later actions can read it — in templates ({{ name }}) or via the when gate below. To make those custom values type-checked (instead of unknown) in when / run / editor callbacks, declare them with defineConfig<Context> — see Typing the run context.

Every action takes an optional when predicate. Return false and the action is skipped. It receives the current context:

prompt({ name: 'useTypeScript', type: 'confirm', message: 'Use TypeScript?' }),
render({
files: ['tsconfig.json'],
when: (ctx) => ctx.useTypeScript === true,
}),

Gate on the selected part with ctx.bluprintPart:

execute('pnpm install', { when: (ctx) => ctx.bluprintPart === 'app' }),

If an action throws, it’s logged and skipped so the rest of the run continues. Set failOnError: true to abort the whole run instead:

execute('pnpm build', { failOnError: true }),

render, log, json values, and destination paths for copy/move are rendered with mustache against the context, with these string helpers available: camelize, capitalize, dasherize, humanize, latinise, slugify, titleCase, underscore.

The helpers are mustache lambda sections, so wrap the value in a section (note the #): {{# helper }}…{{ /helper }}.

# {{# titleCase }}{{ name }}{{ /titleCase }}
Created {{ year }} in {{ dirname }}.

For example, a templated copy destination: src/{{# slugify }}{{ name }}{{ /slugify }}.ts.

render also supports EJS via engine: 'ejs'.

Ask the user a question; the answer is stored in the context under name. The spec is a discriminated union on typetext, confirm, select, multiselect, or datetime:

prompt({ name: 'name', type: 'text', message: 'Project name?' }),
prompt({ name: 'analytics', type: 'confirm', message: 'Add analytics?' }),
prompt({
name: 'kind',
type: 'select',
message: 'What kind of project?',
options: [
{ value: 'blog', label: 'Blog' },
{ value: 'scraper', label: 'Data scraper' },
],
}),

Render template files in place against the context (plus any extra context you pass). Defaults to the mustache engine.

render({ files: ['README.md', 'package.json'] }),
render({ files: ['index.html'], engine: 'ejs', context: { analytics: true } }),

Copy or move files and directories. Take a single [from, to] pair or an array of them; the destination is rendered as a mustache template.

copy(['templates/component.tsx', 'src/{{ name }}.tsx']),
move([
['gitignore', '.gitignore'],
['README.template.md', 'README.md'],
]),

Delete files and directories matching one or more globs.

remove(['**/*.test.ts', 'internal', 'TODO.md']),

Replace content in files with regular expressions. Each replacement is [pattern, replacement, flags?] (flags default to gm); the replacement is mustache-rendered.

regexreplace({
files: ['README.md'],
replace: [
['^# .*$', '# {{ name }}'],
['\\d{4}', '{{ year }}', 'g'],
],
}),

Run a shell command in the project. Pass a string (run via the shell) or a [command, ...args] array (run without one). By default the command’s output streams to the terminal; with silent, output is hidden and a spinner is shown instead.

execute('pnpm install', { silent: true }),
execute(['git', 'init']),

Edit a JSON file. The editor receives the parsed data and the context and must return the data to write (mutate then return, or return a new value):

json('package.json', (pkg, ctx) => {
pkg.name = ctx.name;
pkg.private = true;
return pkg;
}),

Pass a type argument for typed editing: json<PackageJson>('package.json', …).

The json analog for YAML files (e.g. CI workflows). Same must-return editor.

yaml('.github/workflows/ci.yml', (workflow, ctx) => {
workflow.name = ctx.name;
return workflow;
}),

Add (templated) content to a file, creating it if missing:

append('.gitignore', 'dist\n.env\n'),
prepend('src/index.ts', '// @generated for {{ name }}\n'),

Print a message. Rendered with mustache, then styled with chalk-template color tags:

log('Scaffolded {green {{ name }}}! Run {yellow pnpm dev} to start.'),

The escape hatch: run any function. It receives the context and may return a partial context to merge in for later actions. Async is supported.

run(async (ctx) => {
const res = await fetch(`https://example.com/${ctx.name}`);
return { headline: (await res.json()).title }; // available to later actions
}),

Under a typed config, ctx and the returned object are typed: declare the keys run contributes on your Context, and later actions read them type-safely.

interface Context {
name: string;
headline: string; // contributed by the run action
}
// inside defineConfig<Context>({ actions: [ … ] })
run(async (ctx): Promise<Partial<Context>> => {
const res = await fetch(`https://example.com/${ctx.name}`);
return { headline: (await res.json()).title };
}),