-
Notifications
You must be signed in to change notification settings - Fork 361
[ xdebug ] Add --experimental-unsafe-ide-integration option in Playground CLI
#2777
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
brandonpayton
merged 49 commits into
trunk
from
add-xdebug-path-mappings-with-experimental-ide
Oct 28, 2025
Merged
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 fd9ab80
Implement IDE config addition and removal
mho22 66c8620
Add try catch around JSON.parse
mho22 2db6d02
replace JSON.parse with JSONC parser
mho22 7f6ce1c
Add --experimental-ide=vscode|phpstorm and create config file if non …
mho22 6a197d7
Merge branch 'trunk' into add-xdebug-path-mappings-with-experimental-ide
brandonpayton 5593471
Allow mapping current dir
brandonpayton 4d0a9cc
Add explicit host and port args
brandonpayton e201565
Fix bug when PhpStorm component is not found
brandonpayton 84412a9
Avoid issues seen with setting PhpStorm server config
brandonpayton f203470
Log errors with logger.error() instead of console.log()
brandonpayton 0446899
Fix Xdebug connections for PhpStorm
brandonpayton 0fe21d9
Avoid race condition between clear and add IDE config
brandonpayton 48fc035
Make add/remove export names more specific
brandonpayton 644f659
Use separate constants instead of reusable var
brandonpayton 0127864
Make XML updates and searches more resilient to unexpected structure
brandonpayton f928a97
Switch to parser/builder with fewer special cases
brandonpayton 3a0a93d
Make attributes a little easier to reference
brandonpayton 87b4047
Add TODO for discussion
brandonpayton 44fbf40
Reject invalid XML and exit process in run-cli
brandonpayton 529302a
Remove TODO
brandonpayton bfd3c25
Move and expand on --experimental-ide comment
brandonpayton 83c5210
Remove another TODO
brandonpayton c2aec94
Update TODO for strange PhpStorm host/port config
brandonpayton ffa09cf
Try to be clear that IDE integration carries risk
brandonpayton cd28b57
Use a more specific and purposeful name for the Playground symlink
brandonpayton a0fd85e
Only support PhpStorm project version 4
brandonpayton 5cab07e
Tweak option description
brandonpayton f67f148
Check for truthy element lookups instead of not-undefined
brandonpayton d471f20
Fix dev-time ES module loader to provide real URL for Windows
brandonpayton 61e01c2
Revert "Fix dev-time ES module loader to provide real URL for Windows"
brandonpayton 9ed05cd
Fix symlink creation to work on Windows
brandonpayton 748d31b
Corrected: Fix dev-time ES module loader to provide real URL for Windows
brandonpayton a92ee49
Remove unnecessary port value in PHPServers server attribute
mho22 55da995
Add ide config node types and refactor code to match same logic betwe…
mho22 3504c34
Move process values and functions outside the path mappings file
mho22 ef6759c
Add new error if specific IDE requested but IDE directory missing, an…
mho22 63e2876
Add XML or JSON validation before writing config file
mho22 100f7bf
Merge conflicts with trunk
mho22 f9c0ccc
Improve JSONC editing/reparsing and exit on XDebug configuration error
adamziel 1986549
Add XDebug setup instructions and PHPStorm run configuration
adamziel 98a4874
Format
adamziel eddbc5f
Unit test updateVSCodeConfig() and updatePhpStormConfig()
adamziel 0b3c468
Resolve type errors
adamziel cd75dc8
Lint, typecheck
adamziel 2313fd1
Lint, typecheck
adamziel d593a18
Adjust xdebug ini test for new addition
brandonpayton 8fb5404
Catch possible errors from clearXDebugIDEConfig
brandonpayton 8dd4bc7
Restore previous launch.json targets
brandonpayton File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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({ | ||
mho22 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| $: { 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); | ||
mho22 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| 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); | ||
mho22 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| pathMappingsSet = true; | ||
| } | ||
|
|
||
| configFilePath = path.join(process.cwd(), '.vscode/launch.json'); | ||
| // VSCode | ||
| if (fs.existsSync(configFilePath)) { | ||
adamziel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| let config; | ||
| try { | ||
| config = JSON.parse(fs.readFileSync(configFilePath, 'utf-8')); | ||
| } catch { | ||
| logger.warn( | ||
mho22 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| '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( | ||
adamziel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /^\.\/?/, | ||
| '' | ||
| )}`; | ||
| return acc; | ||
| }, {} as Record<string, string>), | ||
| }; | ||
|
|
||
| if (!config.configurations) { | ||
| logger.warn( | ||
| "VSCode configuration file is missing a 'configurations' array. Skipping path mapping." | ||
adamziel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ); | ||
| 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); | ||
| } | ||
| } | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.