diff --git a/packages/php-wasm/xdebug-bridge/project.json b/packages/php-wasm/xdebug-bridge/project.json index 12d126e850..535063450e 100644 --- a/packages/php-wasm/xdebug-bridge/project.json +++ b/packages/php-wasm/xdebug-bridge/project.json @@ -67,8 +67,7 @@ "{workspaceRoot}/coverage/packages/php-wasm/xdebug-bridge" ], "options": { - "reportsDirectory": "../../../coverage/packages/php-wasm/xdebug-bridge", - "testFiles": ["mock-test.spec.ts"] + "reportsDirectory": "../../../coverage/packages/php-wasm/xdebug-bridge" } }, "typecheck": { diff --git a/packages/php-wasm/xdebug-bridge/src/lib/cdp-server.ts b/packages/php-wasm/xdebug-bridge/src/lib/cdp-server.ts index 2cf0130146..d0fb17c4cb 100644 --- a/packages/php-wasm/xdebug-bridge/src/lib/cdp-server.ts +++ b/packages/php-wasm/xdebug-bridge/src/lib/cdp-server.ts @@ -47,4 +47,8 @@ export class CDPServer extends EventEmitter { console.log('\x1b[1;32m[CDP][send]\x1b[0m', json); this.ws.send(json); } + + close() { + this.wss.close(); + } } diff --git a/packages/php-wasm/xdebug-bridge/src/lib/dbgp-session.ts b/packages/php-wasm/xdebug-bridge/src/lib/dbgp-session.ts index fb2a4e2a89..add8d412ad 100644 --- a/packages/php-wasm/xdebug-bridge/src/lib/dbgp-session.ts +++ b/packages/php-wasm/xdebug-bridge/src/lib/dbgp-session.ts @@ -80,4 +80,8 @@ export class DbgpSession extends EventEmitter { // Commands must end with null terminator this.socket.write(command + '\x00'); } + + close() { + this.server.close(); + } } diff --git a/packages/php-wasm/xdebug-bridge/src/lib/start-bridge.ts b/packages/php-wasm/xdebug-bridge/src/lib/start-bridge.ts index 61643d3e41..da95c84a25 100644 --- a/packages/php-wasm/xdebug-bridge/src/lib/start-bridge.ts +++ b/packages/php-wasm/xdebug-bridge/src/lib/start-bridge.ts @@ -12,6 +12,7 @@ export type StartBridgeConfig = { phpRoot?: string; remoteRoot?: string; localRoot?: string; + excludedPaths?: string[]; phpInstance?: PHP; getPHPFile?: (path: string) => string | Promise; @@ -76,5 +77,6 @@ export async function startBridge(config: StartBridgeConfig) { remoteRoot: config.remoteRoot, localRoot: config.localRoot, getPHPFile, + excludedPaths: config.excludedPaths, }); } diff --git a/packages/php-wasm/xdebug-bridge/src/lib/xdebug-cdp-bridge.ts b/packages/php-wasm/xdebug-bridge/src/lib/xdebug-cdp-bridge.ts index 10b192bd22..2c210a0c75 100644 --- a/packages/php-wasm/xdebug-bridge/src/lib/xdebug-cdp-bridge.ts +++ b/packages/php-wasm/xdebug-bridge/src/lib/xdebug-cdp-bridge.ts @@ -28,6 +28,7 @@ export interface XdebugCDPBridgeConfig { knownScriptUrls: string[]; remoteRoot?: string; localRoot?: string; + excludedPaths?: string[]; getPHPFile(path: string): string | Promise; } @@ -48,6 +49,7 @@ export class XdebugCDPBridge { private readPHPFile: (path: string) => string | Promise; private remoteRoot: string; private localRoot: string; + private excludedPaths: string[]; constructor( dbgp: DbgpSession, @@ -59,6 +61,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)); } @@ -136,9 +139,18 @@ export class XdebugCDPBridge { }); } + stop() { + this.dbgp.close(); + this.cdp.close(); + } + 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', @@ -155,19 +167,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) { @@ -544,17 +564,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'); + 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') { diff --git a/packages/php-wasm/xdebug-bridge/src/tests/fixtures/test.php b/packages/php-wasm/xdebug-bridge/src/tests/fixtures/test.php new file mode 100644 index 0000000000..0bb2aa8f0c --- /dev/null +++ b/packages/php-wasm/xdebug-bridge/src/tests/fixtures/test.php @@ -0,0 +1,11 @@ + { + beforeEach(async () => { + vi.spyOn(global, 'setTimeout').mockImplementation( + (cb) => global.setImmediate(() => cb()) as unknown as NodeJS.Timeout + ); + vi.spyOn(EventEmitter.prototype, 'on').mockImplementation(function ( + this: EventEmitter, + event, + cb + ) { + if (event === 'clientConnected') { + setTimeout(cb, 0); + } + return this; + }); + + vi.spyOn( + await import('../lib/cdp-server'), + 'CDPServer' + ).mockReturnThis(); + vi.spyOn( + await import('../lib/dbgp-session'), + 'DbgpSession' + ).mockReturnThis(); + vi.spyOn( + await import('../lib/xdebug-cdp-bridge'), + 'XdebugCDPBridge' + ).mockReturnThis(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + 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); + }); +}); diff --git a/packages/php-wasm/xdebug-bridge/src/tests/xdebug-cdp-bridge.spec.ts b/packages/php-wasm/xdebug-bridge/src/tests/xdebug-cdp-bridge.spec.ts new file mode 100644 index 0000000000..972428cb6a --- /dev/null +++ b/packages/php-wasm/xdebug-bridge/src/tests/xdebug-cdp-bridge.spec.ts @@ -0,0 +1,139 @@ +import fs from 'fs'; +import { vi } from 'vitest'; +import { DbgpSession } from '../lib/dbgp-session'; +import { CDPServer } from '../lib/cdp-server'; +import { XdebugCDPBridge } from '../lib/xdebug-cdp-bridge'; +import { PHP } from '@php-wasm/universal'; +import { RecommendedPHPVersion } from '@wp-playground/common'; +import { loadNodeRuntime } from '@php-wasm/node'; + +describe('XdebugCDPBridge', () => { + let php: PHP; + let dbgpSession: DbgpSession; + let cdpServer: CDPServer; + let bridge: XdebugCDPBridge; + + beforeEach(async () => { + 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), + }); + + vi.spyOn(dbgpSession, 'sendCommand'); + vi.spyOn(dbgpSession, 'on'); + vi.spyOn(cdpServer, 'sendMessage'); + vi.spyOn(cdpServer, 'on'); + }); + + afterEach(() => { + vi.clearAllMocks(); + + bridge.stop(); + + php.exit(); + }); + + 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((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((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), + }), + }), + ]) + ); + }); +}); diff --git a/packages/php-wasm/xdebug-bridge/tests/mock-test.spec.ts b/packages/php-wasm/xdebug-bridge/tests/mock-test.spec.ts deleted file mode 100644 index f40ba17587..0000000000 --- a/packages/php-wasm/xdebug-bridge/tests/mock-test.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { test, expect } from 'vitest'; - -test('mock test to prevent vitest from failing', () => { - // This is a placeholder test to ensure vitest doesn't fail due to no tests - // TODO: Add actual tests for the xdebug-bridge functionality - expect(true).toBe(true); -}); diff --git a/packages/php-wasm/xdebug-bridge/vite.config.ts b/packages/php-wasm/xdebug-bridge/vite.config.ts index 921542c587..48f44ddf89 100644 --- a/packages/php-wasm/xdebug-bridge/vite.config.ts +++ b/packages/php-wasm/xdebug-bridge/vite.config.ts @@ -1,6 +1,7 @@ /// import { join } from 'path'; -import { defineConfig } from 'vite'; +import { defineConfig, type Plugin } from 'vite'; +import path from 'path'; import dts from 'vite-plugin-dts'; // eslint-disable-next-line @nx/enforce-module-boundaries @@ -19,6 +20,37 @@ export default defineConfig({ viteTsConfigPaths({ root: '../../../', }), + { + name: 'import-url', + enforce: 'pre', + + resolveId(id: string, importer: string): any { + if (id.startsWith('import-url:')) { + return id; + } + + if (!path.isAbsolute(id) && id.endsWith('?url')) { + const filepath = path.resolve(path.dirname(importer), id); + return `import-url:${filepath}`; + } + + return null; + }, + + load(id: string): any { + if (id.startsWith('import-url:')) { + const encodedPath = id.slice('import-url:'.length); + const filePath = encodedPath.replace('?url', ''); + + return { + code: `export default ${JSON.stringify(filePath)};`, + map: null, + }; + } + + return null; + }, + } as Plugin, ], build: { diff --git a/packages/playground/cli/src/run-cli.ts b/packages/playground/cli/src/run-cli.ts index 0bec82a7e7..c5220a969a 100644 --- a/packages/playground/cli/src/run-cli.ts +++ b/packages/playground/cli/src/run-cli.ts @@ -548,6 +548,7 @@ export async function runCLI(args: RunCLIArgs): Promise { const bridge = await startBridge({ getPHPFile: async (path: string) => await playground!.readFileAsText(path), + excludedPaths: ['/internal'], }); bridge.start(); diff --git a/packages/playground/wordpress-builds/src/sqlite-database-integration/get-sqlite-driver-module-details.ts b/packages/playground/wordpress-builds/src/sqlite-database-integration/get-sqlite-driver-module-details.ts index 0cda6d0408..a185569ef5 100644 --- a/packages/playground/wordpress-builds/src/sqlite-database-integration/get-sqlite-driver-module-details.ts +++ b/packages/playground/wordpress-builds/src/sqlite-database-integration/get-sqlite-driver-module-details.ts @@ -19,7 +19,6 @@ export function getSqliteDriverModuleDetails( url: string; } { switch (version) { - case 'develop': /** @ts-ignore */ return {