Ouracademy

Creando un command line (cli) con nodejs

Hace varios años atras cuando empece mi blog hecho en next.js (cuando next.js estaba en la versión 1), escribia mis posts en puro html (bueno en jsx). Sin embargo, estaba cansado de estar escribiendo posts usando jsx, es muy engorroso...

Si quieres escribir un parrafo con una palabra importante señalada (en negrita), tenia que hacer <p>Hola mundo, esto es <strong>importante!</strong> </p>

En cambio en markdown podia hacer, simplemente: Hola mundo, esto es **importante!**

Sin embargo me gustaba la idea de usar jsx porque podía agregar componentes React, es decir podia hacer cosas como: <p>Han visto domain driven design esta de vuelta de moda: </p> <YoutubeVideos of={"domain-driven-design"}/>

Estuve mucho tiempo asi hasta que, hace 3 años atras, me entere de mdx, una tecnologia que combina lo mejor de ambos mundos markdown + jsx. Decidi entonces hacer que mi blog use mdx...pero tambien queria que sea muy muy veloz.. queria que mis paginas estaticas (y en ese tiempo next.js solo servia para server side rendering), asi que me movi a gatsby.js, esos tiempos inestable gatsby + mdx...todos los lios que me meti 😝

Porque te cuento eso? Para contarte de mi vida? Claro que si 🤪

No nada es que haremos una command line o cli que genere archivos en .mdx! Eso fue lo que hice ya hace creo +1 de año, si te interesa de frente el codigo, te dejo el link aca

npm run new-post command in action
El cli que haremos

Empecemos con lo básico, usaremos nodejs y la librería inquirer, asi que primero, creemos un proyecto e instalemos la libreria

mkdir mi-cli
cd mi-cli
npm init
npm install inquirer

Inquirer es una librería que facilita hacer cli apps, creemos un ejemplo basico, un cli que nos pida el titulo de nuestro post a crear.

const inquirer = require("inquirer");
console.log("Hola 🤖! Soy un cli que te ayudare a crear tu post");
inquirer
  .prompt([
    {
      name: "title",
      message: "Por favor, dame el titulo de tu post:",
    },
  ])
  .then((answers) => {
    console.log(`Post creado con titulo: ${answers.title}`);
  });

Basicamente inquirer.prompt quiere decir que inquirer hara preguntas (un arreglo de preguntas cada una con un mensaje, message, y nombre name) y luego de preguntar al usuario, el then nos dara las respuestas a nuestras preguntas en un objeto (en base al name de cada pregunta).

Sencillo?

En inquirer podemos tambien indicar que el usuario solo pueda escoger un par de opciones al responder una pregunta. Por ejemplo, ahora tambien preguntemos quien es el autor del post.

const inquirer = require("inquirer");
const authors = ["arthur", "diana"]; // imaginate poder obtenerlo de un REST api
console.log("Hola 🤖! Soy un cli que te ayudare a crear tu post");
inquirer
  .prompt([
    {
      name: "title",
      message: "Por favor, dame el titulo de tu post:",
    },
    {
      type: "list",
      name: "author",
      message: "Quien escribe el post?",
      choices: authors,
    },
  ])
  .then((answers) => {
    console.log(
      `Post creado con titulo: ${answers.title}, autor: ${answers.author}`
    );
  });

Bueno ta todo muy bien pero de vez en cuando queremos validar algunas preguntas, que tal si usamos la libreria joi para validar por ejemplo que el titulo no pase de 60 caracteres, porque? bueno es recomendable que si creas un post uses las mejores prácticas de SEO 😃

Hagamos con joi lo basico, no te olvides 1ro de instalarlo, npm install joi

const Joi = require("joi");

const title =
  "un titulo muy largoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo";

const { error } = Joi.string().max(60).validate(title);
console.log(error.message);
// saldra: "value" length must be less than or equal to 60 characters long'

Con el codigo de arriba podemos validar que nuestro titulo no sea muy largo, incluso podriamos poner un mensaje más personalizado:

const Joi = require("joi");

const title =
  "un titulo muy largoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo";

const sugerenciaSEO =
  `Por favor no exceda de {#limit} caracteres, por propositos de SEO 😉\n` +
  `Chequea: https://moz.com/learn/seo/title-tag \n`;

const { error } = Joi.string()
  .max(60)
  .messages({ "string.max": sugerenciaSEO })
  .validate(title);
console.log(error.message);
// saldra: Por favor no exceda de 60 caracteres

Nota como el {#limit} actua como una interpolacion, es decir el limit = 60 Increible no?

Bueno tiempo de juntar joi y inquirer, para ello agregaremos a la pregunta de titulo, una propiedad validate, que debe retornar true si es que el valor (el titulo) ingresado por el usuario es valido o retornar un string, un mensaje de error en caso que no sea valido.

const inquirer = require("inquirer");
const Joi = require("joi");

const authors = ["arthur", "diana"]; // imaginate poder obtenerlo de un REST api

const sugerenciaSEO =
  `Por favor no exceda de {#limit} caracteres, por propositos de SEO 😉\n` +
  `Chequea: https://moz.com/learn/seo/title-tag \n`;

console.log("Hola 🤖! Soy un cli que te ayudare a crear tu post");
inquirer
  .prompt([
    {
      name: "title",
      message: "Por favor, dame el titulo de tu post:",
      validate: (title) => {
        // aca la integracion con joi :)
        const { error } = Joi.string()
          .max(60)
          .messages({ "string.max": sugerenciaSEO })
          .validate(title);
        return error ? error.message : true;
      },
    },
    {
      type: "list",
      name: "author",
      message: "Quien escribe el post?",
      choices: authors,
    },
  ])
  .then((answers) => {
    console.log(
      `Post creado con titulo: ${answers.title}, autor: ${answers.author}`
    );
  });

Bueno se puso bueno la cosa no?, que tal si ahora creamos un archivo .mdx o si deseas un markdown a partir de estos datos en base a una plantilla, para ello usaremos otra librería mustache! Esto se va a poner bueno 🤪

const Mustache = require("mustache");

const generateTemplateOf = (post) =>
  Mustache.render(
    `---
    title: "{{title}}"
    author: {{author}}
    ---

    Your content in markdown but with the power of JSX. MDX!`,
    post
  );

// podemos llamarlo
generateTemplateOf({ title: "Un post sobre cli", author: "arthur" });
// con ello nos generara un string, interpolando los datos de nuestro post
// es decir hara algo como:
// ---
// title: "Un post sobre cli"
// author: arthur
// ---

Sencillo no? Dado ello podemos guardarlo en un archivo .mdx, de la siguiente forma

const fs = require("fs");
const path = "mi-post.mdx";
const post = { title: "Un post sobre cli", author: "arthur" };
fs.writeFileSync(path, generateTemplateOf(post));

Momento de juntar todo

const fs = require("fs");
const Mustache = require("mustache");
const inquirer = require("inquirer");
const Joi = require("joi");

const generateTemplateOf = (post) =>
  Mustache.render(
    `---
    title: "{{title}}"
    author: {{author}}
    ---

    Your content in markdown but with the power of JSX. MDX!`,
    post
  );

const authors = ["arthur", "diana"]; // imaginate poder obtenerlo de un REST api

const sugerenciaSEO =
  `Por favor no exceda de {#limit} caracteres, por propositos de SEO 😉\n` +
  `Chequea: https://moz.com/learn/seo/title-tag \n`;

console.log("Hola 🤖! Soy un cli que te ayudare a crear tu post");
inquirer
  .prompt([
    {
      name: "title",
      message: "Por favor, dame el titulo de tu post:",
      validate: (title) => {
        // aca la integracion con joi :)
        const { error } = Joi.string()
          .max(60)
          .messages({ "string.max": sugerenciaSEO })
          .validate(title);
        return error ? error.message : true;
      },
    },
    {
      type: "list",
      name: "author",
      message: "Quien escribe el post?",
      choices: authors,
    },
  ])
  .then((answers) => {
    const post = answers;

    // a partir del titulo puedo generar el path
    // por ejemplo poniendolo en minuscula y eliminando espacios en blanco
    // o mejor usando slugify
    const path = post.title + ".mdx";
    fs.writeFileSync(path, generateTemplateOf(post));
    console.log(`Post creado en ${path}`);
  });

Eso es todo por este post, espero que te haya gustado 😝

Cambios y revisiones:

06/01/2021: use adjusted gif cli

06/01/2021: add cli gif

31/12/2020: creando un cli en nodejs

Si te fue útil este artículo, por favor compártelo. Apreciamos los comentarios y el aliento.
Compartelo por:

Quiza te pueda interesar...

Animando URLs con Javascript y Emojis

¿Sabias que puedes usar Emojis en (y otros carácteres gráficos de Unicode) en las URLs?. Es super genial. Sin embargo, parece que nadie lo…

CodeOwnership—Propiedad de código

que es CodeOwnership, cual es la diferencia entre CodeOwnership individual y colectivo traducido de Martin Fowler

Cuando crear un Tipo de dato?

string, float, date...pero por que no crear tu propio tipo de dato? traducido de Martin Fowler