36
Crea un generador de proyectos con React 🚀
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! 😁
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"]
}
La estructura final quedaría de la siguiente forma la cual veremos para que sirve cada uno de los archivos.
Aquí irá nuestro código para crear el CLI 👋.
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
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
}
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))
}
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
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: '.'
}
//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;
}
}
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
};
}
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:
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";
//...
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>
)
}
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 />);
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 😊.
36