From ffd1cef085948934001f5839632bf75c4052915f Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 22 Mar 2017 15:09:09 +0100 Subject: [PATCH] Plugin: Implement block registering API (#289) * Remove namespace from PHP `register_block` stub * Plugin: Implement block registering API * Add .nvmrc * Add babel-plugin-transform-runtime Right now, this is to polyfill `Object.values` (used in `getBlocks`). Node.js and older browsers lack support for this function. * Add Mocha unit tests --- .babelrc | 10 +- .eslintignore | 1 + .eslintrc.json | 3 +- .gitignore | 1 + .nvmrc | 1 + bootstrap-test.js | 7 ++ index.php | 12 +-- modules/blocks/index.js | 56 ++++++++--- modules/blocks/test/index.js | 112 ++++++++++++++++++++++ modules/editor/blocks/text-block/index.js | 2 +- package.json | 10 +- webpack.config.js | 15 ++- 12 files changed, 205 insertions(+), 25 deletions(-) create mode 100644 .eslintignore create mode 100644 .nvmrc create mode 100644 bootstrap-test.js create mode 100644 modules/blocks/test/index.js diff --git a/.babelrc b/.babelrc index a297ca0ee703e..f0c62e1f6d3a1 100644 --- a/.babelrc +++ b/.babelrc @@ -5,5 +5,13 @@ "modules": false } } ] - ] + ], + "plugins": [ + "transform-runtime" + ], + "env": { + "test": { + "presets": [ "latest" ] + } + } } diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000000..1f6015f87718e --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +modules/*/build diff --git a/.eslintrc.json b/.eslintrc.json index 6d63ad1472709..187d1d3dbf1e4 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,7 +3,8 @@ "extends": "wordpress", "env": { "browser": true, - "node": true + "node": true, + "mocha": true }, "parserOptions": { "sourceType": "module" diff --git a/.gitignore b/.gitignore index 01a5e93c1293f..21966ade97a60 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules build *.log +yarn.lock diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000000..62014a4029e59 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v6.10.0 diff --git a/bootstrap-test.js b/bootstrap-test.js new file mode 100644 index 0000000000000..29e3ea7dfba6b --- /dev/null +++ b/bootstrap-test.js @@ -0,0 +1,7 @@ +/** + * External dependencies + */ +import chai from 'chai'; +import dirtyChai from 'dirty-chai'; + +chai.use( dirtyChai ); diff --git a/index.php b/index.php index 9ae36018250eb..4d7014e9b3273 100644 --- a/index.php +++ b/index.php @@ -76,11 +76,11 @@ function the_gutenberg_project() { /** * Registers a block. * - * @param string $namespace Block grouping unique to package or plugin. - * @param string $block Block name. - * @param array $args Optional. Array of settings for the block. Default empty array. - * @return bool True on success, false on error. + * @param string $name Block name including namespace. + * @param array $args Optional. Array of settings for the block. Default + * empty array. + * @return bool True on success, false on error. */ -function register_block( $namespace, $block, $args = array() ) { - +function register_block( $name, $args = array() ) { + // Not implemented yet. } diff --git a/modules/blocks/index.js b/modules/blocks/index.js index aca9c624b86d2..4d123eec91c99 100644 --- a/modules/blocks/index.js +++ b/modules/blocks/index.js @@ -1,32 +1,66 @@ export { default as Editable } from './components/editable'; +/** + * Block settings keyed by block slug. + * + * @var {Object} blocks + */ +const blocks = {}; + /** * Registers a block. * - * @param {string} namespace Block grouping unique to package or plugin - * @param {string} block Block name - * @param {Object} settings Block settings + * @param {string} slug Block slug + * @param {Object} settings Block settings */ -export function registerBlock( namespace, block, settings ) { +export function registerBlock( slug, settings ) { + if ( typeof slug !== 'string' ) { + throw new Error( + 'Block slugs must be strings.' + ); + } + if ( ! /^[a-z0-9-]+\/[a-z0-9-]+$/.test( slug ) ) { + throw new Error( + 'Block slugs must contain a namespace prefix. Example: my-plugin/my-custom-block' + ); + } + if ( blocks[ slug ] ) { + throw new Error( + 'Block "' + slug + '" is already registered.' + ); + } + blocks[ slug ] = Object.assign( { slug }, settings ); +} +/** + * Unregisters a block. + * + * @param {string} slug Block slug + */ +export function unregisterBlock( slug ) { + if ( ! blocks[ slug ] ) { + throw new Error( + 'Block "' + slug + '" is not registered.' + ); + } + delete blocks[ slug ]; } /** * Returns settings associated with a block. * - * @param {string} namespace Block grouping unique to package or plugin - * @param {string} block Block name - * @return {?Object} Block settings + * @param {string} slug Block slug + * @return {?Object} Block settings */ -export function getBlockSettings( namespace, block ) { - +export function getBlockSettings( slug ) { + return blocks[ slug ]; } /** * Returns all registered blocks. * - * @return {Object} Block settings keyed by block name + * @return {Array} Block settings */ export function getBlocks() { - + return Object.values( blocks ); } diff --git a/modules/blocks/test/index.js b/modules/blocks/test/index.js new file mode 100644 index 0000000000000..d9824e18c5b27 --- /dev/null +++ b/modules/blocks/test/index.js @@ -0,0 +1,112 @@ +/** + * External dependencies + */ +import { expect } from 'chai'; + +/** + * Internal dependencies + */ +import * as blocks from '../'; + +describe( 'blocks API', () => { + // Reset block state before each test. + beforeEach( () => { + blocks.getBlocks().forEach( block => { + blocks.unregisterBlock( block.slug ); + } ); + } ); + + describe( 'registerBlock', () => { + it( 'should reject numbers', () => { + expect( + () => blocks.registerBlock( 999 ) + ).to.throw( 'Block slugs must be strings.' ); + } ); + + it( 'should reject blocks without a namespace', () => { + expect( + () => blocks.registerBlock( 'doing-it-wrong' ) + ).to.throw( /^Block slugs must contain a namespace prefix/ ); + } ); + + it( 'should reject blocks with invalid characters', () => { + expect( + () => blocks.registerBlock( 'still/_doing_it_wrong' ) + ).to.throw( /^Block slugs must contain a namespace prefix/ ); + } ); + + it( 'should accept valid block names', () => { + expect( + () => blocks.registerBlock( 'my-plugin/fancy-block-4' ) + ).not.to.throw(); + } ); + + it( 'should prohibit registering the same block twice', () => { + blocks.registerBlock( 'core/test-block' ); + expect( + () => blocks.registerBlock( 'core/test-block' ) + ).to.throw( 'Block "core/test-block" is already registered.' ); + } ); + + it( 'should store a copy of block settings', () => { + const blockSettings = { settingName: 'settingValue' }; + blocks.registerBlock( 'core/test-block-with-settings', blockSettings ); + blockSettings.mutated = true; + expect( blocks.getBlockSettings( 'core/test-block-with-settings' ) ).to.eql( { + slug: 'core/test-block-with-settings', + settingName: 'settingValue', + } ); + } ); + } ); + + describe( 'unregisterBlock', () => { + it( 'should fail if a block is not registered', () => { + expect( + () => blocks.unregisterBlock( 'core/test-block' ) + ).to.throw( 'Block "core/test-block" is not registered.' ); + } ); + + it( 'should unregister existing blocks', () => { + blocks.registerBlock( 'core/test-block' ); + expect( blocks.getBlocks() ).to.eql( [ + { slug: 'core/test-block' }, + ] ); + blocks.unregisterBlock( 'core/test-block' ); + expect( blocks.getBlocks() ).to.eql( [] ); + } ); + } ); + + describe( 'getBlockSettings', () => { + it( 'should return { slug } for blocks with no settings', () => { + blocks.registerBlock( 'core/test-block' ); + expect( blocks.getBlockSettings( 'core/test-block' ) ).to.eql( { + slug: 'core/test-block', + } ); + } ); + + it( 'should return all block settings', () => { + const blockSettings = { settingName: 'settingValue' }; + blocks.registerBlock( 'core/test-block-with-settings', blockSettings ); + expect( blocks.getBlockSettings( 'core/test-block-with-settings' ) ).to.eql( { + slug: 'core/test-block-with-settings', + settingName: 'settingValue', + } ); + } ); + } ); + + describe( 'getBlocks', () => { + it( 'should return an empty array at first', () => { + expect( blocks.getBlocks() ).to.eql( [] ); + } ); + + it( 'should return all registered blocks', () => { + blocks.registerBlock( 'core/test-block' ); + const blockSettings = { settingName: 'settingValue' }; + blocks.registerBlock( 'core/test-block-with-settings', blockSettings ); + expect( blocks.getBlocks() ).to.eql( [ + { slug: 'core/test-block' }, + { slug: 'core/test-block-with-settings', settingName: 'settingValue' }, + ] ); + } ); + } ); +} ); diff --git a/modules/editor/blocks/text-block/index.js b/modules/editor/blocks/text-block/index.js index 62ab077fc621e..d75027f2eba10 100644 --- a/modules/editor/blocks/text-block/index.js +++ b/modules/editor/blocks/text-block/index.js @@ -1,4 +1,4 @@ -wp.blocks.registerBlock( 'wp', 'Text', { +wp.blocks.registerBlock( 'wp/text', { edit( state, onChange ) { return wp.element.createElement( wp.blocks.Editable, { value: state.value, diff --git a/package.json b/package.json index 1ffb9a8986ca3..4370be9eb0876 100644 --- a/package.json +++ b/package.json @@ -11,19 +11,25 @@ "editor" ], "scripts": { + "test-unit": "cross-env NODE_ENV=test mocha modules/**/test/*.js --compilers js:babel-register --recursive --require ./bootstrap-test.js", "build": "cross-env NODE_ENV=production webpack", - "lint": "eslint editor", - "test": "npm run lint" + "lint": "eslint modules", + "test": "npm run lint && npm run test-unit" }, "devDependencies": { "autoprefixer": "^6.7.7", "babel-core": "^6.24.0", "babel-loader": "^6.4.1", + "babel-plugin-transform-runtime": "^6.23.0", "babel-preset-latest": "^6.24.0", + "babel-register": "^6.24.0", + "chai": "^3.5.0", "cross-env": "^3.2.4", "css-loader": "^0.27.3", + "dirty-chai": "^1.2.2", "eslint": "^3.17.1", "eslint-config-wordpress": "^1.1.0", + "mocha": "^3.2.0", "node-sass": "^4.5.0", "postcss-loader": "^1.3.3", "sass-loader": "^6.0.3", diff --git a/webpack.config.js b/webpack.config.js index 9c0a34b379cf6..425c05422b58c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -29,7 +29,7 @@ const entry = fs.readdirSync( BASE_PATH ).reduce( ( memo, filename ) => { return memo; }, {} ); -const config = module.exports = { +const config = { entry: entry, output: { filename: '[name]/build/index.js', @@ -39,8 +39,9 @@ const config = module.exports = { }, resolve: { modules: [ - 'editor', - 'external', + ...Object.keys( entry ).map( ( filename ) => { + return path.join( BASE_PATH, filename ); + } ), 'node_modules' ] }, @@ -48,6 +49,7 @@ const config = module.exports = { rules: [ { test: /\.js$/, + exclude: /node_modules/, use: 'babel-loader' }, { @@ -62,6 +64,11 @@ const config = module.exports = { ] }, plugins: [ + new webpack.DefinePlugin( { + 'process.env': { + NODE_ENV: JSON.stringify( process.env.NODE_ENV ) + } + } ), new webpack.LoaderOptionsPlugin( { minimize: process.env.NODE_ENV === 'production', debug: process.env.NODE_ENV !== 'production', @@ -79,3 +86,5 @@ if ( 'production' === process.env.NODE_ENV ) { } else { config.devtool = 'source-map'; } + +module.exports = config;