Creating a Distribution
One of the reasons for choosing this kind of modularization in the frontend space is to allow maximum flexibility for creating your own distribution. That way, you can use what you find useful and drop what you don't like to see in your distribution.
This concept is transported to almost all areas including, but not limited to, the available frontend modules, the delivered app shell, and the method of serving the application.
This guide walks you through creating an O3 distribution from start to finish, including building, testing, and deploying it.
Prerequisites
Before you begin, ensure you have:
- Node.js version 16.17 (LTS) or later installed. We recommend using nvm (opens in a new tab) or fnm (opens in a new tab) to manage Node.js versions.
- npm (comes with Node.js) - You'll use this to run the
openmrsCLI tool vianpx - An OpenMRS backend - You'll need a running OpenMRS backend to connect your distribution to (for testing and production)
Installation
The openmrs CLI tool can be run without installation using npx (which comes with npm):
npx openmrs --helpAlternatively, you can install it globally:
npm install -g openmrsThen you can run commands directly without npx:
openmrs --helpFor CI/CD environments, it's recommended to use npx --legacy-peer-deps openmrs@${APP_SHELL_VERSION:-next} to ensure consistent versions.
Quick Start: Complete Workflow
Here's a complete step-by-step workflow to create your first distribution:
Step 1: Create your project directory
Create a new directory for your distribution:
mkdir my-o3-distribution
cd my-o3-distributionStep 2: Create the assemble configuration
Create a spa-assemble-config.json file that defines which frontend modules to include:
{
"publicUrl": ".",
"frontendModules": {
"@openmrs/esm-patient-chart-app": "latest",
"@openmrs/esm-patient-registration-app": "latest",
"@openmrs/esm-home-app": "latest"
},
"frontendModuleExcludes": []
}Step 3: Create the build configuration
Create a spa-build-config.json file that configures runtime properties:
{
"spaPath": "/openmrs/spa/",
"apiUrl": "/openmrs/",
"configUrls": [],
"defaultLocale": "en",
"importmap": "./spa/importmap.json",
"routes": "./spa/routes.registry.json",
"supportOffline": false
}Step 4: Assemble frontend modules
Run the assemble command to gather all frontend module assets:
npx openmrs assemble --manifest --mode config --config spa-assemble-config.json --target ./spaThis creates the importmap.json and routes.registry.json files in the ./spa directory.
Step 5: Build the app shell
Run the build command to create the distributable app shell:
npx openmrs build --build-config spa-build-config.json --target ./spaVerify the build succeeded by checking that ./spa/index.html exists.
Step 6: Test locally
Test your distribution using a simple HTTP server:
cd ./spa
python3 -m http.server 8080Then open http://localhost:8080/openmrs/spa/ in your browser (adjust the path based on your spaPath configuration).
Note: For the SPA to fully function, you'll need to:
- Serve it behind a proxy that forwards API requests to your OpenMRS backend
- Or configure CORS on your backend to allow requests from
http://localhost:8080
Step 7: Deploy
Deploy the ./spa directory contents to your web server. See the Serving Your Distribution section below for detailed deployment options.
Local Build vs CI Setup
You may be tempted to clone the openmrs-esm-core repository for building your distribution. Don't do this unless you know exactly why you want to work against the repository. The repository is only there for development of the OpenMRS Frontend. It is not there for building distributions.
To build your own distribution a simple Node.js tool called openmrs was created. This allows:
- creating an import map with all resources for the contained frontend modules (
openmrs assemble) - build a new app shell to host frontend modules (
openmrs build) - start a debugging session of the shell and a frontend module (
openmrs debug) - start a debugging session of a frontend module in the shell (
openmrs develop) - start the default app shell locally (
openmrs start)
For creating a distribution you need to complete two steps in order:
- Assemble the import map (
openmrs assemble) - This gathers all frontend module assets and creates the import map and routes registry. - Build the app shell (
openmrs build) - This builds the app shell that will host your frontend modules, using the import map and routes from step 1.
The import map is used to define what frontend modules are included and where these frontend modules are located. The build step requires the output from the assemble step.
Here's a complete example workflow:
# Step 1: Assemble frontend modules and create import map
openmrs assemble --manifest --mode config --config spa-assemble-config.json --target ./spa
# Step 2: Build the app shell
openmrs build --build-config spa-build-config.json --target ./spaNote: In CI/CD environments (like Docker), you may want to use npx --legacy-peer-deps openmrs@${APP_SHELL_VERSION:-next} instead of just openmrs to ensure consistent versions. The --manifest flag outputs version information, and --mode config explicitly uses config file mode (though it's the default when --config is provided).
Important: The assemble step must run before build, and both commands should use the same --target directory. The build step reads the importmap.json and routes.registry.json files created by the assemble step.
Customizing the Import Map
By building the app shell you'll already get a rudimentary version of an import map, which can be used for development purposes. Generally, however, you should provide your own.
An import map can also be specified as an URL. For instance, for the development instance at dev3.openmrs.org we have https://dev3.openmrs.org/openmrs/spa/import-map.json (opens in a new tab). The contents of this import map are updated once an update to any (official) frontend module has been pushed. Thus, while this import map may be great for development purposes, it should be considered unstable. Avoid this for your distribution or any application that should not break unexpectedly.
A custom import map can be created using the openmrs assemble command. If run directly the command will open a command line survey, guiding you through the different options. It will list all OpenMRS frontend modules that can be found on the NPM registry.
For CI/CD purposes we encourage you to use a configuration file spa-assemble-config.json instead. This file defines the wanted frontend modules and configures the whole process. Note that spa-assemble-config.json is different from spa-build-config.json, which is used for runtime configuration properties (see the Configuration overview guide for details).
To use the configuration file, run:
openmrs assemble --config spa-assemble-config.jsonYou can also specify additional options:
--target <directory>: The target directory where the gathered artifacts will be stored (default:dist)--fresh: Clean the output directory before the run--manifest: Output a manifest file with version information--mode config: Use config file mode (default when--configis provided)
The file may look as follows:
{
"publicUrl": ".",
"frontendModules": {
"@openmrs/esm-patient-chart-app": "latest",
"@openmrs/esm-patient-registration-app": "3.0.0"
},
"frontendModuleExcludes": []
}The frontendModuleExcludes array allows you to exclude specific modules that might be included as dependencies of other modules. This is useful when you want to remove a module that would otherwise be included automatically.
Note: Some distributions may use excludedFrontendModules instead of frontendModuleExcludes. Both are supported, but frontendModuleExcludes is the canonical property name used by the openmrs CLI tool.
The publicUrl may be important for later. If the gathered resources are placed (and served) in the same folder as the SPA resources then . is good. If they are uploaded to say a CDN, then the (base) URL of the CDN should be defined.
Example:
{
"publicUrl": "https://openmrs-cdn-example.com/mf",
"frontendModules": {
"@openmrs/esm-patient-chart-app": "latest",
"@openmrs/esm-patient-registration-app": "3.0.0"
}
}In this case the resulting import-map.json could look as follows:
{
"imports": {
"@openmrs/esm-patient-chart-app": "https://openmrs-cdn-example.com/mf/openmrs-esm-patient-chart-app-3.2.1/openmrs-esm-patient-chart-app.js",
"@openmrs/esm-patient-registration-app": "https://openmrs-cdn-example.com/mf/openmrs-esm-patient-registration-app-3.0.0/openmrs-esm-patient-registration-app.js"
}
}Either way the assemble command makes sure to have all assets made available properly.
Building the App Shell
After assembling your frontend modules, you need to build the app shell. The build process requires a spa-build-config.json file that configures runtime properties for your distribution.
The spa-build-config.json file should include:
{
"spaPath": "/openmrs/spa/",
"apiUrl": "/openmrs/",
"configUrls": ["/openmrs/spa/config.json"],
"defaultLocale": "en",
"importmap": "./spa/importmap.json",
"routes": "./spa/routes.registry.json",
"supportOffline": false
}Key properties:
spaPath: The path where the SPA will be served (e.g.,/openmrs/spa/)apiUrl: The URL of the OpenMRS backend APIconfigUrls: Array of URLs to frontend configuration JSON filesdefaultLocale: Default language code (e.g.,en,fr)importmap: Path to the import map file created byassemble(relative to build target)routes: Path to the routes registry file created byassemble(relative to build target)supportOffline: Whether to enable offline support via service worker
You can also use environment variables in the config file (they will be interpolated at build time):
{
"spaPath": "$SPA_PATH",
"apiUrl": "$API_URL",
"configUrls": ["$SPA_CONFIG_URLS"],
"defaultLocale": "$SPA_DEFAULT_LOCALE",
"importmap": "$SPA_PATH/importmap.json",
"routes": "$SPA_PATH/routes.registry.json",
"supportOffline": false
}To build the app shell, run:
openmrs build --build-config spa-build-config.json --target ./spaAdditional build options:
--target <directory>: The target directory (should match the assemble target)--fresh: Clean the output directory before building--support-offline: Enable offline support (can override config file)
After a successful build, your ./spa directory will contain:
index.html- The main HTML file for the SPAimportmap.json- The import map created by the assemble steproutes.registry.json- The routes registry created by the assemble step- JavaScript bundles and other assets needed to run the application
Build verification: After running the build command, check that ./spa/index.html exists. If it doesn't, the build failed. Check the build logs for errors.
Important: If you're using configUrls in your spa-build-config.json, you'll also need to ensure those configuration JSON files are available and served alongside your built files. For example, if your config references /openmrs/spa/config.json, that file must be accessible at that URL when the SPA loads.
Serving Your Distribution
After building your distribution, you need to serve the built files using a web server. The built files are static assets that can be served by any web server (nginx, Apache, etc.).
Basic Setup
The simplest way to serve your distribution is using a static file server. The built files should be served at the path specified in your spaPath configuration (e.g., /openmrs/spa/).
Important considerations:
-
Config files: If you're using
configUrlsin yourspa-build-config.json, make sure those configuration JSON files are also accessible via HTTP/HTTPS at the specified URLs. The SPA will fetch them at runtime. -
Backend connection: Ensure your web server can proxy API requests to your OpenMRS backend, or configure CORS on your backend to allow requests from your frontend domain.
-
Path configuration: The
spaPathin your build config must match the path where you serve the files. For example, ifspaPathis/openmrs/spa/, your web server should serve the files at that path.
Example: Using nginx
Here's a basic nginx configuration to serve your distribution:
server {
listen 80;
server_name localhost;
# Serve the SPA at /openmrs/spa/
location /openmrs/spa/ {
alias /usr/share/nginx/html/;
try_files $uri $uri/ /openmrs/spa/index.html;
}
# Proxy API requests to OpenMRS backend
location /openmrs/ {
proxy_pass http://backend:8080/openmrs/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}Testing Locally
To test your distribution locally, you can:
-
Use a simple HTTP server:
cd ./spa python3 -m http.server 8080Then access it at
http://localhost:8080/openmrs/spa/(adjust the path based on yourspaPathconfiguration). -
Use the
openmrs startcommand for development (though this uses a default import map, not your custom one). -
Use Docker with nginx, similar to the reference distribution setup.
Deployment
For production deployments:
- Docker: Copy the built
./spadirectory into an nginx container (see the reference distribution's Dockerfile for an example) - Static hosting: Upload the
./spadirectory contents to your static hosting service (CDN, S3, etc.) - Traditional web server: Copy the files to your web server's document root
Remember to also deploy any configuration JSON files referenced in your configUrls and ensure they're accessible at the specified URLs.
Canary vs Stable
Regarding the versioning you'll have three options:
- Go for the
latesttag - Go for the
nexttag - Go for a specific (i.e., explicit) version
In general we recommend to stay on non-preview (e.g., 3.2.1) versions. Preview versions (e.g., 3.2.1-pre.0) are for development purposes and may not be stable.
For creating a working distribution ideally you'll stick to explicit versioning of non-preview versions. If you use latest then individual frontend modules may work as expected, but incompatibilities (e.g., if a certain frontend module was updated but is now incompatible to another frontend module that you also use) may then exist - making additional testing required. With an explicit version you can be sure that a working system remains as such in rebuild scenarios.