# Simon Boisset full agent context

> Aggregated public Markdown content for agents. HTML pages remain canonical.

# Migration React Native vers Expo.

Type: homepage
Language: fr-FR
Canonical URL: https://simonboisset.com/

J'aide votre équipe à migrer vers Expo/EAS sans bloquer les releases ni perdre la main.

## Ce que je sécurise pendant la migration Expo.

La plupart des migrations bloquent aux mêmes endroits : compatibilité native et déploiements EAS/stores. Je traite les deux dans le même diagnostic.

- Surface native: Dépendances, permissions, config plugins, prebuild et modules qui peuvent bloquer Expo. Vous obtenez une carte de compatibilité et des options claires de remplacement ou de repli.
- Déploiements EAS et QA: EAS Build, Submit, Update, channels, CI et distribution QA. Je conseille votre équipe sur les options applicables au projet pour optimiser les phases de déploiement.
- Accompagnement sur mesure: Arbitrages selon votre stack, vos contraintes stores et votre cadence produit. Je choisis avec vous les options utiles au projet, sans appliquer un workflow Expo générique.
- Autonomie équipe: Décisions, playbooks et arbitrages sont écrits pour l'équipe interne. La migration se termine avec un workflow que votre équipe peut opérer sans moi.

## Retours de mission

Quelques retours concrets de missions React Native, Expo et mobile legacy.

- Eric: Nous partions d'un legacy assez complexe. Simon a repris le projet, remis de l'ordre dans toute la stack et continue de nous accompagner au quotidien.
- Julie: Simon a construit notre app React Native depuis zéro, en restant aligné avec l'app web. En quelques semaines, nous avions les rappels, les messages, le partage de photos et les appels vidéo.
- Matthieu: Nous traînions cette migration Expo depuis plus d'un an. Simon nous a rassurés, l'a livrée en deux semaines, et nous avons pu publier une version propre derrière.
- Thomas: Simon est arrivé sur un projet où le front, le backend, le mobile et l'infra étaient tous liés. Il a vite compris comment tout s'emboîtait, ce qui nous a évité beaucoup d'allers-retours.
- Romuald: Simon a migré vers Expo trois de nos apps React Native maintenues depuis plus de 9 ans. La migration s'est faite en douceur, et nous en mesurons déjà les bénéfices : montées de version bien plus rapides, builds enfin fiables.
- Antoine: Notre codebase mobile était devenue trop complexe : trop de cas particuliers, trop de dépendances, plus beaucoup de certitudes. Simon nous a aidés à reprendre la maîtrise et à remettre une structure claire.

## De votre app actuelle à une release Expo

Votre produit continue d'avancer. Je retire les blocages Expo, je prouve le chemin de release, puis l'équipe reprend la main.

- Repérer ce qui bloque Expo: Je vérifie les modules natifs, la config app, la CI et les contraintes stores avant de toucher au code. Outcome: Carte des risques, ordre de traitement et devis de prestation
- Migration du projet: Je configure le projet et les fichiers Expo, remplace les librairies incompatibles avec l'écosystème, puis sécurise les builds. Outcome: App Expo qui build proprement
- Tester une vraie sortie: Je valide EAS, les canaux QA, les versions et les règles store sur une release que l'équipe peut inspecter. Outcome: Chemin QA vers store prêt
- Passer la main à l'équipe: Je documente les commandes, les choix et les options de rollback utilisés pendant la migration. Outcome: Équipe prête pour les prochaines releases

## Related links
- [Website](https://simonboisset.com/)
- [Blog](https://simonboisset.com/blog)
- [Legal documents](https://simonboisset.com/docs)
- [Schedule a call](https://cal.eu/simon-boisset/meet-simon)

# React Native to Expo migration.

Type: homepage
Language: en-US
Canonical URL: https://simonboisset.com/en

I help your team migrate to Expo/EAS without blocking releases or losing ownership.

## What I secure during an Expo migration.

Most migrations get stuck in the same places: native compatibility and EAS/store deployments. I handle both in the same diagnosis.

- Native surface: Dependencies, permissions, config plugins, prebuild, and modules that can block Expo. You get a compatibility map and clear replacement or fallback options.
- EAS and QA deployments: EAS Build, Submit, Update, channels, CI, and QA distribution. I advise your team on the options that actually fit the project to optimize deployment phases.
- Tailored guidance: Tradeoffs matched to your stack, store constraints, and product cadence. I choose the useful Expo options with you instead of applying a generic workflow.
- Team ownership: Decisions, playbooks, and tradeoffs are written for the internal team. The migration ends with a workflow your team can operate without me.

## Mission feedback

Concrete notes from React Native, Expo, and legacy mobile missions.

- Eric: We started from a fairly complex legacy codebase. Simon took over the project, brought structure back to the whole stack, and still supports us day to day.
- Julie: Simon built our React Native app from scratch while keeping it aligned with the existing web app. In a few weeks, we had reminders, messaging, photo sharing, and video calls.
- Matthieu: We had been dragging this Expo migration for more than a year. Simon reassured us, delivered it in two weeks, and we were able to publish a clean version afterwards.
- Thomas: Simon joined a project where frontend, backend, mobile, and infrastructure were all tied together. He quickly understood how everything fit together, which saved us a lot of back and forth.
- Romuald: Simon migrated three of our React Native apps to Expo after more than 9 years of maintenance. The migration went smoothly, and we are already seeing the benefits: much faster upgrades and finally reliable builds.
- Antoine: Our mobile codebase had become too complex: too many special cases, too many dependencies, and not much certainty left. Simon helped us regain control and put a clear structure back in place.

## From your current app to an Expo release

You keep the product moving. I remove the Expo blockers, prove the release path, then hand the workflow back to your team.

- Find what blocks Expo: I review the native modules, app config, CI, and store constraints before changing the code. Outcome: Risk map, treatment order, and service quote
- Project migration: I set up the Expo project and config files, replace libraries that conflict with the ecosystem, then secure the builds. Outcome: Expo app that can build cleanly
- Run a real release path: I validate EAS, QA channels, versions, and store rules on a release your team can inspect. Outcome: QA to store path ready
- Give the team control: I document the commands, decisions, and rollback options used during the migration. Outcome: Team ready for the next releases

## Related links
- [Website](https://simonboisset.com/en)
- [Blog](https://simonboisset.com/en/blog)
- [Legal documents](https://simonboisset.com/en/docs)
- [Schedule a call](https://cal.eu/simon-boisset/meet-simon)

# Comment j’utilise Expo pour publier des apps marque blanche on-premise

Type: blog post
Language: fr-FR
Canonical URL: https://simonboisset.com/blog/expo-white-label-on-premise-apps
Published: 25 juin 2026
Summary: Publier une app mobile marque blanche ne devrait pas imposer de cloner tout le projet : Expo permet de garder un socle commun et des variantes propres. Sur u...

Publier une app mobile marque blanche ne devrait pas imposer de cloner tout le projet : Expo permet de garder un socle commun et des variantes propres.

Sur un produit comme Questovery, une même base React Native doit pouvoir servir plusieurs contextes : l’app SaaS standard, une app de preview interne, une app dédiée à un client, ou une variante on-premise avec sa propre identité. Le piège classique consiste à copier l’application mobile pour chaque client. Ça marche au début, puis chaque correctif, chaque dépendance native et chaque mise à jour store devient une opération répétée.

L’approche que je préfère est différente : **un shell mobile partagé, des variantes fines, et une configuration Expo/EAS qui porte l’identité native de chaque build**.

## Le problème réel des apps marque blanche

Une app marque blanche n’est pas seulement un changement de logo. Elle peut impliquer :

- un nom d’application différent
- un bundle identifier iOS et un package Android dédiés
- un scheme deep link spécifique
- des assets natifs distincts
- des domaines associés différents
- des endpoints API, web et PowerSync propres à l’instance
- des channels EAS Update isolés
- parfois une fiche store et une stratégie de publication séparées

Si tout cela est dupliqué dans des dossiers d’app différents, la dette arrive vite. On finit par maintenir plusieurs apps presque identiques, avec des divergences accidentelles difficiles à auditer.

Dans Questovery, l’objectif est plutôt de garder le comportement commun dans les packages partagés, et de laisser les dossiers d’app porter uniquement la configuration, la marque, les profils EAS et les métadonnées de publication.

## Un shell partagé, des variantes explicites

La structure ressemble à ceci :

```txt
apps/mobile-on-prem
  App.tsx
  app.config.ts
  eas.json
  src/on-prem-brands.js
  assets/branding/*
  .eas/workflows/*

packages/mobile-shell
packages/sdk
packages/common
```

Le shell mobile contient les écrans, la navigation, l’authentification, les services runtime, l’analytics et l’expérience de jeu. Le dossier `apps/mobile-on-prem` choisit seulement quelle variante charger.

Une variable comme `APP_VARIANT` sélectionne la marque active :

```ts
const brandConfig = resolveOnPremBrandConfig(process.env.APP_VARIANT);
```

Cette variante peut ensuite définir le nom de l’app, les identifiants natifs, les assets, les domaines et les endpoints. Le code métier, lui, reste dans le shell et les packages communs.

C’est le point important : **la marque est une configuration, pas un fork produit**.

## `app.config.ts` comme point d’assemblage natif

Expo est très pratique pour ce modèle parce que `app.config.ts` peut être dynamique. Au moment du build, on résout la variante active, puis on injecte les champs natifs attendus par iOS, Android et EAS.

Exemple simplifié :

```ts
export default context => {
  const brandConfig = resolveOnPremBrandConfig(process.env.APP_VARIANT);

  return buildExpoConfig(
    {
      ...brandConfig.brand,
      easProjectId: 'shared-project-id',
      slug: 'questovery-on-premise',
    },
    version,
  )(context);
};
```

Dans ce modèle, le projet Expo peut rester commun au dossier d’app, tandis que l’isolation opérationnelle se fait ailleurs : bundle ids, package names, schemes, assets, profils EAS, channels, endpoints et fiches store.

Ce choix évite de transformer l’identifiant de projet Expo en propriété métier du client. Le projet EAS appartient au dossier d’app. La variante appartient au build.

## `eas.json` porte les environnements

Ensuite, `eas.json` décrit les profils disponibles. Une variante peut avoir un profil `dev`, `staging` et `production`, chacun avec ses propres variables publiques :

```json
{
  "build": {
    "client-staging": {
      "extends": "staging",
      "channel": "onprem-client-staging",
      "env": {
        "APP_VARIANT": "client",
        "EXPO_PUBLIC_EAS_ENV": "preview",
        "EXPO_PUBLIC_INSTANCE_KEY": "client-instance",
        "EXPO_PUBLIC_API_URL": "https://api-staging.client.example.com",
        "EXPO_PUBLIC_WEB_URL": "https://staging.client.example.com",
        "EXPO_PUBLIC_POWERSYNC_URL": "https://powersync-staging.client.example.com"
      }
    }
  }
}
```

Il y a deux catégories à bien distinguer :

- `APP_VARIANT` est une variable de build native, utilisée pour résoudre la marque
- les `EXPO_PUBLIC_*` sont des entrées bundle/update-time, embarquées dans le JavaScript ou dans une update OTA

Cette séparation évite de mélanger l’identité native, les endpoints runtime et les règles produit.

## OTA : attention aux channels, branches et variables

Avec EAS Update, les OTA sont très utiles pour les variantes marque blanche, mais elles ne doivent pas devenir un raccourci dangereux.

Une règle simple fonctionne bien :

- si le changement touche le binaire natif, on refait un build
- si le changement touche uniquement le JavaScript compatible avec le runtime existant, on publie une update OTA

Les workflows modernes peuvent calculer un fingerprint, chercher un build existant, puis choisir entre build natif et update. C’est particulièrement efficace pour des apps on-premise où certaines variantes ne doivent pas builder à chaque commit.

Le détail qui m’a déjà évité des erreurs : les jobs `fingerprint`, `update` et les scripts de liaison de channel doivent recevoir les mêmes variables que le profil EAS concerné. Un job d’update ne doit pas supposer qu’il hérite automatiquement de tout ce qui est dans le profil de build.

En pratique, je préfère tester ce contrat : si une workflow publie une OTA pour `APP_VARIANT=client`, elle doit aussi porter les `EXPO_PUBLIC_API_URL`, `EXPO_PUBLIC_WEB_URL`, `EXPO_PUBLIC_INSTANCE_KEY` et autres valeurs nécessaires à cette variante.

## Quand réutiliser une app on-prem partagée

Toutes les marques blanches ne méritent pas un nouveau dossier mobile.

Je garde une app on-prem partagée quand :

- le comportement produit est identique
- la différence porte surtout sur l’identité, les endpoints, le catalogue ou la publication
- les clients peuvent partager le même shell et les mêmes règles de navigation
- les divergences restent testables par configuration

Je crée une app dédiée quand :

- le client a une vraie divergence fonctionnelle
- la stratégie store impose une autonomie forte
- le modèle de données ou de synchronisation doit évoluer séparément
- le risque de configuration devient plus élevé que le coût d’un dossier dédié

C’est une décision d’architecture, pas seulement une décision de build.

## Pourquoi c’est utile pour Questovery

Questovery permet de créer et exploiter des parcours, jeux de piste, visites guidées et activités terrain avec une app mobile pour les participants. Pour l’offre dédiée, le sujet n’est pas seulement d’apposer un logo : il faut livrer une expérience mobile cohérente, isolée, connectée au bon backend, et maintenable dans le temps.

C’est exactement le genre de cas où Expo et EAS sont efficaces : on garde un socle commun solide, puis on industrialise les variantes autour de la configuration native, des channels et des workflows.

Pour voir le produit côté usage, j’ai documenté l’offre dédiée ici : [Questovery Dédié, app mobile en marque blanche et environnement isolé](https://www.questovery.com/fr/offers/dedicated#white-label-apps).

## Le vrai bénéfice

La marque blanche devient durable quand elle est traitée comme un problème de plateforme :

- les différences client sont explicites
- les builds sont reproductibles
- les updates OTA restent isolées
- le shell mobile reste commun
- les choix on-premise sont visibles dans la configuration
- les tests peuvent vérifier les contrats de workflow

Expo ne supprime pas la complexité. Il donne surtout un bon endroit pour la ranger.

## Related links
- [Blog index](https://simonboisset.com/blog)
- [Website](https://simonboisset.com/)

# Workflows Expo CI/CD (EAS) : builds intelligents, fingerprint et OTA

Type: blog post
Language: fr-FR
Canonical URL: https://simonboisset.com/blog/expo-ci-cd-workflows-fingerprint-ota
Published: 12 janvier 2026
Summary: Workflows Expo CI/CD (EAS) : builds intelligents, fingerprint et OTA Dans un projet Expo/React Native, la vraie question n'est pas "comment builder ?" mais "...

# Workflows Expo CI/CD (EAS) : builds intelligents, fingerprint et OTA

Dans un projet Expo/React Native, la vraie question n'est pas "comment builder ?" mais "quand faut-il vraiment builder ?". Les builds natifs sont lents (et coûteux en CI), alors que les updates OTA sont rapides... mais ne couvrent pas tous les changements.

L'objectif du workflow : **automatiser toute la chaîne** (builds natifs, OTA, soumissions) **tout en évitant les builds inutiles**, grâce au **fingerprint Expo** et à un **versioning explicite**.

---

## Structure type : staging + production

Deux environnements, un seul flux :

- `dev`/`staging` -> environnement **preview**
- `main` -> environnement **production**

Même logique dans les deux cas :

1. calculer un fingerprint
2. décider entre **build natif** ou **OTA**
3. publier automatiquement

En production, on ajoute la **soumission store** lorsqu'un build natif a eu lieu.

---

## Fingerprint : décider automatiquement

Le fingerprint est un hash basé sur ce qui impacte vraiment le binaire natif :

- dépendances et modules natifs
- config Expo + plugins
- fichiers de config
- version marketing (si injectée dans `app.config`)

Règle simple :

- **Fingerprint identique** à un build existant -> **OTA**
- **Fingerprint différent** -> **build natif**

---

## Exemple de workflow EAS (générique)

Voici un exemple inspiré d'un workflow réel, mais volontairement générique. Adapte les `paths`, `profile` et `channel` à ton projet.

```yml
name: Production builds

on:
  push:
    branches:
      - main
    tags:
      - v*.*.*
      - "!v*.*.*-**"
    paths:
      - apps/mobile/**
      - packages/**
      - "!**/*.md"

jobs:
  fingerprint:
    name: Fingerprint
    type: fingerprint
    environment: production

  get_android_build:
    name: Check existing Android build
    needs: [fingerprint]
    type: get-build
    environment: production
    params:
      fingerprint_hash: ${{ needs.fingerprint.outputs.android_fingerprint_hash }}
      profile: production

  get_ios_build:
    name: Check existing iOS build
    needs: [fingerprint]
    type: get-build
    environment: production
    params:
      fingerprint_hash: ${{ needs.fingerprint.outputs.ios_fingerprint_hash }}
      profile: production

  build_android:
    name: Build Android
    needs: [get_android_build]
    if: ${{ !needs.get_android_build.outputs.build_id }}
    type: build
    environment: production
    params:
      platform: android
      profile: production

  build_ios:
    name: Build iOS
    needs: [get_ios_build]
    if: ${{ !needs.get_ios_build.outputs.build_id }}
    type: build
    environment: production
    params:
      platform: ios
      profile: production

  submit_android:
    name: Submit Android
    needs: [build_android]
    if: ${{ needs.build_android.outputs.build_id }}
    type: submit
    environment: production
    params:
      build_id: ${{ needs.build_android.outputs.build_id }}
      profile: production

  submit_ios:
    name: Submit iOS
    needs: [build_ios]
    if: ${{ needs.build_ios.outputs.build_id }}
    type: submit
    environment: production
    params:
      build_id: ${{ needs.build_ios.outputs.build_id }}
      profile: production

  update_android:
    name: Update Android
    needs: [get_android_build, get_ios_build]
    if: ${{ needs.get_android_build.outputs.build_id && !needs.get_ios_build.outputs.build_id }}
    type: update
    environment: production
    params:
      channel: production
      platform: android

  update_ios:
    name: Update iOS
    needs: [get_android_build, get_ios_build]
    if: ${{ needs.get_ios_build.outputs.build_id && !needs.get_android_build.outputs.build_id }}
    type: update
    environment: production
    params:
      channel: production
      platform: ios

  update_all:
    name: Update All
    needs: [get_android_build, get_ios_build]
    if: ${{ needs.get_android_build.outputs.build_id && needs.get_ios_build.outputs.build_id }}
    type: update
    environment: production
    params:
      channel: production
      platform: all
```

---

## Versioning : version marketing explicite, builds auto-incrémentés

Je sépare volontairement :

- **version marketing** (lisible, intentionnelle) : gérée par nous
- **numéros de build** (`buildNumber` iOS / `versionCode` Android) : auto-incrémentés par EAS

La version marketing est stockée dans `package.json` :

```json
{
  "name": "my-app",
  "version": "1.4.0"
}
```

Et injectée dans `app.config.ts` :

```ts
import type { ConfigContext, ExpoConfig } from "expo/config";
import pkg from "./package.json";

export default ({ config }: ConfigContext): ExpoConfig => ({
  ...config,
  version: pkg.version,
});
```

---

## Conclusion & accompagnement

Ce type de pipeline **réduit fortement les coûts de build**, **accélère les cycles de livraison** et **sécurise les mises en production** grâce à des règles simples et automatisées.

👉 **Vous souhaitez mettre en place ou optimiser un workflow Expo CI/CD (fingerprint, OTA, versioning, soumissions automatiques) ?**

Je vous accompagne sur l'architecture, la configuration EAS, la CI et les bonnes pratiques Expo/React Native.

➡️ *Prenez rendez-vous avec moi pour construire un pipeline adapté à votre application.*

## Related links
- [Blog index](https://simonboisset.com/blog)
- [Website](https://simonboisset.com/)

# Comment créer sa propre librairie TypeScript en 2024 : Un guide pas à pas

Type: blog post
Language: fr-FR
Canonical URL: https://simonboisset.com/blog/create-typescript-library-tsup
Published: 24 juillet 2024
Summary: Dans ce tutoriel, nous allons voir comment créer une librairie TypeScript à partir de zéro. Nous couvrirons la configuration du projet, la compilation, les t...

Dans ce tutoriel, nous allons voir comment créer une librairie TypeScript à partir de zéro. Nous couvrirons la configuration du projet, la compilation, les tests et la publication. Ce guide est conçu pour être accessible aux développeurs ayant une connaissance de base de TypeScript et de npm.

## Étape 1 : Initialisation du projet

Commençons par créer un nouveau dossier pour notre projet et initialiser un projet npm.

```bash
mkdir ma-librairie-ts
cd ma-librairie-ts
npm init -y
```

Cette commande crée un fichier `package.json` de base. Nous le modifierons plus tard.

## Étape 2 : Installation des dépendances

Installons TypeScript et les outils nécessaires pour notre projet :

```bash
npm install --save-dev typescript tsup vitest
```

- `typescript` : Le compilateur TypeScript
- `tsup` : Un outil de build pour TypeScript
- `vitest` : Un framework de test rapide

## Étape 3 : Configuration de TypeScript

Créons un fichier `tsconfig.json` à la racine du projet :

```json
{
  "include": ["src"],
  "exclude": ["**/*.test.ts"],
  "compilerOptions": {
    "module": "esnext",
    "target": "esnext",
    "lib": ["esnext"],
    "declaration": true,
    "strict": true,
    "moduleResolution": "node",
    "skipLibCheck": true,
    "esModuleInterop": true,
    "outDir": "dist",
    "rootDir": "src"
  }
}
```

Cette configuration indique à TypeScript comment compiler notre code.

## Étape 4 : Configuration de tsup

Créons un fichier `tsup.config.ts` pour configurer notre build :

```typescript
import { defineConfig } from "tsup";

export default defineConfig({
  entry: ["src/index.ts"],
  clean: true,
  format: ["cjs", "esm"],
  dts: true,
});
```

Cette configuration permet de générer des builds CommonJS et ES modules, ainsi que les fichiers de déclaration TypeScript.

## Étape 5 : Configuration de Vitest

Créons un fichier `vitest.config.ts` pour configurer nos tests :

```typescript
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    globals: true,
    environment: "node",
  },
});
```

## Étape 6 : Mise à jour du package.json

Mettons à jour notre `package.json` avec les informations nécessaires :

```json
{
  "name": "ma-librairie-ts",
  "version": "0.1.0",
  "description": "Ma librairie TypeScript",
  "main": "dist/index.js",
  "module": "dist/index.mjs",
  "types": "dist/index.d.ts",
  "files": ["dist"],
  "scripts": {
    "build": "tsup",
    "dev": "tsup --watch",
    "test": "vitest run",
    "test:watch": "vitest"
  },
  "keywords": ["typescript", "library"],
  "author": "Votre Nom",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/votre-nom-utilisateur/ma-librairie-ts"
  },
  "bugs": {
    "url": "https://github.com/votre-nom-utilisateur/ma-librairie-ts/issues"
  },
  "homepage": "https://github.com/votre-nom-utilisateur/ma-librairie-ts#readme"
}
```

## Étape 7 : Écriture du code de la librairie

Créons un dossier `src` et un fichier `index.ts` à l'intérieur :

```bash
mkdir src
touch src/index.ts
```

Dans `src/index.ts`, écrivons une fonction simple :

```typescript
export function greet(name: string): string {
  return `Hello, ${name}!`;
}
```

## Étape 8 : Écriture des tests

Créons un fichier de test `src/index.test.ts` :

```typescript
import { expect, test } from "vitest";
import { greet } from "./index";

test("greet function", () => {
  expect(greet("World")).toBe("Hello, World!");
});
```

## Étape 9 : Build et test

Exécutons nos scripts pour construire et tester notre librairie :

```bash
npm run build
npm test
```

Si tout se passe bien, vous devriez voir que le test passe et que les fichiers de build sont générés dans le dossier `dist`.

## Étape 10 : Préparation pour la publication

Avant de publier, assurez-vous que votre `package.json` est à jour avec la bonne version, description, et autres métadonnées.

## Étape 11 : Publication sur npm

Si vous êtes prêt à publier votre librairie sur npm, suivez ces étapes :

1. Créez un compte sur npmjs.com si vous n'en avez pas déjà un.
2. Connectez-vous à npm via le terminal :

```bash
npm login
```

3. Publiez votre package :

```bash
npm publish
```

Félicitations ! Vous avez maintenant créé et publié votre propre librairie TypeScript !

## Conclusion

Ce tutoriel vous a guidé à travers les étapes de création d'une librairie TypeScript de base. N'oubliez pas d'ajouter de la documentation, des exemples d'utilisation, et de maintenir votre librairie à jour. Bonne chance dans vos futurs projets !

## Related links
- [Blog index](https://simonboisset.com/blog)
- [Website](https://simonboisset.com/)

# Sauver mon i18n en le typant

Type: blog post
Language: fr-FR
Canonical URL: https://simonboisset.com/blog/i18n-type-safe-approach
Published: 19 juillet 2024
Summary: L'internationalisation (i18n) est un aspect crucial du développement web moderne. Cet article explore comment implémenter une solution i18n typée en utilisan...

L'internationalisation (i18n) est un aspect crucial du développement web moderne. Cet article explore comment implémenter une solution i18n typée en utilisant la bibliothèque typed-locale dans une application React.

## Introduction à typed-locale

typed-locale est une bibliothèque d'internationalisation légère et typée, conçue pour fonctionner avec TypeScript. Elle fournit une API pour gérer les traductions avec une sécurité de type pour les clés et les variables.

## Mise en place du projet

Créons un nouveau projet React en utilisant Vite avec TypeScript :

```bash
npm create vite@latest my-i18n-app -- --template react-ts
cd my-i18n-app
npm install
```

Maintenant, installons typed-locale :

```bash
npm install typed-locale
```

## Définition des traductions

Créez un nouveau fichier appelé `translations.ts` dans le dossier `src` :

```typescript
// src/translations.ts
import { InferTranslation, plural } from "typed-locale";

export const en = {
  greeting: "Hello, {{name}}!",
  itemCount: plural({
    none: "You have no items.",
    one: "You have one item.",
    other: "You have {{count}} items.",
  }),
  nav: {
    home: "Home",
    about: "About",
    contact: "Contact",
  },
} as const;

export type Translation = InferTranslation<typeof en>;

export const fr: Translation = {
  greeting: "Bonjour, {{name}} !",
  itemCount: plural({
    none: "Vous n'avez aucun article.",
    one: "Vous avez un article.",
    other: "Vous avez {{count}} articles.",
  }),
  nav: {
    home: "Accueil",
    about: "À propos",
    contact: "Contact",
  },
};
```

## Création du traducteur

Créons maintenant un hook personnalisé pour utiliser nos traductions. Créez un nouveau fichier appelé `useTranslator.ts` :

```typescript
// src/useTranslator.ts
import { createTranslatorFromDictionary } from "typed-locale";
import { useMemo } from "react";
import { en, fr, Translation } from "./translations";

const dictionary = { en, fr };

export const useTranslator = (locale: keyof typeof dictionary) => {
  return useMemo(
    () =>
      createTranslatorFromDictionary<Translation>({
        dictionary,
        locale,
        defaultLocale: "en",
      }),
    [locale]
  );
};
```

## Utilisation du traducteur dans les composants

Maintenant, utilisons notre traducteur dans un composant React. Mettez à jour votre `App.tsx` :

```tsx
// src/App.tsx
import React, { useState } from "react";
import { useTranslator } from "./useTranslator";

const App: React.FC = () => {
  const [locale, setLocale] = useState<"en" | "fr">("en");
  const [itemCount, setItemCount] = useState(0);
  const translator = useTranslator(locale);

  return (
    <div>
      <select
        value={locale}
        onChange={(e) => setLocale(e.target.value as "en" | "fr")}
      >
        <option value="en">English</option>
        <option value="fr">Français</option>
      </select>

      <nav>
        <ul>
          <li>{translator((t) => t.nav.home)}</li>
          <li>{translator((t) => t.nav.about)}</li>
          <li>{translator((t) => t.nav.contact)}</li>
        </ul>
      </nav>

      <h1>{translator((t) => t.greeting, { name: "World" })}</h1>

      <p>{translator((t) => t.itemCount, { count: itemCount })}</p>
      <button onClick={() => setItemCount(itemCount + 1)}>Add Item</button>
      <button onClick={() => setItemCount(Math.max(0, itemCount - 1))}>
        Remove Item
      </button>
    </div>
  );
};

export default App;
```

## Fonctionnalités de sécurité de type

typed-locale offre plusieurs fonctionnalités de sécurité de type :

1. **Auto-complétion pour les clés de traduction** : L'IDE fournit des suggestions d'auto-complétion pour toutes les clés de traduction disponibles.

2. **Vérification de type pour les variables** : TypeScript détecte l'utilisation incorrecte des variables :

   ```typescript
   // Ceci provoquera une erreur TypeScript
   translator((t) => t.greeting, { wrongVariable: "World" });
   ```

3. **Traductions imbriquées** : Le système de types comprend les traductions imbriquées, permettant `translator(t => t.nav.home)`.

4. **Pluralisation** : La traduction `itemCount` démontre comment typed-locale gère la pluralisation, en sélectionnant automatiquement la forme plurielle correcte en fonction de la valeur de `count`.

## Avantages techniques

L'approche typée utilisant typed-locale offre plusieurs avantages techniques :

1. **Détection des erreurs à la compilation** : Les erreurs liées aux traductions sont détectées lors de la compilation plutôt qu'à l'exécution.

2. **Amélioration de l'expérience développeur** : L'auto-complétion pour les clés de traduction améliore la productivité.

3. **Support de refactoring** : Les outils de refactoring de TypeScript fonctionnent parfaitement avec les clés de traduction.

4. **Prévention des fautes de frappe dans les clés** : L'approche par callback élimine les fautes de frappe dans les chaînes de caractères pour les clés de traduction.

5. **Taille de bundle réduite** : Avec seulement 1 Ko, typed-locale a un impact minimal sur la taille de l'application.

6. **Indépendant du framework** : Bien que cet exemple utilise React, typed-locale peut être utilisé avec n'importe quel framework JavaScript ou en JavaScript vanilla.

## Conclusion

L'implémentation de l'i18n avec une approche typée utilisant typed-locale fournit une solution robuste pour gérer les traductions dans les projets TypeScript. En tirant parti du système de types, les développeurs peuvent créer des applications internationalisées plus fiables tout en maintenant la qualité du code et la productivité.

## Related links
- [Blog index](https://simonboisset.com/blog)
- [Website](https://simonboisset.com/)

# Sécuriser les Appels API avec `typed-api-call`

Type: blog post
Language: fr-FR
Canonical URL: https://simonboisset.com/blog/typed-api-call-guide
Published: 21 janvier 2024
Summary: Introduction Dans le monde du développement web, effectuer des appels API est une tâche quotidienne. Cependant, le processus n'est pas toujours simple, et de...

## Introduction

Dans le monde du développement web, effectuer des appels API est une tâche quotidienne. Cependant, le processus n'est pas toujours simple, et des erreurs peuvent facilement se glisser. Pour relever ce défi, la bibliothèque `typed-api-call` essaye d'offrire une manière sûre sur le plan des types et efficace de gérer les appels API.

## Installation Simplifiée

https://github.com/simonboisset/typed-api-call

Commencer avec `typed-api-call` est un jeu d'enfant. Commencez par installer la bibliothèque avec npm :

```bash
npm install typed-api-call
```

Explorons maintenant les fonctionnalités et découvrons comment cette bibliothèque peut améliorer la fiabilité de vos appels API.

## Introduction à `typed-api-call`

L'objectif principal de `typed-api-call` est de créer une enveloppe sûre sur le plan des types autour de l'API fetch. En définissant des appels API et leurs schémas, les développeurs peuvent générer des fonctions qui effectuent ces appels et renvoient des réponses avec les types corrects. Cela rend non seulement les appels API plus sûrs, mais simplifie également le processus de développement.

### Fonctionnalités en un Coup d'Œil

- **Sûreté des Types** : La bibliothèque s'assure que tant les définitions d'appels API que les appels eux-mêmes sont exempts d'erreurs, réduisant ainsi la probabilité d'erreurs d'exécution.
- **Validation de Schéma** : `typed-api-call` vérifie la réponse de chaque appel API par rapport à un schéma défini. Toute déviation déclenche une erreur, fournissant une couche supplémentaire de validation.
- **Extensible** : Bien qu'elle prenne actuellement en charge Zod pour les schémas, la bibliothèque est conçue pour accueillir d'autres bibliothèques de validation à l'avenir.

## Exemple d'Utilisation Pratique

Parcourons un exemple concret pour illustrer comment `typed-api-call` peut être intégré de manière transparente dans votre flux de travail.

```typescript
import { createApiCall } from "typed-api-call";
import { z } from "zod";

// Définir une fonction pour obtenir les en-têtes
export const getHeaders = ({ token }: { token?: string }) => {
  // ... (détails de l'implémentation)
};

// Créer une instance de l'appel API avec l'URL de base et la fonction getHeaders
const myApiCall = createApiCall({ url: "https://my-api.com/", getHeaders });

// Définir un schéma pour les données utilisateur
const userSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string(),
});

// Créer un appel API pour obtenir des utilisateurs
const getUsers = myApiCall({
  url: "users",
  method: "GET",
  input: z.object({ email: z.array(z.string().email()) }),
  response: z.object({ data: z.array(userSchema) }),
});

// Effectuer l'appel API
const users = await getUsers({
  params: undefined,
  data: { email: ["john.doe@gmail.com"] },
});

// ... (d'autres appels API peuvent être définis et exécutés de manière similaire)
```

## Comprendre l'API

### `createApiCall`

La fonction `createApiCall` établit les bases de vos appels API. Elle prend deux paramètres :

- `url` : L'URL de base de votre API.
- `getHeaders` : Une fonction qui renvoie les en-têtes pour votre appel API. Si la fonction nécessite des paramètres, ils peuvent être passés lors de la définition de l'appel API ou lors de l'appel lui-même.

### Définition d'Appel API

La structure d'une définition d'appel API comprend divers composants tels que l'URL, la méthode, les en-têtes, le schéma d'entrée, le schéma de réponse, et le schéma de paramètres (le cas échéant). Ces composants assurent collectivement une définition complète de l'appel API.

### Effectuer un Appel API

Exécuter un appel API implique de fournir des paramètres, des données et des en-têtes (si nécessaire). La bibliothèque `typed-api-call` se charge du reste, garantissant que l'appel est sûr sur le plan des types et respecte les schémas définis.

## Conclusion

Dans le domaine du développement web, où la précision et la fiabilité sont primordiales, `typed-api-call` s'avère être un outil intéressant. En améliorant la sûreté des types et en fournissant une validation de schéma, la bibliothèque simplifie le processus d'effectuer des appels API, réduisant les erreurs et renforçant la confiance des développeurs.

## Related links
- [Blog index](https://simonboisset.com/blog)
- [Website](https://simonboisset.com/)

# Créer un blog statique avec Next.js et Tailwind CSS

Type: blog post
Language: fr-FR
Canonical URL: https://simonboisset.com/blog/next-static-blog-tailwind
Published: 22 octobre 2023
Summary: Introduction J'ai récemment décidé de créer un blog statique pour partager mes connaissances et mes expériences. J'ai choisi Next.js pour sa simplicité et sa...

## Introduction

J'ai récemment décidé de créer un blog statique pour partager mes connaissances et mes expériences. J'ai choisi Next.js pour sa simplicité et sa flexibilité. Dans cet article, je vais vous montrer comment créer un blog statique avec Next.js et Tailwind CSS.

Vous pouvez consulter mon [github](https://github.com/simonboisset/simonboisset) pour voir le code de mon blog.

## Tour des outils existants

Il existe de nombreux outils pour créer un blog statique. Voici une liste non exhaustive :

### Docusaurus

[Docusaurus](https://docusaurus.io/) est un générateur de site statique open source créé par Facebook. Il est principalement utilisé pour créer des sites de documentation mais il est possible de l'utiliser pour créer un blog statique.

### Nextra

[Nextra](https://nextra.site/) est un générateur de site statique open source basé sur Next.js. Il est pensé pour créer des documentations comme Docusaurus.

## Pourquoi ne pas utiliser ces outils ?

A vrai dire la première version de mon site était basée sur Docusaurus. C'est un super outil et je le recommande pour toute personne qui souhaite créer un site de documentation clé en main.

Malgré tout, j'avais quand même la sensation d'avoir un setup over ingineered pour un simple blog. Et j'avais envie de partir de zéro pour mieux maitriser les fonctionnalités de mon site.

J'ai donc décidé de réécrire mon site avec Next.js uniquement.

> J'aurais pu utiliser Remix également cela pourra être l'objet d'un prochain article.

## Setup du projet

Je suis parti d'un projet Next.js vierge. J'avais besoin d'un design system. J'ai donc choisi [Tailwind CSS](https://tailwindcss.com/) et [shadcn](https://ui.shadcn.com/) qui est un design system basé sur Tailwind CSS et [radix ui](https://www.radix-ui.com/) pour gérer l'accéssibilité.

Avec ces outils, j'ai pu créer un design system complet et personnalisable et implémenter facilement la landing page de mon site.

Il ne me reste plus qu'a créer les pages de mon blog.

## Les articles du blog

Pour créer les articles de mon blog, j'ai choisi d'utiliser markdown. C'est un format de fichier très simple à écrire et à lire. Il est également très facile de le transformer en HTML.

Je souahaitais également avoir un système de traduction. J'ai donc créé un dossier `posts` dans lequel j'ai créé un dossier par article. Chaque dossier contient un fichier markdown par langue.

```
posts
├── my-first-post
│   ├── en.md
│   └── fr.md
└── my-second-post
    ├── en.md
    └── fr.md
```

## Importer les articles

Afin d'import les articles, j'ai choisi d'utiliser `raw-loader` pour importer les fichiers markdown et `marked` pour parser le markdown en HTML.

```bash
yarn add raw-loader marked
```

Il faut modifier le fichier `next.config.js` pour ajouter le loader `raw-loader` :

```js
module.exports = {
  webpack: (config) => {
    config.module.rules.push({
      test: /\.md$/,
      use: "raw-loader",
    });
    return config;
  },
};
```

Ensuite, il faut créer un fichier `posts.ts` qui sera un server action. Ce fichier va importer les fichiers markdown et les parser en HTML.

```ts
import "server-only";

import { redirect } from "next/navigation";
import z from "zod";
import { Locale } from "../../../dictionaries";

const postSlugs = ["my-first-post", "my-second-post"] as const;
export const postSlugSchema = z.enum(postSlugs);

export const getPost = async (slug: string, locale: Locale) => {
  const safeSlug = postSlugSchema.safeParse(slug);
  if (!safeSlug.success) {
    throw redirect("/blog");
  }
  return import(`./${slug}/${locale}.md`).then(
    (module) => module.default as string
  );
};
```

Pour récupérer la liste des articles, j'ai créé une fonction `getPostList` qui va importer tous les articles et les parser en objet.

```ts
export const getPostList = async (locale: Locale) => {
  const posts = await Promise.all(
    postSlugs.map(async (slug) => {
      const post = await import(`./${slug}/${locale}.md`).then(
        (module) => module.default as string
      );

      return {
        slug,
        title: getTitleFromMarkdown(post),
        preview: getPreviewFromMarkdown(post),
        img: getImgFromMarkdown(post),
        date: getDateFromMarkdown(post),
      };
    })
  );
  return posts;
};
```

Il ne reste plus qu'a créer un composant pour afficher un post.

On créer un dossier post pour le router et on créer un dossier enfant `[slug]`.

La page `post/[slug]` va récupérer le contenu du post et l'afficher.

```tsx
export default async function BlogPostPage({
  params: { slug, lang },
}: {
  params: { slug: string; lang: Locale };
}) {
  const md = await getPost(slug, lang);

  return <div dangerouslySetInnerHTML={{ __html: marked(md) }} />;
}
```

Et voila, on a un blog statique avec Next.js.

Bon, par contre ce n'est pas super beau encore. Il va falloir ajouter un peu de style. Et pour cela, on va utiliser Tailwind CSS.

## Ajouter du style

Pour ajouter du style, on va utiliser une extension de Tailwind CSS : `@tailwindcss/typography`.

```js
plugins: [require('@tailwindcss/typography')],
```

Maintenant on peut ajouter un layout à notre page.

```tsx
export default async function BlogPostLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div
      className={cn(
        "prose mx-auto prose-p:text-justify prose-a:underline prose-a:font-bold",
        "prose-img:rounded-lg prose-img:shadow-lg prose-img:object-cover prose-img:p-0",
        "prose-headings:text-primary prose-p:text-primary prose-code:text-primary",
        "prose-li:text-primary prose-a:text-primary prose-li:marker:text-primary",
        "w-full max-w-screen-lg mx-auto mt-32 px-12 prose-pre:text-primary prose-pre:bg-foreground/10",
        "prose-pre:rounded-lg prose-pre:shadow-lg prose-pre:overflow-x-auto prose-pre:p-6"
      )}
    >
      {children}
    </div>
  );
}
```

Maintenant, on a un blog statique avec un style qu'on va pouvoir améliorer au gré de nos envies.

## Conclusion

Si vous souhaitez créer un blog statique, Next.js complété par Tailwind CSS est un combo gagnant. Vous avez un setup simple et flexible qui vous permettra de créer un blog statique à votre image. En plus avec les server components, vous avez la meilleure experience de développement possible.

> Je vous donne un dernier hack pour écrire vos articles. Utiisez copilot pour vous assister dans l'écriture de vos articles. C'est un gain de temps énorme. Il suffit de l'activer dans les settings de VSCode.

```json
"github.copilot.enable": {
    "markdown": true
  }
```

---

Je suis Simon Boisset, développeur fullstack freelance. Je travaille principalement avec React, React Native et Node.js. Je suis disponible pour des missions de développement ou de conseil. N'hésitez pas à me contacter sur [mon site](https://simonboisset.com/).

## Related links
- [Blog index](https://simonboisset.com/blog)
- [Website](https://simonboisset.com/)

# Créer et publier un package npm avec esbuild, typescript et react

Type: blog post
Language: fr-FR
Canonical URL: https://simonboisset.com/blog/publish-npm-library-with-esbuild
Published: 2 octobre 2022
Summary: Introduction Personnellement j'apprécie pouvoir partager mon code avec la communauté open source. Publier des packages npm pour cela est une des principales...

## Introduction

Personnellement j'apprécie pouvoir partager mon code avec la communauté open source. Publier des packages npm pour cela est une des principales options. Mais pour s'assurer que son code soit exécutable sur n'importe quel environnement il faut certains prérequis.

La règle numéro 1 à suivre est de supporter le common js.

Pendant longtemps il fallait utiliser un bundler comme rollup, webpack ou babel.

Mais aujourd'hui une nouvelle génération de bundler arrivent, plus performants et plus faciles à paramétrer. esbuild est l'un d'eux et nous allons l'utiliser pour publier notre premier module npm.

## Initialisation

Créez un projet et initialisez le.

```bash
yarn init
```

Votre fichier package.json doit être comme ceci :

```json
{
  "name": "my-react-library",
  "version": "1.0.0"
}
```

Maintenant, installons nos librairies.

## Les dev dependencies

```bash
yarn add -D react react-dom typescript @types/react @types/react-dom
```

> Pensez à ajouter react comme peer dependency si vous souhaitez écrire une librairie react avec des hooks ou des composants.

```json
"peerDependencies": {
   "react": ">=16"
 },
```

## Typescript config

Ajoutez le fichier `tsconfig.json`.

```json
{
  "include": ["src"],
  "compilerOptions": {
    "module": "esnext",
    "target": "esnext",
    "lib": ["dom", "dom.iterable", "esnext"],
    "declaration": true,
    "strict": true,
    "moduleResolution": "node",
    "jsx": "react",
    "skipLibCheck": true,
    "esModuleInterop": true,
    "emitDeclarationOnly": true,
    "outDir": "dist",
    "rootDir": "src"
  }
}
```

> On n'utilise pas le compilateur typescript car nous allons utiliser esbuild pour cela. C'est pour ça que `emitDeclarationOnly` est à true

## Notre module

Maintenant, on peut écrire le module de nos envies. Voici un exemple basique.

Créez un dossier `src` puis un fichier `index.tsx`.

```tsx
export const MyComponent = () => {
  return <div>Hello word</div>;
};
```

## Build de notre librairie

On souhaite maintenant builder notre package en CJS avant de le publier.

### Configuration esbuild

Ajoutez esbuild :

```bash
yarn add -D esbuild esbuild-node-externals
```

Créez un script esbuild dans un fichier `esbuild.js` :

```js
const esbuild = require("esbuild");
const { nodeExternalsPlugin } = require("esbuild-node-externals");
esbuild
  .build({
    entryPoints: ["./src/index.ts"],
    outfile: "dist/index.js",
    bundle: true,
    minify: true,
    treeShaking: true,
    platform: "node",
    format: "cjs",
    target: "node14",
    plugins: [nodeExternalsPlugin()],
  })
  .catch(() => process.exit(1));
```

### Ajoutez la commande de build

Enfin, ajoutez le script dans le fichier package.json :

```json
 "scripts": {
    "build": "rm -rf dist && node esbuild.js && tsc"
  },
```

Maintenant, vous pouvez lancer le script :

```bash
yarn build
```

## Npm publish

> Avant de publier sur npm vous devez vous [authentifier](https://docs.npmjs.com/creating-a-new-npm-user-account)

Ajoutez ces lignes à votre package.json :

```json
 "main": "dist/index.js",
  "files": [
    "dist"
  ],
```

Puis publiez votre package :

```bash
npm publish
```

Félicitations, venez de publier votre propre librairie Typescript.

## Automatiser la publication avec les Github actions

Finalement on veut juste publier une nouvelle version quand on déclenche une action sur Github. On va donc ajouter un fichier
`.github/workflows/publish.yml`:

```yml
name: Publish
on:
  workflow_dispatch:
    inputs:
      release:
        description: "major | minor | patch"
        required: true
        default: "patch"
        type: choice
        options:
          - major
          - minor
          - patch
jobs:
  publish-new-version:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout main
        uses: actions/checkout@v2
      - name: Use Node
        uses: actions/setup-node@v1
        with:
          node-version: "14"
          registry-url: https://registry.npmjs.org/
      - name: Install dependencies
        run: yarn
      - name: Build
        run: yarn build
      - name: Publish New Version
        env:
          NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
        run: |
          git config --local user.email "myEmail"
          git config --local user.name "myUsername"
          yarn version --new-version ${{github.event.inputs.release}} --no-git-tag-version
          yarn publish --access public
          PACKAGE_VERSION=$(node -p "require('./package.json').version")
          git commit -a -m "v${PACKAGE_VERSION}"
          git push
```

### Ajoutez votre npm token

Pour faire fonctionner les actions on doit enregistrer un secret NPM_TOKEN.

Il faut générer un access token sur npm :

![Npm access token](https://directus.services.lezo.dev/assets/213a95c1-1ce9-45b4-bc5c-bd23c8ea2dca)


Copiez le token dans les secrets Github :
![Github action secret](https://directus.services.lezo.dev/assets/9bc983ff-e328-475b-ae4c-37799b7ee0f3)


Maintenant tout fonctionne, votre publication se fera toute seule.
![Trigger github action](https://directus.services.lezo.dev/assets/cb86ea78-a42a-43a3-b068-f513139573b7)


---

Je suis Simon Boisset, développeur fullstack freelance. Je travaille principalement avec React, React Native et Node.js. Je suis disponible pour des missions de développement ou de conseil. N'hésitez pas à me contacter sur [mon site](https://simonboisset.com/).

## Related links
- [Blog index](https://simonboisset.com/blog)
- [Website](https://simonboisset.com/)

# Partager son code entre des projets React et React Native sur un monorepo

Type: blog post
Language: fr-FR
Canonical URL: https://simonboisset.com/blog/share-react-native-monorepo
Published: 2 octobre 2022
Summary: Introduction Comment lier un package npm core entre un projet web React par exemple et un projet mobile React Native ? Dans cet article, j'ai fait le choix d...

## Introduction

Comment lier un package npm `core` entre un projet web React par exemple et un projet `mobile` React Native ?

Dans cet article, j'ai fait le choix d'utiliser `yarn` comme gestionnaire de paquet. Il est certainement possible de le réaliser avec `pnpm` ou même `npm`, mais je n'ai pas fait le test.

## Pourquoi partager des packages en local

Quand on travaille sur des projets qui impliquent plusieurs sous-projets à la fois, il arrive très souvent qu'on ait de la logique à partager entre son api et son front voir son mobile.

Pour limiter la duplication de code, la première solution est de créer un package supplémentaire qu'on appelle `core` par exemple pour le partager en local comme une dépendance partagée entre chaque projet.

Tous les projets vont se retrouver avec cette dépendance dans leur fichier package.json :

```json
"dependencies": {
   "core": "*"
}
```

L'inconvénient c'est qu'il faut publier le package `core` pour l'installer et cela rend le développement local ainsi que la logique de la CI complexe.

## Yarn link

Heureusement, une première solution simple à ce problème existe. La commande `yarn link` permet de créer un lien symbolique en local entre nos différents packages. Pour ça, il suffit d'aller exécuter les commandes suivantes :

```sh
cd core
yarn link
yarn build

cd web
yarn link core

cd server
yarn link core
```

Maintenant vos projets `server` et `web` utiliseront la version locale de votre build de `core`.

## Le cas React Native

On va maintenant complexifier notre projet en ajoutant une application mobile React Native. On fait le choix de créer le projet avec [Expo](https://docs.expo.dev/).

Le problème dans ce cas, c'est que les liens symboliques ne fonctionnent pas. Votre application vous enverra un joli crash au démarrage.

Nous allons voir ensemble comment réussir a partager son code proprement entre tous ces projets.

# Monorepo

C'est sympa de lier ses packages entre eux en local, mais quand on commence à en avoir de nombreux ça peut devenir lourd à mettre en place.

Une des solutions les plus utilisée aujourd'hui est de passer par un monorepo, c'est-à-dire un repo qui centralisera tout notre code. Ça permet de simplifier l'organisation du code et le versionning puisque tous les projets suivent les déploiements au même rythme.

## Yarn workspace

Toujours en utilisant yarn, maintenant que nous avons nos projets sur un monorepo nous allons utiliser une configuration très utile pour un monorepo qu'est le [workspace yarn](https://classic.yarnpkg.com/lang/en/docs/workspaces/).

Pour faire simple au lieu de faire un `yarn install` pour chacun de vos projets vous allez le faire une seule fois à la racine de votre monorepo et yarn va installer tous vos node_modules à la racine.

## Turborepo

Pour mettre en place notre monorepo nous allons utiliser [Turborepo](https://turborepo.org/) qui va s'occuper de nous installer les paramètres de base.

Je ne rentre pas dans les détails de cette configuration, car cela pourrait être le sujet d'un article complet et la doc est très bien faite.

```sh
npx create-turbo@latest
```

# Notre monorepo

C'est parti !
Maintenant, créons nos projets web et mobile :

```sh
cd apps
yarn create react-app web
...

npx create-expo-app mobile
```

Maintenant créons notre package core :

```sh
cd packages
mkdir core
yarn init
...
```

Si vous ne savez pas comment créer un package node réutilisable en dépendance, vous pouvez lire [mon article](https://dev.to/simonboisset/create-and-publish-npm-module-library-with-esbuild-typescript-and-react-1acn) à ce sujet.

Il reste à ajouter dans les dépendance de web et mobile :

```json
"dependencies": {
   "core": "*"
}
```

## Metro config

On se rapproche du but. Vous pouvez déjà build `core` et faire tourner `web` avec.

Pour le mobile, ça ne passe toujours pas, mais rassurez vous, on y est presque.

En réalité maintenant notre build de core est situé à la racine de notre projet. Il suffit de préciser à `metro` d'aller chercher ses `node_module` non pas dans sont dossier mais à la racine.

On va d'abord modifier la propriété main du `package.json` :

```json
"main": "index.js"
```

Maintenant on créer le fichier `index.js` :

```js
import { registerRootComponent } from "expo";

import App from "./App";
registerRootComponent(App);
```

Enfin on créer la configuration `metro` pour le monorepo avec le fichier `metro.config.js` :

```js
const { getDefaultConfig } = require("expo/metro-config");
const path = require("path");

const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, "../..");

const config = getDefaultConfig(projectRoot);

config.watchFolders = [workspaceRoot];
config.resolver.nodeModulesPaths = [
  path.resolve(projectRoot, "node_modules"),
  path.resolve(workspaceRoot, "node_modules"),
];
config.resolver.disableHierarchicalLookup = true;

module.exports = config;
```

Maintenant refaite un test en faisant un build de `core` et en lançant le mobile et vous êtes bons pour démarrer votre développement.

L'exemple de cet article peut être retrouvé [ici](https://github.com/simonboisset/examples/tree/main/shared-react-native-monorepo).

---

Je suis Simon Boisset, développeur fullstack freelance. Je travaille principalement avec React, React Native et Node.js. Je suis disponible pour des missions de développement ou de conseil. N'hésitez pas à me contacter sur [mon site](https://simonboisset.com/).

## Related links
- [Blog index](https://simonboisset.com/blog)
- [Website](https://simonboisset.com/)

# Comment configurer esbuild pour créer une application React avec Typescript

Type: blog post
Language: fr-FR
Canonical URL: https://simonboisset.com/blog/create-react-app-with-esbuild
Published: 25 août 2022
Summary: Introduction esbuild: https://esbuild.github.io/ est un compilateur JavaScript rapide et simple qui prend en charge JSX et TypeScript. Dans cet article, nous...

## Introduction

[esbuild](https://esbuild.github.io/) est un compilateur JavaScript rapide et simple qui prend en charge JSX et TypeScript. Dans cet article, nous allons configurer esbuild pour créer une application React avec Typescript.

Vous pouvez vérifier le code sur ce [repos](https://github.com/simonboisset/templates/tree/main/templates/react-spa).

> J'ai écrit cet article en 2022. C'est plus un POC qu'une véritable application prête pour la production. Je l'utilise pour tester esbuild et créer un modèle pour mes futurs projets. Aujourd'hui, je ne recommanderais pas styled-components, mais j'utiliserais plutôt tailwindcss.

## Initialisation

Créez votre dossier de projet et initialisez-le.

```bash
yarn init
```

```json
{
  "name": "esbuild-static",
  "version": "1.0.0"
}
```

## Installer les dépendances

```bash
yarn add esbuild dotenv react react-dom styled-components
```

Ensuite, ajoutez les devdependencies.

```bash
yarn add --dev typescript @types/react @types/react-dom @types/styled-components @types/node serve-handler @types/serve-handler
```

## Configuration Typescript

Ajoutez le fichier `tsconfig.json`.

```json
{
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src",
    "module": "commonjs",
    "target": "ESNext",
    "lib": ["dom", "dom.iterable", "esnext"],
    "moduleResolution": "node",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noImplicitReturns": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "jsx": "react"
  },
  "include": ["src"],
  "exclude": ["**/node_modules", "**/.*/"]
}
```

## Configuration Esbuild

Créez le dossier `esbuild` puis ajoutez les fichiers `dev.js` et `prod.js`.

La configuration dev surveille les modifications des fichiers et démarre un serveur pour le rechargement à chaud et les fichiers statiques. Vous pouvez également ajouter des variables d'environnement.

```js
const { spawn } = require("child_process");
const esbuild = require("esbuild");
const { createServer, request } = require("http");
require("dotenv").config();
const handler = require("serve-handler");

const clientEnv = { "process.env.NODE_ENV": `'dev'` };
const clients = [];

Object.keys(process.env).forEach((key) => {
  if (key.indexOf("CLIENT_") === 0) {
    clientEnv[`process.env.${key}`] = `'${process.env[key]}'`;
  }
});

const openBrowser = () => {
  setTimeout(() => {
    const op = {
      darwin: ["open"],
      linux: ["xdg-open"],
      win32: ["cmd", "/c", "start"],
    };
    if (clients.length === 0)
      spawn(op[process.platform][0], ["http://localhost:3000"]);
  }, 1000);
};

esbuild
  .build({
    entryPoints: ["src/index.tsx"],
    bundle: true,
    minify: true,
    define: clientEnv,
    outfile: "dist/index.js",
    sourcemap: "inline",
    watch: {
      onRebuild(error) {
        setTimeout(() => {
          clients.forEach((res) => res.write("data: update\n\n"));
        }, 1000);
        console.log(error || "client rebuilt");
      },
    },
  })
  .catch((err) => {
    console.log(err);
    process.exit(1);
  });

esbuild.serve({ servedir: "./" }, {}).then((result) => {
  createServer((req, res) => {
    const { url, method, headers } = req;
    if (req.url === "/esbuild") {
      return clients.push(
        res.writeHead(200, {
          "Content-Type": "text/event-stream",
          "Cache-Control": "no-cache",
          "Access-Control-Allow-Origin": "*",
          Connection: "keep-alive",
        })
      );
    }

    const path = url.split("/").pop().indexOf(".") ? url : `/index.html`;
    const proxyReq = request(
      { hostname: "0.0.0.0", port: 8000, path, method, headers },
      (prxRes) => {
        res.writeHead(prxRes.statusCode, prxRes.headers);
        prxRes.pipe(res, { end: true });
      }
    );
    req.pipe(proxyReq, { end: true });
    return null;
  }).listen(5010);

  createServer((req, res) => {
    return handler(req, res, { public: "dist" });
  }).listen(3000);

  openBrowser();
});
```

```js
const esbuild = require("esbuild");
require("dotenv").config();

const clientEnv = { "process.env.NODE_ENV": `'production'` };
for (const key in process.env) {
  if (key.indexOf("CLIENT_") === 0) {
    clientEnv[`process.env.${key}`] = `'${process.env[key]}'`;
  }
}
esbuild
  .build({
    entryPoints: ["src/index.tsx"],
    bundle: true,
    minify: true,
    define: clientEnv,
    outfile: "dist/index.js",
  })
  .catch(() => process.exit(1));
```

## Ajouter des scripts

```json
"scripts": {
    "build": "node esbuild/prod",
    "type-check": "tsc --noEmit",
    "lint": "eslint src/**/*.ts src/**/*.tsx",
    "start": "nodemon --watch dist --exec 'yarn type-check & yarn lint' & node esbuild/dev"
  },
```

## React app

Créez le fichier `src/index.tsx`.

```tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import GlobalStyle from "./globalStyle";

ReactDOM.render(
  <>
    <GlobalStyle />
    <App />
  </>,
  document.getElementById("root")
);
```

## Hot reload

Pour écouter le rechargement du serveur esbuild dev, nous devons ajouter un hook pour le développement.

```tsx
import { useEffect } from "react";

const useHMR = () => {
  useEffect(() => {
    if (process.env.NODE_ENV !== "production") {
      new EventSource("http://localhost:5010/esbuild").onmessage = () =>
        window.location.reload();
    }
  }, []);
};
export default useHMR;
```

## Global style

Ajoutez le fichier `src/globalStyle.ts`.

```css
import { createGlobalStyle } from 'styled-components';

 const GlobalStyle = createGlobalStyle`
  body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}
.App {
  text-align: center;
}

.App-logo {
  height: 40vmin;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .App-logo {
    animation: App-logo-spin infinite 20s linear;
  }
}

.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.App-link {
  color: #61dafb;
}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
`;
export default GlobalStyle
```

## App

Créez le fichier `src/App.tsx`.

```tsx
import React from "react";
import useHMR from "./useHMR";

const App: React.FC = () => {
  useHMR();
  return (
    <div className="App">
      <header className="App-header">
        <img src="/logo.svg" className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
};

export default App;
```

## Fichiers statiques

Pour ajouter des fichiers statiques, créez le dossier `public` et ajoutez le fichier `logo.svg`.

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="React App" />
    <link rel="apple-touch-icon" href="/logo192.png" />
    <link rel="manifest" href="/manifest.json" />
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>
<script src="/index.js"></script>
```

Puis ajoutez les autres fichiers statiques comme `favicon.ico`, `logo192.png` et `manifest.json`.

## Lancer le serveur

```bash
yarn start
```

## Build

```bash
yarn build
```

## Conclusion

Vous pouvez maintenant créer une application React avec Typescript et esbuild.

---

Je suis Simon Boisset, développeur fullstack freelance. Je travaille principalement avec React, React Native et Node.js. Je suis disponible pour des missions de développement ou de conseil. N'hésitez pas à me contacter sur [mon site](https://simonboisset.com/).

## Related links
- [Blog index](https://simonboisset.com/blog)
- [Website](https://simonboisset.com/)

# Publier une librairie avec plusieurs packages à l'aide de Turborepo

Type: blog post
Language: fr-FR
Canonical URL: https://simonboisset.com/blog/share-packages-monorepo
Published: 10 février 2022
Summary: Plusieurs packages ? Maintenant que nous savons publier un package sur npm, nous allons voir comment gérer une librairie qui comporte plusieurs packages. Vou...

## Plusieurs packages ?

Maintenant que nous savons publier un package sur npm, nous allons voir comment gérer une librairie qui comporte plusieurs packages.

Vous pouvez consulter [cet exemple](https://github.com/simonboisset/validest) pour voir une application concrète.

## Configuration

Pour gérer plusieurs modules ensemble nous allons utiliser Turborepo que vous avez déjà pu voir dans d'un article précédent.

```bash
npx create-turbo@latest
```

Vous pouvez supprimer les différents exemples de packages de base, nous allons créer les nôtres par la suite.

## Builder

On va commencer par créer un package pour gérer les builds de nos librairies. Celui-ci ne sera pas publié mais uniquement partagé dans notre monorepo.

Créez un dossier `builder` dans `packages` puis créez les fichiers suivants :

`package.json`

```json
{
  "name": "builder",
  "private": true,
  "version": "0.1.0",
  "bin": "index.js",
  "dependencies": { "esbuild": "^0.14.42", "esbuild-node-externals": "^1.4.1" }
}
```

`index.js`

```js
#!/usr/bin/env node

const esbuild = require("esbuild");
const { nodeExternalsPlugin } = require("esbuild-node-externals");
esbuild
  .build({
    entryPoints: ["./src/index.ts"],
    outfile: "dist/index.js",
    bundle: true,
    minify: true,
    treeShaking: true,
    platform: "node",
    format: "cjs",
    target: "node14",
    plugins: [nodeExternalsPlugin()],
  })
  .catch(() => process.exit(1));
```

## Nos packages

Maintenant nous pouvons créer nos différents packages. Pour l'exemple nous allons en créer deux.

### Core

Toujours dans le dossier `packages` on va créer un dossier `core` et les fichiers suivants :

`package.json`

```json
{
  "name": "@example/core",
  "version": "0.1.0",
  "description": "A description",
  "main": "dist/index.js",
  "typings": "dist/index.d.ts",
  "files": ["dist"],
  "scripts": {
    "build": "rm -rf dist && builder && tsc"
  },
  "author": "Your name",
  "license": "MIT",
  "devDependencies": {
    "builder": "*",
    "typescript": "^4.7.4"
  }
}
```

> Vous remarquerez que nous avons utilisé un scope pour notre nom de package `@example`. Pour utiliser le votre vous devez créer une organisation du même nom sur npm.

`src/index.ts`

```ts
export const hello = (name: string) => {
  return "Hello " + name;
};
```

`tsconfig.json`

```json
{
  "extends": "builder/ts.json",
  "include": ["src"],
  "exclude": ["src/**/*.test.ts"],
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src",
    "module": "esnext",
    "target": "esnext",
    "lib": ["dom", "dom.iterable", "esnext"],
    "declaration": true,
    "strict": true,
    "moduleResolution": "node",
    "jsx": "react",
    "skipLibCheck": true,
    "esModuleInterop": true,
    "emitDeclarationOnly": true
  }
}
```

## Age

Même chose pour notre deuxième package `@example/age`

`package.json`

On ajoute une dependance à core :

```json
"dependencies": { "@example/core": "0.1.0" },
```

`index.ts`

```ts
import { hello } from "@example/core";

export const age = (name: string, age: number) => {
  return hello(name) + " you are " + age;
};
```

## Turbo build

Nos deux packages sont prêts, il ne reste plus qu'à les builder ensemble. Il faut tout de même paramétrer Turborepo pour builder `core` avant `age`.

A la racine du repo :

`turbo.json`

```json
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    }
  }
}
```

`package.json`

```json
{
  "name": "example",
  "version": "0.1.0",
  "private": true,
  "workspaces": ["packages/*"],
  "scripts": {
    "build": "turbo run build --filter=@example/*"
  },
  "devDependencies": {
    "prettier": "latest",
    "turbo": "latest"
  },
  "engines": {
    "npm": ">=7.0.0",
    "node": ">=14.0.0"
  },
  "prettier": {
    "singleQuote": true,
    "tabWidth": 2,
    "printWidth": 120
  },
  "packageManager": "yarn@1.22.5"
}
```

On peut déjà exécuter les commandes suivantes pour un premier build :

```bash
yarn
yarn build
```

## Test

Si vous souhaitez ajouter des tests je vous conseille [Vitest](https://vitest.dev/). Vous pouvez suivre la docs de [Turborepo](https://turbo.build/repo/docs/handbook/testing) à ce sujet.

## Versionning

Une étape un peu complexe est de gérer les bonnes versions des dépendances. Le plus simple est de considérer que tous nos packages possèdent le même numéro de version. On va tout de même devoir utiliser un script pour incrémenter notre numéro de versions sur toutes nos dépendances avant chaque publication.

Pour ça on utilise `turboversion` :

```bash
yarn add -W -D turboversion
```

Le script sera exécuté avant chaque publication dans notre CI. Pour expliquer rapidement ce qu'il fait, il va scanner tout notre monorepo et incrémenter le numéro de version (patch, minor ou major) des dépendances et packages de notre scope `example`.

## Publication

On va reprendre notre Github Action de l'article précédent pour l'adapter à notre monorepo :

`.github/workflows/publish.yml`

```yml
name: Publish
on:
  workflow_dispatch:
    inputs:
      release:
        description: "major | minor | patch"
        required: true
        default: "patch"
        type: choice
        options:
          - major
          - minor
          - patch
jobs:
  publish-new-version:
    runs-on: ubuntu-latest
    steps:
      - name: 🔌 Checkout
        uses: actions/checkout@v3
      - name: 🏗 Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: 16.x
          cache: "yarn"
          registry-url: https://registry.npmjs.org/
          scope: "@example"
      - name: ⏳ Yarn install
        run: yarn
      - name: 🚀 Publish New Version
        env:
          NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
        run: |
          git config --local user.email "youremail"
          git config --local user.name "yourname"
          yarn turboversion example ${{github.event.inputs.release}}
          yarn publish:lib
          PACKAGE_VERSION=$(node -p "require('./package.json').version")
          git commit -a -m "v${PACKAGE_VERSION}"
          git push
```

Voilà, vous pouvez maintenant publier votre librairie et l'ensemble de ses packages avec un numéro de version cohérent.

---

Je suis Simon Boisset, développeur fullstack freelance. Je travaille principalement avec React, React Native et Node.js. Je suis disponible pour des missions de développement ou de conseil. N'hésitez pas à me contacter sur [mon site](https://simonboisset.com/).

## Related links
- [Blog index](https://simonboisset.com/blog)
- [Website](https://simonboisset.com/)

# How I use Expo to ship white-label on-premise mobile apps

Type: blog post
Language: en-US
Canonical URL: https://simonboisset.com/en/blog/expo-white-label-on-premise-apps
Published: June 25, 2026
Summary: Shipping a white-label mobile app should not require cloning the whole product: Expo makes it possible to keep one core and publish clean variants. On a prod...

Shipping a white-label mobile app should not require cloning the whole product: Expo makes it possible to keep one core and publish clean variants.

On a product like Questovery, the same React Native foundation may need to serve several contexts: the standard SaaS app, an internal preview app, a dedicated client app, or an on-premise variant with its own identity. The common mistake is to copy the mobile application for every client. It works at first, then every fix, native dependency and store release becomes repeated work.

The approach I prefer is different: **a shared mobile shell, thin variants, and Expo/EAS configuration that carries the native identity of each build**.

## The real problem with white-label apps

A white-label app is not just a logo change. It can involve:

- a different app name
- dedicated iOS bundle identifiers and Android package names
- a specific deep link scheme
- distinct native assets
- different associated domains
- API, web and PowerSync endpoints for the right instance
- isolated EAS Update channels
- sometimes a separate store listing and release strategy

If all of that is duplicated across app folders, the debt appears quickly. You end up maintaining several almost-identical apps, with accidental differences that are hard to audit.

In Questovery, the goal is to keep common behavior in shared packages, while app folders only carry configuration, branding, EAS profiles and publishing metadata.

## One shared shell, explicit variants

The structure looks like this:

```txt
apps/mobile-on-prem
  App.tsx
  app.config.ts
  eas.json
  src/on-prem-brands.js
  assets/branding/*
  .eas/workflows/*

packages/mobile-shell
packages/sdk
packages/common
```

The mobile shell owns screens, navigation, authentication, runtime services, analytics and the game experience. The `apps/mobile-on-prem` folder only decides which variant to load.

A variable such as `APP_VARIANT` selects the active brand:

```ts
const brandConfig = resolveOnPremBrandConfig(process.env.APP_VARIANT);
```

That variant can define the app name, native identifiers, assets, domains and endpoints. Business behavior stays in the shell and shared packages.

That is the key point: **the brand is configuration, not a product fork**.

## `app.config.ts` as the native assembly point

Expo is a good fit for this model because `app.config.ts` can be dynamic. At build time, the active variant is resolved and native fields are injected for iOS, Android and EAS.

Simplified example:

```ts
export default context => {
  const brandConfig = resolveOnPremBrandConfig(process.env.APP_VARIANT);

  return buildExpoConfig(
    {
      ...brandConfig.brand,
      easProjectId: 'shared-project-id',
      slug: 'questovery-on-premise',
    },
    version,
  )(context);
};
```

In this model, the Expo project can remain shared by the app folder, while operational isolation happens elsewhere: bundle ids, package names, schemes, assets, EAS profiles, channels, endpoints and store listings.

This avoids turning the Expo project id into a client-facing business property. The EAS project belongs to the app folder. The variant belongs to the build.

## `eas.json` owns the environments

Then `eas.json` describes the available profiles. A variant can have `dev`, `staging` and `production` profiles, each with its own public variables:

```json
{
  "build": {
    "client-staging": {
      "extends": "staging",
      "channel": "onprem-client-staging",
      "env": {
        "APP_VARIANT": "client",
        "EXPO_PUBLIC_EAS_ENV": "preview",
        "EXPO_PUBLIC_INSTANCE_KEY": "client-instance",
        "EXPO_PUBLIC_API_URL": "https://api-staging.client.example.com",
        "EXPO_PUBLIC_WEB_URL": "https://staging.client.example.com",
        "EXPO_PUBLIC_POWERSYNC_URL": "https://powersync-staging.client.example.com"
      }
    }
  }
}
```

There are two categories worth keeping separate:

- `APP_VARIANT` is a native build-time variable used to resolve the brand
- `EXPO_PUBLIC_*` values are bundle/update-time inputs embedded in JavaScript or an OTA update

That separation prevents native identity, runtime endpoints and product rules from being mixed together.

## OTA updates: channels, branches and variables matter

With EAS Update, OTA updates are very useful for white-label variants, but they should not become a dangerous shortcut.

A simple rule works well:

- if the change affects the native binary, create a new build
- if the change only affects JavaScript compatible with the existing runtime, publish an OTA update

Modern workflows can compute a fingerprint, look for an existing build, then choose between a native build and an update. This is especially useful for on-premise apps where some variants should not build on every commit.

One detail has saved me from real mistakes: `fingerprint`, `update` and channel-linking jobs must receive the same variables as the EAS profile they target. An update job should not assume it automatically inherits everything from the build profile.

In practice, I prefer to test that contract: if a workflow publishes an OTA for `APP_VARIANT=client`, it must also carry `EXPO_PUBLIC_API_URL`, `EXPO_PUBLIC_WEB_URL`, `EXPO_PUBLIC_INSTANCE_KEY` and any other values required by that variant.

## When to reuse a shared on-prem app

Not every white-label brand deserves a new mobile folder.

I keep a shared on-prem app when:

- product behavior is identical
- the difference is mostly identity, endpoints, catalog or publishing
- clients can share the same shell and navigation rules
- divergences remain testable through configuration

I create a dedicated app when:

- the client has a real functional divergence
- the store strategy needs strong autonomy
- the data or sync model must evolve separately
- configuration risk becomes higher than the cost of a dedicated folder

This is an architecture decision, not only a build decision.

## Why this matters for Questovery

Questovery lets organizers create and run trails, scavenger hunts, guided visits and field activities with a mobile app for participants. For the dedicated offer, the point is not just adding a logo: the app has to feel coherent, isolated, connected to the right backend and maintainable over time.

That is exactly where Expo and EAS are useful: keep a solid common foundation, then industrialize variants through native configuration, channels and workflows.

For the product side of that model, I documented the dedicated offer here: [Questovery Dedicated, white-label mobile app and isolated environment](https://www.questovery.com/en/offers/dedicated#white-label-apps).

## The real benefit

White-label becomes sustainable when it is treated as a platform problem:

- client differences are explicit
- builds are reproducible
- OTA updates stay isolated
- the mobile shell remains shared
- on-premise choices are visible in configuration
- tests can verify workflow contracts

Expo does not remove the complexity. It gives you a good place to put it.

## Related links
- [Blog index](https://simonboisset.com/en/blog)
- [Website](https://simonboisset.com/en)

# Expo CI/CD workflows (EAS): smarter builds, fingerprinting, and OTA

Type: blog post
Language: en-US
Canonical URL: https://simonboisset.com/en/blog/expo-ci-cd-workflows-fingerprint-ota
Published: January 12, 2026
Summary: Expo CI/CD workflows (EAS): smarter builds, fingerprinting, and OTA In an Expo/React Native project, the real question isn't "how do I build?" but "when do I...

# Expo CI/CD workflows (EAS): smarter builds, fingerprinting, and OTA

In an Expo/React Native project, the real question isn't "how do I build?" but "when do I truly need to build?". Native builds are slow (and expensive in CI), while OTA updates are fast... but they don't cover every kind of change.

The goal of this workflow is to **automate the full chain** (native builds, OTA updates, submissions) **while avoiding unnecessary builds**, using **Expo fingerprinting** and **explicit versioning**.

---

## Typical structure: staging + production

Two environments, one flow:

- `dev`/`staging` -> **preview** environment
- `main` -> **production** environment

Same logic in both cases:

1. compute a fingerprint
2. decide between **native build** or **OTA**
3. publish automatically

In production, add **store submission** when a native build happened.

---

## Fingerprinting: automatic decisions

The fingerprint is a hash based on what really impacts the native binary:

- native dependencies and modules
- Expo config and plugins
- config files
- marketing version (if injected into `app.config`)

Simple rule:

- **Fingerprint matches** an existing build -> **OTA**
- **Fingerprint differs** -> **native build**

---

## Generic EAS workflow example

This is a real-world inspired example, but intentionally generic. Adjust `paths`, `profile`, and `channel` to your project.

```yml
name: Production builds

on:
  push:
    branches:
      - main
    tags:
      - v*.*.*
      - "!v*.*.*-**"
    paths:
      - apps/mobile/**
      - packages/**
      - "!**/*.md"

jobs:
  fingerprint:
    name: Fingerprint
    type: fingerprint
    environment: production

  get_android_build:
    name: Check existing Android build
    needs: [fingerprint]
    type: get-build
    environment: production
    params:
      fingerprint_hash: ${{ needs.fingerprint.outputs.android_fingerprint_hash }}
      profile: production

  get_ios_build:
    name: Check existing iOS build
    needs: [fingerprint]
    type: get-build
    environment: production
    params:
      fingerprint_hash: ${{ needs.fingerprint.outputs.ios_fingerprint_hash }}
      profile: production

  build_android:
    name: Build Android
    needs: [get_android_build]
    if: ${{ !needs.get_android_build.outputs.build_id }}
    type: build
    environment: production
    params:
      platform: android
      profile: production

  build_ios:
    name: Build iOS
    needs: [get_ios_build]
    if: ${{ !needs.get_ios_build.outputs.build_id }}
    type: build
    environment: production
    params:
      platform: ios
      profile: production

  submit_android:
    name: Submit Android
    needs: [build_android]
    if: ${{ needs.build_android.outputs.build_id }}
    type: submit
    environment: production
    params:
      build_id: ${{ needs.build_android.outputs.build_id }}
      profile: production

  submit_ios:
    name: Submit iOS
    needs: [build_ios]
    if: ${{ needs.build_ios.outputs.build_id }}
    type: submit
    environment: production
    params:
      build_id: ${{ needs.build_ios.outputs.build_id }}
      profile: production

  update_android:
    name: Update Android
    needs: [get_android_build, get_ios_build]
    if: ${{ needs.get_android_build.outputs.build_id && !needs.get_ios_build.outputs.build_id }}
    type: update
    environment: production
    params:
      channel: production
      platform: android

  update_ios:
    name: Update iOS
    needs: [get_android_build, get_ios_build]
    if: ${{ needs.get_ios_build.outputs.build_id && !needs.get_android_build.outputs.build_id }}
    type: update
    environment: production
    params:
      channel: production
      platform: ios

  update_all:
    name: Update All
    needs: [get_android_build, get_ios_build]
    if: ${{ needs.get_android_build.outputs.build_id && needs.get_ios_build.outputs.build_id }}
    type: update
    environment: production
    params:
      channel: production
      platform: all
```

---

## Versioning: explicit marketing version, auto-incremented build numbers

I intentionally separate:

- **marketing version** (readable, intentional): owned by us
- **build numbers** (`buildNumber` on iOS / `versionCode` on Android): auto-incremented by EAS

The marketing version lives in `package.json`:

```json
{
  "name": "my-app",
  "version": "1.4.0"
}
```

And is injected into `app.config.ts`:

```ts
import type { ConfigContext, ExpoConfig } from "expo/config";
import pkg from "./package.json";

export default ({ config }: ConfigContext): ExpoConfig => ({
  ...config,
  version: pkg.version,
});
```

---

## Conclusion & how I can help

This kind of pipeline **reduces build costs**, **speeds up release cycles**, and **makes production releases safer** with clear, automated rules.

👉 **Want to implement or improve an Expo CI/CD workflow (fingerprinting, OTA channels, versioning, automated submissions)?**

I can help you design the architecture, configure EAS, wire up CI, and apply Expo/React Native best practices end-to-end.

➡️ *Book a call and we'll build a workflow that fits your product and your team.*

## Related links
- [Blog index](https://simonboisset.com/en/blog)
- [Website](https://simonboisset.com/en)

# How to Create Your Own TypeScript Library in 2024: A Step-by-Step Guide

Type: blog post
Language: en-US
Canonical URL: https://simonboisset.com/en/blog/create-typescript-library-tsup
Published: July 24, 2024
Summary: In this tutorial, we'll walk through the process of creating a TypeScript library from scratch. We'll cover project setup, compilation, testing, and publishi...

In this tutorial, we'll walk through the process of creating a TypeScript library from scratch. We'll cover project setup, compilation, testing, and publishing. This guide is designed to be accessible for developers with basic knowledge of TypeScript and npm.

## Step 1: Project Initialization

Let's start by creating a new folder for our project and initializing an npm project.

```bash
mkdir my-ts-library
cd my-ts-library
npm init -y
```

This command creates a basic `package.json` file. We'll modify it later.

## Step 2: Installing Dependencies

Let's install TypeScript and the necessary tools for our project:

```bash
npm install --save-dev typescript tsup vitest
```

- `typescript`: The TypeScript compiler
- `tsup`: A build tool for TypeScript
- `vitest`: A fast test framework

## Step 3: TypeScript Configuration

Create a `tsconfig.json` file in the project root:

```json
{
  "include": ["src"],
  "exclude": ["**/*.test.ts"],
  "compilerOptions": {
    "module": "esnext",
    "target": "esnext",
    "lib": ["esnext"],
    "declaration": true,
    "strict": true,
    "moduleResolution": "node",
    "skipLibCheck": true,
    "esModuleInterop": true,
    "outDir": "dist",
    "rootDir": "src"
  }
}
```

This configuration tells TypeScript how to compile our code.

## Step 4: tsup Configuration

Create a `tsup.config.ts` file to configure our build:

```typescript
import { defineConfig } from "tsup";

export default defineConfig({
  entry: ["src/index.ts"],
  clean: true,
  format: ["cjs", "esm"],
  dts: true,
});
```

This configuration generates CommonJS and ES module builds, as well as TypeScript declaration files.

## Step 5: Vitest Configuration

Create a `vitest.config.ts` file to configure our tests:

```typescript
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    globals: true,
    environment: "node",
  },
});
```

## Step 6: Updating package.json

Let's update our `package.json` with the necessary information:

```json
{
  "name": "my-ts-library",
  "version": "0.1.0",
  "description": "My TypeScript Library",
  "main": "dist/index.js",
  "module": "dist/index.mjs",
  "types": "dist/index.d.ts",
  "files": ["dist"],
  "scripts": {
    "build": "tsup",
    "dev": "tsup --watch",
    "test": "vitest run",
    "test:watch": "vitest"
  },
  "keywords": ["typescript", "library"],
  "author": "Your Name",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/your-username/my-ts-library"
  },
  "bugs": {
    "url": "https://github.com/your-username/my-ts-library/issues"
  },
  "homepage": "https://github.com/your-username/my-ts-library#readme"
}
```

## Step 7: Writing Library Code

Create a `src` folder and an `index.ts` file inside it:

```bash
mkdir src
touch src/index.ts
```

In `src/index.ts`, let's write a simple function:

```typescript
export function greet(name: string): string {
  return `Hello, ${name}!`;
}
```

## Step 8: Writing Tests

Create a test file `src/index.test.ts`:

```typescript
import { expect, test } from "vitest";
import { greet } from "./index";

test("greet function", () => {
  expect(greet("World")).toBe("Hello, World!");
});
```

## Step 9: Build and Test

Let's run our scripts to build and test our library:

```bash
npm run build
npm test
```

If everything goes well, you should see that the test passes and the build files are generated in the `dist` folder.

## Step 10: Preparing for Publication

Before publishing, make sure your `package.json` is up to date with the correct version, description, and other metadata.

## Step 11: Publishing to npm

If you're ready to publish your library to npm, follow these steps:

1. Create an account on npmjs.com if you don't already have one.
2. Log in to npm via the terminal:

```bash
npm login
```

3. Publish your package:

```bash
npm publish
```

Congratulations! You have now created and published your own TypeScript library!

## Conclusion

This tutorial has guided you through the steps of creating a basic TypeScript library. Remember to add documentation, usage examples, and keep your library up to date. Good luck with your future projects!

## Related links
- [Blog index](https://simonboisset.com/en/blog)
- [Website](https://simonboisset.com/en)

# i18n: The Type-Safe Approach

Type: blog post
Language: en-US
Canonical URL: https://simonboisset.com/en/blog/i18n-type-safe-approach
Published: July 19, 2024
Summary: Internationalization (i18n) is a crucial aspect of modern web development. This article explores how to implement a type-safe i18n solution using the typed-l...

Internationalization (i18n) is a crucial aspect of modern web development. This article explores how to implement a type-safe i18n solution using the typed-locale library in a React application.

## Introduction to typed-locale

typed-locale is a lightweight, type-safe internationalization library designed to work with TypeScript. It provides an API for managing translations with type safety for both keys and variables.

## Setting Up the Project

Let's create a new React project using Vite with TypeScript:

```bash
npm create vite@latest my-i18n-app -- --template react-ts
cd my-i18n-app
npm install
```

Now, install typed-locale:

```bash
npm install typed-locale
```

## Defining Translations

Create a new file called `translations.ts` in the `src` folder:

```typescript
// src/translations.ts
import { InferTranslation, plural } from "typed-locale";

export const en = {
  greeting: "Hello, {{name}}!",
  itemCount: plural({
    none: "You have no items.",
    one: "You have one item.",
    other: "You have {{count}} items.",
  }),
  nav: {
    home: "Home",
    about: "About",
    contact: "Contact",
  },
} as const;

export type Translation = InferTranslation<typeof en>;

export const fr: Translation = {
  greeting: "Bonjour, {{name}} !",
  itemCount: plural({
    none: "Vous n'avez aucun article.",
    one: "Vous avez un article.",
    other: "Vous avez {{count}} articles.",
  }),
  nav: {
    home: "Accueil",
    about: "À propos",
    contact: "Contact",
  },
};
```

## Creating the Translator

Now, let's create a custom hook to use our translations. Create a new file called `useTranslator.ts`:

```typescript
// src/useTranslator.ts
import { createTranslatorFromDictionary } from "typed-locale";
import { useMemo } from "react";
import { en, fr, Translation } from "./translations";

const dictionary = { en, fr };

export const useTranslator = (locale: keyof typeof dictionary) => {
  return useMemo(
    () =>
      createTranslatorFromDictionary<Translation>({
        dictionary,
        locale,
        defaultLocale: "en",
      }),
    [locale]
  );
};
```

## Using the Translator in Components

Now, let's use our translator in a React component. Update your `App.tsx`:

```tsx
// src/App.tsx
import React, { useState } from "react";
import { useTranslator } from "./useTranslator";

const App: React.FC = () => {
  const [locale, setLocale] = useState<"en" | "fr">("en");
  const [itemCount, setItemCount] = useState(0);
  const translator = useTranslator(locale);

  return (
    <div>
      <select
        value={locale}
        onChange={(e) => setLocale(e.target.value as "en" | "fr")}
      >
        <option value="en">English</option>
        <option value="fr">Français</option>
      </select>

      <nav>
        <ul>
          <li>{translator((t) => t.nav.home)}</li>
          <li>{translator((t) => t.nav.about)}</li>
          <li>{translator((t) => t.nav.contact)}</li>
        </ul>
      </nav>

      <h1>{translator((t) => t.greeting, { name: "World" })}</h1>

      <p>{translator((t) => t.itemCount, { count: itemCount })}</p>
      <button onClick={() => setItemCount(itemCount + 1)}>Add Item</button>
      <button onClick={() => setItemCount(Math.max(0, itemCount - 1))}>
        Remove Item
      </button>
    </div>
  );
};

export default App;
```

## Type Safety Features

typed-locale provides several type safety features:

1. **Autocomplete for translation keys**: The IDE provides autocomplete suggestions for all available translation keys.

2. **Type checking for variables**: TypeScript catches incorrect variable usage:

   ```typescript
   // This will cause a TypeScript error
   translator((t) => t.greeting, { wrongVariable: "World" });
   ```

3. **Nested translations**: The type system understands nested translations, allowing `translator(t => t.nav.home)`.

4. **Pluralization**: The `itemCount` translation demonstrates how typed-locale handles pluralization, automatically selecting the correct plural form based on the `count` value.

## Technical Advantages

The type-safe approach using typed-locale offers several technical advantages:

1. **Compile-time error detection**: Translation-related errors are caught during compilation rather than at runtime.

2. **Improved developer experience**: Autocomplete for translation keys enhances productivity.

3. **Refactoring support**: TypeScript's refactoring tools work seamlessly with the translation keys.

4. **Prevents key typos**: The callback approach eliminates string typos in translation keys.

5. **Small bundle size**: At 1kB, typed-locale has minimal impact on application size.

6. **Framework agnostic**: While this example uses React, typed-locale can be used with any JavaScript framework or vanilla JS.

## Conclusion

Implementing i18n with a type-safe approach using typed-locale provides a robust solution for managing translations in TypeScript projects. By leveraging the type system, developers can create more reliable internationalized applications while maintaining code quality and productivity.

## Related links
- [Blog index](https://simonboisset.com/en/blog)
- [Website](https://simonboisset.com/en)

# A Comprehensive Guide to `typed-api-call`

Type: blog post
Language: en-US
Canonical URL: https://simonboisset.com/en/blog/typed-api-call-guide
Published: January 21, 2024
Summary: Introduction In the world of web development, making API calls is a routine task. However, the process is not always straightforward, and errors can easily s...

## Introduction

In the world of web development, making API calls is a routine task. However, the process is not always straightforward, and errors can easily slip through the cracks. To address this challenge, the `typed-api-call` library try to make API calls more robust by enhancing typesafety and providing schema validation.

## Installation Made Simple

https://github.com/simonboisset/typed-api-call

Getting started with `typed-api-call` is a breeze. Begin by installing the library using npm:

```bash
npm install typed-api-call
```

Now, let's delve into the functionality and explore how this library can enhance the reliability of your API calls.

## Introduction to `typed-api-call`

The primary goal of `typed-api-call` is to create a typesafe wrapper around the fetch API. By defining API calls and their schemas, developers can generate functions that execute these calls and return responses with the correct types. This not only makes API calls safer but also simplifies the development process.

### Features at a Glance

- **Typesafety**: The library ensures that both API call definitions and the calls themselves are free from mistakes, reducing the likelihood of runtime errors.
- **Schema Validation**: `typed-api-call` checks the response of each API call against a defined schema. Any deviation triggers an error, providing an extra layer of validation.
- **Customizable**: The library is highly customizable, allowing developers to define their own schemas and headers.

## Practical Usage Example

Let's walk through a practical example to demonstrate how `typed-api-call` can be seamlessly integrated into your workflow.

```typescript
import { createApiCall } from "typed-api-call";
import { z } from "zod";

// Define a function to get headers
export const getHeaders = ({ token }: { token?: string }) => {
  // ... (implementation details)
};

// Create an instance of the API call with base URL and getHeaders function
const myApiCall = createApiCall({ url: "https://my-api.com/", getHeaders });

// Define a schema for user data
const userSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string(),
});

// Create an API call for fetching users
const getUsers = myApiCall({
  url: "users",
  method: "GET",
  input: z.object({ email: z.array(z.string().email()) }),
  response: z.object({ data: z.array(userSchema) }),
});

// Make the API call
const users = await getUsers({
  params: undefined,
  data: { email: ["john.doe@gmail.com"] },
});

// ... (further API calls can be defined and executed similarly)
```

## Understanding the API

### `createApiCall`

The `createApiCall` function sets up the foundation for your API calls. It takes two parameters:

- `url`: The base URL of your API.
- `getHeaders`: A function that returns the headers for your API call. If the function requires parameters, they can be passed when defining the API call or when making the call itself.

### API Call Definition

The structure of an API call definition includes various components such as URL, method, headers, input schema, response schema, and parameter schema (if applicable). These components collectively ensure a comprehensive definition of the API call.

### Making an API Call

Executing an API call involves providing parameters, data, and headers (if needed). The `typed-api-call` library takes care of the rest, ensuring that the call is typesafe and adheres to the defined schemas.

## Conclusion

In the realm of web development, where precision and reliability are paramount, `typed-api-call` proves to be an invaluable tool. By enhancing typesafety and providing schema validation, the library streamlines the process of making API calls, minimizing errors and boosting developer confidence.

## Related links
- [Blog index](https://simonboisset.com/en/blog)
- [Website](https://simonboisset.com/en)

# Create a static blog with Next.js and Tailwind CSS

Type: blog post
Language: en-US
Canonical URL: https://simonboisset.com/en/blog/next-static-blog-tailwind
Published: October 22, 2023
Summary: Introduction I recently decided to create a static blog to share my knowledge and experiences. I chose Next.js for its simplicity and flexibility. In this ar...

## Introduction

I recently decided to create a static blog to share my knowledge and experiences. I chose Next.js for its simplicity and flexibility. In this article, I will show you how to create a static blog with Next.js and Tailwind CSS.

You can check out my [github](https://github.com/simonboisset/simonboisset) to see the code of my blog.

## Tour of existing tools

There are many tools to create a static blog. Here is a non-exhaustive list:

### Docusaurus

[Docusaurus](https://docusaurus.io/) is an open source static site generator created by Facebook. It is mainly used to create documentation sites but it is possible to use it to create a static blog.

### Nextra

[Nextra](https://nextra.site/) is an open source static site generator based on Next.js. It is very thought out to create documentations like Docusaurus.

## Why not use these tools?

To be honest the first version of my site was based on Docusaurus. It's a great tool and I recommend it for anyone who wants to create a turnkey documentation site.

Nevertheless, I still had the feeling that I had an over ingineered setup for a simple blog. And I wanted to start from scratch to better master the features of my site.

So I decided to rewrite my site with Next.js only.

> I could have used Remix as well, this could be the subject of a future article.

## Project setup

I started from a blank Next.js project. I needed a design system. So I chose [Tailwind CSS](https://tailwindcss.com/) and [shadcn](https://ui.shadcn.com/) which is a design system based on Tailwind CSS and [radix ui](https://www.radix-ui.com/) to manage accessibility.

With these tools, I was able to create a complete and customizable design system and easily implement the landing page of my site.

Now I just need to create the pages of my blog.

## Blog articles

To create the articles of my blog, I chose to use markdown. It's a very simple format to write and read. It is also very easy to transform it into HTML.

I also wanted to have a translation system. So I created a `posts` folder in which I created a folder for each article. Each folder contains one markdown file per language.

```
posts
├── my-first-post
│   ├── en.md
│   └── fr.md
└── my-second-post
    ├── en.md
    └── fr.md
```

## Import articles

In order to import the articles, I chose to use `raw-loader` to import the markdown files and `marked` to parse the markdown into HTML.

```bash
yarn add raw-loader marked
```

You have to modify the `next.config.js` file to add the `raw-loader` loader:

```js
module.exports = {
  webpack: (config) => {
    config.module.rules.push({
      test: /\.md$/,
      use: "raw-loader",
    });
    return config;
  },
};
```

Then, you have to create a `posts.ts` file which will be a server action. This file will import the markdown files and parse them into HTML.

```ts
import "server-only";

import { redirect } from "next/navigation";
import z from "zod";
import { Locale } from "../../../dictionaries";

const postSlugs = ["my-first-post", "my-second-post"] as const;
export const postSlugSchema = z.enum(postSlugs);

export const getPost = async (slug: string, locale: Locale) => {
  const safeSlug = postSlugSchema.safeParse(slug);
  if (!safeSlug.success) {
    throw redirect("/blog");
  }
  return import(`./${slug}/${locale}.md`).then(
    (module) => module.default as string
  );
};
```

To get the list of articles, I created a `getPostList` function that will import all the articles and parse them into an object.

```ts
export const getPostList = async (locale: Locale) => {
  const posts = await Promise.all(
    postSlugs.map(async (slug) => {
      const post = await import(`./${slug}/${locale}.md`).then(
        (module) => module.default as string
      );

      return {
        slug,
        title: getTitleFromMarkdown(post),
        preview: getPreviewFromMarkdown(post),
        img: getImgFromMarkdown(post),
        date: getDateFromMarkdown(post),
      };
    })
  );
  return posts;
};
```

Now we just have to create a component to display a post.

We create a post folder for the router and we create a child folder `[slug]`.

The `post/[slug]` page will get the content of the post and display it.

```tsx
export default async function BlogPostPage({
  params: { slug, lang },
}: {
  params: { slug: string; lang: Locale };
}) {
  const md = await getPost(slug, lang);

  return <div dangerouslySetInnerHTML={{ __html: marked(md) }} />;
}
```

And voila, we have a static blog with Next.js.

Well, on the other hand it's not super beautiful yet. We'll have to add some style. And for that, we'll use Tailwind CSS.

## Add style

To add style, we will use a Tailwind CSS extension: `@tailwindcss/typography`.

```js
plugins: [require('@tailwindcss/typography')],
```

Now we can add a layout to our page.

```tsx
export default async function BlogPostLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div
      className={cn(
        "prose mx-auto prose-p:text-justify prose-a:underline prose-a:font-bold",
        "prose-img:rounded-lg prose-img:shadow-lg prose-img:object-cover prose-img:p-0",
        "prose-headings:text-primary prose-p:text-primary prose-code:text-primary",
        "prose-li:text-primary prose-a:text-primary prose-li:marker:text-primary",
        "w-full max-w-screen-lg mx-auto mt-32 px-12 prose-pre:text-primary prose-pre:bg-foreground/10",
        "prose-pre:rounded-lg prose-pre:shadow-lg prose-pre:overflow-x-auto prose-pre:p-6"
      )}
    >
      {children}
    </div>
  );
}
```

Now, we have a static blog with a style that we will be able to improve as we wish.

## Conclusion

If you want to create a static blog, Next.js complemented by Tailwind CSS is a winning combo. You have a simple and flexible setup that will allow you to create a static blog in your image. Plus with server components, you have the best development experience possible.

> I'll give you one last hack for writing your articles. Use copilot to assist you in writing your articles. It's a huge time saver. Just activate it in the VSCode settings.

```json
"github.copilot.enable": {
    "markdown": true
  }
```

---

I'm Simon Boisset, freelance fullstack developer. I mainly work with React, React Native and Node.js. I'm available for development or consulting missions. Feel free to contact me on [my website](https://simonboisset.com/).

## Related links
- [Blog index](https://simonboisset.com/en/blog)
- [Website](https://simonboisset.com/en)

# Create and publish npm module library with esbuild typescript and react

Type: blog post
Language: en-US
Canonical URL: https://simonboisset.com/en/blog/publish-npm-library-with-esbuild
Published: October 2, 2022
Summary: Introduction As an open source enthusiast, I like to share my code and give everyone the opportunity to use it. For that npm is my first choice. But there ar...

## Introduction

As an open source enthusiast, I like to share my code and give everyone the opportunity to use it. For that npm is my first choice. But there are some conditions to have a good library release on npm.

The first recommendation to follow is to support common js as Matteo Collina said :

<blockquote>Here is my recommendation to all my fellow npm module authors: don’t drop support for CJS and go esm-only. The community is not ready to migrate just yet.
Matteo Collina (@matteocollina) <a href="https://twitter.com/matteocollina/status/1560658851682168834?ref_src=twsrc%5Etfw">August 19, 2022</a></blockquote>

For long time you needed to use js bundler like rollup, webpack and babel.

But that time is over because of a new generation of bundler. esbuild is one of them.

Today we will see how to create and publish a react library on npm with esbuild.

You can go to [this repository](https://github.com/simonboisset/remix-feature-routes) for an example of this post.

## Initialization

Create your folder project and initialize it.

```bash
yarn init
```

Your package.json file should be defined :

```json
{
  "name": "my-react-library",
  "version": "1.0.0"
}
```

Now we will install our dependencies.

## Install dev dependencies

```bash
yarn add -D react react-dom typescript @types/react @types/react-dom
```

> Don't forget to add react as a peer dependency if you are writing a react library with hooks or components.

```json
"peerDependencies": {
   "react": ">=16"
 },
```

## Typescript config

Add `tsconfig.json` file.

```json
{
  "include": ["src"],
  "compilerOptions": {
    "module": "esnext",
    "target": "esnext",
    "lib": ["dom", "dom.iterable", "esnext"],
    "declaration": true,
    "strict": true,
    "moduleResolution": "node",
    "jsx": "react",
    "skipLibCheck": true,
    "esModuleInterop": true,
    "emitDeclarationOnly": true,
    "outDir": "dist",
    "rootDir": "src"
  }
}
```

> we don't need to build file with typescript because we will use esbuild for that. That's why `emitDeclarationOnly` is set to true

## Write your module

Now you can write what you want. For example we will just write a function component to export.

Create src `folder` then `index.tsx` inside it.

```tsx
export const MyComponent = () => {
  return <div>Hello word</div>;
};
```

## Build your library

Now we need to build our package to CJS before publish it to npm.

### Configure esbuild

Add esbuild :

```bash
yarn add -D esbuild esbuild-node-externals
```

Create an esbuild script for the bundle. Add a file `esbuild.js` :

```js
const esbuild = require("esbuild");
const { nodeExternalsPlugin } = require("esbuild-node-externals");
esbuild
  .build({
    entryPoints: ["./src/index.ts"],
    outfile: "dist/index.js",
    bundle: true,
    minify: true,
    treeShaking: true,
    platform: "node",
    format: "cjs",
    target: "node14",
    plugins: [nodeExternalsPlugin()],
  })
  .catch(() => process.exit(1));
```

### Add a build commande

And finally add a script in your package.json :

```json
 "scripts": {
    "build": "rm -rf dist && node esbuild.js && tsc"
  },
```

Now you can run :

```bash
yarn build
```

## Npm publish

> Before publishing you need to be [authenticated](https://docs.npmjs.com/creating-a-new-npm-user-account)

Add these lines to your package.json :

```json
 "main": "dist/index.js",
  "files": [
    "dist"
  ],
```

Then publish it :

```bash
npm publish
```

Congratulations, you have published your own library.

## Publish package with Github action

Finally we just want to publish package when we trigger a github action. For that we have to add a file to `.github/workflows/publish.yml`:

```yml
name: Publish
on:
  workflow_dispatch:
    inputs:
      release:
        description: "major | minor | patch"
        required: true
        default: "patch"
        type: choice
        options:
          - major
          - minor
          - patch
jobs:
  publish-new-version:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout main
        uses: actions/checkout@v2
      - name: Use Node
        uses: actions/setup-node@v1
        with:
          node-version: "14"
          registry-url: https://registry.npmjs.org/
      - name: Install dependencies
        run: yarn
      - name: Build
        run: yarn build
      - name: Publish New Version
        env:
          NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
        run: |
          git config --local user.email "myEmail"
          git config --local user.name "myUsername"
          yarn version --new-version ${{github.event.inputs.release}} --no-git-tag-version
          yarn publish --access public
          PACKAGE_VERSION=$(node -p "require('./package.json').version")
          git commit -a -m "v${PACKAGE_VERSION}"
          git push
```

### Add secret npm token

To run your action you need to add NPM_TOKEN environement variable.

Go to you npm account and generate an access token for CI :
![Npm access token](https://directus.services.lezo.dev/assets/213a95c1-1ce9-45b4-bc5c-bd23c8ea2dca)

Copy past this token in Github secret of your repository :
![Github action secret](https://directus.services.lezo.dev/assets/9bc983ff-e328-475b-ae4c-37799b7ee0f3)

Now run your github action and take a coffe.
![Trigger github action](https://directus.services.lezo.dev/assets/cb86ea78-a42a-43a3-b068-f513139573b7)

---

I'm Simon Boisset, freelance fullstack developer. I mainly work with React, React Native and Node.js. I'm available for development or consulting missions. Feel free to contact me on [my website](https://simonboisset.com).

## Related links
- [Blog index](https://simonboisset.com/en/blog)
- [Website](https://simonboisset.com/en)

# Share packages between React Native and Web project in monorepo

Type: blog post
Language: en-US
Canonical URL: https://simonboisset.com/en/blog/share-react-native-monorepo
Published: October 2, 2022
Summary: Introduction How to link an npm core package between a React web project and a React Native mobile project? In this article, I will use yarn as package manag...

## Introduction

How to link an npm `core` package between a `React` web project and a `React Native` mobile project?

In this article, I will use yarn as package manager. You can probably do this with `pnpm` or even `npm`, but I haven't tested it.

## Why sharing packages locally ?

When you work on projects that includes many sub-projects, you should want to share code between your API and your front or your mobile.

To minimize code duplication, the first solution is to create an additional package called `core`, for example, to share it locally as a shared dependency between each project.

All projects will include this dependency in their `package.json` file :

```json
"dependencies": {
   "core": "*"
}
```

The disadvantage is that you have to publish the core package to install it and makes local development and CI logic much complex.

## Yarn link

A simple solution to solve this problem is to use `yarn link` to create a symlink locally between our packages :

```sh
cd core
yarn link
yarn build

cd web
yarn link core

cd server
yarn link core
```

Now your `server` and `web` will use the local version of your `core` build.

## React Native

Now We will make our project more complex by adding a React Native mobile application. We made the choice to create the project with [Expo](https://docs.expo.dev/).

The problem in this case is that the symlinks don't work. Your app will give you a nice crash.

We will see together how to succeed in sharing your code properly between all these projects.

## Monorepo

It's nice to link your packages together locally, but when your project adding many of them, it can be onerous to set up.

One of the most used solutions today is to go through a monorepo. To put it simply, it's a repo that will centralize all our code. This simplifies code organization and versioning since all projects follow deployments at the same time.

## Yarn workspace

Still using yarn, now that we have our projects on a monorepo we are going to use a very useful configuration for a monorepo which is the [yarn workspace](https://classic.yarnpkg.com/lang/en/docs/workspaces/).

Briefly, instead of doing a yarn install for each of your projects, you will do it once at the root of your monorepo and yarn will install all your `node_modules` at the root.

## Turborepo

To set up our monorepo we will use [Turborepo](https://turborepo.org/) which will take care of installing the basic parameters for us.

I will not explains of this configuration, because it could be the subject of a complete article and the documentation is very good.

```sh
npx create-turbo@latest
```

# Our monorepo

Let's go!
Now, let's create our web and mobile projects :

```sh
cd apps
yarn create react-app web
...

npx create-expo-app mobile
```

Now we create our core package :

```sh
cd packages
mkdir core
yarn init
...
```

If you don't know how to create a dependency reusable node package, you can read [my article](https://dev.to/simonboisset/create-and-publish-npm-module-library-with-esbuild-typescript-and-react-1acn) about it.

It remains to add in the dependencies of web and mobile :

```json
"dependencies": {
   "core": "*"
}
```

# Metro config

We are getting closer to the goal.
You can already build `core` and run `web` with it.

For `mobile`, it still doesn't work, but don't worry, we're almost there.

Now our `core` build is located at the `root` of our project. Just tell `metro` to get its `node_module` not in its folder but in the root.

We will first modify the `main` property of the `package.json` :

```json
"main": "index.js"
```

Now we create the `index.js` file:

```js
import { registerRootComponent } from "expo";

import App from "./App";
registerRootComponent(App);
```

Finally we create the `metro` configuration for the monorepo with the `metro.config.js` file:

```js
const { getDefaultConfig } = require("expo/metro-config");
const path = require("path");

const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, "../..");

const config = getDefaultConfig(projectRoot);

config.watchFolders = [workspaceRoot];
config.resolver.nodeModulesPaths = [
  path.resolve(projectRoot, "node_modules"),
  path.resolve(workspaceRoot, "node_modules"),
];
config.resolver.disableHierarchicalLookup = true;

module.exports = config;
```

Now you can retry a core build and launch the mobile and you are able to start your development.

The example of this article can be found [here](https://github.com/simonboisset/examples/tree/main/shared-react-native-monorepo).

---

I'm Simon Boisset, freelance fullstack developer. I mainly work with React, React Native and Node.js. I'm available for development or consulting missions. Feel free to contact me on [my website](https://simonboisset.com/).

## Related links
- [Blog index](https://simonboisset.com/en/blog)
- [Website](https://simonboisset.com/en)

# Create a react app with esbuild

Type: blog post
Language: en-US
Canonical URL: https://simonboisset.com/en/blog/create-react-app-with-esbuild
Published: August 25, 2022
Summary: Introduction Esbuild: https://esbuild.github.io/ is a new javascript bundler. It's written with Go and is extremely fast. Let's go to use it to create react...

## Introduction

[Esbuild](https://esbuild.github.io/) is a new javascript bundler. It's written with Go and is extremely fast. Let's go to use it to create react with hot reload app from scratch without webpack

You can check the code on this [repos](https://github.com/simonboisset/templates/tree/main/templates/react-spa).

> I wrote this article in 2022. It's more a POC than a real production ready app. I use it to test esbuild and create a template for my future projects. Today I would not recommend styled-components instead I would use tailwindcss.

## Initialization

Create your folder project and initialize it.

```bash
yarn init
```

```json
{
  "name": "esbuild-static",
  "version": "1.0.0"
}
```

### Install dependencies

```bash
yarn add esbuild dotenv react react-dom styled-components
```

Then add devdependencies.

```bash
yarn add --dev typescript @types/react @types/react-dom @types/styled-components @types/node serve-handler @types/serve-handler
```

### Typescript config

Add `tsconfig.json` file.

```json
{
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src",
    "module": "commonjs",
    "target": "ESNext",
    "lib": ["dom", "dom.iterable", "esnext"],
    "moduleResolution": "node",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noImplicitReturns": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "jsx": "react"
  },
  "include": ["src"],
  "exclude": ["**/node_modules", "**/.*/"]
}
```

### Esbuild config

Create `esbuild` folder then add `dev.js` and `prod.js` files.

The dev config watch files changes and start a server for hot reload and static files. You can add environment variables too.

```js
const { spawn } = require("child_process");
const esbuild = require("esbuild");
const { createServer, request } = require("http");
require("dotenv").config();
const handler = require("serve-handler");

const clientEnv = { "process.env.NODE_ENV": `'dev'` };
const clients = [];

Object.keys(process.env).forEach((key) => {
  if (key.indexOf("CLIENT_") === 0) {
    clientEnv[`process.env.${key}`] = `'${process.env[key]}'`;
  }
});

const openBrowser = () => {
  setTimeout(() => {
    const op = {
      darwin: ["open"],
      linux: ["xdg-open"],
      win32: ["cmd", "/c", "start"],
    };
    if (clients.length === 0)
      spawn(op[process.platform][0], ["http://localhost:3000"]);
  }, 1000);
};

esbuild
  .build({
    entryPoints: ["src/index.tsx"],
    bundle: true,
    minify: true,
    define: clientEnv,
    outfile: "dist/index.js",
    sourcemap: "inline",
    watch: {
      onRebuild(error) {
        setTimeout(() => {
          clients.forEach((res) => res.write("data: update\n\n"));
        }, 1000);
        console.log(error || "client rebuilt");
      },
    },
  })
  .catch((err) => {
    console.log(err);
    process.exit(1);
  });

esbuild.serve({ servedir: "./" }, {}).then((result) => {
  createServer((req, res) => {
    const { url, method, headers } = req;
    if (req.url === "/esbuild") {
      return clients.push(
        res.writeHead(200, {
          "Content-Type": "text/event-stream",
          "Cache-Control": "no-cache",
          "Access-Control-Allow-Origin": "*",
          Connection: "keep-alive",
        })
      );
    }

    const path = url.split("/").pop().indexOf(".") ? url : `/index.html`;
    const proxyReq = request(
      { hostname: "0.0.0.0", port: 8000, path, method, headers },
      (prxRes) => {
        res.writeHead(prxRes.statusCode, prxRes.headers);
        prxRes.pipe(res, { end: true });
      }
    );
    req.pipe(proxyReq, { end: true });
    return null;
  }).listen(5010);

  createServer((req, res) => {
    return handler(req, res, { public: "dist" });
  }).listen(3000);

  openBrowser();
});
```

```js
const esbuild = require("esbuild");
require("dotenv").config();

const clientEnv = { "process.env.NODE_ENV": `'production'` };
for (const key in process.env) {
  if (key.indexOf("CLIENT_") === 0) {
    clientEnv[`process.env.${key}`] = `'${process.env[key]}'`;
  }
}
esbuild
  .build({
    entryPoints: ["src/index.tsx"],
    bundle: true,
    minify: true,
    define: clientEnv,
    outfile: "dist/index.js",
  })
  .catch(() => process.exit(1));
```

### Eslint config

Install eslint.

```bash
yarn add --dev eslint eslint-config-react-app @typescript-eslint/eslint-plugin @typescript-eslint/parser
```

Add `.eslintrc.js` file.

```jsx
module.exports = {
  env: {
    browser: true,
    es2021: true,
  },
  extends: ["react-app"],
  parser: "@typescript-eslint/parser",
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 13,
    sourceType: "module",
  },
  plugins: ["react", "@typescript-eslint"],
  settings: {
    "import/resolver": {
      node: {
        extensions: [".js", ".jsx", ".ts", ".tsx"],
      },
    },
  },
};
```

### Scripts

Add scripts to package.json

```json
"scripts": {
    "build": "node esbuild/prod",
    "type-check": "tsc --noEmit",
    "lint": "eslint src/**/*.ts src/**/*.tsx",
    "start": "nodemon --watch dist --exec 'yarn type-check & yarn lint' & node esbuild/dev"
  },
```

## React app

In `src` folder add `index.tsx` file.

```tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import GlobalStyle from "./globalStyle";

ReactDOM.render(
  <>
    <GlobalStyle />
    <App />
  </>,
  document.getElementById("root")
);
```

### Hot reload tools

For listening esbuild dev server reload we must add a hook for development.

```tsx
import { useEffect } from "react";

const useHMR = () => {
  useEffect(() => {
    if (process.env.NODE_ENV !== "production") {
      new EventSource("http://localhost:5010/esbuild").onmessage = () =>
        window.location.reload();
    }
  }, []);
};
export default useHMR;
```

### CSS with styled-components

Add global style with `styled-components`

```css
import { createGlobalStyle } from 'styled-components';

 const GlobalStyle = createGlobalStyle`
  body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}
.App {
  text-align: center;
}

.App-logo {
  height: 40vmin;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .App-logo {
    animation: App-logo-spin infinite 20s linear;
  }
}

.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.App-link {
  color: #61dafb;
}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
`;
export default GlobalStyle
```

### App

Fanaly create the App component.

```tsx
import React, { FC } from "react";
import useHMR from "./useHMR";
import Logo from "./Logo";

const App: FC = () => {
  useHMR();
  return (
    <div className="App">
      <header className="App-header">
        <Logo />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
};

export default App;
```

## **Static files**

Add static files in `dist` folder.

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="React App" />
    <link rel="apple-touch-icon" href="/logo192.png" />
    <link rel="manifest" href="/manifest.json" />
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>
<script src="/index.js"></script>
```

Then create other files : `favicon.ico`, `manifest.json`, `logo192.png`

## Run

Start dev server.

```bash
yarn start
```

Build for production

```bash
yarn build
```

> Now let's go to code

---

I'm Simon Boisset, freelance fullstack developer. I mainly work with React, React Native and Node.js. I'm available for development or consulting missions. Feel free to contact me on [my website](https://simonboisset.com/).

## Related links
- [Blog index](https://simonboisset.com/en/blog)
- [Website](https://simonboisset.com/en)

# Publish a library with multiple packages using Turborepo

Type: blog post
Language: en-US
Canonical URL: https://simonboisset.com/en/blog/share-packages-monorepo
Published: February 10, 2022
Summary: Multiple packages? Now that we know how to publish a package on npm, we will see how to manage a library that has multiple packages. You can check out this e...

## Multiple packages?

Now that we know how to publish a package on npm, we will see how to manage a library that has multiple packages.

You can check out [this example](https://github.com/simonboisset/validest) to see a concrete application.

## Configuration

To manage multiple modules together we will use Turborepo that you have already seen in a previous article.

```bash
npx create-turbo@latest
```

You can delete the different examples of base packages, we will create our own later.

## Builder

We will start by creating a package to manage the builds of our libraries. This one will not be published but only shared in our monorepo.

Create a `builder` folder in `packages` then create the following files:

`package.json`

```json
{
  "name": "builder",
  "private": true,
  "version": "0.1.0",
  "bin": "index.js",
  "dependencies": { "esbuild": "^0.14.42", "esbuild-node-externals": "^1.4.1" }
}
```

`index.js`

```js
#!/usr/bin/env node

const esbuild = require("esbuild");

const { nodeExternalsPlugin } = require("esbuild-node-externals");

esbuild
  .build({
    entryPoints: ["./src/index.ts"],
    outfile: "dist/index.js",
    bundle: true,
    minify: true,
    treeShaking: true,
    platform: "node",
    format: "cjs",
    target: "node14",
    plugins: [nodeExternalsPlugin()],
  })
  .catch(() => process.exit(1));
```

## Our packages

Now we can create our different packages. For the example we will create two.

## Core

Still in the `packages` folder we will create a `core` folder and the following files:

`package.json`

```json
{
  "name": "@example/core",
  "version": "0.1.0",
  "description": "A description",
  "main": "dist/index.js",
  "typings": "dist/index.d.ts",
  "files": ["dist"],
  "scripts": {
    "build": "rm -rf dist && builder && tsc"
  },
  "author": "Your name",
  "license": "MIT",
  "devDependencies": {
    "builder": "*",
    "typescript": "^4.7.4"
  }
}
```

> You will notice that we used a scope for our package name `@example`. To use your own you must create an organization with the same name on npm.

`src/index.ts`

```ts
export const hello = (name: string) => {
  return "Hello " + name;
};
```

`tsconfig.json`

```json
{
  "extends": "builder/ts.json",
  "include": ["src"],
  "exclude": ["src/**/*.test.ts"],
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src",
    "module": "esnext",
    "target": "esnext",
    "lib": ["dom", "dom.iterable", "esnext"],
    "declaration": true,
    "strict": true,
    "moduleResolution": "node",
    "jsx": "react",
    "skipLibCheck": true,
    "esModuleInterop": true,
    "emitDeclarationOnly": true
  }
}
```

## Age

Same thing for our second package `@example/age`

`package.json`

We add a dependency to core:

```json

"dependencies": { "@example/core": "0.1.0" },
```

`index.ts`

```ts
import { hello } from "@example/core";

export const age = (name: string, age: number) => {
  return hello(name) + " you are " + age;
};
```

## Turbo build

Our two packages are ready, we just have to build them together. We will still have to set up Turborepo to build `core` before `age`.

At the root of the repo:

`turbo.json`

```json
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    }
  }
}
```

`package.json`

```json
{
  "name": "example",
  "version": "0.1.0",
  "private": true,
  "workspaces": ["packages/*"],
  "scripts": {
    "build": "turbo run build --filter=@example/*"
  },
  "devDependencies": {
    "prettier": "latest",
    "turbo": "latest"
  },
  "engines": {
    "npm": ">=7.0.0",
    "node": ">=14.0.0"
  },
  "prettier": {
    "singleQuote": true,
    "tabWidth": 2,
    "printWidth": 120
  },
  "packageManager": "yarn@1.22.5"
}
```

We can already run the following commands for a first build:

```bash
yarn
yarn build
```

## Test

If you want to add tests I recommend [Vitest](https://vitest.dev/). You can follow the docs of [Turborepo](https://turbo.build/repo/docs/handbook/testing) about it.

## Versioning

A somewhat complex step is to manage the correct versions of dependencies. The simplest thing is to consider that all our packages have the same version number. We will still have to use a script to increment our version number (patch, minor or major) of dependencies and packages before each publication.

For this we use `turboversion`:

```bash
yarn add -W -D turboversion
```

The script will be executed before each publication in our CI. To explain quickly what it does, it will scan our entire monorepo and increment the version number (patch, minor or major) of the dependencies and packages of our scope `example`.

## Publication

We will take back our Github Action from the previous article to adapt it to our monorepo:

`.github/workflows/publish.yml`

```yml
name: Publish
on:
  workflow_dispatch:
    inputs:
      release:
        description: "major | minor | patch"
        required: true
        default: "patch"
        type: choice
        options:
          - major
          - minor
          - patch
jobs:
  publish-new-version:
    runs-on: ubuntu-latest
    steps:
      - name: 🔌 Checkout
        uses: actions/checkout@v3
      - name: 🏗 Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: 16.x
          cache: "yarn"
          registry-url: https://registry.npmjs.org/
          scope: "@example"
      - name: ⏳ Yarn install
        run: yarn
      - name: 🚀 Publish New Version
        env:
          NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
        run: |
          git config --local user.email "youremail"
          git config --local user.name "yourname"
          yarn turboversion example ${{github.event.inputs.release}}
          yarn publish:lib
          PACKAGE_VERSION=$(node -p "require('./package.json').version")
          git commit -a -m "v${PACKAGE_VERSION}"
          git push
```

There you go, you can now publish your library and all its packages with a consistent version number.

---

I'm Simon Boisset, freelance fullstack developer. I mainly work with React, React Native and Node.js. I'm available for development or consulting missions. Feel free to contact me on [my website](https://simonboisset.com/).

## Related links
- [Blog index](https://simonboisset.com/en/blog)
- [Website](https://simonboisset.com/en)

# Mentions légales

Type: document
Language: fr-FR
Canonical URL: https://simonboisset.com/docs/legal
Summary: Éditeur de l'application Société : Lezo (EURL) RCS : 921 329 025 R.C.S. Nantes Capital social : 100 euro TVA intracommunautaire : FR 23 921329025 Adresse : 2...

## Éditeur de l'application
  - Société : Lezo (EURL)
  - RCS : 921 329 025 R.C.S. Nantes
  - Capital social : 100 euro
  - TVA intracommunautaire : FR 23 921329025
  - Adresse : 21 boulevard Auguste Pageot
    44000 Nantes
    France

  ## Hébergement
  Le site est hébergé par Lezo (auto-hébergé) à l'adresse suivante :
  21 boulevard Auguste Pageot
  44000 Nantes
  France

  ## Directeur de la publication
  Simon Boisset

  ## Propriété intellectuelle
  L'ensemble des contenus (textes, visuels, marques, logos) est protégé par le
  droit de la propriété intellectuelle. Toute reproduction ou utilisation non
  autorisée est interdite.

  ## Responsabilité
  L'éditeur met en oeuvre tous les moyens raisonnables pour assurer la fiabilité
  des informations publiées. Il ne peut toutefois pas garantir l'absence
  d'erreurs ou d'omissions.

  ## Contact
  Pour toute question, vous pouvez ecrire a [support@lezo.dev]
  (mailto:support@lezo.dev).

## Related links
- [Documents index](https://simonboisset.com/docs)
- [Website](https://simonboisset.com/)

# Politique de confidentialité

Type: document
Language: fr-FR
Canonical URL: https://simonboisset.com/docs/privacy
Summary: Responsable du traitement Le responsable du traitement des données est l'éditeur de l'application : Lezo (EURL), 21 boulevard Auguste Pageot, 44000 Nantes, F...

## Responsable du traitement
  Le responsable du traitement des données est l'éditeur de l'application : Lezo
  (EURL), 21 boulevard Auguste Pageot, 44000 Nantes, France.

  ## DPO
  Vous pouvez contacter le délégué à la protection des données (DPO) Simon
  Boisset à l'adresse [support@lezo.dev](mailto:support@lezo.dev).

  ## Données collectées
  Les données collectées sont strictement nécessaires pour répondre aux demandes
  de contact et organiser les rendez-vous.
  - Identité (nom, société)
  - Coordonnées (email, téléphone si fourni)
  - Informations liées à votre projet
  - Données de navigation (pages visitées, appareil, source de trafic) en cas de
  consentement analytics

  ## Finalités
  - Répondre aux demandes de contact
  - Organiser les rendez-vous
  - Suivre les demandes commerciales
  - Mesurer l'audience et améliorer le site (avec consentement)

  ## Base légale
  Le traitement est fondé sur l'intérêt légitime à répondre aux demandes et sur
  l'exécution de mesures précontractuelles. Les traitements analytics reposent
  sur votre consentement.

  ## Conservation
  Les données sont conservées pendant une durée maximale de 3 ans à compter du
  dernier contact, sauf obligation légale contraire.

  ## Partage des données
  Aucune donnée personnelle n'est vendue ou partagée avec des tiers, hors
  obligations légales ou nécessité de fourniture du service de rendez-vous.

  ## Sous-traitants
  Le service de prise de rendez-vous est opéré par Google (Google Calendar). Ce
  prestataire peut traiter des données en dehors de l'Union européenne.
  [Politique de confidentialité de Google](https://policies.google.com/privacy).
  Les mesures d'audience sont réalisées avec PostHog (hébergement UE).
  [Politique de confidentialité de PostHog](https://posthog.com/privacy).

  ## Vos droits
  Vous disposez d'un droit d'accès, de rectification, d'opposition, d'effacement
  et de portabilité. Pour exercer ces droits, merci de nous contacter à
  [support@lezo.dev](mailto:support@lezo.dev).
  Vous pouvez également introduire une réclamation auprès de la CNIL
  ([www.cnil.fr](https://www.cnil.fr)).

  ## Cookies et traceurs
  Le site n'installe pas de cookies publicitaires. Des cookies techniques
  peuvent être déposés par le service de prise de rendez-vous de Google pour
  assurer son bon fonctionnement. Les cookies analytics PostHog ne sont activés
  qu'après consentement.

  ## Sécurité
  Les mesures techniques et organisationnelles appropriées sont mises en oeuvre
  pour protéger les données contre l'accès non autorisé ou la perte.

## Related links
- [Documents index](https://simonboisset.com/docs)
- [Website](https://simonboisset.com/)

# Legal notice

Type: document
Language: en-US
Canonical URL: https://simonboisset.com/en/docs/legal
Summary: Publisher Company: Lezo (EURL) Trade register (RCS): 921 329 025 R.C.S. Nantes Share capital: EUR 100 VAT number: FR 23 921329025 Address: 21 boulevard Augus...

## Publisher
  - Company: Lezo (EURL)
  - Trade register (RCS): 921 329 025 R.C.S. Nantes
  - Share capital: EUR 100
  - VAT number: FR 23 921329025
  - Address: 21 boulevard Auguste Pageot
    44000 Nantes
    France

  ## Hosting
  The site is hosted by Lezo (self-hosted) at:
  21 boulevard Auguste Pageot
  44000 Nantes
  France

  ## Publication director
  Simon Boisset.

  ## Intellectual property
  All content (texts, visuals, trademarks, logos) is protected by intellectual
  property law. Any unauthorized reproduction or use is prohibited.

  ## Liability
  The publisher uses reasonable efforts to ensure the accuracy of the
  information published, but cannot guarantee the absence of errors or
  omissions.

  ## Contact
  For any question, you can write to [support@lezo.dev]
  (mailto:support@lezo.dev).

## Related links
- [Documents index](https://simonboisset.com/en/docs)
- [Website](https://simonboisset.com/en)

# Privacy policy

Type: document
Language: en-US
Canonical URL: https://simonboisset.com/en/docs/privacy
Summary: Data controller The data controller is the publisher of the site: Lezo (EURL), 21 boulevard Auguste Pageot, 44000 Nantes, France. DPO You can contact the Dat...

## Data controller
  The data controller is the publisher of the site: Lezo (EURL), 21 boulevard
  Auguste Pageot, 44000 Nantes, France.

  ## DPO
  You can contact the Data Protection Officer (DPO) Simon Boisset at
  [support@lezo.dev](mailto:support@lezo.dev).

  ## Data collected
  The data collected is strictly necessary to respond to contact requests and
  schedule appointments.
  - Identity (name, company)
  - Contact details (email, phone if provided)
  - Information related to your project
  - Browsing data (visited pages, device, traffic source) if analytics consent
  is given

  ## Purposes
  - Respond to contact requests
  - Schedule appointments
  - Track business requests
  - Measure audience and improve the site (with consent)

  ## Legal basis
  Processing is based on legitimate interest to respond to requests and on pre-
  contractual steps. Analytics processing relies on your consent.

  ## Retention
  Data is kept for a maximum of 3 years from the last contact, unless legal
  obligations require otherwise.

  ## Data sharing
  No personal data is sold or shared with third parties, except for legal
  obligations or to provide the scheduling service.

  ## Subprocessors
  The scheduling service is operated by Google (Google Calendar). This provider
  may process data outside the European Union. [Google privacy policy](https://
  policies.google.com/privacy).
  Audience measurement is performed with PostHog (EU hosting). [PostHog privacy
  policy](https://posthog.com/privacy).

  ## Your rights
  You have the right of access, rectification, objection, erasure, and
  portability. To exercise these rights, please contact [support@lezo.dev]
  (mailto:support@lezo.dev).
  You can also file a complaint with the CNIL ([www.cnil.fr](https://
  www.cnil.fr)).

  ## Cookies and trackers
  The site does not set advertising cookies. Technical cookies may be placed by
  Google’s scheduling service to ensure proper operation. PostHog analytics
  cookies are enabled only after consent.

  ## Security
  Appropriate technical and organizational measures are implemented to protect
  data against unauthorized access or loss.

## Related links
- [Documents index](https://simonboisset.com/en/docs)
- [Website](https://simonboisset.com/en)
