webpack-dev-server icon indicating copy to clipboard operation
webpack-dev-server copied to clipboard

Dev server breaks library entry type

Open DanielStout5 opened this issue 3 years ago • 13 comments

Bug report

We have an app with two entries - the main one for the app, and a second entry for a library. Both should be served up by the dev server, but the hot reloading breaks the library one.

Actual Behavior

Bottom of app entry file:

/******/ 	__webpack_require__.O(undefined, ["chunk-vendors"], function() { return __webpack_require__("./node_modules/whatwg-fetch/fetch.js"); })
/******/ 	__webpack_require__.O(undefined, ["chunk-vendors"], function() { return __webpack_require__("./node_modules/webpack-dev-server/client/index.js?protocol=wss&hostname=192.168.0.109&port=8081&pathname=%2Fws&logging=none&reconnect=10"); })
/******/ 	__webpack_require__.O(undefined, ["chunk-vendors"], function() { return __webpack_require__("./node_modules/webpack/hot/dev-server.js"); })
/******/ 	var __webpack_exports__ = __webpack_require__.O(undefined, ["chunk-vendors"], function() { return __webpack_require__("./src/main.js"); })
/******/ 	__webpack_exports__ = __webpack_require__.O(__webpack_exports__);

Bottom of library entry file:

/******/ 	__webpack_require__.O(undefined, ["chunk-vendors"], function() { return __webpack_require__("./node_modules/whatwg-fetch/fetch.js"); })
/******/ 	__webpack_require__.O(undefined, ["chunk-vendors"], function() { return __webpack_require__("./node_modules/webpack-dev-server/client/index.js?protocol=wss&hostname=192.168.0.109&port=8081&pathname=%2Fws&logging=none&reconnect=10"); })
/******/ 	__webpack_require__.O(undefined, ["chunk-vendors"], function() { return __webpack_require__("./node_modules/webpack/hot/dev-server.js"); })
/******/ 	var __webpack_exports__ = __webpack_require__.O(undefined, ["chunk-vendors"], function() { return __webpack_require__("./src/library.js"); })
/******/ 	__webpack_exports__ = __webpack_require__.O(__webpack_exports__);
/******/ 	MyLibrary = __webpack_exports__;

The value for MyLibrary ends up being undefined.

Expected Behavior

MyLibrary should be the exported module, not undefined.

It does work in release mode, but not in dev mode when webpack-dev-server is in use.

How Do We Reproduce?

  • Create a project with multiple entries, one of which is a library (var output)
  • Run it in dev mode
  • Here's an example reproduction: https://github.com/DanielStout5/WebpackLibraryHMRIssue

Please paste the results of npx webpack-cli info here, and mention other relevant information

System: OS: Windows 10 10.0.19044 CPU: (16) x64 11th Gen Intel(R) Core(TM) i7-11700F @ 2.50GHz Memory: 9.00 GB / 31.73 GB Binaries: Node: 14.18.1 - C:\Program Files\nodejs\node.EXE npm: 6.14.15 - C:\Program Files\nodejs\npm.CMD Browsers: Edge: Spartan (44.19041.1266.0), Chromium (101.0.1210.53)
Internet Explorer: 11.0.19041.1566 Packages: webpack-bundle-analyzer: 4.5.0 => 4.5.0 webpack-dev-server: 4.9.1 => 4.9.1

I've looked at:

  • https://github.com/webpack/webpack-dev-server/issues/2484 (says to upgrade to webpack-dev-server 4, which I'm above)
  • https://github.com/webpack/webpack-dev-server/issues/2692 (about module federation, not exactly what I'm doing - and this one is still open)
  • https://github.com/webpack/webpack/issues/12793 (Also says fixed in wds 4)

I don't really care about having hot reloading for the library. If I could somehow disable that while keeping it for the main app, that would be acceptable.

DanielStout5 avatar May 31 '22 17:05 DanielStout5

How Do We Reproduce?

Please provide more infromation, I think you have a problem in configuration

alexander-akait avatar May 31 '22 18:05 alexander-akait

Here's a sample project demonstrating the issue: https://github.com/DanielStout5/WebpackLibraryHMRIssue

It looks like it might be caused by the combination of the dev server and optimization.splitChunks. The project in question above is actually a Vue one and the Vue-CLI adds a default splitChunks config that specifies a chunk-vendors cache group. Before I added the optimization config, the library export actually did function correctly.

DanielStout5 avatar May 31 '22 19:05 DanielStout5

Please try to set https://webpack.js.org/configuration/optimization/#optimizationruntimechunk, true should be enough

alexander-akait avatar Jun 01 '22 20:06 alexander-akait

That configuration is set by Vue-CLI.

My workaround was to delete the optimization section entirely when running in dev mode:

    if (process.env.NODE_ENV == "development") {
      delete config.optimization;
    }

DanielStout5 avatar Jun 02 '22 12:06 DanielStout5

I think something wrong on vue-cli side in configuration or default configuration is not for you, you have library and application compilation using entries, it is exotic usage, most of developers move library compilation to own compiler (look at multi compiler mode for webpack), also it will allow to reduce time of build and cache

alexander-akait avatar Jun 02 '22 16:06 alexander-akait

Yeah I figured it was a bit of an unusual configuration/setup.

Is webpack-dev-server not expected to work with optimization turned on? Because disabling the optimization does fix the issue that seems to be caused by webpack-dev-server.

Failing that, is there a way to exclude webpack-dev-server from affecting a certain entry?

DanielStout5 avatar Jun 02 '22 16:06 DanielStout5

@DanielStout5 In theory it shoudl work, but if you have non official plugins it can be a problem. You can disable runtime chunks using client: false and add client chunk for required entries (the same approach used for electron applications)

alexander-akait avatar Jun 02 '22 17:06 alexander-akait

Hmm.. Specifying client: false (and hot: false) doesn't seem to prevent all the dev-server additions.

E.g.:

devServer: {
  client: false,
  hot: false
},

That does remove the hot/dev-server line but the webpack-dev-server one with the websocket url is still there:

/******/ 	__webpack_require__("./node_modules/whatwg-fetch/fetch.js");
/******/ 	__webpack_require__("./node_modules/webpack-dev-server/client/index.js?protocol=wss&hostname=192.168.0.109&port=8081&pathname=%2Fws&logging=none&reconnect=10");

DanielStout5 avatar Jun 03 '22 12:06 DanielStout5

Set hot: false too

alexander-akait avatar Jun 03 '22 14:06 alexander-akait

No dice unfortunately. With both client and hot set to false, it still adds the __webpack_require__("./node_modules/webpack-dev-server... line

DanielStout5 avatar Jun 03 '22 14:06 DanielStout5

@DanielStout5 webpack-dev-server injects entries here https://github.com/webpack/webpack-dev-server/blob/master/lib/Server.js#L612 (and below) (there are three entries, two of them are always inject based on options), as you can see no magic here, try to disable these options

alexander-akait avatar Jun 04 '22 13:06 alexander-akait

@alexander-akait thanks for that link, that was helpful.

I found a very hacky workaround that seems to work. It's a little more complicated because this project is using the Vue CLI and not just plain webpack by itself.

Set webSocketServer to false to prevent adding the additional entries in initialize:

  devServer: {
    webSocketServer: false,

I used the setupMiddlewares callback to restore HMR (normally added in initialize) and set the webSocketServer option back to its default value so createServer will get called later:

    setupMiddlewares(middlewares, devServer) {
      let hmr = new devServer.compiler.webpack.HotModuleReplacementPlugin();
      hmr.apply(devServer.compiler);

      devServer.options.webSocketServer = {
        type: "ws",
        options: { path: "/ws" },
      };
      return middlewares;
    },

And to add the WDS entries only to the app and not my library entrypoint I hardcoded the paths:

    if (process.env.NODE_ENV == "development") {
      appEntries.unshift(
        "./node_modules/webpack-dev-server/client/index.js?protocol=wss&hostname=0.0.0.0&port=8081&pathname=%2Fws&logging=none&reconnect=10"
      );
      appEntries.unshift("./node_modules/webpack/hot/dev-server.js");
    }

Thanks again for your help!

DanielStout5 avatar Jun 14 '22 13:06 DanielStout5

Yes, you can do it, we added setupMiddlewares for such edge cases, I don't think we will break this API in future, it is very simple and flexibility

alexander-akait avatar Jun 14 '22 19:06 alexander-akait

I had this exact problem. Upgrading to the following latest versions solved it for me

I'm wondering if one other person can replicate this, and then maybe this issue can be closed.

"webpack-dev-server": "^4.11.1",
"webpack": "^5.74.0",
"webpack-cli": "4.10.0"

The relevant portion of my webpack.config.js looks like below. (FYI I'm using webpack with Rails, so the convention is to read from app/frontend/* and output to public/packs/*)

const ASSETS_DIR = path.resolve(__dirname, 'app/frontend');
const PACKS_DIR = path.resolve(__dirname, 'public/packs');
const PUBLIC_DIR = path.resolve(__dirname, 'public');
const PUBLIC_PATH = '/packs/';

module.exports = {
  output: {
    filename: '[name]-[fullhash].js',
    chunkFilename: '[name]-[chunkhash].chunk.js',
    path: PACKS_DIR,
    publicPath: PUBLIC_PATH,
    pathinfo: true,
    library: {
      // Expose each "pack" below as `MyLibrary.<pack>`
      // The value will be the `default` export from each file listed as the
      // `entry...import` below.
      name: ['MyLibrary', '[name]'],
      type: 'var',
      export: 'default'
    }
  },
  devtool: 'cheap-module-source-map',
  devServer: {
    client: {
      logging: 'none'
    },
    compress: true,
    host: 'localhost',
    port: 3035,
    https: false,
    hot: true,
    historyApiFallback: {
      disableDotRule: true
    },
    headers: {
      'Access-Control-Allow-Origin': '*'
    },
    static: {
      directory: PUBLIC_DIR,
      watch: {
        ignored: '/node_modules/'
      }
    }
  },
  entry: {
    admin: {
      import: `${ASSETS_DIR}/javascript/packs/admin.js`,
      dependOn: 'common'
    },
    auth: {
      import: `${ASSETS_DIR}/javascript/packs/auth.js`,
      dependOn: 'common'
    },
    common: `${ASSETS_DIR}/javascript/packs/common.js`
  }
}

abhchand avatar Oct 22 '22 02:10 abhchand

Great! I want to close the issue, and marked as solved based on answers above, feel free to feedback

alexander-akait avatar Oct 22 '22 02:10 alexander-akait

@DanielStout5 I have a similar use case to this, where we are running Docusaurus (webpack + WDS) for our library. I am compiling and including our library as a separate entrypoint (UMD) so that we are effectively building BOTH the docs + library. The reason we'd want to do this is because for embedding live playgrounds where we want our local library code to be reflected (not our npm package). This makes local DX much nicer so when updating docs/compiled examples, it's all synced.

I am ALMOST there, where I can fetch and get the contents of our library.js file when running locally, but WDS is injecting its HMR + dev-server stuff which breaks the UMD module.

I tried your solution but I'm confused on the last bit because it's missing some context.

Where is appEntries in your code? Where would I do that similar logic?

For Docusaurus, we can do something like this:

configureWebpack(config, isServer, util) {

  return {
   devServer: {
     // your middleware code
   }
  }
}

Multi compiler?

most of developers move library compilation to own compiler (look at multi compiler mode for webpack), also it will allow to reduce time of build and cache

I did try this as well by patching multi-config support into my local Docusaurus packages. This still resulted in WDS+HMR code being injected into the bundle because the Docusaurus config comes first and uses that devServer config.

I felt like I was closer, the bundle was cleaner, but still ran into the same problem with the UMD module just not being executed on load.

kamranayub avatar Nov 20 '23 05:11 kamranayub

@kamranayub appEntries is just a list of entrypoints I defined:

const appEntries = ["./src/main.js"];

Then I used them like so:

config.entry = () => {
      return {
        app: appEntries,
        tenant: { // alternate non-app config here },
      };
    };

My particular use case was in the context of a Vue CLI application, which does a bunch of webpack config by default that I needed to only apply conditionally which would probably be unnecessary for other use cases

DanielStout5 avatar Nov 21 '23 12:11 DanielStout5