drupal/core-project-message
A small Composer plugin used by Drupal project templates to display a helpful post-install message. After Composer creates a Drupal project, it surfaces next steps, documentation links, and setup guidance directly in the console, making it easier for new users to get started.
drupal/core-project-message is a tiny Composer plugin (type composer-plugin) that subscribes to Composer's post-create-project-cmd event and prints whatever lines you configured in the root composer.json. It does no installation work itself — it is a declarative "welcome banner" hook. It ships as part of Drupal's recommended-project/legacy-project templates, but the mechanism is framework-agnostic and works for any composer create-project flow you control.
Install
composer require drupal/core-project-message
Put it in require (not require-dev) of the template package you intend to create-project, because create-project installs the template's runtime requirements before the event fires.
Allow the plugin (Composer ≥ 2.2)
composer config allow-plugins.drupal/core-project-message true
Or commit it in composer.json:
"config": {
"allow-plugins": {
"drupal/core-project-message": true
}
}
Where to look first
extra.drupal-core-project-message.post-create-project-cmd-message array — this is the entire public API surface.vendor/drupal/core-project-message/src/Plugin.php (namespace Drupal\CoreProjectMessage) — ~40 lines; read it before assuming behavior.composer.json for the extra.class autoload wiring.First use case — brand the first-run output of your company starter kit:
"extra": {
"drupal-core-project-message": {
"post-create-project-cmd-message": [
"<bg=green;fg=black> Acme Site installed </>",
"",
"Next steps:",
" 1. cp .env.example .env",
" 2. composer run dev",
" Docs: https://internal.acme/site-template"
]
}
}
Then composer create-project acme/site-template ./my-site prints it once, right after the scaffold and post-create-project-cmd scripts complete.
Declarative banner in the template. Each array entry becomes one output line. Keep it to onboarding instructions that are only true at creation time — credentials to set, the dev server command, where to file issues. Do not put anything that must be revisited later.
White-labeling a reusable starter kit. Since the message is read from the installed root package's extra, the same template can be re-badged by forking and editing only composer.json. No PHP changes, no plugin fork.
Symfony Console styling. The strings are passed to Composer's IO, so <bg=blue;fg=white>…</>, <info>, <comment>, <error>, and <href=…> all work. Style block-level banners with full-width padding so they render as a solid bar:
"<bg=blue;fg=white> </>",
"<bg=blue;fg=white> Congratulations, your site is installed </>",
"<bg=blue;fg=white> </>"
Separate "print" from "do". This plugin only prints. Any real first-run work (generating keys, copying .env, seeding a DB) belongs in scripts.post-create-project-cmd in the same composer.json; the two run in the same phase, so the banner and the actions feel like one onboarding step.
"scripts": {
"post-create-project-cmd": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
"@php artisan key:generate --ansi"
]
}
Reusing the pattern outside Drupal. If you want messages on post-install-cmd/post-update-cmd too, this package will not do it — copy Plugin.php into a private composer-plugin package and add the extra event keys to getSubscribedEvents(). That is roughly a 20-line fork and is the intended extension point.
Local test loop. Point a path repository at your template and run create-project into a throwaway directory:
composer create-project acme/site-template /tmp/site-test \
--repository='{"type":"path","url":"../site-template","options":{"symlink":false}}'
Delete /tmp/site-test between runs; the event only fires on a genuinely new project directory.
CI integration. For smoke tests, assert on the output with --no-ansi so the formatter tags become plain text and greppable:
composer create-project acme/site-template /tmp/ci --no-ansi | grep -q "Next steps"
allow-plugins. On Composer ≥ 2.2 an unallowed plugin is skipped with only a warning during install, so the banner never appears and there is no error at create-project time. In non-interactive CI, set it via composer config allow-plugins... true before running.post-create-project-cmd only. Editing the message array in an existing checkout does nothing — install and update never dispatch this event. Do not use it for upgrade notices; use a post-update-cmd script or your own plugin.--no-install and --no-scripts kill it. composer create-project --no-install skips the install phase entirely, so the listener never runs. Verify your onboarding docs don't recommend flags that suppress it.extra key is misspelled or the value is not an array, the plugin finds nothing and prints nothing — no exception, no warning. When a banner "disappears," diff the key name first.OutputFormatter only understands its own tag grammar; unknown tags typically pass through literally, and text containing < or > (e.g. <[email protected]>) can be swallowed or mangled. Escape or rephrase angle-bracket content.post-create-project-cmd scripts fire for the same event; do not assume the banner prints before or after your .env generation. If sequencing matters, keep the banner purely informational.--quiet. They show up in CI logs and shell history captures — never put tokens, one-time passwords, or internal hostnames you would not commit to a repo.composer-plugin-api) and re-test after major Composer upgrades, since event names and IO behavior are Composer's to change.Plugin.php into a proprietary product; reimplementing the idea (it is trivially small) is usually cheaper than a license review.How can I help you explore Laravel packages today?