Webpack: Split dynamic imported modules into separate chunks while keeping the library in the main bundle

created a simple github repo with configuration you need.

  1. SplitChunks configuration (change test and name functions to yours if you want):
    splitChunks: {
            cacheGroups: {
                widgets: {
                    test: module => {
                        return module.identifier().includes('src/widgets');
                    },
                    name: module => {
                        const list = module.identifier().split('/');
                        list.pop();
                        return list.pop();
                    },
                    chunks: 'async',
                    enforce: true
                }
            }
    }
  1. If you want PopularWidget to be in main, you should not import it dynamically. Use regular import statement. Because webpack will ALWAYS create a new chunk for any dynamic import. So please take a look at this file.
    import { Widget } from '../widgets/popular-widget';

    export class WidgetFactory {
        async create(name) {
            if (name === 'popular-widget') return await Promise.resolve(Widget);
            return await import(`../widgets/${name}/index`)
        }
    }

UPD
Changed configuration to keep related node_modules with widgets.
I wrote two simple functions for new widgets chunk creation.
The trick here is module.issuer property which means who imported this file. So function isRelativeToWidget will return true for any dependencies (node_modules or not) imported at any depth of widget's file structure.
Code can be found here.
Resulting configuration:

splitChunks: {
            chunks: 'all',
            cacheGroups: {
                vendors: false,
                default: false,
                edgeCaseWidget: getSplitChunksRuleForWidget('edge-case-widget'),
                interestingWidget: getSplitChunksRuleForWidget('interesting-widget')
            }
        }

function isRelativeToWidget(module, widgetName) {
    if (module.identifier().includes(widgetName)) return true;

    if (module.issuer) return isRelativeToWidget(module.issuer, widgetName)
}

function getSplitChunksRuleForWidget(widgetName) {
    return {
        test: module => isRelativeToWidget(module, widgetName),
        name: widgetName,
        chunks: 'async',
        enforce: true,
    }
}