Qu'est ce qu'un CLI ? 😱

Un CLI, Command-Line Interface, est une interface en ligne de commande (les plus anglophones l'auront deviné avant même que je le traduise).

Vous avez probablement déjà utilisé un CLI, que ce soit pour lancer un projet, se balader dans ses fichiers via le shell, ou même pour lancer des tests pour les plus aguerris.

Cette interface permet de communiquer avec un programme à travers des commandes, souvent sous forme de texte. De nos jours, on observe une grande utilisation des interfaces graphiques avec lesquelles on communique via des cliques de souris. Mais les CLI restent des outils très puissants qui permettent souvent d'avoir de meilleures performances.

cli example
Source image: https://k9scli.io

Une rapide présentation de React ⚛️

Pour ceux qui ne connaissent pas du tout React, c'est une librairie Javascript créée par Facebook en 2013, qui permet de construire des interfaces utilisateur. Le principe étant de créer des composants réutilisables gérant leur propre logique.

React utilise JSX (JavaScript XML ou JavaScript Syntax Extension), une extension syntaxique du Javascript qui ressemble à du HTML.
Pour React, cela permet d'abstraire la création de composant React pour que ce soit plus lisible.
Pour une grande partie des exemples, nous utiliserons le TSX, qui n'est rien de plus que la version Typescript du JSX. Un exemple vaut bien plus que de longues explications.

const CharacterPresentation = ({ name, catchPhrase }) => {
  return (
    <div>
      <h1>{name}</h1>
      <p>{name}: <em>`{catchPhrase}`</em></p>
    </div>
  );
};

Chacun de ces composants peut avoir des paramètres qu'on appelle des props. Tout comme en HTML on peut passer des attributs aux différentes balises, on peut passer des props à nos composants.

import { FunctionComponent } from 'react';

interface CharacterCardProps {
  name: string;
  catchPhrase: string;
}

/**
* On définit un composant avec des props qui lui sont propres
*/ 
const CharacterCard: FunctionComponent<CharacterCardProps> = (props) => {
  const { name, catchPhrase } = props;

  return (
    <div>
      <h1>{name}</h1>
      <p>{name} dis souvent <em>{catchPhrase}</em></p>
    </div>
  );
};

/**
* On utilise le composant précédemment déclaré avec des props différents
* On évite la duplication en réutilisant le composant
*/
const CharactersList: FunctionComponent = () => {
  return (
    <CharacterCard name="Rémi" catchPhrase="React Ink c'est le futur !" />
    <CharacterCard name="Guillaume" catchPhrase="Qu'est ce que je fais là ?" />
  );
};

Pour ceux qui souhaiteraient approfondir, je vous redirige vers la [documentation de React] (https://fr.reactjs.org/docs/hello-world.html) ou vers leur nouvelle documentation.

Les composants de React Ink

React permet de faire des interfaces utilisateur dans le navigateur, or dans notre cas nous voulons produire un CLI. Nous ne pouvons donc pas utiliser les balises HTML dont on a l'habitude.
Pour remedier à ça, cette superbe librairie qu'est React Ink nous met à disposition des composants de base pour construire notre propre CLI.
Nous allons voir les spécificités, plus ou moins complexes, de chacun de ces composants.

Le composant Text

Le composant Text est le composant principal, il permet d'afficher du texte dans notre CLI.
Ce composant peut prendre différents props qui permettent d'altérer l'affichage du texte.

  • color, modifier la couleur du texte
  • backgroundColor, modifier l'arrière plan du texte
  • dimColor, permet de rendre le texte moins lumineux
  • inverse, permet d'inverser la couleur du texte avec celle en arrière plan
  • bold / italic / underline / strikethrough, qui permettent respectivement de mettre le texte en gras, en italique, de le souligner et de le ~~barrer~~.
  • wrap, pour dire à React Ink de modifier l'affichage du texte lorsque celui-ci dépasse la taille de son conteneur.
import React from 'react';
import { Text } from 'ink';

const App = () => (
  <Text color="#20cb81" bold underline>Bonjour monde !</Text>
);
text component example

Le composant Box

Comme vous le savez peut être déjà, en HTML/CSS, tout est considéré comme étant une boîte/box.
Le composant Box permet d'entourer d'autres composants pour y appliquer des styles comme on le ferait avec du CSS.
Ce composant permet donc de pouvoir contrôler, pour les éléments qu'il entoure:

  • margin
  • padding
  • width
  • height
  • flex, uniquements les fonctionnalités flex-direction, align-items, align-self et justify-content sont disponibles.
  • border
import React, { FunctionComponent } from "react";
import { Box, Text } from "ink";

type CustomizedTextBoxProps = { text: string };

const CustomTextBox: FunctionComponent<CustomizedTextBoxProps> = ({ text }) => (
	<Box
		borderStyle="round"
		borderColor="#20cb81"
		paddingY={1}
		flexGrow={1}
		flexShrink={1}
		flexBasis={0}
		justifyContent="center"
	>
		<Text color="white" bold>
			{text}
		</Text>
	</Box>
);

const App = () => (
	<Box flexDirection="row-reverse">
		<CustomTextBox text="cool" />
		<CustomTextBox text="is" />
		<CustomTextBox text="Ink" />
		<CustomTextBox text="React" />
	</Box>
);
box component example

Le composant Newline

Le composant Newline est plutôt simple, il permet de sauter une ligne. On peut le voir comme un équivalent de \n ou de <br />.

Le composant Spacer

Le composant Spacer, quant à lui, permet, en fonction de l'orientation (horizontale ou verticale), d'espacer les élements qu'il sépare aux extrémité de l'axe sur lequel le contenu est orienté.

import { Box, Spacer } from 'ink';

const App = () => (
  <Box>
    <CustomTextBox text="🌬" />

		<Spacer />

    <CustomTextBox text="🍂" />
  </Box>
);
Spacer component example

Le composant Static

Le composant Static prend comme props un tableau d'élément et permet de modifier l'affichage des éléments du tableau qui ont changé.
Cela peut être utile lorsqu'on récupère de la donnée périodiquement, on évite de re-rendre le composant, et permet donc d'optimiser notre code.

import { FunctionComponent } from 'react';
import { Static, Text } from 'ink';

type LogsListProps = {
  logs: string[];
};

const LogsList: FunctionComponent<LogsListProps> = (props) => {
  const { logs } = props;

  return (
    <Static items={logs}>
      {logs.map(log => {
        return (
          <Text key={log.id}>{log.message}</Text>
        );
      })}
    </Static>
  )
};

À chaque fois que l'on va recevoir de nouvelles valeurs pour les logs, il y aura un rendu uniquement des nouveaux éléments de la liste de logs et non pas de l'ensemble du composant.

Le composant Transform

Le composant Transform a un props transform qui prend une fonction ayant comme paramètre le texte qui va afficher par les composants enfants de Transform et doit retourner une string.

import { Transform } from 'ink';

const App = () => (
  const setEvenLetterUpperCase = (output) => {
		return output.split('').map((letter, index) => {
			if (index % 2 === 0) {
				return letter.toUpperCase();
			}
			return letter.toLowerCase();
		}).join(' ');
	};

	<Transform transform={setEvenLetterUpperCase}>
		<Text>Bonjour monde !</Text>
	</Transform>
);
Transform component example

Ici, la transformation du texte va rendre les lettres en position paire en majuscule et les autres en minuscules.

Les composants de bases fournis par la librairie ne sont pas une fin, et peuvent être réutilisés pour construire des composants personnalisés bien plus complexes.
En l'occurrence, des contributeurs ont produit des composants avancés qui utilisent ceux de base.

Les hooks 🪝

Les hooks en React 🍂

Jusqu'à présent nous avons vu comment afficher une interface utilisateur, mais il n'y avait aucune logique. C'est en ça que les hooks vont nous être utiles.
Les hooks sont des fonctions qui permettent de gérer des états, des modifications, de mémoïser des valeurs ou des fonctions et bien plus encore.
Les hooks suivent certaines règles, comme le fait de toujours commencer par le mot clé use.

Il existe des hooks fournis par React:

  • useState, qui permet de gérer un état.
    Il prend comme paramètre la valeur initiale de l'état (cela peut aussi bien être une valeur primitive telle que les nombres, les chaînes de caractères, les booléens, ... Mais aussi des tableaux ou des objets).
    Ce hook renvoie un tuple ayant en première position la valeur de l'état et en seconde position la fonction permettant de modifier l'état.
const [value, setValue] = useState(0);
  • useEffect prend une fonction en premier paramètre qui contient le code à exécuter, ainsi qu'un second paramètre optionnel qui est tableau contenant les variables dont dépend la fonction passé au hook d'effet.
    De base, cette fonction que l'on passe est exécutée à chaque rendu du composant qui contient le hook.
    Cependant si on y passe un tableau, on lui associe des dépendances, et le hook d'effet s'executera uniquement lorsque ses dépendances changent.
    Si le tableau est vide alors le hook d'effet ne sera exécuté qu'une seule fois au rendu initial du composant. Si des variables sont passées, alors il sera exécuté à chaque fois qu'une de ces variables change.
// La phrase sera affichée dans la console à chaque fois que la variable *count* sera modifiée
useEffect(() => {
  console.log(`Hello Gojob ${count} fois !`);
}, [count]);

Il est possible de créer ses propre hooks, et donc de pouvoir factoriser de la logique entre plusieurs composants. Les hooks personnalisés utilisent majoritairement des hooks de base de React et abstraient donc une certaine complexité.

/**
* Notre hook personnalisé nous permet d'abstraire la récupération d'un utilisateur mais aussi 
* de pouvoir réutiliser cette logique dans les différents composants qui en ont besoin.
*/
const useUser = (userId: string) => {
  const [user, setUser] = useState(undefined);

  useEffect(() => {
    fetchUser(userId).then(response => setUser(response.user));
  }, [userId]);

  return user;
};

Il existe bien d'autres hooks pour React, et si cela vous intéresse, je vous redirige vers la documentation officielle des hooks de React.

Les hooks dans React Ink

Comme dit précédemment, il est possible de créer ses propres hooks personnalisés. La librairie React Ink a donc les siens.
Comme pour les hooks de React, nous ne verrons pas tous les hooks que propose React Ink.

  • useInput est un hook propre à React Ink qui permet de réagir aux entrées utilisateurs.
    Il prend en paramètre une fonction avec comme argument input et key qui représente ce qui a été tapé par l'utilisateur.
  useInput((input, key) => {
    if (input === 'q') {
      // exit the entire app
    }
    
    if (input === 'r') {
      // refetch data 
    }

    if (key.downArrow) {
      // scroll down 
    }
  });
  • useApp permet d'arrêter complètement l'application.
const { exit } = useApp();

useInput((input, key) => {
  if (input === 'q') {
    exit();
  }
});

Il existe d'autres hooks que met à disposition React Ink, vous pouvez les trouver dans la documentation.

React Ink propose aussi une librairie pour tester votre CLI, je vous invite à l'utiliser si d'aventure vous décidiez de développer votre propre Command Line Interface.