Jhony Vega
Posted on December 11, 2021
En este pequeño tutorial crearemos un CLI el cual nos permita crear proyectos en la ruta donde estemos.
Para realizar esto usaremos una base de plantillas y un archivo de configuración.
Una de las cosas interesantes es que usaremos React para definir opciones mas dinámicas y para ellos nos estaremos apoyando de la librería React Ink. Comencemos! 😁
Configurando el proyecto
Primero se instalará las siguientes dependencias.
# dependencias
$ yarn add ink ink-select-input ink-spinner ink-text-input react yaml fs-extra @babel/runtime
# dependencias de desarrollo
$ yarn add @babel/cli @babel/core @babel/node @babel/preset-env @babel/preset-react @babel/plugin-transform-runtime babel-loader nodemon --dev
Una vez instalado, añadimos en el archivo package.json los siguientes scripts, para poder usar en desarrollo y para generar nuestro código listo para producción.
{
"scripts": {
"build": "babel src -d dist",
"dev": "nodemon --no-stdin --exec babel-node src/index.js",
"start": "node ./dist/index.js"
}
}
Y ahora que sigue? Creamos un archivo .babelrc donde solo añadiremos la configuración de los presets y plugins necesarios.
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": ["@babel/plugin-transform-runtime"]
}
Estructurando los archivos
La estructura final quedaría de la siguiente forma la cual veremos para que sirve cada uno de los archivos.
src
Aquí irá nuestro código para crear el CLI 👋.
templates.generator.yaml
Archivo de configuración para definir nuestros proyectos que podremos generar. Como se puede ver en la imagen también existe una carpeta templates.generator la cual contiene el mismo nombre que el archivo yaml. Aquí se encontrará nuestros proyectos base. Por ejemplo:
version: 1.0
templates:
- name: angular project
path: /angular
- name: react project
path: /react
- name: vue project
path: /vue
Aquí tendríamos una lista de plantillas, cada uno con su nombre y la ruta donde se encuentra, no es necesario añadir la carpeta de templates.generator ya que automaticamente lo detectaría.
Dentro de la carpeta se tendría la siguiente estructura:
templates.generator
├── angular
├── react
└── vue
Creando el CLI
Creando las constantes necesarios
Usaremos 4 constantes principales:
-
currentDirectory
: para ubicarnos en el directorio actual. -
templateDirectory
: Directorio donde se tendrá las plantillas. -
templateName
: Nombre del archivo de configuración. -
STEPS
: Pasos que se irán mostrando en el CLI.
//src/constants.js
export const currentDirectory = process.cwd();
export const templateDirectory = "templates.generator"
export const templateName = `${templateDirectory}.yaml`
export const STEPS = {
"NAME" : 1,
"SELECT" : 2,
"LOADING" : 3,
"END" : 4
}
Definiendo funciones principales
Usaremos 3 funciones principales, para obtener el archivo de configuración YAML como json, formatear el json con rutas absolutas y la última para copiar una carpeta o archivo a otro directorio.
//src/utils.js
import { currentDirectory, templateDirectory, templateName } from "./constants";
import fs from "fs";
import Yaml from "yaml";
import path from "path";
import fsExtra from "fs-extra"
export async function getTemplateGenerator() {
const file = fs.readFileSync(
path.join(currentDirectory, templateName),
"utf8"
);
const parseFile = Yaml.parse(file);
return formatPathsInTemplate(parseFile);
}
export function formatPathsInTemplate(json) {
const generator = { ...json };
generator.templates = generator.templates.map((template) => {
return {
...template,
path: path.join(currentDirectory,templateDirectory, template.path),
};
});
return generator.templates;
}
export function copyTemplateToCurrentDirectory({from,to}) {
return fsExtra.copy(from,path.join(currentDirectory,to))
}
Creando al archivo principal
Por el momento solo crearemos un simple mensaje para poder ver su uso.
//src/index.js
import React from "react";
import { render, Box, Text } from "ink";
const App = () => {
return(
<Box>
<Text>Hello world</Text>
</Box>
)
}
render(<App/>)
Si ahora ejecutamos el script yarn dev
verémos en consola lo siguiente:
$ Hello world
Definiendo el state
Creamos un estado inicial para los siguientes casos: el paso en el que se encuentra, la lista de plantillas y el directorio en donde se creará el proyecto.
//src/core/state.js
import { STEPS } from "../constants";
export const state = {
step : STEPS.NAME,
templates: [],
directory: '.'
}
Añadiendo el reducer
//src/core/reducer.js
export const ACTIONS = {
SET_TEMPLATES: "SET_TEMPLATES",
SET_STEP: "SET_STEP",
SET_NAME_DIRECTORY: "SET_NAME_DIRECTORY",
};
export function reducer(state, action) {
switch (action.type) {
case ACTIONS.SET_TEMPLATES:
return {
...state,
templates: action.payload,
};
case ACTIONS.SET_STEP:
return {
...state,
step: action.payload,
};
case ACTIONS.SET_NAME_DIRECTORY:
return {
...state,
directory: action.payload
}
default:
return state;
}
}
Creando el hook useGenerator
Y ahora creamos el hook en el cuál estaremos encapsulando la lógica necesaria para generar proyectos, leer la lista de opciones que tenemos del archivo YAML y movernos a los siguientes o anteriores pasos.
//src/useGenerator.js
import { useReducer } from "react";
import { STEPS } from "./constants";
import { ACTIONS, reducer } from "./core/reducer";
import { state as initialState } from "./core/state";
import { copyTemplateToCurrentDirectory } from "./utils";
export default function useGenerator() {
const [state, dispatch] = useReducer(reducer, initialState);
const setDirectory = (payload) => {
dispatch({
type: ACTIONS.SET_NAME_DIRECTORY,
payload,
});
};
const setStep = (payload) => {
dispatch({
type: ACTIONS.SET_STEP,
payload,
});
};
const setTemplates = (payload) => {
dispatch({
type: ACTIONS.SET_TEMPLATES,
payload,
});
};
const onSelectTemplate = async ({value}) => {
try {
setStep(STEPS.LOADING);
await copyTemplateToCurrentDirectory({
from: value,
to: state.directory,
});
setStep(STEPS.END);
process.exit();
} catch (error) {
console.log(error.message);
}
}
const onCompleteTypingDirectory = () => {
setStep(STEPS.SELECT);
}
return {
onSelectTemplate,
onCompleteTypingDirectory,
state,
setTemplates,
setDirectory,
setStep,
dispatch
};
}
Redefiniendo el componente principal
Es momento de actualizar el archivo donde se encontraba nuestro componente añadiendo los pasos y nuevos componentes creados con esta librería. Nos apoyaremos de 3 principales:
Importando lo necesario
Inicialmente importaremos todo lo que usaremos para crear el CLI.
//src/index.js
import React, { useEffect, useMemo } from "react";
import { render, Box, Text } from "ink";
import Select from "ink-select-input";
import Loading from "ink-spinner";
import { getTemplateGenerator } from "./utils";
import { STEPS } from "./constants";
import Input from "ink-text-input";
import useGenerator from "./useGenerator";
//...
Integrando el hook useGenerator
Primero daremos un formato a la lista de opciones para que el componente Select pueda aceptarlo. Asímismo vamos a traer la lista de las plantillas para poder elejir la que se requiera.
const App = () => {
const {
state,
setTemplates,
setDirectory,
onCompleteTypingDirectory,
onSelectTemplate,
} = useGenerator();
const templateItems = useMemo(
() =>
state.templates.map((template) => {
return {
label: template.name,
value: template.path,
};
}),
[state.templates]
);
useEffect(() => {
getTemplateGenerator().then(setTemplates);
}, []);
return(
<Box>
<Text>hello</Text>
</Box>
)
}
Añadiendo los componentes con las interacciones
Finalmente añadimos los componentes usando el hook y los datos necesarios para mostrar cada paso y generar un proyecto.
const App = () => {
/// ...
return (
<Box>
{state.step === STEPS.NAME && (
<Box>
<Text color="cyanBright">Name directory:</Text>
<Input
value={state.directory}
onChange={setDirectory}
onSubmit={onCompleteTypingDirectory}
/>
</Box>
)}
{state.step === STEPS.SELECT && (
<Box flexDirection="column">
<Box marginTop={1}>
<Text color="cyanBright">Select a template</Text>
</Box>
<Select items={templateItems} onSelect={onSelectTemplate} />
</Box>
)}
{state.step === STEPS.LOADING && (
<Box>
<Text color="yellowBright">
<Loading type="dots" />
<Loading type="dots" />
<Loading type="dots" />
</Text>
<Text color="yellow">Creando proyecto...</Text>
</Box>
)}
{state.step === STEPS.END && (
<Box paddingY={2}>
<Text color="rgb(50,220,230)">
====================== ✨ Proyecto creado!!! ✨ ======================
</Text>
</Box>
)}
</Box>
);
};
render(<App />);
Uso final
Para este caso ejecutaremos el siguiente script yarn build
y después yarn start
para poder ver el funcionamiento. Y listo, lo logramos!! 😄🎉🎉.
En caso de querer más detalles te dejo el link del repositorio y el link de la librería 😊.
Posted on December 11, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.