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.

Related: #3573022: [upstream] Data loss: `drush config:import` deletes config (e.g. code component + component config entities) and bypasses config system data integrity checks.

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 every canvas.js_component.* and every canvas.component.* with source === '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 every canvas.js_component.* and every canvas.component.* whose source is js from 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\CodeComponentSyncTransformerTest exercises both modes with MemoryStorage: DB-only js_component survives import in preserve_production; sync entry overrides DB; paired js-sourced component is copied while block-sourced is left alone; cli mode strips both on import and export; non-default collections are ignored.
  • Functional test Drupal\Tests\canvas\Functional\CanvasSettingsFormTest covers 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.

Issue fork canvas-3591147

Command icon 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

mglaman created an issue. See original summary.

mglaman’s picture

Status: Active » Needs review
wim leers’s picture

Priority: Normal » Major
Issue tags: +Needs security review, +data integrity, +AI-accelerated

🤯 😱

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 on CodeComponentSyncTransformer but 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.)

wim leers’s picture

Assigned: Unassigned » mglaman
Status: Needs review » Needs work
bernardm28’s picture

without the patch and with the patch.
This patch works, though I wonder if it should also include the canvas folder. Even that is just metadata.

The first drush cst on 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 cst is 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.
 production error on page with a code component that's missing

new form for canvas component

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

bernardm28’s picture

Also, I should add that I tested this with.
Drupal core 11.3.10
Canvas 1.4.1