civicrm/composer-compile-plugin
Composer plugin that adds a lightweight compile step for CiviCRM extensions and other PHP packages. Declare compile tasks in composer.json and it runs them after install or update, skipping unchanged packages via caching. A composer compile command allows manual rebuilds.
1. Install the plugin in the root project (it is a Composer plugin, not a library):
composer require --dev civicrm/composer-compile-plugin
Composer 2.2+ will refuse to load it unless you allow it. Either answer y at the prompt or add explicitly:
{
"config": {
"allow-plugins": {
"civicrm/composer-compile-plugin": true
}
}
}
2. Where to look first. Two places, in this order:
extra.compile block of any installed package that uses it — that is the entire public API surface. composer show -a <vendor/pkg> then read the package's composer.json in vendor/.type, name, path, script/command keys) for the version you pinned. The plugin is small and low-traffic; treat the README shipped with your pinned tag as authoritative over blog posts.After a run, inspect the package directory under vendor/ — you should see generated artifacts plus a marker/state file the plugin uses to remember that compilation already happened.
3. First use case. You maintain an internal package that ships sources (SCSS, TypeScript, a .phar build script, generated PHP proxies) and you want consumers to get built output on composer install without committing it. Add a compile task to that package's composer.json, then in a scratch project requiring it via a path repository, run composer install -vvv and watch the task execute. That loop — path repo + -vvv — is the fastest way to learn the plugin.
Declaring tasks (in the library/consumer package, not the root):
{
"name": "acme/widgets",
"extra": {
"compile": [
{
"name": "Build CSS/JS",
"type": "script",
"path": ".",
"script": ["npm ci", "npm run build"]
}
]
}
}
Tasks run in listed order, in the package's own directory, when the package is installed or updated. Ordering across packages follows Composer's install graph, so a package whose compile step depends on another package's output should declare that dependency in require (not just assume ordering).
Library vs. root split. The golden rule: libraries declare extra.compile; only the root project installs the plugin. If a library puts the plugin in its own require, you force a plugin (and arbitrary install-time code execution) onto every consumer. Document instead: "If you install from source, add civicrm/composer-compile-plugin to your root require-dev."
Dev-vs-prod gating. Putting the plugin in the root's require-dev means composer install --no-dev in production won't load it, so no compilers (Node, Sass, etc.) are needed on prod boxes. Ship prebuilt artifacts for prod, or accept that prod installs from source will be missing generated files — decide deliberately and write it down.
Root-project build hooks. For your own code, plain Composer scripts (post-install-cmd, post-update-cmd) is simpler and needs no plugin. Reach for this plugin specifically when the build step must live inside a dependency's composer.json so it travels with the package.
CI pattern. Cache node_modules/compiler toolchains, run composer install, then assert the expected artifact exists (a file-existence check in a test) rather than trusting exit codes alone. Compilation is a side effect; make it observable.
Local development loop.
{
"repositories": [{ "type": "path", "url": "../widgets", "options": { "symlink": false } }],
"require": { "acme/widgets": "*" },
"require-dev": { "civicrm/composer-compile-plugin": "^0.x" }
}
"symlink": false is important: with symlinks, the "package directory" is your working copy, and compile artifacts land in your source tree instead of a disposable vendor/ copy — which is sometimes what you want, sometimes a mess.
allow-plugins means the plugin never loads and nothing is printed. composer install -vvv and look for plugin/event lines before debugging anything else.vendor/ is disposable. Compiled output lives inside vendor/<pkg> and is wiped by composer update, composer reinstall, and often by rm -rf vendor. Never hand-edit or point production at a hand-patched build; if you need persistence, commit the artifacts to the package repo and make the compile step optional.composer reinstall <vendor/pkg> (Composer 2.4+) or delete the package's marker/state file. Check the README for the exact filename and any force/refresh environment variable before inventing your own workaround.npm, node, sass, php extensions, network access to registries — none of this is declared by extra.compile. A task that needs network will fail on air-gapped build agents. Prefer tasks that use vendored/local tools, and pin tool versions in the command itself (e.g., npx --no-install).script tasks execute through the shell; rm -rf, cp -r, and && chains behave differently on Windows. If Windows matters, write a small PHP script and invoke php build.php instead of shell syntax.npm run build shouldn't corrupt anything) and must exit non-zero on failure — a compile that "succeeds" while emitting broken output is worse than a failed install. Add a post-task artifact check where you can.require in libraries, pin the plugin version in the root, and review any third-party package that declares extra.compile the same way you'd review a post-install-cmd script.extra.compile.How can I help you explore Laravel packages today?