Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Composer Cleanup Plugin Laravel Package

barryvdh/composer-cleanup-plugin

Composer plugin that cleans up your vendor directory by removing unnecessary files such as tests, documentation, and examples from installed packages, keeping your deployment lean and reducing disk usage without affecting runtime behavior.

View on GitHub
Deep Wiki
Context7

Getting Started

Mental model first. This is a Composer install/update lifecycle plugin, not a Laravel package. There is no service provider, no config/ publish, no container binding, nothing registered at runtime. It hooks the moment Composer unpacks a package into vendor/ and deletes files matching glob rules. Treat it like a line in your Dockerfile or a CI script — it lives in the build phase and has zero presence in the running application.

Step 0 — measure before you install anything. If you can't see the bloat, the plugin will buy you nothing.

du -sh vendor
du -sh vendor/*/* | sort -rh | head -20
find vendor -type f | wc -l

On a modern Laravel app most dependencies already ship .gitattributes with export-ignore, so --prefer-dist installs are already lean. You're looking for a handful of laggard packages holding tests/docs/CI folders, not a systemic problem.

Step 1 — install and pin.

composer require --dev barryvdh/composer-cleanup-plugin
composer show barryvdh/composer-cleanup-plugin   # note the resolved version

Then hard-pin it in composer.json ("barryvdh/composer-cleanup-plugin": "0.x.y", no caret). The package is archived with no future releases — a floating constraint gets you nothing but a future surprise.

Step 2 — allowlist the plugin. Composer 2.2+ blocks plugins by default:

{
    "config": {
        "allow-plugins": {
            "barryvdh/composer-cleanup-plugin": true
        }
    }
}

Commit this. A missing entry doesn't fail the build — Composer warns, skips the plugin, and your install "succeeds" with a fat vendor tree.

Step 3 — rules. Defaults are applied under extra.cleanup.rules keyed by package name, with * as the catch-all:

{
    "extra": {
        "cleanup": {
            "rules": {
                "*": ["tests", "Tests", "docs", "examples", "phpunit.xml*", ".github", ".git"],
                "vendor/some-legacy-package": ["!docs/examples"]
            }
        }
    }
}

Always check the actual src/ of the plugin (it's small) for the exact key names, glob semantics, and whether negation (!) is supported before you rely on either.

First real use case: a single service that is measurably over a hard artifact limit — a Lambda zip ceiling, a registry bill, a slow cold start. Run the plugin on that one build, diff the result, and stop there. Don't roll it out org-wide.

Implementation Patterns

Decide require vs require-dev before anything else

This is the trap that wastes an afternoon. The plugin must be installed to run, so:

  • composer install --no-dev → require-dev packages are skipped → the plugin is never installed and never runs.
  • Therefore, if your production build uses the standard --no-dev install, the plugin has to go in require, not require-dev.
// composer.json — production build uses --no-dev
"require": {
    "barryvdh/composer-cleanup-plugin": "0.x.y"
},
"config": {
    "allow-plugins": { "barryvdh/composer-cleanup-plugin": true }
}

It's a build tool sitting in your production dependency list, which is ugly but correct. Document why in a comment or the README so the next person doesn't "clean it up" into require-dev and silently disable it.

Production-only, in the build stage

FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install \
      --no-dev --prefer-dist --optimize-autoloader \
      --no-interaction --no-progress

The plugin runs transparently inside that RUN. Do not copy vendor/ from a dev machine or a cache that was built without the plugin — you'll get inconsistent layers.

Surgical per-package overrides

Start wide, then narrow only the packages that misbehave. The * default should be conservative — tests, docs, examples, CI metadata — and per-package keys should exist solely to remove a pattern for a package where deletion broke something.

"extra": {
    "cleanup": {
        "rules": {
            "*": ["tests", "Tests", "docs", "phpunit.xml*", ".travis.yml", ".github"],
            "spatie/laravel-permission": ["!tests/Fixtures"],
            "some/package-with-license-in-docs": ["!docs/LICENSE*"]
        }
    }
}

Verify what you actually deleted

Treat cleanup as a change to the artifact that must be proven, not assumed:

# baseline (plugin disabled)
composer install --no-plugins --no-dev --prefer-dist
find vendor -type f | sort > /tmp/vendor-before.txt
du -sh vendor

# cleaned
rm -rf vendor && composer install --no-dev --prefer-dist
find vendor -type f | sort > /tmp/vendor-after.txt
du -sh vendor

diff /tmp/vendor-before.txt /tmp/vendor-after.txt | head -100

Gate on this in CI alongside php artisan test and a staging smoke deploy. If the delta is under a few MB, delete the plugin — you didn't have a problem.

Keep local dev boring

Because it's in require, it runs on developers' laptops too. That's usually harmless, but it breaks IDE navigation into package tests, confuses anyone debugging a third-party bug, and wrecks path repositories outright. If it annoys the team, install locally with composer install --no-plugins, or move the rules behind a build-time composer.json variant.

The escape hatch, documented

composer install --no-plugins   # bypass entirely if a build breaks
rm -rf vendor && composer install --no-plugins  # restore deleted files

There is no "undo" — cleanup is destructive and irreversible without a fresh install. Put those two lines in the runbook next to the plugin's pinned version.

Gotchas and Tips

  • Silent skip via allow-plugins. Missing entry = no cleanup, no failure. Assert it in CI: composer config allow-plugins.barryvdh/composer-cleanup-plugin should return true, or grep the install log for the plugin's output.
  • --no-dev + require-dev = no-op. Covered above, but it is the single most common reason "the plugin doesn't work."
  • Cold-install ordering. On a fresh vendor/ (every Docker build), the plugin is itself part of the install transaction. Packages unpacked before the plugin activates may not get cleaned. Always validate the resulting tree rather than trusting that the plugin ran for everything.
  • It's archived. Last release 2023-08-05, zero dependents. No fix will arrive for a future Composer major or an over-broad glob. Pin exactly, commit the lockfile, and accept that a defect means you fork it.
  • Licensing. Blanket *.md, docs/, or LICENSE* rules strip attribution notices. If legal, procurement, or your SBOM process expects license text in the shipped artifact, explicitly exclude LICENSE* and NOTICE* from every rule set — including the * default.
  • path repositories and symlinks. Cleanup writes through symlinks, so it will mutate your actual local package checkout, not just vendor/. If your build matrix has any {"type": "path"} repos, add an exclusion or disable the plugin for that build.
  • --prefer-source installs. The plugin can delete .git and other VCS metadata from a source clone, leaving you with a checkout Composer no longer treats as pristine. Stick to --prefer-dist for builds that use this.
  • PSR-4 roots and --classmap-authoritative. Never let a rule match a path inside an autoloaded namespace. Deleting files under a src/ root breaks dump-autoload --classmap-authoritative and can fatal at runtime on a files autoload entry. Restrict rules to names that are unambiguously dev-only (tests, docs, phpunit.xml*, .github).
  • post-install-cmd / post-autoload-dump scripts run after cleanup. If a package's fixtures or stubs are consumed by your own scripts (or by something like composer-patches), those files may already be gone. Test the full pipeline, not just composer install.
  • Docker layer caching. Rules live in composer.json, so editing them invalidates the dependency layer and forces a full reinstall. That's correct behaviour, but budget for it.
  • Case sensitivity. tests and Tests are different directories on Linux, identical on macOS. List both variants explicitly; never rely on case-insensitive matching in CI.
  • Glob scope is per-package. Patterns are relative to each package root — docs deletes vendor/foo/bar/docs, never your project's docs/.
  • Fewer files helps your IDE. A side benefit worth mentioning to the team: PhpStorm indexing and vendor/ search get noticeably faster after a real cleanup.
  • The native-flags-first baseline. Before adding a third-party install-time plugin, confirm you've already got:
"config": {
    "preferred-install": "dist",
    "optimize-autoloader": true
}

plus --no-dev, a multi-stage Docker build, and a .dockerignore. Those usually deliver more than this plugin does, with no install-time code execution.

  • The zero-dependency alternative. If you just want the bytes and don't want the plugin:
find vendor -mindepth 2 -maxdepth 3 -type d \
  \( -name tests -o -name Tests -o -name docs -o -name examples \) \
  -prune -exec rm -rf {} +

A five-line CI step you own, patch, and delete whenever you like.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums