Migrating to esm-core v5
This guide is for migrating frontend modules to Core v5. Please check your frontend module's package.json
entries for @openmrs/esm-framework
and openmrs
to see if you need to migrate. If you're running anything higher than @openmrs/esm-framework@5.0.0
and openmrs@5.0.0
, you're already on Core v5. If not, then you need to migrate.
- 1 Introduction
- 2 Case Study: Login frontend module
- 3 Factor out static metadata
- 4 Factor out dynamic metadata
- 4.1 1. Move the moduleName and options variables to the top level
- 4.2 2. Make the importTranslation function a named export
- 4.3 3. Make pages and extensions named exports
- 4.4 4. Create a startupApp function and move the defineConfigSchema call into it
- 4.5 5. Remove extraneous metadata
- 4.6 6. Use fewer dynamic imports for improved performance
- 4.7 Final index.ts file
- 5 Upgrade core dependencies
- 6 Troubleshooting
- 6.1 I've pulled the latest changes but I can't get a local dev server running
- 6.2 I'm getting a SyntaxError: Unexpected token 'export' error when I run tests that's related to Dexie
- 6.3 I'm getting a Module not found: Error: Can’t resolve ‘css-loader’ error
- 6.4 I'm getting a minified single-spa error #10: Invalid mount lifecycle on parcel when I run my frontend module
- 7 More examples
Introduction
O3 provides a powerful module loading system that handles loading of frontend modules in the app shell. This system leverages Webpack module federation and forms the basis of our microfrontends architecture. However, it does suffer from a few historical drawbacks:
All frontend modules get sequentially loaded at app startup time, which has a big performance impact.
Because we're loading all modules at startup, we're also incurring the cost of executing all the dynamic import code for each module, even if the module is not used in the current page.
To address these issues, we've introduced a new module loading mechanism in Core v5. This new system essentially swaps out the app shell's implementation that loads all the frontend modules from the import map for an implementation that loads modules on demand. This means that modules are only loaded when they are needed, and only the code that is needed is executed. This yields a significant performance improvement compared to the old system. For example, in local testing, we've seen an approximately 3x reduction in the number of network requests needed to load the login page. Additionally, we've seen improvements to core web vitals metrics such as First Contentful Paint (FCP), Largest Contentful Paint (LCP), and Speed Index (as tested using Lighthouse on Google Chrome).
To leverage these improvements, you'll need to migrate your existing frontend modules to Core v5. This guide will walk you through the process of doing so.
In general, you need to do the following:
Factor out static metadata into a
routes.json
fileFactor out dynamic metadata into a
startupApp
activator function
Case Study: Login frontend module
Let's take a look at the Login frontend module as an example. The original index.ts
file for the module looks like this:
import { getAsyncLifecycle, defineConfigSchema } from "@openmrs/esm-framework";
import { configSchema } from "./config-schema";
declare var __VERSION__: string;
// __VERSION__ is replaced by Webpack with the version from package.json
const version = __VERSION__;
const importTranslation = require.context("../translations", false, /.json$/, "lazy");
const backendDependencies = {
"webservices.rest": "^2.24.0",
};
const sharedOnlineOfflineProps = {
online: {
isLoginEnabled: true,
},
offline: {
isLoginEnabled: false,
},
};
function setupOpenMRS() {
const moduleName = "@openmrs/esm-login-app";
const options = {
featureName: "login",
moduleName,
};
defineConfigSchema(moduleName, configSchema);
return {
pages: [
{
load: getAsyncLifecycle(() => import("./root.component"), options),
route: "login",
...sharedOnlineOfflineProps,
},
{
load: getAsyncLifecycle(() => import("./root.component"), options),
route: "logout",
...sharedOnlineOfflineProps,
},
],
extensions: [
{
name: "location-picker",
slot: "location-picker",
load: getAsyncLifecycle(() => import("./location-picker/location-picker.component"), options),
...sharedOnlineOfflineProps,
},
{
name: "logout-button",
slot: "user-panel-actions-slot",
load: getAsyncLifecycle(() => import("./logout/logout.component"), options),
online: true,
offline: false,
},
{
name: "location-changer",
slot: "user-panel-slot",
order: 1,
load: getAsyncLifecycle(() => import("./change-location-link/change-location-link.component"), options),
...sharedOnlineOfflineProps,
},
],
};
}
export { setupOpenMRS, importTranslation, backendDependencies, version };Factor out static metadata
Factor out static metadata
Each frontend module defines metadata that are either static or dynamic in nature. Static metadata include:
backendDependencies
- the versions of backend dependencies that the module depends on.pages
- the pages that the module provides.extensions
- the extensions that the module provides.
These metadata are static in the sense that they do not change at runtime. They are also the metadata that are used by the app shell to load the module.
Looking at the entrypoint for the Login
example from above, the static metadata we need to factor out are shown in below:
import { getAsyncLifecycle, defineConfigSchema } from "@openmrs/esm-framework";
import { configSchema } from "./config-schema";
declare var __VERSION__: string;
// __VERSION__ is replaced by Webpack with the version from package.json
const version = __VERSION__;
const importTranslation = require.context("../translations", false, /.json$/, "lazy");
const backendDependencies = {
"webservices.rest": "^2.24.0",
};
const sharedOnlineOfflineProps = {
online: {
isLoginEnabled: true,
},
offline: {
isLoginEnabled: false,
},
};
function setupOpenMRS() {
const moduleName = "@openmrs/esm-login-app";
const options = {
featureName: "login",
moduleName,
};
defineConfigSchema(moduleName, configSchema);
return {
pages: [
{
load: getAsyncLifecycle(() => import("./root.component"), options),
route: "login",
...sharedOnlineOfflineProps,
},
{
load: getAsyncLifecycle(() => import("./root.component"), options),
route: "logout",
...sharedOnlineOfflineProps,
},
],
extensions: [
{
name: "location-picker",
slot: "location-picker",
load: getAsyncLifecycle(() => import("./location-picker/location-picker.component"), options),
...sharedOnlineOfflineProps,
},
{
name: "logout-button",
slot: "user-panel-actions-slot",
load: getAsyncLifecycle(() => import("./logout/logout.component"), options),
online: true,
offline: false,
},
{
name: "location-changer",
slot: "user-panel-slot",
order: 1,
load: getAsyncLifecycle(() => import("./change-location-link/change-location-link.component"), options),
...sharedOnlineOfflineProps,
},
],
};
}
export { setupOpenMRS, importTranslation, backendDependencies, version };
Let's walk through the changes that we need to make to this file to build out the routes.json
file step by step.
1. Create a routes.json
file
Create a routes.json
file in the module's root directory:
{
"$schema": "https://json.openmrs.org/routes.schema.json"
}
The $schema
property points to the routes schema file which is a standard JSON schema that enables your IDE to provide autocompletion and validation for the routes.json
file.
2: Move backendDependencies
backendDependencies
represents a list of backend modules necessary for this frontend module to work and the corresponding required versions. Move backendDependencies
from index.ts
to routes.json
as follows:
3. Move pages
pages
are automatically mounted ba