Using Vite in Adobe Experience Manager
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:
- Local AEM instance running.
- Some basic AEM frontend knowledge.
- NodeJS
Plan:
- Create new empty project from archetype
- Understand what deliverables are to be produced
- Understand the source code
- Vite configuration
- Profit!
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 theMiniCssExtractPlugin
from the bundle (imported inmain.ts
). - The target folder is
resolve(__dirname, 'dist')
. - All the files under
/resources
are copied into the dist folder underclientlib-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 inassets/
). - 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.