Overview
As of #3523841: Versioned Component config entities (SDC, JS: prop_field_definitions, block: default_setting, all: slots for fallback) + component instances refer to versions ⇒ less data to store per XB field row, a new Component version is created whenever the ComponentSource-specific settings for that component change.
For SDCs and code components, that's prop_field_definitions:
$version = ComponentEntity::generateVersionStringForData($settings, 'experience_builder.component_source_settings.sdc');
and
$version = ComponentEntity::generateVersionStringForData($settings, 'experience_builder.component_source_settings.js');
$component
->createVersion($version)
->deleteVersionIfExists(ComponentInterface::FALLBACK_VERSION)
->setSettings($settings);
But … shouldn't a new version also be created when the set of slots changes? Otherwise, a change in slots doesn't cause a new version to be created? (Realized this while reviewing #3519891: Constrain slot names allowed by XB in Components (and in its component tree).)
Otherwise, we can end up in a place where the Fallback source (introduced in #3519168: Handle components provided by ComponentSources EXPLICITLY disappearing — enables deleting JS components that are in use) won't be able to know the actual slots available for a particular version of the component: as long as the props of an SDC/code component remain the same, we'd end up overwriting the information in fallback_metadata.slot_definitions for that same version.
Proposed resolution
Update the logic to let the following affect the deterministic hash (actually deterministic since #3528159: Ensure deterministic version hashes for ComponentSource-specific settings, thanks to config schema-powered normalization):
- ✅
ComponentSource-specific settings - ✅ normalized slots: the result of
ComponentSourceWithSlotsInterface::getSlotDefinitions()but without thedescriptionand only keeping the first example - normalized explicit input schema:
- ✅ the result of
\Drupal\experience_builder\PropShape\PropShape::normalize()for SDC + code components, plus with the default value (i.e. the first example) ⇒ ⚠️this is strictly speaking not necessary for these component sources thanks toprop_field_definitionsbeing part of the settings - ✅ the combination of a normalized config schema for that block plugin
, plus the block plugin's(default values end up in::defaultConfigurationtype: experience_builder.component_source_settings.block)
- ✅ the result of
The deterministic nature of the version hash must be validated, by validating the active_version. But older versions should not be validated, for the reasons outlines in #12.
IOW: this is necessary for #3524751: [later phase] Component Source plugins: generalized support for schema changes of explicit inputs to be possible without BC breaks. Now that many pieces exist, it's clear that this part that was descoped at #3520484-44: [META] Production-ready ComponentSource plugins actually is necessary. (This used to be mentioned in the data storage IS until a few weeks ago — see for example @longwave's comment at #3520449-17: [META] Production-ready data storage).
This should be easily manually tested to verify the expected real-world impact from a component developer POV (the example used is the block component source, but this should work equally well for SDC etc.):
- Go to
/admin/config/development/configuration/single/exportand exportblock.system_menu_block.tools - Modify e.g.
levelin\Drupal\system\Plugin\Block\SystemMenuBlock::defaultConfiguration(), then rebuild. - Repeat step 1 and observe: a new version, with a new default value in
default_settings. - Now go and modify
block.settings.system_menu_block:*and add a new optional key-value pair, for example:
diff --git a/core/modules/system/config/schema/system.schema.yml b/core/modules/system/config/schema/system.schema.yml index 0c7cfac313c..d38a75778ff 100644 --- a/core/modules/system/config/schema/system.schema.yml +++ b/core/modules/system/config/schema/system.schema.yml @@ -448,6 +448,9 @@ block.settings.system_menu_block:*: expand_all_items: type: boolean label: 'Expand all items' + yar: + type: boolean + requiredKey: false block.settings.local_tasks_block: type: block_settings - Repeat step 1 and observe: a new version, with the same default values in
default_settings, but … a new version, because config schema has changed.
User interface changes
/admin/appearance/component now shows the number of versions of each component, and shows the active version upon hover.

| Comment | File | Size | Author |
|---|---|---|---|
| #23 | Screenshot 2025-06-10 at 11.11.22 AM.png | 218.64 KB | wim leers |
Issue fork experience_builder-3528362
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
wim leersComment #3
wim leersCan hardly be a bug if #3528159 has just landed.
Comment #5
larowlanFrom
BlockManagerWe already include default configuration - does that cover off the schema already?
Can you provide an example scenario where we need item 3?
Will make a start on item 2.
Comment #6
larowlanI think this raises an issue if we're generating the hash based on information that isn't stored in the config entity.
Specifically things that aren't available in
\Drupal\experience_builder\Entity\Component::validateVersionsin order to validate the configuration.It would require making that rely on actual source plugin instances and consulting them to ask what else should be considered in calculating the expected hash.
Will have a think about how to approach that, I don't know that booting an instance of the source plugin during validation is the correct approach when we're dealing with typed data (specifically config mapping data) and not config entities.
Comment #7
larowlanI think if we inject the source manager into the constraint validator we could boot an instance without the config entity, I think that's acceptable, will do that.
This puts us into the Production ready component source plugins meta as much as the data storage one as it might require new methods on the interface
Comment #8
larowlanI might have a way to do it with raw config typed data
Comment #10
wim leers#5: block plugins’ default configuration might stay the same, but the validation constraints might become tighter or looser, which would then affect the freedom of component instances. Which then could lead to existing instances failing to render, and/or the need for an update path.
#7: yep, this straddles both. But component source plugins just must work well enough for beta1, the API is not expected to be final by beta1. Data storage must not change after beta1, and version hashes changing would be … bad.
Will review, curious what you came up with!
Comment #11
wim leersDidn't get to reviewing this today :/
Comment #12
wim leers#6: gave this some more thought.
You’re right that it’s impossible to validate old versions (any besides the active one) if they depend on data not contained within the config entity.
So … I think the solution is actually very simple, plus it would prevent brittleness in the future: only validate
active_version!That way, even if a
ComponentSourceplugin changes what information is used to deterministically compute the hash, old versions (hashes) continue to work fine. Because in the proposal I made in the issue summary, any such logic change would have caused all old versions (hashes) to become invalid 🙈And that is actually perfectly in line with the source-specific settings for old (non-active) versions: those already use
type: ignorebecause the config schema for older versions might have been different. What we’re dealing with here is similar.Comment #13
larowlanRe #12 that makes sense.
I think we can include the 'current schema' in the hash calculation and that gives us item 3 from the issue summary
Will pick that up today
Comment #14
wim leers🥳
BTW, #3519891: Constrain slot names allowed by XB in Components (and in its component tree) added a todo pointing here — this MR should be able to drop those lines.
Comment #15
larowlanGot something going that uses the schema, but I'm not 100% happy with it.
I think its probably ok, but there's a chicken-egg scenario with creating a version string vs having a component typed data adapter that prevents using being able to use
\Drupal\Core\Config\Schema\TypeResolver::resolveDynamicTypeNameThe workaround seems ok, but is not ideal.
Comment #16
wim leersThanks! Will review 😊
Comment #17
wim leersAFAICT this also blocks #3519878 now, see #3519878-22: ContentCreatorVisibleXbConfigEntityAccessControlHandler's `view` access must refuse access to disabled config entities. IOW: I think/hope that once this is fixed, the remaining test failures there will disappear.
The test coverage here is definitely sufficient, so removing tag.
P.S.: This removed a few
@todos pointing to #3525759: SdcPropKeysConstraintValidator::validate() should complain about extraneous keys too, not just missing keys, because those bits were getting in the way of getting this MR to pass. This is critical for data model stability, so this takes precedence.Comment #18
wim leers@larowlan mentioned he wasn't happy with one area. I investigated and implemented a counterproposal.
Comment #19
larowlanPicking up where Wim left off.
Comment #20
larowlanThis is passing now
Comment #21
wim leersComment #22
wim leersThanks, @larowlan, for pushing this to completion! All I had to do here was fix nits, and add docs for the tricky bits 😊
Issue summary updated.
Comment #23
wim leersUpdated
/admin/appearance/componentto show the number of versions of each component, and shows the active version upon hover.Comment #25
wim leers