Overview
"Code components clobbered by config sync — add sync_mode toggle"
AI helped write this issue and plan the fix.
canvas.js_component.* and the paired canvas.component.* entities whose source is js are saved from the in-browser editor in production. drush config:export and drush config:import treat the sync directory as authoritative, so any production-only code component is wiped on the next deploy. This breaks the implicit promise that code components can be authored and edited in production.
Two real workflows need to be supported. Small and medium sites still want config sync but expect production-created components to survive an import — the WordPress model. Sites that adopt Canvas CLI (packages/cli/) want config sync to ignore code components entirely so the CLI is the sole source of truth.
Proposed resolution
1. Add a canvas.settings config object with a sync-mode toggle.
Default config in config/install/canvas.settings.yml:
code_components: sync_mode: preserve_production
Schema appended to config/schema/canvas.schema.yml with a Choice constraint covering preserve_production and cli.
2. Add CodeComponentSyncTransformer event subscriber.
New class Drupal\canvas\EventSubscriber\CodeComponentSyncTransformer subscribing to ConfigEvents::STORAGE_TRANSFORM_IMPORT and ConfigEvents::STORAGE_TRANSFORM_EXPORT. Mirrors the pattern in Drupal\canvas\EventSubscriber\ComponentTreeConfigEntityTransformer. No config_ignore contrib module needed — Drupal core's storage transform events are the supported mechanism.
Behaviour per mode:
preserve_production(default): on import, iterate everycanvas.js_component.*and everycanvas.component.*withsource === 'js'in the active config storage. For any name absent from the incoming sync storage, write the active value into the sync buffer so the importer treats it as already present and never deletes it. Entries that exist in sync still win — tracked components remain authoritative. Export is unchanged.cli: on both import and export, delete everycanvas.js_component.*and everycanvas.component.*whosesourceisjsfrom the sync buffer.
The paired component entity is matched by reading source from the config payload, not by config-name prefix — canvas.component.* is shared with block, sdc, and other sources, which must not be filtered. JsComponent::SOURCE_PLUGIN_ID ('js') is the canonical match.
Service definition in canvas.services.yml:
Drupal\canvas\EventSubscriber\CodeComponentSyncTransformer: arguments: $configFactory: '@config.factory' $activeStorage: '@config.storage' tags: - { name: event_subscriber }
3. Add CanvasSettingsForm.
New Drupal\canvas\Form\CanvasSettingsForm extending ConfigFormBase exposes a radio under "Code components". Route canvas.settings at /admin/config/canvas/settings, gated by administer code components. Menu link under system.admin_config_user_interface.
4. Tests.
- Kernel test
Drupal\Tests\canvas\Kernel\Config\CodeComponentSyncTransformerTestexercises both modes withMemoryStorage: DB-onlyjs_componentsurvives import inpreserve_production; sync entry overrides DB; pairedjs-sourcedcomponentis copied whileblock-sourced is left alone;climode strips both on import and export; non-default collections are ignored. - Functional test
Drupal\Tests\canvas\Functional\CanvasSettingsFormTestcovers form access, persistence, and 403 for unauthorized users.
5. Out of scope.
- Per-component granularity — global toggle only.
- Features-like reverter UI for diffing active vs sync.
- Pre-import validation step that warns about pending production edits.
- Content templates have the same underlying problem (in-browser editing of a config entity) and will be addressed in a follow-up; their tree-shaped data needs separate consideration.
User interface changes
A new admin page at /admin/config/canvas/settings ("Canvas") under Configuration → User interface. It contains a single "Code components" section with a radio:
- Preserve production-created components (recommended) — default.
- Manage exclusively via Canvas CLI — opt-in.
No other visible changes. Existing sites get preserve_production on the next install/update, which is strictly safer than the current "hard sync" behaviour — it only prevents deletions, it never overrides config-tracked components.
| Comment | File | Size | Author |
|---|---|---|---|
| #7 | Screenshot 2026-05-21 at 12.45.02 AM.png | 78.64 KB | bernardm28 |
| #7 | Screenshot 2026-05-21 at 3.34.52 PM.png | 227.88 KB | bernardm28 |
| #7 | Screenshot 2026-05-21 at 3.33.35 PM.png | 68.83 KB | bernardm28 |
Issue fork canvas-3591147
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 #3
mglamanComment #4
mglamanComment #5
wim leers🤯 😱
This is essentially overriding/customizing core config sync mechanisms.
This IMHO violates one of the key principles we started from: rely only on core's standard config management best practices.
At a minimum, I'm missing
docs/additions in the MR that explain why this is justified. I see a nice docblock onCodeComponentSyncTransformerbut it doesn't explain why this is not actually an upstream bug that should be fixed, which is what #3573022: [upstream] Data loss: `drush config:import` deletes config (e.g. code component + component config entities) and bypasses config system data integrity checks concluded?I bet that to somebody who's debugged this inside-out, that it's clear why this is necessary. But it's not to me. (I debugged #3573022, but that's ~2 months ago. I don't recall details.)
(The issue summary is extremely long, but describes only the how in detail, not the why.)
Comment #6
wim leersComment #7
bernardm28 commentedThis patch works, though I wonder if it should also include the canvas folder. Even that is just metadata.
The first
drush cston the image above is without the patch. As seen in the image above, this makes code components not usable in production or as a quick way for a developer to patch a change before capturing said changes and placing them in the codebase.Personally, I used them to get ahead of some features a dripyard theme was rolling out in a few weeks.
However, my changes got wiped, like a week later, when the host ran an out-of-schedule Drush deploy. I understand that would not be an issue if the code component were in the code base, but for small and medium sites with quick out-of-schedule changes, that's not realistic.
I can't say this is the most appropriate fix, but I stopped using code components in January because they can't be trusted. If I already have SDC's code components become complementary, and as such, if I made them, I expected them to survive a simple drush deploy or drush cim.
This PR helps make that distinction, so sites that require a fast turnaround or those that are competing with WordPress prototyping speeds can roll out simple to medium complexity components, such as a banner, quickly in prod, then capture the code and code config and deploy it at a later date.
The second
drush cstis with the patch. Which would preserve code components made prod, and deploy as expected.I appreciate the ability for code components to handle many different things, but do not make them so that they can't be used on pro bono sites.
Code components usage should also provide a 1-to-1 equivalent to the HTML block in Gutenberg, which provides the ability for one to create a "code component" in production if needed as a stopgap. The HTML block can be in any environment, and deploying new changes does not wipe it out. Even their templates and patterns require the user input to be reset for good and bad.
Ps: for reference, here is the nice error that greets you. To remind you not to prototype code components in prod.

I like the new form, though I did not find how to get to it from the UI. It was pretty good when I used the URI. /admin/config/canvas/settings
Comment #8
bernardm28 commentedAlso, I should add that I tested this with.
Drupal core 11.3.10
Canvas 1.4.1