aslamhus / wordpress-hmr
HMR setup with webpack and wp-scripts for wordpress development and production
Requires (Dev)
- phpunit/phpunit: ^9.6
This package is auto-updated.
Last update: 2026-03-30 18:06:45 UTC
README
Features
- Hot module replacement (HMR) for theme development (no CSS reloads!)
- Containerized WordPress environment with Docker
- Simple whr.json config for defining, conditionally enqueuing, and managing all theme assets
- Optimized production builds via wp-scripts with statically generated asset enqueuing
- Child theme scaffolding, WP-CLI, and PHPMyAdmin included out of the box
- One-command setup —
vendor/bin/whr installgets a full containerized WP site running from scratch - CLI tool — run WP-CLI commands, manage containers, and scaffold themes without touching Docker directly
Introduction
This is an experimental package inspired by wp-env, which allows you to develop containerized wordpress sites with Docker, Webpack and hot module replacement (HMR). It uses the Wordpress wp-scripts package in tandem with a custom enqueuing algorithm to enqueue assets in your theme.
WordpressHMR now comes with a CLI tool that allows you to spin up a containerized wordpress site with one command.
Defining and enqueuing assets is as simple as managing a single whr.json (Wordpress Hot module Replacement) file. This file contains the configuration for your assets, including the path to your scripts and styles, the hooks where they should be enqueued, and any conditions for enqueuing them.
Who is this package for?
Anyone who wants to develop Wordpress themes with the fast paced development experience of HMR in a containerized environment. If you're familiar with wp-env, setup is a breeze.
Why not just use wp-env?
wp-env is fantastic for custom block development, but it lacks the ability to hot reload styles and scripts. While you can leverage hot module replacement in custom block development, it is not currently possible to do so for theme development. This package combines the power of wp-scripts to manage asset dependencies with a dynamic enqueuing algorithm to provide hot module replacement for your theme development.
Requirements
This package requires Docker, PHP, Composer, and Node.js, similar to the setup for wp-env.
Getting started
- Install WordpressHMR package
composer require --dev aslamhus/wordpress-hmr
- Run the installer and follow the prompts.
vendor/bin/whr install
- Create a child theme
The default installation creates a wordpress site for you, but it is recommended that you create a child theme. To do so, simply run:
vendor/bin/whr create-child-theme
Follow the prompts to use a default theme or install your own. If you want to manually install a theme, simply add its directory to the public/wp-content/themes directory in your project.
- Start your container!
# start your container vendor/bin/whr start # start your container with HMR vendor/bin/whr start --hot
That's it! Try changing the css in your resources/assets/screen.scss file to see the changes immediately reflected in your browser. By default, the package adds two scripts to your theme, screen.js and editor.js. Each of these scripts is defined in the whr.json file with conditions to enqueue them in the public facing area of your wordpress site nd editor respectively. Each script imports a corresponding css file in your theme. Add more scripts or more css / scss files as you like. Try updating the css and see the changes reflected without a reload.
Webpack HMR Entry points limitation
This package supports multiple entry points, however HMR will only work with one entry point per page. This is a known issue with Webpack HMR when multiple entry points are loaded on the same page.
In development, I recommend having a single entry point per page so that you can take advantage of the fast paced development experience of HMR, and then use a separate config for your production environment. This can easily be facilitated with separate whr.json files, according to your environment.
Import a database by default
If you have a mysql dump of a database you'd like installed, place it in the Docker/data/db directory. To rebuild the container with this dump file imported, run vendor/bin/whr start --reset. Make sure your dump file uses the database wp_database.
If you want to use a different database name, you'll need to change it in the compose.yml file, found in your project root. Search for all references to wp_database and replace it with the database name of your choice.
Configuration (whr.json)
The whr.json file is where you define assets.
It is a simple JSON configuration file that contains the following properties:
config- This is where you define the configuration for your assets.assets- This is where you define your scripts. Each property of theassetsobject is a hook where you want to enqueue your script. The value of each property is an array of script objects.
Config
handlePrefix- This is the prefix for your asset handles. This is useful if you want to avoid conflicts with other plugins or themes.host- This is the host of your local development server.site- The container url.port- This is the port of your local development server.protocol- This is the protocol of your local development server.theme- the name of your theme. Change your theme manually in whr.json or create it using the CLI tool, vendor/bin/whr create-child-theme. If you change it manually, the theme will be activated automatically when you start your container (as long as that theme is available in your themes directory)src- Points to the src directory where your webpack entry points live. Files here will be emitted to the theme you specify.themePath- This is the path to your themes directory.
Assets
An array of script objects. Each script object contains the following properties:
handle- The handle of the script. This is the name that you will use to enqueue the script.hooks- An array of hooks where the script will be enqueued.path- The path to the script file.ext- The file extension of the script file.dependencies- An array of dependencies the script requires.condition- Set a condition to enqueue the asset. For more on conditional enqueuing, see the Conditional enqueuing section below.
Example
{ "config": { "handlePrefix": "my-asset-handle", "host": "localhost", "site": "http://localhost:8889", "port": "8889", "protocol": "http", "theme": "twentytwentyfive", "src": "resources", "themePath": "public/wp-content/themes/" }, "assets": { "enqueue_block_editor_assets": [ { "handle": "editor-js", "path": "/js/editor", "ext": "js" } ], "wp_enqueue_scripts": [ { "path": "/js/screen", "ext": "js", "condition": ["is_user_logged_in", null, false] } ] } }
Adding a new asset / HMR entry point
To add a new asset, simply add a new object to the assets array in the whr.json file. You will have to run vendor/bin/whr build to generate the necessary asset files.
Let's add some styles to our theme which we only want to appear on our custom template, "My Custom Template".
-
Create a scss/css file in your
resources/assets/cssdirectory. For example,my-custom-template.scss. -
Create a script in your
resources/jsdirectory that will import the scss file. For example,my-custom-template.js.import "../css/my-custom-template.scss";
-
Add the following to your
whr.jsonfile:{ "assets": { "wp_enqueue_scripts": [ { "path": "/js/my-custom-template", "ext": "js", "condition": ["is_page_template", "My Custom Template", true] } ] } } -
Run
vendor/bin/whr buildto generate the necessary asset files. -
Restart your development server with
vendor/bin/whr start. The styles should now be enqueued on the "My Custom Template" page.
You're done! Try changing the styles in your my-custom-template.scss file and see the changes reflected in your browser only on pages that use the "My Custom Template" page template.
For a list of condition functions that you can use, see the Wordpress Conditional Tags.
A note on block theme hooks
When enqueuing assets for block themes, you will need to use the following hooks:
enqueue_block_editor_assets - to load only in editor view
enqueue_block_assets - loads both on frontend and editor view
Conditional enqueuing
Example 1: Enqueue a script only on the block editor
In this example, we only want to enqueue a script in the block editor. We set the path to the script file as well as the extension. We don't need to specify a condition for this script, as it will be enqueued in the block editor by default.
{
"assets": {
"enqueue_block_editor_assets": [
{
"handle": "editor-js",
"path": "/js/editor",
"ext": "js"
}
]
}
}
Example 2: Enqueue a script only on the front end
Same as above, but we want to enqueue a script only on the front end. We use the wp_enqueue_scripts hook to enqueue the script.
{
"assets": {
"wp_enqueue_scripts": [
{
"path": "/js/screen",
"ext": "js"
}
]
}
}
Example 3: Enqueue a script only on a specific page template
In this example, we only want to enqueue a script on a specific page template. We use the get_page_template_slug function to get the page template slug and compare it to our custom template slug.
You can find the page template slug by looking at the classname of the body tag of any page given the template.
// for custom template { "assets": { "wp_enqueue_scripts": [ { "path": "/js/screen", "ext": "js", "condition": ["get_page_template_slug", "", "my-custom-template"] } ] } } // for default page template { "handle": "pages-feature-image", "path": "/js/templates/pages-feature-image", "ext": "js", "condition": ["get_page_template_slug", null, "page-template-default"] } // for a custom post type { "handle": "instructor-js", "path": "/js/instructor-single", "ext": "js", "condition": ["get_post_type", ["function", "get_the_id"], "custom-instructors"] } // get the id of the page { "condition": ["get_the_id", null, "2039"] } // targeting block editor styles only "enqueue_block_assets": [ { "handle": "editor-js", "path": "/js/editor", "ext": "js", "condition": ["is_admin", null, true] } ],
Example 4: Use a conditional argument that takes the result of a function as an argument
You can use a function as an argument by declaring the type in an array, followed by the function name, and then a list of optional arguments.
In this example, we only want to enqueue an asset on the about page. We need the id value of the current page to use the get_the_title method. We can use the get_the_id function to get the id of the current page.
This will evaluate to get_the_title(get_the_id()) === 'about'.
{
"assets": {
"wp_enqueue_scripts": [
{
"path": "/js/screen",
"ext": "js",
"condition": ["get_the_title", ["function", "get_the_id"], "about"]
}
]
}
}
Build for production
Building is a very simple process, and leverages wp-scripts to generate the necessary asset files. A little magic is performed by Aslamhus\WordpressHMR\EnqueueAssets to create a static script which enqueues the assets in your theme, avoiding the performance overhead of enqueuing assets dynamically. Run the build command and then check out your enqueue-assets.php file in your build folder public/wp-content/themes/your-theme/inc/enqueue-assets.php
vendor/bin/whr build
Project directory structure
The installer will create the following folders/files:
Dockerdirectory, which serves as an entrypoint for files in your project root and the docker container.publicdirectory, with the latest version of wordpress installed. The installer will also prompt you to create a custom theme directory with the name you specify.resourcesdirectory where all your theme files will reside. Webpack copies all the files from your resources folder to the current active theme.resources/assetsdirectory where your css / scss files will reside.resources/incdirectory where theenqueue-assets.phpandfunctions.phpfile will reside.resources/jsdirectory where your entry points will reside.vendordirectory where your composer packages are stored. This directory is mounted in the docker container in the parent directory of your wordpress site (/var/www/).compose.ymlDocker compose settings.whr.jsonwhere you define your assets and theme
Accessing Docker containers
Commands
WordpressHMR provides some convenient commands to access your container. To run wp-cli commands inside your container you can use:
vendor/bin/whr wp --info
To run commands inside your wordpress container you can use:
vendor/bin/whr exec <commands>
Note that not all common binaries exist in the wordpress container. Therefore, it's recommended to run commands in the wordpress container like so:
vendor/bin/whr exec bash -c 'cd ../ && ls -1'
Container entry points
Your containers can access files in your local project through Docker/data/tmp. You'll find any files you add to this directory at the container path /tmp/Docker. If you want to change this directory, please see volumes in your compose.yml file, located in your project root.
When is this useful? Say you want to import / export a database. Add your .sql dump file to Docker/data/tmp in your project. Then import it into your container's database by running vendor/bin/whr wp db import /Docker/tmp.
Likewise, you can export your database by running vendor/bin/whr wp db export /tmp/Docker/dump.sql
Container settings
Wordpress
If you want to change values in the wp-config.php file, you can do so through your compose.yml file in your project root. In the services.wordpress.environment section, you'll find default values you can change. For example, if you'd like to display more error reporting with ini_set, you can add those values under WORDPRESS_CONFIG_EXTRA. Your container will evaluate that string value directly into the wp-config.php file.
Note: For changes to take effect, you'll need to restart your container with vendor/bin/whr start
services: wordpress: ... environment: WORDPRESS_DB_NAME: wp_database WORDPRESS_TABLE_PREFIX: wp_ WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: admin WORDPRESS_DB_PASSWORD: password WORDPRESS_DEBUG: true WORDPRESS_CONFIG_EXTRA: | /* Remove https for admin */ define('FORCE_SSL_ADMIN', false ); define('WP_DEBUG_LOG', true); define('WP_DEBUG_DISPLAY',true); /* add error reporting */ ini_set('display_errors', true); ini_set('display_startup_errors', true); error_reporting(E_ALL);
Adding files to your wordpress container
By default, only your wp-content directory is mounted in the container. It is the bridge between your local project and the container. If you'd like to mount another file in the container, you can add it in your compose.yml file.
For example, say you wanted to add your own .htaccess file. Add it to your public directory and then edit your compose.yml file to include it as a volume:
services: wordpress: volumes: # htaccess - ./public/.htaccess:/var/www/html/.htaccess # Note: docker splits the volume string by a the colon (:) # You specify the path to your file / directory you'd like to mount, # followed by location in the container. # /var/www/html is where your wordpress site lives.
WARNING: Be careful adding a file like wp-config.php since the wordpress Docker image uses a specific wp-config.php with its own logic for setting environment variables.
Troubleshooting
Webpack Errors
If you encounter an error such as Module parse failed: 'import' and 'export' may appear only with 'sourceType: module', please make sure your root directory of your project does not contain a package.json file where the type is set to "commonjs". While aslamhus/wordpress-hmr uses ES6 syntax for its webpack configuration, wp-scripts uses CommonJS and specifying a type can cause errors.
HMR isn't working
If you aren't seeing changes in your wordpress reflected in your site, try:
-
You've started your container with the
--hotoption. -
Checking if the following line is in your resources/functions.php file. It should be added automatically when you install or change your theme.
/** Enqueue Custom Theme Assets */ require_once get_stylesheet_directory() . '/inc/enqueue-assets.php';
Wordpress Errors
Can't login to my admin
Here's where the whr command line tool comes in handy. Simply add a new user with administrator privileges to login:
vendor/bin/whr wp user create admin admin@example.com --role=administrator
# a password will be generated for you. Now you can log in!
Modifying the Webpack configurations
You'll find all the webpack config files in the vendor/aslamhus/wordpress-hmr/build.
Contributing
If you would like to contribute to this package, please feel free to submit a pull request. I would love to hear your feedback and suggestions for improvement.
This package was proudly built without AI, just one single human clacking away at the keyboard.