Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ symbols.svg
.tmp/
.cache-loader
dist/
dist-feature-segments/
dist-multi-sourcekey/
test/dist-splitchunks/
test/dist-splitchunks-collision/

# Svg Icons Temp folder
tmp/
Expand Down
6 changes: 6 additions & 0 deletions config/clientlibs.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,14 @@ const skipCategories = ['myproject.author'];
// Object to be able to generate clientlibs for new CSS files (created in build process) in target folder
const extraEntries = {};

// When true, generates .content.xml and js.txt metadata for webpack split chunks
// (runtimeChunk + splitChunks cacheGroups). Chunks must each have a unique output
// subfolder; chunks sharing a folder are skipped with a warning.
const generateSplitChunksClientlibs = false;

module.exports = {
override,
skipCategories,
extraEntries,
generateSplitChunksClientlibs,
};
22 changes: 22 additions & 0 deletions config/general.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,30 @@ const nodeModules = path.join(rootPath, 'node_modules');

*/
// what is the source files key suffix to compile
// can be a string (e.g. 'source') or an array (e.g. ['publishlibs', 'authorlibs', 'dialoglibs', 'editorlibs'] etc)
const sourceKey = 'source';

// what is the compiled bundle key
const bundleKey = 'bundle';

// When true, the sourceKey segment becomes an additional folder level in the
// dist entry key and is dropped from the dist filename.
// e.g. component.main.publishlibs.js -> dist/.../main/publishlibs/component.main.bundle.js
const sourceKeyAsDistFolder = false;

// When true, dot-separated segments between the component prefix and sourceKey
// in the source filename are expanded into folder levels in the dist entry key.
// e.g. component.main.publishlibs.js -> dist/.../main/component.main.bundle.js
const fileNameDotSuffixesAsDistFolder = false;

// Values excluded from folder promotion when sourceKeyAsDistFolder or
// fileNameDotSuffixesAsDistFolder are true. Each entry may be:
// - a string: exact case-sensitive match (e.g. 'clientlibs')
// - a RegExp: pattern match (e.g. /^v\d+$/ to exclude v1, v2, v3 …)
// Applies to both sourceKey values and feature segment values.
// Empty array means no exclusions (default).
const excludeFileNameDotSuffixes = [];

// source file types ['js', 'scss']
const sourceTypes = ['js', 'scss'];

Expand Down Expand Up @@ -97,6 +116,9 @@ module.exports = {
projectKey,
sourceKey,
bundleKey,
fileNameDotSuffixesAsDistFolder,
excludeFileNameDotSuffixes,
sourceKeyAsDistFolder,
sourceTypes,
rootPath,
sourcesPath,
Expand Down
7 changes: 3 additions & 4 deletions config/optimization.config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
const path = require('path')
const { isProduction, excludedFromVendors, bundleKey } = require('./general.config');
const checkChunk = require('../utils/checkChunk');
const nodeModules = `node_modules`;

// curry to pass the module to check
const test = ( excludes, includes) => (mod) => checkChunk(mod.context, excludes, includes);
// curry to pass the webpack module object to checkChunk
const test = (excludes, includes) => (mod) => checkChunk(mod, excludes, includes);

module.exports = {
minimize: isProduction,
Expand All @@ -16,7 +15,7 @@ module.exports = {
chunks: 'initial',
minChunks: 2,
cacheGroups: {
// Treeshake vendors in node_modules (but keep unique vendors at the clientlibs it belongs)
// Treeshake vendors in node_modules (but keep unique vendors at the clientlibs it belongs)
vendors: {
test: test([nodeModules], excludedFromVendors),
minChunks: 2,
Expand Down
170 changes: 169 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,25 @@ module.exports = {
general: {
// Your project name with which ClientLibs category are prefixed
projectKey: "myproj",
// Only the source files with this suffix will be compiled
// Only the source files with this suffix will be compiled.
// Accepts a string or an array of strings.
sourceKey: "source",
// The compiled bundle filename suffix
bundleKey: "bundle",
// When true, the sourceKey segment becomes an additional dist folder level
// and is dropped from the dist filename.
// e.g. component.main.publishlibs.js -> dist/.../main/publishlibs/component.main.bundle.js
sourceKeyAsDistFolder: false,

// When true, dot-separated segments between the component prefix and sourceKey
// in the source filename are expanded into folder levels in the dist entry key.
// e.g. component.main.publishlibs.js -> dist/.../main/component.main.bundle.js
fileNameDotSuffixesAsDistFolder: false,

// Values excluded from folder promotion when sourceKeyAsDistFolder or
// fileNameDotSuffixesAsDistFolder are true. Empty array means no exclusions.
excludeFileNameDotSuffixes: [],

// The path to the directory with your source files
sourcesPath: "src",
// Path to the dir with the code shared among Scss and JS files
Expand Down Expand Up @@ -105,6 +120,101 @@ module.exports = {
}
```

#### Filename-based clientlib convention

By default, `sourceKey` is a single string (e.g. `"source"`). The build finds every file matching `**/*.source.js` and compiles it to a dist file in the same relative folder. For example:

```
src/components/video/video.source.js -> dist/components/video/video.bundle.js
```

This works well for simple setups, but AEM has a specific requirement: **each clientlib must live in its own dedicated folder** with a `.content.xml` file. A single component often needs multiple clientlibs - one for publish, one for author, and possibly additional ones for optional features loaded conditionally by AEM configuration. Producing that folder structure in dist would otherwise require mirroring it in the source tree, leading to deeply nested folders that are awkward to navigate.

There are three options that make this easier: an array `sourceKey`, `sourceKeyAsDistFolder`, and `fileNameDotSuffixesAsDistFolder`. Used together, they let you keep the source tree flat while the build derives the required dist folder hierarchy from the filename.

##### `sourceKey` as an array + `sourceKeyAsDistFolder`

A component commonly needs separate clientlibs for publish and author mode. Without any flags, you would need separate source folders to keep those files apart. With an array `sourceKey` and `sourceKeyAsDistFolder: true`, you can keep them co-located in the same source folder - the sourceKey suffix in the filename (`publishlibs` vs `authorlibs`) tells the build which dist folder each file belongs to:

```
src/components/video/
video.publishlibs.scss
video.authorlibs.scss
```

```
dist/
components/video/publishlibs/video.bundle.css # clientlib: myproj.video.publishlibs
components/video/authorlibs/video.bundle.css # clientlib: myproj.video.authorlibs
```

##### `fileNameDotSuffixesAsDistFolder`

A component may need feature-specific clientlibs - for example, HD video support or caption styles - that AEM loads conditionally. Each must be a separate clientlib folder in dist. With `fileNameDotSuffixesAsDistFolder: true`, the dot-segments between the component name and the sourceKey suffix become folder levels in dist:

```
src/components/video/
video.main.publishlibs.scss
video.hd.publishlibs.scss
video.captions.publishlibs.scss
```

```
dist/components/video/
main/publishlibs/video.bundle.css # base clientlib
hd/publishlibs/video.hd.bundle.css # loaded when HD feature is active
captions/publishlibs/video.captions.bundle.css
```

No `main`, `hd/` or `captions/` subfolders needed in source - the feature name lives in the filename.

##### Enabling the convention

```javascript
module.exports = {
general: {
sourceKey: ['publishlibs', 'authorlibs'],
fileNameDotSuffixesAsDistFolder: true,
sourceKeyAsDistFolder: true,
}
}
```

| Flag combination | `video.main.publishlibs.js` -> dist key |
|---|---|
| `false` / `false` | `video.main.bundle.js` (default) |
| `true` / `false` | `main/video.main.bundle.js` |
| `false` / `true` | `publishlibs/video.main.bundle.js` |
| `true` / `true` | `main/publishlibs/video.main.bundle.js` |

Both flags default to `false` - existing projects are unaffected unless they opt in.

##### `excludeFileNameDotSuffixes`

When using `sourceKeyAsDistFolder` or `fileNameDotSuffixesAsDistFolder`, every matched value is promoted to a folder by default. `excludeFileNameDotSuffixes` lets you opt specific values out of that promotion while keeping them as valid source keys. Each entry can be a plain string (exact match) or a `RegExp` (pattern match).

**Motivating scenario:** A project migrating from `sourceKey: 'clientlibs'` to `sourceKey: ['clientlibs', 'publishlibs', 'authorlibs']` wants folder promotion only for the new values. The existing `*.clientlibs.*` files should continue producing flat dist entries without renaming:

```javascript
module.exports = {
general: {
sourceKey: ['clientlibs', 'publishlibs', 'authorlibs'],
sourceKeyAsDistFolder: true,
excludeFileNameDotSuffixes: ['clientlibs', /^v\d+$/], // exact string or RegExp - e.g. /^v\d+$/ matches v1, v2, v10 ...
}
}
```

| Source file | Dist entry key |
|---|---|
| `author/author.clientlibs.js` | `author/author.bundle.js` - excluded, stays flat |
| `author/author.authorlibs.js` | `author/authorlibs/author.bundle.js` - promoted |
| `publish/publish.publishlibs.js` | `publish/publishlibs/publish.bundle.js` - promoted |

The same exclusion applies to feature segments when `fileNameDotSuffixesAsDistFolder: true`. Any segment listed in `excludeFileNameDotSuffixes` is not promoted to a folder, but remains in the dist filename.

`excludeFileNameDotSuffixes` defaults to `[]` - no exclusions. It is a no-op when both promotion flags are `false`.

### Babel

Babel webpack plugin, enabled by default in the option `general.modules`.
Expand Down Expand Up @@ -300,3 +410,61 @@ Advantages of having a separated vendor:
**treeshaking.bundle.js**

This is intended to optimize the codebase of the project, by code splitting the modules that are the building blocks of it, like core-js, babel and @your modules.

### Clientlibs

Controls AEM clientlib metadata generation. Defaults:

```javascript
{
clientlibs: {
// When true, overwrites existing .content.xml and txt files
override: false,
// Categories to skip when override is true
skipCategories: ['myproject.author'],
// Extra entries to generate clientlibs for (CSS files produced in the build process)
extraEntries: {},
// When true, also generates .content.xml and js.txt for webpack split chunks
// (runtimeChunk + splitChunks cacheGroups). Each chunk must have its own unique
// output subfolder; chunks sharing a folder are skipped with a warning.
generateSplitChunksClientlibs: false,
}
}
```

#### `generateSplitChunksClientlibs`

Webpack split chunks (`treeshaking.bundle.js`, `vendors.bundle.js`) are emitted to dist but have no AEM clientlib metadata by default. Enabling this flag generates `.content.xml` and `js.txt` for each split chunk so AEM can discover them as clientlibs.

**Requirement:** each chunk must live in its own dedicated subfolder. The default config places both chunks in `commons/` - this causes a collision and both are skipped with a warning. To use this flag, give each chunk a unique subfolder in `optimization`:

```javascript
module.exports = {
clientlibs: {
generateSplitChunksClientlibs: true,
},
optimization: {
runtimeChunk: { name: 'commons/treeshaking/treeshaking.bundle.js' },
splitChunks: {
cacheGroups: {
vendors: { name: 'commons/vendors/vendors.bundle.js' },
treeshaking: { name: 'commons/treeshaking/treeshaking.bundle.js' },
}
}
}
};
```

This produces:

```
dist/commons/treeshaking/
.content.xml # category: myproj.commons.treeshaking
js.txt # treeshaking.bundle.js

dist/commons/vendors/
.content.xml # category: myproj.commons.vendors
js.txt # vendors.bundle.js
```

If two or more chunks share the same output folder, a warning is logged and no metadata is written for that folder. No error is thrown.
50 changes: 10 additions & 40 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"url": "https://github.com/Netcentric/fe-build.git "
},
"scripts": {
"clean": "rm -rf ./test/dist",
"clean": "rm -rf ./test/dist ./test/dist-feature-segments ./test/dist-multi-sourcekey ./test/dist-splitchunks ./test/dist-splitchunks-collision",
"test": "npm run clean && npm run jest",
"jest": "NODE_OPTIONS=\"--experimental-vm-modules --no-warnings\" jest -u --runInBand"
},
Expand Down Expand Up @@ -43,7 +43,7 @@
"@babel/preset-env": "^7.26.7",
"autoprefixer": "^10.4.20",
"babel-loader": "9.1.2",
"chokidar": "^5.0.0",
"chokidar": "^4.0.3",
"core-js": "^3.40.0",
"eslint": "^8.57.1",
"eslint-webpack-plugin": "^4.2.0",
Expand Down
Loading
Loading