Module javascript
Every application module can contribute JavaScript to a theme build without touching the theme
itself. The build system (WebFacade) collects all module contributions automatically: entry
points, import aliases, static assets, and JS overrides.
Module entry point (module.js)
If a file named module.js exists at Resources/{area}/web/module.js or
Resources/global/web/module.js, it is automatically discovered and called during the theme
bootstrap, before the theme's own bootstrap.js runs.
The file must export a default async function:
const run = async () => {
// one-time setup: register Alpine stores, tag DI services, bind DOM events
};
export default run;
All registered module.js files are called in parallel via await Promise.all, so module
order should not be assumed. The generated bootstrap sequence is:
1. All module.js files (await each in registration order)
2. Parent theme bootstrap.js (if any, innermost first)
3. Active theme bootstrap.js
4. alpineInitialize() ← start Alpine.js
5. vueInitialize() ← mount Vue app
Import aliases
Every module automatically gets a @ModuleName/* import alias that resolves to both its
area-specific and global web/ directories. The alias name is derived by camelising the
module name and prepending @:
| Module name | Alias |
|---|---|
Amarant_Ui |
@AmarantUi |
Amarant_Framework |
@AmarantFramework |
Vendor_ModuleName |
@VendorModuleName |
The alias covers both Resources/{area}/web/ and Resources/global/web/:
// resolves to code/Amarant/Ui/Resources/global/web/js/form/form.js
import {handleForm} from '@AmarantUi/js/form/form';
// resolves to code/Vendor/ModuleName/Resources/backend/web/my-component.js
import MyComponent from '@VendorModuleName/my-component';
Aliases are written to jsconfig.json and tsconfig.json in the project root on every build,
so IDEs pick them up automatically.
Module-level static files
Modules can ship static files that are copied to the public output on build. Place files under:
| Directory | Behaviour |
|---|---|
Resources/{area}/web/static/ |
Copied and content-hashed. Referenced from Twig via module_static_asset(). |
Resources/global/web/static/ |
Same, for global-area assets. |
Resources/{area}/web/static-unhashed/ |
Copied as-is, no hash in filename. |
Files land at /static/assets/modules/{ModuleName}/{relative-path}. A static manifest JSON
is written alongside the theme build output so Twig helpers can resolve the hashed filenames.
Note
Static files are only copied during a production build (--build-mode=build), not in
development watch mode.
JS module overrides (plugins.json)
Any module can wrap or replace a JS export from any other module without forking it. The
mechanism is the vite-plugin-module-mixins Vite plugin, driven by a plugins.json file per
module/area.
How it works
- A module declares its overrides in
Resources/{area}/web/plugins.json(orglobal/web/). WebFacadecollects allplugins.jsonfiles across modules and merges them intovite.plugins.{area}.{theme}.jsonat build time.- The
vite-plugin-module-mixinsVite plugin intercepts any import whose path matches a registered target and replaces it with a virtual module that wraps the original with each declared wrapper function, in priority order.
plugins.json format
{
"@AmarantUi/js/form/form": [
{
"path": "js/override/form.override.js",
"priority": 10
}
]
}
Each key is an import path (the module being overridden). Each entry has:
| Field | Type | Description |
|---|---|---|
path |
string |
Path to the wrapper file, relative to the module's web/ directory. |
priority |
int (optional) |
Lower numbers run first (innermost wrapper). Default 0. |
Writing a wrapper
A wrapper receives the full exports object of the original (or the previous wrapper in the
chain) and returns a new exports object. Both default and named exports are forwarded.
export default function (original) {
const originalHandleForm = original.handleForm;
return {
...original,
handleForm(submitSelector, messageEl) {
// run your code before or after the original
console.log('Form handler wrapped');
return originalHandleForm(submitSelector, messageEl);
}
};
}
Wrappers are chained from lowest priority to highest (last wrapper is the outermost call). When multiple modules provide wrappers for the same target, module order in the registrar determines the outer/inner relationship — later-registered modules wrap further out.
HMR is fully supported: changing a wrapper file or the manifest JSON invalidates only the affected virtual modules.
Generated files
WebFacade writes several files to the project root on every view:theme:build invocation.
These are build artefacts and should be committed to .gitignore:
| File | Description |
|---|---|
vite.config.{area}.{theme}.mts |
The Vite configuration for the given area + theme. Passed to npm exec vite. |
vite.plugins.{area}.{theme}.json |
Merged plugins.json manifest from all modules. Read by vite-plugin-module-mixins. |
jsconfig.json / {area}.jsconfig.json |
Module alias paths for IDE JS support. |
tsconfig.json / {area}.tsconfig.json |
Same, for TypeScript. Inherits from tsconfig.base.json or {area}.tsconfig.base.json if present. |
generated/design/{area}/{theme}/bootstrap.js |
Auto-generated theme bootstrap that imports all modules, parent themes, and the active theme. |
Bundle splitting
WebFacade automatically splits heavy third-party libraries into separate Rollup chunks to
avoid bundling them into the main theme file. The default chunk groups are:
| Chunk name | Matches packages starting with |
|---|---|
ckeditor |
ckeditor5, @ckeditor/ckeditor5- |
ace |
ace- |
boxicons |
boxicons |
sweetalert |
sweetalert |
vue |
vue |
prime |
prime |
These become separate JS files that the browser can cache independently of the main bundle.