Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Info

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.

Table of Contents
stylenone

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:

...

To address these issues, we've introduced a new module loading mechanism(opens in a new tab) in Core v5(opens in a new tab). 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).

...

Let's take a look at the Login(opens in a new tab) frontend module as an example. The original index.ts file for the module looks like this:

Code Block
languagets
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.jsonconstjson
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

...

Looking at the entrypoint for the Login example from above, the static metadata we need to factor out are highlighted shown in below:

Code Block
languagets
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.jsonconstjson
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 };

...

Create a routes.json file in the module's root directory:

Code Block
languagejson
{
  "$schema": "https://json.openmrs.org/routes.schema.json"
}

The $schema property points to the routes schema(opens in a new tab) file which is a standard JSON schema(opens in a new tab) that enables your IDE to provide autocompletion and validation for the routes.json file.

...

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:

Code Block
languagejson
{
  "$schema": "https://json.openmrs.org/routes.schema.json",
  "backendDependencies": {
    "webservices.rest": "^2.24.0"
  }
}

3. Move pages

pages are automatically mounted based on a route.

...

  • component - a string property that represents the name of the component exported by this frontend module.

  • route - string or boolean property that represents the route that the page is accessible at. This is the same as the route property in the original pages array. If set to a string property, this is used to indicate that this page is accessible at the specified route. For example, name will match when the current page is ${window.spaBase/name}. If a boolean, this indicates the component should always be rendered or should never be rendered.

  • routeRegex - A regular expression that is used to match the current route to determine if this page should be rendered. Note that ${window.spaBase} gets removed before attempting to match the route, so setting this to ^name.+ will any route that starts with ${window.spaBase}/name. You can only specify one of route or routeRegex.

  • privilege - array or string property that represents one or more privileges that a user must have in order for this page to be rendered. If the user does not have the specified privileges, the page will not be rendered.

  • online - optional boolean property. Defaults to true. Determines whether the component renders while the browser is connected to the internet. If false, the page will not be rendered while online.

  • offline - optional boolean property. Defaults to true. Determines whether the component renders while the browser is not connected to the internet. If false, the page will not be rendered while offline.

  • order - integer property. Determines the order in which the DOM element that renders this page is rendered. Should be used sparingly, but is sometimes necessary to ensure the resulting markup is correct. Minimum value is 0.ℹ️

Info

component is the only required property. All other properties are optional.

To move pages from index.ts to routes.json, we need to extract the following properties from each page definition in the pages array:

  • component - the named export of the component. This gets obtained from the load property of the page definition. Looking at the login example:

    Code Block
    languagets
      {
        load: getAsyncLifecycle(() => import("./root.component"), options),
        route: "login",
        // Properties of the `sharedOnlineOfflineProps` object get spread here for brevity
        online: {
          isLoginEnabled: true,
        },
        offline: {
          isLoginEnabled: false,
        }
      },
      {
        load: getAsyncLifecycle(() => import("./root.component"), options),
        route: "logout",
        // Properties of the `sharedOnlineOfflineProps` object get spread here for brevity
        online: {
          isLoginEnabled: true,
        },
        offline: {
          isLoginEnabled: false,
        }
      }

    We could extract the following component:

    Code Block
    languagets
    export const root = getAsyncLifecycle(() => import("./root.component"), options);
  • route - we can extract this directly from the route property of the page definition. In the login example, this would be "login" and "logout" respectively. These routes both use the same component, so we can use the same component property for both of them.

  • online - we'll use the default value true.

  • offline - we'll use the default value true.

Putting this together gives us the following pages definition:

Code Block
languagejson
{
  "$schema": "https://json.openmrs.org/routes.schema.json",
  "backendDependencies": {
    "webservices.rest": "^2.24.0"
  },
  "pages": [
    {
      "component": "root",
      "route": "login",
      "online": true,
      "offline": true
    },
    {
      "component": "root",
      "route": "logout",
      "online": true,
      "offline": true
    }
  ]
}

4. Move extensions

extensions is an array of all the extensions supported by a frontend module. Extensions can be mounted in extensions slots via declarations in the routes.json file or dynamically via configuration.

...

  • name - string property that refers to the name of the extension. This is the same as the name property in the original extensions array.

  • component - string property that refers to the name of the component exported by this frontend module. This is the same as the component property of the pages array from the previous step.

  • slot - string property that refers to the name of the slot that this extension should be mounted in. This is the same as the slot property in the original extensions array.

  • privilege - string or array property that refers to the privilege(s) that a user must have in order for this extension to be rendered.

  • online - optional boolean property. Defaults to true. Determines whether the component renders while the browser is connected to the internet. If false, the page will not be rendered while online.

  • offline - optional boolean property. Defaults to true. Determines whether the component renders while the browser is not connected to the internet. If false, the page will not be rendered while offline.

  • order - integer property. Determines the order in which this component renders in its default extension slot. Note that this can be overridden by configuration. Minimum value is 0.

  • meta - object property that describes any properties that get passed down to the extension when it gets loaded.ℹ️

Info

name and component are required properties. All other properties are optional.

To move extensions from index.ts to routes.json, we need to extract the following properties from each extension definition in the extensions array:

...

Putting this all together gives us the following extensions definition:

Code Block
languagejson
{
  "$schema": "https://json.openmrs.org/routes.schema.json",
  "backendDependencies": {
    "webservices.rest": "^2.24.0"
  },
  "pages": [
    {
      "component": "root",
      "route": "login",
      "online": true,
      "offline": true
    },
    {
      "component": "root",
      "route": "logout",
      "online": true,
      "offline": true
    }
  ],
  "extensions": [
    {
      "name": "location-picker",
      "slot": "location-picker",
      "component": "locationPicker",
      "online": true,
      "offline": true
    },
    {
      "name": "logout-button",
      "slot": "user-panel-actions-slot",
      "component": "logoutButton",
      "online": true,
      "offline": true
    },
    {
      "name": "location-changer",
      "slot": "user-panel-slot",
      "component": "changeLocationLink",
      "online": true,
      "offline": true,
      "order": 1
    }
  ]
}

Final routes.json file

The final routes.json file looks like this:

Code Block
languagejson
{
  "$schema": "https://json.openmrs.org/routes.schema.json",
  "backendDependencies": {
    "webservices.rest": "^2.24.0"
  },
  "pages": [
    {
      "component": "root",
      "route": "login",
      "online": true,
      "offline": true
    },
    {
      "component": "root",
      "route": "logout",
      "online": true,
      "offline": true
    }
  ],
  "extensions": [
    {
      "name": "location-picker",
      "slot": "location-picker",
      "component": "locationPicker",
      "online": true,
      "offline": true
    },
    {
      "name": "logout-button",
      "slot": "user-panel-actions-slot",
      "component": "logoutButton",
      "online": true,
      "offline": true
    },
    {
      "name": "location-changer",
      "slot": "user-panel-slot",
      "component": "changeLocationLink",
      "online": true,
      "offline": true,
      "order": 1
    }
  ]
}

Factor out dynamic metadata

...

Code Block
languagets
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.jsonconstjson
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 };

...

Code Block
languagets
import { getAsyncLifecycle, defineConfigSchema } from "@openmrs/esm-framework";
import { configSchema } from "./config-schema";
 
const moduleName = "@openmrs/esm-login-app";
 
const options = {
  featureName: "login",
  moduleName,
};

2. Make the importTranslation function a named export

...

Code Block
languagets
export const root = getAsyncLifecycle(() => import("./root.component"), options);
 
export const locationPicker = getAsyncLifecycle(() => import("./location-picker/location-picker.component"), options);
 
export const logoutButton = getAsyncLifecycle(() => import("./logout/logout.component"), options);
 
export const changeLocationLink = getAsyncLifecycle(
  () => import("./change-location-link/change-location-link.component"),
  options
);

These correspond to the component property of each page and extension in the pages and extensions arrays in the routes.json file.

...

Code Block
languagets
export function startupApp() {
  defineConfigSchema(moduleName, configSchema);
}

5. Remove extraneous metadata

...

6. Use fewer dynamic imports for improved performance

...

Info

This is a recent performance optimization that has been shown to reduce the number of JavaScript files loaded at runtime significantly.

By importing components directly instead of using a function call, we can reduce the number of chunks that Webpack creates for a frontend module. Browsers have a limit on the number of network requests that can be made at any given time. For example, Chrome has a limit of 6 concurrent requests per domain. This means that the more chunks we have, the longer it takes to load a frontend module. We've seen a marked performance improvement from reducing the number of chunks for a frontend module, especially for modules with a large number of components.

...

Code Block
languagets
import rootComponent from './root.component';
 
export const root = getSyncLifecycle(rootComponent, options);

...

Code Block
languagets
import { getAsyncLifecycle, defineConfigSchema } from "@openmrs/esm-framework";
import { configSchema } from "./config-schema";
 
import rootComponent from "./root.component";
import locationPickerComponent from "./location-picker/location-picker.component";
import logoutButtonComponent from "./logout/logout.component";
import changeLocationLinkComponent from "./change-location-link/change-location-link.component";
 
const moduleName = "@openmrs/esm-login-app";
 
const options = {
  featureName: "login",
  moduleName,
};
 
export const importTranslation = require.context("../translations", false, /.json$/, "lazy");
 
export function startupApp() {
  defineConfigSchema(moduleName, configSchema);
}
 
export const root = getSyncLifecycle(rootComponent, options);
 
export const locationPicker = getSyncLifecycle(locationPickerComponent, options);
 
export const logoutButton = getSyncLifecycle(logoutButtonComponent, options);;
 
export const changeLocationLink = getSyncLifecycle(
  changeLocationLinkComponent,
  options
);

Final index.ts file

Bringing all these changes together, we get the following index.ts file:

Code Block
languagets
import { getAsyncLifecycle, defineConfigSchema } from "@openmrs/esm-framework";
import { configSchema } from "./config-schema";
 
import rootComponent from "./root.component";
import locationPickerComponent from "./location-picker/location-picker.component";
import logoutButtonComponent from "./logout/logout.component";
import changeLocationLinkComponent from "./change-location-link/change-location-link.component";
 
const moduleName = "@openmrs/esm-login-app";
 
const options = {
  featureName: "login",
  moduleName,
};
 
export const importTranslation = require.context("../translations", false, /.json$/, "lazy");
 
export function startupApp() {
  defineConfigSchema(moduleName, configSchema);
}
 
export const root = getSyncLifecycle(rootComponent, options);
 
export const locationPicker = getSyncLifecycle(locationPickerComponent, options);
 
export const logoutButton = getSyncLifecycle(logoutButtonComponent, options);
 
export const changeLocationLink = getSyncLifecycle(
  changeLocationLinkComponent,
  options
);

Upgrade core dependencies

...

Code Block
languagesh
└─ @openmrs/esm-form-builder-app@workspace:.
   └─ openmrs@npm:5.0.3-pre.846 (via npm:next)

This step is important because the latest versions of the framework include critical bug fixes and improvements(opens in a new tab) to the app shell and the core framework.

...

Code Block
languagesh
export { Dexie$1 as Dexie, RangeSet, Dexie$1 as default, liveQuery, mergeRanges, rangesOverlap };
    ^^^^^^
    SyntaxError: Unexpected token ‘export’

...

If your Jest config is in a JSON file, you might want to move that over to a JavaScript file instead. See this commit's diff(opens in a new tab) for guidance on what to change.

...

This error means that you're missing css-loader(opens in a new tab) dependency, which the framework uses . To fix this, make sure you install swc-node/loader as a devDependency in your frontend module:

...

I'm getting a minified single-spa error #10: Invalid mount lifecycle on parcel when I run my frontend module

Screenshot of the error messageImage RemovedScreenshot of the error messageImage Added

This error means that your frontend module has an invalid mount lifecycle function. The usual culprit is a misconfigured named export in your app's index.ts file. Make sure that your named exports directly reference exports from invoking getAsyncLifecycle and getSyncLifecycle:

Code Block
languagets
// This is incorrect. `root`. Remove the function call
callexportexport const root = () => getAsyncLifecycle(() => import("./root.component"), options);
 
// This is correct way to import the componentexportcomponent
export const root = getAsyncLifecycle(() => import("./root.component"), options);

This is a common mistake(opens in a new tab) when upgrading from the old frontend module structure to the new one. Don't get caught out.

...

To see more examples of how to upgrade a frontend module to the new structure, check out index.ts and routes.json files in any of our key repositories. For example, below are links to the index.ts and routes.json files for the @openmrs/esm-patient-chart-app frontend module in the Patient Chart repo: