Skip to content

Conversation

mho22
Copy link
Collaborator

@mho22 mho22 commented Oct 14, 2025

Motivation for the change, related issues

Based on issue #2763

The current pain we have when we run a Playground CLI command with Xdebug enabled is the absence of the files located in the VFS in our IDE.

This pull request gives visibility to the VFS files with the help of a .playground symlink and the generation of path mappings inside the developer's IDE.

Only the mounts from inside the current working directory are mapped.

Implementation details

  1. Symlinking > .playground
  • It first removes a possibly existing symlink with name .playground in the current working directory.
  • If --xdebug and --experimental-ide options are present, we symlink the temporary playground cli directory inside the current working directory.
  1. Path mapping in IDEs
  • It first clears all the configs named WP Playground CLI - Listen for Xdebug in VSCode and PHPStorm config files.
  • If --xdebug and --experimental-ide options are present, we add IDE configs and path mappings in the related configs.
  • PHPStorm : it adds a new server with name WP Playground CLI - Listen for Xdebug in .idea/workspace.xml.
  • VSCode : it adds a new configuration with name WP Playground CLI - Listen for Xdebug in .vscode/launch.json.

Next

  • Add tests

Testing Instructions (or ideally a Blueprint)

CI

@mho22 mho22 changed the title [ xdebug ] Add --experimental-ide option to set xdebug path mappings [ xdebug ] Add --experimental-ide option in Playground CLI Oct 14, 2025
@mho22 mho22 mentioned this pull request Oct 14, 2025
9 tasks
@adamziel
Copy link
Collaborator

Let's support an optional explicit value, e.g. --experimental-ide=vscode just in case I work in multiple IDEs and only want to debug in one of them.

Also, this is the top of my vscode.json file:

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",

It means we cannot trivially use JSON.parse() :( These files are JSONC (JSON with comments) and we need a JSONC parser and serializer to work with them. Perhaps this one from Microsoft would work, maybe it's even the one used by VS Code.

try {
config = JSON.parse(fs.readFileSync(configFilePath, 'utf-8'));
} catch {
logger.warn(
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 a big deal, let's display it in red and kill the process at this point. Otherwise, when things don't work, the user will scratch their head and think "oh that option doesn't work?"

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I used jsonc-parser as suggested in your comment, and it became easier to manipulate JSON.

Now I parse the file and if it finds errors, it logs an error and exits with process.exit(1).

My question is, should I use the process.exit() function inside the xdebug-path-mappings file or should I return to the run-cli file to centralize process.exit functions ?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Good question! Would throw new Error() suffice? AFAIR CLI has an error handler that already calls exit()

// Then, if xdebug, and experimental IDE are enabled,
// recreate the symlink pointing to the temporary
// directory and add the new IDE config.
if (args.xdebug && args.experimentalIde) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

cc @brandonpayton for reviews

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@brandonpayton and @adamziel, your reviews are warmly welcomed in this pull request, since I am not sure I will list all the use cases.

@adamziel
Copy link
Collaborator

Some problems identifying the debug configuration:

CleanShot 2025-10-15 at 17 31 45@2x

Also, it's weird how the host is 127.0.0.1:9400 and port is 80.

(c: { $: { name: string } }) => c.$.name === name
);
if (!servers) {
component.servers[0].server.push(server);
Copy link
Collaborator

Choose a reason for hiding this comment

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

this line keep failing for me, every part of this expression may be missing (like it was in my config).

Copy link
Member

Choose a reason for hiding this comment

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

I had this same experience and believe I've fixed it now.

);
if (!component) {
config.project.component = [];
config.project.component.push({
Copy link
Collaborator

Choose a reason for hiding this comment

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

should we set component to the pushed element?

Copy link
Member

Choose a reason for hiding this comment

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

This should be fixed now.

@adamziel
Copy link
Collaborator

XDebug is running even during the Blueprint execution – how cool! Also, that's likely only useful for code developers. Let's set some php.ini option to disable xdebug until WordPress fully boots.

@adamziel
Copy link
Collaborator

We're definitely getting there! 🎉

CleanShot 2025-10-15 at 17 40 11@2x

@brandonpayton
Copy link
Member

I've been testing and reviewing the code and made some adjustments. The server host an port are now explicitly configured based on CLI's host and port.

I've seen PhpStorm report an Xdebug connection but haven't been able to hit any breakpoints yet.

@brandonpayton
Copy link
Member

brandonpayton commented Oct 18, 2025

I've been testing with an auto-mounted WordPress dir nested under the Playground repo like:
~/src/playground/wp-plugin-test-wordpress% node --experimental-strip-types --experimental-transform-types --disable-warning=ExperimentalWarning --import ../packages/meta/src/node-es-module-loader/register.mts ../packages/playground/cli/src/cli.ts server --auto-mount=. --verbosity=debug --skip-wordpress-setup --xdebug --experimental-ide

@brandonpayton
Copy link
Member

brandonpayton commented Oct 18, 2025

@adamziel is there any additional step in PhpStorm that I should have to do to tell PhpStorm to be able to hit breakpoints? Or should it just work if you have PhpStorm open and Playground CLI started with Xdebug support?

@adamziel
Copy link
Collaborator

adamziel commented Oct 18, 2025

I typically check "stop on first line" and set breakpoints from there @brandonpayton. And you need to turn on the "accept debug connections" toggle

@brandonpayton
Copy link
Member

Thanks, @adamziel! I hadn't been testing with "Stop on First Line".

For some reason, PhpStorm still isn't breaking in the UI. I grabbed WireShark to capture the interaction between Playground CLI and the IDE, and based on my naive reading of the exchange, it appears to be trying to break (or declaring a break?) for auto_prepend_file.php but then ends with a "detach" command before doing anything within the UI.

I am thinking about why this may be, including considering whether I broke anything with fixes over the weekend.

WireShark Logs from the TCP stream 493.<?xml version="1.0" encoding="iso-8859-1"?> <init xmlns="urn:debugger_protocol_v1" xmlns:xdebug="https://xdebug.org/dbgp/xdebug" fileuri="file:///internal/shared/auto_prepend_file.php" language="PHP" xdebug:language_version="8.3.26-dev" protocol_version="1.0" appid="42"><engine version="3.4.6-dev"><![CDATA[Xdebug]]></engine><author><![CDATA[Derick Rethans]]></author><url><![CDATA[https://xdebug.org]]></url><copyright><![CDATA[Copyright (c) 2002-2025 by Derick Rethans]]></copyright></init>. eval -i 1 -- KHN0cmluZykoaW5pX2dldCgneGRlYnVnLmNvdmVyYWdlX2VuYWJsZScpLic7Jy5pbmlfZ2V0KCd4ZGVidWcucHJvZmlsZXJfZW5hYmxlJykuJzsnLmluaV9nZXQoJ3hkZWJ1Zy5yZW1vdGVfYXV0b3N0YXJ0JykuJzsnLmluaV9nZXQoJ3hkZWJ1Zy5yZW1vdGVfY29ubmVjdF9iYWNrJykuJzsnLmluaV9nZXQoJ3hkZWJ1Zy5yZW1vdGVfbW9kZScpKQ==.feature_set -i 2 -n show_hidden -v 1. 1102.<?xml version="1.0" encoding="iso-8859-1"?> <response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="https://xdebug.org/dbgp/xdebug" command="eval" transaction_id="1"><property type="string" size="635" encoding="base64"><![CDATA[VGhpcyBzZXR0aW5nIGhhcyBiZWVuIGNoYW5nZWQsIHNlZSB0aGUgdXBncmFkaW5nIGd1aWRlIGF0IGh0dHBzOi8veGRlYnVnLm9yZy9kb2NzL3VwZ3JhZGVfZ3VpZGUjY2hhbmdlZC14ZGVidWcuY292ZXJhZ2VfZW5hYmxlO1RoaXMgc2V0dGluZyBoYXMgYmVlbiBjaGFuZ2VkLCBzZWUgdGhlIHVwZ3JhZGluZyBndWlkZSBhdCBodHRwczovL3hkZWJ1Zy5vcmcvZG9jcy91cGdyYWRlX2d1aWRlI2NoYW5nZWQteGRlYnVnLnByb2ZpbGVyX2VuYWJsZTtUaGlzIHNldHRpbmcgaGFzIGJlZW4gY2hhbmdlZCwgc2VlIHRoZSB1cGdyYWRpbmcgZ3VpZGUgYXQgaHR0cHM6Ly94ZGVidWcub3JnL2RvY3MvdXBncmFkZV9ndWlkZSNjaGFuZ2VkLXhkZWJ1Zy5yZW1vdGVfYXV0b3N0YXJ0O1RoaXMgc2V0dGluZyBoYXMgYmVlbiBjaGFuZ2VkLCBzZWUgdGhlIHVwZ3JhZGluZyBndWlkZSBhdCBodHRwczovL3hkZWJ1Zy5vcmcvZG9jcy91cGdyYWRlX2d1aWRlI2NoYW5nZWQteGRlYnVnLnJlbW90ZV9jb25uZWN0X2JhY2s7VGhpcyBzZXR0aW5nIGhhcyBiZWVuIGNoYW5nZWQsIHNlZSB0aGUgdXBncmFkaW5nIGd1aWRlIGF0IGh0dHBzOi8veGRlYnVnLm9yZy9kb2NzL3VwZ3JhZGVfZ3VpZGUjY2hhbmdlZC14ZGVidWcucmVtb3RlX21vZGU=]]></property></response>.219.<?xml version="1.0" encoding="iso-8859-1"?> <response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="https://xdebug.org/dbgp/xdebug" command="feature_set" transaction_id="2" feature="show_hidden" success="1"></response>. feature_set -i 3 -n max_depth -v 1. 217.<?xml version="1.0" encoding="iso-8859-1"?> <response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="https://xdebug.org/dbgp/xdebug" command="feature_set" transaction_id="3" feature="max_depth" success="1"></response>. feature_set -i 4 -n max_children -v 100. 220.<?xml version="1.0" encoding="iso-8859-1"?> <response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="https://xdebug.org/dbgp/xdebug" command="feature_set" transaction_id="4" feature="max_children" success="1"></response>. feature_set -i 5 -n extended_properties -v 1. 227.<?xml version="1.0" encoding="iso-8859-1"?> <response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="https://xdebug.org/dbgp/xdebug" command="feature_set" transaction_id="5" feature="extended_properties" success="1"></response>. feature_set -i 6 -n notify_ok -v 1. 217.<?xml version="1.0" encoding="iso-8859-1"?> <response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="https://xdebug.org/dbgp/xdebug" command="feature_set" transaction_id="6" feature="notify_ok" success="1"></response>. feature_set -i 7 -n resolved_breakpoints -v 1. 228.<?xml version="1.0" encoding="iso-8859-1"?> <response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="https://xdebug.org/dbgp/xdebug" command="feature_set" transaction_id="7" feature="resolved_breakpoints" success="1"></response>. feature_set -i 8 -n breakpoint_include_return_value -v 1. 239.<?xml version="1.0" encoding="iso-8859-1"?> <response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="https://xdebug.org/dbgp/xdebug" command="feature_set" transaction_id="8" feature="breakpoint_include_return_value" success="1"></response>. stdout -i 9 -c 1. 192.<?xml version="1.0" encoding="iso-8859-1"?> <response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="https://xdebug.org/dbgp/xdebug" command="stdout" transaction_id="9" success="1"></response>. status -i 10. 211.<?xml version="1.0" encoding="iso-8859-1"?> <response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="https://xdebug.org/dbgp/xdebug" command="status" transaction_id="10" status="starting" reason="ok"></response>. step_into -i 11. 312.<?xml version="1.0" encoding="iso-8859-1"?> <response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="https://xdebug.org/dbgp/xdebug" command="step_into" transaction_id="11" status="break" reason="ok"><xdebug:message filename="file:///internal/shared/auto_prepend_file.php" lineno="3"></xdebug:message></response>. eval -i 12 -- Z2V0ZW52KCdQSFBfSURFX0NPTkZJRycpIT1mYWxzZQ==. 225.<?xml version="1.0" encoding="iso-8859-1"?> <response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="https://xdebug.org/dbgp/xdebug" command="eval" transaction_id="12"><property type="bool"><![CDATA[0]]></property></response>. eval -i 13 -- aXNzZXQoJF9TRVJWRVJbJ1NFUlZFUl9OQU1FJ10p. 225.<?xml version="1.0" encoding="iso-8859-1"?> <response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="https://xdebug.org/dbgp/xdebug" command="eval" transaction_id="13"><property type="bool"><![CDATA[1]]></property></response>. eval -i 14 -- KHN0cmluZykoJF9TRVJWRVJbJ1NFUlZFUl9OQU1FJ10p. 274.<?xml version="1.0" encoding="iso-8859-1"?> <response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="https://xdebug.org/dbgp/xdebug" command="eval" transaction_id="14"><property type="string" size="15" encoding="base64"><![CDATA[ZXhhbXBsZS5jb206NDQz]]></property></response>. eval -i 15 -- KHN0cmluZykoJF9TRVJWRVJbJ1NFUlZFUl9QT1JUJ10p. 257.<?xml version="1.0" encoding="iso-8859-1"?> <response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="https://xdebug.org/dbgp/xdebug" command="eval" transaction_id="15"><property type="string" size="2" encoding="base64"><![CDATA[ODA=]]></property></response>. eval -i 16 -- KHN0cmluZykoJF9TRVJWRVJbJ1JFUVVFU1RfVVJJJ10p. 253.<?xml version="1.0" encoding="iso-8859-1"?> <response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="https://xdebug.org/dbgp/xdebug" command="eval" transaction_id="16"><property type="string" size="0" encoding="base64"><![CDATA[]]></property></response>. detach -i 17 -- Q2Fubm90IGJpbmQgZmlsZSAvaW50ZXJuYWwvc2hhcmVkL2F1dG9fcHJlcGVuZF9maWxlLnBocCB0byB0aGUgd2ViIHNlcnZlciBwcm9qZWN0LiBGb3IgbW9yZSBpbmZvcm1hdGlvbiwgcmVmZXIgdG8gaHR0cHM6Ly93d3cuamV0YnJhaW5zLmNvbS9oZWxwL3BocHN0b3JtL3Ryb3VibGVzaG9vdGluZy1waHAtZGVidWdnaW5nLmh0bWwjZGV0YWNoLWZyb20tZGVidWctc2Vzc2lvbi1yZWFzb25zLg==. 211.<?xml version="1.0" encoding="iso-8859-1"?> <response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="https://xdebug.org/dbgp/xdebug" command="detach" transaction_id="17" status="stopping" reason="ok"></response>. stop -i 18.

@brandonpayton
Copy link
Member

brandonpayton commented Oct 20, 2025

Also, it's weird how the host is 127.0.0.1:9400 and port is 80.

Funny. I "fixed" this over the weekend but manually changing back to the above setting fixed the issue for me. I've hit my first breakpoint in PhpStorm. 🎉

@brandonpayton
Copy link
Member

I'm going to test with VSCode next. If that is working, I want to find the simplest version of this we can ship and then continue iterating.

@brandonpayton
Copy link
Member

After fixing a race condition between clear-ide-config and add-ide-config, the VSCode setup works great. I was able to hit breakpoints with no problem.

Now, I'm reviewing the code to clean things up and see whether we are in a place where we can merge this and follow-up with a PR to further refine the feature.

@brandonpayton
Copy link
Member

brandonpayton commented Oct 22, 2025

I kept running into bugs with different special cases in the xml2js parser output. For example, sometimes a child element was an object, but if there was more than one child, the property was an array. Other times, an element that might contain child elements would be an array containing a single empty string. I guess these features are supposed to make parsing easier, but they were just making it more error prone. This way of representing parsed XML also does not preserve node order when the XML is fed back to the builder.

To make sure element order was preserved (the respectful thing to do IMO) and to make the parsing result more consistent (and thus easier to think about), I adjusted the xml2js parser options so that every element would have a children array property if it had children at all. This looked promising, and indeed, it was clear to code against. But it turned out that the xml2js builder wasn't able to build based on that parser output structure. It complained that the element name assigned to objects in children[] was an invalid attribute name. Apparently, others had this issue as well. See this GitHub issue.

I also noticed that xml2js was replacing CDATA sections with text nodes. This might have been fixable by changing xml2js options, but at that point, I was already pursuing a less magical parser/builder.

I moved to fast-xml-parser as an alternative library and have been having better luck with it. The structure produced by the parser is consistent and does not have special cases AFAICT. It can be a bit clunky to manually descend into the element hierarchy, but the process is straightforward and mechanical which ultimately should lead to fewer bugs.

I see that we are also using xml2js with the xdebug-cdp-bridge, but I think that is fine for now. And if we want, we can switch to a single XML library again in the future.

@brandonpayton
Copy link
Member

brandonpayton commented Oct 22, 2025

Note: I updated the previous comment with additional info.

@brandonpayton
Copy link
Member

I plan to continue looking at this in the morning. After moving to the new parser, the PhpStorm config parsing and modification seems much more stable.

I'm troubled a bit that this feature could complete screw up a person's IDE config if there are bugs and am considering ways we might avoid this.

One idea:
See if fast-xml-parser can capture metadata about the start and end of various elements, and just splice server config to and from the XML string instead of parsing and regurgitating the entire config file every time. This way, we could still break the config file with a mistake, but we might leave most of it intact.

Maybe what we are doing now will be OK, but the above concern is on my mind.

Copy link
Member

@brandonpayton brandonpayton left a comment

Choose a reason for hiding this comment

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

@adamziel, I think this is ready for reviews.

Regarding the fact that we can hit breakpoints during boot, it seems like we can handle that with a change to the Xdebug feature itself in a follow-up PR.

// TODO: Should we warn users and ask them to confirm that
// we will be modifying their IDE config files?
// It could be painful for folks if their IDE configs are
// inadvertently broken.
Copy link
Member

Choose a reason for hiding this comment

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

@mho22 and @adamziel, what do you think about this?

Copy link
Member

Choose a reason for hiding this comment

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

As a user, I would appreciate such a prompt with a warning that this carries some risk, and it might not be bad to ask if they want Playground CLI to create a backup.

Though, after the initial prompt, I'd probably want a way to tell Playground CLI not to prompt me.

Copy link
Member

Choose a reason for hiding this comment

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

What if we name this feature something like --unsafe-ide-integration or something? I'd rather not have a name this dramatic, but maybe it would be good for the option name include some acknowledgement of the risk.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I like unsafe in the name. Also, we need a prompt-less mode at least for integrators. Perhaps based on the option value? Default could be ask and there could be another value available such as dont-ask

if (projectElement === undefined) {
projectElement = {
project: [],
':@': { version: '4' },
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to tie ourselves to a specific version number here, and if so, should we refuse to modify configs with unrecognized project versions?

Copy link
Collaborator

@adamziel adamziel Oct 22, 2025

Choose a reason for hiding this comment

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

Yeah let's tie ourselves to a specific version. We have no idea what version 5 will look like. Also, it seems like 4 was around at least since 2017 so maybe it will stick around for another 8 years.

const symlinkName = '.playground';
const symlinkPath = path.join(process.cwd(), symlinkName);

removePlaygroundCliTempDirSymlink(symlinkPath);
Copy link
Member

Choose a reason for hiding this comment

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

Rare but possible case:
What if a user project already includes a .playground dir?

Maybe we should name the symlink .playground-files-for-debugging or something else specific.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Good spot, I think some projects on GitHub include a directory like that. .playground-xdebug-root perhaps?

@brandonpayton brandonpayton marked this pull request as ready for review October 22, 2025 20:55
@brandonpayton brandonpayton requested a review from a team as a code owner October 22, 2025 20:55
@brandonpayton
Copy link
Member

brandonpayton commented Oct 23, 2025

Regarding the fact that we can hit breakpoints during boot, it seems like we can handle that with a change to the Xdebug feature itself in a follow-up PR.

It's late. I don't want to create another issue right now, so I'll just note this here for now:

I think we can fix this case in a separate PR by:

  1. Not configuring the primary worker to use Xdebug.
  2. Disposing of the primary worker after boot.
  3. Configuring the secondary/subsequent workers to use Xdebug.
  4. Creating at least one secondary worker to service requests after boot.
  5. Stop differentiating between "primary" and "secondary" workers. All workers are the same, except that we can create a dedicated worker for boot that is configured a bit differently to accomplish boot.

And if we want to enable devs to use Xdebug during boot, we can support a separate command line flag for that.

@brandonpayton
Copy link
Member

Thanks for your responses, @adamziel. They all make sense, and I should be able to implement them in the morning.

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