DEV Community

Natalia Venditto
Natalia Venditto

Posted on

4 3

Integrating a modern frontend in a multi tenant AEM project (part 2)

It's time we release the second part of this series. And get to the actual frontend setup!

The frontend build module

if you haven't yet, I recommend you start by reviewing the proposed structure, by reading the first part. It is important for everything else to make sense.

As mentioned, the frontend build will be in a completely separated Maven module. Why? Well, to begin with, because then our backend counterparts can skip that part of the compilation (by, for example, implementing a Maven profile). Frontend build times are exponentially higher now, and sometimes not necessary for some backend features.

Another reason, separation of concerns. It's better to encapsulate code that does something completely specialized. Same as we do with functions, etc, right?

So we will extend our current structure by adding a new module called frontend, like this.

That makes the frontend one, a folder. But when it comes to Maven, in order for it to be a module, you need to add a pom.xml file to it. We will get to this pom file later (or rather, the one that's inside of the package 2 folders below), but now, let's focus purely in frontend matters.

The frontend module as an npm package

For maven, this will be a module. But for npm, this will be a package. So let's go ahead and make it one with npm init. As usual, we answer a few questions and get our package.json. Here I am assuming you have node and npm installed either globally or locally. Now we could potentially, start installing dependencies.

What dependencies?

Well, that will very much depend on the stack you choose, and the browsers you have to give support to. But since we decided in the part 1 of this series that we were going to write our javascript as ES6 and our css as .scss, we will need loaders and plugins to load, transpile/compile those languages. And if we mention loaders and plugins, is because we're using webpack, so we will need that, too. We also said we won't use a task manager such as Gulp or Grunt, but will use the npm CLI to run script directly from our package.json

So at a minimum, these are some of the node modules you will need as dev dependencies.

  • webpack
  • webpack-cli
  • npm-run-all

to run the whole show. You will need, at minimum

to transpile your ES. And

  • node-sass

to work your scss code.

Linters

Also, because you care for code quality, you will want to have due linting configuration and tools, such as

  • eslint
  • eslint-loader
  • eslint-plugin-import
  • eslint-config-airbnb-base (this is an industry standard, and the one I follow, with some tweaks, here and there)

Additionally, of course, you would want to keep an eye on your styles with

  • stylelint

You will need

  • postcss
  • postcss-loader and
  • browserslist

to run

  • autoprefixer

and ensure cross browser support.

And because you will have to be traversing your structure to gather clientlib entries here and there, you will also need

  • fast-glob (or glob, but fast-glob is...well, faster and lighter)

Once you have all that, run and npm install.

NOTE: Whether you make each one a dependency or devDependency, will very much depend on if you need them at build time, or run time. Make conscious decisions!

Now you need to focus on the rest of items inside of this package. So my recommendation is that you create a series of folders to store

  • configurations
  • tasks
  • other scripts
  • utilities

So that that folder contents looks something like this

Configuration

It's time to hit the configuration scene. The configurable aspect of webpack is by far its best feature when it comes to large enterprise platforms, like the one we're discussing, it's essencial.

But since you have so many tools, and you probably want to keep concerns sparated so they're more maintainable, I encourage you to create a configs folder inside your package, and import them (or relevant members) to the webpack config, in aggregation.

That way you will have several configurations in that folder, like this:

  • babel.config.js
  • clientlibs.config.js
  • poject.paths.config.js
  • project.alias.config.js
  • webpack.css.config.js
  • webpack.js.config.js
  • wepack.config.js

One of the most important configuration files, is where you will store all the paths to accomplish your tasks (we call it poject.paths.config.js here. Since you have to go collect .scss and js entry files from different components and modules, you want to establish certain patterns, and then traverse your structure to fetch them.
For you to have an idea, something like this

const path = require('path');
// this comes from maven env
const TENANT = process.env.NPM_FRONTEND_MODULE_BASE_DIR;
const TENANT_NAME = TENANT.split('/').reverse()[2];
const TENANT_COMPONENTS = TENANT.substr(0, TENANT.lastIndexOf('/'));
const PROJECT_ID = 'project-id';
console.log(`############## FRONTEND BUILD ##############`);
console.log(`############## STARTING BUILD FOR TENANT: ${TENANT_NAME} ##############`);
console.log(`############## ******* ##############`);
const CONSTANTS = {
SUFFIX_SOURCE: '.entry',
SUFFIX_TARGET: '.bundle',
SUFFIX_AUTHOR: 'author', // eg: author.bundle.js, dialog.author.bundle.js
// Project root (relative to config folder)
BASE_DIR: '../../',
// Partial from tenant package (subproject id)
TENANTS_PACKAGE: `${TENANT}`,
// Path to frontend working directory (or where the package.json is at) (relative to BASE_DIR)
FRONTEND_PACKAGE: '',
// Path to tenant root
PROJECT_COMMONS: `apps/${PROJECT_ID}`,
// Path to shared and abstracts include
SHARED_INCLUDE: 'commons/shared',
// Path to vendor (js) code alias
VENDORS_INCLUDE: 'commons/vendor',
// Path to assets (such as svg icons)
ASSETS_INCLUDE: 'theme/assets',
// Path to dialogs styles include
DIALOG_CSS_INCLUDE: 'dialog',
// Path to tenants package
TENANTS_COMPONENTS_INCLUDE: `${TENANTS_COMPONENTS}`,
// Path to components package
COMPONENTS_INCLUDE: 'components',
};

Once the example repository is ready, you will be able to see exactly what this file looks like but it's basically a series of constants receiving the paths values to be able to retrieve all the commons and components .scss and .js files, in order to process them. Here is a gist of it

const {
GLOBAL_VENDOR_JS = '',
GLOBAL_COMPONENTS = '',
TENANT_COMPONENTS = '',
} = require('./project.paths.config');
// export alias for JS
module.exports = {
'global.vendors': GLOBAL_VENDOR_JS,
'global.components': GLOBAL_COMPONENTS,
'tenant.components': TENANT_COMPONENTS,
};

Some of those constants will receive values from the environment (hence they are called environment variables), like NODE_ENV, or whatever other information you need to process at build time.

We will learn more about this in a bit.

Another important piece of information are the aliases. Especially for webpack. You won't want to be counting dots back and forth to import your modules or its members, your utilities and common code. This is why you will want to create project aliases, that you can both use to import JS as much as to include paths for .scss

(here I will have to assume, that you are used to working with webpack, and understand what I am talking about already. If you don't, follow this link )

Utilities

As usual, utilities may be additional helper functions and snippets that are necessary to run your tasks, but are generic enough that can be called from different sources. It is a good practice to keep them in their own files, and under a utility folder.

Task time!

Now you have done some configuration and got stocked with utilities, you have to do something with all that! Common tasks you may want to perform include linting your code, compiling your .scss, implementing plugins. All those tasks will live in your tasks folder, and you will be able to execute those tasks directly from your package.json file, when executing npm run build . Of course you may just use webpack mechanics, default or custom to initiate all those processes, but at large scale, it's not unlikely you need to perform additional operations.

And what about the other scripts?

As you've seen me mention before, I really like keeping things separated, so whatever tasks may not be strictly necessary on each run, or are very specific, like generating svg sprites, or processing other assets in any way, etc, you may want to put in this folder.

Collecting the entries from a tenants components

If you're familiar with how webpack works, you know that it basically takes an entry, processes it, and then produces an output. If you're still not familiar, please read about it here

In the case of a multi tenant project, you will have a collection of entry points to process, and you want to make sure to collect them all, according to a certain pattern (since it's basically impossible to hardcode a path in this case!). So how do you do that? Basically you use glob to traverse your tree and get those files that match, according to a pattern or naming convention, and their extension. Something like this:

const glob = require('fast-glob');
const path = require('path');
function collectEntries(config, extension) {
// we have called all our js and scss files like this `component-name.entry.js` and `component-name.entry.scss`
const pattern = `**/*.entry.${extension}`;
// if you take a look at project structure we defined
// and the project.paths.conig.js file above, you will see the path to iterate in this case
// is the `tenants/tenantN/components/package/src/frontend/components`
// since when globbing cwd will be the directory where this file is at
// you need to pass this path as options
const options = { cwd: `tenants/tenantN/components/package/src/frontend/components` };
const tree = glob.sync(pattern, options);
let sources = {};
tree.forEach((file) => {
// operate here
});
return sources;
};

Executing your tasks from your package.json

Again, if you're familiar with npm, you know that every package.json has a scripts property, where you can declare execution commands.

One module that is very helpful, and I recommend you install, is npm-run-all. As explained here the npm run-script command can only run one script at a time, so you're much better when you're able to simplify this.

Like this:

// ...
"paths": {
"conf": "./config",
"tasks": "./tasks"
},
"scripts": {
"build": "npm-run-all --parallel build:*",
// you cannot write comments in package.json (or any json, so don't do this! ;)
// but to explain this, $npm_package_paths_configs refers to what you have as value in the
// paths property up there
"build:general": "webpack-cli --config \"$npm_package_paths_conf/webpack.config.js\"",
"build:js": "webpack-cli --config \"$npm_package_paths_conf/webpack.js.config.js\"",
"build:css": "webpack-cli --config \"$npm_package_paths_conf/webpack.css.onfig.js\"",
// ...
"clean": "node $NODE_DEBUG_OPTION \"$npm_package_paths_tasks/clean\"",
...
}
view raw package.json hosted with ❤ by GitHub
const jsConfig = require('./webpack.js.config');
const cssConfig = require('./webpack.css.config');
const merge = require('webpack-merge');
const generalConfig = merge([
jsConfig,
cssConfig
]);
module.exports = generalConfig;

But we were talking about a frontend build in AEM!

Yes! And how do you make sure you get all the environment variables and have the frontend build running, when you (or Jenkins!) execute a maven build? Learn in the next part of this series!

Billboard image

Deploy and scale your apps on AWS and GCP with a world class developer experience

Coherence makes it easy to set up and maintain cloud infrastructure. Harness the extensibility, compliance and cost efficiency of the cloud.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay