Comment le module ui.frontend transforme un build Webpack en ClientLib AEM
Comment ui.frontend compile le JS/CSS avec webpack via frontend-maven-plugin, et comment aem-clientlib-generator transforme ce dist/ en un cq:ClientLibraryFolder qu'ui.apps finit par livrer dans AEM.
ui.frontend est le module de l’archetype de projet AEM d’Adobe où vit
réellement le code frontend : TypeScript/JavaScript, Sass/CSS, et la
configuration d’un bundler, compilé avec npm exactement comme dans un
projet frontend autonome. Un développeur frontend peut y travailler,
lancer npm run start, sans jamais toucher à Java ni au reste du reactor
Maven. La seule chose spécifique à AEM dans tout cela, c’est ce qui se
passe à la fin du build : sa sortie doit devenir un clientlib AEM, car
c’est le seul moyen pour AEM de servir du CSS/JS à une page rendue. Cet
article couvre exactement ce point de bascule — d’un dossier dist/
webpack à un cq:ClientLibraryFolder qui finit dans ui.apps.
Ce qu’est réellement ui.frontend
Quand on exécute le aem-project-archetype d’Adobe, la propriété
frontendModule détermine la variante de ui.frontend générée :
general (webpack simple + TypeScript/Sass), angular, react, ou
none/decoupled si l’on n’en veut pas. Cet article se concentre sur la
variante general, car c’est la base la plus courante et celle dont
partent la plupart des projets ; les variantes React et Angular changent
de bundler (react-scripts, Angular CLI) mais convergent vers le même
mécanisme de sortie décrit ci-dessous.
Le package.json du module general déclare webpack 5, TypeScript,
Babel, Sass et ESLint comme devDependencies, et expose ces scripts npm :
{
"scripts": {
"dev": "webpack --env dev --config ./webpack.dev.js && clientlib --verbose",
"prod": "webpack --config ./webpack.prod.js && clientlib --verbose",
"start": "webpack-dev-server --open --config ./webpack.dev.js",
},
}
npm run start démarre un webpack-dev-server avec rechargement en direct
sur un gabarit HTML statique — utile pour itérer sur le markup/les styles
sans instance AEM en cours d’exécution, même s’il ne reflète pas le markup
réellement rendu par AEM. npm run dev et npm run prod sont les deux
scripts qui comptent pour AEM : ils lancent un build webpack complet puis
passent la main à aem-clientlib-generator (la commande clientlib), qui
est la partie qui produit réellement du contenu AEM.
Le ui.frontend/pom.xml relie tout cela à Maven via
com.github.eirslett:frontend-maven-plugin, lié à la phase
generate-resources :
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<executions>
<execution>
<id>npm run prod</id>
<phase>generate-resources</phase>
<goals><goal>npm</goal></goals>
<configuration>
<arguments>run prod</arguments>
</configuration>
</execution>
</executions>
</plugin>
frontend-maven-plugin installe une distribution locale de Node/npm si
nécessaire et exécute npm run prod. C’est pourquoi un mvn clean install sur un projet AEM tout juste généré peut compiler le frontend
sans que personne n’ait Node installé globalement, et pourquoi un
développeur frontend peut itérer sur ui.frontend avec de simples
commandes npm sans avoir besoin de Maven ni d’un JDK — les deux mondes ne
se rencontrent qu’à ce seul point d’attache du plugin. (Un profil Maven
fedDev substitue npm run dev, pour des source maps et une sortie non
minifiée pendant le développement local.)
De la sortie webpack à un clientlib AEM
Webpack seul ne sait produire que des bundles JS/CSS — il n’a aucune idée
de ce qu’est un cq:ClientLibraryFolder. Cette traduction est le travail
de la seconde commande dans npm run prod : clientlib --verbose, du
paquet npm
aem-clientlib-generator,
configuré dans ui.frontend/clientlib.config.js.
Webpack écrit lui-même sa sortie dans ui.frontend/dist/, répartie en
deux librairies logiques :
dist/clientlib-site/—site.js,site.css, et un dossierresources/pour les images/polices référencées depuis le CSS.dist/clientlib-dependencies/—dependencies.js,dependencies.css(code tiers, gardé séparé pour pouvoir être mis en cache/mis à jour indépendamment du code propre au site).
clientlib.config.js indique à aem-clientlib-generator comment
transformer chacun de ces dossiers en clientlib AEM et, surtout, où
l’écrire :
const CLIENTLIB_DIR = path.join(
__dirname,
'..',
'ui.apps',
'src',
'main',
'content',
'jcr_root',
'apps',
'${appId}',
'clientlibs',
);
module.exports = {
context: path.join(__dirname, 'dist'),
clientLibRoot: CLIENTLIB_DIR,
libs: [
{
name: 'clientlib-dependencies',
categories: ['${appId}.dependencies'],
allowProxy: true,
serializationFormat: 'xml',
assets: {
js: { cwd: 'clientlib-dependencies', files: ['**/*.js'] },
css: { cwd: 'clientlib-dependencies', files: ['**/*.css'] },
},
},
{
name: 'clientlib-site',
categories: ['${appId}.site'],
dependencies: ['${appId}.dependencies'],
allowProxy: true,
serializationFormat: 'xml',
assets: {
js: { cwd: 'clientlib-site', files: ['**/*.js'] },
css: { cwd: 'clientlib-site', files: ['**/*.css'] },
resources: {
cwd: 'clientlib-site',
files: ['**/*.*'],
ignore: ['**/*.js', '**/*.css'],
},
},
},
],
};
${appId} est la propriété de l’archetype Maven choisie à la génération
du projet (l’identifiant court de l’application, p. ex. myproject) —
elle devient à la fois le nom du nœud /apps/<appId> et le préfixe de
chaque catégorie. Remarquez clientLibRoot : il ne pointe nulle part dans
ui.frontend — il pointe directement dans l’arborescence source de
ui.apps. C’est le mécanisme réel derrière « ui.frontend produit le
clientlib qui vit dans ui.apps » : une simple écriture sur le système de
fichiers dans les sources d’un autre module, pas une étape de packaging ni
une déclaration de dépendance au runtime.
Anatomie du clientlib généré
Avec la configuration ci-dessus, un build produit cette structure
directement sous
ui.apps/src/main/content/jcr_root/apps/myproject/clientlibs/clientlib-site/ :
clientlib-site/
├── .content.xml
├── js.txt
├── css.txt
├── js/
│ └── site.js
├── css/
│ └── site.css
└── resources/
└── site.js.map
.content.xml déclare le nœud cq:ClientLibraryFolder avec les
propriétés issues de la configuration :
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="cq:ClientLibraryFolder"
allowProxy="{Boolean}true"
categories="[myproject.site]"
dependencies="[myproject.dependencies]"/>
categoriesest ce que HTL et la Page Policy référencent pour inclure cette librairie dans une page — cela n’a aucun rapport avec le nom du dossier.dependenciesindique au HTML Library Manager d’AEM de toujours résoudre et émettremyproject.dependenciesavantmyproject.site, pour que le code tiers se charge en premier quel que soit l’ordre du markup.allowProxyexpose la librairie sous le chemin versionné/etc.clientlibs/...(obligatoire sur AEM as a Cloud Service, puisque/appsn’est pas servi publiquement) plutôt que le chemin brut/apps/myproject/clientlibs/....embed(non utilisé ci-dessus, mais courant dans les clientlibs écrits à la main) permet à une librairie d’incorporer statiquement le contenu d’une autre catégorie au moment du build/de la requête — on le voit surmyproject.dependenciesdans les projets qui embarquent un clientlib vendor partagé plutôt que de le déclarer comme simple dépendance.
js.txt est le manifeste que le HTML Library Manager lit pour savoir
quels fichiers sous js/ concaténer, et dans quel ordre :
#base=js
site.js
css.txt suit le même modèle sur le dossier css/. Comme
aem-clientlib-generator régénère les deux fichiers à chaque build à
partir de ce que webpack a produit, on ne les maintient jamais à la main
pour ce module — c’est d’ailleurs exactement pour cela qu’il ne faut pas
les éditer à la main (plus de détails plus bas).
Le relais vers ui.apps
ui.frontend et ui.apps sont des modules Maven indépendants, sans
<dependency> entre eux, mais le pom.xml parent liste ui.frontend
avant ui.apps dans <modules>, et le reactor Maven respecte cet ordre
déclaré tant qu’aucun graphe de dépendances n’impose autre chose. Ainsi,
dans un mvn clean install complet, au moment où le
filevault-package-maven-plugin d’ui.apps parcourt
ui.apps/src/main/content/jcr_root pour construire le content package,
les dossiers clientlib-site et clientlib-dependencies sont déjà
présents sur le disque — écrits par aem-clientlib-generator durant la
phase generate-resources précédente d’ui.frontend. ui.apps les
empaquette alors comme n’importe quel autre contenu sous jcr_root, sans
traitement particulier.
C’est tout le relais : une écriture sur le système de fichiers d’un module
vers l’arborescence source d’un autre, synchronisée par l’ordre des phases
Maven. C’est aussi pourquoi un mvn -pl ui.apps (ou tout reactor partiel
qui saute ui.frontend) livre silencieusement ce qui a été généré en
dernier sur le disque, périmé ou non — Maven n’a aucun moyen de savoir que
le frontend doit être reconstruit, sauf si ui.frontend fait réellement
partie du reactor de cette exécution.
Une fois le clientlib présent dans un package ui.apps déployé, un script
HTL référence sa catégorie — jamais un chemin de fichier — via le gabarit
clientlib des Core Components :
<sly
data-sly-use.clientlib="core/wcm/components/commons/v1/templates/clientlib.html"
data-sly-call="${clientlib.css @ categories='myproject.site'}"
/>
<sly data-sly-call="${clientlib.js @ categories='myproject.site'}" />
En pratique, la plupart des projets générés par l’archetype ne codent même
pas cela en dur dans chaque composant — clientlib-site et
clientlib-dependencies sont configurés dans la Page Policy de la
page (Content Page Template → Page Information → Page Policy), donc
chaque page utilisant ce gabarit les récupère automatiquement.
Là où cela se manifeste en pratique
- Un changement frontend a compilé sans erreur mais n’apparaît jamais
sur la page. Vérifiez dans cet ordre :
ui.frontends’est-il réellement exécuté dans ce build (un reactor partielmvn -pl ui.appsne le touche pas, et ne s’en plaint pas non plus) ;npm run prod/devs’est-il terminé sans échouer silencieusement avant l’étapeclientlib; et seulement ensuite, soupçonnez le cache. - Cache de clientlib périmé. Les fichiers fusionnés/minifiés
site.js/site.cssne changent pas de nom entre les builds (pas de hash de contenu dans le nom de fichier), donc le cache du navigateur ou du dispatcher/CDN peut continuer à servir une ancienne version après un redéploiement réussi. Ajoutez?debugClientLibs=trueà l’URL de la page pour désactiver la fusion et la minification et charger chaque fichier source séparément — cela indique immédiatement si vous êtes face à un cache périmé ou à un build qui n’a réellement pas inclus votre changement. - Désaccord de catégorie. Si la propriété d’archetype
appIdou les noms de catégorie dansclientlib.config.jschangent mais que la Page Policy ou un script HTL référencent toujours l’ancienne chaîne de catégorie, le build réussit et le clientlib existe bien dans le dépôt — il n’est simplement jamais inclus sur aucune page. - Éditer directement les fichiers générés. Ne modifiez jamais à la
main quoi que ce soit sous
ui.apps/.../clientlibs/clientlib-site/. Ces fichiersjs/,css/,js.txtet.content.xmlsont la sortie d’aem-clientlib-generator, entièrement régénérée à chaquenpm run prod/dev. La véritable source vit sousui.frontend/src; une modification faite directement dansui.appsne survit que jusqu’au prochain build frontend, qui l’écrase silencieusement.