Come il modulo ui.frontend trasforma una build Webpack in una ClientLib di AEM
Come ui.frontend compila JS/CSS con webpack tramite frontend-maven-plugin, e come aem-clientlib-generator trasforma quel dist/ in un cq:ClientLibraryFolder che ui.apps finisce per distribuire in AEM.
ui.frontend è il modulo dell’archetype di progetto AEM di Adobe dove
vive davvero il codice frontend: TypeScript/JavaScript, Sass/CSS e la
configurazione di un bundler, compilato con npm esattamente come in un
progetto frontend autonomo. Uno sviluppatore frontend può lavorarci
dentro, eseguire npm run start, e non toccare mai Java né il resto del
reactor Maven. L’unica cosa specifica di AEM in tutto questo è cosa
succede alla fine della build: il suo output deve diventare una clientlib
di AEM, perché è l’unico modo in cui AEM serve CSS/JS a una pagina
renderizzata. Questo articolo copre esattamente quel punto di passaggio —
da una cartella dist/ di webpack a un cq:ClientLibraryFolder che
finisce dentro ui.apps.
Cos’è davvero ui.frontend
Quando esegui l’aem-project-archetype di Adobe, la proprietà
frontendModule sceglie quale variante di ui.frontend viene generata:
general (webpack semplice + TypeScript/Sass), angular, react, oppure
none/decoupled se non se ne vuole nessuna. Questo articolo si
concentra sulla variante general, perché è la base più comune e quella
da cui partono la maggior parte dei progetti; le varianti React e Angular
cambiano il bundler (react-scripts, Angular CLI) ma convergono sullo
stesso meccanismo di output descritto di seguito.
Il package.json del modulo general dichiara webpack 5, TypeScript,
Babel, Sass ed ESLint come devDependencies, ed espone questi script 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 avvia un webpack-dev-server con live reload su un
template HTML statico — utile per iterare su markup/stili senza
un’istanza AEM in esecuzione, anche se non riflette il markup
effettivamente renderizzato da AEM. npm run dev e npm run prod sono i
due che contano per AEM: eseguono una build completa di webpack e poi
passano il testimone ad aem-clientlib-generator (il comando
clientlib), che è la parte che produce davvero contenuto AEM.
Il ui.frontend/pom.xml collega questo a Maven tramite
com.github.eirslett:frontend-maven-plugin, legato alla fase
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 installa una distribuzione locale di Node/npm se
necessario ed esegue npm run prod. Per questo un mvn clean install su
un progetto AEM appena generato può compilare il frontend senza che
nessuno abbia Node installato globalmente, e per questo uno sviluppatore
frontend può iterare su ui.frontend con semplici comandi npm senza avere
bisogno di Maven o di un JDK — i due mondi si incontrano solo in questo
unico punto di aggancio del plugin. (Un profilo Maven fedDev sostituisce
questo con npm run dev, per avere source map e output non minificato
durante lo sviluppo locale.)
Dall’output di webpack a una clientlib AEM
Webpack da solo sa produrre solo bundle JS/CSS — non ha idea di cosa sia
un cq:ClientLibraryFolder. Questa traduzione è il compito del secondo
comando dentro npm run prod: clientlib --verbose, dal pacchetto npm
aem-clientlib-generator,
configurato in ui.frontend/clientlib.config.js.
Webpack stesso scrive il proprio output in ui.frontend/dist/, diviso in
due librerie logiche:
dist/clientlib-site/—site.js,site.csse una cartellaresources/per immagini/font referenziati dal CSS.dist/clientlib-dependencies/—dependencies.js,dependencies.css(codice di terze parti, tenuto separato per poter essere cachato/aggiornato in modo indipendente dal codice proprio del sito).
clientlib.config.js dice ad aem-clientlib-generator come trasformare
ciascuna di queste cartelle in una clientlib AEM e, punto cruciale, dove
scriverla:
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} è la proprietà dell’archetype Maven scelta al momento della
generazione del progetto (l’id breve dell’applicazione, es. myproject)
— diventa sia il nome del nodo /apps/<appId> sia il prefisso di ogni
categoria. Nota clientLibRoot: non punta da nessuna parte dentro
ui.frontend — punta direttamente nell’albero sorgente di ui.apps.
Questo è il meccanismo reale dietro “ui.frontend produce la clientlib che
vive in ui.apps”: è una semplice scrittura su filesystem nelle sorgenti di
un altro modulo, non uno step di packaging né una dichiarazione di
dipendenza a runtime.
Anatomia della clientlib generata
Con la configurazione sopra, una build produce questa struttura
direttamente sotto
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 dichiara il nodo cq:ClientLibraryFolder con le proprietà
provenienti dalla configurazione:
<?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]"/>
categoriesè ciò che HTL e la Page Policy referenziano per includere questa libreria in una pagina — non ha alcuna relazione con il nome della cartella.dependenciesdice all’HTML Library Manager di AEM di risolvere ed emettere sempremyproject.dependenciesprima dimyproject.site, così il codice di terze parti si carica per primo indipendentemente dall’ordine nel markup.allowProxyespone la libreria sotto il percorso con versione/etc.clientlibs/...(obbligatorio su AEM as a Cloud Service, dato che/appsnon è servito pubblicamente) invece del percorso grezzo/apps/myproject/clientlibs/....embed(non usato sopra, ma comune nelle clientlib scritte a mano) permette a una libreria di incorporare staticamente il contenuto di un’altra categoria in fase di build/richiesta — lo si vede sumyproject.dependenciesnei progetti che incorporano una clientlib vendor condivisa invece di dichiararla come semplice dipendenza.
js.txt è il manifesto che l’HTML Library Manager legge per sapere quali
file sotto js/ concatenare, e in quale ordine:
#base=js
site.js
css.txt segue lo stesso schema sulla cartella css/. Poiché
aem-clientlib-generator rigenera entrambi i file a ogni build a partire
da ciò che webpack ha prodotto, per questo modulo non si mantengono mai a
mano — ed è esattamente per questo che non andrebbero modificati a mano
(più dettagli più avanti).
Il passaggio di consegne a ui.apps
ui.frontend e ui.apps sono moduli Maven indipendenti, senza alcuna
<dependency> tra loro, ma il pom.xml padre elenca ui.frontend prima
di ui.apps in <modules>, e il reactor di Maven rispetta quell’ordine
dichiarato quando non c’è un grafo di dipendenze che imponga altrimenti.
Quindi in un mvn clean install completo, quando il
filevault-package-maven-plugin di ui.apps percorre
ui.apps/src/main/content/jcr_root per costruire il content package, le
cartelle clientlib-site e clientlib-dependencies sono già presenti su
disco — scritte da aem-clientlib-generator durante la precedente fase
generate-resources di ui.frontend. ui.apps le impacchetta poi come
qualsiasi altro contenuto sotto jcr_root, senza alcun trattamento
speciale.
Questo è tutto il passaggio di consegne: una scrittura sul filesystem da
un modulo verso l’albero sorgente di un altro, sincronizzata dall’ordine
delle fasi Maven. È anche il motivo per cui un mvn -pl ui.apps (o
qualsiasi reactor parziale che salta ui.frontend) distribuisce
silenziosamente quanto era stato generato per ultimo su disco, aggiornato
o meno — Maven non ha modo di sapere che il frontend deve essere
ricostruito a meno che ui.frontend non sia effettivamente presente nel
reactor di quell’esecuzione.
Una volta che la clientlib è dentro un pacchetto ui.apps distribuito, uno
script HTL referenzia la sua categoria — mai un percorso di file —
attraverso il template clientlib dei 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'}" />
In pratica, la maggior parte dei progetti generati con l’archetype non
codifica nemmeno questo in ogni componente — clientlib-site e
clientlib-dependencies sono configurate nella Page Policy della
pagina (Content Page Template → Page Information → Page Policy), così
ogni pagina che usa quel template le ottiene automaticamente.
Dove questo si presenta nella pratica
- Una modifica frontend ha compilato correttamente ma non compare mai
sulla pagina. Controlla in quest’ordine:
ui.frontendsi è davvero eseguito in questa build (un reactor parzialemvn -pl ui.appsnon lo tocca, e non si lamenta nemmeno);npm run prod/devè terminato senza fallire silenziosamente prima dello stepclientlib; e solo a quel punto sospetta della cache. - Cache della clientlib non aggiornata. I file uniti e minificati
site.js/site.cssnon cambiano nome tra una build e l’altra (nessun hash del contenuto nel nome del file), quindi la cache del browser o del dispatcher/CDN può continuare a servire una versione vecchia dopo un redeploy riuscito. Aggiungi?debugClientLibs=trueall’URL della pagina per disabilitare l’unione e la minificazione e caricare ogni file sorgente separatamente — ti dice immediatamente se stai guardando una cache non aggiornata o una build che davvero non includeva la tua modifica. - Disallineamento di categoria. Se la proprietà dell’archetype
appIdo i nomi delle categorie inclientlib.config.jscambiano ma la Page Policy o uno script HTL continuano a referenziare la vecchia stringa di categoria, la build ha successo e la clientlib esiste nel repository — semplicemente non viene mai inclusa in nessuna pagina. - Modificare direttamente i file generati. Non modificare mai a mano
nulla sotto
ui.apps/.../clientlibs/clientlib-site/. Quei filejs/,css/,js.txte.content.xmlsono output diaem-clientlib-generator, rigenerati completamente a ogninpm run prod/dev. La sorgente reale vive sottoui.frontend/src; una modifica fatta direttamente inui.appssopravvive solo fino alla build frontend successiva, che la sovrascrive silenziosamente.