Aller au contenu

Générer un projet React from scratch en 2023

Que ce soit pour comprendre ce que créent les générateurs de projets, pour uniformiser le démarrage des projets au sein de votre entreprise ou se préparer à lancer un POC en production (ça n'arrivera jamais), il peut être utile de créer son propre générateur. Découvrez dans cet article comment !

Photo by Rahul Mishra / Unsplash

Les générateurs de projets React, tels que create-react-app, sont des outils extrêmement utiles en développement. Ils automatisent le travail laborieux de configuration de l'environnement, et permettent de créer rapidement des applications React prêtes à être développées et déployées.

Ces générateurs intègrent les bonnes pratiques, fournissent un support out-of-the-box pour Babel, Webpack ou ViteJS et offrent même un ensemble d'outils pour le développement et le testing. Les connaître et comprendre comment ils fonctionnent peut vous faire gagner un temps considérable et vous permet d'éviter les erreurs courantes, tout en fournissant un point de départ solide pour votre projet.

Cependant, tous les projets ne sont pas identiques. Les entreprises peuvent avoir des exigences spécifiques en termes d'architecture de projet, de bibliothèques tierces, de styles de code, ou de process de déploiement. C'est là qu'un générateur de projets sur mesure peut être précieux.

En créant votre propre générateur, vous pouvez définir une architecture standard, imposer des conventions de code et ajouter des outils propres à votre entreprise. Cela peut aider à maintenir une cohérence entre les projets, à améliorer la productivité de l'équipe et à assurer que les nouvelles applications respectent les critères de qualité de l'entreprise dès le premier jour.

Poser les bases du projet

Les invites de commandes commencent toutes par créer un dossier dans lequel nous aurons le code de notre application.

mkdir mon-projet
cd mon-projet

Afin de gérer notre application et ses dépendances, nous utiliserons Node.js et NPM (Node Package Manager). Nous en profitons pour fixer la version à utiliser, cela évitera les problèmes à l'installation des dépendances et à l'exécution.

npm init -y
// package.json
{
  ...
  "engines": {
    "node": "18.17.0",
    "npm": "9.6.7"
  },
  ...
}

Npmrc est un outil essentiel pour gérer les configurations spécifiques à votre projet NPM. C'est comme le couteau suisse des configurations NPM. Vous travaillez sur plusieurs projets avec différentes exigences ? Pas de soucis ! Avec npmrc, vous pouvez facilement basculer entre différents fichiers de configuration .npmrc, ce qui vous permet de jongler entre différents contextes sans transpirer.

// .npmrc
engine-strict=true

NVM, ou Node Version Manager, est votre allié pour gérer et naviguer à travers différentes versions de Node.js. Vous avez déjà eu des maux de tête à cause de la dépendance d'un projet sur une version spécifique de Node.js ? Avec NVM, vous pouvez facilement passer d'une version à une autre, et même définir une version de Node par défaut pour chaque projet. C'est comme avoir un interrupteur pour chaque version de Node.js que vos projets pourraient nécessiter.

// .nvmrc
v18.17.0

N'oublions pas git pour le versionning.

git init
// .gitignore
node_modules

Un peu d'outillage

Commençons par ajouter Typescript pour le typage de notre code ainsi qu'une configuration basique pour un projet React.

npm install --save-dev typescript
// tsconfig.json
{
  "compilerOptions": {
    "jsx": "react",
    "target": "es2016",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}

Prettier est votre meilleur ami pour écrire un code impeccable. C'est un formateur de code "d'opinion" qui impose des styles de code cohérents en formatant automatiquement votre code. Il réduit les débats inutiles sur le style du code lors des revues de code, ce qui augmente la productivité du développeur et améliore la lisibilité du code.

Nous ajoutons une configuration tout à fait arbitraire.

npm install --save-dev prettier
// .prettierrc.json
{
  "printWidth": 200,
  "tabWidth": 2,
  "useTabs": false,
  "semi": true,
  "singleQuote": true,
  "trailingComma": "all",
  "bracketSpacing": true,
  "arrowParens": "always"
}

ESLint est le professeur strict mais aimé de chaque développeur. Il vous aide à écrire un meilleur code en mettant en évidence les erreurs et les anti-pattern, et peut même les corriger automatiquement. Il est entièrement personnalisable, ce qui signifie que vous pouvez définir vos propres règles pour faire respecter les meilleures pratiques spécifiques à votre équipe ou à votre projet.

Ici nous nous contenterons des recommandations données pour React et Typescript.

npm install --save-dev eslint eslint-plugin-react@latest @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest
// .eslintrc.json
{
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint", "react"],
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "parserOptions": {
    "ecmaVersion": "latest",
    "sourceType": "module",
    "ecmaFeatures": {
      "jsx": true
    }
  },
  "rules": {},
  "env": {
    "browser": true,
    "es2021": true
  },
  "settings": {
    "react": {
      "version": "detect"
    }
  }
}

Pour servir et empaqueter vos applications vous aurez principalement le choix entre Webpack ou Vite (dont vous pourrez trouver une présentation ici).

npm install --save-dev vite @vitejs/plugin-react
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
});

Vitest s'inspire de Jest et s'intègre harmonieusement à Vite. Il vous offre des environnements de test indépendants et isolés pour chaque test, ce qui améliore l'efficacité et la précision de vos tests.

npm install --save-dev vitest
// vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    include: ["**/*.test.ts*"],
  },
});

Il est temps d'ajouter les scripts NPM pour tout ces outillages.

//package.json
{
  ...
  "scripts": {
    "build": "vite build --config vite.config.ts",
    "lint-fix": "eslint --fix ./src",
    "prettier-fix": "prettier --write ./src",
    "start": "vite --config vite.config.ts",
    "test": "vitest --config vite.config.ts"
  },
  ...
}

Si vous souhaitez que certains test soient effectués avant que votre code ne passe dans la CI alors Husky est l'outils qu'il vous faut. Il vous permet de surcharger facilement les hooks git (lisez l'article "Devenez accro aux crochets git"), vous pourrez ainsi ajouter de la validation pre-commit.

npm install --save-dev husky
npx husky install

Profitons des scripts créés précédemment pour, avant chaque commit, déclencher un formatage du code, une vérification des erreurs et des types puis une exécution des tests.

npx husky add .husky/pre-commit "npm run lint-fix && npm run prettier-fix && npm run type-check && npm run test"

Commitlint est votre guide de style de commit personnel. Il impose des conventions sur vos messages de commit, ce qui facilite la lecture et la compréhension de l'historique du projet. C'est comme avoir un correcteur d'orthographe pour vos messages de commit, ce qui facilite la navigation et l'interprétation de votre historique de commit par le reste de votre équipe.

npm install --save-dev @commitlint/cli @commitlint/config-conventional
// commitlint.config.js
/* eslint-disable */
module.exports = {
  extends: ["@commitlint/config-conventional"],
};

Profitons de Husky pour ajouter un hook validant automatiquement nos messages de commit en exécutant commitlint.

npx husky add .husky/commit-msg 'npx --no-install commitlint --edit $1'

Le framework

Il est temps d'ajouter les dépendances React nécessaires pour démarrer une application ainsi que les dépendances de typage.

npm install --save react react-dom
npm install --save-dev @types/react @types/react-dom

L'application

Pour l'exemple nous créerons une application basique comprenant un composant App injecté par un script de boot dans une page HTML.

// src/app.tsx
import React from "react";

export const App = () => {
  return <div>Hello World</div>;
};
// src/index.tsx
import { App } from "./app";
import React from "react";
import { createRoot } from "react-dom/client";

const rootElement = document.getElementById("root");
if (!rootElement) throw new Error("Root element not found");
createRoot(rootElement).render(<App />);
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Mon projet</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="./src/index.tsx"></script>
  </body>
</html>

Automatisation

Assemblons tout ça dans un script shell afin de le rendre réutilisable via l'invite de commandes.

// do.sh
#!/bin/sh
PROJECT_NAME=$1

# Création du dossier du projet
rm -rf $PROJECT_NAME
mkdir $PROJECT_NAME
cd $PROJECT_NAME

# Création d'un nouveau projet node
npm init -y
jq '.engines = {"node": "18.17.0", "npm": "9.6.7"}' package.json > temp.json && mv temp.json package.json
echo "engine-strict=true" >> .npmrc
echo "v18.17.0" >> .nvmrc

# Installation de git
git init
echo "node_modules" >> .gitignore

# Installation de typescript
npm install --save-dev typescript
echo "{
  \"compilerOptions\": {
    \"jsx\": \"react\",
    \"target\": \"es2016\",
    \"module\": \"commonjs\",
    \"esModuleInterop\": true,
    \"forceConsistentCasingInFileNames\": true,
    \"strict\": true,
    \"skipLibCheck\": true
  }
}" >> tsconfig.json

# Installation de prettier
npm install --save-dev prettier
echo "{
  \"printWidth\": 200,
  \"tabWidth\": 2,
  \"useTabs\": false,
  \"semi\": true,
  \"singleQuote\": true,
  \"trailingComma\": \"all\",
  \"bracketSpacing\": true,
  \"arrowParens\": \"always\"
}" >> .prettierrc.json

# Installation de eslint
npm install --save-dev eslint eslint-plugin-react@latest @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest
echo "{
  \"parser\": \"@typescript-eslint/parser\",
  \"plugins\": [\"@typescript-eslint\", \"react\"],
  \"extends\": [
    \"eslint:recommended\",
    \"plugin:react/recommended\",
    \"plugin:@typescript-eslint/recommended\"
  ],
  \"parserOptions\": {
    \"ecmaVersion\": \"latest\",
    \"sourceType\": \"module\",
    \"ecmaFeatures\": {
      \"jsx\": true
    }
  },
  \"rules\": {
  },
  \"env\": {
    \"browser\": true,
    \"es2021\": true
  },
  \"settings\": {
    \"react\": {
      \"version\": \"detect\"
    }
  }
}" >> .eslintrc.json

# Installation de ViteJS
npm install --save-dev vite @vitejs/plugin-react
echo "import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()]
});" >> vite.config.ts

# Installation de vitest
npm install --save-dev vitest
echo "import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    include: ['**/*.test.ts*'],
  },
});" >> vitest.config.ts

# Ajout des scripts npm
jq '.scripts += {"build": "vite build --config vite.config.ts", "lint-fix": "eslint --fix ./src", "prettier-fix": "prettier --write ./src", "start": "vite --config vite.config.ts", "test": "vitest --config vitest.config.ts", "type-check": "tsc"}' package.json > temp.json && mv temp.json package.json

# Installation de Husky
npm install --save-dev husky
npx husky install
npx husky add .husky/pre-commit "npm run lint-fix && npm run prettier-fix && npm run type-check && npm run test"

# Installation de commitlint
npm install --save-dev @commitlint/cli @commitlint/config-conventional
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
npx husky add .husky/commit-msg 'npx --no-install commitlint --edit $1'

# Création du dossier src
mkdir src

# Installation de react
npm install --save react react-dom
npm install --save-dev @types/react @types/react-dom
# Création du fichier app.tsx
echo "import React from 'react';

export const App = () => {
  return <div>Hello World</div>;
};" >> src/app.tsx

# Création du fichier index.tsx
echo "import { App } from './app';
import React from 'react';
import { createRoot } from 'react-dom/client';

const rootElement = document.getElementById('root');
if (!rootElement) throw new Error('Root element not found');
createRoot(rootElement).render(<App />);
" >> src/index.tsx

# Création du fichier index.html
echo "<!doctype html>
<html lang=\"en\">
  <head>
    <meta charset=\"UTF-8\" />
    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />
    <title>${PROJECT_NAME}</title>
  </head>
  <body>
    <div id=\"root\"></div>
    <script type=\"module\" src=\"./src/index.tsx\"></script>
  </body>
</html>" >> index.html

Enfin rendons ce script exécutable.

chmod +X ./do.sh

Ça y est, vous avez votre premier générateur de projet personnalisé. À vous d'y ajouter vos configurations et dépendances afin de l'ajuster à vos besoins.

Dernier