roots/wordpress-packager
Roots tooling that builds and publishes Composer packages for WordPress core and plugins, so you can install WordPress and extensions as standard dependencies with version constraints. Automates release packaging for Bedrock-style projects managed entirely through Composer.
Understand what you install first. roots/wordpress-packager is a build tool, not a runtime dependency. It clones/downloads WordPress from wordpress.org, deletes the bundled themes (twentytwenty*) and plugins (akismet, hello.php), rewrites the manifest, and publishes the result as the Composer package roots/wordpress (the repo also publishes a fuller variant that keeps bundled themes — check Packagist before assuming which you want). In day-to-day work you almost never run the packager itself; you consume its artifact. The only time you clone the repo is when you need to produce a bespoke artifact (patched core, internal mirror, air-gapped build).
Where to look first:
composer.json → the scripts block, to see the exact local build invocation, and the require block, to see the WP source/version plumbing.src/ → the PHP classes doing download, strip and repackage. This is where you'd add exclusions..github/workflows/ → the release pipeline; it is the canonical, working example of a full build, more reliable than any README snippet.Minimal setup in a Bedrock (or Bedrock-style) project:
composer require roots/wordpress-core-installer
composer require roots/wordpress:^6.5
Then in composer.json:
{
"config": {
"allow-plugins": {
"roots/wordpress-core-installer": true,
"composer/installers": true
}
},
"extra": {
"wordpress-install-dir": "web/wp",
"installer-paths": {
"web/app/plugins/{$name}/": ["type:wordpress-plugin"],
"web/app/themes/{$name}/": ["type:wordpress-theme"]
}
}
}
composer install drops core into web/wp instead of vendor/, leaving web/app (your wp-content) entirely under your control.
First use case — upgrading WordPress: composer require roots/wordpress:^6.6, run wp core update-db, smoke-test the admin, commit composer.json + composer.lock. That single lockfile diff is your entire, reviewable WordPress upgrade — reproducible on every environment and CI run.
Pinned-core, lockfile-driven upgrades. Constrain to a minor (^6.6) and let the lockfile pin the patch. Upgrades become a two-command ceremony:
composer outdated roots/wordpress
composer update roots/wordpress --with-dependencies
wp core version && wp core update-db
Never loosen to *; WordPress patch releases occasionally break plugins, and you want that surfaced as an explicit lockfile diff in a PR.
Treat web/wp as immutable build output. This is the single most important architectural habit the package enables. Add /web/wp/ to .gitignore (Bedrock does this by default), disable WP's own updater (DISALLOW_FILE_MODS, AUTOMATIC_UPDATER_DISABLED, WP_AUTO_UPDATE_CORE => false in config/application.php), and let Composer be the only writer. Anything that writes into core will silently vanish on the next deploy.
Deploy hook pattern (Forge/Deployer/Envoyer):
composer install --no-dev --no-interaction --optimize-autoloader
wp core update-db --path=web/wp
wp cache flush
Install runs before the symlink swap, so the new release is fully composed on disk. Because web/wp is a Composer dist zip, the install step is fast and cacheable — a good candidate for CI artifact caching keyed on composer.lock.
Keep the plugin/theme layer composable. With core packaged, everything in web/app is yours to declare:
"require": {
"wpackagist-plugin/advanced-custom-fields": "^6.2",
"wpackagist-plugin/redis-cache": "^2.5"
},
"repositories": [
{ "type": "composer", "url": "https://wpackagist.org" },
{ "type": "path", "url": "packages/my-plugin", "options": { "symlink": true } }
]
Use type: path with symlink: true for in-house plugins you're actively developing — you get hot editing without a cp -r dance, while third-party plugins stay version-pinned.
Self-hosted artifact pattern (private fork of the packager). If you need a patched core, an internal mirror, or a build that strips additional files, fork the repo, adjust the strip list / src/, and publish to Satis, Private Packagist or a cloud Composer registry. You then depend on yourco/wordpress and the whole workflow above is unchanged — that's the real payoff of the packaging abstraction.
CI matrix pattern. Because the core version is data in composer.lock, you can test a plugin against two WP versions cheaply: composer require roots/wordpress:^6.5 in one job, ^6.6 in another, sharing the rest of the install. Cheap, high-signal compatibility checks before a core bump lands on production.
Hybrid Laravel pattern. Common in agencies running Laravel and WordPress side by side: use this to keep core versioned, then read WP data from Laravel via Corcel or expose Bedrock's wp_users for shared auth. The value is that the WP half is now a Composer dependency like any Laravel package — same lockfile discipline, same deploy pipeline, no FTP.
allow-plugins is the #1 first-run failure. Composer 2.2+ refuses to run roots/wordpress-core-installer unless it's allow-listed. On CI, the error is easy to miss behind --no-interaction; add the config to composer.json, not just your local global config.twentytwentyfour doesn't exist. Fallbacks, block-theme tests, and wp theme list behave differently. If a plugin assumes a default theme, require it explicitly from WPackagist or switch to the full variant.wp core update / wp core download must not be used. They write into web/wp and get clobbered. Also expect wp core verify-checksums to be noisy or fail — the artifact is repackaged, so don't gate deploys on it. Verify with composer show roots/wordpress instead.composer update does not run the DB upgrade. Bedrock-era WP is usually tolerant, but always run wp core update-db in the deploy, and check wp core is-installed before it in fresh environments.wp-content ownership. The packager ships core only. WP_CONTENT_DIR/WP_CONTENT_URL point at web/app; if you see themes installed under web/wp/wp-content, your installer paths or constants are wrong. Check with wp --info and wp eval 'echo WP_CONTENT_DIR;'.@dev stability flag will. Keep minimum-stability at stable and use per-package stability flags if you truly need a beta.composer install fails non-deterministically. Cache ~/.composer/cache in CI or use a Satis mirror for large teams.web/app/languages and survive core upgrades — but they aren't managed by Composer. Decide deliberately: wp language core update in deploy, or pin them in a plugin.composer why roots/wordpress (who pulled it in), composer show roots/wordpress (installed version), wp core version, then git diff on the lockfile to see whether the version actually moved. Nine times out of ten the answer is that web/wp was edited in place and never re-installed.wp core update-db --down.How can I help you explore Laravel packages today?