AEM Guide

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.frontendwebpackclientlibmavenfrontend-maven-plugin

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:

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]"/>

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