How to create multiple Gutenberg blocks in one plugin

I’ve updated this answer with more than one way to accomplish this. Though maybe it could go without saying the context of these is using the @wordpress/scripts package for development.

Single bundle for all blocks

This setup is most suitable for building some combination of blocks that have parent/child relationships (like Columns/Column) and thus one wouldn’t be used without the other.

Specify multiple entry points in the build and start scripts. Example with start:

"start": "wp-scripts start --entry ./hero/src/index.js ./call-to-action/src/index.js"

That will bundle the js into main.js as opposed to the default of index.js so the editorScript path in the block’s json will need updated. (Since a single bundle is output only one of the blocks’ json files will actually need to specify it).

Separate bundles per block

To have separate files output for each block there are two ways I’m aware of.

Provide your own webpack config file

This will end up being the most flexible option and can be quite simple. For the blocks in the question a config like the following should do:

const defaultConfig = require('@wordpress/scripts/config/webpack.config');

module.exports = {
    ...defaultConfig,
    entry: {
        'hero': './blocks/hero/src',
        'call-to-action': './blocks/call-to-action/src',
    },
};

Ryan Welcher has an example repo and a video walk-through of its creation (that link is to the part about creating the custom webpack config).

Specify multiple entry points in the build and start scripts without using (--entry)

This is the way the official docs currently demonstrate:

"build:custom": "wp-scripts build entry-one.js entry-two.js --output-path=custom"

A potential catch here is that with the latest versions of @wordpress/scripts (19.1.0 and probably back to 18) the filenames of each entry point must be unique or else files will be left out of the build. So for a project structure as exemplified in the question, it would fail to include both blocks because they’re both named index.js. In earlier versions of @wordpress/scripts both would be included although bundled into a single file. This behavior may change.