Skip to content

Conversation

@mho22
Copy link
Collaborator

@mho22 mho22 commented Jul 29, 2025

Motivation for the change, related issues

🚧 This draft is an experimental work in progress since I am not sure this should be the correct behavior. 🚧

When running node node_modules/@wp-playground/cli/cli.js server --xdebug --experimental-devtools --auto-mount it returns :

Starting a PHP server...
Setting up WordPress latest
Resolved WordPress release URL: https://downloads.w.org/release/wordpress-6.8.2.zip
Fetching SQLite integration plugin...
Booting WordPress...
Booted!
Running the Blueprint...
Running the Blueprint – 100%
Finished running the blueprint
WordPress is running on http://127.0.0.1:9400
Connect Chrome DevTools to CDP at:
devtools://devtools/bundled/inspector.html?ws=localhost:9229

Chrome connected! Initializing Xdebug receiver...
XDebug receiver running on port 9003
Running a PHP script with Xdebug enabled...

But nothing next. It doesn't run a php script with Xdebug enabled since no playground.run() is called.

This pull request aims to run a chosen script when using --auto-mount. Since auto-mount will smartly recognize a Plugin or a Theme, it should also debug the given Plugin script.

Implementation details

if --experimental-devtools, --xdebug and --auto-mount, the script will check if a Plugin is available in process.cwd() and debug it.

Testing Instructions

In a empty directory named recolor-wp-admin-plugin, add file :

index.php

<?php
/**
 * Plugin Name: Recolor wp-admin
 * Description: A simple plugin to recolor the wp-admin interface
 * Version: 1.0
 * Author: Adam Zielinski
 */

function blue_admin_nav_enqueue()
{
    wp_enqueue_style( 'blue-admin', plugin_dir_url( __FILE__ ) . 'style.css' );
}

add_action( 'admin_enqueue_scripts', 'blue_admin_nav_enqueue' );

style.css

#wpadminbar,
#adminmenu,
#adminmenuback,
#adminmenuwrap,
#adminmenu .wp-submenu,
#adminmenu li.menu-top:hover
{
	background-color: blue !important;
}

Now run :

> cd recolor-wp-admin-plugin

> node ../node_modules/@wp-playground/cli/cli.js server --login --xdebug --experimental-devtools --auto-mount
screenshot_001 screenshot_005 screenshot_007

@mho22 mho22 changed the title [Xdebug Bridge] Load Plugin file in Devtools if --auto-mount option [Xdebug Bridge] Load Plugin file in Devtools if --auto-mount option enabled Jul 29, 2025
@mho22 mho22 mentioned this pull request Jul 29, 2025
11 tasks
@mho22 mho22 changed the title [Xdebug Bridge] Load Plugin file in Devtools if --auto-mount option enabled [XDebug Bridge] Load Plugin file in Devtools if --auto-mount option enabled Jul 29, 2025
@mho22
Copy link
Collaborator Author

mho22 commented Jul 31, 2025

I found a way to break into the Plugin file when we add --auto-mount and it recognizes a Plugin. But first I need to write my observations.

It is not possible to ask Devtools to add a breakpoint. The Devtools is only a user interface, with clicks and stuff. These clicks will run actions that can be listened by the bridge but the bridge can only send 4 events to the Devtools :

Debugger.paused
Debugger.resumed
Debugger.scriptFailedToParse
Debugger.scriptParsed

The bridge only sends paused, resumed and scriptParsed.

So, if I want my cli command to break on the plugin file it founds, and if I can't ask this to the Devtools, I need to ask it to Xdebug. By DBGp requesting it during the init request.

To set a breakpoint, we need a file path and a line where it should break. :

breakpoint_set -t line -f URI -n LINENUMBER

Thanks to a function from playground/cli/src/mounts.ts, we can get the Plugin file. But what about the line number? I quickly made a function named findFirstDebuggableLine to find it :

function findFirstDebuggableLine(content: string) {
	const lines = content.split('\n');
	let inBlockComment = false;
	let inFunctionOrClass = false;
	let braceDepth = 0;

	for (let i = 0; i < lines.length; i++) {
		const lineRaw = lines[i];
		const line = lineRaw.trim();

		if (line === '') continue;

		if (line.startsWith('/*')) {
			inBlockComment = true;
			continue;
		}

		if (inBlockComment) {
			if (line.includes('*/')) inBlockComment = false;
			continue;
		}

		if (line.match(/^\s*(function|class)\b/)) {
			inFunctionOrClass = true;
		}

		braceDepth += (line.match(/{/g) || []).length;
		braceDepth -= (line.match(/}/g) || []).length;

		if (inFunctionOrClass && braceDepth === 0) {
			inFunctionOrClass = false;
			continue;
		}

		if (inFunctionOrClass || braceDepth > 0) {
			continue;
		}

		if (
			line.startsWith('//') ||
			line.startsWith('#') ||
			line === '<?php' ||
			line === '?>'
		) {
			continue;
		}

		if (
			line.match(
				/^\s*(var_dump|print|echo|exit|die|return|require|include|define|[$]\w+|\w+\s*\()/
			)
		) {
			return i + 1;
		}
	}

	return 0;
}

I am not satisfied by this and I think we could make that function more accurate with the help of a parser or AIssistants.

However, now that we have the file path and the line, we can break. The last thing is to run a php file with Xdebug enabled when we are connected to the Devtools :

await playground!.run({
        scriptPath: '/wordpress/index.php',
});
screenshot_005 screenshot_006 screenshot_007

Note: For the example here, I added another breakpoint on wordpress/index.php to show the process :

  1. First step into the first PHP file Xdebug encounters: internal/shared/auto_prepend_file.php
  2. After clicking on resume : It stops at the first debuggable line of my first breakpoint : wordpress/index.php
  3. After clicking on resume : It stops at the first debuggable line of my second breakpoint : wp-content/plugins/recolor-wp-admin-plugin/index.php

@adamziel
Copy link
Collaborator

adamziel commented Aug 4, 2025

I'll follow up here

adamziel pushed a commit that referenced this pull request Aug 6, 2025
…nreachable` crashes when using Devtools (#2454)

## Motivation for the change, related issues

I listed the two crashes I encountered while using Devtools in the
comments.

I am currently recompiling `php-wasm-node:asyncify`. 

## Implementation details

Added the following functions in `ASYNCIFY_ONLY_PREFIXED` :

```diff
// NEEDED TO PREVENT THE FIRST CRASH
+ php_fopen_primary_script
+ persistent_stream_open_function
+ php_stream_open_for_zend
+ zend_error_zstr
+ zend_register_constant
+ zif_define

// NEEDED TO PREVENT THE SECOND CRASH
+ zend_undefined_index
```

## Testing Instructions

Based on the instructions from #2442

One test is to step into the running files 68 times until breaking on
`define( 'WP_DEBUG', false );`. The next step would crash previously.
Not anymore.

The second test is to quit the Devtools tab while it is running. It
crashed previously. Not anymore.
@mho22
Copy link
Collaborator Author

mho22 commented Aug 6, 2025

I tested with the wordpress-import plugin instead and I removed the playground.run() line. I listed the user experience steps it needed to access the Plugin code in the devtools tab.

  1. Run npm run local-package-repository from your local wordpress-playground repository.

  1. Download the plugin
  2. Unzip it and go to the directory with your terminal
  3. Install the playground-cli package from the local-package-repository link inside the plugin directory.
  4. Run node_modules/.bin/wp-playground-cli server --login --xdebug --experimental-devtools --auto-mount
  5. Open the playground tab http://127.0.0.1:9400
  6. Move to wp-admin/import.php
  7. Open the devtools tab devtools://devtools/bundled/inspector.html?ws=localhost:9229
  8. Wait for the message Running a PHP script with Xdebug enabled... to appear in the terminal
  9. Go back on the playground tab and click on Run Importer
screenshot_001

The devtools tab takes the focus and breaks on the first line of the internal/shared/auto_preprend_file.php.

screenshot_002
  1. Click on Resume script execution

The debugging goes on the first breakpoint of the first plugin file detected wordpress-importer.php on line 61

screenshot_003
  1. Click on Resume script execution
  2. Click on Pause script execution

Import page is now loaded.

screenshot_004

This seems to be almost a good user experience right?

Let's imagine we have a page with two iframes, one is the Playground and the second one is the devtools, with a button that could enable step debugging when you're ready to step debug your request?

I don't know, just sharing my thoughts here.

@adamziel
Copy link
Collaborator

adamziel commented Aug 7, 2025

Thank you @mho22! This is pretty neat and I think we're not far off from v1.

For v2, my worry is having to interact with devtools on every request. I wonder if we could make "break on the first line" configurable (enabled by default) and start CDP connection separately from the XDebug connection. That is, we could give the user the opportunity to navigate the codebase and set breakpoints without making a request first and without pausing on the very first line.

@@ -0,0 +1,58 @@
export function findFirstDebuggableLine(content: string) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's document the intention here


if (line === '') continue;

if (line.startsWith('/*')) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is somewhat naive – we're parsing PHP code using regular expressions. It works while it works but it will break in cases such as:

<?php /** Plugin name: Test */ weirdly_all_code_is_minified();

These may not be popular use-cases, but I'm sure they are out there. I'd worry .phar files would pose another challenge with this approach.

I'm more and more convinced we shouldn't try to figure this out for the user. Instead, let's just enable them to navigate their own plugin and set the breakpoint where they need.

My thinking goes towards:

  • A way to browse the entire WordPress file structure in the chrome devtools – so core files and my plugins
  • A limited mode that only exposes paths I've explicitly mounted, e.g. plugins, themes, uploads. This is tricky, e.g. how would we step through / into any WordPress core functions? Perhaps this is a good problem to solve for later and not for starters?
  • A separation between browsing and debugging my code in devtools. It would be nice if we could connect via CDP just to poke around and find the right file without implicitly setting any breakpoints.
  • A way to tell Playground either:
    • "Break on the first line of PHP code" – which implies revealing the entire WordPress file structure in devtools
    • "I will set my own breakpoints" – which allows the user to do what they need.

@adamziel
Copy link
Collaborator

adamziel commented Aug 8, 2025

Great explorations @mho22!

It's been a great stone to turn. This usage pattern would work in some cases, but it doesn't seem that great in the general scenario, especially with how it forces us into parsing PHP. It becomes clear to me that we've been looking in a wrong direction. Every XDebug-integrated IDE out there separates browsing the code and setting breakpoints from debugging the code. I think we should do the same thing. Then we'll be able to follow the established usage patterns instead of inventing new ways of debugging.

@mho22
Copy link
Collaborator Author

mho22 commented Aug 8, 2025

@adamziel You're right. Let's forget that one, and focus on your suggestion instead. I already have an idea to integrate it. Let's close this pull request.

@mho22 mho22 closed this Aug 8, 2025
adamziel pushed a commit that referenced this pull request Sep 2, 2025
… enabled (#2527)

## Motivation for the change, related issues

Based on the following pull request : 

- #2442

The first approach was to load Devtools when running a file with Xdebug
enabled PHP.wasm. Unfortunately, the user experience was not great. e.g.
files were loaded only when executed.

The new approach opens de Devtools Source panel with the relevant loaded
files ready to be manipulated.
Once the breakpoints are set, the PHP file can be executed and paused
with Xdebug enabled PHP.wasm.

## Implementation details

- DevTools Sources Opening : Devtools Source panel is automatically
opened when bridge starts.
- Console Instructions : A startup message guides users on how to debug
with the bridge.
- Source File Loading : Relevant PHP files are preloaded in DevTools for
inspection and breakpoints.
- Pending Breakpoints : Breakpoints set before Xdebug init are applied
once the session starts.
- URI Normalization : Paths are consistently mapped between Bridge, CDP,
and DBGP.
- CDP Command Buffer : CDP requests are buffered until the bridge is
fully initialized.
- `breakOnFirstLine` Option : Allows breaking on the first executed line
when no breakpoints exist.
- Test Coverage

## Testing Instructions with Xdebug Bridge

1. Run the devtools

```
npx xdebug-bridge
```

2. Connect to the devtools

```
Starting XDebug Bridge...
Connect Chrome DevTools to CDP at:
devtools://devtools/bundled/inspector.html?ws=localhost:9229
```

3. Set a breakpoint in a file

<img width="1920" height="656" alt="screenshot-001"
src="https://github.com/user-attachments/assets/3591ea63-7344-4a53-98ee-6ff70440dbaf"
/>

4. Run the PHP script with PHP.wasm CLI and Xdebug option

```
npx php-wasm-cli test.php --xdebug
```

<img width="1920" height="651" alt="screenshot-002"
src="https://github.com/user-attachments/assets/e5c9049d-c556-41b3-b99c-a9a2000f5da4"
/>


5. Resume script execution and repeat step 4 indefinitely

<img width="1919" height="667" alt="screenshot-003"
src="https://github.com/user-attachments/assets/aacfaf3d-0afb-497e-b5cd-8d3eb0ce98de"
/>

## Testing Instructions with PHP.wasm CLI

1. Run the script with Xdebug and Devtools options

```
npx php-wasm-cli test.php --experimental-devtools --xdebug
```

2. Connect to the devtools

```
Starting XDebug Bridge...
Connect Chrome DevTools to CDP at:
devtools://devtools/bundled/inspector.html?ws=localhost:9229
```

3. It will pause on the first breakable PHP code

<img width="1920" height="651" alt="screenshot-002"
src="https://github.com/user-attachments/assets/e5c9049d-c556-41b3-b99c-a9a2000f5da4"
/>

## Testing Instructions with PHP.wasm Node

1. Write the following script 

```
import { PHP } from '@php-wasm/universal';
import { loadNodeRuntime } from '@php-wasm/node';
import { startBridge } from '@php-wasm/xdebug-bridge';


const php = new PHP(await loadNodeRuntime('8.4', {withXdebug: true}));

const bridge = await startBridge({phpInstance: php, breakOnFirstLine: true});

bridge.start();

await php.runStream({scriptPath: `test.php`});
```

1. Run the script with Node

```
node script.js
```

3. Connect to the devtools

```
Starting XDebug Bridge...
Connect Chrome DevTools to CDP at:
devtools://devtools/bundled/inspector.html?ws=localhost:9229
```

4. It will pause on the first breakable PHP code

<img width="1920" height="651" alt="screenshot-002"
src="https://github.com/user-attachments/assets/e5c9049d-c556-41b3-b99c-a9a2000f5da4"
/>


## Testing Instructions in `wordpress-playground` 

1. Run the devtools

```
nx reset && nx run php-wasm-xdebug-bridge:dev --php-root /absolute/path/to/the/debuggable/directory
```

2. Connect to the devtools

```
Starting XDebug Bridge...
Connect Chrome DevTools to CDP at:
devtools://devtools/bundled/inspector.html?ws=localhost:9229
```

3. Set a breakpoint in a file

<img width="1920" height="656" alt="screenshot-001"
src="https://github.com/user-attachments/assets/3591ea63-7344-4a53-98ee-6ff70440dbaf"
/>

5. Run the PHP script

```
nx reset && nx run php-wasm-cli:dev /absolute/path/to/the/debuggable/directory/file.php --xdebug
```

<img width="1920" height="651" alt="screenshot-002"
src="https://github.com/user-attachments/assets/e5c9049d-c556-41b3-b99c-a9a2000f5da4"
/>


6. Resume script execution and repeat step 4 indefinitely

<img width="1919" height="667" alt="screenshot-003"
src="https://github.com/user-attachments/assets/aacfaf3d-0afb-497e-b5cd-8d3eb0ce98de"
/>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants