AEM Guide

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

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:

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

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