Skip to content

Commit 08d7a8e

Browse files
authored
New playground CLI function interface (#40)
## Motivation for the change, related issues Details here: https://playgroundp2.wordpress.com/2025/02/11/studio-php-execution-in-playground-cli-child-process/ ## Implementation details The PR adds the new `runCLI` function that exports the `Express` server and the `PHPRequestHandler`. The caller then can access to the `PHP` object as well as start/stop the server by doing, for example: ```javascript const res = runCLI( { command: 'server', ... } ); res.server.close( () => { res.requestHandler.getPrimaryPhp().exit(); } ); ``` ## Testing Instructions (or ideally a Blueprint) Ensure this is still working with various flags passed: ``` bun ./packages/playground/cli/src/cli.ts server ``` Then build the package using `npm run build` and check if the `dist/packages/playground/cli/index.js` file exists with a `runCLI` exported.
1 parent 756ea70 commit 08d7a8e

File tree

5 files changed

+357
-298
lines changed

5 files changed

+357
-298
lines changed

packages/playground/cli/src/cli.ts

Lines changed: 10 additions & 287 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,11 @@
1+
import { SupportedPHPVersions } from '@php-wasm/universal';
2+
import { RecommendedPHPVersion } from '@wp-playground/common';
13
import fs from 'fs';
24
import path from 'path';
35
import yargs from 'yargs';
4-
import readline from 'readline';
5-
import { startServer } from './server';
6-
import {
7-
PHP,
8-
PHPRequest,
9-
PHPRequestHandler,
10-
PHPResponse,
11-
SupportedPHPVersion,
12-
SupportedPHPVersions,
13-
} from '@php-wasm/universal';
14-
import { logger, errorLogPath } from '@php-wasm/logger';
15-
import {
16-
Blueprint,
17-
compileBlueprint,
18-
runBlueprintSteps,
19-
} from '@wp-playground/blueprints';
206
import { isValidWordPressSlug } from './is-valid-wordpress-slug';
21-
import { EmscriptenDownloadMonitor, ProgressTracker } from '@php-wasm/progress';
22-
import { createNodeFsMountHandler, loadNodeRuntime } from '@php-wasm/node';
23-
import { RecommendedPHPVersion, zipDirectory } from '@wp-playground/common';
24-
import { bootWordPress } from '@wp-playground/wordpress';
25-
import { rootCertificates } from 'tls';
26-
import {
27-
CACHE_FOLDER,
28-
cachedDownload,
29-
fetchSqliteIntegration,
30-
readAsFile,
31-
} from './download';
32-
import { resolveWordPressRelease } from '@wp-playground/wordpress';
7+
import { runCLI, RunCLIArgs } from './run-cli';
8+
339
export interface Mount {
3410
hostPath: string;
3511
vfsPath: string;
@@ -40,12 +16,12 @@ async function run() {
4016
* @TODO This looks similar to Query API args https://wordpress.github.io/wordpress-playground/developers/apis/query-api/
4117
* Perhaps the two could be handled by the same code?
4218
*/
43-
const yargsObject = await yargs(process.argv.slice(2))
19+
const yargsObject = yargs(process.argv.slice(2))
4420
.usage('Usage: wp-playground <command> [options]')
4521
.positional('command', {
4622
describe: 'Command to run',
47-
type: 'string',
48-
choices: ['server', 'run-blueprint', 'build-snapshot'],
23+
choices: ['server', 'run-blueprint', 'build-snapshot'] as const,
24+
demandOption: true,
4925
})
5026
.option('outfile', {
5127
describe: 'When building, write to this output file.',
@@ -142,269 +118,16 @@ async function run() {
142118
yargsObject.wrap(yargsObject.terminalWidth());
143119
const args = await yargsObject.argv;
144120

145-
if (args.quiet) {
146-
// @ts-ignore
147-
logger.handlers = [];
148-
}
149-
150-
/**
151-
* TODO: This exact feature will be provided in the PHP Blueprints library.
152-
* Let's use it when it ships. Let's also use it in the web Playground
153-
* app.
154-
*/
155-
async function zipSite(outfile: string) {
156-
// Fake URL for the build
157-
const { php, reap } =
158-
await requestHandler.processManager.acquirePHPInstance();
159-
try {
160-
await php.run({
161-
code: `<?php
162-
$zip = new ZipArchive();
163-
if(false === $zip->open('/tmp/build.zip', ZipArchive::CREATE | ZipArchive::OVERWRITE)) {
164-
throw new Exception('Failed to create ZIP');
165-
}
166-
$files = new RecursiveIteratorIterator(
167-
new RecursiveDirectoryIterator('/wordpress')
168-
);
169-
foreach ($files as $file) {
170-
echo $file . PHP_EOL;
171-
if (!$file->isFile()) {
172-
continue;
173-
}
174-
$zip->addFile($file->getPathname(), $file->getPathname());
175-
}
176-
$zip->close();
177-
178-
`,
179-
});
180-
const zip = php.readFileAsBuffer('/tmp/build.zip');
181-
fs.writeFileSync(outfile, zip);
182-
} finally {
183-
reap();
184-
}
185-
}
186-
187-
function mountResources(php: PHP, rawMounts: string[]) {
188-
const parsedMounts = rawMounts.map((mount) => {
189-
const [source, vfsPath] = mount.split(':');
190-
return {
191-
hostPath: path.resolve(process.cwd(), source),
192-
vfsPath,
193-
};
194-
});
195-
for (const mount of parsedMounts) {
196-
php.mkdir(mount.vfsPath);
197-
php.mount(mount.vfsPath, createNodeFsMountHandler(mount.hostPath));
198-
}
199-
}
200-
201-
function compileInputBlueprint() {
202-
/**
203-
* @TODO This looks similar to the resolveBlueprint() call in the website package:
204-
* https://github.com/WordPress/wordpress-playground/blob/ce586059e5885d185376184fdd2f52335cca32b0/packages/playground/website/src/main.tsx#L41
205-
*
206-
* Also the Blueprint Builder tool does something similar.
207-
* Perhaps all these cases could be handled by the same function?
208-
*/
209-
let blueprint: Blueprint | undefined;
210-
if (args.blueprint) {
211-
blueprint = args.blueprint as Blueprint;
212-
} else {
213-
blueprint = {
214-
preferredVersions: {
215-
php: args.php as SupportedPHPVersion,
216-
wp: args.wp,
217-
},
218-
login: args.login,
219-
};
220-
}
221-
222-
const tracker = new ProgressTracker();
223-
let lastCaption = '';
224-
let progress100 = false;
225-
tracker.addEventListener('progress', (e: any) => {
226-
if (progress100) {
227-
return;
228-
} else if (e.detail.progress === 100) {
229-
progress100 = true;
230-
}
231-
lastCaption =
232-
e.detail.caption || lastCaption || 'Running the Blueprint';
233-
readline.clearLine(process.stdout, 0);
234-
readline.cursorTo(process.stdout, 0);
235-
process.stdout.write(
236-
'\r\x1b[K' + `${lastCaption.trim()}${e.detail.progress}%`
237-
);
238-
if (progress100) {
239-
process.stdout.write('\n');
240-
}
241-
});
242-
return compileBlueprint(blueprint as Blueprint, {
243-
progress: tracker,
244-
});
245-
}
246-
247121
const command = args._[0] as string;
122+
248123
if (!['run-blueprint', 'server', 'build-snapshot'].includes(command)) {
249124
yargsObject.showHelp();
250125
process.exit(1);
251126
}
252127

253-
const compiledBlueprint = compileInputBlueprint();
254-
255-
let requestHandler: PHPRequestHandler;
256-
let wordPressReady = false;
128+
args.command = args._[0] as any;
257129

258-
logger.log('Starting a PHP server...');
259-
260-
startServer({
261-
port: args['port'] as number,
262-
onBind: async (port: number) => {
263-
const absoluteUrl = `http://127.0.0.1:${port}`;
264-
265-
logger.log(`Setting up WordPress ${args.wp}`);
266-
let wpDetails: any = undefined;
267-
const monitor = new EmscriptenDownloadMonitor();
268-
if (!args.skipWordPressSetup) {
269-
// @TODO: Rename to FetchProgressMonitor. There's nothing Emscripten
270-
// about that class anymore.
271-
monitor.addEventListener('progress', ((
272-
e: CustomEvent<ProgressEvent & { finished: boolean }>
273-
) => {
274-
// @TODO Every progres bar will want percentages. The
275-
// download monitor should just provide that.
276-
const percentProgress = Math.round(
277-
Math.min(100, (100 * e.detail.loaded) / e.detail.total)
278-
);
279-
if (!args.quiet) {
280-
readline.clearLine(process.stdout, 0);
281-
readline.cursorTo(process.stdout, 0);
282-
process.stdout.write(
283-
`Downloading WordPress ${percentProgress}%...`
284-
);
285-
}
286-
}) as any);
287-
288-
wpDetails = await resolveWordPressRelease(args.wp);
289-
}
290-
logger.log(
291-
`Resolved WordPress release URL: ${wpDetails?.releaseUrl}`
292-
);
293-
294-
const preinstalledWpContentPath =
295-
wpDetails &&
296-
path.join(
297-
CACHE_FOLDER,
298-
`prebuilt-wp-content-for-wp-${wpDetails.version}.zip`
299-
);
300-
const wordPressZip = !wpDetails
301-
? undefined
302-
: fs.existsSync(preinstalledWpContentPath)
303-
? readAsFile(preinstalledWpContentPath)
304-
: await cachedDownload(
305-
wpDetails.releaseUrl,
306-
`${wpDetails.version}.zip`,
307-
monitor
308-
);
309-
310-
const constants: Record<string, string | number | boolean | null> =
311-
{
312-
WP_DEBUG: true,
313-
WP_DEBUG_LOG: true,
314-
WP_DEBUG_DISPLAY: false,
315-
};
316-
317-
logger.log(`Booting WordPress...`);
318-
requestHandler = await bootWordPress({
319-
siteUrl: absoluteUrl,
320-
createPhpRuntime: async () =>
321-
await loadNodeRuntime(compiledBlueprint.versions.php),
322-
wordPressZip,
323-
sqliteIntegrationPluginZip: fetchSqliteIntegration(monitor),
324-
sapiName: 'cli',
325-
createFiles: {
326-
'/internal/shared/ca-bundle.crt':
327-
rootCertificates.join('\n'),
328-
},
329-
constants,
330-
phpIniEntries: {
331-
'openssl.cafile': '/internal/shared/ca-bundle.crt',
332-
allow_url_fopen: '1',
333-
disable_functions: '',
334-
},
335-
hooks: {
336-
async beforeWordPressFiles(php) {
337-
if (args.mountBeforeInstall) {
338-
mountResources(php, args.mountBeforeInstall);
339-
}
340-
},
341-
},
342-
});
343-
logger.log(`Booted!`);
344-
345-
const php = await requestHandler.getPrimaryPhp();
346-
try {
347-
if (
348-
wpDetails &&
349-
!args.mountBeforeInstall &&
350-
!fs.existsSync(preinstalledWpContentPath)
351-
) {
352-
logger.log(
353-
`Caching preinstalled WordPress for the next boot...`
354-
);
355-
fs.writeFileSync(
356-
preinstalledWpContentPath,
357-
await zipDirectory(php, '/wordpress')
358-
);
359-
logger.log(`Cached!`);
360-
}
361-
362-
if (args.mount) {
363-
mountResources(php, args.mount);
364-
}
365-
366-
wordPressReady = true;
367-
368-
if (compiledBlueprint) {
369-
const { php, reap } =
370-
await requestHandler.processManager.acquirePHPInstance();
371-
try {
372-
logger.log(`Running the Blueprint...`);
373-
await runBlueprintSteps(compiledBlueprint, php);
374-
logger.log(`Finished running the blueprint`);
375-
} finally {
376-
reap();
377-
}
378-
}
379-
380-
if (command === 'build-snapshot') {
381-
await zipSite(args.outfile as string);
382-
logger.log(`WordPress exported to ${args.outfile}`);
383-
process.exit(0);
384-
} else if (command === 'run-blueprint') {
385-
logger.log(`Blueprint executed`);
386-
process.exit(0);
387-
} else {
388-
logger.log(`WordPress is running on ${absoluteUrl}`);
389-
}
390-
} catch (error) {
391-
if (!args.debug) {
392-
throw error;
393-
}
394-
const phpLogs = php.readFileAsText(errorLogPath);
395-
throw new Error(phpLogs, { cause: error });
396-
}
397-
},
398-
async handleRequest(request: PHPRequest) {
399-
if (!wordPressReady) {
400-
return PHPResponse.forHttpCode(
401-
502,
402-
'WordPress is not ready yet'
403-
);
404-
}
405-
return await requestHandler.request(request);
406-
},
407-
});
130+
return runCLI(args as RunCLIArgs);
408131
}
409132

410133
run();

packages/playground/cli/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './run-cli';

0 commit comments

Comments
 (0)