Créer son propre loader Webpack
Comment créer un loader Webpack? Et, plus difficile, comment l'utiliser avec create-react-app
? Voici mon retour d'expérience et mes conseils.
Le cas d'usage
Dans un projet client dont l'interface est proposée en plusieurs langues, nous avons utilisé polyglot, un projet open-source de Airbnb, pour la traduction. Polyglot stocke les traductions dans des fichiers au format JSON :
// in i18n/en.js
{
"support": "Please contact the support team",
"basket": {
"add": "Add to basket"
}
}
// in i18n/fr.js
{
"support": "Merci de contacter le service support",
"basket": {
"add": "Ajouter au panier"
}
}
Au moment de faire traduire les intitulés dans de nouvelles langues, le client a réalisé que ses traducteurs préfèrent le format gettext pour les traductions. Ce format facilite la collaboration en permettant aux traducteurs de modifier les traductions directement dans des interfaces graphiques. Il est basé sur des fichiers .po
. Un fichier .po
est un fichier texte de la forme suivante :
msgid "Please contact the support team"
msgstr "Merci de contacter le service support"
msgid "Add to basket"
msgstr "Ajouter au panier"
Où msgid
est le texte dans la langue de base (ici, l'anglais), et mgstr
le texte traduit dans la langue cible (ici, le français).
Mon collégue Gildas a donc créé un outil pour convertir les fichiers de traduction de Polyglot en JSON au format po (polyglot-po). Ainsi, le code continue d'utiliser Polyglot et les traducteurs peuvent utiliser gettext.
Mais nous nous sommes alors retrouvés avec un probléme : il y avait deux sources de vérité pour les fichiers de traduction.
- Les fichiers JSON que polyglot utilise, où les développeurs ajoutent les nouveaux termes durant le développement.
- Et les fichiers
.po
qui sont édités par les traducteurs.
Je vous laisse imaginer les éventuels conflits qui peuvent émerger. Nous avions besoin d'avoir un seul type de fichier à maintenir, et donc une seule source de vérité : en l'occurence, ne garder que les fichiers .po
.
Le projet utilise déjà Webpack, notamment pour convertir le TypeScript en JavaScript. Nous avons maintenant besoin de dire à webpack de convertir également les fichiers po en json lors de la publication.
Ainsi nous pourrons importer des traduction JSON depuis des fichier po:
import frenchTranslations from 'fr.po';
Comment ? Avec un loader webpack.
## Le loader webpack
Un loader webpack ?! Mais ça doit être trés compliqué !
En fait pas du tout, voici le code de notre polyglot-po-loader
:
const { convertPoStringToJson } = require('polyglot-po');
module.exports = function polyglotPoLoader(content) {
const json = convertPoStringToJson(content);
return `module.exports = ${JSON.stringify(json)}`;
};
Et c'est tout.
Un loader webpack n'est qu'une fonction qui est appelée avec le contenu de chaque fichier. Le loader peut modifier ce contenu comme bon lui semble. Il faut juste retourner une chaîne de caractères à la fin.
L'histoire aurait pu s'arrêter là, seulement voilà, notre projet utilise create-react-app
, un utilitaire de configuration pour les sites React.
Et c'est là que les ennuis ont commencé.
Ajouter le loader à create-react-app
create-react-app
gére la configuration Webpack, et il ne permet pas de l'éditer. A moins d'éjecter l'application, mais on perd alors tout l'intérêt de create-react-app
.
Donc non.
Heureusement il existe react-app-rewired. Cet utilitaire se greffe par dessus create-react-app
, et permet de modifier la configuration Webpack à l'aide d'un fichier config-overrides.js
.
const path = require('path');
module.exports = webpackConfig => {
// do what you want to the config
return webpackConfig;
};
Très bien!
Nous voulons donc ajouter un nouveau loader. Ils se trouvent dans config.module.rules
:
module.exports = config => {
config.module.rules.push({
test: /\.po$/,
use: [{
loader: 'polyglot-po-loader',
}],
});
return config;
};
Et voilà le fichier .po
converti en json :
module.exports = __webpack_public_path__ + "static/media/en.f9a36ed4.po";
Ou pas.
J'ai passé plusieurs heures à essayer de comprendre pourquoi polyglot-po-loader
générait mes fichiers dans le dossier static
. Et créait un fichier exportant le lien vers ce fichier statique au lieu d'exporter directement le json généré.
La réponse se trouvait dans la configuration webpack de create-react-app :
// "file" loader makes sure those assets get served by WebpackDevServer.
// When you `import` an asset, you get its (virtual) filename.
// In production, they would get copied to the `build` folder.
// This loader doesn't use a "test" so it will catch all modules
// that fall through the other loaders.
{
loader: require.resolve('file-loader'),
// Exclude `js` files to keep "css" loader working as it injects
// its runtime that would otherwise be processed through "file" loader.
// Also exclude `html` and `json` extensions so they get processed
// by webpacks internal loaders.
exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
options: {
name: 'static/media/[name].[hash:8].[ext]',
},
},
// ** STOP ** Are you adding a new loader?
// Make sure to add the new loader(s) before the "file" loader.
Donc pour ajouter un loader sur un format autre que JS dans create-react-app
, il faut placer notre loader avant le file-loader "attrape tout" de create-react-app
:
const path = require('path');
module.exports = config => {
const lastLoader = config.module.rules.pop();
lastLoader.oneOf.unshift({
test: /\.po$/,
use: [{
loader: 'polyglot-po-loader',
}],
});
config.module.rules.push(lastLoader);
return config;
};
Conclusion
create-react-app
, est un bonne idée en théorie, mais en pratique il ne laisse pas assez d'options pour personnaliser la configuration qu'il met en place - et notamment celle de Webpack.
Personnellement, aujourd'hui, je préfére tout mettre en place à la main. Il est vrai que je passe un peu plus de temps au démarrage du projet. Mais je maîtrise la configuration et regagne ce temps dès qu'il s'agit de la personnaliser. Et sur tout projet conséquent, ça finit toujours par arriver.