├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── build.js ├── index.js └── package.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ "@babel/preset-env", { 4 | "corejs": "2.6.5", 5 | "useBuiltIns": "entry" 6 | } ] 7 | ] 8 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Top-most EditorConfig file (see EditorConfig.org) 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | # Tab indentation most places 6 | [*] 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = tab 10 | trim_trailing_whitespace = true 11 | 12 | # YAML files require spaces. JSON doesn't, but our file uses them. 13 | [*.{yaml,yml,json}] 14 | indent_style = space 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build.js 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | /* eslint-disable array-element-newline,array-bracket-newline */ 3 | module.exports = { 4 | 'env': { 5 | 'browser': true, 6 | 'es6': true 7 | }, 8 | 'extends': 'eslint:recommended', 9 | 'globals': { 10 | 'Atomics': 'readonly', 11 | 'SharedArrayBuffer': 'readonly' 12 | }, 13 | 'parserOptions': { 14 | 'ecmaVersion': 2020, 15 | 'sourceType': 'module' 16 | }, 17 | 'rules': { 18 | 'accessor-pairs': 'error', 19 | 'array-bracket-newline': 'error', 20 | 'array-bracket-spacing': [ 'error', 'always' ], 21 | 'array-callback-return': 'error', 22 | 'array-element-newline': 'error', 23 | 'arrow-body-style': 'error', 24 | 'arrow-parens': [ 'error', 'always' ], 25 | 'arrow-spacing': [ 26 | 'error', 27 | { 28 | 'after': true, 29 | 'before': true 30 | } 31 | ], 32 | 'block-scoped-var': 'error', 33 | 'block-spacing': 'error', 34 | 'brace-style': [ 35 | 'error', 36 | '1tbs' 37 | ], 38 | 'callback-return': 'error', 39 | 'camelcase': 'error', 40 | 'capitalized-comments': [ 41 | 'error', 42 | 'always' 43 | ], 44 | 'class-methods-use-this': 'error', 45 | 'comma-dangle': 'off', 46 | 'comma-spacing': [ 47 | 'error', 48 | { 49 | 'after': true, 50 | 'before': false 51 | } 52 | ], 53 | 'comma-style': [ 54 | 'error', 55 | 'last' 56 | ], 57 | 'complexity': 'error', 58 | 'computed-property-spacing': [ 59 | 'error', 60 | 'always' 61 | ], 62 | 'consistent-return': 'error', 63 | 'consistent-this': 'error', 64 | 'curly': 'error', 65 | 'default-case': 'error', 66 | 'default-param-last': 'error', 67 | 'dot-location': 'error', 68 | 'dot-notation': 'error', 69 | 'eol-last': 'error', 70 | 'eqeqeq': 'error', 71 | 'func-call-spacing': 'error', 72 | 'func-name-matching': 'error', 73 | 'func-names': 'error', 74 | 'func-style': [ 75 | 'error', 76 | 'expression' 77 | ], 78 | 'function-call-argument-newline': [ 79 | 'error', 80 | 'consistent' 81 | ], 82 | 'function-paren-newline': 'error', 83 | 'generator-star-spacing': 'error', 84 | 'global-require': 'error', 85 | 'grouped-accessor-pairs': 'error', 86 | 'guard-for-in': 'error', 87 | 'handle-callback-err': 'error', 88 | 'id-blacklist': 'error', 89 | 'id-length': 'error', 90 | 'id-match': 'error', 91 | 'implicit-arrow-linebreak': [ 92 | 'error', 93 | 'beside' 94 | ], 95 | 'indent': 'off', 96 | 'indent-legacy': 'off', 97 | 'init-declarations': 'error', 98 | 'jsx-quotes': 'error', 99 | 'key-spacing': 'error', 100 | 'keyword-spacing': [ 101 | 'error', 102 | { 103 | 'after': true, 104 | 'before': true 105 | } 106 | ], 107 | 'line-comment-position': 'error', 108 | 'linebreak-style': [ 109 | 'error', 110 | 'unix' 111 | ], 112 | 'lines-around-comment': 'error', 113 | 'lines-around-directive': 'error', 114 | 'lines-between-class-members': 'error', 115 | 'max-classes-per-file': 'error', 116 | 'max-depth': 'error', 117 | 'max-len': 'off', 118 | 'max-lines': 'off', 119 | 'max-lines-per-function': 'error', 120 | 'max-nested-callbacks': 'error', 121 | 'max-params': 'error', 122 | 'max-statements-per-line': 'error', 123 | 'multiline-comment-style': 'error', 124 | 'multiline-ternary': 'error', 125 | 'new-cap': 'error', 126 | 'new-parens': 'error', 127 | 'newline-after-var': 'off', 128 | 'newline-before-return': 'error', 129 | 'newline-per-chained-call': 'off', 130 | 'no-alert': 'error', 131 | 'no-array-constructor': 'error', 132 | 'no-await-in-loop': 'error', 133 | 'no-bitwise': 'error', 134 | 'no-buffer-constructor': 'error', 135 | 'no-caller': 'error', 136 | 'no-catch-shadow': 'error', 137 | 'no-confusing-arrow': 'error', 138 | 'no-console': 'off', 139 | 'no-constructor-return': 'error', 140 | 'no-continue': 'error', 141 | 'no-div-regex': 'error', 142 | 'no-dupe-else-if': 'error', 143 | 'no-duplicate-imports': 'error', 144 | 'no-else-return': 'error', 145 | 'no-empty-function': 'off', 146 | 'no-eq-null': 'error', 147 | 'no-eval': 'error', 148 | 'no-extend-native': 'error', 149 | 'no-extra-bind': 'error', 150 | 'no-extra-label': 'error', 151 | 'no-floating-decimal': 'error', 152 | 'no-implicit-coercion': 'error', 153 | 'no-implicit-globals': 'error', 154 | 'no-implied-eval': 'error', 155 | 'no-import-assign': 'error', 156 | 'no-inline-comments': 'error', 157 | 'no-invalid-this': 'error', 158 | 'no-iterator': 'error', 159 | 'no-label-var': 'error', 160 | 'no-labels': 'error', 161 | 'no-lone-blocks': 'error', 162 | 'no-lonely-if': 'error', 163 | 'no-loop-func': 'error', 164 | 'no-magic-numbers': 'error', 165 | 'no-mixed-operators': 'error', 166 | 'no-mixed-requires': 'error', 167 | 'no-multi-assign': 'error', 168 | 'no-multi-spaces': 'error', 169 | 'no-multi-str': 'error', 170 | 'no-multiple-empty-lines': 'error', 171 | 'no-native-reassign': 'error', 172 | 'no-negated-condition': 'error', 173 | 'no-negated-in-lhs': 'error', 174 | 'no-nested-ternary': 'error', 175 | 'no-new': 'error', 176 | 'no-new-func': 'error', 177 | 'no-new-object': 'error', 178 | 'no-new-require': 'error', 179 | 'no-new-wrappers': 'error', 180 | 'no-octal-escape': 'error', 181 | 'no-param-reassign': 'error', 182 | 'no-path-concat': 'error', 183 | 'no-plusplus': 'error', 184 | 'no-process-env': 'error', 185 | 'no-process-exit': 'error', 186 | 'no-proto': 'error', 187 | 'no-restricted-globals': 'error', 188 | 'no-restricted-imports': 'error', 189 | 'no-restricted-modules': 'error', 190 | 'no-restricted-properties': 'error', 191 | 'no-restricted-syntax': 'error', 192 | 'no-return-assign': 'error', 193 | 'no-return-await': 'error', 194 | 'no-script-url': 'error', 195 | 'no-self-compare': 'error', 196 | 'no-sequences': 'error', 197 | 'no-setter-return': 'error', 198 | 'no-shadow': 'error', 199 | 'no-spaced-func': 'error', 200 | 'no-sync': 'error', 201 | 'no-tabs': [ 202 | 'error', 203 | { 204 | 'allowIndentationTabs': true 205 | } 206 | ], 207 | 'no-template-curly-in-string': 'error', 208 | 'no-ternary': 'error', 209 | 'no-throw-literal': 'error', 210 | 'no-trailing-spaces': 'error', 211 | 'no-undef-init': 'error', 212 | 'no-undefined': 'error', 213 | 'no-underscore-dangle': 'error', 214 | 'no-unmodified-loop-condition': 'error', 215 | 'no-unneeded-ternary': 'error', 216 | 'no-unused-expressions': 'error', 217 | 'no-use-before-define': 'error', 218 | 'no-useless-call': 'error', 219 | 'no-useless-computed-key': 'error', 220 | 'no-useless-concat': 'error', 221 | 'no-useless-constructor': 'error', 222 | 'no-useless-rename': 'error', 223 | 'no-useless-return': 'error', 224 | 'no-var': 'error', 225 | 'no-void': 'error', 226 | 'no-warning-comments': 'error', 227 | 'no-whitespace-before-property': 'error', 228 | 'nonblock-statement-body-position': 'error', 229 | 'object-curly-newline': 'error', 230 | 'object-curly-spacing': [ 231 | 'error', 232 | 'always' 233 | ], 234 | 'object-property-newline': 'error', 235 | 'object-shorthand': 'error', 236 | 'one-var': 'off', 237 | 'one-var-declaration-per-line': 'error', 238 | 'operator-assignment': 'error', 239 | 'operator-linebreak': 'error', 240 | 'padded-blocks': 'off', 241 | 'padding-line-between-statements': 'error', 242 | 'prefer-arrow-callback': 'error', 243 | 'prefer-const': 'error', 244 | 'prefer-exponentiation-operator': 'error', 245 | 'prefer-named-capture-group': 'error', 246 | 'prefer-numeric-literals': 'error', 247 | 'prefer-object-spread': 'error', 248 | 'prefer-promise-reject-errors': 'error', 249 | 'prefer-reflect': 'error', 250 | 'prefer-regex-literals': 'error', 251 | 'prefer-rest-params': 'error', 252 | 'prefer-spread': 'error', 253 | 'prefer-template': 'error', 254 | 'quotes': [ 255 | 'error', 256 | 'single' 257 | ], 258 | 'radix': 'error', 259 | 'require-await': 'error', 260 | 'require-jsdoc': 'error', 261 | 'require-unicode-regexp': 'error', 262 | 'rest-spread-spacing': 'error', 263 | 'semi': 'error', 264 | 'semi-spacing': 'error', 265 | 'semi-style': [ 266 | 'error', 267 | 'last' 268 | ], 269 | 'sort-imports': 'error', 270 | 'sort-keys': 'off', 271 | 'sort-vars': 'error', 272 | 'space-before-blocks': 'error', 273 | 'space-before-function-paren': [ 'error', { 274 | 'anonymous': 'never', 275 | 'named': 'never', 276 | 'asyncArrow': 'always' 277 | } ], 278 | 'space-in-parens': [ 279 | 'error', 280 | 'always' 281 | ], 282 | 'space-infix-ops': 'error', 283 | 'spaced-comment': [ 284 | 'error', 285 | 'always' 286 | ], 287 | 'strict': 'error', 288 | 'switch-colon-spacing': 'error', 289 | 'symbol-description': 'error', 290 | 'template-curly-spacing': [ 'error', 'always' ], 291 | 'template-tag-spacing': 'error', 292 | 'unicode-bom': [ 293 | 'error', 294 | 'never' 295 | ], 296 | 'valid-jsdoc': 'off', 297 | 'vars-on-top': 'error', 298 | 'wrap-iife': 'error', 299 | 'wrap-regex': 'error', 300 | 'yield-star-spacing': 'error', 301 | 'yoda': 'error' 302 | } 303 | }; 304 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # Lockfile 64 | package-lock.json 65 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License (ISC) 2 | 3 | Copyright 2018 K Adam White 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 14 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hot-Reloading Utilities for the WordPress Block Editor 2 | 3 | This library aims to make hot-reloading Gutenberg editor blocks & plugins as simple as possible. 4 | 5 | ## Auto-Loading Blocks 6 | 7 | Assuming your blocks are stored in a folder organized like this: 8 | 9 | ``` 10 | src 11 | ├── blocks 12 | │ ├── block-a 13 | │ │ └── index.js 14 | │ ├── block-b 15 | │ │ └── index.js 16 | │ └── block-c 17 | │ └── index.js 18 | └── blocks.js 19 | ``` 20 | and that your block files export at minimum **either** a `settings` object and `name` string: 21 | 22 | ```js 23 | export const name = 'myplugin/block-a'; 24 | 25 | export const settings = { 26 | title: 'Block A', 27 | 28 | description: 'An excellent example block', 29 | 30 | // icon, category, attributes, edit, save, etcetera 31 | } 32 | 33 | ``` 34 | 35 | **or** a `settings` object that has a `name` string property (e.g., when using `block.json` to manage your block's metadata): 36 | 37 | ```js 38 | import metadata from './block.json'; 39 | 40 | export const settings = { 41 | ...metadata 42 | 43 | // edit, save, other dynamic data 44 | } 45 | 46 | ``` 47 | 48 | then you can put this code in `blocks.js` to automatically load and configure every block in your plugin: 49 | 50 | ```js 51 | /** 52 | * blocks.js: 53 | * Dynamically locate, load & register all Gutenberg blocks. 54 | */ 55 | import { autoloadBlocks } from 'block-editor-hmr'; 56 | 57 | // Load all block index files. 58 | autoloadBlocks( 59 | { 60 | /** 61 | * Return a project-specific require.context. 62 | */ 63 | getContext: () => require.context( './blocks', true, /index\.js$/ ), 64 | }, 65 | ( context, loadModules ) => { 66 | if ( module.hot ) { 67 | module.hot.accept( context.id, loadModules ); 68 | } 69 | } 70 | ); 71 | 72 | ``` 73 | 74 | ## Block Editor Plugins 75 | 76 | The same logic applies if you want to register block editor plugins: export a `name` and `settings` from each plugin module, then use the provided `registerPlugin` and `unregisterPlugin` methods within your plugins entrypoint file. 77 | 78 | ```js 79 | /** 80 | * plugins.js: 81 | * Dynamically locate, load & register all Gutenberg plugins. 82 | */ 83 | import { autoloadPlugins } from 'block-editor-hmr'; 84 | 85 | // Load all plugin index files. 86 | autoloadPlugins( 87 | { 88 | /** 89 | * Return a project-specific require.context. 90 | */ 91 | getContext: () => require.context( './plugins', true, /index\.js$/ ), 92 | }, 93 | ( context, loadModules ) => { 94 | if ( module.hot ) { 95 | module.hot.accept( context.id, loadModules ); 96 | } 97 | } 98 | ); 99 | ``` 100 | 101 | ## Block Editor Formats 102 | 103 | As with blocks and plugins, helpers are also available to register [Block Formats](https://developer.wordpress.org/block-editor/tutorials/format-api/). 104 | 105 | ```js 106 | /** 107 | * Dynamically locate, load & register all Gutenberg formats. 108 | */ 109 | import { autoloadFormats } from 'block-editor-hmr'; 110 | 111 | // Load all format index files. 112 | autoloadFormats( 113 | { 114 | /** 115 | * Return a project-specific require.context. 116 | */ 117 | getContext: () => require.context( './formats', true, /index\.js$/ ), 118 | }, 119 | ( context, loadModules ) => { 120 | if ( module.hot ) { 121 | module.hot.accept( context.id, loadModules ); 122 | } 123 | } 124 | ); 125 | 126 | ``` 127 | 128 | ## Need More Control? 129 | 130 | In case you need more control over things, the library also exports a generic `autoload` function, as well as any block- or plugin-specific function that is used as a default value. 131 | 132 | ```js 133 | import { 134 | autoload, 135 | 136 | registerBlock, 137 | unregisterBlock, 138 | beforeUpdateBlocks, 139 | afterUpdateBlocks, 140 | 141 | registerPlugin, 142 | unregisterPlugin, 143 | } from 'block-editor-hmr'; 144 | ``` 145 | 146 | This means you can either pass select custom values to `autoloadBlocks` and `autoloadPlugins`, or roll your own autoloader via a fully custom `autoload`. 147 | 148 | ## Script Dependencies 149 | 150 | For this to work, the bundle which utilizes these methods must be enqueued specifying `wp-blocks`, `wp-plugins`, `wp-hooks`, and `wp-data` as script dependencies. 151 | 152 | ## How does it work? 153 | 154 | [The `require.context` Webpack documentation is available here.](https://webpack.js.org/guides/dependency-management/#requirecontext) 155 | 156 | `require.context` allows you to pass in a directory to search, a flag indicating whether subdirectories should be searched too, and a regular expression to match files against. The `autoload` method takes this context, uses it to load matching JS modules, then passes those modules through the `register` and `unregister` hooks as necessary. `before` and `after` hooks are provided to support things like maintaining block context, so that an update doesn't deselect the block you're working on. 157 | 158 | It's possible this could be simplified further, but testing to date indicates that `require.context` and `module.hot.accept` _must_ be called from the entrypoint file within your project, rather than being abstracted within the third-party NPM module. 159 | 160 | ## A note on ESNext 161 | 162 | Note that at present, this file is not transpiled and may break some build processes. A built file with wider browser compatibility is my next step for this project. 163 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } 4 | Object.defineProperty(exports, "__esModule", { 5 | value: true 6 | }); 7 | exports.unregisterPlugin = exports.unregisterFormat = exports.unregisterBlock = exports.registerPlugin = exports.registerFormat = exports.registerBlock = exports.beforeUpdateBlocks = exports.autoloadPlugins = exports.autoloadFormats = exports.autoloadBlocks = exports.autoload = exports.afterUpdateBlocks = exports._apply_wp_5_4_hmr_patch = void 0; 8 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 9 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } } 10 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } 11 | function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); } 12 | function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } 13 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); } 14 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } 15 | function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } 16 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); } 17 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } 18 | function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } } 19 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } 20 | /** 21 | * Provide helper methods to dynamically locate, load & register Blocks & Plugins. 22 | */ 23 | var _window$wp = window.wp, 24 | blocks = _window$wp.blocks, 25 | plugins = _window$wp.plugins, 26 | richText = _window$wp.richText, 27 | hooks = _window$wp.hooks, 28 | data = _window$wp.data; 29 | 30 | /** 31 | * No-op function for use as a default argument value. 32 | */ 33 | var noop = function noop() {}; 34 | 35 | /** 36 | * Require a set of modules and configure them for hot module replacement. 37 | * The consuming function must opt-in to HMR by passing a callback to 38 | * accept updates for the context where this function is used. 39 | * 40 | * The first argument should be a function returning a `require.context()` 41 | * call. All modules loaded from this context are cached, and on each rebuild 42 | * the incoming updated modules are checked against the cache. Updated modules 43 | * which already exist in the cache are unregistered with the provided function, 44 | * then any incoming (new or updated) modules will be registered. 45 | * 46 | * @param {Object} options Configuration object defining callbacks. 47 | * @param {Function} options.getContext Execute and return a `require.context()` call. 48 | * @param {Function} options.register Function to register accepted modules. 49 | * @param {Function} options.unregister Function to unregister replaced modules. 50 | * @param {Function} options.[before] Function to run before updating modules. 51 | * @param {Function} options.[after] Function to run after updating modules. 52 | * @param {Function} [callback] A callback function which will be passed the 53 | * generated `context` object and `loadModules` 54 | * function, which can be used to opt-in to HMR. 55 | */ 56 | var autoload = function autoload(_ref) { 57 | var getContext = _ref.getContext, 58 | register = _ref.register, 59 | unregister = _ref.unregister, 60 | _ref$before = _ref.before, 61 | before = _ref$before === void 0 ? noop : _ref$before, 62 | _ref$after = _ref.after, 63 | after = _ref$after === void 0 ? noop : _ref$after; 64 | var callback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : noop; 65 | var cache = {}; 66 | var loadModules = function loadModules() { 67 | before(); 68 | var context = getContext(); 69 | var changed = []; 70 | context.keys().forEach(function (key) { 71 | var module = context(key); 72 | if (module === cache[key]) { 73 | // Module unchanged: no further action needed. 74 | return; 75 | } 76 | var isHotUpdate = cache[key]; 77 | if (isHotUpdate && console.groupCollapsed) { 78 | console.groupCollapsed("hot update: ".concat(key)); 79 | } 80 | if (isHotUpdate) { 81 | // Module changed, and prior copy detected: unregister old module. 82 | unregister(cache[key]); 83 | } 84 | // Register new module and update cache. 85 | register(module); 86 | changed.push(module); 87 | cache[key] = module; 88 | if (isHotUpdate && console.groupCollapsed) { 89 | console.groupEnd(); 90 | } 91 | }); 92 | after(changed); 93 | 94 | // Return the context for HMR initialization. 95 | return context; 96 | }; 97 | var context = loadModules(); 98 | callback(context, loadModules); 99 | }; 100 | 101 | // Maintain the selected block ID across HMR updates. 102 | exports.autoload = autoload; 103 | var selectedBlockId = null; 104 | 105 | /** 106 | * Register a new or updated block, filters, or style variations. 107 | * 108 | * @param {Object} block The exported block module. 109 | * @param {String} block.name Block name. May be included in configuration object. 110 | * @param {Object} [block.settings] Optional block configuration object. 111 | * @param {Object[]} [block.filters] Optional array of filters to bind. 112 | * @param {Object[]} [block.styles] Optional array of block styles to bind. 113 | */ 114 | var registerBlock = function registerBlock(_ref2) { 115 | var name = _ref2.name, 116 | settings = _ref2.settings, 117 | filters = _ref2.filters, 118 | styles = _ref2.styles; 119 | if ((name || settings !== null && settings !== void 0 && settings.name) && settings) { 120 | blocks.registerBlockType(name || (settings === null || settings === void 0 ? void 0 : settings.name), settings); 121 | } 122 | if (filters && Array.isArray(filters)) { 123 | filters.forEach(function (_ref3) { 124 | var hook = _ref3.hook, 125 | namespace = _ref3.namespace, 126 | callback = _ref3.callback; 127 | hooks.addFilter(hook, namespace, callback); 128 | }); 129 | } 130 | if (styles && Array.isArray(styles)) { 131 | styles.forEach(function (style) { 132 | return blocks.registerBlockStyle(name, style); 133 | }); 134 | } 135 | }; 136 | 137 | /** 138 | * Unregister an updated or removed block, filters, or style variations. 139 | * 140 | * @param {Object} block The exported block module. 141 | * @param {String} block.name Block name. May be included in configuration object. 142 | * @param {Object} [block.settings] Optional block configuration object. 143 | * @param {Object[]} [block.filters] Optional array of filters to bind. 144 | * @param {Object[]} [block.styles] Optional array of block styles to bind. 145 | */ 146 | exports.registerBlock = registerBlock; 147 | var unregisterBlock = function unregisterBlock(_ref4) { 148 | var name = _ref4.name, 149 | settings = _ref4.settings, 150 | filters = _ref4.filters, 151 | styles = _ref4.styles; 152 | if ((name || settings !== null && settings !== void 0 && settings.name) && settings) { 153 | blocks.unregisterBlockType(name || (settings === null || settings === void 0 ? void 0 : settings.name)); 154 | } 155 | if (filters && Array.isArray(filters)) { 156 | filters.forEach(function (_ref5) { 157 | var hook = _ref5.hook, 158 | namespace = _ref5.namespace; 159 | hooks.removeFilter(hook, namespace); 160 | }); 161 | } 162 | if (styles && Array.isArray(styles)) { 163 | styles.forEach(function (style) { 164 | return blocks.unregisterBlockStyle(name, style.name); 165 | }); 166 | } 167 | }; 168 | 169 | /** 170 | * Store the selected block to persist selection across block-swaps. 171 | */ 172 | exports.unregisterBlock = unregisterBlock; 173 | var beforeUpdateBlocks = function beforeUpdateBlocks() { 174 | selectedBlockId = data.select('core/block-editor').getSelectedBlockClientId(); 175 | data.dispatch('core/block-editor').clearSelectedBlock(); 176 | }; 177 | 178 | /** 179 | * Trigger a re-render on all blocks which have changed. 180 | * 181 | * @param {Object[]} changed Array of changed module objects. 182 | */ 183 | exports.beforeUpdateBlocks = beforeUpdateBlocks; 184 | var afterUpdateBlocks = function afterUpdateBlocks() { 185 | var changed = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 186 | var changedNames = changed.map(function (module) { 187 | return module.name; 188 | }); 189 | if (!changedNames.length) { 190 | return; 191 | } 192 | 193 | // Refresh all blocks by iteratively selecting each one that has changed. 194 | data.select('core/block-editor').getBlocks().forEach(function (_ref6) { 195 | var name = _ref6.name, 196 | clientId = _ref6.clientId; 197 | if (changedNames.includes(name)) { 198 | data.dispatch('core/block-editor').selectBlock(clientId); 199 | } 200 | }); 201 | 202 | // Reselect whatever was selected in the beginning. 203 | if (selectedBlockId) { 204 | data.dispatch('core/block-editor').selectBlock(selectedBlockId); 205 | } else { 206 | data.dispatch('core/block-editor').clearSelectedBlock(); 207 | } 208 | selectedBlockId = null; 209 | }; 210 | 211 | /** 212 | * Require a set of blocks and configure them for hot module replacement. 213 | * 214 | * @see autoload 215 | * 216 | * @param {Object} options Configuration object defining callbacks. 217 | * @param {Function} options.getContext Execute and return a `require.context()` call. 218 | * @param {Function} options.[register] Function to register accepted blocks. 219 | * @param {Function} options.[unregister] Function to unregister replaced blocks. 220 | * @param {Function} options.[before] Function to run before updating blocks. 221 | * @param {Function} options.[after] Function to run after updating blocks. 222 | * @param {Function} [callback] A callback function which will be passed the 223 | * generated `context` object and `loadModules` 224 | * function, which can be used to opt-in to HMR. 225 | */ 226 | exports.afterUpdateBlocks = afterUpdateBlocks; 227 | var autoloadBlocks = function autoloadBlocks(_ref7, callback) { 228 | var getContext = _ref7.getContext, 229 | _ref7$register = _ref7.register, 230 | register = _ref7$register === void 0 ? registerBlock : _ref7$register, 231 | _ref7$unregister = _ref7.unregister, 232 | unregister = _ref7$unregister === void 0 ? unregisterBlock : _ref7$unregister, 233 | _ref7$before = _ref7.before, 234 | before = _ref7$before === void 0 ? beforeUpdateBlocks : _ref7$before, 235 | _ref7$after = _ref7.after, 236 | after = _ref7$after === void 0 ? afterUpdateBlocks : _ref7$after; 237 | autoload({ 238 | getContext: getContext, 239 | register: register, 240 | unregister: unregister, 241 | before: before, 242 | after: after 243 | }, callback); 244 | }; 245 | 246 | /** 247 | * Register a new or updated plugin. 248 | * 249 | * @param {Object} plugin The exported plugin module. 250 | * @param {String} plugin.name Plugin name. 251 | * @param {Object} plugin.settings Plugin configuration object. 252 | * @param {Object[]} [plugin.filters] Optional array of filters to bind. 253 | */ 254 | exports.autoloadBlocks = autoloadBlocks; 255 | var registerPlugin = function registerPlugin(_ref8) { 256 | var name = _ref8.name, 257 | settings = _ref8.settings, 258 | filters = _ref8.filters; 259 | if (name && settings) { 260 | plugins.registerPlugin(name, settings); 261 | } 262 | if (filters && Array.isArray(filters)) { 263 | filters.forEach(function (_ref9) { 264 | var hook = _ref9.hook, 265 | namespace = _ref9.namespace, 266 | callback = _ref9.callback; 267 | hooks.addFilter(hook, namespace, callback); 268 | }); 269 | } 270 | }; 271 | 272 | /** 273 | * Unregister an updated or removed plugin. 274 | * 275 | * @param {Object} plugin The exported plugin module. 276 | * @param {String} plugin.name Plugin name. 277 | * @param {Object} plugin.settings Plugin configuration object. 278 | * @param {Object[]} [plugin.filters] Optional array of filters to bind. 279 | */ 280 | exports.registerPlugin = registerPlugin; 281 | var unregisterPlugin = function unregisterPlugin(_ref10) { 282 | var name = _ref10.name, 283 | settings = _ref10.settings, 284 | filters = _ref10.filters; 285 | if (name && settings) { 286 | plugins.unregisterPlugin(name); 287 | } 288 | if (filters && Array.isArray(filters)) { 289 | filters.forEach(function (_ref11) { 290 | var hook = _ref11.hook, 291 | namespace = _ref11.namespace; 292 | hooks.removeFilter(hook, namespace); 293 | }); 294 | } 295 | }; 296 | 297 | /** 298 | * Require a set of plugins and configure them for hot module replacement. 299 | * 300 | * @see autoload 301 | * 302 | * @param {Object} options Configuration object defining callbacks. 303 | * @param {Function} options.getContext Execute and return a `require.context()` call. 304 | * @param {Function} options.[register] Function to register accepted plugins. 305 | * @param {Function} options.[unregister] Function to unregister replaced plugins. 306 | * @param {Function} options.[before] Function to run before updating plugins. 307 | * @param {Function} options.[after] Function to run after updating plugins. 308 | * @param {Function} [callback] A callback function which will be passed the 309 | * generated `context` object and `loadModules` 310 | * function, which can be used to opt-in to HMR. 311 | */ 312 | exports.unregisterPlugin = unregisterPlugin; 313 | var autoloadPlugins = function autoloadPlugins(_ref12, callback) { 314 | var getContext = _ref12.getContext, 315 | _ref12$register = _ref12.register, 316 | register = _ref12$register === void 0 ? registerPlugin : _ref12$register, 317 | _ref12$unregister = _ref12.unregister, 318 | unregister = _ref12$unregister === void 0 ? unregisterPlugin : _ref12$unregister, 319 | before = _ref12.before, 320 | after = _ref12.after; 321 | autoload({ 322 | getContext: getContext, 323 | register: register, 324 | unregister: unregister, 325 | before: before, 326 | after: after 327 | }, callback); 328 | }; 329 | 330 | /** 331 | * Register a new or updated format type 332 | * 333 | * @param {Object} format The exported format module. 334 | * @param {String} format.name Format type name. 335 | * @param {Object} format.settings Format type configuration object. 336 | */ 337 | exports.autoloadPlugins = autoloadPlugins; 338 | var registerFormat = function registerFormat(_ref13) { 339 | var name = _ref13.name, 340 | settings = _ref13.settings; 341 | if (name && settings) { 342 | richText.registerFormatType(name, settings); 343 | } 344 | }; 345 | 346 | /** 347 | * Unregister an updated or removed format type. 348 | * 349 | * @param {Object} format The exported format module. 350 | * @param {String} format.name Format type name. 351 | * @param {Object} format.settings Format type configuration object. 352 | */ 353 | exports.registerFormat = registerFormat; 354 | var unregisterFormat = function unregisterFormat(_ref14) { 355 | var name = _ref14.name, 356 | settings = _ref14.settings; 357 | if (name && settings) { 358 | richText.unregisterFormatType(name); 359 | } 360 | }; 361 | 362 | /** 363 | * Require a set of format types and configure them for hot module replacement. 364 | * 365 | * @see autoload 366 | * 367 | * @param {Object} options Configuration object defining callbacks. 368 | * @param {Function} options.getContext Execute and return a `require.context()` call. 369 | * @param {Function} options.[register] Function to register accepted formats. 370 | * @param {Function} options.[unregister] Function to unregister replaced formats. 371 | * @param {Function} options.[before] Function to run before updating formats. 372 | * @param {Function} options.[after] Function to run after updating formats. 373 | * @param {Function} [callback] A callback function which will be passed the 374 | * generated `context` object and `loadModules` 375 | * function, which can be used to opt-in to HMR. 376 | */ 377 | exports.unregisterFormat = unregisterFormat; 378 | var autoloadFormats = function autoloadFormats(_ref15, callback) { 379 | var getContext = _ref15.getContext, 380 | _ref15$register = _ref15.register, 381 | register = _ref15$register === void 0 ? registerFormat : _ref15$register, 382 | _ref15$unregister = _ref15.unregister, 383 | unregister = _ref15$unregister === void 0 ? unregisterFormat : _ref15$unregister, 384 | before = _ref15.before, 385 | after = _ref15.after; 386 | autoload({ 387 | getContext: getContext, 388 | register: register, 389 | unregister: unregister, 390 | before: before, 391 | after: after 392 | }, callback); 393 | }; 394 | 395 | /* eslint-disable no-underscore-dangle */ 396 | /* eslint-disable camelcase */ 397 | /** 398 | * Work around a full-page crash in WordPress 5.4 caused by a forced render of 399 | * the BlockListBlock component following the state dispatch triggered upon block 400 | * unregistration. 401 | * 402 | * This function filters the BlockListBlock component to wrap it in an error 403 | * boundary, which catches the error when BlockListBlock tries to access a 404 | * property on the removed block type, suppresses the error by returning null, 405 | * and then schedules the BlockListBlock to try rendering again on the next 406 | * tick (by which point our hot-swapped block type should be available again). 407 | */ 408 | exports.autoloadFormats = autoloadFormats; 409 | var _apply_wp_5_4_hmr_patch = function _apply_wp_5_4_hmr_patch() { 410 | /* eslint-enable */ 411 | var React = window.React; 412 | var Component = React.Component, 413 | Fragment = React.Fragment, 414 | createElement = React.createElement; 415 | hooks.addFilter('editor.BlockListBlock', 'block-editor-hmr/prevent-block-swapping-error', function (BlockListBlock) { 416 | var ErrorWrapper = /*#__PURE__*/function (_Component) { 417 | _inherits(ErrorWrapper, _Component); 418 | var _super = _createSuper(ErrorWrapper); 419 | function ErrorWrapper(props) { 420 | var _this; 421 | _classCallCheck(this, ErrorWrapper); 422 | _this = _super.call(this, props); 423 | _this.state = { 424 | hasError: false 425 | }; 426 | return _this; 427 | } 428 | 429 | // eslint-disable-next-line no-unused-vars 430 | _createClass(ErrorWrapper, [{ 431 | key: "componentDidUpdate", 432 | value: function componentDidUpdate(prevProps, prevState) { 433 | var _this2 = this; 434 | if (this.state.hasError && this.state.hasError !== prevState.hasError) { 435 | setTimeout(function () { 436 | _this2.setState({ 437 | hasError: false 438 | }); 439 | }); 440 | } 441 | } 442 | }, { 443 | key: "render", 444 | value: function render() { 445 | if (this.state.hasError) { 446 | return null; 447 | } 448 | return createElement(Fragment, null, createElement(BlockListBlock, this.props)); 449 | } 450 | }], [{ 451 | key: "getDerivedStateFromError", 452 | value: function getDerivedStateFromError(error) { 453 | return { 454 | hasError: true 455 | }; 456 | } 457 | }]); 458 | return ErrorWrapper; 459 | }(Component); 460 | return ErrorWrapper; 461 | }); 462 | }; 463 | exports._apply_wp_5_4_hmr_patch = _apply_wp_5_4_hmr_patch; 464 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Provide helper methods to dynamically locate, load & register Blocks & Plugins. 3 | */ 4 | const { 5 | blocks, 6 | plugins, 7 | richText, 8 | hooks, 9 | data, 10 | } = window.wp; 11 | 12 | /** 13 | * No-op function for use as a default argument value. 14 | */ 15 | const noop = () => {}; 16 | 17 | /** 18 | * Require a set of modules and configure them for hot module replacement. 19 | * The consuming function must opt-in to HMR by passing a callback to 20 | * accept updates for the context where this function is used. 21 | * 22 | * The first argument should be a function returning a `require.context()` 23 | * call. All modules loaded from this context are cached, and on each rebuild 24 | * the incoming updated modules are checked against the cache. Updated modules 25 | * which already exist in the cache are unregistered with the provided function, 26 | * then any incoming (new or updated) modules will be registered. 27 | * 28 | * @param {Object} options Configuration object defining callbacks. 29 | * @param {Function} options.getContext Execute and return a `require.context()` call. 30 | * @param {Function} options.register Function to register accepted modules. 31 | * @param {Function} options.unregister Function to unregister replaced modules. 32 | * @param {Function} options.[before] Function to run before updating modules. 33 | * @param {Function} options.[after] Function to run after updating modules. 34 | * @param {Function} [callback] A callback function which will be passed the 35 | * generated `context` object and `loadModules` 36 | * function, which can be used to opt-in to HMR. 37 | */ 38 | export const autoload = ( 39 | { 40 | getContext, 41 | register, 42 | unregister, 43 | before = noop, 44 | after = noop, 45 | }, 46 | callback = noop 47 | ) => { 48 | const cache = {}; 49 | const loadModules = () => { 50 | before(); 51 | 52 | const context = getContext(); 53 | const changed = []; 54 | context.keys().forEach( ( key ) => { 55 | const module = context( key ); 56 | if ( module === cache[ key ] ) { 57 | // Module unchanged: no further action needed. 58 | return; 59 | } 60 | const isHotUpdate = cache[ key ]; 61 | if ( isHotUpdate && console.groupCollapsed ) { 62 | console.groupCollapsed( `hot update: ${ key }` ); 63 | } 64 | if ( isHotUpdate ) { 65 | // Module changed, and prior copy detected: unregister old module. 66 | unregister( cache[ key ] ); 67 | } 68 | // Register new module and update cache. 69 | register( module ); 70 | changed.push( module ); 71 | cache[ key ] = module; 72 | 73 | if ( isHotUpdate && console.groupCollapsed ) { 74 | console.groupEnd(); 75 | } 76 | } ); 77 | 78 | after( changed ); 79 | 80 | // Return the context for HMR initialization. 81 | return context; 82 | }; 83 | 84 | const context = loadModules(); 85 | 86 | callback( context, loadModules ); 87 | }; 88 | 89 | // Maintain the selected block ID across HMR updates. 90 | let selectedBlockId = null; 91 | 92 | /** 93 | * Register a new or updated block, filters, or style variations. 94 | * 95 | * @param {Object} block The exported block module. 96 | * @param {String} block.name Block name. May be included in configuration object. 97 | * @param {Object} [block.settings] Optional block configuration object. 98 | * @param {Object[]} [block.filters] Optional array of filters to bind. 99 | * @param {Object[]} [block.styles] Optional array of block styles to bind. 100 | */ 101 | export const registerBlock = ( { name, settings, filters, styles } ) => { 102 | if ( ( name || settings?.name ) && settings ) { 103 | blocks.registerBlockType( ( name || settings?.name ), settings ); 104 | } 105 | 106 | if ( filters && Array.isArray( filters ) ) { 107 | filters.forEach( ( { hook, namespace, callback } ) => { 108 | hooks.addFilter( hook, namespace, callback ); 109 | } ); 110 | } 111 | 112 | if ( styles && Array.isArray( styles ) ) { 113 | styles.forEach( ( style ) => blocks.registerBlockStyle( name, style ) ); 114 | } 115 | }; 116 | 117 | /** 118 | * Unregister an updated or removed block, filters, or style variations. 119 | * 120 | * @param {Object} block The exported block module. 121 | * @param {String} block.name Block name. May be included in configuration object. 122 | * @param {Object} [block.settings] Optional block configuration object. 123 | * @param {Object[]} [block.filters] Optional array of filters to bind. 124 | * @param {Object[]} [block.styles] Optional array of block styles to bind. 125 | */ 126 | export const unregisterBlock = ( { name, settings, filters, styles } ) => { 127 | if ( ( name || settings?.name ) && settings ) { 128 | blocks.unregisterBlockType( ( name || settings?.name ) ); 129 | } 130 | 131 | if ( filters && Array.isArray( filters ) ) { 132 | filters.forEach( ( { hook, namespace } ) => { 133 | hooks.removeFilter( hook, namespace ); 134 | } ); 135 | } 136 | 137 | if ( styles && Array.isArray( styles ) ) { 138 | styles.forEach( ( style ) => blocks.unregisterBlockStyle( name, style.name ) ); 139 | } 140 | }; 141 | 142 | /** 143 | * Store the selected block to persist selection across block-swaps. 144 | */ 145 | export const beforeUpdateBlocks = () => { 146 | selectedBlockId = data.select( 'core/block-editor' ).getSelectedBlockClientId(); 147 | data.dispatch( 'core/block-editor' ).clearSelectedBlock(); 148 | }; 149 | 150 | /** 151 | * Trigger a re-render on all blocks which have changed. 152 | * 153 | * @param {Object[]} changed Array of changed module objects. 154 | */ 155 | export const afterUpdateBlocks = ( changed = [] ) => { 156 | const changedNames = changed.map( ( module ) => module.name ); 157 | 158 | if ( ! changedNames.length ) { 159 | return; 160 | } 161 | 162 | // Refresh all blocks by iteratively selecting each one that has changed. 163 | data.select( 'core/block-editor' ).getBlocks().forEach( ( { name, clientId } ) => { 164 | if ( changedNames.includes( name ) ) { 165 | data.dispatch( 'core/block-editor' ).selectBlock( clientId ); 166 | } 167 | } ); 168 | 169 | // Reselect whatever was selected in the beginning. 170 | if ( selectedBlockId ) { 171 | data.dispatch( 'core/block-editor' ).selectBlock( selectedBlockId ); 172 | } else { 173 | data.dispatch( 'core/block-editor' ).clearSelectedBlock(); 174 | } 175 | selectedBlockId = null; 176 | }; 177 | 178 | /** 179 | * Require a set of blocks and configure them for hot module replacement. 180 | * 181 | * @see autoload 182 | * 183 | * @param {Object} options Configuration object defining callbacks. 184 | * @param {Function} options.getContext Execute and return a `require.context()` call. 185 | * @param {Function} options.[register] Function to register accepted blocks. 186 | * @param {Function} options.[unregister] Function to unregister replaced blocks. 187 | * @param {Function} options.[before] Function to run before updating blocks. 188 | * @param {Function} options.[after] Function to run after updating blocks. 189 | * @param {Function} [callback] A callback function which will be passed the 190 | * generated `context` object and `loadModules` 191 | * function, which can be used to opt-in to HMR. 192 | */ 193 | export const autoloadBlocks = ( 194 | { 195 | getContext, 196 | register = registerBlock, 197 | unregister = unregisterBlock, 198 | before = beforeUpdateBlocks, 199 | after = afterUpdateBlocks, 200 | }, 201 | callback 202 | ) => { 203 | autoload( 204 | { 205 | getContext, 206 | register, 207 | unregister, 208 | before, 209 | after, 210 | }, 211 | callback 212 | ); 213 | }; 214 | 215 | /** 216 | * Register a new or updated plugin. 217 | * 218 | * @param {Object} plugin The exported plugin module. 219 | * @param {String} plugin.name Plugin name. 220 | * @param {Object} plugin.settings Plugin configuration object. 221 | * @param {Object[]} [plugin.filters] Optional array of filters to bind. 222 | */ 223 | export const registerPlugin = ( { name, settings, filters } ) => { 224 | if ( name && settings ) { 225 | plugins.registerPlugin( name, settings ); 226 | } 227 | 228 | if ( filters && Array.isArray( filters ) ) { 229 | filters.forEach( ( { hook, namespace, callback } ) => { 230 | hooks.addFilter( hook, namespace, callback ); 231 | } ); 232 | } 233 | }; 234 | 235 | /** 236 | * Unregister an updated or removed plugin. 237 | * 238 | * @param {Object} plugin The exported plugin module. 239 | * @param {String} plugin.name Plugin name. 240 | * @param {Object} plugin.settings Plugin configuration object. 241 | * @param {Object[]} [plugin.filters] Optional array of filters to bind. 242 | */ 243 | export const unregisterPlugin = ( { name, settings, filters } ) => { 244 | if ( name && settings ) { 245 | plugins.unregisterPlugin( name ); 246 | } 247 | 248 | if ( filters && Array.isArray( filters ) ) { 249 | filters.forEach( ( { hook, namespace } ) => { 250 | hooks.removeFilter( hook, namespace ); 251 | } ); 252 | } 253 | }; 254 | 255 | /** 256 | * Require a set of plugins and configure them for hot module replacement. 257 | * 258 | * @see autoload 259 | * 260 | * @param {Object} options Configuration object defining callbacks. 261 | * @param {Function} options.getContext Execute and return a `require.context()` call. 262 | * @param {Function} options.[register] Function to register accepted plugins. 263 | * @param {Function} options.[unregister] Function to unregister replaced plugins. 264 | * @param {Function} options.[before] Function to run before updating plugins. 265 | * @param {Function} options.[after] Function to run after updating plugins. 266 | * @param {Function} [callback] A callback function which will be passed the 267 | * generated `context` object and `loadModules` 268 | * function, which can be used to opt-in to HMR. 269 | */ 270 | export const autoloadPlugins = ( 271 | { 272 | getContext, 273 | register = registerPlugin, 274 | unregister = unregisterPlugin, 275 | before, 276 | after, 277 | }, 278 | callback 279 | ) => { 280 | autoload( 281 | { 282 | getContext, 283 | register, 284 | unregister, 285 | before, 286 | after, 287 | }, 288 | callback 289 | ); 290 | }; 291 | 292 | /** 293 | * Register a new or updated format type 294 | * 295 | * @param {Object} format The exported format module. 296 | * @param {String} format.name Format type name. 297 | * @param {Object} format.settings Format type configuration object. 298 | */ 299 | export const registerFormat = ( { name, settings } ) => { 300 | if ( name && settings ) { 301 | richText.registerFormatType( name, settings ); 302 | } 303 | }; 304 | 305 | /** 306 | * Unregister an updated or removed format type. 307 | * 308 | * @param {Object} format The exported format module. 309 | * @param {String} format.name Format type name. 310 | * @param {Object} format.settings Format type configuration object. 311 | */ 312 | export const unregisterFormat = ( { name, settings } ) => { 313 | if ( name && settings ) { 314 | richText.unregisterFormatType( name ); 315 | } 316 | }; 317 | 318 | /** 319 | * Require a set of format types and configure them for hot module replacement. 320 | * 321 | * @see autoload 322 | * 323 | * @param {Object} options Configuration object defining callbacks. 324 | * @param {Function} options.getContext Execute and return a `require.context()` call. 325 | * @param {Function} options.[register] Function to register accepted formats. 326 | * @param {Function} options.[unregister] Function to unregister replaced formats. 327 | * @param {Function} options.[before] Function to run before updating formats. 328 | * @param {Function} options.[after] Function to run after updating formats. 329 | * @param {Function} [callback] A callback function which will be passed the 330 | * generated `context` object and `loadModules` 331 | * function, which can be used to opt-in to HMR. 332 | */ 333 | export const autoloadFormats = ( 334 | { 335 | getContext, 336 | register = registerFormat, 337 | unregister = unregisterFormat, 338 | before, 339 | after, 340 | }, 341 | callback 342 | ) => { 343 | autoload( 344 | { 345 | getContext, 346 | register, 347 | unregister, 348 | before, 349 | after, 350 | }, 351 | callback 352 | ); 353 | }; 354 | 355 | /* eslint-disable no-underscore-dangle */ 356 | /* eslint-disable camelcase */ 357 | /** 358 | * Work around a full-page crash in WordPress 5.4 caused by a forced render of 359 | * the BlockListBlock component following the state dispatch triggered upon block 360 | * unregistration. 361 | * 362 | * This function filters the BlockListBlock component to wrap it in an error 363 | * boundary, which catches the error when BlockListBlock tries to access a 364 | * property on the removed block type, suppresses the error by returning null, 365 | * and then schedules the BlockListBlock to try rendering again on the next 366 | * tick (by which point our hot-swapped block type should be available again). 367 | */ 368 | export const _apply_wp_5_4_hmr_patch = () => { 369 | /* eslint-enable */ 370 | const React = window.React; 371 | const { Component, Fragment, createElement } = React; 372 | 373 | hooks.addFilter( 374 | 'editor.BlockListBlock', 375 | 'block-editor-hmr/prevent-block-swapping-error', 376 | ( BlockListBlock ) => { 377 | class ErrorWrapper extends Component { 378 | constructor( props ) { 379 | super( props ); 380 | this.state = { hasError: false }; 381 | } 382 | 383 | // eslint-disable-next-line no-unused-vars 384 | static getDerivedStateFromError( error ) { 385 | return { hasError: true }; 386 | } 387 | 388 | componentDidUpdate( prevProps, prevState ) { 389 | if ( this.state.hasError && this.state.hasError !== prevState.hasError ) { 390 | setTimeout( () => { 391 | this.setState( { hasError: false } ); 392 | } ); 393 | } 394 | } 395 | 396 | render() { 397 | if ( this.state.hasError ) { 398 | return null; 399 | } 400 | 401 | return createElement( 402 | Fragment, 403 | null, 404 | createElement( BlockListBlock, this.props ) 405 | ); 406 | } 407 | } 408 | 409 | return ErrorWrapper; 410 | } 411 | ); 412 | }; 413 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "block-editor-hmr", 3 | "version": "0.7.0", 4 | "description": "Utilities to autoload and hot-reload WordPress Block Editor modules.", 5 | "main": "build.js", 6 | "homepage": "https://github.com/kadamwhite/block-editor-hmr#readme", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/kadamwhite/block-editor-hmr.git" 10 | }, 11 | "keywords": [ 12 | "webpack", 13 | "hmr" 14 | ], 15 | "author": "K Adam White", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/kadamwhite/block-editor-hmr/issues" 19 | }, 20 | "browserslist": [ 21 | ">1%", 22 | "last 4 versions", 23 | "Firefox ESR", 24 | "not ie < 10" 25 | ], 26 | "scripts": { 27 | "prepublish": "npm run build", 28 | "build": "babel index.js --out-file build.js", 29 | "lint": "eslint index.js", 30 | "test": "echo \"Error: no test specified\" && exit 1" 31 | }, 32 | "devDependencies": { 33 | "@babel/cli": "^7.10.5", 34 | "@babel/core": "^7.10.5", 35 | "@babel/eslint-parser": "^7.21.8", 36 | "@babel/preset-env": "^7.10.4", 37 | "core-js": "^2.6.11", 38 | "eslint": "^8.40.0" 39 | } 40 | } 41 | --------------------------------------------------------------------------------