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

feat: custom block generators #121

Merged
merged 2 commits into from
Oct 26, 2023
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
97 changes: 81 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ The following files will be created based on your input:

[Page Template documentation](https://developer.wordpress.org/themes/template-files-section/page-template-files/)

## Reusable Pattern
### Reusable Pattern

The generator for reusable patterns will prompt you for a name, description, and categories for the pattern, then create a script to register a reusable pattern with metadata based on your inputs and instructions for how to create the markup for the pattern.

Expand All @@ -364,6 +364,45 @@ The following file will be created based on your input:

[Reusable pattern (a.k.a. Block pattern) documentation](https://developer.wordpress.org/themes/advanced-topics/block-patterns/)

### Custom Blocks Plugin

The generator for custom blocks plugins will prompt you for a plugin name and description, WordPress and PHP versions (for compatibility info), and author. It uses this info to scaffold the configuration and readme files for a custom blocks plugin that initially does not have any blocks.

```sh
npm run generate:custom-blocks-plugin
```

The following files will be created based on your input:

- `src/plugins/<plugin-name>/src/.gitkeep`
- `src/plugins/<plugin-name>/<plugin-name>.php`
- `src/plugins/<plugin-name>/readme.txt`

It will also modify these files to automatically updated the build/development processes and configuration:

- `package.json`
- `docker-compose.yml`

### Custom Block

The generator for custom blocks will prompt you for the plugin that the block should belong to, a block name and description, and whether the block needs a `view.js` file for client-side JS. Note: this should only be run after a custom blocks plugin has been generated from `npm run generate:custom-blocks-plugin`.

```sh
npm run generate:custom-block
```

The following files will be created based on your input:

- `src/plugins/<plugin-name>/src/<block-name>/block.json`
- `src/plugins/<plugin-name>/src/<block-name>/edit.js`
- `src/plugins/<plugin-name>/src/<block-name>/editor.scss`
- `src/plugins/<plugin-name>/src/<block-name>/index.js`
- `src/plugins/<plugin-name>/src/<block-name>/save.js`
- `src/plugins/<plugin-name>/src/<block-name>/styles.scss`
- `src/plugins/<plugin-name>/src/<block-name>/view.js` (optional)

See [custom block structure](#custom-block-structure) for more info on what these files are for.

## Plugins

### Installing Plugins
Expand Down Expand Up @@ -402,26 +441,52 @@ This is a non-comprehensive list of plugins that we have found useful on other p

## Custom Blocks

We have a plugin for custom blocks called `example-blocks`, which lives in `src/plugins`. For the blocks to be available in WordPress, you must activate the "Example Blocks" plugin from the WordPress admin's plugins page.
We have two [generators](#generators) that can be used in tandem to create the necessary scaffolding for custom blocks. The first is `npm run generate:custom-blocks-plugin`, which should be run first to create the plugin config, readme, directory, and `package.json`/`docker-compose.yml` changes necessary to make the plugin available to WordPress. The second is `npm run generate:custom-block`, which creates the boilerplate files necessary to create a single custom block within the plugin.

Note: you will need to restart your development process to pick up the changes after adding a custom blocks plugin and/or a custom block.

Once you have created a custom blocks plugin that has at least one custom block, you should be able to activate it in the WordPress admin page for Plugins.

The custom blocks plugin generator should handle creating the npm scripts for you, but the general format is as follows:

```json
"plugins:dev": "run-p plugins:dev:* || echo \"Unable to build plugins\"",
"plugins:build": "run-s plugins:build:* || echo \"Unable to build plugins\"",
"plugins:dev:<plugin-name>": "wp-scripts start --webpack-src-dir=src/plugins/<plugin-name>/src --output-path=src/plugins/<plugin-name>/build",
"plugins:build:<plugin-name>": "wp-scripts build --webpack-src-dir=src/plugins/<plugin-name>/src --output-path=src/plugins/<plugin-name>/build"
```

The `plugins:dev` and `plugins:build` scripts will automatically pick up any `plugins:dev:*` and `plugins:build:*` scripts that get added, minimizing the maintenance overhead from adding more plugins.

Similarly, the `docker-compose.yml` volume mapping should automatically be updated by the generator, but if not, each plugin needs to be mapped to a folder within the container's `/var/www/html/wp-content/plugins/<plugin-name>` folder, like so:

```yml
services:
web:
volumes:
- ./src/plugins/<plugin-name>:/var/www/html/wp-content/plugins/<plugin-name>
```

### Custom Block Structure

Each custom block will have most, if not all, of the following files:

The plugins can be built with `npm run plugins:dev` or `npm run plugins:build`, but that generally shouldn't be necessary, since those scripts are run as part of the standard `npm start` and `npm run build:prod` scripts.
- `block.json`: configuration/metadata for the block
- `edit.js`: the component used while editing
- `editor.scss`: custom styles for the editor view
- `index.js`: entry point for the JS bundle
- `save.js`: the component rendered on the site
- `style.scss`: custom styles for the block when rendered on the site
- `view.js`: any JS that needs to run when the block is rendered on a non-admin page (optional)

### Creating a New Custom Block
It's important to note that while `save.js` is written like a React component, it does not have reactivity when rendered on the site. The React component is used to serialize HTML that is sent to the client from the server, so hooks like `useEffect` will not run when the component is rendered. If your component requires JS for its functionality, you need to provide that JS in the `view.js` file.

Follow these steps to create a new custom block and wire it up with the normal development/build processes:
Once the boilerplate files have been created, follow these steps to build out the custom block to fit your needs.

1. Create a new folder at `src/plugins/example-blocks/src/<block-name>`
1. Either copy files from another block or manually create these files:
- `block.json`: configuration/metadata for the block
- `index.js`: entry point for the JS bundle
- `edit.js`: the component used while editing
- `save.js`: the component rendered on the site
- `view.js`: any JS that needs to run when the block is rendered on a non-admin page (optional)
- `editor.scss`: custom styles for the editor view
- `style.scss`: custom styles for the block when rendered on the site
1. Configure the custom block by updating `block.json`, namely the `name`, `title`, `icon`, and `description` fields. If you don't need a `view.js` file, delete the `viewScript` key.
1. Implement the edit function, which will usually be form controls corresponding to attributes that you define in `index.js`
1. Configure the custom block by [updating `block.json`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/). Depending on how you answered prompts from the generator, this may be mostly done. You'll likely want to update the `icon` field with a [dashicon name](https://developer.wordpress.org/resource/dashicons)
1. Implement the edit function, which will control how the block is rendered/created in the Gutenberg editor
1. Implement the save function, which will consume the attributes defined in `index.js` and render the block's desired markup
1. Implement the front-end JS for the component in `view.js` if needed

### Useful Resources

Expand Down
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ services:
volumes:
- ./uploads:/var/www/html/wp-content/uploads
- ./theme:/var/www/html/wp-content/themes/sparkpress-theme
- ./src/plugins/example-blocks:/var/www/html/wp-content/plugins/example-blocks
- ./wp-configs/wp-config.php:/var/www/html/wp-config.php
- ./wp-configs/php.ini:/var/www/html/php.ini
- ./.env:/var/www/html/.env
Expand Down
259 changes: 259 additions & 0 deletions generators/custom-block.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
const { writeFileSync, mkdirSync, readdirSync } = require('fs');
const { join } = require('path');
const prompts = require('prompts');

const getBlockJsonTemplate = ({ pluginSlug, slugName, name, description, hasViewScript }) => `{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "${pluginSlug}/${slugName}",
"version": "0.1.0",
"title": "${name}",
"category": "${pluginSlug}",
"icon": "admin-generic",
"description": "${description}",
"supports": {
"html": false
},
"textdomain": "${pluginSlug}",
"editorScript": "file:index.js",
"editorStyle": "file:index.css",
"style": "file:style-index.css"${
hasViewScript
? `,
"viewScript": "file:view.js"`
: ''
}
}
`;

const getEditJSTemplate = ({ name }) => `/**
* React hook that is used to mark the block wrapper element.
* It provides all the necessary props like the class name.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops
*/
import { useBlockProps } from '@wordpress/block-editor';

/**
* Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
* Those files can contain any CSS code that gets applied to the editor.
*
* @see https://www.npmjs.com/package/@wordpress/scripts#using-css
*/
import './editor.scss';

/**
* The edit function describes the structure of your block in the context of the
* editor. This represents what the editor will render when the block is used.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#edit
*
* @return {WPElement} Element to render.
*/
export default function Edit() {
return <p {...useBlockProps()}>${name}</p>;
}
`;

const getEditorSCSSTemplate = ({ pluginSlug, slugName }) => `/**
* The following styles get applied inside the editor only.
*
* Replace them with your own styles or remove the file completely.
*/

.wp-block-${pluginSlug}-${slugName} {
/* insert custom styles here */
}
`;

const getIndexJSTemplate = () => `/**
* Registers a new block provided a unique name and an object defining its behavior.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
*/
import { registerBlockType } from '@wordpress/blocks';

/**
* Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
* All files containing \`style\` keyword are bundled together. The code used
* gets applied both to the front of your site and to the editor.
*
* @see https://www.npmjs.com/package/@wordpress/scripts#using-css
*/
import './style.scss';

/**
* Internal dependencies
*/
import Edit from './edit';
import save from './save';
import metadata from './block.json';

/**
* Every block starts by registering a new block type definition.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
*/
registerBlockType(metadata.name, {
/**
* @see ./edit.js
*/
edit: Edit,

/**
* @see ./save.js
*/
save,
});
`;

const getSaveJSTemplate = ({ name }) => `/**
* React hook that is used to mark the block wrapper element.
* It provides all the necessary props like the class name.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops
*/
import { useBlockProps } from '@wordpress/block-editor';

/**
* The save function defines the way in which the different attributes should
* be combined into the final markup, which is then serialized by the block
* editor into \`post_content\`.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#save
*
* @return {WPElement} Element to render.
*/
export default function save() {
return <p {...useBlockProps.save()}>{'${name}'}</p>;
}
`;

const getStyleSCSSTemplate = ({ pluginSlug, slugName }) => `/**
* The following styles get applied both on the front of your site
* and in the editor.
*
* Replace them with your own styles or remove the file completely.
*/

.wp-block-${pluginSlug}-${slugName} {
/* insert custom styles here */
}
`;

const getViewJSTemplate = ({ name }) => `/**
* Put any JS here that is needed for your block to function when rendered outside of the editor.
*/
console.log('Hello from the ${name} block!');
`;

const getCustomBlockPluginOptions = () => {
const directories = readdirSync(join(__dirname, '../src/plugins'));
return directories
.filter((dir) => dir !== '.gitkeep')
.map((dir) => ({
title: dir,
value: dir,
}));
};

const getDetails = async () => {
const pluginOptions = getCustomBlockPluginOptions();
if (!pluginOptions.length) {
console.log(
'There are no existing plugins in `src/plugins` to add custom blocks to. Please run `npm run generate:custom-blocks-plugin` to scaffold a plugin before running `npm run generate:custom-block`'
);
return;
}

const questions = [
{
type: 'select',
name: 'pluginSlug',
message: 'Which custom blocks plugin should this block belong to?',
choices: pluginOptions,
initial: 0,
},
{
type: 'text',
name: 'name',
message:
'What should the custom block be called? (This is the name editors will use to search for the block)',
},
{
type: 'text',
name: 'description',
message:
'Please describe the custom block. (This description will help editors understand how to use the block)',
},
{
type: 'select',
name: 'hasViewScript',
message:
'Will this block require JavaScript to function when rendered on the site? (If yes, a `view.js` file will be created)',
choices: [
{
title: 'Yes',
value: true,
},
{
title: 'No',
value: false,
},
],
initial: 0,
},
];

const response = await prompts(questions);

return response;
};

const generateCustomBlocksPlugin = async () => {
const { pluginSlug, name, description, hasViewScript } = await getDetails();
const slugName = name.toLowerCase().replace(/\W/g, '-');
const templateParams = { pluginSlug, slugName, name, description, hasViewScript };

const blockPath = join(__dirname, '../src/plugins', pluginSlug, 'src', slugName);
mkdirSync(blockPath);

const blockJsonTemplate = getBlockJsonTemplate(templateParams);
const blockJsonPath = join(blockPath, 'block.json');
writeFileSync(blockJsonPath, blockJsonTemplate, 'utf-8');
console.log(`Created ${blockJsonPath}`);

const editJSTemplate = getEditJSTemplate(templateParams);
const editJSPath = join(blockPath, 'edit.js');
writeFileSync(editJSPath, editJSTemplate, 'utf-8');
console.log(`Created ${editJSPath}`);

const editorSCSSTemplate = getEditorSCSSTemplate(templateParams);
const editorSCSSPath = join(blockPath, 'editor.scss');
writeFileSync(editorSCSSPath, editorSCSSTemplate, 'utf-8');
console.log(`Created ${editorSCSSPath}`);

const indexJSTemplate = getIndexJSTemplate(templateParams);
const indexJSPath = join(blockPath, 'index.js');
writeFileSync(indexJSPath, indexJSTemplate, 'utf-8');
console.log(`Created ${indexJSPath}`);

const saveJSTemplate = getSaveJSTemplate(templateParams);
const saveJSPath = join(blockPath, 'save.js');
writeFileSync(saveJSPath, saveJSTemplate, 'utf-8');
console.log(`Created ${saveJSPath}`);

const styleSCSSTemplate = getStyleSCSSTemplate(templateParams);
const styleSCSSPath = join(blockPath, 'style.scss');
writeFileSync(styleSCSSPath, styleSCSSTemplate, 'utf-8');
console.log(`Created ${styleSCSSPath}`);

if (hasViewScript) {
const viewJSTemplate = getViewJSTemplate(templateParams);
const viewJSPath = join(blockPath, 'view.js');
writeFileSync(viewJSPath, viewJSTemplate, 'utf-8');
console.log(`Created ${viewJSPath}`);
}
};

generateCustomBlocksPlugin();
Loading