This guide describes how HonestJS CLI scaffolds projects from templates. Use it to add a new template to this repo or to build your own template layout.
Each template lives in a directory under templates/ (e.g.
templates/barebone). The directory must contain:
| Item | Required | Description |
|---|---|---|
| template.json | Yes | Metadata: name, description, version, author, optional variables, optional runtimes, etc. |
| files/ | Yes | Directory whose contents are copied into the new project. Everything under files/ is copied except paths containing node_modules or .git. |
Optional template.json keys:
| Key | Description |
|---|---|
| runtimes | Array of supported runtimes, e.g. ["bun"] or ["bun", "node"]. When absent, all runtimes are assumed. Used by the CLI to restrict package manager choice and to show compatibility in honestjs list. Built-in templates are Bun-only (["bun"]). External templates may support other runtimes. The template directory’s template.json is the source of truth; templates.json can override when needed. |
Optional files:
| Item | Description |
|---|---|
| prompts.js | Exports a prompts array (e.g. prompts format). The CLI runs these after the user picks a template and before general options (package manager, TypeScript, ESLint, etc.). Answers are merged into the project config and passed to transforms and variable substitution. |
| transforms.js | Exports a transforms object: keys are file paths or glob patterns, values are transform functions. See Transforms below. |
Registry: The repo root must have templates.json that lists each template
with at least name, description, and path (e.g. templates/barebone).
Common scripts and devDependencies are defined in shared/package/ as
JavaScript modules and merged into every scaffolded project when the CLI
applies project config (with context).
- shared/package/scripts.js (or
.mjs) – Export a default function(context) => Record<string, string>. The CLI calls it with the project config (eslint, prettier, docker, etc.) and merges the returned scripts. Built-in templates use Bun-only scripts (no{{pm}}/{{pmExec}}placeholders). - shared/package/devDependencies.js (or
.mjs) – Export a default function(context) => Record<string, string>. The CLI calls it with the project config and merges the returned devDependencies. Built-in templates use Bun-only deps (@types/bun; no tsx/tsup). - shared/package/dependencies.js (or
.mjs) – Export a default function(context) => Record<string, string>. The CLI calls it with the project config and merges the returned runtime dependencies (e.g. honestjs, hono).
After copying a template’s files/, the CLI merges shared scripts and
devDependencies into the project’s package.json during project configuration:
template-specific keys override shared. Your template’s package.json can
be minimal (name, dependencies, empty or partial scripts/devDependencies).
After copying files/ into the project, the CLI replaces placeholders {{key}}
in files with extension .json, .md, .js, or .ts.
- Substitution map: Built from
template.json’svariables(template defaults) and the project config (user choices and prompt answers). For overlapping keys, config wins over template variables. - Values: Only primitive values (string, number, boolean) are used.
nullandundefinedbecome an empty string. Nested objects are skipped so you don’t get[object Object]in files. - Example: If
template.jsonhas"variables": { "apiPrefix": "/v1" }and the user’s project name ismy-api, then{{apiPrefix}}and{{projectName}}in your template files become/v1andmy-api(project name comes from config).
You can use any config key as a placeholder, e.g. {{name}},
{{packageManager}}, {{template}}, {{testing}}, {{eslint}}, plus any keys
you define in variables or collect via prompts.js.
If transforms.js exists, the CLI loads it and runs a transform for each
project file that matches a key in the exported transforms object.
Each transform is a function:
(content, config) => string | null | { source: string } | Promise<...>content: Current file content (string).config: Project config (name, template, packageManager, typescript, eslint, and any prompt answers liketesting,frontend).
Return value:
string– Replace the file content with this string.null– Delete the file (e.g. remove test files when the user opted out of testing).{ source: string }– Replace the file with the file atsource(path relative to the templates root, or absolute). Used to pull in another template file.
Transforms may return a Promise of the above (e.g. for async I/O); the CLI awaits them.
- Exact path: Key is a relative path from the project root, e.g.
'package.json','README.md'. The path must use forward slashes when matching on Windows. - Glob: Key contains
*or?and is matched with minimatch, e.g.'src/**/*.test.ts','src/**/*.spec.ts'. Use globs to apply the same transform to many files (e.g. delete all test files whenconfig.testingis false).
The CLI tries an exact match first, then iterates glob keys. The first matching key wins.
You can reuse common transform logic from shared/transforms/base.js, which handles:
- Setting
package.jsonname and optionally removing test scripts whenconfig.testingis false. - Replacing
{{projectName}}and{{packageManager}}in README.md. - Deleting
src/**/*.test.tsandsrc/**/*.spec.tswhen testing is disabled.
Compose it with your template-specific transforms:
import { baseTransforms } from "../../shared/transforms/base.js";
export const transforms = {
...baseTransforms,
// your overrides or extra keys
};Template-specific keys override base (same key wins). If you don’t need any
extra transforms, export only the spread of baseTransforms.
- Copy
files/into the project. - Compose package.json with shared base scripts and devDependencies (template overrides shared).
- Apply variable substitution (all
{{key}}in .json, .md, .js, .ts). - Run transforms (each matching file is read, the transform is called, and
the file is updated, removed, or replaced from
source). - Apply project configuration (package.json name and scripts, README placeholders, git init, install if requested).
- Copy shared configs (see below).
Optional tooling files (ESLint, Prettier, TypeScript, Docker, .gitignore, etc.)
are not stored inside each template’s files/. They live in shared/configs/
and are copied into the project only when the user has enabled the corresponding
option.
- Manifest:
shared/configs/manifest.jsonlists entries{ "file": "<filename>", "condition": "<configKey>" }. For example"condition": "eslint"means “copy this file only whenconfig.eslintis truthy.” Use"condition": truefor files that are always copied. - Config keys match the CLI’s project config:
eslint,prettier,typescript,docker,git, etc. These are set by the user duringhonestjs new(or defaults with--yes). Adding a new shared config (e.g. a new tool) only requires adding an entry toshared/configs/manifest.jsonand placing the file inshared/configs/; no CLI code change is needed.
- Put your template files in files/; the whole tree is copied (excluding
node_modulesand.git). - package.json in your template can be minimal (name, module, type, and template-specific dependencies); scripts, devDependencies, and shared dependencies are merged from shared/package/*.js (template overrides shared).
- Use template.json for metadata and optional variables for default placeholders.
- Use {{key}} in .json, .md, .js, .ts; keys come from config and template variables (config wins).
- Use prompts.js to ask template-specific questions; answers go into config and placeholders/transforms.
- Use transforms.js to change or remove files; spread
shared/transforms/base.js and add overrides, or return string, null, or
{ source }(sync or async). - Shared configs in shared/configs/ are added based on manifest.json and user options (eslint, prettier, docker, etc.).
For examples, see the existing templates in this repo (e.g.
templates/barebone).