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

SufijoComportamiento
.templateSe procesa con EJS y se elimina el sufijo del nombre de salida
.appendEl contenido se agrega al archivo equivalente que ya existe en el proyecto
.if-pnpmSolo 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 %>.

VariableDescripciónEjemplo
<%= projectName %>Nombre del proyecto que introduce el usuariomy-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ónnpm install
<%= runCommand %>Comando para ejecutar scriptsnpm 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"
    }
  ]
}
CampoDescripción
nameSe usa como <%= name %> en las plantillas y coincide con los directorios [name]/
typeTipo de prompt ("text" es el estándar)
messagePregunta que se muestra en la CLI
initialValor por defecto (se usa automáticamente en modo no interactivo / CI)
requiredOpcional. El valor por defecto es true

cna.config.json vive en templates/<slug>/cna.config.json (hermano de template/) para funcionar tanto con resolución por slug como con URLs locales file://. No pongas customOptions en templates.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.json estático con cna.config.json para prompts (las 10 plantillas hacen esto). El package/index.js heredado ya no se usa — no introduzcas un directorio package/.
  • Incluye una suite real de docs/; nunca enlaces landings o READMEs a docs que no existen.
  • Los scripts lint / test deben hacer trabajo real (u omitirse): nada de stubs con echo ni scripts falsos en READMEs.
  • Usa react-vite-starter / nextjs-starter como referencia; no trates nextjs-saas-ai-starter como el alcance por defecto.