diff --git a/package-lock.json b/package-lock.json index 3201aaf06682d..d49fb33f5c707 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56499,6 +56499,7 @@ "jest-dev-server": "^9.0.1", "jest-environment-jsdom": "^29.6.2", "jest-environment-node": "^29.6.2", + "json2php": "^0.0.9", "markdownlint-cli": "^0.31.1", "merge-deep": "^3.0.3", "mini-css-extract-plugin": "^2.5.1", @@ -56724,6 +56725,13 @@ "dev": true, "license": "MIT" }, + "packages/scripts/node_modules/json2php": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/json2php/-/json2php-0.0.9.tgz", + "integrity": "sha512-fQMYwvPsQt8hxRnCGyg1r2JVi6yL8Um0DIIawiKiMK9yhAAkcRNj5UsBWoaFvFzPpcWbgw9L6wzj+UMYA702Mw==", + "dev": true, + "license": "BSD" + }, "packages/scripts/node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index 4e450974d334f..41dcf4d54922f 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -10,6 +10,10 @@ - Refactor to extract license related logic to a reusable module ([#66179](https://github.com/WordPress/gutenberg/pull/66179)). +### New Features + +- Add new `build-blocks-manifest` command to generate a PHP file containing block metadata from all `block.json` files in a project ([#65866](https://github.com/WordPress/gutenberg/pull/65866)). + ## 30.2.0 (2024-10-16) ## 30.1.0 (2024-10-03) diff --git a/packages/scripts/README.md b/packages/scripts/README.md index 9973090a3e11e..46e1a1265662c 100644 --- a/packages/scripts/README.md +++ b/packages/scripts/README.md @@ -117,6 +117,41 @@ and should be registered in WordPress using the Modules API. This script uses [webpack](https://webpack.js.org/) behind the scenes. It’ll look for a webpack config in the top-level directory of your package and will use it if it finds one. If none is found, it’ll use the default config provided by `@wordpress/scripts` packages. Learn more in the [Advanced Usage](#advanced-usage) section. + +### `build-blocks-manifest` + +This script generates a PHP file containing block metadata from all +`block.json` files in the project. This is useful for enhancing performance +when registering multiple block types, as it allows you to use +`wp_register_block_metadata_collection()` in WordPress. + +Usage: `wp-scripts build-blocks-manifest [options]` + +Options: +- `--input`: Specify the input directory (default: 'build') +- `--output`: Specify the output file path (default: 'build/blocks-manifest.php') + +Example: +```bash +wp-scripts build-blocks-manifest --input=src --output=dist/blocks-manifest.php +``` + +This command will scan the specified input directory for `block.json` files, +compile their metadata into a single PHP file, and output it to the specified +location. You can then use this file with +`wp_register_block_metadata_collection()` in your plugin: + +```php +wp_register_block_metadata_collection( + plugin_dir_path( __FILE__ ) . 'dist', + plugin_dir_path( __FILE__ ) . 'dist/blocks-manifest.php' +); +``` + +Using this approach can improve performance when registering multiple block +types, especially for plugins with several custom blocks. Note that this +feature is only available in WordPress 6.7 and later versions. + ### `check-engines` Checks if the current `node`, `npm` (or `yarn`) versions match the given [semantic version](https://semver.org/) ranges. If the given version is not satisfied, information about installing the needed version is printed and the program exits with an error code. diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 07cb9799275db..c98f341b81265 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -66,6 +66,7 @@ "jest-dev-server": "^9.0.1", "jest-environment-jsdom": "^29.6.2", "jest-environment-node": "^29.6.2", + "json2php": "^0.0.9", "markdownlint-cli": "^0.31.1", "merge-deep": "^3.0.3", "mini-css-extract-plugin": "^2.5.1", diff --git a/packages/scripts/scripts/build-blocks-manifest.js b/packages/scripts/scripts/build-blocks-manifest.js new file mode 100644 index 0000000000000..7138505de59ed --- /dev/null +++ b/packages/scripts/scripts/build-blocks-manifest.js @@ -0,0 +1,72 @@ +/** + * External dependencies + */ +const fs = require( 'fs' ); +const path = require( 'path' ); +const { sync: glob } = require( 'fast-glob' ); +const json2php = require( 'json2php' ); +const chalk = require( 'chalk' ); + +/** + * Internal dependencies + */ +const { getArgFromCLI } = require( '../utils' ); + +// Set default paths +const defaultInputDir = 'build'; +const defaultOutputFile = path.join( 'build', 'blocks-manifest.php' ); + +// Parse command line arguments +const inputDir = getArgFromCLI( '--input' ) || defaultInputDir; +const outputFile = getArgFromCLI( '--output' ) || defaultOutputFile; + +const resolvedInputDir = path.resolve( process.cwd(), inputDir ); +if ( ! fs.existsSync( resolvedInputDir ) ) { + const ERROR = chalk.reset.inverse.bold.red( ' ERROR ' ); + process.stdout.write( + `${ ERROR } Input directory "${ inputDir }" does not exist.\n` + ); + process.exit( 1 ); +} + +// Find all block.json files +const blockJsonFiles = glob( './**/block.json', { + cwd: resolvedInputDir, + absolute: true, +} ); + +const blocks = {}; + +blockJsonFiles.forEach( ( file ) => { + const blockJson = JSON.parse( fs.readFileSync( file, 'utf8' ) ); + const directoryName = path.basename( path.dirname( file ) ); + blocks[ directoryName ] = blockJson; +} ); + +if ( Object.keys( blocks ).length === 0 ) { + const ERROR = chalk.reset.inverse.bold.red( ' ERROR ' ); + process.stdout.write( + `${ ERROR } No block.json files were found in path: ${ inputDir }.\n` + ); + process.exit( 1 ); +} + +// Generate PHP content +const printer = json2php.make( { linebreak: '\n', indent: '\t' } ); +const phpContent = ` array( + '$schema' => 'https://schemas.wp.org/trunk/block.json', + 'apiVersion' => 2, + 'name' => 'my-plugin/custom-header', + 'title' => 'Custom Header', + 'category' => 'text', + 'icon' => 'heading', + 'description' => 'A custom header block with color options.', + 'attributes' => array( + 'content' => array( + 'type' => 'string', + 'source' => 'html', + 'selector' => 'h2' + ), + 'textColor' => array( + 'type' => 'string' + ), + 'backgroundColor' => array( + 'type' => 'string' + ) + ), + 'supports' => array( + 'html' => false, + 'color' => array( + 'background' => true, + 'text' => true + ) + ), + 'textdomain' => 'my-plugin', + 'editorScript' => 'file:./index.js', + 'editorStyle' => 'file:./index.css', + 'style' => 'file:./style-index.css' + ), + 'image-gallery' => array( + '$schema' => 'https://schemas.wp.org/trunk/block.json', + 'apiVersion' => 2, + 'name' => 'my-plugin/image-gallery', + 'title' => 'Image Gallery', + 'category' => 'media', + 'icon' => 'format-gallery', + 'description' => 'An image gallery block with customizable layout.', + 'attributes' => array( + 'images' => array( + 'type' => 'array', + 'default' => array( + + ), + 'source' => 'query', + 'selector' => 'img', + 'query' => array( + 'url' => array( + 'type' => 'string', + 'source' => 'attribute', + 'attribute' => 'src' + ), + 'alt' => array( + 'type' => 'string', + 'source' => 'attribute', + 'attribute' => 'alt', + 'default' => '' + ), + 'id' => array( + 'type' => 'number', + 'source' => 'attribute', + 'attribute' => 'data-id' + ) + ) + ), + 'columns' => array( + 'type' => 'number', + 'default' => 3 + ), + 'imageCrop' => array( + 'type' => 'boolean', + 'default' => true + ) + ), + 'supports' => array( + 'align' => array( + 'wide', + 'full' + ) + ), + 'textdomain' => 'my-plugin', + 'editorScript' => 'file:./index.js', + 'editorStyle' => 'file:./index.css', + 'style' => 'file:./style-index.css' + ), + 'simple-button' => array( + '$schema' => 'https://schemas.wp.org/trunk/block.json', + 'apiVersion' => 2, + 'name' => 'my-plugin/simple-button', + 'title' => 'Simple Button', + 'category' => 'design', + 'icon' => 'button', + 'description' => 'A simple button block.', + 'supports' => array( + 'html' => false + ), + 'textdomain' => 'my-plugin', + 'editorScript' => 'file:./index.js', + 'editorStyle' => 'file:./index.css', + 'style' => 'file:./style-index.css' + ) +); +" +`; diff --git a/packages/scripts/scripts/test/build-blocks-manifest.js b/packages/scripts/scripts/test/build-blocks-manifest.js new file mode 100644 index 0000000000000..70009bd6087b5 --- /dev/null +++ b/packages/scripts/scripts/test/build-blocks-manifest.js @@ -0,0 +1,101 @@ +/** + * External dependencies + */ +const fs = require( 'fs' ); +const path = require( 'path' ); +const { execSync } = require( 'child_process' ); +const rimraf = require( 'rimraf' ); + +const fixturesPath = path.join( + __dirname, + 'fixtures', + 'build-blocks-manifest' +); +const outputPath = path.join( __dirname, 'build', 'test-blocks-manifest' ); + +describe( 'build-blocks-manifest script', () => { + beforeAll( () => { + if ( ! fs.existsSync( outputPath ) ) { + fs.mkdirSync( outputPath, { recursive: true } ); + } + rimraf.sync( outputPath ); + } ); + + afterAll( () => { + rimraf.sync( outputPath ); + } ); + + it( 'should generate expected blocks manifest', () => { + const inputDir = path.join( fixturesPath, 'input' ); + const outputFile = path.join( outputPath, 'blocks-manifest.php' ); + + // Run the build-blocks-manifest script + const scriptPath = path.resolve( + __dirname, + '..', + 'build-blocks-manifest.js' + ); + execSync( + `node ${ scriptPath } --input=${ inputDir } --output=${ outputFile }` + ); + + const generatedContent = fs.readFileSync( outputFile, 'utf8' ); + expect( generatedContent ).toMatchSnapshot(); + } ); + + it( 'should error on empty input directory', () => { + const emptyInputDir = path.join( fixturesPath, 'empty-input' ); + const outputFile = path.join( outputPath, 'empty-blocks-manifest.php' ); + + const scriptPath = path.resolve( + __dirname, + '..', + 'build-blocks-manifest.js' + ); + let error; + try { + execSync( + `node ${ scriptPath } --input=${ emptyInputDir } --output=${ outputFile }`, + { encoding: 'utf8' } + ); + } catch ( e ) { + error = e; + } + + // Check that an error was thrown. + expect( error ).toBeDefined(); + expect( error.stdout ).toContain( + `No block.json files were found in path` + ); + + // Ensure that the output file was not created + expect( fs.existsSync( outputFile ) ).toBe( false ); + } ); + + it( 'should error on missing input directory', () => { + const nonExistentInputDir = path.join( fixturesPath, 'missing-input' ); + const outputFile = path.join( outputPath, 'empty-blocks-manifest.php' ); + + const scriptPath = path.resolve( + __dirname, + '..', + 'build-blocks-manifest.js' + ); + let error; + try { + execSync( + `node ${ scriptPath } --input=${ nonExistentInputDir } --output=${ outputFile }`, + { encoding: 'utf8' } + ); + } catch ( e ) { + error = e; + } + + // Check that an error was thrown. + expect( error ).toBeDefined(); + expect( error.stdout ).toContain( `does not exist` ); + + // Ensure that the output file was not created + expect( fs.existsSync( outputFile ) ).toBe( false ); + } ); +} ); diff --git a/packages/scripts/scripts/test/fixtures/build-blocks-manifest/empty-input/.keep b/packages/scripts/scripts/test/fixtures/build-blocks-manifest/empty-input/.keep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/scripts/scripts/test/fixtures/build-blocks-manifest/input/custom-header/block.json b/packages/scripts/scripts/test/fixtures/build-blocks-manifest/input/custom-header/block.json new file mode 100644 index 0000000000000..056bffbe28b18 --- /dev/null +++ b/packages/scripts/scripts/test/fixtures/build-blocks-manifest/input/custom-header/block.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "my-plugin/custom-header", + "title": "Custom Header", + "category": "text", + "icon": "heading", + "description": "A custom header block with color options.", + "attributes": { + "content": { + "type": "string", + "source": "html", + "selector": "h2" + }, + "textColor": { + "type": "string" + }, + "backgroundColor": { + "type": "string" + } + }, + "supports": { + "html": false, + "color": { + "background": true, + "text": true + } + }, + "textdomain": "my-plugin", + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./style-index.css" +} diff --git a/packages/scripts/scripts/test/fixtures/build-blocks-manifest/input/image-gallery/block.json b/packages/scripts/scripts/test/fixtures/build-blocks-manifest/input/image-gallery/block.json new file mode 100644 index 0000000000000..9c84fff433870 --- /dev/null +++ b/packages/scripts/scripts/test/fixtures/build-blocks-manifest/input/image-gallery/block.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "my-plugin/image-gallery", + "title": "Image Gallery", + "category": "media", + "icon": "format-gallery", + "description": "An image gallery block with customizable layout.", + "attributes": { + "images": { + "type": "array", + "default": [], + "source": "query", + "selector": "img", + "query": { + "url": { + "type": "string", + "source": "attribute", + "attribute": "src" + }, + "alt": { + "type": "string", + "source": "attribute", + "attribute": "alt", + "default": "" + }, + "id": { + "type": "number", + "source": "attribute", + "attribute": "data-id" + } + } + }, + "columns": { + "type": "number", + "default": 3 + }, + "imageCrop": { + "type": "boolean", + "default": true + } + }, + "supports": { + "align": [ "wide", "full" ] + }, + "textdomain": "my-plugin", + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./style-index.css" +} diff --git a/packages/scripts/scripts/test/fixtures/build-blocks-manifest/input/simple-button/block.json b/packages/scripts/scripts/test/fixtures/build-blocks-manifest/input/simple-button/block.json new file mode 100644 index 0000000000000..23e607c411fbd --- /dev/null +++ b/packages/scripts/scripts/test/fixtures/build-blocks-manifest/input/simple-button/block.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "my-plugin/simple-button", + "title": "Simple Button", + "category": "design", + "icon": "button", + "description": "A simple button block.", + "supports": { + "html": false + }, + "textdomain": "my-plugin", + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./style-index.css" +}