Creación de plantillas y extensiones
August 23, 2026 · View on GitHub
Estructura del directorio de plantilla
my-template/
├── cna.config.json # customOptions (prompts interactivos)
├── template/
│ ├── package.json # manifiesto estático — las 10 plantillas usan esto
│ ├── [src]/ # Se renombra según la customOption `srcDir`
│ │ └── App.tsx.template # Se procesa con EJS
│ ├── vite.config.ts.template
│ └── .gitignore # Estático: se copia tal cual
template/package.json (manifiesto estático)
Todas las plantillas incluyen un template/package.json estático. La CLI lo
copia tal cual (tras procesar EJS) como manifiesto base; luego las extensiones
fusionan sus dependencias.
El package/index.js heredado ((setup, { appName, runCommand, usePnpm }) => packageJson
con package/dependencies.js / devDependencies.js) ya no se usa — ninguna
plantilla lo incluye actualmente. No añadas un directorio package/ junto a
template/package.json (Node resuelve …/package a package.json y el
resolver dinámico nunca se ejecutaría; ver docs/TESTING.md).
{
"name": "my-template",
"version": "0.1.0",
"scripts": { "dev": "vite", "build": "tsc && vite build" },
"dependencies": { "react": "^19.0.0" },
"devDependencies": { "vite": "^6.0.0" }
}
Convenciones de nombres de archivo
| Sufijo | Comportamiento |
|---|---|
.template | Se procesa con EJS y se elimina el sufijo del nombre de salida |
.append | El contenido se agrega al archivo equivalente que ya existe en el proyecto |
.if-pnpm | Solo se incluye cuando el usuario elige pnpm; se elimina el sufijo |
[name]/ | El directorio se renombra al valor de la customOption name |
Variables EJS
Todos los archivos .template usan la sintaxis <%= variableName %>.
| Variable | Descripción | Ejemplo |
|---|---|---|
<%= projectName %> | Nombre del proyecto que introduce el usuario | my-app |
<%= srcDir %> | Directorio de origen (desde customOption) | src |
<%= projectImportPath %> | Alias de importación (desde customOption) | @/ |
<%= scope %> | Alcance del paquete en un monorepo | @my-org/ |
<%= installCommand %> | Comando completo de instalación | npm install |
<%= runCommand %> | Comando para ejecutar scripts | npm run |
Estructura de extensiones
Las extensiones son más simples: solo agregan archivos y dependencias.
Patrón más habitual — un package.json sencillo con dependencias para fusionar:
{ "devDependencies": { "husky": "^9.0.0" } }
Todo lo demás en el directorio de la extensión se copia al proyecto, respetando las convenciones de sufijos descritas arriba.
customOptions — Prompts interactivos
Solo las plantillas pueden definirlos. Se convierten en variables EJS y controlan el renombrado de directorios entre corchetes.
Se definen en cna.config.json en templates/<slug>/cna.config.json (hermano de template/):
{
"customOptions": [
{
"name": "srcDir",
"type": "text",
"message": "Source directory (e.g. `src`). Leave blank for root.",
"initial": "src"
}
]
}
| Campo | Descripción |
|---|---|
name | Se usa como <%= name %> en las plantillas y coincide con los directorios [name]/ |
type | Tipo de prompt ("text" es el estándar) |
message | Pregunta que se muestra en la CLI |
initial | Valor por defecto (se usa automáticamente en modo no interactivo / CI) |
required | Opcional. El valor por defecto es true |
cna.config.jsonvive entemplates/<slug>/cna.config.json(hermano detemplate/) para funcionar tanto con resolución por slug como con URLs localesfile://. No pongascustomOptionsentemplates.json; ya no se leen desde ahí.
Madurez de plantilla
Antes de fusionar un starter nuevo o muy modificado, cumple el nivel M1 mature scaffold documentado en MAINTENANCE_TEMPLATES.md §11.
En resumen:
- Usa
template/package.jsonestático concna.config.jsonpara prompts (las 10 plantillas hacen esto). Elpackage/index.jsheredado ya no se usa — no introduzcas un directoriopackage/. - Incluye una suite real de
docs/; nunca enlaces landings o READMEs a docs que no existen. - Los scripts
lint/testdeben hacer trabajo real (u omitirse): nada de stubs conechoni scripts falsos en READMEs. - Usa
react-vite-starter/nextjs-startercomo referencia; no tratesnextjs-saas-ai-startercomo el alcance por defecto.