drupal/core-composer-scaffold
Composer plugin used by Drupal core to scaffold essential project files such as .htaccess, robots.txt, and settings into the web root during install and update, with configurable file mappings and overwrite behavior.
What the package actually is. drupal/core-composer-scaffold is a Composer plugin (type: composer-plugin), not a Laravel package and not a library with a PHP API. It subscribes to Composer's post-create-project-cmd, post-autoload-dump, post-install-cmd and post-update-cmd events and copies files out of dependency packages into a configured project root ("scaffold") location. Behavior is driven entirely by declarative config in the root composer.json under extra.drupal-scaffold: allowed-packages, file-mapping, locations (web-root), gitignore, omit-default-file-mappings.
Architecture fit: adjacent, not native. Laravel has no first-class equivalent. New projects are handled by composer create-project laravel/laravel (a skeleton), and post-creation boilerplate is normally either committed by hand, generated by an Artisan command we write ourselves, or spread across tooling (laravel/pint, PHPStan, Docker, CI). The plugin's real value to us is as a generic, config-driven file distributor: publish phpstan.neon, pint.json, .editorconfig, docker-compose.yml, .github/workflows/*, base .env.example, Makefile from one internal acme/laravel-scaffold package into N services, with per-project overrides via file-mapping modes (skip, append, prepend, overwrite). That is a genuine, if narrow, fit — it addresses config drift across a fleet of Laravel repos, which is a recurring TPM-visible problem.
Alternatives and why this one is still on the table:
| Option | Trade-off |
|---|---|
| Custom Artisan command in a shared package | Full control, no GPL, no plugin code execution; but requires writing ~300–500 LOC of idempotent copy/conflict logic and installing the package first (chicken-and-egg) |
Custom composer create-project skeleton |
Best for greenfield; useless for the 20 existing services we actually need to standardize |
| Manual PRs / repo template sync | No new dependency; drift returns within two quarters |
drupal/core-composer-scaffold |
Battle-tested at very large scale, zero code we own; but GPL-2.0, Drupal-centric docs, plugin executes at install time |
Integration feasibility: yes, with conditions. Requires Composer 2.x and an explicit config.allow-plugins entry (drupal/core-composer-scaffold: true) — Composer 2.2+ blocks unlisted plugins by default, which is a deliberate hardening feature we would be opting out of for this one package. Setting locations.web-root to . maps files into the Laravel project root rather than web/, which is the only structural change needed. PHP 8.x and Laravel 10/11/12 impose no constraints.
Technical risk.
composer.lock committed, provenance verified.Repository: unknown plus 54 stars and a 0.63 score is a reporting artifact, not necessarily a quality signal — the canonical source is Drupal.org's git with a GitHub mirror, and the package ships as a transitive dependency of effectively every Drupal 9/10/11 site, so real-world install volume is enormous. Still, we must confirm the exact origin and checksum before pinning; a low-signal registry entry is not something to pull into 20 repos on trust..env.example, phpunit.xml, and CI workflows are routinely edited per project, and surprising overwrites would be a trust-destroying incident.post-autoload-dump, the same event Laravel uses for @php artisan package:discover. Ordering between the two is not something we control, so nothing scaffolded should be required during that step.Key questions to resolve before committing.
composer.json for this package match what the mirror shows, and what is the pinned version + hash we are approving?--no-plugins hardened production build in use anywhere? If so, the plugin is a no-op in exactly the environment where correctness matters most.acme/laravel-scaffold package, or is this a solution looking for a problem? Quantify current config drift first.Stack fit. Composer 2.2+, PHP 8.1+, Laravel 10–12. No framework coupling, no service provider, no runtime footprint — the plugin is inert outside Composer commands. That is the main architectural virtue: it cannot break request handling, queue workers, or Octane.
Configuration shape. Root composer.json per service:
"config": { "allow-plugins": { "drupal/core-composer-scaffold": true } },
"require-dev": { "acme/laravel-scaffold": "^1.0" },
"extra": {
"drupal-scaffold": {
"locations": { "web-root": "." },
"allowed-packages": ["acme/laravel-scaffold"],
"gitignore": false,
"file-mapping": {
"[project-root]/phpstan.neon": { "mode": "skip" },
"[project-root]/.editorconfig": { "mode": "overwrite" },
"[project-root]/.github/workflows/ci.yml": { "mode": "skip" }
}
}
}
skip for files teams legitimately customize; overwrite only for files that must never diverge. gitignore: false unless we want the plugin appending managed blocks to .gitignore.
Migration path. Greenfield-first, brownfield by exception.
package:discover, behavior under composer install --no-dev, and behavior when allow-plugins is absent (expect a hard failure we can tolerate). Output: a decision memo, not code.acme/laravel-scaffold with 5–8 genuinely shared files only. Apply to 2–3 willing services. Measure: files still diverging after adoption, CI failures attributable to the plugin, developer complaints.Compatibility and sequencing cautions. Do the allow-plugins change and the require-dev addition in the same PR — an allow-plugins: true entry with no installed plugin is harmless, but a plugin present without the allowance fails the build with a confusing message. Land the internal scaffold package and publish a first tag before touching consumer repos. Keep the plugin in require-dev; production deploys should not need scaffolding.
Honest sequencing alternative. Run steps 1–2 in parallel with a one-week evaluation of the ~400-LOC custom-Artisan-command option. If the internal scaffold package is going to hold fewer than ~8 files and fewer than ~10 repos, the custom command wins on license, control, and readability. The Drupal plugin only pays off at fleet scale.
Maintenance. Upstream tracks Drupal core's release cadence (minors plus security releases), which means a steady but low-volume stream of patch and minor updates. Version majors align to Drupal majors, so pinning to a caret range on a single major is stable. Our own maintenance burden is the internal scaffold package, not the plugin — but that package is now a shared dependency for every Laravel service, which converts a previously invisible concern (config drift) into a Tier-2 platform responsibility with an on-call implication we must staff explicitly.
Support. No Laravel-community support path. The issue queue is Drupal's and the documentation assumes web/ roots and drupal/core defaults. Expect the first non-obvious failure to be diagnosed internally from source. Budget one engineer as designated owner; without that, adoption stalls the first time someone hits a confusing skip.
Scaling. Runtime cost is zero. Install cost is milliseconds. The scaling constraint is organizational: every consuming repo carries a composer.json block that can drift, so the config itself becomes an artifact needing standardization — mitigate by generating the block from a template or by keeping it identical verbatim across repos and diffing it in CI.
Failure modes and detection.
| Failure mode | Blast radius | Detection |
|---|---|---|
Plugin blocked by missing allow-plugins |
Build fails closed (good) | CI, immediate |
| GPL-2.0 flag in license gate | Blocks all builds in that pipeline | License scanner; add explicit exception or it will resurface every audit |
| Silent skip of a locally modified file | Drift persists with false confidence | Needs explicit CI assertion that key files match the source hash |
| Overwrite of a customized file | Lost work, developer trust incident | Spike must confirm the protective default; back up before first run in brownfield |
--no-plugins in hardened deploy path |
Scaffolding absent in prod; usually harmless, occasionally not | Audit deploy scripts before rollout |
| Upstream major bump changes mapping semantics | Fleet-wide breakage on composer update |
Dependabot/Renovate with the internal package grouped; never auto-merge majors |
| Stale pinned version accumulates CVEs | Toolchain exposure | Standard dependency update cadence |
Ramp-up. Technical ramp-up is genuinely low — one config block, half a day for a competent Laravel developer. Non-technical ramp-up is the real cost: license exception, security review, and provenance verification will consume more calendar time than the engineering. Plan for 3–5 weeks from decision to first production service, of which perhaps 3 days is code.
Verdict. The package is technically sound and proven at a scale Laravel shops rarely encounter, and the low star count is a registry artifact rather than a quality signal. But the fit is opportunistic — Laravel has no need for the Drupal scaffold model — and the costs (GPL-2.0, install-time code execution, provenance verification, platform ownership) are all incurred up front, while the benefit is contingent on having a real fleet-wide config-drift problem. Recommend: authorize the one-week spike and the governance review in parallel, and hold the rollout decision until both close. If the answer to "how many Laravel repos need to share files" is under ten, the plugin is the wrong tool and a thin internal Artisan command is the better product decision.
How can I help you explore Laravel packages today?