@openmrs/esm-config 8.0.1-pre.3439 → 8.0.1-pre.3457

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 (194.74ms)
1
+ [0] Successfully compiled: 8 files with swc (148.75ms)
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
@@ -90,3 +90,12 @@ export declare function processConfig(schema: ConfigSchema, providedConfig: Conf
90
90
  * @internal
91
91
  */
92
92
  export declare function clearConfigErrors(keyPath?: string): void;
93
+ /**
94
+ * Cleans up all config store subscriptions and re-establishes them. This is primarily
95
+ * useful for testing, where subscriptions set up at module load time need to be cleared
96
+ * between tests to prevent infinite update loops. After clearing, subscriptions are
97
+ * re-established so the config system continues to work normally.
98
+ *
99
+ * @internal
100
+ */
101
+ export declare function resetConfigSystem(): void;
@@ -1,8 +1,8 @@
1
- /** @module @category Config */ import { clone, reduce, mergeDeepRight, equals, omit } from "ramda";
1
+ /** @module @category Config */ import { clone, equals, reduce, mergeDeepRight, omit } from "ramda";
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 { configInternalStore, configExtensionStore, getConfigStore, getExtensionConfig, getExtensionsConfigStore, implementerToolsConfigStore, temporaryConfigStore, getExtensionSlotsConfigStore } from "./state.js";
5
+ import { configExtensionStore, configInternalStore, getConfigStore, getExtensionConfig, getExtensionSlotsConfigStore, getExtensionsConfigStore, implementerToolsConfigStore, temporaryConfigStore } from "./state.js";
6
6
  /**
7
7
  * Store setup
8
8
  *
@@ -22,25 +22,31 @@ import { configInternalStore, configExtensionStore, getConfigStore, getExtension
22
22
  * (or are supposed to be), other than the fact that they all `setState`
23
23
  * store values at the end. `computeExtensionConfigs` calls `getGlobalStore`,
24
24
  * which creates stores.
25
- */ computeModuleConfig(configInternalStore.getState(), temporaryConfigStore.getState());
26
- configInternalStore.subscribe((configState)=>computeModuleConfig(configState, temporaryConfigStore.getState()));
27
- temporaryConfigStore.subscribe((tempConfigState)=>computeModuleConfig(configInternalStore.getState(), tempConfigState));
28
- computeImplementerToolsConfig(configInternalStore.getState(), temporaryConfigStore.getState());
29
- configInternalStore.subscribe((configState)=>computeImplementerToolsConfig(configState, temporaryConfigStore.getState()));
30
- temporaryConfigStore.subscribe((tempConfigState)=>computeImplementerToolsConfig(configInternalStore.getState(), tempConfigState));
31
- computeExtensionSlotConfigs(configInternalStore.getState(), temporaryConfigStore.getState());
32
- configInternalStore.subscribe((configState)=>computeExtensionSlotConfigs(configState, temporaryConfigStore.getState()));
33
- temporaryConfigStore.subscribe((tempConfigState)=>computeExtensionSlotConfigs(configInternalStore.getState(), tempConfigState));
34
- computeExtensionConfigs(configInternalStore.getState(), configExtensionStore.getState(), temporaryConfigStore.getState());
35
- configInternalStore.subscribe((configState)=>{
36
- computeExtensionConfigs(configState, configExtensionStore.getState(), temporaryConfigStore.getState());
37
- });
38
- configExtensionStore.subscribe((extensionState)=>{
39
- computeExtensionConfigs(configInternalStore.getState(), extensionState, temporaryConfigStore.getState());
40
- });
41
- temporaryConfigStore.subscribe((tempConfigState)=>{
42
- computeExtensionConfigs(configInternalStore.getState(), configExtensionStore.getState(), tempConfigState);
43
- });
25
+ */ // Store unsubscribe functions to allow cleanup (e.g., in tests or hot module reloading)
26
+ const configSubscriptions = [];
27
+ /**
28
+ * Recomputes all configuration derived stores based on current state of input stores.
29
+ * Called whenever any input store (configInternalStore, temporaryConfigStore, configExtensionStore) changes.
30
+ */ function recomputeAllConfigs() {
31
+ const configState = configInternalStore.getState();
32
+ const tempConfigState = temporaryConfigStore.getState();
33
+ const extensionState = configExtensionStore.getState();
34
+ computeModuleConfig(configState, tempConfigState);
35
+ computeImplementerToolsConfig(configState, tempConfigState);
36
+ computeExtensionSlotConfigs(configState, tempConfigState);
37
+ computeExtensionConfigs(configState, extensionState, tempConfigState);
38
+ }
39
+ function setupConfigSubscriptions() {
40
+ // Initial computation
41
+ recomputeAllConfigs();
42
+ // Subscribe to all input stores with a single handler
43
+ // This ensures we only recompute once even if multiple stores change simultaneously
44
+ configSubscriptions.push(configInternalStore.subscribe(recomputeAllConfigs));
45
+ configSubscriptions.push(temporaryConfigStore.subscribe(recomputeAllConfigs));
46
+ configSubscriptions.push(configExtensionStore.subscribe(recomputeAllConfigs));
47
+ }
48
+ // Set up subscriptions at module load time
49
+ setupConfigSubscriptions();
44
50
  function computeModuleConfig(state, tempState) {
45
51
  for (let moduleName of Object.keys(state.schemas)){
46
52
  // At this point the schema could be either just the implicit schema or the actually
@@ -50,21 +56,23 @@ function computeModuleConfig(state, tempState) {
50
56
  // available, which as of this writing blocks the schema definition from occurring
51
57
  // for modules loaded based on their extensions.
52
58
  const moduleStore = getConfigStore(moduleName);
59
+ let newState;
53
60
  if (state.moduleLoaded[moduleName]) {
54
61
  const config = getConfigForModule(moduleName, state, tempState);
55
- moduleStore.setState({
62
+ newState = {
56
63
  translationOverridesLoaded: true,
57
64
  loaded: true,
58
65
  config
59
- });
66
+ };
60
67
  } else {
61
68
  const config = getConfigForModuleImplicitSchema(moduleName, state, tempState);
62
- moduleStore.setState({
69
+ newState = {
63
70
  translationOverridesLoaded: true,
64
71
  loaded: false,
65
72
  config
66
- });
73
+ };
67
74
  }
75
+ moduleStore.setState(newState);
68
76
  }
69
77
  }
70
78
  function computeExtensionSlotConfigs(state, tempState) {
@@ -84,15 +92,20 @@ function computeExtensionSlotConfigs(state, tempState) {
84
92
  ...newSlotStoreEntries
85
93
  }
86
94
  };
87
- if (!equals(oldState, newState)) {
95
+ if (!equals(oldState.slots, newState.slots)) {
88
96
  slotStore.setState(newState);
89
97
  }
90
98
  }
91
99
  function computeImplementerToolsConfig(state, tempConfigState) {
100
+ const oldState = implementerToolsConfigStore.getState();
92
101
  const config = getImplementerToolsConfig(state, tempConfigState);
93
- implementerToolsConfigStore.setState({
102
+ const newState = {
94
103
  config
95
- });
104
+ };
105
+ // Use deep equality on the actual config content, not the wrapper object
106
+ if (!equals(oldState.config, newState.config)) {
107
+ implementerToolsConfigStore.setState(newState);
108
+ }
96
109
  }
97
110
  function computeExtensionConfigs(configState, extensionState, tempConfigState) {
98
111
  const configs = {};
@@ -100,17 +113,23 @@ function computeExtensionConfigs(configState, extensionState, tempConfigState) {
100
113
  // it contains is mounted.
101
114
  for (let extension of extensionState.mountedExtensions){
102
115
  const config = computeExtensionConfig(extension.slotModuleName, extension.extensionModuleName, extension.slotName, extension.extensionId, configState, tempConfigState);
103
- configs[extension.slotName] = {
104
- ...configs[extension.slotName],
105
- [extension.extensionId]: {
106
- config,
107
- loaded: true
108
- }
116
+ if (!configs[extension.slotName]) {
117
+ configs[extension.slotName] = {};
118
+ }
119
+ configs[extension.slotName][extension.extensionId] = {
120
+ config,
121
+ loaded: true
109
122
  };
110
123
  }
111
- getExtensionsConfigStore().setState({
124
+ const extensionsConfigStore = getExtensionsConfigStore();
125
+ const oldState = extensionsConfigStore.getState();
126
+ const newState = {
112
127
  configs
113
- });
128
+ };
129
+ // Use deep equality to only update if configs actually changed
130
+ if (!equals(oldState.configs, newState.configs)) {
131
+ extensionsConfigStore.setState(newState);
132
+ }
114
133
  }
115
134
  /*
116
135
  * API
@@ -751,6 +770,18 @@ function logError(keyPath, message) {
751
770
  displayedValidationMessages.clear();
752
771
  }
753
772
  }
773
+ /**
774
+ * Cleans up all config store subscriptions and re-establishes them. This is primarily
775
+ * useful for testing, where subscriptions set up at module load time need to be cleared
776
+ * between tests to prevent infinite update loops. After clearing, subscriptions are
777
+ * re-established so the config system continues to work normally.
778
+ *
779
+ * @internal
780
+ */ export function resetConfigSystem() {
781
+ configSubscriptions.forEach((unsubscribe)=>unsubscribe());
782
+ configSubscriptions.length = 0;
783
+ setupConfigSubscriptions();
784
+ }
754
785
  /**
755
786
  * Copied over from esm-extensions. It rightly belongs to that module, but esm-config
756
787
  * cannot depend on esm-extensions.
@@ -782,6 +813,11 @@ function logError(keyPath, message) {
782
813
  _description: 'The privilege(s) the user must have to use this extension',
783
814
  _type: Type.Array,
784
815
  _default: []
816
+ },
817
+ expression: {
818
+ _description: 'The expression that determines whether the extension is displayed',
819
+ _type: Type.String,
820
+ _default: undefined
785
821
  }
786
822
  },
787
823
  ...translationOverridesSchema
@@ -35,7 +35,7 @@ function initializeConfigStore() {
35
35
  };
36
36
  }
37
37
  /** @internal */ export function getConfigStore(moduleName) {
38
- // We use a store for each module's config, named `config-${moduleName}`
38
+ // We use a store for each module's config, named `config-module-${moduleName}`
39
39
  return getGlobalStore(`config-module-${moduleName}`, initializeConfigStore());
40
40
  }
41
41
  /** @internal */ export function getExtensionSlotsConfigStore() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-config",
3
- "version": "8.0.1-pre.3439",
3
+ "version": "8.0.1-pre.3457",
4
4
  "license": "MPL-2.0",
5
5
  "description": "A configuration library for the OpenMRS Single-Spa framework.",
6
6
  "type": "module",
@@ -61,9 +61,9 @@
61
61
  "single-spa": "6.x"
62
62
  },
63
63
  "devDependencies": {
64
- "@openmrs/esm-globals": "8.0.1-pre.3439",
65
- "@openmrs/esm-state": "8.0.1-pre.3439",
66
- "@openmrs/esm-utils": "8.0.1-pre.3439",
64
+ "@openmrs/esm-globals": "8.0.1-pre.3457",
65
+ "@openmrs/esm-state": "8.0.1-pre.3457",
66
+ "@openmrs/esm-utils": "8.0.1-pre.3457",
67
67
  "@swc/cli": "^0.7.7",
68
68
  "@swc/core": "^1.11.29",
69
69
  "@types/ramda": "^0.26.44",
@@ -1031,6 +1031,13 @@ describe('implementer tools config', () => {
1031
1031
  _type: Type.Array,
1032
1032
  _value: [],
1033
1033
  },
1034
+ expression: {
1035
+ _default: undefined,
1036
+ _description: expect.any(String),
1037
+ _source: 'default',
1038
+ _type: Type.String,
1039
+ _value: undefined,
1040
+ },
1034
1041
  },
1035
1042
  'Translation overrides': {
1036
1043
  _default: {},
@@ -1261,7 +1268,7 @@ describe('extension config', () => {
1261
1268
  expect(result).toStrictEqual({
1262
1269
  bar: 'qux',
1263
1270
  baz: 'bazzy',
1264
- 'Display conditions': { privileges: [] },
1271
+ 'Display conditions': { expression: undefined, privileges: [] },
1265
1272
  'Translation overrides': {},
1266
1273
  });
1267
1274
  expect(console.error).not.toHaveBeenCalled();
@@ -1284,7 +1291,7 @@ describe('extension config', () => {
1284
1291
  expect(result).toStrictEqual({
1285
1292
  bar: 'qux',
1286
1293
  baz: 'quiz',
1287
- 'Display conditions': { privileges: [] },
1294
+ 'Display conditions': { expression: undefined, privileges: [] },
1288
1295
  'Translation overrides': {},
1289
1296
  });
1290
1297
  expect(console.error).not.toHaveBeenCalled();
@@ -1317,10 +1324,9 @@ describe('extension config', () => {
1317
1324
  const result = getExtensionConfig('barSlot', 'fooExt').getState().config;
1318
1325
  expect(result).toStrictEqual({
1319
1326
  qux: 'quxolotl',
1320
- 'Display conditions': { privileges: [] },
1327
+ 'Display conditions': { expression: undefined, privileges: [] },
1321
1328
  'Translation overrides': {},
1322
1329
  });
1323
- expect(console.error).not.toHaveBeenCalled();
1324
1330
  });
1325
1331
 
1326
1332
  it("uses the 'configure' config if one is present, with extension config schema", () => {
@@ -1343,7 +1349,7 @@ describe('extension config', () => {
1343
1349
  const result = getExtensionConfig('barSlot', 'fooExt#id2').getState().config;
1344
1350
  expect(result).toStrictEqual({
1345
1351
  qux: 'quxotic',
1346
- 'Display conditions': { privileges: [] },
1352
+ 'Display conditions': { expression: undefined, privileges: [] },
1347
1353
  'Translation overrides': {},
1348
1354
  });
1349
1355
  });
@@ -1,19 +1,19 @@
1
1
  /** @module @category Config */
2
- import { clone, reduce, mergeDeepRight, equals, omit } from 'ramda';
2
+ import { clone, equals, reduce, mergeDeepRight, omit } from 'ramda';
3
3
  import type { Config, ConfigObject, ConfigSchema, ExtensionSlotConfig } from '../types';
4
4
  import { Type } from '../types';
5
5
  import { isArray, isBoolean, isUuid, isNumber, isObject, isString } from '../validators/type-validators';
6
6
  import { validator } from '../validators/validator';
7
7
  import { type ConfigExtensionStore, type ConfigInternalStore, type ConfigStore } from './state';
8
8
  import {
9
- configInternalStore,
10
9
  configExtensionStore,
10
+ configInternalStore,
11
11
  getConfigStore,
12
12
  getExtensionConfig,
13
+ getExtensionSlotsConfigStore,
13
14
  getExtensionsConfigStore,
14
15
  implementerToolsConfigStore,
15
16
  temporaryConfigStore,
16
- getExtensionSlotsConfigStore,
17
17
  } from './state';
18
18
  import { type TemporaryConfigStore } from '..';
19
19
 
@@ -37,42 +37,37 @@ import { type TemporaryConfigStore } from '..';
37
37
  * store values at the end. `computeExtensionConfigs` calls `getGlobalStore`,
38
38
  * which creates stores.
39
39
  */
40
- computeModuleConfig(configInternalStore.getState(), temporaryConfigStore.getState());
41
- configInternalStore.subscribe((configState) => computeModuleConfig(configState, temporaryConfigStore.getState()));
42
- temporaryConfigStore.subscribe((tempConfigState) =>
43
- computeModuleConfig(configInternalStore.getState(), tempConfigState),
44
- );
45
-
46
- computeImplementerToolsConfig(configInternalStore.getState(), temporaryConfigStore.getState());
47
- configInternalStore.subscribe((configState) =>
48
- computeImplementerToolsConfig(configState, temporaryConfigStore.getState()),
49
- );
50
- temporaryConfigStore.subscribe((tempConfigState) =>
51
- computeImplementerToolsConfig(configInternalStore.getState(), tempConfigState),
52
- );
53
-
54
- computeExtensionSlotConfigs(configInternalStore.getState(), temporaryConfigStore.getState());
55
- configInternalStore.subscribe((configState) =>
56
- computeExtensionSlotConfigs(configState, temporaryConfigStore.getState()),
57
- );
58
- temporaryConfigStore.subscribe((tempConfigState) =>
59
- computeExtensionSlotConfigs(configInternalStore.getState(), tempConfigState),
60
- );
61
-
62
- computeExtensionConfigs(
63
- configInternalStore.getState(),
64
- configExtensionStore.getState(),
65
- temporaryConfigStore.getState(),
66
- );
67
- configInternalStore.subscribe((configState) => {
68
- computeExtensionConfigs(configState, configExtensionStore.getState(), temporaryConfigStore.getState());
69
- });
70
- configExtensionStore.subscribe((extensionState) => {
71
- computeExtensionConfigs(configInternalStore.getState(), extensionState, temporaryConfigStore.getState());
72
- });
73
- temporaryConfigStore.subscribe((tempConfigState) => {
74
- computeExtensionConfigs(configInternalStore.getState(), configExtensionStore.getState(), tempConfigState);
75
- });
40
+ // Store unsubscribe functions to allow cleanup (e.g., in tests or hot module reloading)
41
+ const configSubscriptions: Array<() => void> = [];
42
+
43
+ /**
44
+ * Recomputes all configuration derived stores based on current state of input stores.
45
+ * Called whenever any input store (configInternalStore, temporaryConfigStore, configExtensionStore) changes.
46
+ */
47
+ function recomputeAllConfigs() {
48
+ const configState = configInternalStore.getState();
49
+ const tempConfigState = temporaryConfigStore.getState();
50
+ const extensionState = configExtensionStore.getState();
51
+
52
+ computeModuleConfig(configState, tempConfigState);
53
+ computeImplementerToolsConfig(configState, tempConfigState);
54
+ computeExtensionSlotConfigs(configState, tempConfigState);
55
+ computeExtensionConfigs(configState, extensionState, tempConfigState);
56
+ }
57
+
58
+ function setupConfigSubscriptions() {
59
+ // Initial computation
60
+ recomputeAllConfigs();
61
+
62
+ // Subscribe to all input stores with a single handler
63
+ // This ensures we only recompute once even if multiple stores change simultaneously
64
+ configSubscriptions.push(configInternalStore.subscribe(recomputeAllConfigs));
65
+ configSubscriptions.push(temporaryConfigStore.subscribe(recomputeAllConfigs));
66
+ configSubscriptions.push(configExtensionStore.subscribe(recomputeAllConfigs));
67
+ }
68
+
69
+ // Set up subscriptions at module load time
70
+ setupConfigSubscriptions();
76
71
 
77
72
  function computeModuleConfig(state: ConfigInternalStore, tempState: TemporaryConfigStore) {
78
73
  for (let moduleName of Object.keys(state.schemas)) {
@@ -83,21 +78,24 @@ function computeModuleConfig(state: ConfigInternalStore, tempState: TemporaryCon
83
78
  // available, which as of this writing blocks the schema definition from occurring
84
79
  // for modules loaded based on their extensions.
85
80
  const moduleStore = getConfigStore(moduleName);
81
+ let newState;
86
82
  if (state.moduleLoaded[moduleName]) {
87
83
  const config = getConfigForModule(moduleName, state, tempState);
88
- moduleStore.setState({
84
+ newState = {
89
85
  translationOverridesLoaded: true,
90
86
  loaded: true,
91
87
  config,
92
- });
88
+ };
93
89
  } else {
94
90
  const config = getConfigForModuleImplicitSchema(moduleName, state, tempState);
95
- moduleStore.setState({
91
+ newState = {
96
92
  translationOverridesLoaded: true,
97
93
  loaded: false,
98
94
  config,
99
- });
95
+ };
100
96
  }
97
+
98
+ moduleStore.setState(newState);
101
99
  }
102
100
  }
103
101
 
@@ -109,14 +107,21 @@ function computeExtensionSlotConfigs(state: ConfigInternalStore, tempState: Temp
109
107
  const slotStore = getExtensionSlotsConfigStore();
110
108
  const oldState = slotStore.getState();
111
109
  const newState = { slots: { ...oldState.slots, ...newSlotStoreEntries } };
112
- if (!equals(oldState, newState)) {
110
+
111
+ if (!equals(oldState.slots, newState.slots)) {
113
112
  slotStore.setState(newState);
114
113
  }
115
114
  }
116
115
 
117
116
  function computeImplementerToolsConfig(state: ConfigInternalStore, tempConfigState: TemporaryConfigStore) {
117
+ const oldState = implementerToolsConfigStore.getState();
118
118
  const config = getImplementerToolsConfig(state, tempConfigState);
119
- implementerToolsConfigStore.setState({ config });
119
+ const newState = { config };
120
+
121
+ // Use deep equality on the actual config content, not the wrapper object
122
+ if (!equals(oldState.config, newState.config)) {
123
+ implementerToolsConfigStore.setState(newState);
124
+ }
120
125
  }
121
126
 
122
127
  function computeExtensionConfigs(
@@ -137,12 +142,19 @@ function computeExtensionConfigs(
137
142
  tempConfigState,
138
143
  );
139
144
 
140
- configs[extension.slotName] = {
141
- ...configs[extension.slotName],
142
- [extension.extensionId]: { config, loaded: true },
143
- };
145
+ if (!configs[extension.slotName]) {
146
+ configs[extension.slotName] = {};
147
+ }
148
+ configs[extension.slotName][extension.extensionId] = { config, loaded: true };
149
+ }
150
+ const extensionsConfigStore = getExtensionsConfigStore();
151
+ const oldState = extensionsConfigStore.getState();
152
+ const newState = { configs };
153
+
154
+ // Use deep equality to only update if configs actually changed
155
+ if (!equals(oldState.configs, newState.configs)) {
156
+ extensionsConfigStore.setState(newState);
144
157
  }
145
- getExtensionsConfigStore().setState({ configs });
146
158
  }
147
159
 
148
160
  /*
@@ -862,6 +874,20 @@ export function clearConfigErrors(keyPath?: string) {
862
874
  }
863
875
  }
864
876
 
877
+ /**
878
+ * Cleans up all config store subscriptions and re-establishes them. This is primarily
879
+ * useful for testing, where subscriptions set up at module load time need to be cleared
880
+ * between tests to prevent infinite update loops. After clearing, subscriptions are
881
+ * re-established so the config system continues to work normally.
882
+ *
883
+ * @internal
884
+ */
885
+ export function resetConfigSystem() {
886
+ configSubscriptions.forEach((unsubscribe) => unsubscribe());
887
+ configSubscriptions.length = 0;
888
+ setupConfigSubscriptions();
889
+ }
890
+
865
891
  /**
866
892
  * Copied over from esm-extensions. It rightly belongs to that module, but esm-config
867
893
  * cannot depend on esm-extensions.
@@ -905,6 +931,11 @@ const implicitConfigSchema: ConfigSchema = {
905
931
  _type: Type.Array,
906
932
  _default: [],
907
933
  },
934
+ expression: {
935
+ _description: 'The expression that determines whether the extension is displayed',
936
+ _type: Type.String,
937
+ _default: undefined,
938
+ },
908
939
  },
909
940
  ...translationOverridesSchema,
910
941
  };
@@ -111,7 +111,7 @@ function initializeConfigStore() {
111
111
 
112
112
  /** @internal */
113
113
  export function getConfigStore(moduleName: string) {
114
- // We use a store for each module's config, named `config-${moduleName}`
114
+ // We use a store for each module's config, named `config-module-${moduleName}`
115
115
  return getGlobalStore<ConfigStore>(`config-module-${moduleName}`, initializeConfigStore());
116
116
  }
117
117