-
Notifications
You must be signed in to change notification settings - Fork 268
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds a new step to the blueprints that allows to run WP-CLI commands. Example: ```ts { "landingPage": "/wp-admin/post.php", "login": true, "steps": [ { "step": "wp-cli", "command": "wp post create --post_title='Test post' --post_excerpt='Some content' --no-color" } ] } ``` The command may also be an array of strings to ease dealing with quotes: ```ts { "landingPage": "/wp-admin/post.php", "login": true, "steps": [ { "step": "wp-cli", "command": ["wp", "post", "create", "--post_title=Test post", "--post_excerpt=Some content", "--no-color"] } ] } ``` ## Trade-offs * WP-CLI requires ~7MB of additional downloads: 1.4MB for the compressed WP-CLI.phar, and 5.6MB for the kitchen-sink PHP extensions bundle. ## Implementation * Whenever the Blueprints compiler finds a `wp-cli` step, it: * Downloads the wp-cli.phar file * Forces the `kitchen-sink` PHP extensions bundle as WP-CLI requires mbstring and iconv * `wp-cli.phar` is shipped from playground.wordpress.net. Downloading the official release from raw.githubusercontent.com transfers 7MB, while downloading the pre-compressed version from playground.wordpress.net only transfers 1.4MB. * playground.wordpress.net now always sets the `PHP_SAPI` constant to `cli` to ensure this check in WP-CLI passes: https://github.com/wp-cli/wp-cli-bundle/blob/970d0d4c22d4a89ca890def26ec82e559625abc6/php/boot-phar.php#L3-L10 ## Future work * Minify wp-cli.phar to reduce its size even further. We can remove redundant whitespaces and comments, remove the JavaScript parser library that is unlikely to be useful in Playground, remove parts of the Composer library, and perhaps more. cc @schlessera @swissspidy @danielbachhuber * Move the hardcoded WP-CLI-related parts of the Blueprint `compile()` function into a separate, modular function associated with the `wp-cli` step.
- Loading branch information
Showing
11 changed files
with
345 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
60 changes: 60 additions & 0 deletions
60
packages/playground/blueprints/src/lib/steps/wp-cli.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { NodePHP } from '@php-wasm/node'; | ||
import { splitShellCommand, wpCLI } from './wp-cli'; | ||
import { readFileSync } from 'fs'; | ||
import { join } from 'path'; | ||
import { unzip } from './unzip'; | ||
import { getWordPressModule } from '@wp-playground/wordpress'; | ||
|
||
const phpVersion = '8.0'; | ||
describe('Blueprint step wpCLI', () => { | ||
let php: NodePHP; | ||
|
||
beforeEach(async () => { | ||
php = await NodePHP.load(phpVersion, { | ||
requestHandler: { | ||
documentRoot: '/wordpress', | ||
}, | ||
}); | ||
php.setSapiName('cli'); | ||
await unzip(php, { | ||
zipFile: await getWordPressModule(), | ||
extractToPath: '/wordpress', | ||
}); | ||
const wpCliPath = join(__dirname, '../../test/wp-cli.phar'); | ||
php.writeFile('/tmp/wp-cli.phar', readFileSync(wpCliPath)); | ||
}); | ||
|
||
it('should run wp-cli commands', async () => { | ||
const result = await wpCLI(php, { | ||
command: | ||
"wp post create --post_title='Test post' --post_excerpt='Some content' --no-color", | ||
}); | ||
expect(result.text).toMatch(/Success: Created post/); | ||
}); | ||
}); | ||
|
||
describe('splitShellCommand', () => { | ||
it('Should split a shell command into an array', () => { | ||
const command = | ||
'wp post create --post_title="Test post" --post_excerpt="Some content"'; | ||
const result = splitShellCommand(command); | ||
expect(result).toEqual([ | ||
'wp', | ||
'post', | ||
'create', | ||
'--post_title=Test post', | ||
'--post_excerpt=Some content', | ||
]); | ||
}); | ||
|
||
it('Should treat multiple spaces as a single space', () => { | ||
const command = 'ls --wordpress --playground --is-great'; | ||
const result = splitShellCommand(command); | ||
expect(result).toEqual([ | ||
'ls', | ||
'--wordpress', | ||
'--playground', | ||
'--is-great', | ||
]); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import { PHPResponse } from '@php-wasm/universal'; | ||
import { StepHandler } from '.'; | ||
import { phpVar } from '@php-wasm/util'; | ||
|
||
/** | ||
* @inheritDoc wpCLI | ||
* @hasRunnableExample | ||
* @example | ||
* | ||
* <code> | ||
* { | ||
* "step": "wpCLI", | ||
* "command": "wp post create --post_title='Test post' --post_excerpt='Some content'" | ||
* } | ||
* </code> | ||
*/ | ||
export interface WPCLIStep { | ||
/** The step identifier. */ | ||
step: 'wp-cli'; | ||
/** The WP CLI command to run. */ | ||
command: string | string[]; | ||
/** wp-cli.phar path */ | ||
wpCliPath?: string; | ||
} | ||
|
||
/** | ||
* Runs PHP code. | ||
*/ | ||
export const wpCLI: StepHandler<WPCLIStep, Promise<PHPResponse>> = async ( | ||
playground, | ||
{ command, wpCliPath = '/tmp/wp-cli.phar' } | ||
) => { | ||
if (!(await playground.fileExists(wpCliPath))) { | ||
throw new Error(`wp-cli.phar not found at ${wpCliPath}`); | ||
} | ||
|
||
let args: string[]; | ||
if (typeof command === 'string') { | ||
command = command.trim(); | ||
args = splitShellCommand(command); | ||
} else { | ||
args = command; | ||
} | ||
|
||
const cmd = args.shift(); | ||
if (cmd !== 'wp') { | ||
throw new Error(`The first argument must be "wp".`); | ||
} | ||
|
||
await playground.writeFile('/tmp/stdout', ''); | ||
await playground.writeFile('/tmp/stderr', ''); | ||
await playground.writeFile( | ||
'/wordpress/run-cli.php', | ||
`<?php | ||
// Set up the environment to emulate a shell script | ||
// call. | ||
// Set SHELL_PIPE to 0 to ensure WP-CLI formats | ||
// the output as ASCII tables. | ||
// @see https://github.com/wp-cli/wp-cli/issues/1102 | ||
putenv( 'SHELL_PIPE=0' ); | ||
// Set the argv global. | ||
$GLOBALS['argv'] = array_merge([ | ||
"/tmp/wp-cli.phar", | ||
"--path=/wordpress" | ||
], ${phpVar(args)}); | ||
// Provide stdin, stdout, stderr streams outside of | ||
// the CLI SAPI. | ||
define('STDIN', fopen('php://stdin', 'rb')); | ||
define('STDOUT', fopen('php://stdout', 'wb')); | ||
define('STDERR', fopen('/tmp/stderr', 'wb')); | ||
require( ${phpVar(wpCliPath)} ); | ||
` | ||
); | ||
|
||
const result = await playground.run({ | ||
scriptPath: '/wordpress/run-cli.php', | ||
}); | ||
|
||
if (result.errors) { | ||
throw new Error(result.errors); | ||
} | ||
|
||
return result; | ||
}; | ||
|
||
/** | ||
* Naive shell command parser. | ||
* Ensures that commands like `wp option set blogname "My blog name"` are split into | ||
* `['wp', 'option', 'set', 'blogname', 'My blog name']` instead of | ||
* `['wp', 'option', 'set', 'blogname', 'My', 'blog', 'name']`. | ||
* | ||
* @param command | ||
* @returns | ||
*/ | ||
export function splitShellCommand(command: string) { | ||
const MODE_NORMAL = 0; | ||
const MODE_IN_QUOTE = 1; | ||
|
||
let mode = MODE_NORMAL; | ||
let quote = ''; | ||
|
||
const parts: string[] = []; | ||
let currentPart = ''; | ||
for (let i = 0; i < command.length; i++) { | ||
const char = command[i]; | ||
if (mode === MODE_NORMAL) { | ||
if (char === '"' || char === "'") { | ||
mode = MODE_IN_QUOTE; | ||
quote = char; | ||
} else if (char.match(/\s/)) { | ||
if (currentPart) { | ||
parts.push(currentPart); | ||
} | ||
currentPart = ''; | ||
} else { | ||
currentPart += char; | ||
} | ||
} else if (mode === MODE_IN_QUOTE) { | ||
if (char === '\\') { | ||
i++; | ||
currentPart += command[i]; | ||
} else if (char === quote) { | ||
mode = MODE_NORMAL; | ||
quote = ''; | ||
} else { | ||
currentPart += char; | ||
} | ||
} | ||
} | ||
if (currentPart) { | ||
parts.push(currentPart); | ||
} | ||
return parts; | ||
} |
Binary file not shown.
Oops, something went wrong.