@openmrs/esm-config 11.0.1-pre.5776 → 11.0.1-pre.5799

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 (260.2ms)
1
+ [0] Successfully compiled: 8 files with swc (270.54ms)
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
@@ -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;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"}
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,QAMpD;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;AAioBD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,MAAM,QAUjD;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,SAYhC"}
@@ -178,8 +178,8 @@ function computeExtensionConfigs(configState, extensionState, tempConfigState, s
178
178
  const configs = {};
179
179
  let changed = false;
180
180
  let entryCount = 0;
181
- // We assume that the module schema has already been defined, since the extension
182
- // it contains is mounted.
181
+ // An extension can be mounted before its module loads, so the module schema may still be the
182
+ // implicit one; `computeExtensionConfig` holds back structural validation until it has started.
183
183
  for (let extension of extensionState.mountedExtensions){
184
184
  const previous = oldConfigs[extension.slotName]?.[extension.extensionId];
185
185
  let entry;
@@ -286,6 +286,10 @@ function computeExtensionConfigs(configState, extensionState, tempConfigState, s
286
286
  moduleLoaded: {
287
287
  ...state.moduleLoaded,
288
288
  [moduleName]: true
289
+ },
290
+ moduleStarted: {
291
+ ...state.moduleStarted,
292
+ [moduleName]: true
289
293
  }
290
294
  }));
291
295
  }
@@ -455,6 +459,9 @@ const extensionConfigCache = new Map();
455
459
  */ function computeExtensionConfig(slotModuleName, extensionModuleName, slotName, extensionId, configState, tempConfigState, slotConfig) {
456
460
  const extensionName = getExtensionNameFromId(extensionId);
457
461
  const extensionConfigSchema = configState.schemas[extensionName];
462
+ // Not `moduleLoaded`: `defineConfigSchema` sets that while `startupApp` is still running, before it
463
+ // reaches `defineExtensionConfigSchema`, so the extension would be checked against the module schema.
464
+ const extensionModuleStarted = Boolean(configState.moduleStarted[extensionModuleName]);
458
465
  // JSON string of these elements should be unique-per-extension
459
466
  const cacheKey = JSON.stringify([
460
467
  slotName,
@@ -463,7 +470,7 @@ const extensionConfigCache = new Map();
463
470
  extensionModuleName
464
471
  ]);
465
472
  const cached = extensionConfigCache.get(cacheKey);
466
- if (cached && cached.extensionSchema === extensionConfigSchema && cached.extensionModuleSchema === configState.schemas[extensionModuleName] && cached.providedConfigs === configState.providedConfigs && cached.temporaryConfig === tempConfigState.config) {
473
+ if (cached && cached.extensionSchema === extensionConfigSchema && cached.extensionModuleSchema === configState.schemas[extensionModuleName] && cached.extensionModuleStarted === extensionModuleStarted && cached.providedConfigs === configState.providedConfigs && cached.temporaryConfig === tempConfigState.config) {
467
474
  return cached.config;
468
475
  }
469
476
  const nameOfSchemaSource = extensionConfigSchema ? extensionName : extensionModuleName;
@@ -478,13 +485,17 @@ const extensionConfigCache = new Map();
478
485
  configOverride
479
486
  ]);
480
487
  const schema = extensionConfigSchema ?? configState.schemas[extensionModuleName];
481
- validateStructure(schema, combinedConfig, nameOfSchemaSource);
488
+ // only validate structure once the schema has loaded
489
+ if (extensionConfigSchema || extensionModuleStarted) {
490
+ validateStructure(schema, combinedConfig, nameOfSchemaSource);
491
+ }
482
492
  const config = setDefaults(schema, combinedConfig);
483
493
  runAllValidatorsInConfigTree(schema, config, nameOfSchemaSource);
484
494
  delete config.extensionSlots;
485
495
  extensionConfigCache.set(cacheKey, {
486
496
  extensionSchema: extensionConfigSchema,
487
497
  extensionModuleSchema: configState.schemas[extensionModuleName],
498
+ extensionModuleStarted,
488
499
  providedConfigs: configState.providedConfigs,
489
500
  temporaryConfig: tempConfigState.config,
490
501
  config
@@ -16,6 +16,12 @@ export interface ConfigInternalStore {
16
16
  * whether the schema has actually been defined).
17
17
  */
18
18
  moduleLoaded: Record<string, boolean>;
19
+ /**
20
+ * Whether the app shell has finished starting each module (run its `startupApp`). Unlike
21
+ * `moduleLoaded`, `defineConfigSchema` does not set this, so it stays false while `startupApp`
22
+ * is still defining the module's extension schemas.
23
+ */
24
+ moduleStarted: Record<string, boolean>;
19
25
  }
20
26
  /**
21
27
  * @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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"}
@@ -3,7 +3,8 @@ import { createStore } from "zustand/vanilla";
3
3
  const configInternalStoreInitialValue = {
4
4
  providedConfigs: [],
5
5
  schemas: {},
6
- moduleLoaded: {}
6
+ moduleLoaded: {},
7
+ moduleStarted: {}
7
8
  };
8
9
  /**
9
10
  * @internal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-config",
3
- "version": "11.0.1-pre.5776",
3
+ "version": "11.0.1-pre.5799",
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": "^11.0.1-pre.5776",
61
- "@openmrs/esm-state": "^11.0.1-pre.5776",
62
- "@openmrs/esm-utils": "^11.0.1-pre.5776",
60
+ "@openmrs/esm-globals": "^11.0.1-pre.5799",
61
+ "@openmrs/esm-state": "^11.0.1-pre.5799",
62
+ "@openmrs/esm-utils": "^11.0.1-pre.5799",
63
63
  "single-spa": "6.x"
64
64
  },
65
65
  "devDependencies": {
66
- "@openmrs/esm-globals": "11.0.1-pre.5776",
67
- "@openmrs/esm-state": "11.0.1-pre.5776",
68
- "@openmrs/esm-utils": "11.0.1-pre.5776",
66
+ "@openmrs/esm-globals": "11.0.1-pre.5799",
67
+ "@openmrs/esm-state": "11.0.1-pre.5799",
68
+ "@openmrs/esm-utils": "11.0.1-pre.5799",
69
69
  "@swc/cli": "0.8.1",
70
70
  "@swc/core": "1.15.21",
71
71
  "@types/lodash-es": "^4.17.12",
@@ -18,7 +18,7 @@ describe('a configuration that cannot be derived', () => {
18
18
  beforeEach(() => {
19
19
  consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
20
20
  temporaryConfigStore.setState({ config: {} });
21
- configInternalStore.setState({ providedConfigs: [], schemas: {}, moduleLoaded: {} });
21
+ configInternalStore.setState({ providedConfigs: [], schemas: {}, moduleLoaded: {}, moduleStarted: {} });
22
22
  implementerToolsConfigStore.setState({ config: {}, derivationError: undefined });
23
23
  resetConfigSystem();
24
24
  defineConfigSchema('esm-probe', { thing: { _type: Type.Object, _default: {} } });
@@ -28,7 +28,7 @@ type ImplementerToolsTree = Record<string, ConfigNode>;
28
28
  describe('implementer tools config', () => {
29
29
  beforeEach(() => {
30
30
  temporaryConfigStore.setState({ config: {} });
31
- configInternalStore.setState({ providedConfigs: [], schemas: {}, moduleLoaded: {} });
31
+ configInternalStore.setState({ providedConfigs: [], schemas: {}, moduleLoaded: {}, moduleStarted: {} });
32
32
  resetConfigSystem();
33
33
  defineConfigSchema('esm-flintstone', {
34
34
  label: { _type: Type.String, _default: 'default-label', _description: 'x' },
@@ -1590,6 +1590,105 @@ describe('extension config', () => {
1590
1590
  });
1591
1591
  });
1592
1592
 
1593
+ // `renderExtension` registers an extension before awaiting its bundle, so its config is derived
1594
+ // while the module is known to the config system only by the implicit schema.
1595
+ describe('extension config of a module that has not loaded yet', () => {
1596
+ beforeEach(() => {
1597
+ console.error = vi.fn();
1598
+ });
1599
+
1600
+ afterEach(resetAll);
1601
+
1602
+ const mount = (extensionModuleName: string, extensionId: string) =>
1603
+ configExtensionStore.setState({
1604
+ mountedExtensions: [{ slotModuleName: 'slot-mod', extensionModuleName, slotName: 'early-slot', extensionId }],
1605
+ });
1606
+
1607
+ it('does not call a key unknown before the module defines its schema', () => {
1608
+ Config.provide({ 'early-mod': { thud: 'provided' } });
1609
+ Config.registerModuleWithConfigSystem('early-mod');
1610
+ mount('early-mod', 'earlyExt');
1611
+
1612
+ expect(console.error).not.toHaveBeenCalled();
1613
+
1614
+ Config.defineConfigSchema('early-mod', { thud: { _default: 'default' } });
1615
+ Config.registerModuleLoad('early-mod');
1616
+
1617
+ expect(console.error).not.toHaveBeenCalled();
1618
+ expect(currentExtensionConfig('early-slot', 'earlyExt').config.thud).toBe('provided');
1619
+ });
1620
+
1621
+ it('still calls a key unknown once the module has defined a schema without it', () => {
1622
+ Config.provide({ 'late-mod': { thud: 'provided', plugh: 'unknown' } });
1623
+ Config.registerModuleWithConfigSystem('late-mod');
1624
+ mount('late-mod', 'lateExt');
1625
+
1626
+ expect(console.error).not.toHaveBeenCalled();
1627
+
1628
+ Config.defineConfigSchema('late-mod', { thud: { _default: 'default' } });
1629
+ Config.registerModuleLoad('late-mod');
1630
+
1631
+ expect(console.error).toHaveBeenCalledTimes(1);
1632
+ expect(console.error).toHaveBeenCalledWith(expect.stringMatching(/unknown config key 'late-mod.plugh' provided/i));
1633
+ });
1634
+
1635
+ it('still calls a key unknown once a module without a schema has loaded', () => {
1636
+ // Configured through the slot so that only the extension config, not the module config, sees it.
1637
+ Config.provide({
1638
+ 'slot-mod': { extensionSlots: { 'early-slot': { configure: { bareExt: { xyzzy: 'unknown' } } } } },
1639
+ });
1640
+ Config.registerModuleWithConfigSystem('bare-mod');
1641
+ mount('bare-mod', 'bareExt');
1642
+
1643
+ expect(console.error).not.toHaveBeenCalled();
1644
+
1645
+ Config.registerModuleLoad('bare-mod');
1646
+
1647
+ expect(console.error).toHaveBeenCalledWith(expect.stringMatching(/unknown config key 'bare-mod.xyzzy' provided/i));
1648
+ });
1649
+
1650
+ it('still reports a value of the wrong type once the module has defined its schema', () => {
1651
+ Config.provide({ 'typed-mod': { thud: 42 } });
1652
+ Config.registerModuleWithConfigSystem('typed-mod');
1653
+ mount('typed-mod', 'typedExt');
1654
+
1655
+ expect(console.error).not.toHaveBeenCalled();
1656
+
1657
+ // `defineConfigSchema` marks the module loaded, so the module config reports both errors here. The
1658
+ // extension's own structure check waits for `registerModuleLoad`, and its repeats are deduplicated.
1659
+ Config.defineConfigSchema('typed-mod', {
1660
+ thud: { _type: Type.String, _default: 'default', _validators: [validator((v) => v !== 42, 'must not be 42')] },
1661
+ });
1662
+ Config.registerModuleLoad('typed-mod');
1663
+
1664
+ expect(console.error).toHaveBeenCalledTimes(2);
1665
+ expect(console.error).toHaveBeenCalledWith(expect.stringMatching(/42 for typed-mod.thud: must be a string/));
1666
+ expect(console.error).toHaveBeenCalledWith(expect.stringMatching(/42 for typed-mod.thud: must not be 42/));
1667
+ });
1668
+
1669
+ it('does not call an extension schema key unknown while its module starts up', () => {
1670
+ Config.provide({
1671
+ 'slot-mod': { extensionSlots: { 'early-slot': { configure: { flagsExt: { quux: 'x', corge: 'unknown' } } } } },
1672
+ });
1673
+ Config.registerModuleWithConfigSystem('flags-mod');
1674
+ mount('flags-mod', 'flagsExt');
1675
+ Config.defineConfigSchema('flags-mod', { thud: { _default: 'd' } });
1676
+
1677
+ expect(console.error).not.toHaveBeenCalled();
1678
+
1679
+ // The extension's own schema is checked as soon as it is defined, without waiting for startup.
1680
+ Config.defineExtensionConfigSchema('flagsExt', { quux: { _default: 'd' } });
1681
+
1682
+ expect(console.error).toHaveBeenCalledTimes(1);
1683
+ expect(console.error).toHaveBeenCalledWith(expect.stringMatching(/unknown config key 'flagsExt.corge' provided/i));
1684
+ expect(currentExtensionConfig('early-slot', 'flagsExt').config.quux).toBe('x');
1685
+
1686
+ Config.registerModuleLoad('flags-mod');
1687
+
1688
+ expect(console.error).toHaveBeenCalledTimes(1);
1689
+ });
1690
+ });
1691
+
1593
1692
  describe('translation overrides', () => {
1594
1693
  it('allows obtaining translation overrides before schema is registered', async () => {
1595
1694
  console.error = vi.fn();
@@ -275,8 +275,8 @@ function computeExtensionConfigs(
275
275
  let changed = false;
276
276
  let entryCount = 0;
277
277
 
278
- // We assume that the module schema has already been defined, since the extension
279
- // it contains is mounted.
278
+ // An extension can be mounted before its module loads, so the module schema may still be the
279
+ // implicit one; `computeExtensionConfig` holds back structural validation until it has started.
280
280
  for (let extension of extensionState.mountedExtensions) {
281
281
  const previous = oldConfigs[extension.slotName]?.[extension.extensionId];
282
282
  let entry: ConfigStore | { config: ConfigObject; loaded: true };
@@ -406,6 +406,7 @@ export function registerModuleLoad(moduleName: string) {
406
406
  configInternalStore.setState((state) => ({
407
407
  ...state,
408
408
  moduleLoaded: { ...state.moduleLoaded, [moduleName]: true },
409
+ moduleStarted: { ...state.moduleStarted, [moduleName]: true },
409
410
  }));
410
411
  }
411
412
 
@@ -602,6 +603,7 @@ export function processConfig(schema: ConfigSchema, providedConfig: ConfigObject
602
603
  interface ExtensionConfigCacheEntry {
603
604
  extensionSchema: ConfigSchema | undefined;
604
605
  extensionModuleSchema: ConfigSchema | undefined;
606
+ extensionModuleStarted: boolean;
605
607
  providedConfigs: ConfigInternalStore['providedConfigs'];
606
608
  temporaryConfig: Config;
607
609
  config: ConfigObject;
@@ -634,6 +636,9 @@ function computeExtensionConfig(
634
636
  ) {
635
637
  const extensionName = getExtensionNameFromId(extensionId);
636
638
  const extensionConfigSchema = configState.schemas[extensionName];
639
+ // Not `moduleLoaded`: `defineConfigSchema` sets that while `startupApp` is still running, before it
640
+ // reaches `defineExtensionConfigSchema`, so the extension would be checked against the module schema.
641
+ const extensionModuleStarted = Boolean(configState.moduleStarted[extensionModuleName]);
637
642
  // JSON string of these elements should be unique-per-extension
638
643
  const cacheKey = JSON.stringify([slotName, extensionId, slotModuleName, extensionModuleName]);
639
644
  const cached = extensionConfigCache.get(cacheKey);
@@ -642,6 +647,7 @@ function computeExtensionConfig(
642
647
  cached &&
643
648
  cached.extensionSchema === extensionConfigSchema &&
644
649
  cached.extensionModuleSchema === configState.schemas[extensionModuleName] &&
650
+ cached.extensionModuleStarted === extensionModuleStarted &&
645
651
  cached.providedConfigs === configState.providedConfigs &&
646
652
  cached.temporaryConfig === tempConfigState.config
647
653
  ) {
@@ -656,7 +662,12 @@ function computeExtensionConfig(
656
662
  const extensionConfig = mergeConfigsFor(nameOfSchemaSource, providedConfigs);
657
663
  const combinedConfig = mergeConfigs([extensionConfig, sharedOverride, configOverride]);
658
664
  const schema = extensionConfigSchema ?? configState.schemas[extensionModuleName];
659
- validateStructure(schema, combinedConfig, nameOfSchemaSource);
665
+
666
+ // only validate structure once the schema has loaded
667
+ if (extensionConfigSchema || extensionModuleStarted) {
668
+ validateStructure(schema, combinedConfig, nameOfSchemaSource);
669
+ }
670
+
660
671
  const config = setDefaults(schema, combinedConfig);
661
672
  runAllValidatorsInConfigTree(schema, config, nameOfSchemaSource);
662
673
  delete config.extensionSlots;
@@ -664,6 +675,7 @@ function computeExtensionConfig(
664
675
  extensionConfigCache.set(cacheKey, {
665
676
  extensionSchema: extensionConfigSchema,
666
677
  extensionModuleSchema: configState.schemas[extensionModuleName],
678
+ extensionModuleStarted,
667
679
  providedConfigs: configState.providedConfigs,
668
680
  temporaryConfig: tempConfigState.config,
669
681
  config,
@@ -19,12 +19,19 @@ export interface ConfigInternalStore {
19
19
  * whether the schema has actually been defined).
20
20
  */
21
21
  moduleLoaded: Record<string, boolean>;
22
+ /**
23
+ * Whether the app shell has finished starting each module (run its `startupApp`). Unlike
24
+ * `moduleLoaded`, `defineConfigSchema` does not set this, so it stays false while `startupApp`
25
+ * is still defining the module's extension schemas.
26
+ */
27
+ moduleStarted: Record<string, boolean>;
22
28
  }
23
29
 
24
30
  const configInternalStoreInitialValue = {
25
31
  providedConfigs: [],
26
32
  schemas: {},
27
33
  moduleLoaded: {},
34
+ moduleStarted: {},
28
35
  };
29
36
 
30
37
  /**