Chapitre 11.3 — app.config.ts & les config plugins
Où on en est : vous buildez. La config de l’app (jusqu’ici un app.json statique) devient stratégique : variantes par environnement, plugins qui écrivent le natif. On la passe en TypeScript.
⏱️ TL;DR —
app.config.tsremplace app.json : même contenu, mais du code (variables d’env, logique, types). Le cas d’usage roi : des variantes par profil — Drill Dev et Drill (prod) installées CÔTE À CÔTE (package name différent). Les config plugins sont des fonctions qui modifient les fichiers natifs au prebuild — on lit ce qu’ils font, on en écrit un petit, et on démystifie la dernière boîte noire d’Expo.
🎯 Objectifs
- Convertir app.json en app.config.ts typé, sans régression.
- Mettre en place les variantes dev/prod (deux apps côte à côte sur le téléphone).
- Comprendre ce qu’un config plugin fait VRAIMENT (et lire ceux qu’on utilise déjà).
- Écrire un mini-plugin (la preuve que ce n’est pas magique).
De app.json à app.config.ts
// app.config.ts — remplace app.json (le supprimer après migration)
import type { ExpoConfig } from 'expo/config'
const IS_DEV = process.env.APP_VARIANT === 'development'
const config: ExpoConfig = {
name: IS_DEV ? 'Drill Dev' : 'Drill',
slug: 'drill',
scheme: IS_DEV ? 'drill-dev' : 'drill',
version: '0.1.0',
orientation: 'portrait',
userInterfaceStyle: 'automatic',
newArchEnabled: true,
android: {
package: IS_DEV ? 'com.alex.drill.dev' : 'com.alex.drill', // ← LA ligne des variantes
adaptiveIcon: {
foregroundImage: './assets/images/adaptive-icon.png',
backgroundColor: '#0b1216',
},
},
plugins: [
'expo-router',
['expo-notifications', { icon: './assets/images/notification-icon.png', color: '#087ea4' }],
'expo-sqlite',
],
extra: {
questionsBaseUrl: process.env.QUESTIONS_URL ??
'https://raw.githubusercontent.com/alex/drill-questions/main',
},
}
export default configGains immédiats : types (ExpoConfig — l’autocomplétion sur chaque champ), logique
(la variante), et extra — la config lisible côté app :
import Constants from 'expo-constants'
const BASE = Constants.expoConfig?.extra?.questionsBaseUrl as string(Le BASE en dur du ch. 7.2 déménage ici : une URL de repo de test devient possible par
variable d’env, sans toucher au code.)
Les variantes : Drill Dev à côté de Drill
Android identifie une app par son package name : com.alex.drill.dev ≠ com.alex.drill
= deux apps distinctes sur le téléphone. Le dev build (variante dev) et la vraie app
(P12) coexistent — bases SQLite séparées, notifs séparées, icônes distinguables.
Le branchement dans eas.json :
"development": {
"developmentClient": true,
"distribution": "internal",
"env": { "APP_VARIANT": "development" },
"android": { "buildType": "apk" }
}Et en local : $env:APP_VARIANT = 'development'; npx expo run:android. C’est le pattern
standard de tout studio mobile — vous venez de l’installer en 15 lignes.
Les config plugins, démystifiés
Au prebuild, chaque plugin de la liste est une fonction qui reçoit la config et modifie les projets natifs : AndroidManifest.xml, gradle, res/, Info.plist… C’est le mécanisme qui vous évite d’éditer android/ à la main (CNG, ch. 1.3 — le chapitre suivant boucle la boucle).
Ce que font ceux de Drill : expo-router (deep links, scheme dans le manifest) ;
expo-notifications (icône monochrome dans res/, couleur, permissions) ; expo-sqlite
(config de compilation). Les libs tierces sérieuses livrent le leur — le critère n°2 du
choix de lib (ch. 1.4).
La preuve que ce n’est pas magique — un plugin maison en 15 lignes :
// plugins/with-network-security.ts — exemple : autoriser le cleartext vers le LAN en dev
import { withAndroidManifest, type ConfigPlugin } from 'expo/config-plugins'
const withNetworkSecurity: ConfigPlugin = config =>
withAndroidManifest(config, cfg => {
const app = cfg.modResults.manifest.application?.[0]
if (app && process.env.APP_VARIANT === 'development') {
app.$['android:usesCleartextTraffic'] = 'true'
}
return cfg
})
export default withNetworkSecurity// app.config.ts
plugins: [/* … */, './plugins/with-network-security'],withAndroidManifest (et ses frères withGradleProperties, withStringsXml…) donne le fichier
natif en objet ; vous le modifiez ; prebuild l’écrit. Un config plugin = un codemod de
fichiers natifs, versionné avec l’app. Vous n’en écrirez pas souvent — mais vous ne serez
plus jamais BLOQUÉ par « il faut modifier le manifest ».
⚠️ Piège — Toute modification d’app.config.ts ou d’un plugin = le conteneur change : rebuild du dev build (réflexe du ch. 11.1). Et une subtilité :
extraest lu à l’exécution (pas de rebuild pour changer une URL par env var au BUILD suivant), mais tout le reste (name, package, plugins) n’existe qu’au prebuild. Si un changement de config « ne fait rien », demandez-vous QUAND ce champ est consommé.
💡 Pour un dev Next.js — app.config.ts = next.config.ts (la config-code, les env vars, le typage). Les config plugins n’ont pas d’équivalent direct — pensez « des codemods de plateforme exécutés au build », un peu comme les plugins webpack modifiaient le bundle, mais ciblant AndroidManifest/gradle. Et
extra+ Constants ≈ NEXT_PUBLIC_* (avec la même règle : rien de secret dedans — ch. 7.1).
✏️ Exercices
1. Migrez Drill : app.json → app.config.ts avec variantes complètes (nom, package, scheme, icône teintée en dev si vous voulez le luxe). Build local en variante dev : les deux apps coexistent-elles ?
✅ Solution
Après migration et $env:APP_VARIANT='development'; npx expo run:android : « Drill Dev »
s’installe SANS écraser un éventuel Drill (packages différents). Vérifs : le scheme
drill-dev:// répond, adb shell pm list packages | Select-String drill montre les deux.
Piège classique : oublier de supprimer app.json (il aurait priorité… non : app.config.ts
gagne, mais garder les deux invite les incohérences — on supprime).
2. Le BASE du repo de questions doit pointer vers un repo de TEST quand APP_VARIANT= development. Câblez extra + Constants, et vérifiez sans rebuild que le changement d’env var au prochain build suffit.
✅ Solution
extra.questionsBaseUrl conditionné par env (déjà dans l’exemple), hooks/queries.ts lit
Constants. Nuance de vérification : extra est FIGÉ au build (embarqué dans le manifest de
l’app) — changer l’env var exige un rebuild du conteneur, mais PAS de changement de code.
« Sans rebuild » vaut pour le serveur Metro en dev (le config est relu au démarrage de Metro).
Comprendre QUAND chaque valeur est capturée est exactement l’objectif de l’exercice.
3. Lisez le manifest généré : après un prebuild, ouvrez
android/app/src/main/AndroidManifest.xml et identifiez 3 lignes écrites par vos plugins
(scheme, permission notifications, icône). Ne modifiez rien — c’est le chapitre suivant.
✅ Solution
Attendus : <data android:scheme="drill-dev"/> dans un intent-filter (expo-router/scheme) ;
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> et les meta-data
d’icône/couleur de notification (expo-notifications) ; le package name de la variante sur la
racine. Chaque ligne remonte à UNE entrée de votre config — la boîte noire est ouverte.
🧠 Quiz
1. Trois gains d’app.config.ts sur app.json ?
Réponse
Le typage ExpoConfig, la logique (env vars, variantes dev/prod), et extra (config exposée à l’app via Constants) — la config devient du code versionné.
2. Qu’est-ce qui fait que Drill Dev et Drill coexistent ?
Réponse
Le package name Android différent (com.alex.drill.dev vs com.alex.drill) : identité distincte pour le système — données, notifs, icônes séparées.
3. Qu’est-ce qu’un config plugin, en une phrase ?
Réponse
Une fonction exécutée au prebuild qui reçoit la config et modifie les fichiers natifs générés (manifest, gradle, res/) — un codemod de plateforme versionné.
4. Où mettre une valeur de config lisible à l’exécution, et sa règle de sécurité ?
Réponse
extra (lu via expo-constants). Règle : rien de secret — tout ce qui entre dans le binaire
est public (ch. 7.1).
5. Après modification d’un plugin, que faut-il faire ?
Réponse
Rebuild du dev build : les plugins n’agissent qu’au prebuild — le conteneur natif doit être régénéré pour refléter le changement.
👉 Chapitre suivant : 11.4 — Prebuild & CNG — le dossier android/ ouvert une bonne fois, et refermé pour toujours.