AEM & Vite integration - Part 2

2021-11-09

In the previous part I've shown you how to setup a development integration using AEM and Vite, in this part I'll show you have to prepare for production.

vite build

When it is time to deploy your app for production, simply run the vite build command. By default, it uses <root>/index.html as the build entry point, and produces an application bundle that is suitable to be served over a static hosting service. Check out the Deploying a Static Site for guides about popular services.

- https://vitejs.dev/guide/build.html

In the case of AEM we don't have an HTML file as build entry point, we do have a Javascript file. In this example we specify an input set to main.ts in the vite.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export default defineConfig({
  build: {
    outDir: 'dist/clientlib-esmodule',
    manifest: true,
    rollupOptions: {
      output: {
        assetFileNames:
          'etc.clientlibs/aem-vite-demo/clientlibs/clientlib-esmodule/resources/[ext]/[name].[hash][extname]',
        chunkFileNames:
          'etc.clientlibs/aem-vite-demo/clientlibs/clientlib-esmodule/resources/chunks/[name].[hash].js',
        entryFileNames:
          'etc.clientlibs/aem-vite-demo/clientlibs/clientlib-esmodule/resources/js/[name].[hash].js',
      },
      input: {
        app: 'src/main.ts',
      },
    },
  }
});

Not only the input is important here, also note:

  • outDir: directory where Vite will write its output to. Will be used by vite-aem-clientlib-generator npm package (see later in this post)
  • manifest: when set to true, the build will also generate a manifest.json file that contains a mapping of non-hashed asset filenames to their hashed versions, which can then be used by a server framework to render the correct asset links.
  • fileNames: allows you to specify a directory structure and naming convention that will be used for the files written to the outDir. In the case of how I have setup the AEM & Vite integration it's important that those paths mimic the exact location of chunks living in resources folders of proxy enabled clientlibs. For example: http://localhost:4502/etc.clientlibs/aem-vite-demo/clientlibs/clientlib-esmodule/resources/js/app.405d9e8c.js

The manifest has a Record<name, chunk> structure that contains the following information:

  • file: proxy path pointing to the chunk file living in a /resources clientlib folder
  • isEntry: true if the chunk needs to be loaded directly
  • isDynamicEntry: true if the chunk is dynamically loaded within another chunk
  • imports: list of chunk names that also need to be loaded directly
  • dynamicImports: list of chunk names that are dynamically loaded within another chunk
  • css: lists of proxy paths pointing to css files living in a /resources clientlib folder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
{
  "src/main.ts": {
    "file": "etc.clientlibs/aem-vite-demo/clientlibs/clientlib-esmodule/resources/js/app.405d9e8c.js",
    "src": "src/main.ts",
    "isEntry": true,
    "imports": [
      "_vendor.9c4555cf.js"
    ],
    "dynamicImports": [
      "src/other-module.ts"
    ],
    "css": [
      "etc.clientlibs/aem-vite-demo/clientlibs/clientlib-esmodule/resources/css/app.6c6c8de1.css"
    ]
  },
  "_vendor.9c4555cf.js": {
    "file": "etc.clientlibs/aem-vite-demo/clientlibs/clientlib-esmodule/resources/chunks/vendor.9c4555cf.js"
  },
  "src/other-module.ts": {
    "file": "etc.clientlibs/aem-vite-demo/clientlibs/clientlib-esmodule/resources/chunks/other-module.40df526e.js",
    "src": "src/other-module.ts",
    "isDynamicEntry": true,
    "imports": [
      "src/main.ts",
      "_vendor.9c4555cf.js"
    ],
    "dynamicImports": [
      "src/nested-module.ts"
    ],
    "css": [
      "etc.clientlibs/aem-vite-demo/clientlibs/clientlib-esmodule/resources/css/other-module.d8b88b27.css"
    ]
  },
  "src/nested-module.ts": {
    "file": "etc.clientlibs/aem-vite-demo/clientlibs/clientlib-esmodule/resources/chunks/nested-module.42a67543.js",
    "src": "src/nested-module.ts",
    "isDynamicEntry": true,
    "css": [
      "etc.clientlibs/aem-vite-demo/clientlibs/clientlib-esmodule/resources/css/nested-module.e9adb657.css"
    ]
  }
}

vite-aem-clientlib-generator

I've created a CLI that can be used to inspect the manifest and create a modified clientlib. The clientlib will contain specific properties that can be used to generated the correct tags for the HTML. An example:

1
2
<link rel="stylesheet" href="/assets/{{ manifest['main.js'].css }}" />
<script type="module" src="/assets/{{ manifest['main.js'].file }}"></script>

The CLI can be called using vite-aem-lib generate, the first thing it will do is look for the configuration file: vite.lib.config.js, note that at this time only the CommonJS format is supported. A configuration file for a Vite enabled multi clientlib setup can look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
const path = require('path');

const buildManifest = (name) => {
  return path.join(__dirname, 'dist', name, 'manifest.json');
};

const buildResourcesDir = (name) => {
  return path.join(
    __dirname,
    'dist',
    name,
    'etc.clientlibs',
    'aem-vite-demo',
    'clientlibs',
    name,
    'resources'
  );
};

const buildClientlibDir = (name) => {
  return path.join(
    __dirname,
    '..',
    'ui.apps',
    'src',
    'main',
    'content',
    'jcr_root',
    'apps',
    'aem-vite-demo',
    'clientlibs',
    name
  );
};

const createLib = (name, categories) => {
  return {
    manifest: buildManifest(name),
    resourcesDir: buildResourcesDir(name),
    clientlibDir: buildClientlibDir(name),
    categories: [...categories],
    properties: {
      moduleIdentifier: 'vite',
    },
  };
};

module.exports = {
  libs: [
    createLib('clientlib-esmodule', ['aem-vite-demo.esmodule']),
    createLib('clientlib-esmodule-another', ['aem-vite-demo.esmodule.another']),
  ],
};

This is what CLI does in a nutshell:

  1. Fetches and validates the vite.lib.config.js config file
  2. Loop over all the libraries
  3. Get the manifest living in the outDir
  4. Find the entry
  5. Build a list of resources: direct JS/CSS imports & dynamic JS imports
  6. Create a clientlib at the location provided in the config
  7. Push all chunks to the resources folder
  8. Create a .content.xml file containing information necessary to build the HTML tags.

Here is an example of how the .content.xml file could look like:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
          jcr:primaryType="cq:ClientLibraryFolder" categories="[aem-vite-demo.esmodule]"
          cssProcessor="[default:none,min:none]" jsProcessor="[default:none,min:none]" allowProxy="{Boolean}true"
          scripts="[/etc.clientlibs/aem-vite-demo/clientlibs/clientlib-esmodule/resources/js/app.405d9e8c.js]"
          preloads="[/etc.clientlibs/aem-vite-demo/clientlibs/clientlib-esmodule/resources/chunks/vendor.9c4555cf.js]"
          stylesheets="[/etc.clientlibs/aem-vite-demo/clientlibs/clientlib-esmodule/resources/css/app.6c6c8de1.css]"
          moduleIdentifier="vite"/>

The .content.xml file has 3 custom properties that will be used during tag generation in the Java implementation:

  1. scripts: "<script type=\"module\" crossorigin src=\""+ file +"\"></script>"
  2. preloads: "<link rel=\"modulepreload\" href=\""+ file +"\">"
  3. stylesheets: "<link rel=\"stylesheet\" href=\""+ file +"\">"

Note that none of the dynamic imported chunks are listed in any of these properties, they will be loaded dynamically and are not used during tag generation.

You might also find the modulepreload rel of the style tag weird, at least it was for me at the beginning. We generate <link rel="modulepreload"> directives for entry chunks and their direct imports. I hope the quotes below give you some insight:

Module-based development offers some real advantages in terms of cacheability, helping you reduce the number of bytes you need to ship to your users. The finer granularity of the code also helps with the loading story, by letting you prioritize the critical code in your application.

However, module dependencies introduce a loading problem, in that the browser needs to wait for a module to load before it finds out what its dependencies are. One way around this is by preloading the dependencies, so that the browser knows about all the files ahead of time and can keep the connection busy.

- https://developers.google.com/web/updates/2017/12/modulepreload

<link rel="preload"> tells the browser to download and cache a resource (like a script or a stylesheet) as soon as possible. It’s helpful when you need that resource a few seconds after loading the page, and you want to speed it up.

The browser doesn’t do anything with the resource after downloading it. Scripts aren’t executed, stylesheets aren’t applied. It’s just cached – so that when something else needs it, it’s available immediately.

- https://3perf.com/blog/link-rels

So is <link rel="modulepreload"> just <link rel="preload"> for modules?

In a nutshell, yes. By having a specific link type for preloading modules, we can write simple HTML without worrying about what credentials mode we're using. The defaults just work.

- https://developers.google.com/web/updates/2017/12/modulepreload

Demo project

I've created a demo project based on the AEM archetype, you can look into the repository to see the full code and try it out yourself. Here is a small demonstration on how a multi clientlib setup using Vite looks like:

What is missing?

  1. Legacy support, take a look at the Vite documentation for more info.
  2. modulepreload polyfill as not all browsers support the link relation yet.

I hoped you liked this 2 part integration series as much as I did. Do not hesitate to contact me or leave a comment below.

Created by Jeroen Druwé