saucebase / module-installer
Custom Composer installer for Sauce Base modules (type: saucebase-module). Installs to modules/<name> (lowercase) with configurable base dir.
Package info
github.com/saucebase-dev/module-installer
Type:composer-plugin
pkg:composer/saucebase/module-installer
Requires
- php: ^8.4
- composer-plugin-api: ^2.0
- symfony/finder: ^7.0|^8.0
- symfony/process: ^7.0|^8.0
Requires (Dev)
- composer/composer: ^2.11.1
- laravel/pint: ^1.32.1
- phpunit/phpunit: ^12.5.42
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A Composer plugin that installs Saucebase modules into the correct directory. It ships with saucebase-dev/saucebase, so every module your project requires is placed where the app can find and load it.
How It Works
- Registers a Composer installer for a configurable module package type (defaults to
saucebase-moduleandlaravel-module). - Installs each module inside a configurable modules directory (
modules/by default, lowercase). - Derives the directory name from the package slug:
saucebase/example-module→modules/example-module. - On
composer update, merges the new version into your existing module directory by default, preserving local edits. Conflicts are staged in the git index for review. - Removes configurable directories (e.g.
.github,.git) after each install or update. - Deploys frontend framework files (Vue / React) based on the project's
saucebase.jsonselection. - Protects path-repository modules (symlinks or local
.gitclones) from being overwritten or deleted. - Skips folder deletion by default when a module is removed via
composer remove— the directory stays on disk for manual review.
Requirements
- PHP 8.4 or newer
- Composer 2.x
- A project based on
saucebase-dev/saucebase(the core already requires this plugin)
Installation
saucebase-dev/saucebase already requires this package. When you install the core, Composer pulls in the plugin and activates it through the Saucebase\\ModuleInstaller\\Plugin class — no extra configuration is needed for a typical Saucebase project.
Need the installer for a different Composer project? Require it directly:
composer require saucebase/module-installer
Configuration Reference
All options live in the extra section of the root composer.json. Every key is optional.
| Key | Default | Description |
|---|---|---|
module-type |
saucebase-module and laravel-module |
Composer package type the installer handles. Setting it replaces both defaults with that one type. |
module-dir |
modules |
Directory modules are installed into. |
module-exclude-dirs |
[".github", ".git"] |
Directories removed from each module after install/update. |
module-update-strategy |
merge |
merge preserves local edits; overwrite replaces entirely. |
module-delete-on-remove |
true while backups are on |
Deletes the module folder on composer remove, after backing it up. false keeps it. |
module-backups |
3 |
Backups kept per module before an update or removal changes its folder. 0 turns them off. |
module-allow-symlink |
false |
Set to true to allow a symlinked path repository outside the app. |
Example (the Saucebase app configuration):
{
"extra": {
"module-type": "saucebase-module",
"module-dir": "modules"
}
}
Configuring the Module Type
The installer registers the saucebase-module and laravel-module package types by default. Override it in the root package extra section:
{
"extra": {
"module-type": "acme-module"
}
}
Any module packages must set their composer.json type to the same value.
Configuring the Install Location
By default, modules are installed under modules/ at the project root. Change this with the module-dir key:
{
"extra": {
"module-dir": "custom-modules"
}
}
With the configuration above, saucebase/example-module installs to custom-modules/example-module.
Configuring Excluded Directories
After each install or update, the installer removes directories that should not land in production. The default list is .github and .git. Override it with module-exclude-dirs:
{
"extra": {
"module-exclude-dirs": [".github", ".git", ".vscode", "tests"]
}
}
Set it to [] to skip all removal. The configured list replaces the default entirely.
Configuring Update Behaviour
By default, when you run composer update, the installer merges the new package version into your existing module directory rather than replacing it.
| Strategy | What happens on composer update |
|---|---|
merge (default) |
New files from the package are added. Your edited files are kept as-is. Conflicts between your edits and upstream changes are staged in the git index (conflict markers in the file, both sides visible in git diff). |
overwrite |
The module directory is replaced entirely with the new package contents. All local edits are lost. |
Keeping your customisations (default)
No configuration needed. Running composer update adds new files without touching files you have already edited.
Note: Because your local files take precedence, bug fixes or updates to files you have customised will not be applied automatically. To pick up an upstream change to a specific file, delete it and re-run
composer update.
Merge conflicts
When both you and the upstream package changed the same file, the installer stages a 3-way conflict in the git index (stages 1/2/3). The working-tree file contains standard conflict markers and git status shows it as a conflict for normal resolution via git mergetool or your editor.
Full overwrite (CI / reproducible builds)
Set the strategy to overwrite to produce an exact copy of the package on every update:
{
"extra": {
"module-update-strategy": "overwrite"
}
}
This is recommended for CI pipelines and staging environments where reproducibility matters more than preserving local changes.
Getting a completely fresh copy of a module
With the merge strategy (default), delete the module directory and run composer update:
rm -rf modules/example-module composer update vendor/example-module
The installer performs a clean install into the now-empty path.
Removing a Module
composer remove backs the module folder up to storage/app/module-backups/ (see module-backups), then deletes it. A folder the installer doesn't own is never deleted: a symlink or a git checkout (.git folder or file) is refused, so a path repository's symlink or git folder stays. A path repository copied without its .git is backed up and deleted like any other. A folder another installed module still uses stays too.
With module-backups: 0 there is no backup, so the folder stays unless module-delete-on-remove is true. To always keep it:
{
"extra": {
"module-delete-on-remove": false
}
}
- Keeping the module directory (module-delete-on-remove is off); delete it to finish removing the module: modules/example-module
Delete the folder yourself then: an app with Saucebase core refuses to boot with a module folder Composer doesn't autoload.
Safety
The installer only ever changes its own copy of a module in modules/. Before it deletes or
rewrites anything there, it checks the folder is a real directory inside modules/, not a
symlink and not a git checkout, and stops with an error otherwise.
Backups
Before an update or a removal changes a module's folder, the installer archives it, hidden files
included, to storage/app/module-backups/<module>/<date>-<version>.tar.gz. Laravel's
storage/app/.gitignore already keeps them out of git. The newest three are kept per module; set
module-backups to change that, or 0 to turn them off. To restore one:
tar -xzf storage/app/module-backups/billing/20261101-120000-v2.1.0.tar.gz -C modules
Path repositories outside the app
A path repository is symlinked by default, so a module from a folder outside the app would be
changed in that folder (its .git, .github and unused framework files removed). The installer
refuses it and prints the fix. Copy the module instead:
{
"repositories": [
{ "type": "path", "url": "../my-modules/*", "options": { "symlink": false } }
]
}
To keep the link anyway, set "module-allow-symlink": true. The installer then leaves the
linked module as it is.
Frontend Framework Support
When a module ships JavaScript resources for multiple frameworks (Vue, React, or Svelte), the installer copies only the files for the active framework flat into resources/js/ and removes the other framework subdirectories.
The active framework is read from saucebase.json in the project root:
{
"framework": "vue"
}
Valid values are vue and react. If saucebase.json is absent, unreadable, or has "contributor": true, no framework files are copied and all framework subdirectories are left in place.
Module packages should ship framework files under subdirectories matching the framework name:
resources/
js/
vue/
Component.vue
react/
Component.tsx
After install with "framework": "vue" active, the result is:
resources/
js/
Component.vue ← copied flat from vue/
Creating Sauce Base Modules
To ship a module that works with this installer:
-
Set the package
typein the modulecomposer.jsonto whatever the application expects (e.g.saucebase-module). -
Organise module code inside the directory the installer will create (e.g.
modules/your-module-name). -
Have consumers install the module through Composer:
composer require vendor/your-module-name
The plugin derives the install directory from the package slug.
vendor/your-module-nameinstalls tomodules/your-module-name(or the configuredmodule-dir).
Local Development
Clone the repository and install dependencies:
composer install
Useful scripts:
composer test— run the PHPUnit 12 suite (tests/).composer lint— apply Laravel Pint formatting;composer lint:testchecks it, as CI does.composer validate— verify Composer metadata.
License
Licensed under the MIT License.