@openmrs/esm-app-shell 9.0.3-pre.4533 → 9.0.3-pre.4550

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-app-shell",
3
- "version": "9.0.3-pre.4533",
3
+ "version": "9.0.3-pre.4550",
4
4
  "license": "MPL-2.0",
5
5
  "main": "dist/openmrs.js",
6
6
  "scripts": {
@@ -35,8 +35,8 @@
35
35
  "dependencies": {
36
36
  "@carbon/react": "^1.92.1",
37
37
  "@internationalized/date": "^3.8.0",
38
- "@openmrs/esm-framework": "9.0.3-pre.4533",
39
- "@openmrs/esm-styleguide": "9.0.3-pre.4533",
38
+ "@openmrs/esm-framework": "9.0.3-pre.4550",
39
+ "@openmrs/esm-styleguide": "9.0.3-pre.4550",
40
40
  "@rspack/cli": "1.7.9",
41
41
  "@rspack/core": "1.7.9",
42
42
  "dayjs": "^1.11.13",
@@ -44,7 +44,6 @@
44
44
  "html-webpack-plugin": "5.6.6",
45
45
  "i18next": "^25.5.3",
46
46
  "i18next-browser-languagedetector": "^8.2.0",
47
- "import-map-overrides": "^3.0.0",
48
47
  "lodash-es": "^4.17.21",
49
48
  "mini-css-extract-plugin": "2.9.1",
50
49
  "react": "^18.3.1",
package/rspack.config.js CHANGED
@@ -30,7 +30,40 @@ const openmrsPublicPath = removeTrailingSlash(process.env.OMRS_PUBLIC_PATH || '/
30
30
  const openmrsProxyTarget = process.env.OMRS_PROXY_TARGET || 'https://dev3.openmrs.org/';
31
31
  const openmrsPageTitle = process.env.OMRS_PAGE_TITLE || 'OpenMRS';
32
32
  const openmrsFavicon = process.env.OMRS_FAVICON || `${openmrsPublicPath}/favicon.ico`;
33
- const openmrsEnvironment = process.env.OMRS_ENV || process.env.NODE_ENV || '';
33
+ /**
34
+ * Resolves the target environment from OMRS_ENV, falling back to NODE_ENV / build mode.
35
+ *
36
+ * Accepts aliases ("prod" → "production", "dev" → "development") and defaults
37
+ * to "production" when nothing is set — so dev features are never accidentally
38
+ * enabled in an unconfigured build.
39
+ *
40
+ * @param {string} buildMode rspack/webpack build mode ("production" | "development")
41
+ * @returns {"production" | "development" | "test"}
42
+ */
43
+ function resolveEnvironment(buildMode) {
44
+ const raw = process.env.OMRS_ENV;
45
+
46
+ if (raw) {
47
+ switch (raw) {
48
+ case 'production':
49
+ case 'prod':
50
+ return 'production';
51
+ case 'development':
52
+ case 'dev':
53
+ return 'development';
54
+ case 'test':
55
+ return 'test';
56
+ default:
57
+ console.warn(`Unknown OMRS_ENV value "${raw}", defaulting to "production".`);
58
+ return 'production';
59
+ }
60
+ }
61
+
62
+ // No explicit OMRS_ENV — derive from NODE_ENV or build mode.
63
+ // Only "development" is treated as development; everything else is production.
64
+ const fallback = process.env.NODE_ENV || buildMode || '';
65
+ return fallback === 'development' ? 'development' : 'production';
66
+ }
34
67
  const openmrsOffline = process.env.OMRS_OFFLINE === 'enable';
35
68
  const openmrsDefaultLocale = process.env.OMRS_ESM_DEFAULT_LOCALE || 'en';
36
69
  const openmrsImportmapDef = process.env.OMRS_ESM_IMPORTMAP;
@@ -107,6 +140,7 @@ module.exports = (env, argv = []) => {
107
140
  const mode = argv.mode || process.env.NODE_ENV || production;
108
141
  const outDir = mode === production ? 'dist' : 'lib';
109
142
  const isProd = mode === 'production';
143
+ const openmrsEnvironment = resolveEnvironment(mode);
110
144
  const appPatterns = [];
111
145
 
112
146
  const coreImportmap = {
package/src/index.ts CHANGED
@@ -1,4 +1,3 @@
1
- import 'import-map-overrides';
2
1
  import type { SpaConfig } from '@openmrs/esm-framework/src/internal';
3
2
 
4
3
  function _createSpaBase(baseUrl: string) {
@@ -27,27 +26,53 @@ function setupPaths(config: SpaConfig) {
27
26
  );
28
27
  }
29
28
 
30
- window.openmrsBase = config.apiUrl;
31
- window.spaBase = config.spaPath;
32
- window.spaEnv = config.env || 'production';
33
- window.spaVersion = process.env.BUILD_VERSION ?? 'local';
29
+ // Object.defineProperty used to make these read-only
30
+ Object.defineProperty(window, 'openmrsBase', {
31
+ value: config.apiUrl,
32
+ writable: false,
33
+ configurable: false,
34
+ });
35
+ Object.defineProperty(window, 'spaBase', {
36
+ value: config.spaPath,
37
+ writable: false,
38
+ configurable: false,
39
+ });
40
+ Object.defineProperty(window, 'spaEnv', {
41
+ value: config.env || 'production',
42
+ writable: false,
43
+ configurable: false,
44
+ });
45
+ Object.defineProperty(window, 'spaVersion', {
46
+ value: process.env.BUILD_VERSION ?? 'local',
47
+ writable: false,
48
+ configurable: false,
49
+ });
50
+
34
51
  const spaBaseWithSlash = window.spaBase.endsWith('/') ? window.spaBase : window.spaBase + '/';
35
- window.getOpenmrsSpaBase = _createSpaBase(spaBaseWithSlash);
52
+ Object.defineProperty(window, 'getOpenmrsSpaBase', {
53
+ value: _createSpaBase(spaBaseWithSlash),
54
+ writable: false,
55
+ configurable: false,
56
+ });
36
57
  }
37
58
 
38
59
  export function setupUtils() {
39
- window.copyText = (source: HTMLElement) => {
40
- const sel = window.getSelection();
60
+ Object.defineProperty(window, 'copyText', {
61
+ value: (source: HTMLElement) => {
62
+ const sel = window.getSelection();
41
63
 
42
- if (sel) {
43
- const r = document.createRange();
44
- r.selectNode(source);
45
- sel.removeAllRanges();
46
- sel.addRange(r);
47
- document.execCommand('copy');
48
- sel.removeAllRanges();
49
- }
50
- };
64
+ if (sel) {
65
+ const r = document.createRange();
66
+ r.selectNode(source);
67
+ sel.removeAllRanges();
68
+ sel.addRange(r);
69
+ document.execCommand('copy');
70
+ sel.removeAllRanges();
71
+ }
72
+ },
73
+ writable: false,
74
+ configurable: false,
75
+ });
51
76
  }
52
77
 
53
78
  function wireSpaPaths() {
@@ -58,15 +83,21 @@ function wireSpaPaths() {
58
83
  __webpack_public_path__ = baseHref;
59
84
  }
60
85
 
86
+ let initPromise: Promise<void> | null = null;
87
+
61
88
  /**
62
89
  * Initializes the OpenMRS Frontend App Shell.
63
90
  * @param config The global configuration to apply.
64
91
  */
65
92
  function initializeSpa(config: SpaConfig) {
93
+ if (initPromise) {
94
+ return initPromise;
95
+ }
96
+
66
97
  setupUtils();
67
98
  setupPaths(config);
68
99
  wireSpaPaths();
69
- return Promise.resolve(__webpack_init_sharing__('default')).then(async () => {
100
+ initPromise = Promise.resolve(__webpack_init_sharing__('default')).then(async () => {
70
101
  const shareScope = __webpack_share_scopes__.default;
71
102
  // MF will deduplicate these as they're aliased at build time, but at runtime
72
103
  // apps try to load `@openmrs/esm-framework`, so here we provide a runtime
@@ -76,11 +107,20 @@ function initializeSpa(config: SpaConfig) {
76
107
  }
77
108
 
78
109
  const { configUrls = [], offline = false } = config;
79
- window.offlineEnabled = offline;
110
+ Object.defineProperty(window, 'offlineEnabled', {
111
+ value: offline,
112
+ writable: false,
113
+ configurable: false,
114
+ });
80
115
 
81
116
  const { run } = await import(/* webpackPreload: true */ './run');
82
117
  return run(configUrls);
83
118
  });
119
+ return initPromise;
84
120
  }
85
121
 
86
- window.initializeSpa = initializeSpa;
122
+ Object.defineProperty(window, 'initializeSpa', {
123
+ value: initializeSpa,
124
+ writable: false,
125
+ configurable: false,
126
+ });
package/src/locale.ts CHANGED
@@ -11,7 +11,12 @@ import {
11
11
  registerTranslationNamespace('core');
12
12
 
13
13
  export function setupI18n() {
14
- const i18n = (window.i18next = i18next.default || i18next);
14
+ const i18n = i18next.default || i18next;
15
+ Object.defineProperty(window, 'i18next', {
16
+ value: i18n,
17
+ writable: false,
18
+ configurable: false,
19
+ });
15
20
 
16
21
  const languageChangeObserver = new MutationObserver(() => {
17
22
  i18n.changeLanguage().catch((e) => console.error('i18next failed to re-detect language', e));
package/src/run.ts CHANGED
@@ -2,19 +2,17 @@ import { start, triggerAppChange } from 'single-spa';
2
2
  import { type CalendarIdentifier } from '@internationalized/date';
3
3
  import {
4
4
  activateOfflineCapability,
5
- canAccessStorage,
6
5
  dispatchConnectivityChanged,
7
6
  dispatchPrecacheStaticDependencies,
8
7
  type ExtensionDefinition,
9
8
  finishRegisteringAllApps,
10
9
  fireOpenmrsEvent,
11
10
  getConfig,
11
+ getCurrentPageMap,
12
+ getCurrentRouteMap,
12
13
  getCurrentUser,
13
14
  integrateBreakpoints,
14
15
  interpolateUrl,
15
- isOpenmrsAppRoutes,
16
- isOpenmrsRoutes,
17
- localStorageRoutesPrefix,
18
16
  messageOmrsServiceWorker,
19
17
  openmrsFetch,
20
18
  provide,
@@ -30,7 +28,9 @@ import {
30
28
  restBaseUrl,
31
29
  setupApiModule,
32
30
  setupHistory,
31
+ setupImportMapOverrides,
33
32
  setupModals,
33
+ setupRouteMapOverrides,
34
34
  showActionableNotification,
35
35
  showNotification,
36
36
  showSnackbar,
@@ -43,8 +43,6 @@ import {
43
43
  subscribeToastShown,
44
44
  tryRegisterExtension,
45
45
  type Config,
46
- type OpenmrsAppRoutes,
47
- type OpenmrsRoutes,
48
46
  type StyleguideConfigObject,
49
47
  } from '@openmrs/esm-framework/src/internal';
50
48
  import { setupI18n } from './locale';
@@ -65,106 +63,21 @@ const REGISTRATION_PROMISES = Symbol('openmrs_registration_promises');
65
63
  * as the registry of all apps in the application.
66
64
  */
67
65
  async function setupApps() {
68
- const scriptTags = document.querySelectorAll<HTMLScriptElement>("script[type='openmrs-routes']");
69
-
70
- const promises: Array<Promise<OpenmrsRoutes>> = [];
71
- for (let i = 0; i < scriptTags.length; i++) {
72
- promises.push(
73
- (async (scriptTag) => {
74
- let routes: OpenmrsRoutes | undefined = undefined;
75
- try {
76
- if (scriptTag.textContent) {
77
- routes = JSON.parse(scriptTag.textContent) as OpenmrsRoutes;
78
- } else if (scriptTag.src) {
79
- routes = (await openmrsFetch<OpenmrsRoutes>(scriptTag.src)).data;
80
- }
81
- } catch (e) {
82
- console.error(`Caught error while loading routes from ${scriptTag.src ?? 'JSON script tag content'}`, e);
83
-
84
- return {};
85
- }
86
-
87
- return Promise.resolve(routes ?? {});
88
- })(scriptTags.item(i)),
89
- );
90
- }
91
-
92
- if (canAccessStorage()) {
93
- // load routes overrides from localStorage if any
94
- const localStorage = window.localStorage;
95
- for (let i = 0; i < localStorage.length; i++) {
96
- const key = localStorage.key(i);
97
- if (key?.startsWith(localStorageRoutesPrefix)) {
98
- const localOverride = localStorage.getItem(key);
99
- if (localOverride) {
100
- try {
101
- const maybeOpenmrsRoutes = JSON.parse(localOverride);
102
- if (isOpenmrsAppRoutes(maybeOpenmrsRoutes)) {
103
- promises.push(
104
- Promise.resolve<OpenmrsRoutes>({
105
- [key.slice(localStorageRoutesPrefix.length)]: maybeOpenmrsRoutes,
106
- }),
107
- );
108
- } else if (typeof maybeOpenmrsRoutes === 'string' && maybeOpenmrsRoutes.startsWith('http')) {
109
- promises.push(
110
- openmrsFetch<OpenmrsAppRoutes>(maybeOpenmrsRoutes)
111
- .then((response) => {
112
- if (isOpenmrsAppRoutes(response.data)) {
113
- return Promise.resolve({
114
- [key.slice(localStorageRoutesPrefix.length)]: response.data,
115
- });
116
- }
117
-
118
- return Promise.reject(
119
- `${maybeOpenmrsRoutes} did not resolve to a valid OpenmrsAppRoutes JSON object`,
120
- );
121
- })
122
- .catch((reason) => {
123
- console.warn(
124
- `Failed to fetch route overrides for ${key.slice(localStorageRoutesPrefix.length)}`,
125
- reason,
126
- );
127
-
128
- // still fail the promise
129
- throw reason;
130
- }),
131
- );
132
- } else {
133
- console.warn(
134
- `Route override for ${key.slice(
135
- localStorageRoutesPrefix.length,
136
- )} could not be handled as it was neither a JSON object nor a URL string`,
137
- localOverride,
138
- );
139
- }
140
- } catch (e) {
141
- console.error(`Error parsing local route override for ${key}`, e);
142
- }
143
- }
144
- }
145
- }
146
- }
147
-
148
- const routes: OpenmrsRoutes = (await Promise.allSettled(promises))
149
- .filter((p) => p.status === 'fulfilled')
150
- .map((p) => (p as PromiseFulfilledResult<OpenmrsRoutes>).value)
151
- .filter(isOpenmrsRoutes)
152
- .reduce(
153
- (accumulatedRoutes, routes) => ({
154
- ...accumulatedRoutes,
155
- ...routes,
156
- }),
157
- {},
158
- );
66
+ await setupRouteMapOverrides();
67
+ const routes = await getCurrentRouteMap();
159
68
 
160
69
  const modules: typeof window.installedModules = [];
161
- const registrationPromises = Object.entries(routes).map(async ([module, routes]) => {
162
- modules.push([module, routes]);
163
- registerApp(module, routes);
70
+ const registrationPromises = Object.entries(routes).map(async ([module, appRoutes]) => {
71
+ modules.push([module, appRoutes]);
72
+ registerApp(module, appRoutes);
164
73
  });
165
74
 
166
75
  window[REGISTRATION_PROMISES] = Promise.all(registrationPromises);
167
- window.installedModules = modules;
76
+ Object.defineProperty(window, 'installedModules', {
77
+ value: modules,
78
+ writable: false,
79
+ configurable: false,
80
+ });
168
81
  }
169
82
 
170
83
  /**
@@ -385,7 +298,7 @@ async function precacheGlobalStaticDependencies() {
385
298
  }
386
299
 
387
300
  async function precacheImportMap() {
388
- const importMap = await window.importMapOverrides.getCurrentPageMap();
301
+ const importMap = await getCurrentPageMap();
389
302
  await messageOmrsServiceWorker({
390
303
  type: 'onImportMapChanged',
391
304
  importMap,
@@ -409,6 +322,8 @@ function setupOfflineCssClasses() {
409
322
  }
410
323
 
411
324
  export function run(configUrls: Array<string>) {
325
+ setupImportMapOverrides();
326
+
412
327
  const offlineEnabled = window.offlineEnabled;
413
328
  const closeLoading = showLoadingSpinner();
414
329
  const provideConfigs = createConfigLoader(configUrls);
@@ -441,7 +356,7 @@ export function run(configUrls: Array<string>) {
441
356
 
442
357
  return polyfillReady
443
358
  .then(setupApps)
444
- .then(() => finishRegisteringAllApps())
359
+ .then(() => Promise.resolve(finishRegisteringAllApps()))
445
360
  .then(offlineEnabled ? setupOfflineCssClasses : undefined)
446
361
  .then(offlineEnabled ? registerOfflineHandlers : undefined)
447
362
  .then(provideConfigs)
@@ -47,6 +47,21 @@ async function registerDynamicRoute({ pattern, url, strategy }: RegisterDynamicR
47
47
  * @param event The event data containing the message dispatched to the Service Worker.
48
48
  */
49
49
  export async function handleMessage(event: ExtendableMessageEvent) {
50
+ // Defense-in-depth: only process messages from same-origin Client sources.
51
+ // Reject messages with no source or from cross-origin sources.
52
+ if (!event.source || !('url' in event.source)) {
53
+ return;
54
+ }
55
+ let sourceOrigin: string;
56
+ try {
57
+ sourceOrigin = new URL(event.source.url).origin;
58
+ } catch {
59
+ return;
60
+ }
61
+ if (sourceOrigin !== self.location.origin) {
62
+ return;
63
+ }
64
+
50
65
  const handler = messageHandlers[event.data?.type?.toString() ?? ''];
51
66
 
52
67
  if (!handler) {
package/tsconfig.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "allowSyntheticDefaultImports": true,
7
7
  "jsx": "react",
8
8
  "strictNullChecks": true,
9
- "moduleResolution": "node",
9
+ "moduleResolution": "bundler",
10
10
  "lib": [
11
11
  "dom",
12
12
  "es5",