Maintenance: Templates and Extensions
August 23, 2026 · View on GitHub
How to inspect, fix, add, and update templates and extensions in
cna-templates.Read after the top-level MAINTENANCE_RUNBOOK.md.
1. Core concepts
These are defined in AGENTS.md and docs/ARCHITECTURE.md; the critical points are repeated here because they drive almost every maintenance decision.
1.1 Registry
templates.jsonis the single registry of templates, extensions, and categories.templates.schema.jsonvalidates the registry.- Every template/extension needs:
name,slug,description,url,type,category,labels. - Extensions may also have
incompatibleWithto declare mutually exclusive extensions.
1.2 Type system
- A template has one
typestring. - An extension has a
typestring or array of strings. - An extension is compatible with a template when
template.typeis in[ext.type].flat().
1.3 File conventions
| Convention | Behavior |
|---|---|
template/package.json | Static manifest inside template/ — all 10 templates use this. Legacy package/index.js is no longer used. |
cna.config.json | At templates/<slug>/cna.config.json (sibling to template/); defines customOptions prompts. |
*.template | Processed with EJS; output filename strips .template. |
*.append | Content is appended to the matching file in the project. |
*.if-pnpm | Only included when the user selects pnpm. |
[name]/ directory | Renamed to the value of the name custom option. |
1.4 EJS variables
Common variables available in .template files:
| Variable | Source |
|---|---|
<%= projectName %> | User input or --set projectName=... |
<%= srcDir %> | cna.config.json custom option |
<%= projectImportPath %> | cna.config.json custom option |
<%= scope %> | cna.config.json custom option (monorepo) |
<%= installCommand %> | CLI context |
<%= runCommand %> | CLI context |
1.5 Generation order
- Resolve template + extension URLs from
templates.json. - Copy static files from
template/(includingtemplate/package.json). - Process
.template,.append,.if-pnpmfiles. - Rename
[bracket]/directories based oncustomOptionsfromcna.config.json. - Use the static
template/package.jsonas the base manifest (legacypackage/index.jsis no longer used). - Merge extension files and dependencies on top.
- Run install and post-generation scripts.
2. How to inspect an existing template
cd repos/github.com/Create-Node-App/cna-templates
# List templates
ls templates/
# Read the registry entry
grep -A 15 '"slug": "nestjs-boilerplate"' templates.json
# Read the static manifest
cat templates/nestjs-starter/template/package.json
# Read custom options
cat templates/nestjs-starter/cna.config.json
Key questions:
- What
typedoes it have? - What
customOptionsdoes it define? - What scripts does
template/package.jsondeclare? - Are there
[bracket]/directories that depend on custom options?
3. How to inspect an existing extension
# List extensions
ls extensions/
# Read the registry entry
grep -A 15 '"slug": "storybook"' templates.json
# Read dependencies and scripts
cat extensions/storybook/package.json
# Read files it injects
ls -la extensions/storybook
Key questions:
- Which
types is it compatible with? - Does it inject a
.npmrc? (See dependency resolution.) - Does it have
.templatefiles needing EJS variables? - Does it have
.appendfiles that modify existing template files?
4. Fixing TypeScript / lint / build errors in generated projects
When a generated project fails type-check, lint, or build, the cause is usually in the template or an extension.
4.1 Reproduce locally
REPO=/absolute/path/to/cna-templates
CI=true npx create-awesome-node-app@latest my-app \
-t "file://$REPO?subdir=templates/<slug>" \
--addons "file://$REPO?subdir=extensions/<ext1>"
cd my-app
npm install
npm run lint
npm run type-check
SKIP_ENV_VALIDATION=true npm run build
4.2 Isolate the offending extension
Remove extensions one at a time until the project passes. Then fix the last removed extension.
4.3 Common fixes
| Symptom | Likely cause | Fix |
|---|---|---|
Property 'x' has no initializer | Class property not assigned in constructor under strict mode. | Initialize in lifecycle hook or use a getter with runtime guard. |
Argument of type 'string | undefined' | configService.get('VAR') may return undefined. | Add a fallback: configService.get('VAR') || 'default'. |
Cannot find module | Missing dependency or wrong peer dependency range. | Update the extension package.json. |
| ESLint flat config parser error | Parser not applied to .ts/.tsx. | Check eslint.config.mjs template. |
next build peer conflict | Storybook/other addon does not support current Next major. | Update addon OR pin Next OR use .npmrc with legacy-peer-deps. |
4.4 Case studies in this repo
- #153 —
nestjs-drizzle-sqliteprovider had an uninitializeddbproperty and implicit string types. Fixed by initializing inonModuleInitand adding string fallbacks. - #154 — Storybook 8 peer-required Next
^13\|\|14\|\|15, butnextjs-starteruses Next 16. Historic fix waslegacy-peer-deps=truein an extension.npmrc(current examples:extensions/react-hookstate/.npmrc,extensions/react-semantic-ui/.npmrc,extensions/nestjs-openapi/.npmrc).
5. Adding or modifying a template
5.1 Adding a template
- Create
templates/<directory>/. - Add
template/package.json(static manifest) andcna.config.jsonif interactive prompts are needed — the legacypackage/index.jsis no longer used. - Add source files under
template/(use.template/.append/[bracket]/as needed). - Meet the M1 maturity bar in §11 before merge (docs, DX, honest scripts, landing integrity).
- Add an entry to
templates.jsonundertemplates. - Ensure the entry point matches the directory structure.
- Run local validation against the new template.
- Confirm L1 (
ci-templates.yml) covers the template after merge.
5.2 Directory naming caveat
The directory name in templates/ and the slug in templates.json may differ. For example, nestjs-boilerplate (slug) lives in templates/nestjs-starter (directory). The CLI resolves via url, not slug. When generating locally with file://, use the directory name:
-t "file://$REPO?subdir=templates/nestjs-starter"
5.3 Modifying a template
- Re-scaffold the template alone (
scripts/ci/run-scaffold-check.js). - Apply the change.
- Validate lint/type-check/build/test.
- Re-scaffold with a curated profile from
ci/profiles/(or a one-per-category stack) — do not install all compatible extensions at once.
6. Adding or modifying an extension
6.1 Adding an extension
- Create
extensions/<slug>/. - Add a
package.jsonwith dependencies and scripts to merge. - Add files, templates, appends, or
.npmrcas needed. - Add the extension to
templates.jsonunderextensions. - Set
typeto match compatible template types. - Set
categoryso curated profiles and the CLI keep mutually exclusive choices clear. - Define
incompatibleWithif it cannot coexist with other extensions (keep symmetric). - Validate locally with each compatible canonical template (L2 isolation).
- Confirm weekly L2 picks up the extension after merge.
6.2 Modifying an extension
- Identify all templates compatible with the extension (
typematch). - Test the extension alone against its canonical template (
scripts/ci/run-scaffold-check.js --addon-url ...). - If composition matters, exercise a matching
ci/profiles/*.jsonprofile — never stack every compatible extension.
7. Handling incompatible extensions
When two extensions cannot be used together, declare it explicitly.
7.1 Via incompatibleWith
In templates.json, add incompatibleWith to both extensions:
{
"slug": "react-redux-saga",
"incompatibleWith": ["react-redux-thunk"]
}
The CI profile generator (scripts/ci/generate-matrix.js --layer profiles) never
selects two extensions that declare incompatibleWith each other. L2 isolation
jobs test one extension at a time.
7.2 Via .npmrc
If the incompatibility is only a peer-dependency resolution issue at install time, a .npmrc with legacy-peer-deps=true may be enough. Several extensions already do this:
extensions/react-hookstate/.npmrc
extensions/react-semantic-ui/.npmrc
extensions/react-semantic-ui-less/.npmrc
extensions/react-ionic-capacitor/.npmrc
extensions/nestjs-openapi/.npmrc
This is cheaper than incompatibleWith because it keeps both extensions available. Use it when the conflict is a semver-peer restriction, not a logical conflict.
7.3 Decision matrix
| Situation | Use |
|---|---|
| Extensions logically conflict (e.g., two Redux middleware choices) | incompatibleWith |
Peer dependency disagreement that resolves with legacy-peer-deps | .npmrc |
| Extension breaks a specific template but works elsewhere | Isolate the problem; consider template-specific branch or do not list the type match |
| Extension is obsolete | Remove from templates.json or archive |
8. Updating dependencies inside a template or extension
- Open the relevant
package.json(template/package.jsonfor templates). - Use
npm viewto find the latest compatible version. - Update the range conservatively (prefer caret minors, not arbitrary majors).
- Re-scaffold locally and run validation.
- If the update is security-related, also read MAINTENANCE_SECURITY.md.
See MAINTENANCE_DEPENDENCIES.md for deeper dependency troubleshooting.
9. Local validation command
Use this exact sequence after every template or extension change:
REPO=/absolute/path/to/cna-templates
CI=true npx create-awesome-node-app@latest my-app \
-t "file://$REPO?subdir=templates/<slug>" \
--addons "file://$REPO?subdir=extensions/<ext1>" \
"file://$REPO?subdir=extensions/<ext2>"
cd my-app
npm install
npm run format --if-present
npm run lint:fix --if-present
npm run lint --if-present
npm run type-check --if-present
SKIP_ENV_VALIDATION=true npm run build --if-present
If any step fails, fix the template or extension, then regenerate from scratch. Do not reuse my-app between attempts because files are merged, not reset.
10. Checklist
- Registry entry is valid against
templates.schema.json. -
typematches between template and compatible extensions. -
categoryis set to avoid random CI selecting duplicates. -
incompatibleWithis defined for mutually exclusive extensions. -
.npmrcis added if peer-dependency conflicts exist. -
.templatefiles use available EJS variables. - M1 maturity criteria in §11 are met for new templates. Existing thin starters may merge only with an uplift issue linked until they reach M1.
- Local validation passes.
- Full L1 template baseline is green for template changes.
- Changed extensions have a green L2 isolation job (or a tracked known break).
- Risky composition is covered by a curated L3 profile, not an all-extensions stack.
11. Template maturity (M1 / M2 / M3)
New templates must not ship as thin create-<framework> shells. Use this bar so every starter adds differential CNA value.
Gold references (M1/M2): templates/react-vite-starter, templates/nextjs-starter
Flagship ceiling (M3 — not required for new starters): templates/nextjs-saas-ai-starter
11.1 Maturity tiers
| Tier | Intent | Examples |
|---|---|---|
| M1 — Mature scaffold | Opinionated layout, full docs, real DX tooling, honest README, CNA first-run UX | react-vite-starter |
| M2 — Full-stack / domain baseline | M1 + sample feature, env validation, richer API/testing docs | nextjs-starter (+ Nest after polish) |
| M3 — Flagship product | Multi-domain product (auth, DB, tenancy, CI baked in) | nextjs-saas-ai-starter only |
11.2 M1 checklist (required for smoke-matrix starters)
A — CNA plumbing
- Registry entry complete (
name,slug,description,url,type,category,labels). -
template/package.json(static manifest) +cna.config.jsonif prompts are needed — legacypackage/index.jsis no longer used. -
.template/.append/[bracket]/used correctly; EJS vars only from the documented set. - Local validation (§9) passes: install → format → lint → type-check → build.
B — Docs suite
-
docs/README.mdindexes the suite. - Core docs present (adapt names for backend/test harnesses): structure, configuration, and domain guides as applicable.
-
README.md.template,CONTRIBUTING.md.template,AGENTS.mdorAGENTS.md.template. - Landing / README CTAs must not link to missing files (including after stripping
.template). See DEFAULT_LANDING_GUIDE.md.
C — Architecture
- Opinionated layout (e.g.
features/, Nest modules, RR7 routes, Astrosrc/pages+ layouts). - Feature / module scaffold (
_feature-template_,_module-template_, or documented equivalent).
D — DX tooling
- TypeScript strict + path alias when relevant.
- Real ESLint flat config (
lintmust not be anechostub). - Prettier +
.editorconfig+.node-version. - Core scripts:
dev(or harness equivalent),build,lint,lint:fix,type-check,format.
E — First-run UX
- UI templates: CNA default landing per DEFAULT_LANDING_GUIDE.md.
- API / non-UI templates: branded README + health (or equivalent) endpoint.
F — Env
-
.env.examplewith documented variables (even if empty of secrets). - Server templates should validate env (Zod, Nest Config, t3-env, etc.) at M2; M1 at least documents vars.
G — Honesty
- Every script listed in README exists in
template/package.json. - Tests either ship in-template or are clearly extension-only — never advertise fake
testscripts.
H — Extension seams
- Documented hooks for the template
type(providers, middleware handlers, global CSS, etc.).
11.3 Hard rules (CI / review blockers)
- No dead doc links from landing pages or README to paths that do not exist in the template tree (treat
FOO.md.templateas satisfyingFOO.md). - No stub lint/test scripts that always succeed or always fail without doing work.
- Do not require M3 for new starters. Prefer M1; add M2 when the stack benefits from a sample domain feature.
11.4 Related automation
- Soft dependency warnings:
scripts/check-dependencies.js - Registry validation:
scripts/validate-templates.js - Prefer adding CI integrity checks (dead landing doc links,
shared/assetsdrift) rather than relying on manual review alone.
Track uplift work under #290.