Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@
WP_HOST=yoursite.local

# BrowserSync port. Default: 3000. Change if port 3000 is already in use.
# If you change this, also define ELEMENTARY_THEME_BROWSER_SYNC_URL in wp-config.php
# so PHP enqueues the client script from the correct port.
# Both the build and PHP read this value directly, so changing it here is
# enough. Define ELEMENTARY_THEME_BROWSER_SYNC_URL only to override the full
# client URL (e.g. remote/proxy setups), not just the port.
BS_PORT=3000

# Block HMR dev-server port (used by `npm run start:blocks`).
# Default: 8887. Change if port 8887 is already in use.
BLOCKS_DEV_SERVER_PORT=8887

# Disable BrowserSync client enqueue. Truthy: 1, true, yes, on (default off).
# The BrowserSync server still runs; only the frontend client is skipped.
# DISABLE_BS=true

# SSL cert paths — only needed when your local site runs on HTTPS.
# LocalWP stores certs at:
# macOS: ~/Library/Application Support/Local/run/router/nginx/certs/<domain>.key / .crt
Expand Down
34 changes: 26 additions & 8 deletions docs/hmr.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ This document explains how live reload and hot module replacement work in the th

## Overview

Running `npm start` enables two complementary tools:
Running `npm start` runs two scripts in parallel, each with a complementary tool:

- **BrowserSync** (port 3000) — live reload for the frontend via snippet mode. Your site URL stays unchanged.
- **webpack-dev-server / Fast Refresh** (port 8887) — hot module replacement for block editor React components. Block state is preserved across updates; no full page reload needed.
- **`start:assets` → BrowserSync** (port 3000) — live reload for the frontend via snippet mode. Your site URL stays unchanged.
- **`start:blocks` → webpack-dev-server / Fast Refresh** (port 8887 by default, configurable via `BLOCKS_DEV_SERVER_PORT`) — hot module replacement for block editor React components. Block state is preserved across updates; no full page reload needed.

For BrowserSync:

Expand Down Expand Up @@ -48,11 +48,19 @@ Without `SCRIPT_DEBUG`, WordPress does not support Fast Refresh.

## How It Works

1. `npm start` runs webpack in watch mode.
**Theme assets (`start:assets` + BrowserSync):**

1. `start:assets` runs `wp-scripts start` in watch mode (no `--hot`) using `webpack.config.js`.
2. When a file changes, webpack rebuilds the affected assets in `assets/build/`.
3. BrowserSync detects the change and notifies the browser via the client script.
4. CSS changes are injected in-place. Everything else triggers a full reload.
5. Block changes are detected in editor by the webpack-dev-server run by the `--hot` option

**Blocks (`start:blocks` + Fast Refresh):**

5. `start:blocks` runs `wp-scripts start --hot`, which starts webpack-dev-server, using `webpack.blocks.config.js`.
6. JS/JSX changes to block components hot-swap in the editor without a full reload; block state is preserved.

`webpack.blocks.config.js` is a thin wrapper over `@wordpress/scripts`' default config. It exists only to strip the `devServer.proxy` option: webpack-dev-server v5 (pinned via the `overrides` block in `package.json`) requires `proxy` to be an array, while wp-scripts still emits the v4 object form, which v5 rejects with `options.proxy should be an array`. The wrapper also sets the dev-server port from `BLOCKS_DEV_SERVER_PORT`.

BrowserSync watches the following:

Expand Down Expand Up @@ -94,6 +102,16 @@ define( 'ELEMENTARY_THEME_BROWSER_SYNC_URL', 'https://yoursite.local:3001/browse

`ELEMENTARY_THEME_BROWSER_SYNC_URL` overrides the auto-detected URL entirely, so it also works for remote setups (ddev, reverse proxy) where the BrowserSync server is on a different host or IP.

### Block dev server port

The block Fast Refresh dev server runs on port 8887 by default. If that port is already in use (e.g. two local sites running `start:blocks` at once), set a different port in `.env.local`:

```
BLOCKS_DEV_SERVER_PORT=8889
```

`webpack.blocks.config.js` reads this value and applies it to the dev server. No matching `wp-config.php` constant is needed — the editor loads block scripts from disk, and the HMR client connects to the dev server directly.

### HTTPS

If your local site runs on HTTPS, also add the SSL cert paths:
Expand All @@ -118,10 +136,10 @@ This is required to avoid mixed content errors — the BrowserSync client script

### Disabling BrowserSync

To disable BrowserSync without removing it from the webpack config, define this constant in `wp-config.php`:
To disable BrowserSync without removing it from the webpack config, set this in `.env.local`:

```php
define( 'ELEMENTARY_THEME_DISABLE_BROWSER_SYNC', true );
```
DISABLE_BS=true
```

This prevents PHP from enqueuing the BrowserSync client script. The BrowserSync server still starts (webpack still runs it), but the browser won't connect to it. Useful when working purely in the block editor and you don't want the BrowserSync client loading on the frontend.
Expand Down
4 changes: 0 additions & 4 deletions functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,6 @@ function constants(): void {
if ( ! defined( 'ELEMENTARY_THEME_ENABLE_TAILWIND' ) ) {
define( 'ELEMENTARY_THEME_ENABLE_TAILWIND', file_exists( get_template_directory() . '/src/css/frontend/tailwind.css' ) );
}

if ( ! defined( 'ELEMENTARY_THEME_DISABLE_BROWSER_SYNC' ) ) {
define( 'ELEMENTARY_THEME_DISABLE_BROWSER_SYNC', false );
}
}

constants();
Expand Down
120 changes: 111 additions & 9 deletions inc/Core/Assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
public function register_hooks(): void {
add_action( 'wp_enqueue_scripts', [ $this, 'register_assets' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_browser_sync' ] );
add_filter( 'render_block', [ $this, 'enqueue_block_specific_assets' ], 10, 2 );
}

Expand Down Expand Up @@ -123,17 +124,118 @@
if ( $this->tailwind_enabled ) {
wp_enqueue_style( 'elementary-theme-tailwind' );
}
}

/**
* Enqueue the BrowserSync client script for local live reload.
*
* Only runs in the `local` environment and when not disabled via DISABLE_BS
* in .env.local. The client URL is derived from the site URL and the
* BrowserSync port (BS_PORT in .env.local, default 3000), or taken verbatim
* from the ELEMENTARY_THEME_BROWSER_SYNC_URL constant when defined (for
* custom ports or remote/proxied setups).
*
* @since 1.0.0
*
* @action wp_enqueue_scripts
*/
public function enqueue_browser_sync(): void {
if ( 'local' !== wp_get_environment_type() || $this->is_browser_sync_disabled() ) {
return;
}

if ( defined( 'ELEMENTARY_THEME_BROWSER_SYNC_URL' ) ) {
$bs_url = ELEMENTARY_THEME_BROWSER_SYNC_URL;
} else {
$scheme = is_ssl() ? 'https' : 'http';
$host = wp_parse_url( home_url(), PHP_URL_HOST );
$host = $host ? $host : 'localhost';
$port = $this->get_browser_sync_port();
$bs_url = "{$scheme}://{$host}:{$port}/browser-sync/browser-sync-client.js";
}

if ( 'local' === wp_get_environment_type() && ! ELEMENTARY_THEME_DISABLE_BROWSER_SYNC ) {
if ( defined( 'ELEMENTARY_THEME_BROWSER_SYNC_URL' ) ) {
$bs_url = ELEMENTARY_THEME_BROWSER_SYNC_URL;
} else {
$scheme = is_ssl() ? 'https' : 'http';
$host = wp_parse_url( home_url(), PHP_URL_HOST );
$host = $host ? $host : 'localhost';
$bs_url = "{$scheme}://{$host}:3000/browser-sync/browser-sync-client.js";
wp_enqueue_script( 'elementary-browser-sync', $bs_url, [], ELEMENTARY_THEME_VERSION, true );
}

/**
* Read the BrowserSync port from .env.local (BS_PORT), defaulting to 3000.
*
* Keeps the enqueued client URL in sync with the port webpack/BrowserSync
* actually bind to, which is read from the same .env.local on the build side.
* Falls back to the default when BS_PORT is absent or not a valid TCP port
* (1–65535).
*
* THIS METHOD IS INTENDED FOR LOCAL DEVELOPMENT ENVIRONMENTS ONLY.
*
* @return int BrowserSync port.
*/
private function get_browser_sync_port(): int {
$default = 3000;
$value = $this->get_env_value( 'BS_PORT' );

if ( null !== $value && preg_match( '/^\d+$/', $value ) ) {
$port = (int) $value;

if ( $port >= 1 && $port <= 65535 ) {
return $port;
}
wp_enqueue_script( 'elementary-browser-sync', $bs_url, [], ELEMENTARY_THEME_VERSION, true );
}

return $default;
}

/**
* Whether BrowserSync is disabled via DISABLE_BS in .env.local.
*
* Disabling prevents PHP from enqueuing the BrowserSync client script. The
* BrowserSync server still starts (webpack still runs it), but the browser
* won't connect to it. Truthy values are `1`, `true`, `yes`, and `on`
* (case-insensitive); anything else (or an absent key) keeps it enabled.
*
* THIS METHOD IS INTENDED FOR LOCAL DEVELOPMENT ENVIRONMENTS ONLY.
*
* @return bool True when BrowserSync should be disabled.
*/
private function is_browser_sync_disabled(): bool {
$value = $this->get_env_value( 'DISABLE_BS' );

if ( null === $value ) {
return false;
}

return in_array( strtolower( $value ), [ '1', 'true', 'yes', 'on' ], true );
}

/**
* Read a single key's value from .env.local.
*
* Returns the trimmed value (without surrounding single/double quotes) for
* the given key, or null when the file is unreadable or the key is absent.
*
* THIS METHOD IS INTENDED FOR LOCAL DEVELOPMENT ENVIRONMENTS ONLY.
*
* @param string $key Environment variable name to read.
*
* @return string|null The value, or null when not found.
*/
private function get_env_value( string $key ): ?string {
$env_file = $this->base_dir . '.env.local';

if ( ! is_readable( $env_file ) ) {
return null;
}

// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local dev only; reading a small project file, not remote.
$contents = file_get_contents( $env_file );

Check warning on line 229 in inc/Core/Assets.php

View workflow job for this annotation

GitHub Actions / Lint PHP

file_get_contents() is uncached. If the function is being used to fetch a remote file (e.g. a URL starting with https://), please use wpcom_vip_file_get_contents() to ensure the results are cached. For more details, please see: https://docs.wpvip.com/technical-references/code-quality-and-best-practices/retrieving-remote-data/

if ( false === $contents ) {
return null;
}

if ( preg_match( '/^\s*' . preg_quote( $key, '/' ) . '\s*=\s*(.*)$/m', $contents, $matches ) ) {
return trim( $matches[1], " \t\"'" );
}

return null;
}
}
Loading