@openmrs/esm-config 10.0.1-pre.5364 → 10.0.1-pre.5378

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 (345.21ms)
1
+ [0] Successfully compiled: 8 files with swc (225.96ms)
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;AAySxF;;;;;;;;;;;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;AAomBD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,MAAM,QAUjD;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,SAUhC"}
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;AA+SxF;;;;;;;;;;;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,SAUhC"}
@@ -7,12 +7,13 @@ import { configExtensionStore, configInternalStore, getConfigStore, getExtension
7
7
  * Deep merge two objects, with right-side values taking precedence.
8
8
  * Arrays are replaced entirely (not merged by index), matching the
9
9
  * behavior of ramda's mergeDeepRight.
10
- */ function mergeDeepReplace(left, right) {
11
- return mergeWith({}, left, right, (_objValue, srcValue)=>{
12
- if (Array.isArray(srcValue)) {
13
- return srcValue;
14
- }
15
- });
10
+ */ function replaceArrays(_objValue, srcValue) {
11
+ if (Array.isArray(srcValue)) {
12
+ return srcValue;
13
+ }
14
+ }
15
+ function mergeDeepReplace(left, right) {
16
+ return mergeWith({}, left, right, replaceArrays);
16
17
  }
17
18
  /**
18
19
  * Store setup
@@ -43,8 +44,9 @@ const configSubscriptions = [];
43
44
  const tempConfigState = temporaryConfigStore.getState();
44
45
  const extensionState = configExtensionStore.getState();
45
46
  computeModuleConfig(configState, tempConfigState);
46
- computeExtensionSlotConfigs(configState, tempConfigState);
47
- computeExtensionConfigs(configState, extensionState, tempConfigState);
47
+ const slotConfigs = getExtensionSlotConfigs(configState, tempConfigState);
48
+ computeExtensionSlotConfigs(slotConfigs);
49
+ computeExtensionConfigs(configState, extensionState, tempConfigState, slotConfigs);
48
50
  // Last, because a failure here must not skip the outputs above. Only kept current while the
49
51
  // implementer tools are watching it; otherwise derived on read.
50
52
  invalidateImplementerToolsConfig();
@@ -108,8 +110,7 @@ function computeModuleConfig(state, tempState) {
108
110
  }
109
111
  }
110
112
  }
111
- function computeExtensionSlotConfigs(state, tempState) {
112
- const slotConfigs = getExtensionSlotConfigs(state, tempState);
113
+ function computeExtensionSlotConfigs(slotConfigs) {
113
114
  const slotStore = getExtensionSlotsConfigStore();
114
115
  const oldSlots = slotStore.getState().slots;
115
116
  const slots = {};
@@ -148,7 +149,7 @@ function computeImplementerToolsConfig(state, tempConfigState) {
148
149
  implementerToolsConfigStore.setState(newState);
149
150
  }
150
151
  }
151
- function computeExtensionConfigs(configState, extensionState, tempConfigState) {
152
+ function computeExtensionConfigs(configState, extensionState, tempConfigState, slotConfigs) {
152
153
  const extensionsConfigStore = getExtensionsConfigStore();
153
154
  const oldConfigs = extensionsConfigStore.getState().configs;
154
155
  const configs = {};
@@ -157,7 +158,7 @@ function computeExtensionConfigs(configState, extensionState, tempConfigState) {
157
158
  // We assume that the module schema has already been defined, since the extension
158
159
  // it contains is mounted.
159
160
  for (let extension of extensionState.mountedExtensions){
160
- const config = computeExtensionConfig(extension.slotModuleName, extension.extensionModuleName, extension.slotName, extension.extensionId, configState, tempConfigState);
161
+ const config = computeExtensionConfig(extension.slotModuleName, extension.extensionModuleName, extension.slotName, extension.extensionId, configState, tempConfigState, slotConfigs[extension.slotName]);
161
162
  const previous = oldConfigs[extension.slotName]?.[extension.extensionId];
162
163
  // Entries keep their identity when the configuration behind them hasn't changed, so that
163
164
  // mounting one extension doesn't hand every other mounted extension a new config object.
@@ -410,7 +411,8 @@ const extensionConfigCache = new Map();
410
411
  * @param extensionModuleName The name of the module which defines the extension (and therefore the config schema)
411
412
  * @param slotName The name of the extension slot where the extension is mounted
412
413
  * @param extensionId The ID of the extension in its slot
413
- */ function computeExtensionConfig(slotModuleName, extensionModuleName, slotName, extensionId, configState, tempConfigState) {
414
+ * @param slotConfig The slot's configuration, gathered from every module that configures it
415
+ */ function computeExtensionConfig(slotModuleName, extensionModuleName, slotName, extensionId, configState, tempConfigState, slotConfig) {
414
416
  const extensionName = getExtensionNameFromId(extensionId);
415
417
  const extensionConfigSchema = configState.schemas[extensionName];
416
418
  // JSON string of these elements should be unique-per-extension
@@ -426,11 +428,13 @@ const extensionConfigCache = new Map();
426
428
  }
427
429
  const nameOfSchemaSource = extensionConfigSchema ? extensionName : extensionModuleName;
428
430
  const providedConfigs = getProvidedConfigs(configState, tempConfigState);
431
+ const sharedOverride = slotConfig?.configure?.[extensionId] ?? {};
429
432
  const slotModuleConfig = mergeConfigsFor(slotModuleName, providedConfigs);
430
433
  const configOverride = slotModuleConfig?.extensionSlots?.[slotName]?.configure?.[extensionId] ?? {};
431
434
  const extensionConfig = mergeConfigsFor(nameOfSchemaSource, providedConfigs);
432
435
  const combinedConfig = mergeConfigs([
433
436
  extensionConfig,
437
+ sharedOverride,
434
438
  configOverride
435
439
  ]);
436
440
  const schema = extensionConfigSchema ?? configState.schemas[extensionModuleName];
@@ -558,13 +562,26 @@ function getExtensionSlotConfigs(configState, tempConfigState) {
558
562
  return obj;
559
563
  }, {});
560
564
  validateAllExtensionSlotConfigs(slotConfigPerModule);
561
- const slotConfigs = Object.keys(slotConfigPerModule).reduce((obj, key)=>{
562
- obj = {
563
- ...obj,
564
- ...slotConfigPerModule[key]
565
- };
566
- return obj;
567
- }, {});
565
+ const slotConfigs = {};
566
+ const configureBlocks = {};
567
+ for (const configBySlotName of Object.values(slotConfigPerModule)){
568
+ for (const [slotName, config] of Object.entries(configBySlotName)){
569
+ slotConfigs[slotName] = config;
570
+ if (config.configure) {
571
+ var _configureBlocks, _slotName;
572
+ ((_configureBlocks = configureBlocks)[_slotName = slotName] ?? (_configureBlocks[_slotName] = [])).push(config.configure);
573
+ }
574
+ }
575
+ }
576
+ for (const [slotName, blocks] of Object.entries(configureBlocks)){
577
+ const configure = blocks.length === 1 ? blocks[0] : mergeConfigs(blocks);
578
+ if (slotConfigs[slotName].configure !== configure) {
579
+ slotConfigs[slotName] = {
580
+ ...slotConfigs[slotName],
581
+ configure
582
+ };
583
+ }
584
+ }
568
585
  return slotConfigs;
569
586
  }
570
587
  function validateAllExtensionSlotConfigs(slotConfigPerModule) {
@@ -687,8 +704,10 @@ function mergeConfigsFor(moduleName, allConfigs) {
687
704
  const allConfigsForModule = allConfigs.map(({ [moduleName]: c })=>c).filter((c)=>!!c);
688
705
  return mergeConfigs(allConfigsForModule);
689
706
  }
690
- function mergeConfigs(configs) {
691
- return configs.reduce((acc, config)=>mergeDeepReplace(acc, config), {});
707
+ /**
708
+ * Merges a whole list at once, accumulating into one destination.
709
+ */ function mergeConfigs(configs) {
710
+ return configs.reduce((acc, config)=>mergeWith(acc, config, replaceArrays), {});
692
711
  }
693
712
  /**
694
713
  * Recursively check the provided config tree to make sure that all
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-config",
3
- "version": "10.0.1-pre.5364",
3
+ "version": "10.0.1-pre.5378",
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.5364",
61
- "@openmrs/esm-state": "^10.0.1-pre.5364",
62
- "@openmrs/esm-utils": "^10.0.1-pre.5364",
60
+ "@openmrs/esm-globals": "^10.0.1-pre.5378",
61
+ "@openmrs/esm-state": "^10.0.1-pre.5378",
62
+ "@openmrs/esm-utils": "^10.0.1-pre.5378",
63
63
  "single-spa": "6.x"
64
64
  },
65
65
  "devDependencies": {
66
- "@openmrs/esm-globals": "10.0.1-pre.5364",
67
- "@openmrs/esm-state": "10.0.1-pre.5364",
68
- "@openmrs/esm-utils": "10.0.1-pre.5364",
66
+ "@openmrs/esm-globals": "10.0.1-pre.5378",
67
+ "@openmrs/esm-state": "10.0.1-pre.5378",
68
+ "@openmrs/esm-utils": "10.0.1-pre.5378",
69
69
  "@swc/cli": "0.8.1",
70
70
  "@swc/core": "1.15.21",
71
71
  "@types/lodash-es": "^4.17.12",
@@ -1341,6 +1341,99 @@ describe('extension config', () => {
1341
1341
  expect(console.error).not.toHaveBeenCalled();
1342
1342
  });
1343
1343
 
1344
+ // The module that renders an extension owns the slot it opens, which need not be the module an
1345
+ // implementer configures that slot from.
1346
+ it("uses the 'configure' config written by a module other than the one owning the slot", () => {
1347
+ updateConfigExtensionStore('fooExt#id6');
1348
+ const configureConfig = {
1349
+ 'ext-mod': { bar: 'qux' },
1350
+ 'other-mod': {
1351
+ extensionSlots: {
1352
+ barSlot: {
1353
+ configure: { 'fooExt#id6': { baz: 'quiz' } },
1354
+ },
1355
+ },
1356
+ },
1357
+ };
1358
+ Config.provide(configureConfig);
1359
+ const result = getExtensionConfig('barSlot', 'fooExt#id6').getState().config;
1360
+ expect(result).toStrictEqual({
1361
+ bar: 'qux',
1362
+ baz: 'quiz',
1363
+ 'Display conditions': { expression: undefined, privileges: [] },
1364
+ 'Translation overrides': {},
1365
+ });
1366
+ expect(console.error).not.toHaveBeenCalled();
1367
+ });
1368
+
1369
+ it("prefers the slot-owning module's 'configure' config to another module's", () => {
1370
+ updateConfigExtensionStore('fooExt#id7');
1371
+ const configureConfig = {
1372
+ 'slot-mod': {
1373
+ extensionSlots: {
1374
+ barSlot: {
1375
+ configure: { 'fooExt#id7': { baz: 'from-owner' } },
1376
+ },
1377
+ },
1378
+ },
1379
+ 'other-mod': {
1380
+ extensionSlots: {
1381
+ barSlot: {
1382
+ configure: { 'fooExt#id7': { bar: 'from-other', baz: 'from-other' } },
1383
+ },
1384
+ },
1385
+ },
1386
+ };
1387
+ Config.provide(configureConfig);
1388
+ const result = getExtensionConfig('barSlot', 'fooExt#id7').getState().config;
1389
+ expect(result).toStrictEqual({
1390
+ bar: 'from-other',
1391
+ baz: 'from-owner',
1392
+ 'Display conditions': { expression: undefined, privileges: [] },
1393
+ 'Translation overrides': {},
1394
+ });
1395
+ expect(console.error).not.toHaveBeenCalled();
1396
+ });
1397
+
1398
+ it("gathers 'configure' from every module but leaves 'add' to the last one to declare the slot", () => {
1399
+ updateConfigExtensionStore('fooExt#id8');
1400
+ Config.provide({
1401
+ 'first-mod': {
1402
+ extensionSlots: {
1403
+ barSlot: { add: ['fooExt#unwanted'], configure: { 'fooExt#id8': { bar: 'from-first' } } },
1404
+ },
1405
+ },
1406
+ 'last-mod': {
1407
+ extensionSlots: {
1408
+ barSlot: { add: ['fooExt#id8'], configure: { 'fooExt#id8': { baz: 'from-last' } } },
1409
+ },
1410
+ },
1411
+ });
1412
+
1413
+ // Composing `add` across modules would resurrect entries another module had removed, so the
1414
+ // last module to name the slot still supplies it whole.
1415
+ expect(getExtensionSlotsConfigStore().getState().slots['barSlot'].config.add).toStrictEqual(['fooExt#id8']);
1416
+ expect(getExtensionConfig('barSlot', 'fooExt#id8').getState().config).toStrictEqual({
1417
+ bar: 'from-first',
1418
+ baz: 'from-last',
1419
+ 'Display conditions': { expression: undefined, privileges: [] },
1420
+ 'Translation overrides': {},
1421
+ });
1422
+ });
1423
+
1424
+ it('releases a cross-module configure when the temporary config that set it is cleared', () => {
1425
+ updateConfigExtensionStore('fooExt#id9');
1426
+ temporaryConfigStore.setState({
1427
+ config: { 'other-mod': { extensionSlots: { barSlot: { configure: { 'fooExt#id9': { baz: 'temporary' } } } } } },
1428
+ });
1429
+ expect(getExtensionConfig('barSlot', 'fooExt#id9').getState().config.baz).toBe('temporary');
1430
+
1431
+ // The derived slot config is not part of the extension config cache key, so a released override
1432
+ // is only picked up because that cache also keys on the temporary config it was derived from.
1433
+ temporaryConfigStore.setState({ config: {} });
1434
+ expect(getExtensionConfig('barSlot', 'fooExt#id9').getState().config.baz).toBe('bazzy');
1435
+ });
1436
+
1344
1437
  it('validates the extension configure config, with module config schema', () => {
1345
1438
  updateConfigExtensionStore('fooExt#id1');
1346
1439
  const badConfig = {
@@ -32,12 +32,14 @@ import { type TemporaryConfigStore } from '..';
32
32
  * Arrays are replaced entirely (not merged by index), matching the
33
33
  * behavior of ramda's mergeDeepRight.
34
34
  */
35
+ function replaceArrays(_objValue: unknown, srcValue: unknown) {
36
+ if (Array.isArray(srcValue)) {
37
+ return srcValue;
38
+ }
39
+ }
40
+
35
41
  function mergeDeepReplace<T extends object>(left: T, right: Partial<T>): T {
36
- return mergeWith({}, left, right, (_objValue: unknown, srcValue: unknown) => {
37
- if (Array.isArray(srcValue)) {
38
- return srcValue;
39
- }
40
- }) as T;
42
+ return mergeWith({}, left, right, replaceArrays) as T;
41
43
  }
42
44
 
43
45
  /**
@@ -73,8 +75,11 @@ function recomputeAllConfigs() {
73
75
  const extensionState = configExtensionStore.getState();
74
76
 
75
77
  computeModuleConfig(configState, tempConfigState);
76
- computeExtensionSlotConfigs(configState, tempConfigState);
77
- computeExtensionConfigs(configState, extensionState, tempConfigState);
78
+
79
+ const slotConfigs = getExtensionSlotConfigs(configState, tempConfigState);
80
+ computeExtensionSlotConfigs(slotConfigs);
81
+ computeExtensionConfigs(configState, extensionState, tempConfigState, slotConfigs);
82
+
78
83
  // Last, because a failure here must not skip the outputs above. Only kept current while the
79
84
  // implementer tools are watching it; otherwise derived on read.
80
85
  invalidateImplementerToolsConfig();
@@ -185,8 +190,7 @@ function computeModuleConfig(state: ConfigInternalStore, tempState: TemporaryCon
185
190
  }
186
191
  }
187
192
 
188
- function computeExtensionSlotConfigs(state: ConfigInternalStore, tempState: TemporaryConfigStore) {
189
- const slotConfigs = getExtensionSlotConfigs(state, tempState);
193
+ function computeExtensionSlotConfigs(slotConfigs: Record<string, ExtensionSlotConfig>) {
190
194
  const slotStore = getExtensionSlotsConfigStore();
191
195
  const oldSlots = slotStore.getState().slots;
192
196
  const slots: ExtensionSlotsConfigStore['slots'] = {};
@@ -228,6 +232,7 @@ function computeExtensionConfigs(
228
232
  configState: ConfigInternalStore,
229
233
  extensionState: ConfigExtensionStore,
230
234
  tempConfigState: TemporaryConfigStore,
235
+ slotConfigs: Record<string, ExtensionSlotConfig>,
231
236
  ) {
232
237
  const extensionsConfigStore = getExtensionsConfigStore();
233
238
  const oldConfigs = extensionsConfigStore.getState().configs;
@@ -245,6 +250,7 @@ function computeExtensionConfigs(
245
250
  extension.extensionId,
246
251
  configState,
247
252
  tempConfigState,
253
+ slotConfigs[extension.slotName],
248
254
  );
249
255
 
250
256
  const previous = oldConfigs[extension.slotName]?.[extension.extensionId];
@@ -555,6 +561,7 @@ const extensionConfigCache = new Map<string, ExtensionConfigCacheEntry>();
555
561
  * @param extensionModuleName The name of the module which defines the extension (and therefore the config schema)
556
562
  * @param slotName The name of the extension slot where the extension is mounted
557
563
  * @param extensionId The ID of the extension in its slot
564
+ * @param slotConfig The slot's configuration, gathered from every module that configures it
558
565
  */
559
566
  function computeExtensionConfig(
560
567
  slotModuleName: string,
@@ -563,6 +570,7 @@ function computeExtensionConfig(
563
570
  extensionId: string,
564
571
  configState: ConfigInternalStore,
565
572
  tempConfigState: TemporaryConfigStore,
573
+ slotConfig: ExtensionSlotConfig | undefined,
566
574
  ) {
567
575
  const extensionName = getExtensionNameFromId(extensionId);
568
576
  const extensionConfigSchema = configState.schemas[extensionName];
@@ -582,10 +590,11 @@ function computeExtensionConfig(
582
590
 
583
591
  const nameOfSchemaSource = extensionConfigSchema ? extensionName : extensionModuleName;
584
592
  const providedConfigs = getProvidedConfigs(configState, tempConfigState);
593
+ const sharedOverride = slotConfig?.configure?.[extensionId] ?? {};
585
594
  const slotModuleConfig = mergeConfigsFor(slotModuleName, providedConfigs);
586
595
  const configOverride = slotModuleConfig?.extensionSlots?.[slotName]?.configure?.[extensionId] ?? {};
587
596
  const extensionConfig = mergeConfigsFor(nameOfSchemaSource, providedConfigs);
588
- const combinedConfig = mergeConfigs([extensionConfig, configOverride]);
597
+ const combinedConfig = mergeConfigs([extensionConfig, sharedOverride, configOverride]);
589
598
  const schema = extensionConfigSchema ?? configState.schemas[extensionModuleName];
590
599
  validateStructure(schema, combinedConfig, nameOfSchemaSource);
591
600
  const config = setDefaults(schema, combinedConfig);
@@ -739,11 +748,30 @@ function getExtensionSlotConfigs(
739
748
  },
740
749
  {},
741
750
  );
751
+
742
752
  validateAllExtensionSlotConfigs(slotConfigPerModule);
743
- const slotConfigs = Object.keys(slotConfigPerModule).reduce((obj, key) => {
744
- obj = { ...obj, ...slotConfigPerModule[key] };
745
- return obj;
746
- }, {});
753
+
754
+ const slotConfigs: Record<string, ExtensionSlotConfig> = {};
755
+ const configureBlocks: Record<string, Array<NonNullable<ExtensionSlotConfig['configure']>>> = {};
756
+
757
+ for (const configBySlotName of Object.values(slotConfigPerModule)) {
758
+ for (const [slotName, config] of Object.entries(configBySlotName)) {
759
+ slotConfigs[slotName] = config;
760
+
761
+ if (config.configure) {
762
+ (configureBlocks[slotName] ??= []).push(config.configure);
763
+ }
764
+ }
765
+ }
766
+
767
+ for (const [slotName, blocks] of Object.entries(configureBlocks)) {
768
+ const configure = blocks.length === 1 ? blocks[0] : (mergeConfigs(blocks) as ExtensionSlotConfig['configure']);
769
+
770
+ if (slotConfigs[slotName].configure !== configure) {
771
+ slotConfigs[slotName] = { ...slotConfigs[slotName], configure };
772
+ }
773
+ }
774
+
747
775
  return slotConfigs;
748
776
  }
749
777
 
@@ -901,8 +929,11 @@ function mergeConfigsFor(moduleName: string, allConfigs: Array<Config>): ConfigO
901
929
  return mergeConfigs(allConfigsForModule);
902
930
  }
903
931
 
932
+ /**
933
+ * Merges a whole list at once, accumulating into one destination.
934
+ */
904
935
  function mergeConfigs(configs: Array<Config>) {
905
- return configs.reduce<Config>((acc, config) => mergeDeepReplace(acc, config), {});
936
+ return configs.reduce<Config>((acc, config) => mergeWith(acc, config, replaceArrays), {});
906
937
  }
907
938
 
908
939
  /**