Skip to content

dependency-extraction-webpack-plugin should avoid including unused dependencies #66387

@anomiex

Description

@anomiex

What problem does this address?

Consider a project with the following files:

package.json

{
	"type": "module",
	"dependencies": {
		"@wordpress/dependency-extraction-webpack-plugin": "^6.10.0",
		"@wordpress/url": "^4.10.0",
		"webpack": "^5.95.0",
		"webpack-cli": "^5.1.4"
	}
}

webpack.config.cjs

const DependencyExtractionWebpackPlugin = require( '@wordpress/dependency-extraction-webpack-plugin' );

module.exports = {
    mode: 'production',
    plugins: [ new DependencyExtractionWebpackPlugin() ],
}

src/index.js

import { hello } from './utils.js';

hello();

src/utils.js

import { buildQueryString } from '@wordpress/url';

export function hello() {
    console.log( 'hello, world' );
}

export function makeUrl( query ) {
    return 'https://example.org/?' + buildQueryString( query );
}

If you build this project (e.g. npm install && npm exec webpack), you get the following files as output

dist/main.js

(()=>{"use strict";window.wp.url,console.log("hello, world")})();

dist/main.asset.php

<?php return array('dependencies' => array('wp-url'), 'version' => 'e7734f2fdec823e92bd0');

Note that, even though the use of @wordpress/url was optimized out (and @wordpress/url declares sideEffects: false in its package.json), it's still depended on by the bundle.

What is your proposed solution?

Unfortunately I don't have a perfect solution. Webpack doesn't currently provide a way for externals to indicate whether they have side effects or not. The closest issue related to that seems to be webpack/webpack#15486.

It turns out that Webpack 5's Module class declares a method getSideEffectsConnectionState which is used by the builtin SideEffectsFlagPlugin plugin and other places to remove side-effect-free modules. For ExternalModule this always returns true (i.e. "has side effects"), so external modules are never removed. For normal modules, SideEffectsFlagPlugin plugin examines the sideEffects in package.json and the module's code to determine what it should return.

We can't directly affect the ExternalModule instances as they're created by ExternalsPlugin (no hooks, unfortunately). But we could potentially do something like this:

  1. Change this.externalizedDeps into a Map.
  2. When externalizeWpDeps externalizes a dep, do like this to record it (note getResolve and context are part of the webpack.ExternalItemFunctionData argument to the function):
    getResolve()( context, request, ( err, result, resolveRequest ) => {
        // Don't care about an error, just save the undefined.
        this.externalizedDeps.set( request, resolveRequest?.descriptionFileData?.sideEffects );
        callback( null, externalRequest );
    } );
  3. Then add this hook to munge the ExternalModule instances just before they start being checked:
    compilation.hooks.optimizeDependencies.tap(
        {
            name: this.constructor.name,
            before: 'SideEffectsFlagPlugin',
        },
        modules => {
            for ( const module of modules ) {
                if ( this.externalizedDeps.get( module.userRequest ) === false ) {
                    module.getSideEffectsConnectionState = () => false;
                }
            }
        }
    );

If that seems reasonable, let me know and I'll put together a PR. Or feel free to do so yourself if you'd rather or if you have a better idea.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions