Skip to Content
DocsFrontend modulesLoading modules

Loading frontend modules into the app shell

Frontend modules in O3 are loaded dynamically into the app shell using Module Federation . Most current React modules are built with Rspack, while a few legacy or specialized modules still use Webpack. Both produce Module Federation containers that the app shell can load at runtime.

The app shell uses two distribution artifacts generated during distro assembly:

  • The routes registry (routes.registry.json) tells the app shell which pages, extensions, modals, workspaces, feature flags, and runtime conditions each module declares.
  • The import map (importmap.json) maps each module name to the JavaScript bundle URL that should be fetched when that module’s code is needed.

The import map is exposed to the page through a type="systemjs-importmap" script tag, but the current O3 runtime reads that tag itself through the dynamic-loading framework package. SystemJS is not the piece resolving frontend module URLs at runtime.

The routes registry is exposed through type="openmrs-routes" script tags. Those tags can point at a routes.registry.json URL or contain inline route data, and the runtime merges them before registering apps.

In short: the routes registry registers UI contributions, the import map provides bundle URLs, and Module Federation loads the code. This split lets O3 register the UI up front, load module code on demand, and keep the initial app-shell bundle smaller.

This guide focuses on that runtime loading contract. For authoring the module entry point and routes.json, see Overview. For local overrides, see Development. For bundler configuration, see Using Rspack.

Example flow

Here’s a minimal end-to-end example of how a module gets loaded:

  1. spa-assemble-config.json includes @openmrs/esm-patient-chart-app.

  2. The distro build produces an import map entry like:

    { "imports": { "@openmrs/esm-patient-chart-app": "https://example.org/openmrs/spa/esm-patient-chart-app.js" } }
  3. The distro build also produces a routes.registry.json entry from that module’s routes.json.

  4. On startup, the app shell reads the route data from type="openmrs-routes" script tags and registers the module’s pages, extensions, modals, workspaces, and feature flags.

  5. When a route, extension slot, modal, or workspace needs one of the module’s components, the app shell looks up the module URL in the import map.

  6. The dynamic loader appends a <script> element for that URL.

  7. The loaded script exposes a Module Federation container, and the app shell calls init/get to load the ./start exposed module.

  8. The ./start module provides the named lifecycle export from routes.json; if the module defines startupApp(), O3 runs it once before loading that lifecycle.

Module federation

As mentioned before, our module loading system is based on Module Federation. O3 uses a dynamic remote containers pattern. The application entry point is the app shell, and the list of remote bundle URLs is supplied via the import map.

Each module (“remote”) is provided as a name and URL. For each URL, the app shell appends a <script> element to the DOM. Because those scripts use the var library type, they create a global variable that exposes the container interface (init and get). The app shell calls those methods to load the module’s ./start export, which is normally backed by the module’s src/index.ts.

Last updated on