Authoring Templates and Extensions

March 10, 2026 · View on GitHub

Template Directory Layout

my-template/
├── package/
│   ├── index.js           # Required: exports a function that returns package.json
│   ├── dependencies.js    # module.exports = { "lib": "^1.0.0" }
│   └── devDependencies.js
├── [src]/                 # Renamed based on the `srcDir` customOption
│   └── App.tsx.template   # Processed with EJS
├── vite.config.ts.template
└── .gitignore             # Static — copied as-is

package/index.js

Exports a function that receives user context and returns the full package.json:

const dependencies = require("./dependencies");
const devDependencies = require("./devDependencies");

module.exports = function resolvePackage(setup, { appName, runCommand, usePnpm }) {
  return {
    name: appName,
    version: "0.1.0",
    scripts: { dev: "vite", build: "tsc && vite build" },
    dependencies,
    devDependencies,
  };
};

Available parameters: appName, runCommand, installCommand, usePnpm.

File Naming Conventions

SuffixBehavior
.templateProcessed with EJS, suffix stripped from output filename
.appendContent appended to the matching file already in the project
.if-pnpmIncluded only when the user selects pnpm, suffix stripped
[name]/Directory renamed to the value of the name customOption

EJS Variables

All .template files use <%= variableName %> syntax.

VariableDescriptionExample
<%= projectName %>Project name entered by the usermy-app
<%= srcDir %>Source directory (from customOption)src
<%= projectImportPath %>Import alias (from customOption)@/
<%= scope %>Package scope for monorepo@my-org/
<%= installCommand %>Full install commandnpm install
<%= runCommand %>Script run commandnpm run

Extension Layout

Extensions are simpler — they only add files and dependencies.

Most common pattern — a plain package.json with deps to merge:

{ "devDependencies": { "husky": "^9.0.0" } }

Everything else in the extension directory is copied into the project, respecting all file suffix conventions above.

customOptions — Interactive Prompts

Only templates can define these. They become EJS variables and control bracket directory renaming.

Define them in cna.config.json at the root of the template directory:

{
  "customOptions": [
    {
      "name": "srcDir",
      "type": "text",
      "message": "Source directory (e.g. `src`). Leave blank for root.",
      "initial": "src"
    }
  ]
}
FieldDescription
nameUsed as <%= name %> in templates and matches [name]/ dirs
typePrompt type ("text" is the standard)
messageQuestion shown in the CLI
initialDefault value (used automatically in non-interactive/CI mode)
requiredOptional. Defaults to true

cna.config.json is co-located with the template so it works with both slug resolution and file:// local URLs. Do not put customOptions in templates.json — it is no longer read from there.