Plugins

Composer plugins are PHP code that hooks into Composer's own process; viv has no PHP runtime, so it can't run one as written. Instead:

  • A small set of common plugins — composer/installers, the WordPress core installers, and native adapters for Yii2, Craft, Altis, Drupal scaffolding, Symfony runtime, phpcs, PHPStan and more — are reimplemented in viv itself, so the outcome matches Composer's.
  • A few plugins are known to only affect Composer commands viv doesn't implement; viv ignores them, same as Composer does when a plugin is disabled.
  • Any other plugin stops the install with an error naming the plugin. --no-plugins turns that into a warning and installs the way Composer's own --no-plugins would.

The full list of which plugin falls into which category is documented separately.1 The rule for when a plugin is a data file rather than an adapter lives in docs/plugin-strategy.md, "Data file or adapter".

Inventory

Plugins found in the lock files available on 2026-09-06 (four public projects, the Laravel benchmark lock, two test fixtures and one private WordPress project):

Plugin What it changes Portable?
composer/installers Install path per package type from extra.installer-paths Native (src/plugins/installers.rs), pure path mapping
johnpbloch/wordpress-core-installer Install path of wordpress-core packages from extra.wordpress-install-dir Native (src/plugins/wordpress_core.rs), pure path mapping
dealerdirect/phpcodesniffer-composer-installer Points phpcs's installed_paths at every installed standard Native (src/plugins/phpcs.rs); writes CodeSniffer.conf directly rather than shelling out to phpcs --config-set, so an install needs no PHP runtime (#218)
phpstan/extension-installer Writes GeneratedConfig.php listing every extra.phpstan package Native (src/plugins/phpstan.rs)
php-http/discovery Adds packages to the resolver and generates a discovery file Native (src/plugins/discovery.rs), preAutoloadDump/extra.discovery only; the resolver half (postUpdate) isn't ported
tbachert/spi Generates a service-provider map file after autoload dump Native (src/plugins/spi.rs), extra.spi only
cweagans/composer-patches Applies patches from extra.patches/a patches file Native (src/plugins/patches.rs), git-apply path only (#53)
yiisoft/yii2-composer Writes vendor/yiisoft/extensions.php listing every yii2-extension package Native (src/plugins/yii2.rs) (#92)
craftcms/plugin-installer Writes vendor/craftcms/plugins.php listing every craft-plugin package Native (src/plugins/craft.rs) (#92)
pestphp/pest-plugin Writes vendor/pest-plugins.json listing every package's extra.pest.plugins, root last Native (src/plugins/pest.rs) (#131)
ffraenz/private-composer-installer Substitutes {%NAME}/{%version} placeholders in a dist URL from the environment/.env right before download Native (src/plugins/private_installer.rs) (#98)
codeception/c3 Copies its bundled c3.php into the project root on install/update, unless an existing, edited one is there Native (src/plugins/c3.rs) (#126)
drupal/core-composer-scaffold Copies scaffold files (index.php, .htaccess, settings.php, …) from every allowed package, manages .gitignore, writes vendor/drupal/DrupalInstalled.php and points the root classmap at it Native (src/plugins/drupal_scaffold.rs) (#93)
drupal/core-project-message Prints a message to stdout after create-project/install, no filesystem effect Known inert — Composer prints a message viv does not
drupal/core-recipe-unpack Unpacks a required drupal-recipe package's own dependencies into the root composer.json Known inert for install/update — only subscribes to POST_UPDATE_CMD/POST_CREATE_PROJECT_CMD (via composer require/create-project), never plain install
symfony/runtime Writes vendor/autoload_runtime.php from a fixed template plus extra.runtime options, after autoload dump Native (src/plugins/symfony_runtime.rs) (#93)
altis/cms-installer Copies index.php/wp-config.php/.build-script from vendor/altis/cms/, scaffolds content/{plugins,themes}/.gitignore, then writes vendor/modules.php listing every extra.altis package's load.php plus the root's own extra.altis.modules.*.entrypoint Native (src/plugins/altis_cms_installer.rs) (#355)
altis/core Override_Installer: a wordpress-plugin/wordpress-muplugin package named in any installed package's extra.altis.install-overrides installs at the default vendor/<name> instead of wherever composer/installers would place it Native (src/plugins/altis_core.rs) (#355), pure path mapping, lock-wide
altis/dev-tools-command Seeds .travis.yml/.config/travis.yml from vendor/altis/dev-tools/travis/ if absent, then keeps .travis.yml's pinned ref in sync with the installed altis/dev-tools version Native (src/plugins/altis_dev_tools_command.rs) (#355)
altis/local-server activate() conditionally requires a PHP file in-process for its own later use; subscribes to no events, only adds a composer server CommandProvider Known inert — no disk effect at install (#355)
ion-bazan/composer-diff Adds a composer diff CommandProvider, no event subscription Known inert — command-only (#355)

Plugins named in issue #12 but not seen in any lock yet: symfony/flex (rewrites composer.json and recipes, not portable), bamarni/composer-bin-plugin (nested installs, portable by running viv in each vendor-bin/*).

Rule

For every package of type composer-plugin in the lock that is enabled by config.allow-plugins:

  1. Native adapter exists: viv applies the equivalent behaviour itself. Output must be byte-identical to Composer running the real plugin.
  2. Known inert: the plugin only affects Composer's own commands that viv does not implement (for example ergebnis/composer-normalize). viv ignores it silently. The list lives in code.
  3. Anything else: viv refuses with an error naming the plugin and pointing at this page. --no-plugins turns the refusal into a warning and installs as Composer would with --no-plugins. Silent wrong installs are worse than a loud stop. The refusal also says whether --no-plugins is safe: plugins in BY_DESIGN_REFUSALS (src/plugins/mod.rs) get "by design, you lose nothing" with the one-clause reason from this page; everything else gets the generic "not adapted yet, this skips real work" (#224).

Plugins that allow-plugins sets to false, or that are absent from the map, are ignored, as Composer ignores them.

The full rule for when a plugin is a data file rather than an adapter is in Architecture: Plugin strategy.


  1. docs/plugin-strategy.md lists which plugin falls into which category. ↩