Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ This page provides detailed information on the Playground Contributor Badge and
Any contribution to the WordPress Playground project is highly valued. The Playground team recognizes contributions across several key areas:
-->

- **પ્લેગ્રાઉન્ડ કોડ:** કોડમાં ફેરફાર કરવા અને મુખ્ય પ્રોજેક્ટની સમીક્ષા કરવી.
- **પ્લેગ્રાઉન્ડ UI:** વેબ અનુભવના યુઝર ઇન્ટરફેસમાં સુધારો કરવો.
- **દસ્તાવેજીકરણ:** દસ્તાવેજીકરણ લખવું, અપડેટ કરવું અને સમીક્ષા કરવી.
- **અનુવાદ:** પ્રોજેક્ટના કોઈપણ ભાગનું ભાષાંતર કરવું.
- **બ્લુપ્રિન્ટ્સ ગેલેરી:** નવી બ્લુપ્રિન્ટ્સ બનાવવી અથવા હાલના બ્લુપ્રિન્ટ્સને સુધારવું.
- **પ્લેગ્રાઉન્ડ કોડ:** કોડમાં ફેરફાર કરવા અને મુખ્ય પ્રોજેક્ટની સમીક્ષા કરવી.
- **પ્લેગ્રાઉન્ડ UI:** વેબ અનુભવના યુઝર ઇન્ટરફેસમાં સુધારો કરવો.
- **દસ્તાવેજીકરણ:** દસ્તાવેજીકરણ લખવું, અપડેટ કરવું અને સમીક્ષા કરવી.
- **અનુવાદ:** પ્રોજેક્ટના કોઈપણ ભાગનું ભાષાંતર કરવું.
- **બ્લુપ્રિન્ટ્સ ગેલેરી:** નવી બ્લુપ્રિન્ટ્સ બનાવવી અથવા હાલના બ્લુપ્રિન્ટ્સને સુધારવું.

<!--
- **Playground Code:** Making code changes and reviewing the core project.
Expand Down
2 changes: 2 additions & 0 deletions packages/php-wasm/xdebug-bridge/src/lib/start-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type StartBridgeConfig = {
phpRoot?: string;
remoteRoot?: string;
localRoot?: string;
excludedPaths?: string[];
phpInstance?: PHP;
getPHPFile?: (path: string) => string | Promise<string>;
};
Expand Down Expand Up @@ -79,5 +80,6 @@ export async function startBridge(config: StartBridgeConfig) {
remoteRoot: config.remoteRoot,
localRoot: config.localRoot,
getPHPFile,
excludedPaths: config.excludedPaths,
});
}
65 changes: 43 additions & 22 deletions packages/php-wasm/xdebug-bridge/src/lib/xdebug-cdp-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface XdebugCDPBridgeConfig {
knownScriptUrls: string[];
remoteRoot?: string;
localRoot?: string;
excludedPaths?: string[];
getPHPFile(path: string): string | Promise<string>;
}

Expand All @@ -52,6 +53,7 @@ export class XdebugCDPBridge {
private readPHPFile: (path: string) => string | Promise<string>;
private remoteRoot: string;
private localRoot: string;
private excludedPaths: string[];

constructor(
dbgp: DbgpSession,
Expand All @@ -63,6 +65,7 @@ export class XdebugCDPBridge {
this.readPHPFile = config.getPHPFile;
this.remoteRoot = config.remoteRoot || '';
this.localRoot = config.localRoot || '';
this.excludedPaths = config.excludedPaths || [];
for (const url of config.knownScriptUrls) {
this.scriptIdByUrl.set(url, this.getOrCreateScriptId(url));
}
Expand Down Expand Up @@ -147,7 +150,11 @@ export class XdebugCDPBridge {

private sendInitialScripts() {
// Send scriptParsed for the main file if not already sent
if (this.initFileUri && !this.scriptIdByUrl.has(this.initFileUri)) {
if (
this.initFileUri &&
!this.scriptIdByUrl.has(this.initFileUri) &&
!this.isExcludedPath(this.initFileUri)
) {
const scriptId = this.getOrCreateScriptId(this.initFileUri);
this.cdp.sendMessage({
method: 'Debugger.scriptParsed',
Expand All @@ -164,19 +171,27 @@ export class XdebugCDPBridge {

// Send every script we already know about
for (const [url, scriptId] of this.scriptIdByUrl.entries()) {
this.cdp.sendMessage({
method: 'Debugger.scriptParsed',
params: {
scriptId,
url,
startLine: 0,
startColumn: 0,
executionContextId: 1,
},
});
if (!this.isExcludedPath(url)) {
this.cdp.sendMessage({
method: 'Debugger.scriptParsed',
params: {
scriptId,
url,
startLine: 0,
startColumn: 0,
executionContextId: 1,
},
});
}
}
}

private isExcludedPath(fileUri: string): boolean {
return this.excludedPaths.some((prefix) =>
this.uriToRemotePath(fileUri).startsWith(prefix)
);
}

private getOrCreateScriptId(fileUri: string): string {
let scriptId = this.scriptIdByUrl.get(fileUri);
if (!scriptId) {
Expand Down Expand Up @@ -568,17 +583,23 @@ export class XdebugCDPBridge {
if (response['xdebug:message']) {
const fileUri = response['xdebug:message'].$.filename;
if (fileUri && !this.scriptIdByUrl.has(fileUri)) {
const scriptId = this.getOrCreateScriptId(fileUri);
this.cdp.sendMessage({
method: 'Debugger.scriptParsed',
params: {
scriptId,
url: fileUri,
startLine: 0,
startColumn: 0,
executionContextId: 1,
},
});
if (this.isExcludedPath(fileUri)) {
this.sendDbgpCommand('step_over');
Copy link
Collaborator

@adamziel adamziel Aug 28, 2025

Choose a reason for hiding this comment

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

The excluded file could call a function defined in a non-excluded file. Would step_over ignore the entire call and never step into that other file? This isn't relevant for breaking on the first line but it is relevant for calls made by Playground-level mu-plugins, e.g. the SQLite integration plugin.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oho. I guess it will ignore the file. It should also check the command. If the command is step_into then, don't ignore the file. I'll need to test that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah I didn't understand your sentence correctly a few days ago! The excluded file won't be "loaded" by Devtools but its code will be run as expected. So if an excluded file is running a function from a non excluded file it will pause the script on the first line of the non-excluded file.

The opposite will happen though. If you run a script that calls another one that has an excluded path, you won't see anything from that excluded path script. That is why I understood with my last comment.

That is why I should let the current code change but I should add a second condition in the if statement indicating that if the current command is step_into and the file has an excluded path we should not step_over and show the script.

I am adding a new test for that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done.

Copy link
Collaborator

@adamziel adamziel Sep 2, 2025

Choose a reason for hiding this comment

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

@mho22 to make sure we're aligned, let's discuss different scenarios. Let's also create a test case for each of them.

Scenario 1 – basic exclusion

// In main.php:
require_once "excluded.php";
// Breakpoint is set on this line:
excluded_function();
boot_wordpress();

// In excluded.php:
function excluded_function() {
	// ... logic ...
}

In this scenario, stepping into excluded_function would be the same as stepping over it – we'd advance to boot_wordpress().

Scenario 2 – excluded file in a stack trace

// In main.php:
require_once "excluded.php";
function boot_wordpress() {
    // Breakpoint on the next line
    require "wp-load.php";
}

// Breakpoint is set on this line:
excluded_function();

// In excluded.php:
function excluded_function() {
	global $log;
	$log->info("Booting WordPress");
	boot_wordpress();
}

In this scenario, stepping into excluded_function(); would:

  • Skip the body of excluded_function()
  • Move the execution inside boot_wordpress() where we could continue stepping into or stepping over

Similarly, in this scenario:

// main.php
// breakpoint set here:
require_once "excluded.php";

// excluded.php:
require_once "my-app.php";

// my-app.php:
call1();
call2();

Stepping into the firs require_once would, ideally, move us into the first line of my-app.php.

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'll need some time to test your scenarios.

break;
} else {
const scriptId =
this.getOrCreateScriptId(fileUri);
this.cdp.sendMessage({
method: 'Debugger.scriptParsed',
params: {
scriptId,
url: fileUri,
startLine: 0,
startColumn: 0,
executionContextId: 1,
},
});
}
}
}
if (status === 'break') {
Expand Down
12 changes: 11 additions & 1 deletion packages/php-wasm/xdebug-bridge/src/tests/start-bridge.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import type { PHP } from '@php-wasm/universal';
import { EventEmitter } from 'events';
import { CDPServer } from '../lib/cdp-server';
import { DbgpSession } from '../lib/dbgp-session';
import { XdebugCDPBridge } from '../lib/xdebug-cdp-bridge';
import { startBridge } from '../lib/start-bridge';
import { XdebugCDPBridge } from '../lib/xdebug-cdp-bridge';
import { type Log, logger, LogSeverity } from '@php-wasm/logger';

describe('Bridge', () => {
Expand Down Expand Up @@ -114,6 +114,16 @@ describe('Bridge', () => {
expect(getPHPFile).toHaveBeenCalledWith('file:///custom.php');
expect(result).toBe('<?php echo "Hello World";');
});

it('excludes given paths', async () => {
const paths = ['/foo', '/bar'];

await startBridge({ excludedPaths: paths });

const args = (XdebugCDPBridge as any).mock.calls[0][2];

expect(args.excludedPaths).toEqual(paths);
});
});

describe('Log', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,102 @@ describe('XdebugCDPBridge', () => {
{ name: 'waldo', type: 'string' },
]);
});

it('ignores files from excluded path', async () => {
const excludedPath = '/internal/shared';

const file = `${import.meta.dirname}/fixtures/test.php`;

const messages: any[] = [];

const script = fs.readFileSync(file);

php.writeFile(file, script.toString());

bridge.start();

await php.runStream({ scriptPath: file });

await new Promise<void>((resolve) => {
const original = cdpServer.sendMessage.bind(cdpServer);
vi.spyOn(cdpServer, 'sendMessage').mockImplementation((message) => {
if (message.method === 'Debugger.scriptParsed') {
messages.push(message);
resolve();
}
return original(message);
});
});

expect(messages).toEqual(
expect.arrayContaining([
expect.objectContaining({
params: expect.objectContaining({
url: expect.stringContaining(excludedPath),
}),
}),
])
);
expect(messages).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
params: expect.objectContaining({
url: expect.stringContaining(file),
}),
}),
])
);

bridge.stop();

php.exit();

messages.length = 0;

php = new PHP(
await loadNodeRuntime(RecommendedPHPVersion, { withXdebug: true })
);

dbgpSession = new DbgpSession();
cdpServer = new CDPServer();
bridge = new XdebugCDPBridge(dbgpSession, cdpServer, {
knownScriptUrls: fs.readdirSync(`${import.meta.dirname}/fixtures`),
getPHPFile: (file) => php.readFileAsText(file),
excludedPaths: [excludedPath],
});

bridge.start();

await php.runStream({ scriptPath: file });

await new Promise<void>((resolve) => {
const original = cdpServer.sendMessage.bind(cdpServer);
vi.spyOn(cdpServer, 'sendMessage').mockImplementation((message) => {
if (message.method === 'Debugger.scriptParsed') {
messages.push(message);
resolve();
}
return original(message);
});
});

expect(messages).toEqual(
expect.arrayContaining([
expect.objectContaining({
params: expect.objectContaining({
url: expect.stringContaining(file),
}),
}),
])
);
expect(messages).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
params: expect.objectContaining({
url: expect.stringContaining(excludedPath),
}),
}),
])
);
});
});
1 change: 1 addition & 0 deletions packages/playground/cli/src/run-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,7 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer> {
const bridge = await startBridge({
getPHPFile: async (path: string) =>
await playground!.readFileAsText(path),
excludedPaths: ['/internal'],
});

bridge.start();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export function getSqliteDriverModuleDetails(
url: string;
} {
switch (version) {

case 'develop':
/** @ts-ignore */
return {
Expand Down