Cómo el módulo ui.frontend convierte un build de Webpack en un ClientLib de AEM
Cómo ui.frontend compila JS/CSS con webpack a través de frontend-maven-plugin, y cómo aem-clientlib-generator transforma ese dist/ en un cq:ClientLibraryFolder que ui.apps termina desplegando en AEM.
ui.frontend es el módulo del archetype de proyectos AEM de Adobe donde
vive realmente el código frontend: TypeScript/JavaScript, Sass/CSS y la
configuración de un bundler, compilado con npm exactamente igual que en un
proyecto frontend independiente. Un desarrollador frontend puede trabajar
ahí dentro, ejecutar npm run start, y no tocar Java ni el resto del
reactor de Maven. Lo único específico de AEM en todo esto es qué pasa al
final del build: su salida tiene que convertirse en un clientlib de AEM,
porque esa es la única forma en que AEM sirve CSS/JS a una página
renderizada. Este artículo cubre exactamente ese punto de entrega: de una
carpeta dist/ de webpack a un cq:ClientLibraryFolder que termina dentro
de ui.apps.
Qué es realmente ui.frontend
Cuando ejecutas el aem-project-archetype de Adobe, la propiedad
frontendModule decide qué variante de ui.frontend se genera: general
(webpack plano + TypeScript/Sass), angular, react, o none/decoupled
si no quieres ninguno. Este artículo se centra en la variante general,
porque es la base más habitual y de la que parten la mayoría de proyectos;
las variantes React y Angular cambian el bundler (react-scripts, Angular
CLI) pero convergen en el mismo mecanismo de salida que se describe a
continuación.
El package.json del módulo general declara webpack 5, TypeScript,
Babel, Sass y ESLint como devDependencies, y expone estos 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 levanta un webpack-dev-server con recarga en vivo contra
una plantilla HTML estática — útil para iterar sobre markup/estilos sin un
AEM en marcha, aunque no refleja el markup real que renderiza AEM.
npm run dev y npm run prod son los dos que importan para AEM: ejecutan
un build completo de webpack y luego pasan el testigo a
aem-clientlib-generator (el comando clientlib), que es la parte que
realmente produce contenido AEM.
El ui.frontend/pom.xml conecta esto con Maven mediante
com.github.eirslett:frontend-maven-plugin, ligado a la 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 instala una distribución local de Node/npm si hace
falta y ejecuta npm run prod. Por eso un mvn clean install sobre un
proyecto AEM recién generado puede compilar el frontend sin que nadie
tenga Node instalado globalmente, y por eso un desarrollador frontend
puede iterar sobre ui.frontend con comandos npm normales sin necesitar
Maven ni un JDK en absoluto — ambos mundos solo se tocan en este único
binding del plugin. (Un perfil de Maven fedDev sustituye esto por
npm run dev, para tener source maps y salida sin minificar durante el
desarrollo local.)
De la salida de webpack a un clientlib de AEM
Webpack por sí solo solo sabe producir bundles de JS/CSS — no tiene ni
idea de qué es un cq:ClientLibraryFolder. Esa traducción es el trabajo
del segundo comando dentro de npm run prod: clientlib --verbose, del
paquete npm
aem-clientlib-generator,
configurado en ui.frontend/clientlib.config.js.
Webpack en sí escribe su salida en ui.frontend/dist/, dividida en dos
librerías lógicas:
dist/clientlib-site/—site.js,site.cssy una carpetaresources/para imágenes/fuentes referenciadas desde el CSS.dist/clientlib-dependencies/—dependencies.js,dependencies.css(código de terceros, mantenido aparte para poder cachearlo/actualizarlo de forma independiente al código propio del sitio).
clientlib.config.js le dice a aem-clientlib-generator cómo convertir
cada una de esas carpetas en un clientlib de AEM y, lo importante,
dónde escribirlo:
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} es la propiedad del archetype de Maven elegida al generar el
proyecto (el id corto de la aplicación, p. ej. myproject) — se convierte
tanto en el nombre del nodo /apps/<appId> como en el prefijo de cada
categoría. Fíjate en clientLibRoot: no apunta a ningún sitio dentro de
ui.frontend — apunta directamente al árbol de fuentes de ui.apps. Este
es el mecanismo real detrás de “ui.frontend produce el clientlib que vive
en ui.apps”: es una simple escritura en disco dentro del código fuente de
otro módulo, no un paso de empaquetado ni una declaración de dependencia
en tiempo de ejecución.
Anatomía del clientlib generado
Con la configuración anterior, un build produce esta estructura
directamente bajo
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 declara el nodo cq:ClientLibraryFolder con las
propiedades definidas en la configuración:
<?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]"/>
categorieses lo que HTL y la Page Policy referencian para incluir esta librería en una página — no tiene ninguna relación con el nombre de la carpeta.dependenciesle dice al HTML Library Manager de AEM que resuelva y emita siempremyproject.dependenciesantes quemyproject.site, así el código de terceros carga primero sin importar el orden del markup.allowProxyexpone la librería bajo la ruta versionada/etc.clientlibs/...(obligatorio en AEM as a Cloud Service, ya que/appsno se sirve públicamente) en lugar de la ruta cruda/apps/myproject/clientlibs/....embed(no usado arriba, pero habitual en clientlibs escritos a mano) permite que una librería incorpore de forma estática el contenido de otra categoría en tiempo de build/petición — se ve enmyproject.dependenciesen proyectos que embeben un clientlib de vendor compartido en vez de declararlo como dependencia simple.
js.txt es el manifiesto que lee el HTML Library Manager para saber qué
archivos bajo js/ concatenar, y en qué orden:
#base=js
site.js
css.txt sigue el mismo patrón sobre la carpeta css/. Como
aem-clientlib-generator regenera ambos archivos en cada build a partir
de lo que produjo webpack, nunca se mantienen a mano para este módulo —
que es exactamente la razón por la que no deberías editarlos a mano (más
abajo se explica por qué).
La entrega a ui.apps
ui.frontend y ui.apps son módulos de Maven independientes, sin ningún
<dependency> entre ellos, pero el pom.xml padre lista ui.frontend
antes que ui.apps en <modules>, y el reactor de Maven respeta ese
orden declarado cuando no hay un grafo de dependencias que obligue a otra
cosa. Así que en un mvn clean install completo, cuando el
filevault-package-maven-plugin de ui.apps recorre
ui.apps/src/main/content/jcr_root para construir el content package, las
carpetas clientlib-site y clientlib-dependencies ya están en disco ahí
— escritas por aem-clientlib-generator durante la fase
generate-resources anterior de ui.frontend. ui.apps simplemente las
empaqueta como cualquier otro contenido bajo jcr_root, sin ningún caso
especial.
Esa es toda la entrega: una escritura en el sistema de archivos desde un
módulo hacia el árbol de fuentes de otro, sincronizada por el orden de
fases de Maven. Por eso también un mvn -pl ui.apps (o cualquier reactor
parcial que se salte ui.frontend) despliega silenciosamente lo último
que había generado en disco, esté o no desactualizado — Maven no tiene
forma de saber que el frontend necesita reconstruirse a menos que
ui.frontend esté realmente en el reactor de esa ejecución.
Una vez que el clientlib está dentro de un paquete ui.apps desplegado,
un script HTL referencia su categoría —nunca una ruta de archivo— a
través de la plantilla de clientlib de los 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 la práctica, la mayoría de proyectos generados con el archetype ni
siquiera hardcodean esto en cada componente — clientlib-site y
clientlib-dependencies se configuran en la Page Policy de la página
(Content Page Template → Page Information → Page Policy), así que
cualquier página que use esa plantilla las obtiene automáticamente.
Dónde esto importa en la práctica
- Un cambio de frontend compiló bien pero nunca aparece en la página.
Comprueba en este orden: ¿realmente se ejecutó
ui.frontenden este build (un reactor parcialmvn -pl ui.appsno lo toca, y tampoco se queja); terminónpm run prod/devsin fallar en silencio antes del pasoclientlib; y solo entonces sospecha de la caché. - Caché de clientlib desactualizada. Los archivos
site.js/site.cssfusionados y minificados no cambian de nombre entre builds (no hay hash de contenido en el nombre de archivo), así que la caché del navegador o del dispatcher/CDN puede seguir sirviendo una versión antigua después de un redeploy correcto. Añade?debugClientLibs=truea la URL de la página para desactivar la fusión y la minificación y cargar cada archivo fuente por separado — te dice al instante si estás viendo una caché desactualizada o un build que realmente no incluyó tu cambio. - Desajuste de categoría. Si la propiedad del archetype
appIdo los nombres de categoría enclientlib.config.jscambian pero la Page Policy o un script HTL siguen referenciando la cadena de categoría antigua, el build funciona y el clientlib existe en el repositorio — simplemente nunca se incluye en ninguna página. - Editar los archivos generados directamente. Nunca edites a mano nada
bajo
ui.apps/.../clientlibs/clientlib-site/. Esos archivosjs/,css/,js.txty.content.xmlson salida deaem-clientlib-generator, regenerada por completo en cadanpm run prod/dev. El origen real vive bajoui.frontend/src; un cambio hecho directamente enui.appssobrevive solo hasta el siguiente build de frontend, que lo sobrescribe en silencio.