@openmrs/esm-config 10.0.1-pre.5738 → 10.0.1-pre.5741

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.
@@ -1,3 +1,3 @@
1
- [0] Successfully compiled: 8 files with swc (172.56ms)
1
+ [0] Successfully compiled: 8 files with swc (143.43ms)
2
2
  [0] swc --strip-leading-paths src -d dist exited with code 0
3
3
  [1] tsc --project tsconfig.build.json exited with code 0
@@ -89,7 +89,9 @@ export declare function getConfig<T = Record<string, any>>(moduleName: string):
89
89
  * @param moduleName The name of the module providing the translation context.
90
90
  * @param slotName Optional extension slot name to include slot-specific overrides.
91
91
  * @param extensionId Optional extension ID to include extension-specific overrides.
92
- * @returns A Promise resolving to an array of translation override objects.
92
+ * @returns A Promise resolving to an array of translation override objects. It always settles: a
93
+ * module the config system has not been told about, or an extension that is not mounted,
94
+ * contributes no overrides.
93
95
  *
94
96
  * @internal
95
97
  */
@@ -1 +1 @@
1
- {"version":3,"file":"module-config.d.ts","sourceRoot":"","sources":["../../src/module-config/module-config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,EAAuB,MAAM,UAAU,CAAC;AA4VxF;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,QAS1E;AAED;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAAC,UAAU,EAAE,MAAM,QAKhE;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,QAKpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,MAAM,QAK7D;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,2BAA2B,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,QAetF;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,SAAa,QAK9D;AAiCD;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAMjF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,EACjB,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAoBxD;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,UAKvG;AA6nBD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,MAAM,QAUjD;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,SAYhC"}
1
+ {"version":3,"file":"module-config.d.ts","sourceRoot":"","sources":["../../src/module-config/module-config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,EAAuB,MAAM,UAAU,CAAC;AAkWxF;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,QAS1E;AAED;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAAC,UAAU,EAAE,MAAM,QAKhE;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,QAKpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,MAAM,QAK7D;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,2BAA2B,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,QAetF;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,SAAa,QAK9D;AAiCD;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAMjF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,EACjB,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CA2BxD;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,UAKvG;AAsnBD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,MAAM,QAUjD;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,SAYhC"}
@@ -2,7 +2,7 @@
2
2
  import { Type } from "../types.js";
3
3
  import { isArray, isBoolean, isUuid, isNumber, isObject, isString } from "../validators/type-validators.js";
4
4
  import { validator } from "../validators/validator.js";
5
- import { configExtensionStore, configInternalStore, getConfigStore, getExtensionConfig, getExtensionSlotsConfigStore, getExtensionsConfigStore, clearConfigDerivationError, implementerToolsConfigStore, invalidateImplementerToolsConfig, recordConfigDerivationError, setImplementerToolsConfigRecomputer, temporaryConfigStore } from "./state.js";
5
+ import { configExtensionStore, configInternalStore, getConfigStore, getExtensionConfigFromStore, getExtensionSlotsConfigStore, getExtensionsConfigStore, clearConfigDerivationError, implementerToolsConfigStore, invalidateImplementerToolsConfig, recordConfigDerivationError, setImplementerToolsConfigRecomputer, temporaryConfigStore } from "./state.js";
6
6
  /**
7
7
  * Deep merge two objects, with right-side values taking precedence.
8
8
  * Arrays are replaced entirely (not merged by index), matching the
@@ -36,6 +36,9 @@ function mergeDeepReplace(left, right) {
36
36
  * which creates stores.
37
37
  */ // Store unsubscribe functions to allow cleanup (e.g., in tests or hot module reloading)
38
38
  const configSubscriptions = [];
39
+ /**
40
+ * Track displayed validation messages so they only get displayed once.
41
+ */ const displayedValidationMessages = new Set();
39
42
  /**
40
43
  * Recomputes all configuration derived stores based on current state of input stores.
41
44
  * Called whenever any input store (configInternalStore, temporaryConfigStore, configExtensionStore) changes.
@@ -73,8 +76,9 @@ const configSubscriptions = [];
73
76
  function setupConfigSubscriptions() {
74
77
  // Registered here rather than at module load so that `resetConfigSystem` restores it.
75
78
  setImplementerToolsConfigRecomputer(()=>computeImplementerToolsConfig(configInternalStore.getState(), temporaryConfigStore.getState()));
76
- // Initial computation
77
- recomputeAllConfigs();
79
+ // Guarded: a configuration the user saved through the implementer tools must not be able to stop
80
+ // the framework from loading.
81
+ recomputeAllConfigsSafely();
78
82
  // Subscribe to all input stores with a single handler
79
83
  // This ensures we only recompute once even if multiple stores change simultaneously
80
84
  configSubscriptions.push(configInternalStore.subscribe(recomputeAllConfigsSafely), temporaryConfigStore.subscribe(recomputeAllConfigsSafely), configExtensionStore.subscribe(recomputeAllConfigsSafely));
@@ -398,15 +402,24 @@ function computeExtensionConfigs(configState, extensionState, tempConfigState, s
398
402
  * @param moduleName The name of the module providing the translation context.
399
403
  * @param slotName Optional extension slot name to include slot-specific overrides.
400
404
  * @param extensionId Optional extension ID to include extension-specific overrides.
401
- * @returns A Promise resolving to an array of translation override objects.
405
+ * @returns A Promise resolving to an array of translation override objects. It always settles: a
406
+ * module the config system has not been told about, or an extension that is not mounted,
407
+ * contributes no overrides.
402
408
  *
403
409
  * @internal
404
410
  */ export function getTranslationOverrides(moduleName, slotName, extensionId) {
411
+ // A module's store only ever loads its overrides once the module is registered with the config
412
+ // system, so waiting on one that is not registered would leave the i18next namespace pending.
405
413
  const promises = [
406
- whenStoreReady(getConfigStore(moduleName), (state)=>Boolean(state.translationOverridesLoaded && state.config), (state)=>state.config['Translation overrides'] ?? {})
414
+ moduleName in configInternalStore.getState().schemas ? whenStoreReady(getConfigStore(moduleName), (state)=>Boolean(state.translationOverridesLoaded && state.config), (state)=>state.config['Translation overrides'] ?? {}) : Promise.resolve({})
407
415
  ];
408
416
  if (slotName && extensionId) {
409
- promises.push(whenStoreReady(getExtensionConfig(slotName, extensionId), (state)=>Boolean(state.loaded && state.config), (state)=>state.config['Translation overrides'] ?? {}));
417
+ // Read once rather than waited for: an extension's config entry exists only while that extension
418
+ // is registered as mounted, so a wait started in the gap never settles and leaves the i18next
419
+ // namespace pending.
420
+ const extensionConfig = getExtensionConfigFromStore(getExtensionsConfigStore().getState(), slotName, extensionId);
421
+ const overrides = extensionConfig.loaded && extensionConfig.config ? extensionConfig.config['Translation overrides'] ?? {} : {};
422
+ promises.push(Promise.resolve(overrides));
410
423
  }
411
424
  return Promise.all(promises);
412
425
  }
@@ -920,13 +933,8 @@ function hasObjectSchema(elementsSchema) {
920
933
  function isOrdinaryObject(value) {
921
934
  return typeof value === 'object' && !Array.isArray(value) && value !== null;
922
935
  }
923
- /** Keep track of which validation errors we have displayed. Each one should only be displayed once. */ let displayedValidationMessages = new Set();
924
936
  function logError(keyPath, message) {
925
937
  const key = `${keyPath}:::${message}`;
926
- // technically, this should not be possible, but because of how things wind-up transpiled, this isn't impossible
927
- if (!displayedValidationMessages) {
928
- displayedValidationMessages = new Set();
929
- }
930
938
  if (!displayedValidationMessages.has(key)) {
931
939
  console.error(message);
932
940
  displayedValidationMessages.add(key);
@@ -108,19 +108,6 @@ export interface ExtensionsConfigStore {
108
108
  * @internal
109
109
  */
110
110
  export declare function getExtensionsConfigStore(): StoreApi<ExtensionsConfigStore>;
111
- /**
112
- * The read-only part of a store's API.
113
- * @internal
114
- */
115
- export type ReadableStore<T> = Pick<StoreApi<T>, 'getInitialState' | 'getState' | 'subscribe'>;
116
- /**
117
- * A read-only view of one extension instance's config within a slot. Read-only because these
118
- * configs are derived from the config system's inputs, so a write here would be overwritten by the
119
- * next recomputation; provide a config or set a temporary one instead.
120
- *
121
- * @internal
122
- */
123
- export declare function getExtensionConfig(slotName: string, extensionId: string): ReadableStore<Omit<ConfigStore, 'translationOverridesLoaded'>>;
124
111
  /** @internal */
125
112
  export declare function getExtensionConfigFromStore(state: ExtensionsConfigStore, slotName: string, extensionId: string): ConfigStore;
126
113
  /** @internal */
@@ -1 +1 @@
1
- {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/module-config/state.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/module-config/state.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,4 @@
1
1
  import { createGlobalStore, getGlobalStore, registerGlobalStore } from "@openmrs/esm-state";
2
- import { shallowEqual } from "@openmrs/esm-utils";
3
2
  import { createStore } from "zustand/vanilla";
4
3
  const configInternalStoreInitialValue = {
5
4
  providedConfigs: [],
@@ -74,39 +73,6 @@ function initializeConfigStore() {
74
73
  configs: {}
75
74
  });
76
75
  }
77
- /**
78
- * A read-only view of one extension instance's config within a slot. Read-only because these
79
- * configs are derived from the config system's inputs, so a write here would be overwritten by the
80
- * next recomputation; provide a config or set a temporary one instead.
81
- *
82
- * @internal
83
- */ export function getExtensionConfig(slotName, extensionId) {
84
- if (typeof slotName !== 'string' || typeof extensionId !== 'string' || slotName === '__proto__' || extensionId === '__proto__' || slotName === 'constructor' || extensionId === 'constructor' || slotName === 'prototype' || extensionId === 'prototype') {
85
- throw new Error('Attempted to call `getExtensionConfig()` with invalid argument');
86
- }
87
- const extensionConfigStore = getExtensionsConfigStore();
88
- const selector = (configStore)=>configStore.configs[slotName]?.[extensionId];
89
- return {
90
- getInitialState () {
91
- return selector(extensionConfigStore.getInitialState());
92
- },
93
- getState () {
94
- return selector(extensionConfigStore.getState()) ?? {
95
- loaded: false,
96
- config: null
97
- };
98
- },
99
- subscribe (listener) {
100
- return extensionConfigStore.subscribe((state, prevState)=>{
101
- const newState = selector(state);
102
- const oldState = selector(prevState);
103
- if (!shallowEqual(newState, oldState)) {
104
- listener(newState, oldState);
105
- }
106
- });
107
- }
108
- };
109
- }
110
76
  /** @internal */ export function getExtensionConfigFromStore(state, slotName, extensionId) {
111
77
  const extensionConfig = state.configs[slotName]?.[extensionId];
112
78
  return extensionConfig ?? {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-config",
3
- "version": "10.0.1-pre.5738",
3
+ "version": "10.0.1-pre.5741",
4
4
  "license": "MPL-2.0",
5
5
  "description": "A configuration library for the OpenMRS Single-Spa framework.",
6
6
  "type": "module",
@@ -57,15 +57,15 @@
57
57
  "lodash-es": "^4.17.21"
58
58
  },
59
59
  "peerDependencies": {
60
- "@openmrs/esm-globals": "^10.0.1-pre.5738",
61
- "@openmrs/esm-state": "^10.0.1-pre.5738",
62
- "@openmrs/esm-utils": "^10.0.1-pre.5738",
60
+ "@openmrs/esm-globals": "^10.0.1-pre.5741",
61
+ "@openmrs/esm-state": "^10.0.1-pre.5741",
62
+ "@openmrs/esm-utils": "^10.0.1-pre.5741",
63
63
  "single-spa": "6.x"
64
64
  },
65
65
  "devDependencies": {
66
- "@openmrs/esm-globals": "10.0.1-pre.5738",
67
- "@openmrs/esm-state": "10.0.1-pre.5738",
68
- "@openmrs/esm-utils": "10.0.1-pre.5738",
66
+ "@openmrs/esm-globals": "10.0.1-pre.5741",
67
+ "@openmrs/esm-state": "10.0.1-pre.5741",
68
+ "@openmrs/esm-utils": "10.0.1-pre.5741",
69
69
  "@swc/cli": "0.8.1",
70
70
  "@swc/core": "1.15.21",
71
71
  "@types/lodash-es": "^4.17.12",
@@ -10,7 +10,8 @@ import {
10
10
  configExtensionStore,
11
11
  configInternalStore,
12
12
  getConfigStore,
13
- getExtensionConfig,
13
+ getExtensionConfigFromStore,
14
+ getExtensionsConfigStore,
14
15
  implementerToolsConfigStore,
15
16
  temporaryConfigStore,
16
17
  } from './state';
@@ -1314,7 +1315,7 @@ describe('extension config', () => {
1314
1315
  extensionId,
1315
1316
  })),
1316
1317
  });
1317
- const bar = (extensionId: string) => getExtensionConfig('barSlot', extensionId).getState().config.bar;
1318
+ const bar = (extensionId: string) => currentExtensionConfig('barSlot', extensionId).config.bar;
1318
1319
 
1319
1320
  mount(['fooExt#a']);
1320
1321
  expect(bar('fooExt#a')).toBe('barry');
@@ -1343,7 +1344,7 @@ describe('extension config', () => {
1343
1344
  { slotModuleName: 'slot-mod', extensionModuleName, slotName: 'barSlot', extensionId: 'fooExt' },
1344
1345
  ],
1345
1346
  });
1346
- const bar = () => getExtensionConfig('barSlot', 'fooExt').getState().config.bar;
1347
+ const bar = () => currentExtensionConfig('barSlot', 'fooExt').config.bar;
1347
1348
 
1348
1349
  mount('ext-mod');
1349
1350
  expect(bar()).toBe('barry');
@@ -1356,7 +1357,7 @@ describe('extension config', () => {
1356
1357
  const moduleLevelConfig = { 'ext-mod': { bar: 'qux' } };
1357
1358
  updateConfigExtensionStore();
1358
1359
  Config.provide(moduleLevelConfig);
1359
- const result = getExtensionConfig('barSlot', 'fooExt').getState().config;
1360
+ const result = currentExtensionConfig('barSlot', 'fooExt').config;
1360
1361
  expect(result).toStrictEqual({
1361
1362
  bar: 'qux',
1362
1363
  baz: 'bazzy',
@@ -1379,7 +1380,7 @@ describe('extension config', () => {
1379
1380
  },
1380
1381
  };
1381
1382
  Config.provide(configureConfig);
1382
- const result = getExtensionConfig('barSlot', 'fooExt#id0').getState().config;
1383
+ const result = currentExtensionConfig('barSlot', 'fooExt#id0').config;
1383
1384
  expect(result).toStrictEqual({
1384
1385
  bar: 'qux',
1385
1386
  baz: 'quiz',
@@ -1404,7 +1405,7 @@ describe('extension config', () => {
1404
1405
  },
1405
1406
  };
1406
1407
  Config.provide(configureConfig);
1407
- const result = getExtensionConfig('barSlot', 'fooExt#id6').getState().config;
1408
+ const result = currentExtensionConfig('barSlot', 'fooExt#id6').config;
1408
1409
  expect(result).toStrictEqual({
1409
1410
  bar: 'qux',
1410
1411
  baz: 'quiz',
@@ -1433,7 +1434,7 @@ describe('extension config', () => {
1433
1434
  },
1434
1435
  };
1435
1436
  Config.provide(configureConfig);
1436
- const result = getExtensionConfig('barSlot', 'fooExt#id7').getState().config;
1437
+ const result = currentExtensionConfig('barSlot', 'fooExt#id7').config;
1437
1438
  expect(result).toStrictEqual({
1438
1439
  bar: 'from-other',
1439
1440
  baz: 'from-owner',
@@ -1461,7 +1462,7 @@ describe('extension config', () => {
1461
1462
  // Composing `add` across modules would resurrect entries another module had removed, so the
1462
1463
  // last module to name the slot still supplies it whole.
1463
1464
  expect(getExtensionSlotsConfigStore().getState().slots['barSlot'].config.add).toStrictEqual(['fooExt#id8']);
1464
- expect(getExtensionConfig('barSlot', 'fooExt#id8').getState().config).toStrictEqual({
1465
+ expect(currentExtensionConfig('barSlot', 'fooExt#id8').config).toStrictEqual({
1465
1466
  bar: 'from-first',
1466
1467
  baz: 'from-last',
1467
1468
  'Display conditions': { expression: undefined, privileges: [] },
@@ -1474,12 +1475,12 @@ describe('extension config', () => {
1474
1475
  temporaryConfigStore.setState({
1475
1476
  config: { 'other-mod': { extensionSlots: { barSlot: { configure: { 'fooExt#id9': { baz: 'temporary' } } } } } },
1476
1477
  });
1477
- expect(getExtensionConfig('barSlot', 'fooExt#id9').getState().config.baz).toBe('temporary');
1478
+ expect(currentExtensionConfig('barSlot', 'fooExt#id9').config.baz).toBe('temporary');
1478
1479
 
1479
1480
  // The derived slot config is not part of the extension config cache key, so a released override
1480
1481
  // is only picked up because that cache also keys on the temporary config it was derived from.
1481
1482
  temporaryConfigStore.setState({ config: {} });
1482
- expect(getExtensionConfig('barSlot', 'fooExt#id9').getState().config.baz).toBe('bazzy');
1483
+ expect(currentExtensionConfig('barSlot', 'fooExt#id9').config.baz).toBe('bazzy');
1483
1484
  });
1484
1485
 
1485
1486
  it('validates the extension configure config, with module config schema', () => {
@@ -1506,7 +1507,7 @@ describe('extension config', () => {
1506
1507
  Config.registerModuleLoad('fooExt');
1507
1508
  const extensionAtBaseConfig = { fooExt: { qux: 'quxolotl' } };
1508
1509
  Config.provide(extensionAtBaseConfig);
1509
- const result = getExtensionConfig('barSlot', 'fooExt').getState().config;
1510
+ const result = currentExtensionConfig('barSlot', 'fooExt').config;
1510
1511
  expect(result).toStrictEqual({
1511
1512
  qux: 'quxolotl',
1512
1513
  'Display conditions': { expression: undefined, privileges: [] },
@@ -1531,7 +1532,7 @@ describe('extension config', () => {
1531
1532
  },
1532
1533
  };
1533
1534
  Config.provide(configureConfig);
1534
- const result = getExtensionConfig('barSlot', 'fooExt#id2').getState().config;
1535
+ const result = currentExtensionConfig('barSlot', 'fooExt#id2').config;
1535
1536
  expect(result).toStrictEqual({
1536
1537
  qux: 'quxotic',
1537
1538
  'Display conditions': { expression: undefined, privileges: [] },
@@ -1651,6 +1652,18 @@ describe('promise-based config accessors', () => {
1651
1652
  return counter;
1652
1653
  }
1653
1654
 
1655
+ /**
1656
+ * Rejects if `promise` has not settled within half a second. i18next never retries a namespace
1657
+ * read, so a translation-override wait that never settles leaves every component under that
1658
+ * namespace suspended for good; this turns that hang into a failure.
1659
+ */
1660
+ function settled<T>(promise: Promise<T>) {
1661
+ return Promise.race([
1662
+ promise,
1663
+ new Promise<never>((_, reject) => setTimeout(() => reject(new Error('never settled')), 500)),
1664
+ ]);
1665
+ }
1666
+
1654
1667
  it('getConfig does not subscribe at all when the config has already loaded', async () => {
1655
1668
  Config.defineConfigSchema('leak-module', { foo: { _default: 'qux' } });
1656
1669
  Config.registerModuleLoad('leak-module');
@@ -1692,8 +1705,46 @@ describe('promise-based config accessors', () => {
1692
1705
 
1693
1706
  expect(subscriptions.live).toBe(0);
1694
1707
  });
1708
+
1709
+ it('getTranslationOverrides settles for an extension that is not mounted', async () => {
1710
+ Config.defineConfigSchema('unmounted-ext-module', { foo: { _default: 'qux' } });
1711
+ Config.registerModuleLoad('unmounted-ext-module');
1712
+
1713
+ const overrides = Config.getTranslationOverrides('unmounted-ext-module', 'absent-slot', 'absent-ext');
1714
+
1715
+ await expect(settled(overrides)).resolves.toStrictEqual([{}, {}]);
1716
+ });
1717
+
1718
+ it('getTranslationOverrides settles for a module the config system has not been told about', async () => {
1719
+ const subscriptions = countSubscriptions('unregistered-module');
1720
+
1721
+ await expect(settled(Config.getTranslationOverrides('unregistered-module'))).resolves.toStrictEqual([{}]);
1722
+ expect(subscriptions.live).toBe(0);
1723
+ });
1724
+
1725
+ it("getTranslationOverrides returns a mounted extension's overrides", async () => {
1726
+ Config.defineConfigSchema('ext-mod', {});
1727
+ Config.registerModuleLoad('ext-mod');
1728
+ Config.provide({
1729
+ 'slot-mod': {
1730
+ extensionSlots: {
1731
+ barSlot: { configure: { fooExt: { 'Translation overrides': { fr: { hello: 'Bonjour' } } } } },
1732
+ },
1733
+ },
1734
+ });
1735
+ updateConfigExtensionStore('fooExt');
1736
+
1737
+ await expect(Config.getTranslationOverrides('ext-mod', 'barSlot', 'fooExt')).resolves.toStrictEqual([
1738
+ {},
1739
+ { fr: { hello: 'Bonjour' } },
1740
+ ]);
1741
+ });
1695
1742
  });
1696
1743
 
1744
+ function currentExtensionConfig(slotName: string, extensionId: string) {
1745
+ return getExtensionConfigFromStore(getExtensionsConfigStore().getState(), slotName, extensionId);
1746
+ }
1747
+
1697
1748
  function updateConfigExtensionStore(extensionId = 'fooExt') {
1698
1749
  configExtensionStore.setState({
1699
1750
  mountedExtensions: [
@@ -16,7 +16,7 @@ import {
16
16
  configExtensionStore,
17
17
  configInternalStore,
18
18
  getConfigStore,
19
- getExtensionConfig,
19
+ getExtensionConfigFromStore,
20
20
  getExtensionSlotsConfigStore,
21
21
  getExtensionsConfigStore,
22
22
  clearConfigDerivationError,
@@ -66,6 +66,11 @@ function mergeDeepReplace<T extends object>(left: T, right: Partial<T>): T {
66
66
  // Store unsubscribe functions to allow cleanup (e.g., in tests or hot module reloading)
67
67
  const configSubscriptions: Array<() => void> = [];
68
68
 
69
+ /**
70
+ * Track displayed validation messages so they only get displayed once.
71
+ */
72
+ const displayedValidationMessages = new Set<string>();
73
+
69
74
  /**
70
75
  * Recomputes all configuration derived stores based on current state of input stores.
71
76
  * Called whenever any input store (configInternalStore, temporaryConfigStore, configExtensionStore) changes.
@@ -119,8 +124,9 @@ function setupConfigSubscriptions() {
119
124
  computeImplementerToolsConfig(configInternalStore.getState(), temporaryConfigStore.getState()),
120
125
  );
121
126
 
122
- // Initial computation
123
- recomputeAllConfigs();
127
+ // Guarded: a configuration the user saved through the implementer tools must not be able to stop
128
+ // the framework from loading.
129
+ recomputeAllConfigsSafely();
124
130
 
125
131
  // Subscribe to all input stores with a single handler
126
132
  // This ensures we only recompute once even if multiple stores change simultaneously
@@ -526,7 +532,9 @@ export function getConfig<T = Record<string, any>>(moduleName: string): Promise<
526
532
  * @param moduleName The name of the module providing the translation context.
527
533
  * @param slotName Optional extension slot name to include slot-specific overrides.
528
534
  * @param extensionId Optional extension ID to include extension-specific overrides.
529
- * @returns A Promise resolving to an array of translation override objects.
535
+ * @returns A Promise resolving to an array of translation override objects. It always settles: a
536
+ * module the config system has not been told about, or an extension that is not mounted,
537
+ * contributes no overrides.
530
538
  *
531
539
  * @internal
532
540
  */
@@ -535,22 +543,29 @@ export function getTranslationOverrides(
535
543
  slotName?: string,
536
544
  extensionId?: string,
537
545
  ): Promise<Array<Record<string, Record<string, string>>>> {
546
+ // A module's store only ever loads its overrides once the module is registered with the config
547
+ // system, so waiting on one that is not registered would leave the i18next namespace pending.
538
548
  const promises = [
539
- whenStoreReady(
540
- getConfigStore(moduleName),
541
- (state) => Boolean(state.translationOverridesLoaded && state.config),
542
- (state) => (state.config!['Translation overrides'] ?? {}) as Record<string, Record<string, string>>,
543
- ),
549
+ moduleName in configInternalStore.getState().schemas
550
+ ? whenStoreReady(
551
+ getConfigStore(moduleName),
552
+ (state) => Boolean(state.translationOverridesLoaded && state.config),
553
+ (state) => (state.config!['Translation overrides'] ?? {}) as Record<string, Record<string, string>>,
554
+ )
555
+ : Promise.resolve({}),
544
556
  ];
545
557
 
546
558
  if (slotName && extensionId) {
547
- promises.push(
548
- whenStoreReady(
549
- getExtensionConfig(slotName, extensionId),
550
- (state) => Boolean(state.loaded && state.config),
551
- (state) => (state.config!['Translation overrides'] ?? {}) as Record<string, Record<string, string>>,
552
- ),
553
- );
559
+ // Read once rather than waited for: an extension's config entry exists only while that extension
560
+ // is registered as mounted, so a wait started in the gap never settles and leaves the i18next
561
+ // namespace pending.
562
+ const extensionConfig = getExtensionConfigFromStore(getExtensionsConfigStore().getState(), slotName, extensionId);
563
+ const overrides =
564
+ extensionConfig.loaded && extensionConfig.config
565
+ ? ((extensionConfig.config['Translation overrides'] ?? {}) as Record<string, Record<string, string>>)
566
+ : {};
567
+
568
+ promises.push(Promise.resolve(overrides));
554
569
  }
555
570
 
556
571
  return Promise.all(promises);
@@ -1192,15 +1207,8 @@ function hasObjectSchema(elementsSchema: unknown): elementsSchema is ConfigSchem
1192
1207
  function isOrdinaryObject(value) {
1193
1208
  return typeof value === 'object' && !Array.isArray(value) && value !== null;
1194
1209
  }
1195
- /** Keep track of which validation errors we have displayed. Each one should only be displayed once. */
1196
- let displayedValidationMessages = new Set<string>();
1197
-
1198
1210
  function logError(keyPath: string, message: string) {
1199
1211
  const key = `${keyPath}:::${message}`;
1200
- // technically, this should not be possible, but because of how things wind-up transpiled, this isn't impossible
1201
- if (!displayedValidationMessages) {
1202
- displayedValidationMessages = new Set<string>();
1203
- }
1204
1212
 
1205
1213
  if (!displayedValidationMessages.has(key)) {
1206
1214
  console.error(message);
@@ -0,0 +1,47 @@
1
+ // The config system validates the temporary config restored from local storage while the module is
2
+ // still initializing. This covers that path, which is the only one where `logError` can run before the
3
+ // rest of the module body has been evaluated.
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+
6
+ const temporaryConfigKey = 'openmrs:temporaryConfig';
7
+
8
+ beforeEach(() => {
9
+ vi.resetModules();
10
+ localStorage.clear();
11
+ });
12
+
13
+ afterEach(() => {
14
+ localStorage.clear();
15
+ vi.restoreAllMocks();
16
+ });
17
+
18
+ describe('loading with a temporary config saved by the implementer tools', () => {
19
+ it('reports an invalid extension slot config instead of failing to load', async () => {
20
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
21
+ localStorage.setItem(
22
+ temporaryConfigKey,
23
+ JSON.stringify({ '@openmrs/esm-some-app': { extensionSlots: { 'a-slot': { notAKey: ['x'] } } } }),
24
+ );
25
+
26
+ // The failure this covers happened here, at module evaluation: validation ran before the module
27
+ // body reached the declaration `logError` reads, so the whole framework failed to initialize with
28
+ // "can't access lexical declaration 'displayedValidationMessages' before initialization".
29
+ await expect(import('./module-config')).resolves.toBeDefined();
30
+
31
+ expect(error.mock.calls.flat().join(' ')).toContain("contains invalid keys 'notAKey'");
32
+ });
33
+
34
+ it('still loads when the temporary config is malformed in a way validation cannot describe', async () => {
35
+ vi.spyOn(console, 'error').mockImplementation(() => {});
36
+ localStorage.setItem(
37
+ temporaryConfigKey,
38
+ JSON.stringify({ '@openmrs/esm-some-app': { extensionSlots: { 'a-slot': { add: 'not-an-array' } } } }),
39
+ );
40
+
41
+ await expect(import('./module-config')).resolves.toBeDefined();
42
+ });
43
+
44
+ it('loads normally when there is no temporary config', async () => {
45
+ await expect(import('./module-config')).resolves.toBeDefined();
46
+ });
47
+ });
@@ -1,5 +1,4 @@
1
1
  import { createGlobalStore, getGlobalStore, registerGlobalStore } from '@openmrs/esm-state';
2
- import { shallowEqual } from '@openmrs/esm-utils';
3
2
  import { type StoreApi } from 'zustand';
4
3
  import { createStore } from 'zustand/vanilla';
5
4
  import type { Config, ConfigObject, ConfigSchema, ExtensionSlotConfig, ProvidedConfig } from '../types';
@@ -178,59 +177,6 @@ export function getExtensionsConfigStore() {
178
177
  });
179
178
  }
180
179
 
181
- /**
182
- * The read-only part of a store's API.
183
- * @internal
184
- */
185
- export type ReadableStore<T> = Pick<StoreApi<T>, 'getInitialState' | 'getState' | 'subscribe'>;
186
-
187
- /**
188
- * A read-only view of one extension instance's config within a slot. Read-only because these
189
- * configs are derived from the config system's inputs, so a write here would be overwritten by the
190
- * next recomputation; provide a config or set a temporary one instead.
191
- *
192
- * @internal
193
- */
194
- export function getExtensionConfig(
195
- slotName: string,
196
- extensionId: string,
197
- ): ReadableStore<Omit<ConfigStore, 'translationOverridesLoaded'>> {
198
- if (
199
- typeof slotName !== 'string' ||
200
- typeof extensionId !== 'string' ||
201
- slotName === '__proto__' ||
202
- extensionId === '__proto__' ||
203
- slotName === 'constructor' ||
204
- extensionId === 'constructor' ||
205
- slotName === 'prototype' ||
206
- extensionId === 'prototype'
207
- ) {
208
- throw new Error('Attempted to call `getExtensionConfig()` with invalid argument');
209
- }
210
-
211
- const extensionConfigStore = getExtensionsConfigStore();
212
- const selector = (configStore: ExtensionsConfigStore) => configStore.configs[slotName]?.[extensionId];
213
-
214
- return {
215
- getInitialState() {
216
- return selector(extensionConfigStore.getInitialState());
217
- },
218
- getState() {
219
- return selector(extensionConfigStore.getState()) ?? { loaded: false, config: null };
220
- },
221
- subscribe(listener) {
222
- return extensionConfigStore.subscribe((state, prevState) => {
223
- const newState = selector(state);
224
- const oldState = selector(prevState);
225
-
226
- if (!shallowEqual(newState, oldState)) {
227
- listener(newState, oldState);
228
- }
229
- });
230
- },
231
- };
232
- }
233
-
234
180
  /** @internal */
235
181
  export function getExtensionConfigFromStore(state: ExtensionsConfigStore, slotName: string, extensionId: string) {
236
182
  const extensionConfig = state.configs[slotName]?.[extensionId];