Using Vite in Adobe Experience Manager

Fabio Brunori
Valtech Switzerland
6 min readSep 4, 2023

Webpack is our good ol’ reliable bundler but if you ever dig into the configuration of a project that you didn’t setup yourself, you know how interesting it can get.

Vite comes with quite a few advantages, solving most of the problems we have with webpack:

  • It’s very well supported.
  • Well Maintained.
  • Super Fast. (more on that at the end).
  • Very minimal configuration.
  • All Rollup plugins work OOTB.

If you wanted to give Vite a try in your AEM Frontend setup here’s a quick path to do so:

Prerequisites:

Plan:

  • Create new empty project from archetype
  • Understand what deliverables are to be produced
  • Understand the source code
  • Vite configuration
  • Profit!
Vite Logo

Creating the project:

Create an empty project using the maven AEM archetype with the parameter -D frontendModule="general".

Understanding current solution:

The default setup uses webpack:

  • The only entry point is SOURCE_ROOT + '/site/main.ts'. The scss result will be extracted by the MiniCssExtractPlugin from the bundle (imported in main.ts).
  • The target folder is resolve(__dirname, 'dist').
  • All the files under /resources are copied into the dist folder under clientlib-site/; The js and css bundles are copied there as well.
  • The production build uses Terser and CssMinimizer to reduce the bundle size

After the compile process is done, the build script uses clientlib from aem-clientlib-generator to get the compiled js and css files and create an AEM-compliant client library, that will then be placed into the ui.apps project and finally deployed into the instance.

The final deliverable structure in ui.apps will be like this:

| clientlib-site
| css
- site.css
| js
- site.js
- .content.xml
- css.txt
- js.txt

If we want to update any folder structure, we need to keep in mind that the clientlib.config file will have to be updated as well.

Using Vite:

Let’s get down to business: we want a Vite configuration that can parse scss files, ts files and both using glob imports like import * from './**/*.ts'.

Some key points here:

  • There’s no hash after the filenames (Vite puts it in by default so we have to remove it).
  • The output needs to go to dist/clientlib-site/ and assets should be there (by default would be in assets/).
  • We want css to be an external file and not in-js.
  • For glob imports, Vite comes with the feature. There’re some caveats and we need to do some config changes to make sure it works.
  • Glob imports can’t be used with sass (if you know different please leave a comment, I’ll be happy to rectify).

Let’s write our vite.config.ts file:

import { defineConfig } from 'vite';
import eslintPlugin from 'vite-plugin-eslint';

export default defineConfig(({ mode }) => ({
plugins: [eslintPlugin({ failOnError: true })],
build: {
rollupOptions: {
input: mode === 'development' ? 'src/main/webpack/static/index.html' : 'src/main/webpack/site/main.ts',
output: {
entryFileNames: `[name].js`,
chunkFileNames: `[name].js`,
assetFileNames: '[name].[ext]',
},
},
sourcemap: 'inline',
minify: true,
cssMinify: true,
outDir: 'dist/clientlib-site',
assetsDir: '',
cssCodeSplit: false,
},
publicDir: 'src/main/webpack/resources',
server: {
proxy: {
'/content': {
target: 'http://localhost:4502',
changeOrigin: true,
secure: false,
},
'/etc.clientlibs': {
target: 'http://localhost:4502',
changeOrigin: true,
secure: false,
},
},
open: 'src/main/webpack/static/',
},
}));

Since AEM has minification enabled in the ootb webpack configuration both for js and css assets, we’ll keep it the same in Vite.

Goes by itself that the vite and vite-plugin-eslint need to be added as dev dependencies npm i -D vite vite-plugin-eslint. Note that eslint is already in the package list as it was there before; Feel free to upgrade it to the last version so it is in line with the latest typescript specs)

For the glob imports, we need to allow the import.meta to be used, setting in tsconfig.json to use "module": "ES2022" in the compiler options. We also need to add "vite/client" in the "types" array. our config should look something like this now:

{
"compilerOptions": {
"target": "es5",
"module": "ES2022",
"baseUrl": "../ui.frontend",
"removeComments": true,
"allowJs": true,
"preserveConstEnums": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"types": ["vite/client"]
},
"include": [ "./src/**/*.ts" ]
}

Glob imports don’t work with sass, so we have to explode the import in main.scss:

@import 'variables';
@import 'base';

@import '../components/accordion';
@import '../components/breadcrumb';
@import '../components/button';
@import '../components/carousel';
@import '../components/container';
@import '../components/contentfragment';
@import '../components/contentfragmentlist';
@import '../components/download';
@import '../components/embed';
@import '../components/experiencefragment';
@import '../components/form-button';
@import '../components/form-options';
@import '../components/form-text';
@import '../components/form';
@import '../components/helloworld';
@import '../components/image';
@import '../components/languagenavigation';
@import '../components/list';
@import '../components/navigation';
@import '../components/pdfviewer';
@import '../components/progressbar';
@import '../components/search';
@import '../components/separator';
@import '../components/tabs';
@import '../components/teaser';
@import '../components/text';
@import '../components/title';

@import './styles/container_main';
@import './styles/experiencefragment_footer';
@import './styles/experiencefragment_header';

In main.ts we can now import all the component js files with the new syntax:

// Stylesheets
import "./main.scss";

// Javascript or Typescript
import.meta.glob("./**/*.ts");
import.meta.glob('../components/**/*.js');

We can get rid of all the webpack stuff already:

@babel/core
@babel/plugin-proposal-class-properties
@babel/plugin-proposal-object-rest-spread
css-loader
css-minimizer-webpack-plugin
cssnano
eslint-webpack-plugin
glob-import-loader
html-webpack-plugin
mini-css-extract-plugin
postcss
postcss-loader
sass-loader
source-map-loader
style-loader
terser-webpack-plugin
ts-loader
tsconfig-paths-webpack-plugin
webpack
webpack-cli
webpack-dev-server
webpack-merge

With the related config files:

webpack.common.js
webpack.dev.js
webpack.prod.js
.babelrc

Some packages can also be upgraded, like sass and typescript.

Eslint uses the .eslintrc file by default, so we can rename .eslintrc.js to that and make it a json file:

{
"parser": "@typescript-eslint/parser", // Specifies the ESLint parser
"extends": [
"plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin
],
"parserOptions": {
"ecmaVersion": 2022, // Allows for the parsing of modern ECMAScript features
"sourceType": "module" // Allows for the use of imports
},
"rules": {
"curly": 1,
"@typescript-eslint/explicit-function-return-type": [0],
"@typescript-eslint/no-explicit-any": [0],
"ordered-imports": [0],
"object-literal-sort-keys": [0],
"max-len": [1, 120],
"new-parens": 1,
"no-bitwise": 1,
"no-cond-assign": 1,
"no-trailing-spaces": 0,
"eol-last": 1,
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"semi": 1,
"no-var": 0
}
}

Note that I changed the ecmaVersion to 2022 to stay in line with the tsconfig module configuration.

To check if that works, try removing the empty line at the end of the main.ts file when compiling (more on scripts later).

Finally, to make Vite copy the resources as the webpack config does, that is, from the src/main/webpack/resources folder, all we have to do is to set the publicDir property of the configuration. More info about static resources and the public folder here.

We can now update our prod script in package.json:

{
"scripts": {
"prod": "vite build && clientlib --verbose",
"start": "vite dev",
}
}

As exercise, rewrite the dev script to use a dev version of the build, probably you want to differentiate the two builds’ config to include or exclude minification, etc..

Our start command will now open your default browser directly to the index.html page, with the example content from the archetype.

Some imported scripts in the html file won’t be found by Vite as empty or simply not there. we can remove them from index.html file. The proxy configuration allows the page to fetch files directly from the AEM instance. If we need any resource from there, we might want to update that configuration accordingly.

Since the main.ts file is not imported directly here, we should add it as last element in the <body>: Let’s add <script type="module" src="../site/main.ts"><script>.

Our prod command will build the ui.frontend and push it to the ui.apps project, as it did with webpack.

Profit:

Let’s recap the advantages; we now have:

  • Less dependencies to maintain.
  • Less configuration to maintain (and understand).
  • Startup of the dev server it’s now crazy fast (vite + pnpm takes about 1s to start from cold).
  • HMR is instant.

And about cons:

  • Sass glob imports aren’t supported.

More details on performances:

Tools            Startup Time    HMR Time

Webpack + npm 9.5s 1.1s
Webpack + pnpm 6.2s <0.1s
Vite + npm 1s <1ms
Vite + pnpm 1s <1ms

(Tested on local machine, average of multiple tries, don’t take the numbers too seriously).

These tests are based on the empty project from the archetype real project timings will most-likely scale up in favor of Vite even more.

--

--