3

Is it possible to change the file set-up of @wordpress/create-block to work with multiple blocks?

I've been trying to tweak the files, but maybe it's not even possible. I can't find anything saying it is.

3
+50

It is! I've toyed with this off and on for the last year or so, and have come up with a few different ways to accomplish it. These are just the products of my own fiddling, however - there may well be more compelling solutions out there. Given the direction of @wordpress/scripts development, I would expect this use-case to become easier down the road.

NOTE: If you intend to submit your blocks for inclusion in the public Block Directory, (introduction) current guidelines specify that the plugin should provide only a single top-level block. Any additional blocks should be child blocks and specify their relationship with the top-level block via the parent field in their respective block.json files.


Background: The @wordpress/scripts Package

@wordpress/scripts is the abstraction of build and development tools which is used in plugins scaffolded by @wordpress/create-block in order to simplify JS transpilation, SCSS compilation, and linting code for errors, among other things. It includes babel, webpack, and eslint, to name a few.

The package is also useful as a development dependency in plugins and themes which do not offer blocks; the same configuration process below can be used to better adapt the build tools to your extension's needs and file structure.

The Default Build Configuration

The most common mechanism to change any aspect of how your plugin's assets are built is to tweak or replace the Webpack configuration provided by the @wordpress/scripts package, as is briefly mentioned in the README.

To get an idea of what the structure and settings of the default configuration object are we can refer to the source of wp-script's webpack.config.js file. The main settings that we are concerned with are the entry and output objects, which determine which files serve as entry points to your code and where their assets are compiled to, respectively:

// ...
    entry: {
        index: path.resolve( process.cwd(), 'src', 'index.js' ),
    },
    output: {
        filename: '[name].js',
        path: path.resolve( process.cwd(), 'build' ),
        jsonpFunction: getJsonpFunctionIdentifier(),
    },
// ...

Above, we can see that wp-scripts specifies a single entry-point named index located at ./src/index.js, and produces a JavaScript bundle for each entry-point at ./build/[name].js (where here, index would be substituted in for [name] for our single entry-point).

A number of other assets are produced by the various configured plugins and loaders as well.

Overriding the Webpack Configuration

Overriding the default webpack.config.js is simple - we just create a file of the same name in our project root, and wp-scripts will recognize and use it instead.

To modify the configuration, we can either import wp-scripts' Webpack configuration object, modify it, and export the modified object again - or export a new configuration object entirely to completely replace wp-scripts'.

NOTE: It is a popular convention to use the CommonJS module pattern and a less recent ECMAScript syntax when writing a Webpack configuration file as this file is not usually transpiled into a more global standard. This allows developers using less recent Node.js engines to build your code as well.


Solutions

File Structure

The majority of the following solutions assume a source structure containing two blocks (foo and bar) in a ./src/blocks directory alongside a non-block JS asset (./src/frontend/accordion.js), unless otherwise stated. Specifically, I've configured them for a project structure of my own preference:

@bosco's multiblock project structure

Copying Assets from Source Directories

In my solutions below I use the CopyWebpackPlugin in order to copy each block's block.json file to the output directory. This allows them to be used straight from the output directory using relative paths to assets which make sense in either context. As @wordpress/scripts is currently using Webpack 4 (though not for much longer), you will need version 6 of the plugin, for the time being:

npm install --save-dev copy-webpack-plugin@6

Alternatively, you can load your block.json files from ./src and use large relative paths to point at the built assets (e.g. "editorScript": "file:../../../build/blocks/foo/index.js"), or perhaps collect them in your project root, named as block-{block name}.json or similar.

Loading Blocks from the New Structure

For the most part, you can simply pass the filepath to each block's block.json file to a register_block_type_from_metadata() call in your root project's main plugin file.

PHP relevant to specific blocks including block registration could also be left in the block's source directory, and either imported straight from there or copied over to the output directory.

Multiple Block Projects Solution

The most simple solution results in a somewhat convoluted file structure and build process - to just use @wordpress/create-block to scaffold multiple block projects into your root project. After which, they can be registered by simply loading each block's plugin entry point or migrating all of the register_block_type()/register_block_type_from_metadata() calls into your project's main PHP entry point.

This arrangement directly lends well to monorepo practices such as those detailed in the link in @Luismi's answer as well as this LogRocket article.

Such an approach could also be combined with one of the solutions below in order to consolidate the individual block projects with a shared build process and output directory. This seems pretty compelling on paper, but I have not explored the possibility.

Multi-Config Solution (Most Reasonable ATM)

Webpack supports configuration files exporting an array of configuration objects, which allows you to provide an individual configuration to use for each block.

Unfortunately, since Node.js caches and re-uses module imports and since the default @wordpress/scripts configuration object contains various constructed objects and functions, using even a recursive copy of the object for each block has a potential for causing problems, as multiple Webpack compilations could end up re-using plugin instances which may have a dirty state from prior compilations.

I think that the best way to implement this may be to create a sort of "configuration factory function" which can be used to produce a new configuration object - basically copying and pasting the default configuration into a function.

As a sort of hacky alternative, deleting the default configuration from the Node.js module cache results in a brand new copy of the object each time it's require()'d. I'm not sure how good of a practice this is, however. I would trust the prior method to be more reliable and generally acceptable. Nonetheless, this does make things quite a bit easier:

/**
 * `@wordpress/scripts` multi-config multi-block Webpack configuration.
 * @see https://wordpress.stackexchange.com/questions/390282
 */

// Native Depedencies.
const path = require( 'path' );

// Third-Party Dependencies.
const CopyPlugin = require( 'copy-webpack-plugin' );

const default_config_path = require.resolve( '@wordpress/scripts/config/webpack.config.js' );

/**
 * Retrieves a new instance of `@wordpress/scripts`' default webpack configuration object.
 * @returns WebpackOptions
 */
const getBaseConfig = () => {
  // If the default config's already been imported, clear the module from the cache so that Node
  // will interpret the module file again and provide a brand new object.
  if( require.cache[ default_config_path ] )
    delete require.cache[ default_config_path ];

  // Import a new instance of the default configuration object.
  return require( default_config_path );
};

/**
 * @callback buildConfig~callback
 * @param {WebpackOptions} config An instance of `@wordpress/scripts`' default configuration object.
 * @returns WebpackOptions The modified or replaced configuration object.
 */

/**
 * Returns the result of executing a callback function provided with a new default configuration
 * instance.
 *
 * @param {buildConfig~callback} callback
 * @returns WebpackOptions The modified or replaced configuration object.
 */
const buildConfig = ( callback ) => callback( getBaseConfig() );

/**
 * Extends `@wordpress/scripts`'s default webpack config to build block sources from a common
 * `./src/blocks` directory and output built assets to a common `./build/blocks` directory.
 * 
 * @param {string} block_name 
 * @returns WebpackOptions A configuration object for this block.
 */
const buildBlockConfig = ( block_name ) => buildConfig(
  config => (
    { // Copy all properties from the base config into the new config, then override some.
      ...config,
      // Override the block's "index" entry point to be `./src/blocks/{block name}/index.js`.
      entry: {
        index: path.resolve( process.cwd(), 'src', 'blocks', block_name, 'index.js' ),
      },
      // This block's built assets should be output to `./build/blocks/{block name}/`.
      output: {
        ...config.output,
        path: path.resolve( config.output.path, 'blocks', block_name ),
      },
      // Add a CopyWebpackPlugin to copy over the `block.json` file.
      plugins: [
        ...config.plugins,
        new CopyPlugin(
          {
            patterns: [
              { from: `src/blocks/${block_name}/block.json` },
            ],
          }
        ),
      ]
    }
  )
);

module.exports = [
  buildBlockConfig( 'foo' ),
  buildBlockConfig( 'bar' ),
  // Setup a configuration to build `./src/frontend/accordion.js` to `./build/frontend/`
  buildConfig(
    config => (
      {
        ...config,
        entry: {
          accordion: path.resolve( process.cwd(), 'src', 'frontend', 'accordion.js' ),
        },
        output: {
          ...config.output,
          path: path.resolve( config.output.path, 'frontend' ),
        },
      }
    )
  )
];

Path-Based Entry Names Solution

This solution depends on a change to @wordpress/scripts which will not be available until the next release, (package version > 16.1.3). To use it now, you would need to install the package from GitHub. wp-scripts' impending upgrade to Webpack 5 should also facilitate this approach.

The most convenient solution in my opinion is simply to use partial paths as entry-point names:

/**
 * `@wordpress/scripts` path-based name multi-block Webpack configuration.
 * @see https://wordpress.stackexchange.com/questions/390282
 */

// Native Depedencies.
const path = require( 'path' );

// Third-Party Dependencies.
const CopyPlugin = require( 'copy-webpack-plugin' );
const config = require( '@wordpress/scripts/config/webpack.config.js' );

/**
 * Resolve a series of path parts relative to `./src`.
 * @param string[] path_parts An array of path parts.
 * @returns string A normalized path, relative to `./src`.
 **/
const resolveSource = ( ...path_parts ) => path.resolve( process.cwd(), 'src', ...path_parts );

/**
 * Resolve a block name to the path to it's main `index.js` entry-point.
 * @param string name The name of the block.
 * @returns string A normalized path to the block's entry-point file.
 **/
const resolveBlockEntry = ( name ) => resolveSource( 'blocks', name, 'index.js' );

config.entry = {
  'blocks/foo/index': resolveBlockEntry( 'foo' ),
  'blocks/bar/index': resolveBlockEntry( 'bar' ),
  'frontend/accordion': resolveSource( 'frontend', 'accordion.js' ),
};

// Add a CopyPlugin to copy over block.json files.
config.plugins.push(
  new CopyPlugin(
    {
      patterns: [
        {
          context: 'src',
          from: `blocks/*/block.json`
        },
      ],
    }
  )
);

module.exports = config;

This is something of a convenience in that it's simple, succinct, and maintainable... with a caveat.

Due to the way that @wordpress/scripts handles CSS/SCSS files name "style" and "style.module" in order to work around a Webpack 4 limitation, we need to modify how these files are named in order to make sure that the "style" assets end up in the same directory as the rest of the built assets. It's ugly, and I haven't thoroughly tested possible edge cases (in particular it might do some weird stuff if a "style" file produces multiple chunks) - but with any luck it won't be necessary in Webpack 5:

config.optimization.splitChunks.cacheGroups.style.name = ( module, chunks, group_key ) => {
  const delimeter = config.optimization.splitChunks.cacheGroups.style.automaticNameDelimiter;

  return chunks[0].name.replace(
    /(\/?)([^/]+?)$/,
    `$1${group_key}${delimeter}$2`
  );
};

Flat Output Solution

A very minimal configuration can facilitate compiling assets to a super ugly flat-file output structure:

/**
 * `@wordpress/scripts` flat output multi-block Webpack configuration.
 * @see https://wordpress.stackexchange.com/questions/390282
 */

// Native Depedencies.
const path = require( 'path' );

// Third-Party Dependencies.
const CopyPlugin = require( 'copy-webpack-plugin' );
const config = require( '@wordpress/scripts/config/webpack.config.js' );

config.entry = {
  'foo-block': path.resolve( process.cwd(), 'src', 'blocks', 'foo', 'index.js' ),
  'bar-block': path.resolve( process.cwd(), 'src', 'blocks', 'bar', 'index.js' ),
  'accordion': path.resolve( process.cwd(), 'src', 'frontend', 'accordion.js' ),
};

config.plugins.push(
  new CopyPlugin(
    {
      patterns: [
        {
          context: 'src/blocks',
          from: '*/block.json',
          to: ( { absoluteFilename } ) => `block-${absoluteFilename.match( /[\\/]([^\\/]+)[\\/]block.json/ )[1]}.json`,
        },
      ],
    }
  )
);

module.exports = config;

flat output

On paper one could set a function in config.output.filename in order to have this simple configuration produce a nested output structure again (replacing - in the entry-point name with /, or similar), but this too is not currently possible as a result of the FixWebpackStylePlugin's implementation.

5
  • 2
    I might make a GitHub repo demonstrating these solutions a little later in the week, if there's interest. Would maybe be a lot easier to process than my TED Talk of an answer here.
    – bosco
    Jun 21 at 20:58
  • I'm trying to figure out where I should place php related scripts (register_block_type) and how to configure webpack so that those php are copied to my build folder (or should those PHP files only be in my build folder and not in my src folder?) ; but this is an excellent start, thank you.
    – Will
    Jun 22 at 21:20
  • 1
    Personal preference, I think. For register_block_type()/register_block_type_from_metadata() calls in any solution except perhaps Multiple Block Projects (especially if creating a monorepo where each block should be capable of standing on it's own as an independent plugin), I would argue that registering the block type is not a function of the block type - so it doesn't really belong in the block's directory. I personally would perform all block registrations in a single function and hook it wherever the rest of the plugin's initialization routines live.
    – bosco
    Jun 22 at 21:43
  • 1
    As for the rest of the PHP... There's a pretty common convention in WP extensions to place PHP in a lib directory separate from the asset sources in src. But in the spirit of "component-based project organization," if some PHP functionality is strongly related to a specific block (like code related to the render_callback for a dynamic block, etc) I think it makes a good amount of sense for it to be contained alongside the rest of the block's sources. Both of the linked articles for the Multiple Block Project approach demo this to some extent.
    – bosco
    Jun 22 at 21:47
  • If you choose to intermingle code using the other solutions, you'd add a pattern to CopyPlugin to copy .php files over, e.g. { context: 'src', from: '**/*.php' }. It might make sense to drop the src directory altogether if you go the "component organization route." That, or set it up such that ./build ends up with the whole functional plugin instead of the project root... I think that would work really nicely with something like GitHub Actions/workflows. But I don't have a good example of an extension organized like this off the top of my head :/
    – bosco
    Jun 22 at 21:53
1

Found this article about this topic: https://www.designbombs.com/reusing-functionality-for-wordpress-plugins-with-blocks/

Currently, @wordpress/create-block can only tackle creating single-block plugins, not multi-block plugins (hopefully, in the not so distant future, it will be possible to generate any type of output through templates). However, with a bit of extra effort, we can already leverage @wordpress/create-block to create multi-block plugins too.

Hope this helps.

3
  • Were you able to get on with multi-blocks with that tutorial ?
    – Anoop D
    Sep 11 at 6:02
  • Hi @Anoop-D, not tried myself but combined with bosco's answer sounds a good starting point.
    – Luismi
    Sep 12 at 8:04
  • I tried it and it was not working ........ you can see my comment under that article still unanswered .
    – Anoop D
    Sep 13 at 2:26
0

I also found this article what helped me to create multiple Gutenberg Blocks

File structure and react setup when creating multiple Gutenberg blocks

In the Index.js, you are importing all your blocks and registering them

const registerblocks = (block) =>{

    const { name, settings} = block
  registerBlockType(name, settings)

}

And then you make a folder like /blocks/abcblock

and export the name and settings. Since your're registering them in the index.js file. Just like in the article

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.