Skip to content

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:

code/Vendor/ModuleName/Resources/backend/web/module.js
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

  1. A module declares its overrides in Resources/{area}/web/plugins.json (or global/web/).
  2. WebFacade collects all plugins.json files across modules and merges them into vite.plugins.{area}.{theme}.json at build time.
  3. The vite-plugin-module-mixins Vite 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

code/Vendor/ModuleName/Resources/backend/web/plugins.json
{
  "@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.

code/Vendor/ModuleName/Resources/backend/web/js/override/form.override.js
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.