liberusoftware/composer-installer
A Composer installer plugin for Liberu Software packages that provides custom install paths and package-type routing, helping Liberu modules, themes, and extensions land in the expected Laravel directories and integrate smoothly via Composer.
This is a Composer installer plugin, not a Laravel service provider — it never touches your config/app.php. It hijacks Composer's install path resolution so packages of the supported types land in modules/ and themes/ instead of vendor/.
Require it as a real dependency (plugins belong in require, not require-dev):
composer require <vendor>/<installer-package>
Let Composer trust it. On Composer 2.2+ you'll be prompted; otherwise add it manually:
{ "config": { "allow-plugins": { "<vendor>/<installer-package>": true } } }
Verify activation before trusting it:
composer install -vvv | grep -i plugin
In 1.1.0 the package ships extra.plugin-modifies-install-path: true, so it should activate at the very start of the run. If you don't see that, Composer will resolve paths as if every package lives in vendor/ and your modules/ tree will churn.
First use case: composer require <vendor>/<some-module> and confirm modules/<Name>/ appears (not vendor/<vendor>/<some-module>). The package's type in its own composer.json decides the target directory — read src/LiberuInstaller.php first to see the exact type → path mapping your version supports, since that is the one thing you cannot guess from the docs.
Module/theme authoring. Your package's composer.json is the whole contract:
{ "name": "acme/blog-module", "type": "liberu-module", "require": {} }
The name drives the target directory name. Keep the name and the intended directory in sync — 1.1.0 now enforces that.
Local development with path repositories. Standard pattern still works, and is where you'll hit the collision check most often:
"repositories": [{ "type": "path", "url": "../blog-module", "options": { "symlink": true } }]
Renaming a package (new failure mode in 1.1.0). Treat a rename as a two-step migration, scripted so CI can't get it wrong:
rm -rf modules/OldName themes/OldTheme
composer update acme/new-name
Testing your own code that depends on layout. LiberuInstaller now accepts an optional root directory, defaulting to getcwd(). Use it in your own tests to assert paths without touching the repo:
$installer = new LiberuInstaller(__DIR__ . '/fixtures/project');
$path = $installer->getInstallPath($package); // asserts modules/... etc.
CI. Never run composer install --no-plugins in a pipeline that then tests against modules/ — the plugin is skipped and the tree is silently wrong. Assert layout explicitly rather than assuming success:
composer install --no-interaction
test -f modules/Blog/composer.json
Extending. The supported types live in the installer's path-computation method; adding a type is a small change plus a Pest test. Mirror the 1.1.0 suite structure (path computation, type mapping, name validation, traversal rejection, collision) if you fork.
allow-plugins is silent by default. Skipped plugins produce no error, just packages in vendor/. If modules/ looks stale after a composer update, check this before anything else.plugin-modifies-install-path needs Composer 2.2+. On older versions the key is ignored — you get the old, later activation and the "unexplained churn in tracked package directories" the release notes describe. Pin composer-runtime-api/Composer version in CI images.composer.json names a different package aborts the install. That is exactly right for catching renames, but it also means a half-finished or hand-created directory blocks you. Inspect modules/<Target>/composer.json and delete if stale.composer.json blocks nothing. A directory with no composer.json won't protect itself — writes proceed. Don't rely on the check to guard hand-curated folders; commit them or use your own guard.../) and absolute paths in package names are rejected outright. If a composer require fails with a path error, suspect the package name, not your repository layout.--working-dir moves what the installer considers root. The new injectable root argument mirrors this behaviour — it isn't a new default.Composer\Plugin\Capable. Harmless for consumers; only relevant if you subclassed Plugin and overrode getCapabilities().composer install -vvv for plugin activation order; vendor/composer/installed.json shows the resolved install-path per package, which is the fastest way to see where the installer actually put something.modules/ and themes/ are tracked in this layout, so a botched install is a git diff you can read — and git checkout is your rollback.How can I help you explore Laravel packages today?