* Creates the essential files in Jetpack to build a Gutenberg block.
* @param array $args Positional parameters. Only one is used, that corresponds to the block title.
* @param array $assoc_args Associative parameters defined in the scaffold() method.
public function block( $args, $assoc_args ) {
if ( ! isset( $args[1] ) ) {
WP_CLI::error( esc_html__( 'The title parameter is required.', 'jetpack' ) . ' 👻' );
$title = ucwords( $args[1] );
$slug = isset( $assoc_args['slug'] )
: sanitize_title( $title );
$next_version = "\x24\x24next-version$$"; // Escapes to hide the string from tools/replace-next-version-tag.sh
$variation_options = array( 'production', 'experimental', 'beta' );
$variation = ( isset( $assoc_args['variation'] ) && in_array( $assoc_args['variation'], $variation_options, true ) )
? $assoc_args['variation']
if ( preg_match( '#^jetpack/#', $slug ) ) {
$slug = preg_replace( '#^jetpack/#', '', $slug );
if ( ! preg_match( '/^[a-z][a-z0-9\-]*$/', $slug ) ) {
WP_CLI::error( esc_html__( 'Invalid block slug. They can contain only lowercase alphanumeric characters or dashes, and start with a letter', 'jetpack' ) . ' 👻' );
if ( ! WP_Filesystem() ) {
WP_CLI::error( esc_html__( "Can't write files", 'jetpack' ) . ' 😱' );
$path = JETPACK__PLUGIN_DIR . "extensions/blocks/$slug";
if ( $wp_filesystem->exists( $path ) && $wp_filesystem->is_dir( $path ) ) {
/* translators: %s is path to the conflicting block */
WP_CLI::error( sprintf( esc_html__( 'Name conflicts with the existing block %s', 'jetpack' ), $path ) . ' ⛔️' );
$wp_filesystem->mkdir( $path );
$keywords = isset( $assoc_args['keywords'] )
array_slice( explode( ',', $assoc_args['keywords'] ), 0, 3 )
"$path/block.json" => self::render_block_file(
'title' => wp_json_encode( $title, JSON_UNESCAPED_UNICODE ),
'description' => isset( $assoc_args['description'] )
? wp_json_encode( $assoc_args['description'], JSON_UNESCAPED_UNICODE )
: wp_json_encode( $title, JSON_UNESCAPED_UNICODE ),
'nextVersion' => $next_version,
'keywords' => wp_json_encode( $keywords, JSON_UNESCAPED_UNICODE ),
"$path/$slug.php" => self::render_block_file(
'nextVersion' => $next_version,
'underscoredTitle' => str_replace( ' ', '_', $title ),
"$path/editor.js" => self::render_block_file( 'block-editor-js' ),
"$path/editor.scss" => self::render_block_file(
"$path/edit.js" => self::render_block_file(
'className' => str_replace( ' ', '', ucwords( str_replace( '-', ' ', $slug ) ) ),
$files_written = array();
foreach ( $files as $filename => $contents ) {
if ( $wp_filesystem->put_contents( $filename, $contents ) ) {
$files_written[] = $filename;
/* translators: %s is a file name */
WP_CLI::error( sprintf( esc_html__( 'Error creating %s', 'jetpack' ), $filename ) );
if ( empty( $files_written ) ) {
WP_CLI::log( esc_html__( 'No files were created', 'jetpack' ) );
// Load index.json and insert the slug of the new block in its block variation array.
$block_list_path = JETPACK__PLUGIN_DIR . 'extensions/index.json';
$block_list = $wp_filesystem->get_contents( $block_list_path );
if ( empty( $block_list ) ) {
/* translators: %s is the path to the file with the block list */
WP_CLI::error( sprintf( esc_html__( 'Error fetching contents of %s', 'jetpack' ), $block_list_path ) );
} elseif ( false === stripos( $block_list, $slug ) ) {
$new_block_list = json_decode( $block_list );
$new_block_list->{ $variation }[] = $slug;
// Format the JSON to match our coding standards.
$new_block_list_formatted = wp_json_encode( $new_block_list, JSON_PRETTY_PRINT ) . "\n";
$new_block_list_formatted = preg_replace_callback(
// Find all occurrences of multiples of 4 spaces a the start of the line.
// Replace each occurrence of 4 spaces with a tab character.
return str_repeat( "\t", substr_count( $matches[0], ' ' ) );
$new_block_list_formatted
if ( ! $wp_filesystem->put_contents( $block_list_path, $new_block_list_formatted ) ) {
/* translators: %s is the path to the file with the block list */
WP_CLI::error( sprintf( esc_html__( 'Error writing new %s', 'jetpack' ), $block_list_path ) );
if ( 'beta' === $variation || 'experimental' === $variation ) {
$block_constant = sprintf(
/* translators: the placeholder is a constant name */
esc_html__( 'To load the block, add the constant JETPACK_BLOCKS_VARIATION set to %1$s to your wp-config.php file', 'jetpack' ),
/* translators: the placeholders are a human readable title, and a series of words separated by dashes */
esc_html__( 'Successfully created block %1$s with slug %2$s', 'jetpack' ) . ' 🎉' . "\n" .
"--------------------------------------------------------------------------------------------------------------------\n" .
/* translators: the placeholder is a directory path */
esc_html__( 'The files were created at %3$s', 'jetpack' ) . "\n" .
esc_html__( 'To start using the block, build the blocks with pnpm run build-extensions', 'jetpack' ) . "\n" .
/* translators: the placeholder is a file path */
esc_html__( 'The block slug has been added to the %4$s list at %5$s', 'jetpack' ) . "\n" .
/* translators: the placeholder is a URL */
"\n" . esc_html__( 'Read more at %7$s', 'jetpack' ) . "\n",
'https://github.com/Automattic/jetpack/blob/trunk/projects/plugins/jetpack/extensions/README.md#developing-block-editor-extensions-in-jetpack'
) . '--------------------------------------------------------------------------------------------------------------------'
* Built the file replacing the placeholders in the template with the data supplied.
* @param string $template Template.
* @param array $data Data.
private static function render_block_file( $template, $data = array() ) {
return \WP_CLI\Utils\mustache_render( JETPACK__PLUGIN_DIR . "wp-cli-templates/$template.mustache", $data );
// phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move these functions to some other file.
* Standard "ask for permission to continue" function.
* If action cancelled, ask if they need help.
* Written outside of the class so it's not listed as an executable command w/ 'wp jetpack'
* @param bool $flagged false = normal option | true = flagged by get_jetpack_options_for_reset().
* @param string $error_msg Error message.
function jetpack_cli_are_you_sure( $flagged = false, $error_msg = false ) {
$cli = new Jetpack_CLI();
// Default cancellation message.
__( 'Action cancelled. Have a question?', 'jetpack' )
$prompt_message = _x( 'Are you sure? This cannot be undone. Type "yes" to continue:', '"yes" is a command - do not translate.', 'jetpack' );
$prompt_message = _x( 'Are you sure? Modifying this option may disrupt your Jetpack connection. Type "yes" to continue.', '"yes" is a command - do not translate.', 'jetpack' );
WP_CLI::line( $prompt_message );
$handle = fopen( 'php://stdin', 'r' );
$line = fgets( $handle );
if ( 'yes' !== trim( $line ) ) {
WP_CLI::error( $error_msg );