├── .gitignore ├── log.js ├── package.json ├── LICENSE ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /log.js: -------------------------------------------------------------------------------- 1 | const log = (input) => { 2 | console.log(input); 3 | }; 4 | 5 | export { 6 | log 7 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-wp-block", 3 | "version": "2.6.0", 4 | "description": "Extends the @wordpress/create-block package by adding new features for flexible block scaffolding such as support for multiple blocks.", 5 | "main": "./index.js", 6 | "bin": { 7 | "create-wp-block": "./index.js" 8 | }, 9 | "type": "module", 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/dgwyer/create-wp-block.git" 16 | }, 17 | "keywords": [ 18 | "wordpress", 19 | "gutenberg", 20 | "block", 21 | "scaffold" 22 | ], 23 | "author": "", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/dgwyer/create-wp-block/issues" 27 | }, 28 | "homepage": "https://github.com/dgwyer/create-wp-block#readme", 29 | "dependencies": { 30 | "@wordpress/create-block": "^3.8.0", 31 | "execa": "^6.1.0", 32 | "replace-in-file": "^6.3.5", 33 | "yargs": "^17.5.1" 34 | }, 35 | "publishConfig": { 36 | "access": "public" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 David Gwyer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Create Single or Multiple WordPress Blocks 2 | 3 | Extends the WordPress core `@wordpress/create-block` package that creates a new block plugin. It's basically a thin wrapper around the core CLI which allows us to expose some new features right now until they're officially available, such as: 4 | 5 | - Single named blocks. 6 | - Multiple named blocks. 7 | - Full Tailwind CSS integration. 8 | 9 | The `create-wp-blocks` script is meant to be a useful tool to create blocks with additional functionality currently available in `@wordpress/create-block`. Hopefully, over time, most (or all) of these extra features will be available in the core package and this one can be deprecated. 10 | ## New functionality 11 | Specify a block name via the `--block` (or `-b` alias). By default in `@wordpress/create-block` there's no way to name a block, it's always set to the name of the plugin slug. e.g. `npx create-wp-block test -b block1`. 12 | 13 | Note, if the block name is specified but a plugin name (slug) is, then this will trigger interactive mode which is the default behaviour of `@wordpress/create-block` when no slug is specified. 14 | 15 | Optional Tailwind CSS integration is now available via the `--tw` flag. 16 | 17 | # Usage 18 | 19 | To create a basic plugin containing a single block: 20 | 21 | `npx create-wp-block todo-list` 22 | 23 | **Note: This produces exactly the same plugin as `npx @wordpress/create-block todo-list`** 24 | 25 | Things become interesting when we use the new features: 26 | 27 | `npx create-wp-block todo-list -b block1` 28 | 29 | This will create a plugin with the slug `todo-list`, which contains a single block with the slug `block1`. 30 | 31 | Create multiple blocks with: 32 | 33 | `npx create-wp-block todo-list -b block1 block2 block3` 34 | 35 | This will create a plugin with the slug `todo-list`, which contains three blocks with slugs: `block1`, `block2`, `block3`. Each block is located inside its own sub-folder. e.g. `/src/block1/`. 36 | 37 | Enable full Tailwind integration with the `--tw` option: 38 | 39 | `npx create-wp-block todo-list -b block1 block2 block3 --tw` 40 | 41 | Each block compiles its own Tailwind styles, which is inline with how blocks are compiled with `@wordpress/create-block`. Blocks continue to maintain their own styles independently. 42 | 43 | For quick testing you can disable wp-scripts with the `--ns` option: 44 | 45 | `npx create-wp-block todo-list -b block1 block2 block3 --ns` 46 | 47 | This doesn't install npm modules and creates the block plugin much quicker. However, you'll need to manually run `npm install` to do an initial build of the plugin block JavaScript code. 48 | 49 | # Trouble Shooting 50 | 51 | If no named blocks are specified then the plugin slug will be used as a fallback. 52 | 53 | There will probably be regular updates to this CLI as it's refined and new features are added. Sometimes `npx` will cache the version of the script which can be annoying. To make sure you're always running the latest release version you can add `@latest`. e.g. `npx create-wp-block@latest myplugin -b one`. 54 | 55 | For now (at least) there's no interactive mode if a plugin slug is not specified. This is required or the script will exit with a warning message. You're required to enter a plugin slug. 56 | # Request a Feature? 57 | 58 | Are you looking for a feature to be included in this package? Simply open a [new issue](https://github.com/dgwyer/create-wp-block/issues) and let's talk! All suggestions welcome. 59 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import yargs from "yargs"; 3 | import { execa, execaCommand, execaCommandSync } from 'execa'; 4 | import { join } from 'path'; 5 | import replace from 'replace-in-file'; 6 | import { log } from './log.js'; 7 | import { readFile } from 'fs/promises'; 8 | 9 | const argv = yargs(process.argv.slice(2)) 10 | .alias('name', 'n') 11 | .alias('namespace', 'nsp') 12 | //.alias('no-wp-scripts', 's') 13 | .alias('block', 'b') 14 | .alias('dir', 'd') 15 | .array('name') 16 | .array('block') 17 | .boolean('ns') 18 | //.default('ns', false) 19 | //.number('dropbox_base_index') 20 | .argv; 21 | 22 | let pluginSlug; 23 | 24 | if (argv._) { 25 | if (argv._.length > 0) { 26 | pluginSlug = argv._[0]; 27 | } 28 | if (argv._.length === 0) { 29 | console.log('Exiting! No plugin slug found. Please retry and enter a plugin slug.'); 30 | process.exit(); 31 | } 32 | } 33 | 34 | const json = JSON.parse( 35 | await readFile( 36 | new URL('./package.json', import.meta.url) 37 | ) 38 | ); 39 | log('\nVersion: ' + json.version); 40 | log('\nBy David Gwyer'); 41 | log('\nLet\'s create some blocks!'); 42 | 43 | log('\n---'); 44 | 45 | console.log(`\nCreating a new WordPress plugin with slug: ${pluginSlug}`); 46 | 47 | if (argv.b && typeof (argv.b) === 'object') { 48 | console.log("\nCreating the following named blocks:", argv.b.join(', ')); 49 | } 50 | //console.log("\nPassed in args:\n", argv); 51 | 52 | const cb = (error, stdout, stderr) => { 53 | if (error) { 54 | console.error(`exec error: ${error}`); 55 | return; 56 | } 57 | console.log(stdout); 58 | console.error(stderr); 59 | }; 60 | 61 | const opt = []; 62 | if (argv.ns) { 63 | opt.push('--no-wp-scripts'); 64 | } 65 | if (argv.tw) { 66 | opt.push('-t tw-block'); 67 | } 68 | if (argv.nsp) { 69 | opt.push(`--namespace ${argv.nsp}`); 70 | } 71 | 72 | //opt.push(`--title ${pluginSlug}`); 73 | 74 | log('\nBlock options: ' + opt.join(' ')); 75 | 76 | // log(execaCommandSync(`npx @wordpress/create-block ${pluginSlug}`, { stdin: 'inherit' }).stdout); 77 | log(`\nRunning package: npx @wordpress/create-block ${pluginSlug} ${opt.join(' ')}`); 78 | log('\n---'); 79 | 80 | const subprocess = execaCommand(`npx @wordpress/create-block ${pluginSlug} ${opt.join(' ')}`); 81 | subprocess.stdout.pipe(process.stdout); 82 | const { stdout } = await subprocess; 83 | console.log('\n', stdout); 84 | 85 | // log(execaCommandSync(`npx @wordpress/create-block ${pluginSlug} ${opt.join(' ')}`, { shell: true, stdin: 'inherit' }).stdout); 86 | 87 | // await execa( 88 | // "npx", 89 | // // ["@wordpress/create-block", pluginSlug], 90 | // ["@wordpress/create-block", pluginSlug, "--no-wp-scripts"], 91 | // { 92 | // stdin: 'inherit' 93 | // } 94 | // ).stdout.pipe(process.stdout); 95 | //console.log(createBlockScript.stdout); 96 | 97 | console.log("\nPost processing..."); 98 | 99 | if (argv.b && typeof (argv.b) === 'object') { 100 | 101 | if (argv.b.length === 1) { 102 | console.log("Single block name:", argv.b[0]); 103 | renameBlockFiles(argv.b[0], `${pluginSlug}/src`, pluginSlug); 104 | } 105 | 106 | if (argv.b.length > 1) { 107 | console.log("\nInstalling blocks:", ...argv.b); 108 | 109 | argv.b.forEach((item, index) => { 110 | 111 | // Handle first block slightly differently (move into folder and rename). 112 | if (index === 0) { 113 | // Move block files into a new folder using the block name for the folder. 114 | execaCommandSync(`mkdir ${pluginSlug}/src/${argv.b[index]}`); 115 | // log(execaCommandSync(`mkdir ${pluginSlug}/src/${argv.b[index]} -v`).stdout); 116 | execaCommandSync(`mv *.* ${argv.b[index]}`, { cwd: `${pluginSlug}/src` }); 117 | 118 | // Rename block files. 119 | renameBlockFiles(argv.b[index], `${pluginSlug}/src/${argv.b[index]}`, pluginSlug); 120 | 121 | // Update PHP block registration code to include the block path. 122 | renameFirstPhpBlock(argv.b[index], pluginSlug); 123 | } else { 124 | // For all other blocks just copy first block folder and rename. 125 | 126 | // Copy the first block folder to a new folder using the current block name for the folder. 127 | execaCommandSync(`cp -R ${pluginSlug}/src/${argv.b[0]} ${pluginSlug}/src/${argv.b[index]}`); 128 | 129 | // Rename block files. 130 | renameBlockFiles(argv.b[index], `${pluginSlug}/src/${argv.b[index]}`, argv.b[0]); 131 | 132 | // Update PHP block registration code to include the block path. 133 | renamePhpBlock(argv.b[index], pluginSlug); 134 | } 135 | }); 136 | } 137 | 138 | if (argv.b.length === 0) { 139 | // Use plugin slug if no block name specified. 140 | //console.log("NO BLOCK NAME. USING PLUGIN SLUG", pluginSlug); 141 | } 142 | } else { 143 | //console.log("NO BLOCK NAMES - JUST PROCEED AS NORMAL"); 144 | //pluginSlug = ''; // If no plugin slug then trigger interactive mode for npx @wordpress/create-block 145 | } 146 | 147 | // Rebuild plugin files only if '--no-wp-scripts' isn't set. 148 | if (!argv.ns) { 149 | log('\nRebuilding plugin files for production.'); 150 | log(execaCommandSync(`npm run build`, { cwd: `${pluginSlug}`, stdin: 'inherit' }).stdout); 151 | } 152 | 153 | log('\nAll finished. Happy block development!'); 154 | log('\nFollow me on Twitter: dgwyer'); 155 | 156 | // ============ 157 | 158 | function renameFirstPhpBlock(blockName, path) { 159 | 160 | let options = { 161 | files: `${path}/${pluginSlug}.php`, 162 | from: /build/gm, 163 | to: `build/${blockName.toLowerCase()}`, 164 | }; 165 | 166 | // Synchronous replacement. 167 | try { 168 | const results = replace.sync(options); 169 | //console.log('Replacement results:', results); 170 | } 171 | catch (error) { 172 | //console.error('Error occurred:', error); 173 | } 174 | } 175 | 176 | function renamePhpBlock(blockName, path) { 177 | 178 | let options = { 179 | files: `${path}/${pluginSlug}.php`, 180 | from: /^}/gm, 181 | to: ` register_block_type( __DIR__ . '/build/${blockName}' );\n}`, 182 | }; 183 | 184 | // Synchronous replacement. 185 | try { 186 | const results = replace.sync(options); 187 | //console.log('Replacement results:', results); 188 | } 189 | catch (error) { 190 | //console.error('Error occurred:', error); 191 | } 192 | } 193 | 194 | function renameBlockFiles(blockName, path, replaceStr) { 195 | 196 | // 1. Replace block name. 197 | let options = { 198 | files: `${path}/block.json`, 199 | from: new RegExp(`"name": "(create-block\/{1})(.*)?"`, 'gm'), 200 | to: `"name": "$1${blockName.toLowerCase()}"`, 201 | }; 202 | replaceSync(options); 203 | 204 | // 2. Replace block title. 205 | //console.log(`DEBUGGING: ${path}/block.json >> ${capitalize(replaceStr)} >> ${blockName}`); 206 | options = { 207 | files: `${path}/block.json`, 208 | from: new RegExp(`"title": "(.*?)"`, 'gm'), 209 | to: `"title": "${capitalize(blockName)}"`, 210 | }; 211 | replaceSync(options); 212 | 213 | // 3. Replace style.scss selector. 214 | options = { 215 | files: `${path}/style.scss`, 216 | from: new RegExp(`.wp-block-create-block-${replaceStr}`), 217 | to: `.wp-block-create-block-${blockName.toLowerCase()}`, 218 | }; 219 | replaceSync(options); 220 | 221 | // 4. Replace editor.scss selector. 222 | options = { 223 | files: `${path}/editor.scss`, 224 | from: new RegExp(`.wp-block-create-block-${replaceStr}`), 225 | to: `.wp-block-create-block-${blockName.toLowerCase()}`, 226 | }; 227 | replaceSync(options); 228 | 229 | // 5. Replace block name in index.js. 230 | options = { 231 | files: `${path}/index.js`, 232 | from: new RegExp(`create-block/${replaceStr}`), 233 | to: `create-block/${blockName.toLowerCase()}`, 234 | }; 235 | replaceSync(options); 236 | 237 | // 6. Replace block name in tailwind.config.js, only if we're integrating with Tailwind CSS. 238 | if (argv.tw) { 239 | options = { 240 | files: `${path}/tailwind.config.js`, 241 | from: /content: \[(.*?)\]/gm, 242 | to: `content: ['./src/${blockName.toLowerCase()}/*.js']`, 243 | }; 244 | replaceSync(options); 245 | } 246 | 247 | //const { stdout, stdin, stderr } = await execa("ls"); 248 | //console.log(stdout); 249 | } 250 | 251 | function capitalize(str) { 252 | const lower = str.toLowerCase(); 253 | return str.charAt(0).toUpperCase() + lower.slice(1); 254 | } 255 | 256 | function replaceSync(options, log = false) { 257 | // Synchronous replacement. 258 | try { 259 | const results = replace.sync(options); 260 | if(log) { console.log('Replacement results:', results); } 261 | } 262 | catch (error) { 263 | if (log) { console.error('Error occurred:', error); } 264 | } 265 | } --------------------------------------------------------------------------------