Skip to content
Draft
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
4 changes: 4 additions & 0 deletions packages/env/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Bug Fixes

- `wp-env status` now reports the development URL based on the live Docker port mapping instead of the configured `WP_SITEURL`. This fixes a stale URL when `--auto-port` reassigns the port; consumers reading `urls.development` from `wp-env status --json` no longer have to fall back to constructing the URL from `ports.development` themselves.

## 11.5.0 (2026-04-29)

## 11.4.0 (2026-04-15)
Expand Down
56 changes: 54 additions & 2 deletions packages/env/lib/runtime/docker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,48 @@ const retry = require( '../../retry' );

const CONFIG_CACHE_KEY = 'config_checksum';

/**
* Build the development URL reported by `getStatus()` from the live Docker
* port mapping rather than the configured `WP_SITEURL`. After
* `--auto-port` reassigns the port, the configured value goes stale.
* If the configured site URL has a custom host or path, that shape is
* preserved — only the port is rebased onto the live mapping.
*
* @param {string|undefined} configuredSiteUrl `WP_SITEURL` from config.
* @param {number|null} livePort Live Docker port for the dev
* container, or null when the
* environment is not running.
* @return {string|null} The development URL, or null when not running.
*/
function rebaseSiteUrlPort( configuredSiteUrl, livePort ) {
if ( ! livePort ) {
return null;
}
if ( ! configuredSiteUrl ) {
return `http://localhost:${ livePort }`;
}
let url;
try {
url = new URL( configuredSiteUrl );
} catch {
return `http://localhost:${ livePort }`;
}
// If the user's WP_SITEURL has no port, they explicitly want the URL
// as-configured (e.g. `http://example.com`). Don't append a port.
if ( ! url.port ) {
return configuredSiteUrl;
}
url.port = String( livePort );
let result = url.toString();
// `URL.toString()` emits a trailing slash for empty paths. Match the
// shape of the input so existing consumers that string-compare don't
// see a spurious change.
if ( ! configuredSiteUrl.endsWith( '/' ) && result.endsWith( '/' ) ) {
result = result.slice( 0, -1 );
}
return result;
}

/**
* Docker runtime implementation for wp-env.
*
Expand Down Expand Up @@ -718,15 +760,24 @@ class DockerRuntime {
// Containers are not running.
}

const siteUrl = config.env.development.config.WP_SITEURL;
// Derive the development URL from the live Docker port mapping.
// Reading `config.env.development.config.WP_SITEURL` directly goes
// stale after `--auto-port` reassigns the port, since WP_SITEURL is
// computed once at config-load time from the configured port.
// Replace the port in the configured site URL so a custom
// WP_SITEURL (different host or path) is preserved.
const developmentUrl = rebaseSiteUrlPort(
config.env.development.config.WP_SITEURL,
isRunning ? developmentPort : null
);

const testsEnabled = config.testsEnvironment !== false;

return {
status: isRunning ? 'running' : 'stopped',
runtime: 'docker',
urls: {
development: isRunning ? siteUrl : null,
development: developmentUrl,
phpmyadmin:
isRunning && phpmyadminPort
? `http://localhost:${ phpmyadminPort }`
Expand Down Expand Up @@ -842,3 +893,4 @@ class DockerRuntime {
}

module.exports = DockerRuntime;
module.exports.rebaseSiteUrlPort = rebaseSiteUrlPort;
63 changes: 63 additions & 0 deletions packages/env/lib/runtime/docker/test/rebase-site-url-port.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
'use strict';
/**
* Internal dependencies
*/
const { rebaseSiteUrlPort } = require( '../index' );

describe( 'rebaseSiteUrlPort', () => {
it( 'returns null when the live port is missing', () => {
expect( rebaseSiteUrlPort( 'http://localhost:8888', null ) ).toBeNull();
expect( rebaseSiteUrlPort( 'http://localhost:8888', 0 ) ).toBeNull();
} );

it( 'falls back to localhost when the configured site URL is empty', () => {
expect( rebaseSiteUrlPort( '', 8889 ) ).toBe( 'http://localhost:8889' );
expect( rebaseSiteUrlPort( undefined, 8889 ) ).toBe(
'http://localhost:8889'
);
} );

it( 'replaces the port in the configured site URL with the live port', () => {
expect( rebaseSiteUrlPort( 'http://localhost:8888', 8889 ) ).toBe(
'http://localhost:8889'
);
} );

it( 'preserves a custom host when rebasing the port', () => {
expect( rebaseSiteUrlPort( 'http://my.local:8888', 8889 ) ).toBe(
'http://my.local:8889'
);
} );

it( 'preserves a custom path and scheme when rebasing the port', () => {
expect( rebaseSiteUrlPort( 'https://my.local:8888/wp', 8889 ) ).toBe(
'https://my.local:8889/wp'
);
} );

it( 'returns the configured site URL unchanged when it has no port', () => {
// A user who explicitly set WP_SITEURL without a port wants exactly
// that URL — don't synthesize one onto it.
expect( rebaseSiteUrlPort( 'http://example.com', 8889 ) ).toBe(
'http://example.com'
);
} );

it( 'falls back to localhost when the configured site URL is invalid', () => {
expect( rebaseSiteUrlPort( 'not a url', 8889 ) ).toBe(
'http://localhost:8889'
);
} );

it( 'preserves a trailing slash when present in the configured URL', () => {
expect( rebaseSiteUrlPort( 'http://localhost:8888/', 8889 ) ).toBe(
'http://localhost:8889/'
);
} );

it( 'does not introduce a trailing slash that the configured URL lacked', () => {
expect( rebaseSiteUrlPort( 'http://localhost:8888', 8889 ) ).toBe(
'http://localhost:8889'
);
} );
} );
Loading