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.
drupal/core-composer-scaffold is a Composer plugin (not a library you call) that copies "scaffold" files — index.php, .htaccess, robots.txt, web.config, sites/default/default.settings.php, etc. — out of drupal/core and into your project's web root on every composer install / composer update. Think of it as php artisan vendor:publish for Drupal's docroot: idempotent, Composer-driven, and configured entirely from extra in composer.json.
If you are working in a pure Laravel app, you will not add this as a runtime dependency. You meet it when you touch Drupal 9/10/11 Composer projects (drupal/recommended-project), or when you want to borrow its pattern for a shared-asset plugin of your own.
Minimum setup:
composer require drupal/core-recommended:^10 drupal/core-composer-scaffold:^10
Then make Composer 2.2+ allow it, and point it at your docroot:
{
"config": {
"allow-plugins": {
"drupal/core-composer-scaffold": true,
"drupal/core-project-message": true,
"dealerdirect/phpcodesniffer-composer-installer": true
}
},
"extra": {
"drupal-scaffold": {
"locations": {
"web-root": "web/"
}
}
}
}
Run composer install and verify web/index.php, web/.htaccess, and web/robots.txt exist. Re-run out-of-band with:
composer drupal:scaffold
Where to look first:
composer.json → extra.drupal-scaffold (your project's overrides).vendor/drupal/core/composer.json → extra.drupal-scaffold.file-mapping — this is the canonical list of what core ships and from where (assets/…).vendor/drupal/core-composer-scaffold/README.md for the full option reference (locations, file-mapping, allowed-packages, gitignore, omit-default-file-mappings).First use case: create a project skeleton into web/, commit your app code (web/sites/default/settings.php, web/modules/custom), and let Composer regenerate the boilerplate on every install.
Baseline project config. Keep the project's extra.drupal-scaffold small; core already declares its own mappings.
"extra": {
"drupal-scaffold": {
"locations": { "web-root": "web/" },
"gitignore": true
}
}
Append instead of hand-editing. Never edit web/.htaccess directly — it will be rewritten. Use append/prepend with a file you control:
"file-mapping": {
"[web-root]/.htaccess": {
"mode": "append",
"src": "assets/.htaccess-extra"
},
"[web-root]/robots.txt": {
"mode": "replace",
"src": "assets/robots.txt"
},
"[project-root]/README.md": false
}
mode is replace (default), append, or prepend; assigning false removes the mapping entirely (useful when you want to commit your own file and stop the plugin from touching it). Mappings support the [web-root] and [project-root] tokens; paths without a token are relative to the project root, and src is relative to the contributing package root.
Per-package overrides. A contrib module or internal package can declare its own extra.drupal-scaffold; the project's config wins on merge. Whitelist non-core contributors explicitly:
"drupal-scaffold": {
"allowed-packages": ["drupal/core", "myorg/my-drupal-assets"]
}
Only packages in allowed-packages (plus drupal/core) are permitted to contribute scaffold files. This is the extension point to use — it beats a post-install shell script because it gets the same idempotency and dry-run-ish output as core.
Deploy pipelines. Scaffolding is a Composer script, so it runs with Composer, not after it. The safe production sequence is:
composer install --no-dev --optimize-autoloader # scaffolds here
composer drupal:scaffold # belt-and-braces re-run
drush deploy
If your build separates the Composer step from the artifact, run composer drupal:scaffold in the image build, not in the running container (see gotchas about read-only filesystems).
Borrowing the pattern for Laravel. If you maintain several Laravel services and keep copying the same .htaccess, docker/, phpunit.xml stubs, write a tiny composer-plugin (type composer-plugin, implement PluginInterface + EventSubscriberInterface on post-install-cmd/post-update-cmd) that copies from a stubs/ directory into the project root. The Drupal plugin is a good reference implementation: file mappings as data, replace/append/prepend modes, project-level overrides, and an explicit allow-plugins gate. It is effectively a generalized vendor:publish that runs at Composer time.
allow-plugins is mandatory on Composer 2.2+. Without the config.allow-plugins entry, Composer silently skips the plugin, web/index.php never appears, and Drupal 404s. Check composer diagnose and the install log for "plugin was skipped".file-mapping append/prepend, or set the mapping to false and commit your own copy.gitignore defaults to true. The plugin adds scaffold outputs to .gitignore so they are not committed. If your host/deploy requires the files in the repo (common with shared hosting or "upload the tarball" workflows), set "gitignore": false — otherwise a fresh clone without composer install will be empty under web/.--no-scripts and --no-plugins silently disable it. CI images built with composer install --no-dev --no-scripts ship without index.php. Pair --no-scripts with an explicit composer drupal:scaffold step, or drop the flag.web-root must be set for non-default layouts. If your docroot is public/ or docroot/, set locations.web-root; otherwise core tries to write into web/ and fails confusingly. Paths are project-root-relative, not vendor-relative.drupal/core-composer-scaffold must match the Drupal core major (^10 with Drupal 10, ^11 with Drupal 11). A stale plugin against a newer core produces missing-file or mapping errors after a core jump.allowed-packages is opt-in per dependency. Installing a contrib module that declares scaffold files does nothing until you add its vendor name to allowed-packages. Verify with composer drupal:scaffold output — it lists each copy/skip decision.web/ (or vendor mount) fail on post-install-cmd. Run composer drupal:scaffold during image build, or set locations to a writable path.post-install-cmd scripts touching the same paths, they run in declared order; scaffold can stomp on them or vice versa. Let the scaffold finish before any script that edits web/.htaccess or web/index.php.composer drupal:scaffold is your debugging hammer. When you cannot explain why a docroot file is missing or stale, run it and read the per-file "copy/skip" lines. It will regenerate directories it needs but will not repair a broken web-root config.omit-default-file-mappings lets you start from a blank slate (true) and hand-roll every mapping — useful for headless/minimal Drupal installs, but you then own all core scaffold files forever, including future additions.laravel/framework integration, no service provider, no config file. On a Laravel project the only legitimate uses are (a) maintaining Drupal sites in the same monorepo, or (b) copying the plugin pattern into an internal composer-plugin. Do not try to make it scaffold public/index.php for a Laravel app — laravel/laravel via composer create-project plus vendor:publish covers that.How can I help you explore Laravel packages today?