Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
ce85c95
Add Xdebug path mappings when --experimental-ide option enabled
mho22 Oct 14, 2025
fd9ab80
Implement IDE config addition and removal
mho22 Oct 14, 2025
66c8620
Add try catch around JSON.parse
mho22 Oct 14, 2025
2db6d02
replace JSON.parse with JSONC parser
mho22 Oct 15, 2025
7f6ce1c
Add --experimental-ide=vscode|phpstorm and create config file if non …
mho22 Oct 15, 2025
6a197d7
Merge branch 'trunk' into add-xdebug-path-mappings-with-experimental-ide
brandonpayton Oct 18, 2025
5593471
Allow mapping current dir
brandonpayton Oct 18, 2025
4d0a9cc
Add explicit host and port args
brandonpayton Oct 18, 2025
e201565
Fix bug when PhpStorm component is not found
brandonpayton Oct 18, 2025
84412a9
Avoid issues seen with setting PhpStorm server config
brandonpayton Oct 18, 2025
f203470
Log errors with logger.error() instead of console.log()
brandonpayton Oct 18, 2025
0446899
Fix Xdebug connections for PhpStorm
brandonpayton Oct 20, 2025
0fe21d9
Avoid race condition between clear and add IDE config
brandonpayton Oct 20, 2025
48fc035
Make add/remove export names more specific
brandonpayton Oct 20, 2025
644f659
Use separate constants instead of reusable var
brandonpayton Oct 20, 2025
0127864
Make XML updates and searches more resilient to unexpected structure
brandonpayton Oct 21, 2025
f928a97
Switch to parser/builder with fewer special cases
brandonpayton Oct 22, 2025
3a0a93d
Make attributes a little easier to reference
brandonpayton Oct 22, 2025
87b4047
Add TODO for discussion
brandonpayton Oct 22, 2025
44fbf40
Reject invalid XML and exit process in run-cli
brandonpayton Oct 22, 2025
529302a
Remove TODO
brandonpayton Oct 22, 2025
bfd3c25
Move and expand on --experimental-ide comment
brandonpayton Oct 22, 2025
83c5210
Remove another TODO
brandonpayton Oct 22, 2025
c2aec94
Update TODO for strange PhpStorm host/port config
brandonpayton Oct 22, 2025
ffa09cf
Try to be clear that IDE integration carries risk
brandonpayton Oct 23, 2025
cd28b57
Use a more specific and purposeful name for the Playground symlink
brandonpayton Oct 23, 2025
a0fd85e
Only support PhpStorm project version 4
brandonpayton Oct 23, 2025
5cab07e
Tweak option description
brandonpayton Oct 23, 2025
f67f148
Check for truthy element lookups instead of not-undefined
brandonpayton Oct 24, 2025
d471f20
Fix dev-time ES module loader to provide real URL for Windows
brandonpayton Oct 24, 2025
61e01c2
Revert "Fix dev-time ES module loader to provide real URL for Windows"
brandonpayton Oct 24, 2025
9ed05cd
Fix symlink creation to work on Windows
brandonpayton Oct 24, 2025
748d31b
Corrected: Fix dev-time ES module loader to provide real URL for Windows
brandonpayton Oct 24, 2025
a92ee49
Remove unnecessary port value in PHPServers server attribute
mho22 Oct 25, 2025
55da995
Add ide config node types and refactor code to match same logic betwe…
mho22 Oct 25, 2025
3504c34
Move process values and functions outside the path mappings file
mho22 Oct 25, 2025
ef6759c
Add new error if specific IDE requested but IDE directory missing, an…
mho22 Oct 25, 2025
63e2876
Add XML or JSON validation before writing config file
mho22 Oct 25, 2025
100f7bf
Merge conflicts with trunk
mho22 Oct 27, 2025
f9c0ccc
Improve JSONC editing/reparsing and exit on XDebug configuration error
adamziel Oct 27, 2025
1986549
Add XDebug setup instructions and PHPStorm run configuration
adamziel Oct 27, 2025
98a4874
Format
adamziel Oct 27, 2025
eddbc5f
Unit test updateVSCodeConfig() and updatePhpStormConfig()
adamziel Oct 28, 2025
0b3c468
Resolve type errors
adamziel Oct 28, 2025
cd75dc8
Lint, typecheck
adamziel Oct 28, 2025
2313fd1
Lint, typecheck
adamziel Oct 28, 2025
d593a18
Adjust xdebug ini test for new addition
brandonpayton Oct 28, 2025
8fb5404
Catch possible errors from clearXDebugIDEConfig
brandonpayton Oct 28, 2025
8dd4bc7
Restore previous launch.json targets
brandonpayton Oct 28, 2025
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
41 changes: 39 additions & 2 deletions packages/playground/cli/src/run-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ import {
cleanupStalePlaygroundTempDirs,
createPlaygroundCliTempDir,
} from './temp-dir';
import {
addIDEConfig,
createPlaygroundCliTempDirSymlink,
clearIDEConfig,
removePlaygroundCliTempDirSymlink,
} from './xdebug-path-mappings';

// Inlined worker URLs for static analysis by downstream bundlers
// These are replaced at build time by the Vite plugin in vite.config.ts
Expand Down Expand Up @@ -217,11 +223,15 @@ export async function parseOptionsAndRunCLI() {
type: 'boolean',
default: false,
})
.option('experimental-ide', {
describe: 'Enable experimental IDE development tools.',
type: 'boolean',
})
.option('experimental-devtools', {
describe: 'Enable experimental browser development tools.',
type: 'boolean',
default: false,
})
.conflicts('experimental-ide', 'experimental-devtools')
.option('experimental-multi-worker', {
describe:
'Enable experimental multi-worker support which requires ' +
Expand Down Expand Up @@ -418,6 +428,7 @@ export interface RunCLIArgs {
internalCookieStore?: boolean;
'additional-blueprint-steps'?: any[];
xdebug?: boolean;
experimentalIde?: boolean;
experimentalDevtools?: boolean;
'experimental-blueprints-v2-runner'?: boolean;

Expand Down Expand Up @@ -549,6 +560,32 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer> {
tempDirNameDelimiter
);

// Clear any stale IDE config.
const IDEConfigName = 'WP Playground CLI - Listen for Xdebug';
clearIDEConfig(IDEConfigName);

// Always clean up any existing '.playground' symlink in the project root.
const symlinkName = '.playground';
const symlinkPath = path.join(process.cwd(), symlinkName);

removePlaygroundCliTempDirSymlink(symlinkPath);

// 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) {
createPlaygroundCliTempDirSymlink(nativeDirPath, symlinkPath);

const symlinkMount: Mount = {
hostPath: `./${symlinkName}`,
vfsPath: '/',
};
addIDEConfig(IDEConfigName, [
symlinkMount,
...(args.mount || []),
]);
}

// We do not know the system temp dir,
// but we can try to infer from the location of the current temp dir.
const tempDirRoot = path.dirname(nativeDirPath);
Expand Down Expand Up @@ -744,7 +781,7 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer> {
`WordPress is running on ${serverUrl} with ${totalWorkerCount} worker(s)`
);

if (args.experimentalDevtools && args.xdebug) {
if (args.xdebug && args.experimentalDevtools) {
const bridge = await startBridge({
phpInstance: playground,
phpRoot: '/wordpress',
Expand Down
247 changes: 247 additions & 0 deletions packages/playground/cli/src/xdebug-path-mappings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import fs from 'fs';
import path from 'path';
import { logger } from '@php-wasm/logger';
import { type Mount } from './mounts';
import { Builder, parseStringPromise } from 'xml2js';

/**
* Create a symlink to temp dir for the Playground CLI.
*
* The symlink is created to access the system temp dir
* inside the current debugging directory.
*
* @param nativeDirPath The system temp dir path.
* @param symlinkPath The symlink name.
*/
export async function createPlaygroundCliTempDirSymlink(
nativeDirPath: string,
symlinkPath: string
) {
fs.symlinkSync(nativeDirPath, symlinkPath);
}

/**
* Remove the temp dir symlink if it exists.
*
* @param symlinkPath The symlink path.
*/
export async function removePlaygroundCliTempDirSymlink(symlinkPath: string) {
try {
const stats = fs.lstatSync(symlinkPath);
if (stats.isSymbolicLink()) {
fs.unlinkSync(symlinkPath);
} else {
logger.warn(
`${symlinkPath} exists and is not a symlink. Skipping symlink creation.`
);
}
} catch {
// Symlink does not exist or cannot be accessed, nothing to remove
}
}

/**
* Filters out mounts that are not in the current working directory
*
* @param mounts The Playground CLI mount options.
*/
function filterLocalMounts(mounts: Mount[]) {
return mounts.filter((mount) => {
const absoluteHostPath = path.resolve(mount.hostPath);
return absoluteHostPath.startsWith(process.cwd() + path.sep);
});
}

/**
* Implement necessary parameters and path mappings in IDE configuration files.
*
* @param name The configuration name.
* @param mounts The Playground CLI mount options.
*/
export async function addIDEConfig(name: string, mounts: Mount[]) {
let configFilePath;
let pathMappingsSet = false;
const mappings = filterLocalMounts(mounts);

configFilePath = path.join(process.cwd(), '.idea/workspace.xml');
// PHPstorm
if (fs.existsSync(configFilePath)) {
const contents = fs.readFileSync(configFilePath);
const config = await parseStringPromise(contents);

const server = {
$: {
name: name,
host: '127.0.0.1:9400',
port: '80',
use_path_mappings: 'true',
},
path_mappings: [
{
mapping: mappings.map((mapping) => ({
$: {
'local-root': `$PROJECT_DIR$/${mapping.hostPath.replace(
/^\.\/?/,
''
)}`,
'remote-root': mapping.vfsPath,
},
})),
},
],
};

if (!config.project) {
logger.warn(
'PhpStorm configuration file does not contain a <project> element. Skipping path mapping.'
);
return;
}

const component = config?.project?.component?.find(
(c: { $: { name: string } }) => c.$.name === 'PhpServers'
);
if (!component) {
config.project.component = [];
config.project.component.push({
$: { name: 'PhpServers' },
servers: [{ server: [] }],
});
}

const servers = component?.servers[0]?.server?.find(
(c: { $: { name: string } }) => c.$.name === name
);
if (!servers) {
component.servers[0].server.push(server);
}

const builder = new Builder({
xmldec: { version: '1.0', encoding: 'UTF-8' },
headless: false,
renderOpts: { pretty: true },
});
const xml = builder.buildObject(config);

fs.writeFileSync(configFilePath, xml);

pathMappingsSet = true;
}

configFilePath = path.join(process.cwd(), '.vscode/launch.json');
// VSCode
if (fs.existsSync(configFilePath)) {
let config;
try {
config = JSON.parse(fs.readFileSync(configFilePath, 'utf-8'));
} catch {
logger.warn(
'VSCode configuration file is not valid JSON. Skipping path mapping.'
);
return;
}
const configuration = {
name: name,
type: 'php',
request: 'launch',
port: 9003,
pathMappings: mappings.reduce((acc, mount) => {
acc[
mount.vfsPath
] = `\${workspaceFolder}/${mount.hostPath.replace(
/^\.\/?/,
''
)}`;
return acc;
}, {} as Record<string, string>),
};

if (!config.configurations) {
logger.warn(
"VSCode configuration file is missing a 'configurations' array. Skipping path mapping."
);
return;
}

const component = config.configurations.find(
(c: { name: string }) => c.name === name
);

if (!component) {
config.configurations.push(configuration);
}

const json = JSON.stringify(config, null, 4);

fs.writeFileSync(configFilePath, json);

pathMappingsSet = true;
}

if (!pathMappingsSet) {
logger.warn(
"No IDE configuration file was found. Running with '--experimental-ide' requires an IDE configuration file. Skipping path mapping."
);
}
}

/**
* Remove stale parameters and path mappings in IDE configuration files.
*
* @param name The configuration name.
*/
export async function clearIDEConfig(name: string) {
let configFilePath;

configFilePath = path.join(process.cwd(), '.idea/workspace.xml');
// PHPstorm
if (fs.existsSync(configFilePath)) {
const contents = fs.readFileSync(configFilePath);
const config = await parseStringPromise(contents);

const component = config?.project?.component?.find(
(c: { $: { name: string } }) => c.$.name === 'PhpServers'
);

if (component && component?.servers[0]?.server) {
component.servers[0].server = component.servers[0].server.filter(
(c: { $: { name: string } }) => c.$.name !== name
);

const builder = new Builder({
xmldec: { version: '1.0', encoding: 'UTF-8' },
headless: false,
renderOpts: { pretty: true },
});
const xml = builder.buildObject(config);

fs.writeFileSync(configFilePath, xml);
}
}

configFilePath = path.join(process.cwd(), '.vscode/launch.json');
// VSCode
if (fs.existsSync(configFilePath)) {
let config;
try {
config = JSON.parse(fs.readFileSync(configFilePath, 'utf-8'));
} catch {
logger.warn(
'VSCode configuration file is not valid JSON. Skipping path mapping.'
);
return;
}

const component = config?.configurations?.filter(
(configuration: { name: string }) => configuration.name !== name
);

if (component) {
config.configurations = component;

const json = JSON.stringify(config, null, 4);

fs.writeFileSync(configFilePath, json);
}
}
}