diff --git a/packages/docs/site/docs/developers/05-local-development/04-wp-playground-cli.md b/packages/docs/site/docs/developers/05-local-development/04-wp-playground-cli.md index 065436e484..793215e76c 100644 --- a/packages/docs/site/docs/developers/05-local-development/04-wp-playground-cli.md +++ b/packages/docs/site/docs/developers/05-local-development/04-wp-playground-cli.md @@ -112,7 +112,7 @@ The `server` command supports the following optional arguments: - `--login`: Automatically log the user in as an administrator. - `--skip-wordpress-setup`: Do not download or install WordPress. Useful if you are mounting a full WordPress directory. - `--skip-sqlite-setup`: Do not set up the SQLite database integration. -- `--quiet`: Do not output logs and progress messages. +- `--verbosity`: Output logs and progress messages. Defaults to 'normal'. - `--debug`: Print the PHP error log if an error occurs during boot. ## Need some help with the CLI? diff --git a/packages/php-wasm/logger/project.json b/packages/php-wasm/logger/project.json index 2edc11e17f..3dee7b6b95 100644 --- a/packages/php-wasm/logger/project.json +++ b/packages/php-wasm/logger/project.json @@ -64,15 +64,15 @@ "lintFilePatterns": ["packages/php-wasm/logger/**/*.ts"], "maxWarnings": 0 } - } - }, - "typecheck": { - "executor": "nx:run-commands", - "options": { - "commands": [ - "tsc -p packages/php-wasm/logger/tsconfig.lib.json --noEmit", - "tsc -p packages/php-wasm/logger/tsconfig.spec.json --noEmit" - ] + }, + "typecheck": { + "executor": "nx:run-commands", + "options": { + "commands": [ + "tsc -p packages/php-wasm/logger/tsconfig.lib.json --noEmit", + "tsc -p packages/php-wasm/logger/tsconfig.spec.json --noEmit" + ] + } } } } diff --git a/packages/php-wasm/logger/src/lib/collectors/collect-php-logs.ts b/packages/php-wasm/logger/src/lib/collectors/collect-php-logs.ts index 3d388826e6..d9c4a3f081 100644 --- a/packages/php-wasm/logger/src/lib/collectors/collect-php-logs.ts +++ b/packages/php-wasm/logger/src/lib/collectors/collect-php-logs.ts @@ -1,5 +1,5 @@ import type { UniversalPHP, PHPRequestErrorEvent } from '../types'; -import type { Logger } from '../logger'; +import { type Logger, LogPrefix, LogSeverity } from '../logger'; let lastPHPLogLength = 0; export const errorLogPath = '/wordpress/wp-content/debug.log'; @@ -32,6 +32,7 @@ export const collectPhpLogs = ( const currentLog = log.substring(lastPHPLogLength); loggerInstance.logMessage({ message: currentLog, + severity: LogSeverity.Log, raw: true, }); lastPHPLogLength = log.length; @@ -42,8 +43,9 @@ export const collectPhpLogs = ( if (event.error) { loggerInstance.logMessage({ message: `${event.error.message} ${event.error.stack}`, - severity: 'Fatal', - prefix: event.source === 'request' ? 'PHP' : 'WASM Crash', + severity: LogSeverity.Fatal, + prefix: + event.source === 'request' ? LogPrefix.PHP : LogPrefix.WASM, }); loggerInstance.dispatchEvent( new CustomEvent(loggerInstance.fatalErrorEvent, { diff --git a/packages/php-wasm/logger/src/lib/collectors/collect-window-errors.ts b/packages/php-wasm/logger/src/lib/collectors/collect-window-errors.ts index b3a073cd98..b22f8f6dfa 100644 --- a/packages/php-wasm/logger/src/lib/collectors/collect-window-errors.ts +++ b/packages/php-wasm/logger/src/lib/collectors/collect-window-errors.ts @@ -1,4 +1,4 @@ -import type { Logger } from '../logger'; +import { type Logger, LogSeverity } from '../logger'; /** * Log Windows errors. @@ -9,7 +9,7 @@ import type { Logger } from '../logger'; const logWindowErrorEvent = (loggerInstance: Logger, event: ErrorEvent) => { loggerInstance.logMessage({ message: `${event.message} in ${event.filename} on line ${event.lineno}:${event.colno}`, - severity: 'Error', + severity: LogSeverity.Error, }); }; @@ -30,7 +30,7 @@ const logPromiseRejection = ( const message = event?.reason.stack ?? event.reason; loggerInstance.logMessage({ message, - severity: 'Error', + severity: LogSeverity.Error, }); }; diff --git a/packages/php-wasm/logger/src/lib/handlers/log-event.ts b/packages/php-wasm/logger/src/lib/handlers/log-event.ts index f63901badd..f71bc6c833 100644 --- a/packages/php-wasm/logger/src/lib/handlers/log-event.ts +++ b/packages/php-wasm/logger/src/lib/handlers/log-event.ts @@ -1,6 +1,5 @@ import type { LogHandler } from '../log-handlers'; -import type { Log } from '../logger'; -import { logger } from '../logger'; +import { type Log, logger } from '../logger'; export const logEventType = 'playground-log'; diff --git a/packages/php-wasm/logger/src/lib/handlers/log-to-console.ts b/packages/php-wasm/logger/src/lib/handlers/log-to-console.ts index 24fadf5dc9..da884efdcd 100644 --- a/packages/php-wasm/logger/src/lib/handlers/log-to-console.ts +++ b/packages/php-wasm/logger/src/lib/handlers/log-to-console.ts @@ -1,6 +1,5 @@ import type { LogHandler } from '../log-handlers'; -import type { Log } from '../logger'; -import { prepareLogMessage } from '../logger'; +import { type Log, LogSeverity, prepareLogMessage } from '../logger'; /** * Log message to the console. @@ -23,19 +22,19 @@ export const logToConsole: LogHandler = (log: Log, ...args: any[]): void => { } /* eslint-disable no-console */ switch (log.severity) { - case 'Debug': + case LogSeverity.Debug: console.debug(log.message, ...args); break; - case 'Info': + case LogSeverity.Info: console.info(log.message, ...args); break; - case 'Warn': + case LogSeverity.Warn: console.warn(log.message, ...args); break; - case 'Error': + case LogSeverity.Error: console.error(log.message, ...args); break; - case 'Fatal': + case LogSeverity.Fatal: console.error(log.message, ...args); break; default: diff --git a/packages/php-wasm/logger/src/lib/handlers/log-to-memory.ts b/packages/php-wasm/logger/src/lib/handlers/log-to-memory.ts index f7a2280960..d07da9bca3 100644 --- a/packages/php-wasm/logger/src/lib/handlers/log-to-memory.ts +++ b/packages/php-wasm/logger/src/lib/handlers/log-to-memory.ts @@ -1,6 +1,5 @@ import type { LogHandler } from '../log-handlers'; -import type { Log } from '../logger'; -import { formatLogEntry } from '../logger'; +import { formatLogEntry, type Log, LogPrefix } from '../logger'; const prepareLogMessage = (logMessage: object): string => { if (logMessage instanceof Error) { @@ -26,8 +25,8 @@ export const logToMemory: LogHandler = (log: Log): void => { typeof log.message === 'object' ? prepareLogMessage(log.message) : log.message, - log.severity ?? 'Info', - log.prefix ?? 'JavaScript' + log.severity, + log.prefix ?? LogPrefix.JS ); addToLogArray(message); } diff --git a/packages/php-wasm/logger/src/lib/logger.ts b/packages/php-wasm/logger/src/lib/logger.ts index a572262fdb..531cd73d8b 100644 --- a/packages/php-wasm/logger/src/lib/logger.ts +++ b/packages/php-wasm/logger/src/lib/logger.ts @@ -12,27 +12,54 @@ export { errorLogPath } from './collectors/collect-php-logs'; export type Log = { message: any; - severity?: LogSeverity; + severity: LogSeverity; prefix?: LogPrefix; raw?: boolean; }; +/** + * Log verbosity levels + */ +export const LogVerbosity = { + Normal: 'normal', + Quiet: 'quiet', + Debug: 'debug', +} as const; + +export type LogVerbosity = (typeof LogVerbosity)[keyof typeof LogVerbosity]; + /** * Log severity levels. */ -export type LogSeverity = 'Debug' | 'Info' | 'Warn' | 'Error' | 'Fatal'; +export const LogSeverity = { + Log: { name: 'log', level: 1 }, + Info: { name: 'info', level: 1 }, + Warn: { name: 'warn', level: 1 }, + Error: { name: 'error', level: 1 }, + Fatal: { name: 'fatal', level: 1 }, + Debug: { name: 'debug', level: 2 }, +} as const; + +export type LogSeverity = (typeof LogSeverity)[keyof typeof LogSeverity]; /** * Log prefix. */ -export type LogPrefix = 'WASM Crash' | 'PHP' | 'JavaScript'; +export const LogPrefix = { + WASM: 'Wasm Crash', + PHP: 'PHP', + JS: 'JavaScript', +} as const; + +export type LogPrefix = (typeof LogPrefix)[keyof typeof LogPrefix]; /** * A logger for Playground. */ export class Logger extends EventTarget { public readonly fatalErrorEvent = 'playground-fatal-error'; - private readonly handlers: LogHandler[]; + private handlers: LogHandler[]; + private verbosity = 1; // constructor constructor( @@ -62,14 +89,39 @@ export class Logger extends EventTarget { /** * Log message with severity. * - * @param message any - * @param severity LogSeverity - * @param raw boolean + * @param log Log * @param args any */ - public logMessage(log: Log, ...args: any[]): void { + public logMessage( + log: Omit & { severity?: LogSeverity }, + ...args: any[] + ): void { + const logWithSeverity: Log = { + ...log, + severity: log.severity ?? LogSeverity.Log, + }; for (const handler of this.handlers) { - handler(log, ...args); + if (logWithSeverity.severity.level <= this.verbosity) { + handler(logWithSeverity, ...args); + } + } + } + + /** + * Filter message based on verbosiy + * @param verbosity LogVerbosity + */ + public filterByVerbosity(verbosity: LogVerbosity): void { + switch (verbosity) { + case LogVerbosity.Quiet: + this.verbosity = 0; + break; + case LogVerbosity.Normal: + this.verbosity = 1; + break; + case LogVerbosity.Debug: + this.verbosity = 2; + break; } } @@ -83,8 +135,8 @@ export class Logger extends EventTarget { this.logMessage( { message, - severity: undefined, - prefix: 'JavaScript', + severity: LogSeverity.Log, + prefix: LogPrefix.JS, raw: false, }, ...args @@ -101,8 +153,8 @@ export class Logger extends EventTarget { this.logMessage( { message, - severity: 'Debug', - prefix: 'JavaScript', + severity: LogSeverity.Debug, + prefix: LogPrefix.JS, raw: false, }, ...args @@ -119,8 +171,8 @@ export class Logger extends EventTarget { this.logMessage( { message, - severity: 'Info', - prefix: 'JavaScript', + severity: LogSeverity.Info, + prefix: LogPrefix.JS, raw: false, }, ...args @@ -137,8 +189,8 @@ export class Logger extends EventTarget { this.logMessage( { message, - severity: 'Warn', - prefix: 'JavaScript', + severity: LogSeverity.Warn, + prefix: LogPrefix.JS, raw: false, }, ...args @@ -155,8 +207,8 @@ export class Logger extends EventTarget { this.logMessage( { message, - severity: 'Error', - prefix: 'JavaScript', + severity: LogSeverity.Error, + prefix: LogPrefix.JS, raw: false, }, ...args @@ -209,7 +261,7 @@ export const formatLogEntry = ( }).format(date); const now = formattedDate + ' ' + formattedTime; message = prepareLogMessage(message); - return `[${now}] ${prefix} ${severity}: ${message}`; + return `[${now}] ${prefix} ${severity.name}: ${message}`; }; /** diff --git a/packages/php-wasm/logger/src/test/logger.spec.ts b/packages/php-wasm/logger/src/test/logger.spec.ts index 6e0a28162c..f765c303d1 100644 --- a/packages/php-wasm/logger/src/test/logger.spec.ts +++ b/packages/php-wasm/logger/src/test/logger.spec.ts @@ -1,24 +1,97 @@ -import { logger } from '../lib/logger'; -import { clearMemoryLogs } from '../lib/log-handlers'; +import { type Log, logger, LogVerbosity } from '../lib/logger'; +import { clearMemoryLogs, type LogHandler } from '../lib/log-handlers'; describe('Logger', () => { - beforeEach(async () => { + let output: string[]; + let handlers: LogHandler[]; + + function logToVariable(log: Log, arg?: string) { + output.push(`${log.message}${arg ? arg : ''}`); + } + + beforeAll(() => { + // @ts-ignore + handlers = logger.handlers; + }); + + beforeEach(() => { + output = []; + // @ts-ignore + logger.handlers = [...handlers, logToVariable]; + clearMemoryLogs(); }); - it('Log message should be added', () => { + it('adds message in logs', () => { logger.warn('test'); const logs = logger.getLogs(); expect(logs.length).toBe(1); expect(logs[0]).toMatch( - /\[\d{2}-[A-Za-z]{3,4}-\d{4} \d{2}:\d{2}:\d{2} UTC\] JavaScript Warn: test/ + /\[\d{2}-[A-Za-z]{3,4}-\d{4} \d{2}:\d{2}:\d{2} UTC\] JavaScript warn: test/ ); }); - it('Log event should be dispatched', () => { + it('dispatches log event', () => { const eventListener = vitest.fn(); logger.addEventListener('playground-log', eventListener); logger.warn('test'); expect(eventListener).toHaveBeenCalled(); }); + + it('outputs main logs by default', () => { + logger.log('log'); + logger.info('info'); + logger.warn('warn'); + logger.error('error'); + logger.debug('debug'); + const logs = logger.getLogs(); + expect(logs.length).toBe(4); + expect(output).toEqual(['log', 'info', 'warn', 'error']); + }); + + it('outputs main logs when verbosity is set to normal', () => { + logger.filterByVerbosity(LogVerbosity.Normal); + logger.log('log'); + logger.info('info'); + logger.warn('warn'); + logger.error('error'); + logger.debug('debug'); + const logs = logger.getLogs(); + expect(logs.length).toBe(4); + expect(output).toEqual(['log', 'info', 'warn', 'error']); + }); + + it('outputs main and debug logs when verbosity is set to debug', () => { + logger.filterByVerbosity(LogVerbosity.Debug); + logger.log('log'); + logger.info('info'); + logger.warn('warn'); + logger.error('error'); + logger.debug('debug'); + const logs = logger.getLogs(); + expect(logs.length).toBe(5); + expect(output).toEqual(['log', 'info', 'warn', 'error', 'debug']); + }); + + it('does not output logs when verbosity is set to quiet', () => { + logger.filterByVerbosity(LogVerbosity.Quiet); + logger.log('log'); + logger.info('info'); + logger.warn('warn'); + logger.error('error'); + logger.debug('debug'); + const logs = logger.getLogs(); + expect(logs.length).toBe(0); + expect(output).toEqual([]); + }); + + it('supports logMessage() without explicit severity', () => { + logger.filterByVerbosity(LogVerbosity.Normal); + logger.logMessage({ message: 'test' }); + const logs = logger.getLogs(); + expect(logs.length).toBe(1); + expect(logs[0]).toMatch( + /\[\d{2}-[A-Za-z]{3,4}-\d{4} \d{2}:\d{2}:\d{2} UTC\] JavaScript log: test/ + ); + }); }); diff --git a/packages/php-wasm/xdebug-bridge/README.md b/packages/php-wasm/xdebug-bridge/README.md index fa8963dd08..7e2fbe1598 100644 --- a/packages/php-wasm/xdebug-bridge/README.md +++ b/packages/php-wasm/xdebug-bridge/README.md @@ -1,6 +1,6 @@ # @php-wasm/xdebug-bridge -XDebug bridge server for PHP.wasm that enables debugging connections between XDebug and debugging clients. +A bridge server for XDebug and PHP.wasm that facilitates debugging connections between XDebug and Browser devtools. ## Installation @@ -18,6 +18,10 @@ import { startBridge } from './xdebug-bridge/src/start-bridge'; // Start with default settings const server = startBridge(); await server.start(); +``` + +```typescript +import { startBridge } from './xdebug-bridge/src/start-bridge'; // Start with custom configuration const server = startBridge({ @@ -37,20 +41,29 @@ await server.start(); npx xdebug-bridge # Custom port and verbose logging -npx xdebug-bridge --port 9000 --verbose +npx xdebug-bridge --port 9000 --verbosity debug # Show help npx xdebug-bridge --help ``` -## Configuration Options +## Configuration Options (CLI) + +- `port`: Xdebug port to listen on (default: 9003) +- `host`: Xdebug host to bind to (default: 'localhost') +- `php-root`: Path to PHP root directory (default: './') +- `verbosity`: Output logs and progress messages (default: 'normal') +- `help`: Display help + +## Configuration Options (API) - `cdpPort`: Port to listen for CDP connections (default: 9229) - `cdpHost`: Host to bind to (default: 'localhost') - `dbgpPort`: Port to listen for XDebug connections (default: 9003) -- `phpRoot`: Root path for php files; -- `remoteRoot`: Remote root path for php files; -- `localRoot`: Local root path for php files; +- `phpRoot`: Root path for php files +- `verbosity`: Output logs and progress messages (default: 'normal') +- `remoteRoot`: Remote root path for php files +- `localRoot`: Local root path for php files - `phpInstance`: PHP instance - `getPHPFile`: Custom file listing function @@ -58,11 +71,15 @@ npx xdebug-bridge --help The bridge listens to events for monitoring connection activity: +#### From Xdebug + - `connected`: Xdebug Server has started - `close`: Xdebug Server has stopped - `message`: Raw XDebug data received - `error`: Xdebug Server error occurred +#### To Devtools + - `clientConnected`: Devtools client connected - `clientDisconnected`: Devtools client disconnected - `message`: Raw Devtools data received 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..b1f1a706e2 100644 --- a/packages/php-wasm/xdebug-bridge/src/lib/cdp-server.ts +++ b/packages/php-wasm/xdebug-bridge/src/lib/cdp-server.ts @@ -1,3 +1,4 @@ +import { logger } from '@php-wasm/logger'; import { EventEmitter } from 'events'; import { type WebSocket, WebSocketServer } from 'ws'; @@ -17,7 +18,7 @@ export class CDPServer extends EventEmitter { this.ws = ws; this.emit('clientConnected'); ws.on('message', (data) => { - console.log( + logger.debug( '\x1b[1;32m[CDP][received]\x1b[0m', data.toString() ); @@ -44,7 +45,11 @@ export class CDPServer extends EventEmitter { return; } const json = JSON.stringify(message); - console.log('\x1b[1;32m[CDP][send]\x1b[0m', json); + logger.debug('\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..9aefe97020 100644 --- a/packages/php-wasm/xdebug-bridge/src/lib/dbgp-session.ts +++ b/packages/php-wasm/xdebug-bridge/src/lib/dbgp-session.ts @@ -1,3 +1,4 @@ +import { logger } from '@php-wasm/logger'; import { EventEmitter } from 'events'; import net from 'net'; @@ -33,7 +34,7 @@ export class DbgpSession extends EventEmitter { } private onData(data: string) { - console.log('\x1b[1;32m[XDebug][received]]\x1b[0m', data); + logger.debug('\x1b[1;32m[XDebug][received]]\x1b[0m', data); this.buffer += data; while (true) { if (this.expectedLength === null) { @@ -78,6 +79,11 @@ export class DbgpSession extends EventEmitter { sendCommand(command: string) { if (!this.socket) return; // Commands must end with null terminator + logger.debug('\x1b[1;32m[XDebug][send]\x1b[0m', command); this.socket.write(command + '\x00'); } + + close() { + this.server.close(); + } } diff --git a/packages/php-wasm/xdebug-bridge/src/lib/run-cli.ts b/packages/php-wasm/xdebug-bridge/src/lib/run-cli.ts index 8433050de4..4ed304f390 100644 --- a/packages/php-wasm/xdebug-bridge/src/lib/run-cli.ts +++ b/packages/php-wasm/xdebug-bridge/src/lib/run-cli.ts @@ -1,14 +1,14 @@ import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import { startBridge } from './start-bridge'; +import { LogVerbosity } from '@php-wasm/logger'; interface CLIArgs { - protocol?: 'cdp' | 'dap'; port?: number; host?: string; - verbose?: boolean; - help?: boolean; phpRoot?: string; + verbosity?: LogVerbosity; + help?: boolean; } function parseCliArgs(): CLIArgs { @@ -37,15 +37,22 @@ Usage: xdebug-bridge [options] description: 'Path to PHP root directory', default: './', }) + .option('verbosity', { + type: 'string', + describe: 'Output logs', + choices: Object.values(LogVerbosity), + default: 'normal', + }) .help() .epilog( ` Examples: xdebug-bridge # Start with default settings - xdebug-bridge --port 9000 --verbose # Custom port with verbose logging - xdebug-bridge --php-root /path/to/php/files # Specify PHP root directory + xdebug-bridge --port 9000 --verbosity debug # Custom port with debug logs + xdebug-bridge --php-root /path/to/php/files # Specify PHP root directory ` ) + .wrap(null) .parseSync() as CLIArgs; } @@ -56,13 +63,12 @@ export async function main(): Promise { return; } - console.log('Starting XDebug Bridge...'); - const bridge = await startBridge({ cdpPort: 9229, cdpHost: args.host, dbgpPort: args.port, phpRoot: args.phpRoot, + verbosity: args.verbosity, }); bridge.start(); 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..bc8127184c 100644 --- a/packages/php-wasm/xdebug-bridge/src/lib/start-bridge.ts +++ b/packages/php-wasm/xdebug-bridge/src/lib/start-bridge.ts @@ -1,3 +1,4 @@ +import { logger, type LogVerbosity } from '@php-wasm/logger'; import type { PHP } from '@php-wasm/universal'; import { readdirSync, readFileSync, lstatSync } from 'fs'; import { join } from 'path'; @@ -10,6 +11,7 @@ export type StartBridgeConfig = { cdpHost?: string; dbgpPort?: number; phpRoot?: string; + verbosity?: LogVerbosity; remoteRoot?: string; localRoot?: string; @@ -23,22 +25,29 @@ export async function startBridge(config: StartBridgeConfig) { const cdpHost = config.cdpHost ?? 'localhost'; const phpRoot = config.phpRoot ?? import.meta.dirname; + if (config.verbosity) { + logger.filterByVerbosity(config.verbosity); + } + + logger.log('Starting XDebug Bridge...'); + // index.ts - Entry point to start the service const cdpServer = new CDPServer(cdpPort); - console.log('Connect Chrome DevTools to CDP at:'); - console.log( - `devtools://devtools/bundled/inspector.html?ws=${cdpHost}:${cdpPort}` + logger.log('Connect Chrome DevTools to CDP at:'); + logger.log( + `devtools://devtools/bundled/inspector.html?ws=${cdpHost}:${cdpPort}\n` ); + await new Promise((resolve) => cdpServer.on('clientConnected', resolve)); await new Promise((resolve) => setTimeout(resolve, 2000)); - console.log('Chrome connected! Initializing Xdebug receiver...'); + logger.log('Chrome connected! Initializing Xdebug receiver...'); const dbgpSession = new DbgpSession(dbgpPort); - console.log(`XDebug receiver running on port ${dbgpPort}`); - console.log('Running a PHP script with Xdebug enabled...'); + logger.log(`XDebug receiver running on port ${dbgpPort}`); + logger.log('Running a PHP script with Xdebug enabled...'); // Recursively get a list of .php files in phpRoot function getPhpFiles(dir: string): string[] { 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..d006ad36bb 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 @@ -136,6 +136,11 @@ 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)) { @@ -201,7 +206,6 @@ export class XdebugCDPBridge { } private sendDbgpCommand(command: string, data?: string): string { - console.log('\x1b[1;32m[XDebug][send]\x1b[0m', command, data); const txnId = this.nextTxnId++; const txnIdStr = txnId.toString(); let cmdStr = `${command} -i ${txnIdStr}`; 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/tests/start-bridge.spec.ts b/packages/php-wasm/xdebug-bridge/tests/start-bridge.spec.ts new file mode 100644 index 0000000000..4af4a835aa --- /dev/null +++ b/packages/php-wasm/xdebug-bridge/tests/start-bridge.spec.ts @@ -0,0 +1,89 @@ +import { startBridge } from '../src/lib/start-bridge'; +import { type Log, logger } from '@php-wasm/logger/src'; +import { WebSocket } from 'ws'; + +describe('verbosity', () => { + const port = 9229; + let output: string[]; + + function logToVariable(log: Log, arg?: string) { + output.push(`${log.message}${arg ? arg : ''}`); + } + + beforeEach(() => { + output = []; + // @ts-ignore + logger.handlers = [logToVariable]; + }); + + it('outputs main logs by default', async () => { + new WebSocket(`ws://localhost:${port}`); + + const bridge = await startBridge({ cdpPort: port }); + + expect(output).toEqual([ + 'Starting XDebug Bridge...', + 'Connect Chrome DevTools to CDP at:', + `devtools://devtools/bundled/inspector.html?ws=localhost:${port}\n`, + 'Chrome connected! Initializing Xdebug receiver...', + 'XDebug receiver running on port 9003', + 'Running a PHP script with Xdebug enabled...', + ]); + + bridge.cdp.sendMessage('Hello Xdebug world'); + + bridge.stop(); + + expect(output).not.toContain('[CDP][send]"Hello Xdebug world"'); + }); + + it('outputs main logs with verbosity option set to normal', async () => { + new WebSocket(`ws://localhost:${port}`); + + const bridge = await startBridge({ + cdpPort: port, + verbosity: 'normal', + }); + + expect(output).toEqual([ + 'Starting XDebug Bridge...', + 'Connect Chrome DevTools to CDP at:', + `devtools://devtools/bundled/inspector.html?ws=localhost:${port}\n`, + 'Chrome connected! Initializing Xdebug receiver...', + 'XDebug receiver running on port 9003', + 'Running a PHP script with Xdebug enabled...', + ]); + + bridge.cdp.sendMessage('Hello Xdebug world'); + + bridge.stop(); + + expect(output).not.toContain( + '\x1B[1;32m[CDP][send]\x1B[0m"Hello Xdebug world"' + ); + }); + + it('outputs main logs and the communication inside the bridge with verbosity option set to debug', async () => { + new WebSocket(`ws://localhost:${port}`); + + const bridge = await startBridge({ cdpPort: port, verbosity: 'debug' }); + + bridge.cdp.sendMessage('Hello Xdebug world'); + + bridge.stop(); + + expect(output).toContain( + '\x1B[1;32m[CDP][send]\x1B[0m"Hello Xdebug world"' + ); + }); + + it('does not output logs when verbosity option set to quiet', async () => { + new WebSocket(`ws://localhost:${port}`); + + const bridge = await startBridge({ cdpPort: port, verbosity: 'quiet' }); + + bridge.stop(); + + expect(output).toEqual([]); + }); +}); diff --git a/packages/playground/cli/README.md b/packages/playground/cli/README.md index 2a58fd6758..b0cb7c4988 100644 --- a/packages/playground/cli/README.md +++ b/packages/playground/cli/README.md @@ -93,7 +93,7 @@ The `server` command supports the following optional arguments: - `--login`: Automatically log the user in as an administrator. - `--skip-wordpress-setup`: Do not download or install WordPress. Useful if you are mounting a full WordPress directory. - `--skip-sqlite-setup`: Do not set up the SQLite database integration. -- `--quiet`: Do not output logs and progress messages. +- `--verbosity`: Output logs and progress messages. - `--debug`: Print the PHP error log if an error occurs during boot. - `--follow-symlinks`: Allow Playground to follow symlinks by automatically mounting symlinked directories and files encountered in mounted directories. ⚠️ Warning: Following symlinks will expose files outside mounted directories to Playground and could be a security risk. - `--experimental-multi-worker`: Enables experimental multi-worker support. It needs JSPI and a /wordpress directory on a real filesystem. You can pass a positive number to use a specific number of workers, otherwise, it defaults to the number of CPUs minus one. diff --git a/packages/playground/cli/src/blueprints-v1/blueprints-v1-handler.ts b/packages/playground/cli/src/blueprints-v1/blueprints-v1-handler.ts index ae4409503a..16aec0388d 100644 --- a/packages/playground/cli/src/blueprints-v1/blueprints-v1-handler.ts +++ b/packages/playground/cli/src/blueprints-v1/blueprints-v1-handler.ts @@ -1,4 +1,4 @@ -import { logger } from '@php-wasm/logger'; +import { logger, LogVerbosity } from '@php-wasm/logger'; import { EmscriptenDownloadMonitor, ProgressTracker } from '@php-wasm/progress'; import type { SupportedPHPVersion } from '@php-wasm/universal'; import { consumeAPI } from '@php-wasm/universal'; @@ -96,7 +96,7 @@ export class BlueprintsV1Handler { ); progressReached100 = percentProgress === 100; - if (!this.args.quiet) { + if (this.args.verbosity !== LogVerbosity.Quiet) { this.writeProgressUpdate( process.stdout, `Downloading WordPress ${percentProgress}%...`, @@ -262,7 +262,7 @@ export class BlueprintsV1Handler { lastCaption = e.detail.caption || lastCaption || 'Running the Blueprint'; const message = `${lastCaption.trim()} – ${progressInteger}%`; - if (!args.quiet) { + if (args.verbosity !== LogVerbosity.Quiet) { this.writeProgressUpdate( process.stdout, message, diff --git a/packages/playground/cli/src/run-cli.ts b/packages/playground/cli/src/run-cli.ts index a425046174..3a80b526a2 100644 --- a/packages/playground/cli/src/run-cli.ts +++ b/packages/playground/cli/src/run-cli.ts @@ -1,4 +1,4 @@ -import { errorLogPath, logger } from '@php-wasm/logger'; +import { errorLogPath, logger, LogVerbosity } from '@php-wasm/logger'; import type { PHPRequest, RemoteAPI, @@ -142,10 +142,18 @@ export async function parseOptionsAndRunCLI() { type: 'boolean', default: false, }) + // Hidden - Deprecated in favor of verbosity .option('quiet', { describe: 'Do not output logs and progress messages.', type: 'boolean', default: false, + hidden: true, + }) + .option('verbosity', { + describe: 'Output logs and progress messages.', + type: 'string', + choices: Object.values(LogVerbosity), + default: 'normal', }) .option('debug', { describe: @@ -299,6 +307,7 @@ export interface RunCLIArgs { php?: SupportedPHPVersion; port?: number; quiet?: boolean; + verbosity?: LogVerbosity; wp?: string; autoMount?: boolean; experimentalMultiWorker?: number; @@ -355,9 +364,23 @@ export async function runCLI(args: RunCLIArgs): Promise { args = expandAutoMounts(args); } + // Keeping 'quiet' option to preserve backward compatibility if (args.quiet) { - // @ts-ignore - logger.handlers = []; + args.verbosity = 'quiet'; + delete args['quiet']; + } + + // Promote "debug" flag to verbosity but keep args.debug around – the + // program behavior may change in more ways than just logging verbosity + // when debug mode is enabled, e.g. error objects may carry additional details. + if (args.debug) { + args.verbosity = 'debug'; + } else if (args.verbosity === 'debug') { + args.debug = true; + } + + if (args.verbosity) { + logger.filterByVerbosity(args.verbosity); } // Declare file lock manager outside scope of startServer diff --git a/packages/playground/cli/tests/run-cli.spec.ts b/packages/playground/cli/tests/run-cli.spec.ts index 218ac8d37e..0e21e6cb76 100644 --- a/packages/playground/cli/tests/run-cli.spec.ts +++ b/packages/playground/cli/tests/run-cli.spec.ts @@ -10,6 +10,7 @@ import { exec } from 'node:child_process'; import { readdirSync } from 'node:fs'; import { createHash } from 'node:crypto'; import { MinifiedWordPressVersionsList } from '@wp-playground/wordpress-builds'; +import { type Log, logger } from '@php-wasm/logger'; describe('run-cli', () => { let cliServer: RunCLIServer; @@ -236,4 +237,80 @@ describe('run-cli', () => { expect(await getDirectoryChecksum(tmpDir)).toBe(checksum); }); }); + + describe('verbosity', () => { + let output: string[]; + + function logToVariable(log: Log, arg?: string) { + output.push(`${log.message}${arg ? arg : ''}`); + } + + beforeAll(() => { + // @ts-ignore + logger.handlers = [logToVariable]; + }); + + beforeEach(() => { + output = []; + }); + + test('should output main logs by default', async () => { + cliServer = await runCLI({ + command: 'server', + }); + + expect(output).toEqual( + expect.arrayContaining([ + 'Starting a PHP server...', + 'Setting up WordPress undefined', + expect.stringMatching( + /^Resolved WordPress release URL: https:\/\/downloads\.w\.org\/release\/wordpress-\d+\.\d+\.\d+\.zip$/ + ), + 'Fetching SQLite integration plugin...', + 'Booting WordPress...', + 'Booted!', + 'Running the Blueprint...', + 'Finished running the blueprint', + expect.stringMatching( + /^WordPress is running on http:\/\/127\.0\.0\.1:\d+$/ + ), + ]) + ); + }); + + test('should not output debug logs with verbosity option set to normal', async () => { + cliServer = await runCLI({ + command: 'server', + verbosity: 'normal', + }); + + const test = 'Debug log'; + + logger.debug(test); + + expect(output).not.toContain(test); + }); + + test('should output debug logs bridge with verbosity option set to debug', async () => { + cliServer = await runCLI({ + command: 'server', + verbosity: 'debug', + }); + + const test = 'Debug log'; + + logger.debug(test); + + expect(output).toContain(test); + }); + + it('should not output logs when verbosity option set to quiet', async () => { + cliServer = await runCLI({ + command: 'server', + verbosity: 'quiet', + }); + + expect(output).toEqual([]); + }); + }); }); diff --git a/packages/playground/test-built-npm-packages/es-modules-and-vitest/tests/wp.spec.ts b/packages/playground/test-built-npm-packages/es-modules-and-vitest/tests/wp.spec.ts index 255c839cd3..dd88350efe 100644 --- a/packages/playground/test-built-npm-packages/es-modules-and-vitest/tests/wp.spec.ts +++ b/packages/playground/test-built-npm-packages/es-modules-and-vitest/tests/wp.spec.ts @@ -17,7 +17,7 @@ describe(`PHP ${phpVersion}`, () => { const cli = await runCLI({ command: 'server', php: phpVersion, - quiet: true, + verbosity: 'quiet', exitOnPrimaryWorkerCrash: false, }); try { diff --git a/tools/scripts/local-package-repository.sh b/tools/scripts/local-package-repository.sh index 6922ae8277..9e4f70267f 100755 --- a/tools/scripts/local-package-repository.sh +++ b/tools/scripts/local-package-repository.sh @@ -44,4 +44,4 @@ node \ ./packages/playground/cli/src/cli.ts server \ --port=$PORT \ --mount="$HOST_PATH:/wordpress/$VERSION" \ - --quiet + --verbosity=quiet