Crea un generador de proyectos con React 🚀
Tags: node, react, webdev, tutorial
Published: 2021-12-11 14:49:06

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](https://github.com/vadimdemedes/ink). Comencemos! 😁

### Configurando el proyecto

Primero se instalará las siguientes dependencias.

```bash
# 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.

```json
{
  "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.

```json
{
  "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.

![Project structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mlag0f22jqx26dwxmrhb.png)

### 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:

```yaml
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.


```js
//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.

```js
//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.

```jsx
//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:

```bash
$ 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.

```js
//src/core/state.js
import { STEPS } from "../constants";

export const state = {
    step : STEPS.NAME,
    templates: [],
    directory: '.'
}
```

### Añadiendo el reducer

```js
//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. 

```js
//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:

- [ink-select-input](https://github.com/vadimdemedes/ink-select-input)
- [ink-spinner](https://github.com/vadimdemedes/ink-spinner)
- [ink-text-input](https://github.com/vadimdemedes/ink-text-input)


### Importando lo necesario

Inicialmente importaremos todo lo que usaremos para crear el CLI.

```jsx
//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. 

```jsx
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.

```jsx
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!! 😄🎉🎉.


![React CLI example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/knbipyyusscdzjkxppnx.png)

En caso de querer más detalles te dejo el link del repositorio y el link de la librería 😊.

[Código fuente de ejemplo](https://github.com/jhony-24/react-ink-generator-example)

[React Ink](https://github.com/vadimdemedes/ink)