Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blueprints: wp-cli step #1017

Merged
merged 3 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions packages/playground/blueprints/public/blueprint-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,48 @@
}
},
"required": ["data", "path", "step"]
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"progress": {
"type": "object",
"properties": {
"weight": {
"type": "number"
},
"caption": {
"type": "string"
}
},
"additionalProperties": false
},
"step": {
"type": "string",
"const": "wp-cli",
"description": "The step identifier."
},
"command": {
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"description": "The WP CLI command to run."
},
"wpCliPath": {
"type": "string",
"description": "wp-cli.phar path"
}
},
"required": ["command", "step"]
}
]
},
Expand Down
57 changes: 53 additions & 4 deletions packages/playground/blueprints/src/lib/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,16 @@ import {
import type { SupportedPHPExtensionBundle } from '@php-wasm/universal';
import { FileReference, isFileReference, Resource } from './resources';
import { Step, StepDefinition } from './steps';
import * as stepHandlers from './steps/handlers';
import * as allStepHandlers from './steps/handlers';
import { Blueprint } from './blueprint';

// @TODO: Configure this in the `wp-cli` step, not here.
const { wpCLI, ...otherStepHandlers } = allStepHandlers;
const keyedStepHandlers = {
...otherStepHandlers,
'wp-cli': wpCLI,
};

import Ajv from 'ajv';
/**
* The JSON schema stored in this directory is used to validate the Blueprints
Expand Down Expand Up @@ -126,6 +133,48 @@ export function compileBlueprint(
: blueprint.login),
});
}

/**
* Download WP-CLI. {{{
* Hardcoding this in the compilt() function is a temporary solution
* to provide the wpCLI step with the wp-cli.phar file it needs. Eventually,
* each Blueprint step may be able to specify any pre-requisite resources.
* Also, wp-cli should only be downloaded if it's not already present.
*/
const wpCliStepIndex = blueprint.steps?.findIndex(
(step) => typeof step === 'object' && step?.step === 'wp-cli'
);
if (wpCliStepIndex !== undefined && wpCliStepIndex > -1) {
if (!blueprint.phpExtensionBundles) {
blueprint.phpExtensionBundles = [];
}
if (!blueprint.phpExtensionBundles.includes('kitchen-sink')) {
blueprint.phpExtensionBundles.push('kitchen-sink');
console.warn(
`The WP-CLI step used in your Blueprint requires the iconv and mbstring PHP extensions. ` +
`However, you did not specify the kitchen-sink extension bundle. Playground will override your ` +
`choice and load the kitchen-sink PHP extensions bundle to prevent the WP-CLI step from failing. `
);
}
blueprint.steps?.splice(wpCliStepIndex, 0, {
Copy link
Collaborator Author

@adamziel adamziel Feb 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's only trigger this request if wp-cli.phar doesn't yet exist in VFS, or else running this Blueprint step in wp-now will always trigger fetch().

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The step works on the web. Let's figure out node.js in a follow-up PR.

step: 'writeFile',
data: {
resource: 'url',
/**
* Use compression for downloading the wp-cli.phar file.
* The official release, hosted at raw.githubusercontent.com, is ~7MB and the
* transfer is uncompressed. playground.wordpress.net supports transfer compression
* and only transmits ~1.4MB.
*
* @TODO: minify the wp-cli.phar file. It can be as small as 1MB when all the
* whitespaces and are removed, and even 500KB when libraries like the
* JavaScript parser or Composer are removed.
*/
url: 'https://playground.wordpress.net/wp-cli.phar',
},
path: '/tmp/wp-cli.phar',
});
}
// }}}

const { valid, errors } = validateBlueprint(blueprint);
Expand Down Expand Up @@ -187,11 +236,11 @@ export function compileBlueprint(
const result = await run(playground);
onStepCompleted(result, step);
} catch (e) {
console.error(e);
throw new Error(
`Error when executing the blueprint step #${i} (${JSON.stringify(
step
)}). ` +
`Inspect the cause of this error for more details`,
)}). Inspect the "cause" property of this error for more details`,
{
cause: e,
}
Expand Down Expand Up @@ -358,7 +407,7 @@ function compileStep<S extends StepDefinition>(
const run = async (playground: UniversalPHP) => {
try {
stepProgress.fillSlowly();
return await stepHandlers[step.step](
return await keyedStepHandlers[step.step](
playground,
await resolveArguments(args),
{
Expand Down
1 change: 1 addition & 0 deletions packages/playground/blueprints/src/lib/steps/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ export { runWpInstallationWizard } from './run-wp-installation-wizard';
export { setSiteOptions, updateUserMeta } from './site-data';
export { defineWpConfigConsts } from './define-wp-config-consts';
export { zipWpContent } from './zip-wp-content';
export { wpCLI } from './wp-cli';
5 changes: 4 additions & 1 deletion packages/playground/blueprints/src/lib/steps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { UnzipStep } from './unzip';
import { ImportWordPressFilesStep } from './import-wordpress-files';
import { ImportFileStep } from './import-file';
import { EnableMultisiteStep } from './enable-multisite';
import { WPCLIStep } from './wp-cli';

export type Step = GenericStep<FileReference>;
export type StepDefinition = Step & {
Expand Down Expand Up @@ -68,7 +69,8 @@ export type GenericStep<Resource> =
| SetSiteOptionsStep
| UnzipStep<Resource>
| UpdateUserMetaStep
| WriteFileStep<Resource>;
| WriteFileStep<Resource>
| WPCLIStep;

export type {
ActivatePluginStep,
Expand Down Expand Up @@ -99,6 +101,7 @@ export type {
UnzipStep,
UpdateUserMetaStep,
WriteFileStep,
WPCLIStep,
};

/**
Expand Down
60 changes: 60 additions & 0 deletions packages/playground/blueprints/src/lib/steps/wp-cli.spec.ts
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',
]);
});
});
138 changes: 138 additions & 0 deletions packages/playground/blueprints/src/lib/steps/wp-cli.ts
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.
Loading
Loading