Skip to content

Commit 6170984

Browse files
wp-now: Add executeWPCli() function to download and execute WP-CLI (#395)
## What? - Add the `executeWPCli` function to download and execute `wp-cli. - In the future, we may include the command `wp-now wp`. Currently, we drop it out until we improve the pthreads execution. Currently a PR in progress: #346 - Surface `emscriptenOptions` to catch print and print error for `wp-cli` execution. ## Why? - See https://github.com/WordPress/wordpress-playground/issues/269 ## How? It downloads the wp-cli.phar file if the file doesn't exist, then uses `php.cli()` to execute it. There are some limitations in the `wp-cli` features. Some of them may not work. ## Testing Instructions - Check out this branch. - Copy your path to your theme or plugin - After installing and building the project, run: - Run the tests `npx nx test wp-now` - Observe the tests pass. <!--details> <summary>~`WP_NOW_PROJECT_PATH=/path/to/your-theme-or-plugin npx nx preview wp-now wp user list`~ </summary> ``` > nx run wp-now:preview wp user list +----+------------+--------------+--------------+--------------+---------------+ | ID | user_login | display_name | user_email | user_registe | roles | | | | | | red | | +----+------------+--------------+--------------+--------------+---------------+ | 1 | admin | admin | admin@localh | 2023-05-19 1 | administrator | | | | | ost.com | 7:33:35 | | +----+------------+--------------+--------------+--------------+---------------+ > NX Successfully ran target preview for project wp-now and 12 tasks it depends on (10s) With additional flags: wp user list ``` </details!--> --------- Co-authored-by: Daniel Bachhuber <[email protected]>
1 parent 73122f4 commit 6170984

File tree

8 files changed

+166
-4
lines changed

8 files changed

+166
-4
lines changed

packages/wp-now/src/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,9 @@ export const DEFAULT_PHP_VERSION = '8.0';
2828
* The default WordPress version to use when running the WP Now server.
2929
*/
3030
export const DEFAULT_WORDPRESS_VERSION = 'latest';
31+
32+
/**
33+
* The URL for downloading the "wp-cli" WordPress cli.
34+
*/
35+
export const WP_CLI_URL =
36+
'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar';

packages/wp-now/src/download.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import followRedirects from 'follow-redirects';
44
import unzipper from 'unzipper';
55
import os from 'os';
66
import { IncomingMessage } from 'http';
7-
import { DEFAULT_WORDPRESS_VERSION, SQLITE_URL } from './constants';
7+
import { DEFAULT_WORDPRESS_VERSION, SQLITE_URL, WP_CLI_URL } from './constants';
88
import { isValidWordPressVersion } from './wp-playground-wordpress';
99
import { output } from './output';
1010
import getWpNowPath from './get-wp-now-path';
1111
import getWordpressVersionsPath from './get-wordpress-versions-path';
1212
import getSqlitePath from './get-sqlite-path';
13+
import getWpCliPath from './get-wp-cli-path';
1314

1415
function getWordPressVersionUrl(version = DEFAULT_WORDPRESS_VERSION) {
1516
if (!isValidWordPressVersion(version)) {
@@ -28,6 +29,55 @@ interface DownloadFileAndUnzipResult {
2829
followRedirects.maxRedirects = 5;
2930
const { https } = followRedirects;
3031

32+
async function downloadFile({
33+
url,
34+
destinationFilePath,
35+
itemName,
36+
}): Promise<DownloadFileAndUnzipResult> {
37+
let statusCode = 0;
38+
try {
39+
if (fs.existsSync(destinationFilePath)) {
40+
return { downloaded: false, statusCode: 0 };
41+
}
42+
fs.ensureDirSync(path.dirname(destinationFilePath));
43+
const response = await new Promise<IncomingMessage>((resolve) =>
44+
https.get(url, (response) => resolve(response))
45+
);
46+
statusCode = response.statusCode;
47+
if (response.statusCode !== 200) {
48+
throw new Error(
49+
`Failed to download file (Status code ${response.statusCode}).`
50+
);
51+
}
52+
await new Promise<void>((resolve, reject) => {
53+
fs.ensureFileSync(destinationFilePath);
54+
const file = fs.createWriteStream(destinationFilePath);
55+
response.pipe(file);
56+
file.on('finish', () => {
57+
file.close();
58+
resolve();
59+
});
60+
file.on('error', (error) => {
61+
file.close();
62+
reject(error);
63+
});
64+
});
65+
output?.log(`Downloaded ${itemName} to ${destinationFilePath}`);
66+
return { downloaded: true, statusCode };
67+
} catch (error) {
68+
output?.error(`Error downloading file ${itemName}`, error);
69+
return { downloaded: false, statusCode };
70+
}
71+
}
72+
73+
export async function downloadWPCLI() {
74+
return downloadFile({
75+
url: WP_CLI_URL,
76+
destinationFilePath: getWpCliPath(),
77+
itemName: 'wp-cli',
78+
});
79+
}
80+
3181
async function downloadFileAndUnzip({
3282
url,
3383
destinationFolder,
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import startWPNow from './wp-now';
2+
import { downloadWPCLI } from './download';
3+
import { disableOutput } from './output';
4+
import getWpCliPath from './get-wp-cli-path';
5+
import getWpNowConfig from './config';
6+
import { DEFAULT_PHP_VERSION, DEFAULT_WORDPRESS_VERSION } from './constants';
7+
8+
/**
9+
* This is an unstable API. Multiple wp-cli commands may not work due to a current limitation on php-wasm and pthreads.
10+
* @param args The arguments to pass to wp-cli.
11+
*/
12+
export async function executeWPCli(args: string[]) {
13+
await downloadWPCLI();
14+
disableOutput();
15+
const options = await getWpNowConfig({
16+
php: DEFAULT_PHP_VERSION,
17+
wp: DEFAULT_WORDPRESS_VERSION,
18+
path: process.env.WP_NOW_PROJECT_PATH || process.cwd(),
19+
});
20+
const { phpInstances, options: wpNowOptions } = await startWPNow({
21+
...options,
22+
numberOfPhpInstances: 2,
23+
});
24+
const [, php] = phpInstances;
25+
26+
try {
27+
php.useHostFilesystem();
28+
await php.cli([
29+
'php',
30+
getWpCliPath(),
31+
`--path=${wpNowOptions.documentRoot}`,
32+
...args,
33+
]);
34+
} catch (resultOrError) {
35+
const success =
36+
resultOrError.name === 'ExitStatus' && resultOrError.status === 0;
37+
if (!success) {
38+
throw resultOrError;
39+
}
40+
}
41+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import path from 'path';
2+
import getWpNowPath from './get-wp-now-path';
3+
import getWpCliTmpPath from './get-wp-cli-tmp-path';
4+
5+
/**
6+
* The path for wp-cli phar file within the WP Now folder.
7+
*/
8+
export default function getWpCliPath() {
9+
if (process.env.NODE_ENV !== 'test') {
10+
return path.join(getWpNowPath(), 'wp-cli.phar');
11+
}
12+
return getWpCliTmpPath();
13+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import path from 'path';
2+
import os from 'os';
3+
4+
/**
5+
* The full path to the hidden WP-CLI folder in the user's tmp directory.
6+
*/
7+
export default function getWpCliTmpPath() {
8+
const tmpDirectory = os.tmpdir();
9+
10+
return path.join(tmpDirectory, `wp-now-tests-wp-cli-hidden-folder`);
11+
}

packages/wp-now/src/tests/wp-now.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ import {
1111
} from '../wp-playground-wordpress';
1212
import {
1313
downloadSqliteIntegrationPlugin,
14+
downloadWPCLI,
1415
downloadWordPress,
1516
} from '../download';
1617
import os from 'os';
1718
import crypto from 'crypto';
1819
import getWpNowTmpPath from '../get-wp-now-tmp-path';
20+
import getWpCliTmpPath from '../get-wp-cli-tmp-path';
21+
import { executeWPCli } from '../execute-wp-cli';
1922

2023
const exampleDir = __dirname + '/mode-examples';
2124

@@ -510,3 +513,41 @@ describe('Test starting different modes', () => {
510513
expect(themeName.text).toContain('Twenty Twenty-Three');
511514
});
512515
});
516+
517+
/**
518+
* Test wp-cli command.
519+
*/
520+
describe('wp-cli command', () => {
521+
let consoleSpy;
522+
let output = '';
523+
524+
beforeEach(() => {
525+
function onStdout(outputLine: string) {
526+
output += outputLine;
527+
}
528+
consoleSpy = vi.spyOn(console, 'log');
529+
consoleSpy.mockImplementation(onStdout);
530+
});
531+
532+
afterEach(() => {
533+
output = '';
534+
consoleSpy.mockRestore();
535+
});
536+
537+
beforeAll(async () => {
538+
await downloadWithTimer('wp-cli', downloadWPCLI);
539+
});
540+
541+
afterAll(() => {
542+
fs.removeSync(getWpCliTmpPath());
543+
});
544+
545+
/**
546+
* Test wp-cli displays the version.
547+
* We don't need the WordPress context for this test.
548+
*/
549+
test('wp-cli displays the version', async () => {
550+
await executeWPCli(['cli', 'version']);
551+
expect(output).toMatch(/WP-CLI (\d\.?)+/i);
552+
});
553+
});

packages/wp-now/src/wp-now.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import fs from 'fs-extra';
2-
import { NodePHP } from '@php-wasm/node';
2+
import { NodePHP, PHPLoaderOptions } from '@php-wasm/node';
33
import path from 'path';
44
import { SQLITE_FILENAME } from './constants';
55
import {
@@ -42,7 +42,7 @@ export default async function startWPNow(
4242
options: Partial<WPNowOptions> = {}
4343
): Promise<{ php: NodePHP; phpInstances: NodePHP[]; options: WPNowOptions }> {
4444
const { documentRoot } = options;
45-
const nodePHPOptions = {
45+
const nodePHPOptions: PHPLoaderOptions = {
4646
requestHandler: {
4747
documentRoot,
4848
absoluteUrl: options.absoluteUrl,

packages/wp-now/tsconfig.spec.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"extends": "./tsconfig.json",
33
"compilerOptions": {
44
"outDir": "../../dist/out-tsc",
5-
"types": ["jest", "node"]
5+
"types": ["vitest/globals", "vitest/importMeta", "vite/client", "node"]
66
},
77
"include": [
88
"jest.config.ts",

0 commit comments

Comments
 (0)