OpenMRS frontend apps use a standard webpack (opens in a new tab) configuration defined in esm-core (opens in a new tab). The general purpose of this webpack configuration is to standardise how projects are built and to allow us to ensure that all applications follow standard patterns. While it is not required that you use this default webpack configuration, it is highly recommended that you do so. This page documents some notes on how this webpack configuration bundles modules that are important considerations if you want to create a custom webpack configuration.
The O3 frontend uses a packaging pattern called module federation, which is a pattern that allows runtime code sharing between different webpack applications. This differs from webpack's usual use because external dependencies can be resolved at runtime instead of during the build step. In essence, this allows us to use share code between different apps without needing to rebuild all the full application. You can read more about module federation in the corresponding GitHub org (opens in a new tab) or the Webpack documentation (opens in a new tab). Module federation allows for a number of patterns for sharing code, examples of which can be found in the module-federation-examples repo (opens in a new tab).
O3 uses a module federation pattern called "dynamic remote containers". In essence, this means that not only are dependencies resolved at runtime, but which modules are available is also resolved at runtime. This allows O3 to be a very flexible and modular system: we do not need to know ahead of time what apps and libraries are being used. However, this does have some implications for any module's Webpack configuration so that it can be properly loaded by the O3 app shell. In particular, a module loaded by the O3 app shell should not declare any explicit entry point; all modules aside from the app shell itself are loaded as dynamic remotes. The setup we use is heavily inspired by the dynamic remotes examples (opens in a new tab) provided by the module federation team.
At runtime, we expect each frontend module that loads OpenMRS applications to be built using Webpack's
var type (opens in a new tab).
var type results in the module being exposed as a global property. We expect the name of this variable to be the same as the name of the module
except slightly mangled to produce a valid JS variable name. To mangle the module name, we use the following:
name.replace(/[\/\-@]/g, "_"), which replaces
- characters with underscores so
@openmrs/esm-patient-chart-app is exposed via the variable
_openmrs_esm_patient_chart_app. This is necessary
because to dynamically load code from the module, we need to be able to access the module.
Each federated module that provides either extensions or pages that are intended to be loaded by the app shell needs to expose a module called
contains the exports for:
setupOpenMRS(), etc. In our Webpack configuration, this is done by exposing the
index.ts file as the
"./start" module. If you are dynamically loading code yourself using the
importDynamic function provided by
esm-framework, you can point it to a different
exposed module. However, extensions and pages exposed via other modules will need to be manually registered before they are available.
One of the advantages of the module federation technique is that it allows us to provide shared dependencies that are only loaded once in the entire application.
In OpenMRS's Webpack configuration, each of the declarations in
peerDependencies in the
package.json for an app is treated as a shared federated library.
To simplify things, all shared dependencies are treated as
singletons, which means that we are intentionally opting out of supporting multiple versions of shared
libraries. We do this for two reasons:
- Core dependencies of most frontend modules like React, React DOM, and React Router must be loaded as singletons, because they use unversioned global state to provide their functionality (e.g., React's
- This allows us to ensure that we are using the same versions of, e.g., Carbon in all components. This does have the less desireable effect that upgrading core dependencies has to be done all at once and we may re-evaluate this decision at a latter point.
All shared dependencies are loaded into the
default scope and their share name and exposed import is the same as the dependency, i.e., React is shared as
react and imported as
react in our code. This basically ensures that things work as we expect them to.
Our Webpack configuration is designed to work for the majority of use-cases without any changes. However, not every module will have the same needs and so we provide the ability to override the Webpack configuration in a module by overriding the appropriate variables. The override variables can be found inside our webpack config (opens in a new tab).
The most common limitation that our default Webpack configuration provides is that we assume that all libraries loaded from
node_modules are already in
node_modules folder. The
idea here is that, by not passing the code in
node_modules through a loader, the overall build for all apps will be faster. Moreover, since general use
some situations where we do need to process the libraries in
node_modules. The most obvious one in the general ecosystem is
esm-patient-common-lib, a shared
library for apps that are part of the patient chart.
esm-patient-common-lib does not have a build step and so the published version is just the raw Typescript
source. Since Webpack cannot load Typescript without a loader of some sort, builds will fail is we do not modify the Webpack configuration of the apps consuming
that library to also process those Typescript files. Note that this does not apply to the packages in the openmrs-esm-patient-chart monorepo because they load
esm-patient-common-lib via the Yarn workspace rather than via
node_modules so inside the patient chart monorepo,
esm-patient-common-lib is processed
If your module needs to load
node_modules at build time, we recommend that your module's
webpack.config.js looks like this:
const config = require("openmrs/default-webpack-config"); config.scriptRuleConfig.exclude = /node_modules(?![\/\\]@openmrs)/; // this is a CommonJS module module.exports = config;
That changes the
scriptRule in the default Webpack configuration, telling it to processing anything in the
node_modules folder that is from a package from
openmrs NPM organisation.
The form entry frontend module is an Angular application that encapsulates the capabilities of the Angular form engine). It leverages a custom webpack configuration that supports module federation (opens in a new tab) using dynamic remote containers. These capabilities are enabled by the @angular-architects/module-federation (opens in a new tab) library. This library modifies the Angular workspace configuration (opens in a new tab), adding a an
extraWebpackConfig property to the
serve builder targets. This property points to a custom webpack configuration (opens in a new tab) that among other things:
- Sets up the
ModuleFederationPluginto expose the form entry app as a remote module that can be loaded dynamically by the app shell.
- Specifies shared dependencies based on the
peerDependenciesof the form entry app.
src/index.tsas the entry point for the remote module.