feat(modules): composer as a delivery channel
A module distributed to other sites is an ordinary package with
"type": "johncms-module", installed with composer require. The CMS finds it
by asking the Composer runtime which packages of that type are installed —
InstalledVersions::getInstalledPackagesByType(), already in the vendor
directory — so the module simply stays where Composer put it.
That is not what the plan said. It called for composer/installers plus
oomphinc/composer-installers-extender, which move a package of a custom
type into modules/{$vendor}/{$name}/. Both were installed and both worked,
but Composer reports the extender as abandoned with no replacement, and
without it the custom type means nothing and the package lands in vendor/
anyway. An abandoned dependency in the core, to move a directory, is a bad
trade. Both are gone again.
So a module can now come from two places, and ChainModuleRepository puts
them behind one answer: modules/ first, vendor/ second. Nothing above it
knows the difference — the manifest carries the path.
The hooks only speak. After installing a package they print the command to
finish with and note the key; before removing one they warn that the tables
and the data stay and point at module:uninstall --purge. The previous
attempt at this booted the whole application inside the Composer process
and ran migrations from there, which meant composer install on a developer
machine migrating whatever database the configuration pointed at.
Verified against a real package through a path repository: require put it
in vendor/vasya/blog, module:list saw it as discovered, module:install
finished the job, and composer remove printed the warning.