Skip to content

Commit

Permalink
wp-now: Add executeWPCli() function to download and execute WP-CLI (#…
Browse files Browse the repository at this point in the history
…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]>
  • Loading branch information
sejas and danielbachhuber authored May 30, 2023
1 parent 73122f4 commit 6170984
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 4 deletions.
6 changes: 6 additions & 0 deletions packages/wp-now/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,9 @@ export const DEFAULT_PHP_VERSION = '8.0';
* The default WordPress version to use when running the WP Now server.
*/
export const DEFAULT_WORDPRESS_VERSION = 'latest';

/**
* The URL for downloading the "wp-cli" WordPress cli.
*/
export const WP_CLI_URL =
'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar';
52 changes: 51 additions & 1 deletion packages/wp-now/src/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import followRedirects from 'follow-redirects';
import unzipper from 'unzipper';
import os from 'os';
import { IncomingMessage } from 'http';
import { DEFAULT_WORDPRESS_VERSION, SQLITE_URL } from './constants';
import { DEFAULT_WORDPRESS_VERSION, SQLITE_URL, WP_CLI_URL } from './constants';
import { isValidWordPressVersion } from './wp-playground-wordpress';
import { output } from './output';
import getWpNowPath from './get-wp-now-path';
import getWordpressVersionsPath from './get-wordpress-versions-path';
import getSqlitePath from './get-sqlite-path';
import getWpCliPath from './get-wp-cli-path';

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

async function downloadFile({
url,
destinationFilePath,
itemName,
}): Promise<DownloadFileAndUnzipResult> {
let statusCode = 0;
try {
if (fs.existsSync(destinationFilePath)) {
return { downloaded: false, statusCode: 0 };
}
fs.ensureDirSync(path.dirname(destinationFilePath));
const response = await new Promise<IncomingMessage>((resolve) =>
https.get(url, (response) => resolve(response))
);
statusCode = response.statusCode;
if (response.statusCode !== 200) {
throw new Error(
`Failed to download file (Status code ${response.statusCode}).`
);
}
await new Promise<void>((resolve, reject) => {
fs.ensureFileSync(destinationFilePath);
const file = fs.createWriteStream(destinationFilePath);
response.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
file.on('error', (error) => {
file.close();
reject(error);
});
});
output?.log(`Downloaded ${itemName} to ${destinationFilePath}`);
return { downloaded: true, statusCode };
} catch (error) {
output?.error(`Error downloading file ${itemName}`, error);
return { downloaded: false, statusCode };
}
}

export async function downloadWPCLI() {
return downloadFile({
url: WP_CLI_URL,
destinationFilePath: getWpCliPath(),
itemName: 'wp-cli',
});
}

async function downloadFileAndUnzip({
url,
destinationFolder,
Expand Down
41 changes: 41 additions & 0 deletions packages/wp-now/src/execute-wp-cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import startWPNow from './wp-now';
import { downloadWPCLI } from './download';
import { disableOutput } from './output';
import getWpCliPath from './get-wp-cli-path';
import getWpNowConfig from './config';
import { DEFAULT_PHP_VERSION, DEFAULT_WORDPRESS_VERSION } from './constants';

/**
* This is an unstable API. Multiple wp-cli commands may not work due to a current limitation on php-wasm and pthreads.
* @param args The arguments to pass to wp-cli.
*/
export async function executeWPCli(args: string[]) {
await downloadWPCLI();
disableOutput();
const options = await getWpNowConfig({
php: DEFAULT_PHP_VERSION,
wp: DEFAULT_WORDPRESS_VERSION,
path: process.env.WP_NOW_PROJECT_PATH || process.cwd(),
});
const { phpInstances, options: wpNowOptions } = await startWPNow({
...options,
numberOfPhpInstances: 2,
});
const [, php] = phpInstances;

try {
php.useHostFilesystem();
await php.cli([
'php',
getWpCliPath(),
`--path=${wpNowOptions.documentRoot}`,
...args,
]);
} catch (resultOrError) {
const success =
resultOrError.name === 'ExitStatus' && resultOrError.status === 0;
if (!success) {
throw resultOrError;
}
}
}
13 changes: 13 additions & 0 deletions packages/wp-now/src/get-wp-cli-path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import path from 'path';
import getWpNowPath from './get-wp-now-path';
import getWpCliTmpPath from './get-wp-cli-tmp-path';

/**
* The path for wp-cli phar file within the WP Now folder.
*/
export default function getWpCliPath() {
if (process.env.NODE_ENV !== 'test') {
return path.join(getWpNowPath(), 'wp-cli.phar');
}
return getWpCliTmpPath();
}
11 changes: 11 additions & 0 deletions packages/wp-now/src/get-wp-cli-tmp-path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import path from 'path';
import os from 'os';

/**
* The full path to the hidden WP-CLI folder in the user's tmp directory.
*/
export default function getWpCliTmpPath() {
const tmpDirectory = os.tmpdir();

return path.join(tmpDirectory, `wp-now-tests-wp-cli-hidden-folder`);
}
41 changes: 41 additions & 0 deletions packages/wp-now/src/tests/wp-now.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ import {
} from '../wp-playground-wordpress';
import {
downloadSqliteIntegrationPlugin,
downloadWPCLI,
downloadWordPress,
} from '../download';
import os from 'os';
import crypto from 'crypto';
import getWpNowTmpPath from '../get-wp-now-tmp-path';
import getWpCliTmpPath from '../get-wp-cli-tmp-path';
import { executeWPCli } from '../execute-wp-cli';

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

Expand Down Expand Up @@ -510,3 +513,41 @@ describe('Test starting different modes', () => {
expect(themeName.text).toContain('Twenty Twenty-Three');
});
});

/**
* Test wp-cli command.
*/
describe('wp-cli command', () => {
let consoleSpy;
let output = '';

beforeEach(() => {
function onStdout(outputLine: string) {
output += outputLine;
}
consoleSpy = vi.spyOn(console, 'log');
consoleSpy.mockImplementation(onStdout);
});

afterEach(() => {
output = '';
consoleSpy.mockRestore();
});

beforeAll(async () => {
await downloadWithTimer('wp-cli', downloadWPCLI);
});

afterAll(() => {
fs.removeSync(getWpCliTmpPath());
});

/**
* Test wp-cli displays the version.
* We don't need the WordPress context for this test.
*/
test('wp-cli displays the version', async () => {
await executeWPCli(['cli', 'version']);
expect(output).toMatch(/WP-CLI (\d\.?)+/i);
});
});
4 changes: 2 additions & 2 deletions packages/wp-now/src/wp-now.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import fs from 'fs-extra';
import { NodePHP } from '@php-wasm/node';
import { NodePHP, PHPLoaderOptions } from '@php-wasm/node';
import path from 'path';
import { SQLITE_FILENAME } from './constants';
import {
Expand Down Expand Up @@ -42,7 +42,7 @@ export default async function startWPNow(
options: Partial<WPNowOptions> = {}
): Promise<{ php: NodePHP; phpInstances: NodePHP[]; options: WPNowOptions }> {
const { documentRoot } = options;
const nodePHPOptions = {
const nodePHPOptions: PHPLoaderOptions = {
requestHandler: {
documentRoot,
absoluteUrl: options.absoluteUrl,
Expand Down
2 changes: 1 addition & 1 deletion packages/wp-now/tsconfig.spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": ["jest", "node"]
"types": ["vitest/globals", "vitest/importMeta", "vite/client", "node"]
},
"include": [
"jest.config.ts",
Expand Down

0 comments on commit 6170984

Please sign in to comment.