diff --git a/packages/playground/cli/src/blueprints-v1/worker-thread-v1.ts b/packages/playground/cli/src/blueprints-v1/worker-thread-v1.ts index 6e787fdf09..6cf9a21304 100644 --- a/packages/playground/cli/src/blueprints-v1/worker-thread-v1.ts +++ b/packages/playground/cli/src/blueprints-v1/worker-thread-v1.ts @@ -3,7 +3,6 @@ import { loadNodeRuntime } from '@php-wasm/node'; import { EmscriptenDownloadMonitor } from '@php-wasm/progress'; import type { RemoteAPI, SupportedPHPVersion } from '@php-wasm/universal'; import { - PHPWorker, consumeAPI, consumeAPISync, exposeAPI, @@ -11,10 +10,11 @@ import { } from '@php-wasm/universal'; import { sprintf } from '@php-wasm/util'; import { RecommendedPHPVersion } from '@wp-playground/common'; -import { bootWordPress, bootRequestHandler } from '@wp-playground/wordpress'; +import { bootRequestHandler, bootWordPress } from '@wp-playground/wordpress'; import { rootCertificates } from 'tls'; import { jspi } from 'wasm-feature-detect'; -import { MessageChannel, type MessagePort, parentPort } from 'worker_threads'; +import { MessageChannel, parentPort, type MessagePort } from 'worker_threads'; +import { PlaygroundCliWorker } from '../playground-cli-worker'; import { mountResources } from '../mounts'; export interface Mount { @@ -78,7 +78,7 @@ function tracePhpWasm(processId: number, format: string, ...args: any[]) { ); } -export class PlaygroundCliBlueprintV1Worker extends PHPWorker { +export class PlaygroundCliBlueprintV1Worker extends PlaygroundCliWorker { booted = false; fileLockManager: RemoteAPI | FileLockManager | undefined; @@ -293,11 +293,6 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker { throw e; } } - - // Provide a named disposal method that can be invoked via comlink. - async dispose() { - await this[Symbol.asyncDispose](); - } } const phpChannel = new MessageChannel(); diff --git a/packages/playground/cli/src/blueprints-v2/worker-thread-v2.ts b/packages/playground/cli/src/blueprints-v2/worker-thread-v2.ts index ed5fb8bfb3..7c48a426c6 100644 --- a/packages/playground/cli/src/blueprints-v2/worker-thread-v2.ts +++ b/packages/playground/cli/src/blueprints-v2/worker-thread-v2.ts @@ -3,15 +3,14 @@ import type { FileLockManager } from '@php-wasm/node'; import { createNodeFsMountHandler, loadNodeRuntime } from '@php-wasm/node'; import { EmscriptenDownloadMonitor } from '@php-wasm/progress'; import type { - PHP, FileTree, + PHP, RemoteAPI, SupportedPHPVersion, } from '@php-wasm/universal'; import { PHPExecutionFailureError, PHPResponse, - PHPWorker, consumeAPI, consumeAPISync, exposeAPI, @@ -19,25 +18,24 @@ import { } from '@php-wasm/universal'; import { sprintf } from '@php-wasm/util'; import { - type BlueprintMessage, runBlueprintV2, -} from '@wp-playground/blueprints'; -import { - type ParsedBlueprintV2Declaration, + type BlueprintMessage, type BlueprintV2Declaration, + type ParsedBlueprintV2Declaration, } from '@wp-playground/blueprints'; +import type { + PHPInstanceCreatedHook, + PhpIniOptions, +} from '@wp-playground/wordpress'; import { bootRequestHandler } from '@wp-playground/wordpress'; import { existsSync } from 'fs'; import path from 'path'; import { rootCertificates } from 'tls'; -import { MessageChannel, type MessagePort, parentPort } from 'worker_threads'; -import type { Mount } from '../mounts'; import { jspi } from 'wasm-feature-detect'; +import { MessageChannel, parentPort, type MessagePort } from 'worker_threads'; +import { PlaygroundCliWorker } from '../playground-cli-worker'; +import type { Mount } from '../mounts'; import { type RunCLIArgs } from '../run-cli'; -import type { - PhpIniOptions, - PHPInstanceCreatedHook, -} from '@wp-playground/wordpress'; async function mountResources(php: PHP, mounts: Mount[]) { for (const mount of mounts) { @@ -154,7 +152,7 @@ export type WorkerBootRequestHandlerOptions = Omit< onPHPInstanceCreated: PHPInstanceCreatedHook; }; -export class PlaygroundCliBlueprintV2Worker extends PHPWorker { +export class PlaygroundCliBlueprintV2Worker extends PlaygroundCliWorker { booted = false; blueprintTargetResolved = false; phpInstancesThatNeedMountsAfterTargetResolved = new Set(); @@ -470,11 +468,6 @@ export class PlaygroundCliBlueprintV2Worker extends PHPWorker { throw e; } } - - // Provide a named disposal method that can be invoked via comlink. - async dispose() { - await this[Symbol.asyncDispose](); - } } const phpChannel = new MessageChannel(); diff --git a/packages/playground/cli/src/load-balancer.ts b/packages/playground/cli/src/load-balancer.ts index a4bafeb6dc..8c04580afe 100644 --- a/packages/playground/cli/src/load-balancer.ts +++ b/packages/playground/cli/src/load-balancer.ts @@ -1,8 +1,5 @@ import type { PHPRequest, PHPResponse, RemoteAPI } from '@php-wasm/universal'; -import type { PlaygroundCliBlueprintV1Worker as PlaygroundCliWorkerV1 } from './blueprints-v1/worker-thread-v1'; -import type { PlaygroundCliBlueprintV2Worker as PlaygroundCliWorkerV2 } from './blueprints-v2/worker-thread-v2'; - -type PlaygroundCliWorker = PlaygroundCliWorkerV1 | PlaygroundCliWorkerV2; +import type { PlaygroundCliWorker } from './playground-cli-worker'; // TODO: Let's merge worker management into PHPProcessManager // when we can have multiple workers in both CLI and web. diff --git a/packages/playground/cli/src/playground-cli-worker.ts b/packages/playground/cli/src/playground-cli-worker.ts new file mode 100644 index 0000000000..88c45e0ebd --- /dev/null +++ b/packages/playground/cli/src/playground-cli-worker.ts @@ -0,0 +1,30 @@ +import { PHPWorker } from '@php-wasm/universal'; + +export class PlaygroundCliWorker extends PHPWorker { + async runCLIScript( + argv: string[], + options: { env?: Record } = {} + ) { + const streamedResponse = await this.cli(argv, options); + streamedResponse.stdout.pipeTo( + new WritableStream({ + write(chunk) { + process.stdout.write(chunk); + }, + }) + ); + streamedResponse.stderr.pipeTo( + new WritableStream({ + write(chunk) { + process.stderr.write(chunk); + }, + }) + ); + return await streamedResponse.exitCode; + } + + // Provide a named disposal method that can be invoked via comlink. + async dispose() { + await this[Symbol.asyncDispose](); + } +} diff --git a/packages/playground/cli/src/run-cli.ts b/packages/playground/cli/src/run-cli.ts index 537a2eaca9..379254e09c 100644 --- a/packages/playground/cli/src/run-cli.ts +++ b/packages/playground/cli/src/run-cli.ts @@ -26,11 +26,7 @@ import { parseMountWithDelimiterArguments, } from './mounts'; import { startServer } from './start-server'; -import type { - Mount, - PlaygroundCliBlueprintV1Worker, -} from './blueprints-v1/worker-thread-v1'; -import type { PlaygroundCliBlueprintV2Worker } from './blueprints-v2/worker-thread-v2'; +import type { Mount } from './blueprints-v1/worker-thread-v1'; import { FileLockManagerForNode } from '@php-wasm/node'; import { LoadBalancer } from './load-balancer'; /* eslint-disable no-console */ @@ -49,6 +45,7 @@ import { cleanupStalePlaygroundTempDirs, createPlaygroundCliTempDir, } from './temp-dir'; +import type { PlaygroundCliWorker } from './playground-cli-worker'; // Inlined worker URLs for static analysis by downstream bundlers // These are replaced at build time by the Vite plugin in vite.config.ts @@ -75,7 +72,12 @@ export async function parseOptionsAndRunCLI() { .usage('Usage: wp-playground [options]') .positional('command', { describe: 'Command to run', - choices: ['server', 'run-blueprint', 'build-snapshot'] as const, + choices: [ + 'server', + 'run-blueprint', + 'build-snapshot', + 'php', + ] as const, demandOption: true, }) .option('outfile', { @@ -111,12 +113,14 @@ export async function parseOptionsAndRunCLI() { 'Mount a directory to the PHP runtime (can be used multiple times). Format: /host/path:/vfs/path', type: 'array', string: true, + nargs: 1, coerce: parseMountWithDelimiterArguments, }) .option('mount-before-install', { describe: 'Mount a directory to the PHP runtime before WordPress installation (can be used multiple times). Format: /host/path:/vfs/path', type: 'array', + nargs: 1, string: true, coerce: parseMountWithDelimiterArguments, }) @@ -347,6 +351,12 @@ export async function parseOptionsAndRunCLI() { } return true; + }) + .command('php', 'Run a PHP script', (yargs) => { + return yargs.positional('argv', { + describe: 'arguments to pass to the PHP CLI', + type: 'string', + }); }); yargsObject.wrap(yargsObject.terminalWidth()); @@ -354,7 +364,11 @@ export async function parseOptionsAndRunCLI() { const command = args._[0] as string; - if (!['run-blueprint', 'server', 'build-snapshot'].includes(command)) { + if ( + !['run-blueprint', 'server', 'build-snapshot', 'php'].includes( + command + ) + ) { yargsObject.showHelp(); process.exit(1); } @@ -393,8 +407,13 @@ export async function parseOptionsAndRunCLI() { } export interface RunCLIArgs { + /** + * `_` holds positional tokens in the order they appeared. + * `_[0]` will typically be the command name. + */ + _: string[]; blueprint?: BlueprintDeclaration | BlueprintBundle; - command: 'server' | 'run-blueprint' | 'build-snapshot'; + command: 'server' | 'run-blueprint' | 'build-snapshot' | 'php'; debug?: boolean; login?: boolean; mount?: Mount[]; @@ -436,10 +455,6 @@ export interface RunCLIArgs { allow?: string; } -type PlaygroundCliWorker = - | PlaygroundCliBlueprintV1Worker - | PlaygroundCliBlueprintV2Worker; - export interface RunCLIServer extends AsyncDisposable { playground: RemoteAPI; server: Server; @@ -690,6 +705,21 @@ export async function runCLI(args: RunCLIArgs): Promise { } else if (args.command === 'run-blueprint') { logger.log(`Blueprint executed`); process.exit(0); + } else if (args.command === 'php') { + const argv = [ + // @TODO: import this from somewhere? Hardcoding it feels fragile. + '/internal/shared/bin/php', + /** + * args._ are all unparsed positionals arguments, e.g. + */ + ...((args as any)['_'] || []).slice(1), + ]; + // @TODO: Call .cli(). Problem: It returns StreamedPHPResponse which + // fails to go through comlink machinery. + const exitCode = await playground.runCLIScript(argv); + // Wait until the next tick before exiting to ensure the output is flushed. + await new Promise((resolve) => setTimeout(resolve, 0)); + process.exit(exitCode); } if ( @@ -836,7 +866,6 @@ async function spawnWorkerThreads( } }); worker.once('error', function (e: Error) { - console.error(e); const error = new Error( `Worker failed to load worker. ${ e.message ? `Original error: ${e.message}` : ''