All articles

Cleaning up a Drupal composer.json: dead patches and no-op settings

A Drupal composer.json that has been around for a few years usually carries settings that no longer do anything: patches upstream has already merged, patch files nothing references, flags the plugin ignores, plugins that only mattered on Composer 1, and * constraints nobody meant to keep. The way to clean it up is to check each entry against the current behaviour of the tool that reads it, remove what is inert, and declare in require every Composer plugin the root project actually depends on, instead of inheriting it from another package.

Why this file drifts

A teammate recently audited composer.json across a group of nonprofit membership sites built on one shared Drupal distribution. Each site's root file had been edited by different people over several Drupal and Composer upgrades, and every edit was additive: a patch here, a flag there, an allow-plugins entry when Composer 2.2 started asking. Nothing ever got removed, because nothing broke.

That is the trap. Config that silently does nothing does not fail, so it never gets looked at, and it makes the file harder to reason about the next time something really is wrong. Here is what the audit found.

Patches upstream has already fixed

One site still patched a contrib Commerce module for a bug that the module's maintainers had fixed in a later release the site was already on. With cweagans/composer-patches, that kind of patch either fails to apply (and you notice) or, worse, applies cleanly to code that has changed around it.

The check is simple: for each entry under extra.patches, open the drupal.org issue linked in the description and look at which release the fix landed in. If the installed version is at or past it, the patch goes.

Patch files nothing references

The patches/ directory held files no entry pointed to any more, left behind when a patch was removed from composer.json but not from disk. They do no harm at install time, but they mislead whoever reads the directory next. A quick way to list them:

for f in patches/*.patch; do
  grep -q "$(basename "$f")" composer.json || echo "unreferenced: $f"
done

Flags the plugin ignores

The root set extra.enable-patching to true. In cweagans/composer-patches 1.x, that flag controls whether patches declared by dependencies are applied. Patches declared in the root package are applied regardless. On these sites, the root was where patches lived, so the team concluded the flag did nothing and removed it.

This one deserves care rather than a blanket rule. If any dependency in your tree declares its own patches, and you rely on them, the flag matters. Check before deleting it; the point of the audit is to know what each line does, not to remove lines.

Plugins that only helped Composer 1

zaporylie/composer-drupal-optimizations cut Composer 1's memory use on Drupal projects by pruning old Symfony versions from the dependency pool. Composer 2 solved that problem itself, and the plugin is a no-op there. It was still in require and in allow-plugins, so it came out of both.

The same pass removed allow-plugins entries for plugins that were no longer installed at all. Composer does not complain about those, so they accumulate.

Repositories without names

The repositories section was a JSON array, so its entries were addressed by position. Composer also accepts an object keyed by name, which makes the file readable and lets other tooling refer to an entry by a stable key:

"repositories": {
  "drupal": {
    "type": "composer",
    "url": "https://packages.drupal.org/8"
  },
  "asset-packagist": {
    "type": "composer",
    "url": "https://asset-packagist.org"
  }
}

While in there, the team removed leftover _webform markers from the package repositories that provide Webform's front-end libraries. Those markers come from copying Webform's generated library definitions, and Composer does nothing with them.

Wildcard constraints on libraries

Those same Webform library packages were required with "*". A wildcard means any version, including a future major with breaking changes, the next time someone runs composer update without thinking about it. The team replaced each one with a caret range on the currently installed major, for example "^4.1", so updates stay within compatible releases and a major upgrade becomes a deliberate change.

Plugins the root relies on but does not require

This was the most useful change. Each site's root relied on several Composer plugins to install at all:

  • composer/installers and oomphinc/composer-installers-extender, to put modules, themes and libraries in the right directories;
  • cweagans/composer-patches, to apply its patches;
  • drupal/core-composer-scaffold, to place index.php, .htaccess and the other scaffold files;
  • wikimedia/composer-merge-plugin, to merge in additional composer.json files.

None of them were in the root's own require. They arrived transitively, because the shared distribution required them. That works until the distribution drops or changes one, at which point every site's install changes behaviour with no edit to its own file. The team added each of them to the root require with an explicit constraint, so the root says what it depends on.

What I like about this change is that it moves a hidden contract into the file where people look. If the root's extra section configures a plugin, that plugin belongs in the root's require.

How to run the same audit

  1. For every patch, find the upstream issue and the release that fixed it, and compare with the installed version (composer show drupal/<module>).
  2. List patch files on disk that no entry references, and delete them.
  3. For every key under extra, find the plugin that reads it and check what it does in the version you have installed.
  4. Compare allow-plugins with the installed packages (composer show), and remove entries for plugins that are not installed.
  5. Run composer why <plugin> for every plugin the root configures; if the only reason is another package, add it to the root's require.
  6. Search for "*" constraints and replace them with caret ranges.

Run composer validate and a clean composer install afterwards and compare the installed package list with the one from before. A tidy-up should change the file, not the result.

Key takeaways

  • Configuration that silently does nothing never fails, so a composer.json only gets cleaner when someone audits it deliberately.
  • Remove a patch once the installed release contains the upstream fix, and delete patch files no entry references.
  • In cweagans/composer-patches 1.x, enable-patching only affects patches declared by dependencies, so check whether you have any before removing it.
  • zaporylie/composer-drupal-optimizations is a no-op on Composer 2 and can be removed from require and allow-plugins.
  • Every Composer plugin your root configures should be in the root's own require, not inherited from a distribution.