webpack: commonjs Modules Do Not Work Correctly With externalsType of 'promise'

Bug report

What is the current behavior?

EDIT: As per this comment below this behavior seems to be isolated to commonjs modules (and possibly other non-esm modules).

When using externalsType: 'promise', all known used exports get wrapped in an async function, and their externals dependencies are awaited. Modules marked as __unused_webpack_module do not get wrapped with async, and do not await their dependencies.

This can be resolved by the upstream dependency doing a different thing in their package.json and exports, but it seems unreasonable to expect that to be the case for all dependencies. Quite often upstream dependencies do have exports that are marked as unused, even if they actually are used. These exports will break when externalsType is set to promise.

If the current behavior is a bug, please provide the steps to reproduce.

  1. Pull down this repo: https://github.com/stevematney/webpack-promise-externals-bug-example
  2. Run npm install && npm start
  3. Open localhost:9000 in a browser.
  4. Because we are not also externalizing react-dom, our build does not run.

Notice in the resulting webpack bundle we see react-dom defined with this pattern:

        /***/
        "./node_modules/react-dom/cjs/react-dom.development.js": /*!*************************************************************!*\
  !*** ./node_modules/react-dom/cjs/react-dom.development.js ***!
  \*************************************************************/
        /***/
        ((__unused_webpack_module,exports,__webpack_require__)=>{

            "use strict";
            /** @license React v17.0.2 ... */

Whereas our base module is defined with this pattern:

 /***/
        "./src/index.js": /*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
        /***/
        ((module,__webpack_exports__,__webpack_require__)=>{

            "use strict";
            __webpack_require__.a(module, async(__webpack_handle_async_dependencies__,__webpack_async_result__)=>{

See that react-dom is missing the async setup. When it loads __webpack_require__('react'), it gets a Promise which it does not await, thus breaking when it looks for ReactSharedInternals.ReactCurrentDispatcher. ReactSharedInternals gets defined as undefined instead of the property of the react dependency that it should be.

What is the expected behavior?

All modules should correctly resolve external dependencies using the async setup pattern, whether or not these modules are unused.

Other relevant information:

webpack version: 5.75.0 Node.js version: 16.18.1 Operating System: macOS Ventura - 13.0.1 (22A400)

About this issue

  • Original URL
  • State: open
  • Created 2 years ago
  • Comments: 24 (11 by maintainers)

Commits related to this issue

Most upvoted comments

Just an update: we worked around this issue in our plugin by using a special loader which gets applied if any of the parents of an external are non-esm. The loader preloads the externals into a synchronous object before running the original entry code. The downstream dependencies are overwritten to reference this synchronous object instead of the Promised externals.

Is it possible to reopen this without opening a new issue? This is definitely still a thing we are working around.

Yeah, I see … to be honest I don’t see a universal solution here… anyway give me time to think about it, hard solution is copy/paste externals plugin code and change code (but yes, I undestand, it is an unwanted solution)

hm, do you want to fix it or undestand why it doesn’t work with commonjs?

Solution - use externals as a function:

externals: async function foo({ context, request, contextInfo, getResolve }) {
    if (request === "react") {
     if  (contextInfo.issuer.includes("CommonJsTest.js")) {
       return "React";
     }

      return "Promise.resolve(React)";
    }

    if (request === "react-dom") {
      return "Promise.resolve(ReactDOM)";
    }

    if (request === "@material-ui/core") {
      return "Promise.resolve(MaterialUI)";
    }

    return Promise.resolve();
  },

It happens because you say webpack to wrap react in Promise.resolve(...), and module is commonjs format, due this you need to use await in soure code in this case (you can enable topLevelAwait), also you can look at source code and found that you don’t have __webpack_handle_async_dependencies__ in commonjs

You can also use:

externals: {
   react: "promise React",
   "react-dom": "promise ReactDOM",
   "@material-ui/core": "promise MaterialUI",
},