176

I'm trying to do something that I believe should be possible, but I really can't understand how to do it just from the webpack documentation.

I am writing a JavaScript library with several modules that may or not depend on each other. On top of that, jQuery is used by all modules and some of them may need jQuery plugins. This library will then be used on several different websites which may require some or all modules.

Defining the dependencies between my modules was very easy, but defining their third-party dependencies seems to be harder then I expected.

What I would like to achieve: for each app I want to have two bundle files one with the necessary third-party dependencies and other with the necessary modules from my library.

Example: Let's imagine that my library has the following modules:

  • a (requires: jquery, jquery.plugin1)
  • b (requires: jquery, a)
  • c (requires: jquery, jquery.ui, a, b)
  • d (requires: jquery, jquery.plugin2, a)

And I have an app (see it as a unique entry file) that requires modules a, b and c. Webpack for this case should generate the following files:

  • vendor bundle: with jquery, jquery.plugin1 and jquery.ui;
  • website bundle: with modules a, b and c;

In the end, I would prefer to have jQuery as a global so I don't need to require it on every single file (I could require it only on the main file, for example). And jQuery plugins would just extend the $ global in case they are required (it is not a problem if they are available to other modules that don't need them).

Assuming this is possible, what would be an example of a webpack configuration file for this case? I tried several combinations of loaders, externals, and plugins on my configuration file, but I don't really get what they are doing and which ones should I use. Thank you!

bensampaio
  • 3,266
  • 5
  • 21
  • 19

4 Answers4

141

in my webpack.config.js (Version 1,2,3) file, I have

function isExternal(module) {
  var context = module.context;

  if (typeof context !== 'string') {
    return false;
  }

  return context.indexOf('node_modules') !== -1;
}

in my plugins array

plugins: [
  new CommonsChunkPlugin({
    name: 'vendors',
    minChunks: function(module) {
      return isExternal(module);
    }
  }),
  // Other plugins
]

Now I have a file that only adds 3rd party libs to one file as required.

If you want get more granular where you separate your vendors and entry point files:

plugins: [
  new CommonsChunkPlugin({
    name: 'common',
    minChunks: function(module, count) {
      return !isExternal(module) && count >= 2; // adjustable
    }
  }),
  new CommonsChunkPlugin({
    name: 'vendors',
    chunks: ['common'],
    // or if you have an key value object for your entries
    // chunks: Object.keys(entry).concat('common')
    minChunks: function(module) {
      return isExternal(module);
    }
  })
]

Note that the order of the plugins matters a lot.

Also, this is going to change in version 4. When that's official, I update this answer.

Update: indexOf search change for windows users

Rafael De Leon
  • 1,580
  • 1
  • 12
  • 11
  • 1
    I don't know if this was already possible when I posted my question, but this is indeed what I was looking for. With this solution I no longer need to specify my vendor entry chunk. Thanks a lot! – bensampaio Aug 18 '16 at 14:24
  • 1
    `isExternal` in `minChunks` made my day. How is this not documented? There's downsides? – Wesley Schleumer de Góes Aug 22 '16 at 12:05
  • Thx, but change userRequest.indexOf('/node_modules/') to userRequest.indexOf('node_modules') for windows pathes – Kinjeiro Sep 19 '16 at 14:16
  • @WesleySchleumerdeGóes it is documented but without example `options.minChunks (number|Infinity|function(module, count) -> boolean):` I am not seeing a downside yet. – Rafael De Leon Oct 04 '16 at 16:44
  • Been searching for a solution to split app and vendor resources _that are actually being used in application scripts_ for ages. All other examples used package.json to read the installed libraries, which would include them entirely instead of only including the required parts. Thank you! – Sebastiaan Luca Nov 21 '16 at 02:25
  • @RafaelDeLeon any chance of elaborating on the magick in the more-granular solution please? The output of both is precisely the same in my case. – The Bearded Llama Nov 29 '16 at 15:41
  • 2
    This will not work when using loaders, as the path of the loader will also be in `module.userRequest` (and the loader is probably in `node_modules`). My code for `isExternal()`: `return typeof module.userRequest === 'string' && !!module.userRequest.split('!').pop().match(/(node_modules|bower_components|libraries)/);` – cdauth Dec 07 '16 at 17:05
  • @TheBeardedLlama that example is for the case where you have multiple entry points and want your entry points to share a common file, but also want to separate your vendor code from your entry and common file. – Rafael De Leon Jan 15 '17 at 04:08
  • @cdauth would function isExternal(module) { var userRequest = module.context || module.userRequest; if (typeof userRequest !== 'string') { return false; } return userRequest.indexOf('bower_components') >= 0 || userRequest.indexOf('node_modules') >= 0 || userRequest.indexOf('libraries') >= 0; } work better? – Rafael De Leon Jan 15 '17 at 04:08
  • How is `.userRequest` different than `.resource`, which is used in the [examples](https://webpack.js.org/plugins/commons-chunk-plugin/#passing-the-minchunks-property-a-function) in the docs? – Dimitris Karagiannis Apr 15 '17 at 15:14
  • `userRequest` was for webpack 1 and `resource` wasn't available at the time I gave this answer. Webpack 2 gives more information through the `module` param. Follow the webpack 2 docs since that's getting updated. I don't plan to update this response since I encourage looking and contributing there. – Rafael De Leon Apr 17 '17 at 16:22
  • 1
    Could you say something about the WebPack 4 handling, or mention that you will not do it and give a link in the answer? – Samuel Åslund May 27 '20 at 07:58
56

I am not sure if I fully understand your problem but since I had similar issue recently I will try to help you out.

Vendor bundle.

You should use CommonsChunkPlugin for that. in the configuration you specify the name of the chunk (e.g. vendor), and file name that will be generated (vendor.js).

new webpack.optimize.CommonsChunkPlugin("vendor", "vendor.js", Infinity),

Now important part, you have to now specify what does it mean vendor library and you do that in an entry section. One one more item to entry list with the same name as the name of the newly declared chunk (i.e. 'vendor' in this case). The value of that entry should be the list of all the modules that you want to move to vendor bundle. in your case it should look something like:

entry: {
    app: 'entry.js',
    vendor: ['jquery', 'jquery.plugin1']
}

JQuery as global

Had the same problem and solved it with ProvidePlugin. here you are not defining global object but kind of shurtcuts to modules. i.e. you can configure it like that:

new webpack.ProvidePlugin({
    $: "jquery"
})

And now you can just use $ anywhere in your code - webpack will automatically convert that to

require('jquery')

I hope it helped. you can also look at my webpack configuration file that is here

I love webpack, but I agree that the documentation is not the nicest one in the world... but hey.. people were saying same thing about Angular documentation in the begining :)


Edit:

To have entrypoint-specific vendor chunks just use CommonsChunkPlugins multiple times:

new webpack.optimize.CommonsChunkPlugin("vendor-page1", "vendor-page1.js", Infinity),
new webpack.optimize.CommonsChunkPlugin("vendor-page2", "vendor-page2.js", Infinity),

and then declare different extenral libraries for different files:

entry: {
    page1: ['entry.js'],
    page2: ['entry2.js'],
    "vendor-page1": [
        'lodash'
    ],
    "vendor-page2": [
        'jquery'
    ]
},

If some libraries are overlapping (and for most of them) between entry points then you can extract them to common file using same plugin just with different configuration. See this example.

Michał Margiel
  • 2,912
  • 4
  • 20
  • 19
  • Thank you a lot for your reply. This was the best approach I saw until now but unfortunately it still doesn't solve my problem... I tested your example and the vendor.js file will still contain all the code from 'jquery' and 'jquery.plugin1' even if they are not required by any of my modules. This means in the end they will always be loaded to the browser. If I have lots of jquery plugins this will result in a very big file even if only half of them are used. Is there no way to include 'jquery.plugin1' in the vendor bundle only if it is required? – bensampaio May 20 '15 at 12:08
  • thanks, so I have learnt something also :) I have updated my answer with creation of multiple vendor-chunks. maybe now it will better suits you. – Michał Margiel May 20 '15 at 14:18
  • 4
    The problem with this solution is that it assumes I know what are the dependencies for each page. But I cannot predict that... jQuery should only be included on a vendor bundle if it is required by one of the modules used in the page. By specifying that on the config file it will always be in the vendor bundle even if not required by any module used in the page, right? Basically, I cannot predict the contents of vendor bundles, otherwise I'll have a hell of a work because I don't have just 2 pages I have hundreds... Do you get the problem? Any ideas? :) – bensampaio May 26 '15 at 07:49
  • I understand what you are saying but I dont see that as a problem. If you use new library in a page, then just add it to a vendor library lists for that page. It is just few characters. Anyway in your solution you have to do it by specifying loader.If you do not know which pages will use your newly created module - then let CommonChuncks plugin to extract common libraries from your modules automatically. – Michał Margiel May 27 '15 at 09:09
  • How can I set context separately for vendor files? – harshes53 Jan 08 '16 at 14:57
  • This solved my problem. I initially saw the same results as Anakin (thirdparty code in vendor.js) but then I realized I had not set the minChunks parameter to Infinity per your example. Changed that and now my vendor file ONLY contains what I expected it to. – Thad Peiffer Feb 11 '16 at 02:31
  • is this still the best way to do this? – SuperUberDuper Aug 30 '16 at 01:07
44

In case you're interested in bundling automatically your scripts separately from vendors ones:

var webpack = require('webpack'),
    pkg     = require('./package.json'),  //loads npm config file
    html    = require('html-webpack-plugin');

module.exports = {
  context : __dirname + '/app',
  entry   : {
    app     : __dirname + '/app/index.js',
    vendor  : Object.keys(pkg.dependencies) //get npm vendors deps from config
  },
  output  : {
    path      : __dirname + '/dist',
    filename  : 'app.min-[hash:6].js'
  },
  plugins: [
    //Finally add this line to bundle the vendor code separately
    new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.min-[hash:6].js'),
    new html({template : __dirname + '/app/index.html'})
  ]
};

You can read more about this feature in official documentation.

Freezystem
  • 4,494
  • 1
  • 32
  • 35
  • 4
    Please note that `vendor : Object.keys(pkg.dependencies)` does not always work and is dependent on how the package is built. – markyph Jul 13 '16 at 08:39
  • 2
    You're always depending on how your `package.json` is set. This workaround works in most case but there are exceptions where you'll have to take a different path. Could be interesting to post your own answer to the question to help the community. – Freezystem Jul 13 '16 at 08:48
  • 19
    I like this. It made me pee a little. – cgatian Jul 27 '16 at 19:20
  • 3
    note that it will even include packages that you might not even being using at all in your code... due to `Object.keys(pkg.dependencies)` will bundle everything !!!! let says you have a bunch of loaders listed there... yeah that will be included!!! so take care... separate carrefully what is devDependency and what is dependency – Rafael Milewski Oct 18 '17 at 05:13
  • 1
    @RafaelMilewski why would you have loaders in `dependencies` ? – Pants Mar 30 '20 at 16:16
  • @Pants generally I install everything as -D I don't make any distinction between dependencies and devDependencies unless I'm making some open-source lib that will live on npm and others will install it.. but other than this I keep everything on a single place in my package JSON – Rafael Milewski Mar 31 '20 at 16:21
13

Also not sure if I fully understand your case, but here is config snippet to create separate vendor chunks for each of your bundles:

entry: {
  bundle1: './build/bundles/bundle1.js',
  bundle2: './build/bundles/bundle2.js',
  'vendor-bundle1': [
    'react',
    'react-router'
  ],
  'vendor-bundle2': [
    'react',
    'react-router',
    'flummox',
    'immutable'
  ]
},

plugins: [
  new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor-bundle1',
    chunks: ['bundle1'],
    filename: 'vendor-bundle1.js',
    minChunks: Infinity
  }),
  new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor-bundle2',
    chunks: ['bundle2'],
    filename: 'vendor-bundle2-whatever.js',
    minChunks: Infinity
  }),
]

And link to CommonsChunkPlugin docs: http://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin

Alex Fedoseev
  • 1,135
  • 11
  • 18
  • I believe the problem with this solution is the same as the one provided by Michal. You are assuming I know the vendor dependencies for bundle1 and bundle2, but I don't... Imagine you have 200 bundles would you want to specify all that on the config file? Using your example, `react` should only be present in the vendor bundle if it is explicitly required by bundle1 and bundl2. I shouldn't have to specify that on the config file... Does this make sense? Any ideas? – bensampaio May 26 '15 at 07:54
  • @Anakin the question is why do you want to bundles 200 vendors tool into a separate file. I would only bundles common tools into a separate file, and keep the rest with the project bundles. – maxisam Mar 10 '16 at 19:09
  • @Anakin I think I'm dealing with the same issue, correct me if I'm wrong? http://stackoverflow.com/questions/35944067/how-to-access-modules-bundled-from-webpack-outside-bundles-chunks – pjdicke Mar 11 '16 at 16:13