How to prevent moment.js from loading locales with webpack?
Asked Answered
T

8

219

Is there any way you can stop moment.js from loading all the locales (I just need English) when you're using webpack? I'm looking at the source and it seems that if hasModule is defined, which it is for webpack, then it always tries to require() every locale. I'm pretty sure this needs a pull request to fix. But is there any way we can fix this with the webpack config?

Here is my webpack config to load momentjs:

resolve: {
            alias: {
                moment: path.join(__dirname, "src/lib/bower/moment/moment.js")
            },
        },

Then anywhere I need it, I just do require('moment'). This works but it's adding about 250 kB of unneeded language files to my bundle. Also I'm using the bower version of momentjs and gulp.

Also if this can't be fixed by the webpack config here is a link to the function where it loads the locales. I tried adding && module.exports.loadLocales to the if statement but I guess webpack doesn't actually work in a way where that would work. It just requires no matter what. I think it uses a regex now so I don't really know how you would even go about fixing it.

Topee answered 19/8, 2014 at 13:13 Comment(2)
Have you tried to use moment via nmp instead of bower?Stepheniestephens
I'm using bower for all my client libs, and npm for all my build tools. I want to keep it this way because of how my projects are laid out. Also if you look at the last reply of github.com/moment/moment/issues/1866 I solved my own problem but it requires a minor source edit. I still don't know how to fix this the right way as I dont know how you would distinguish between node and webpack.Topee
L
322

The code require('./locale/' + name) can use every file in the locale dir. So webpack includes every file as module in your bundle. It cannot know which language you are using.

There are two plugins that are useful to give webpack more information about which module should be included in your bundle: ContextReplacementPlugin and IgnorePlugin.

require('./locale/' + name) is called a context (a require which contains an expression). webpack infers some information from this code fragment: A directory and a regular expression. Here: directory = ".../moment/locale" regular expression = /^.*$/. So by default every file in the locale directory is included.

The ContextReplacementPlugin allows to override the inferred information i.e. provide a new regular expression (to choose the languages you want to include).

Another approach is to ignore the require with the IgnorePlugin.

Here is an example:

var webpack = require("webpack");
module.exports = {
  // ...
  plugins: [
    new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /de|fr|hu/)
    // new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
  ]
};
Limbourg answered 21/8, 2014 at 12:9 Comment(9)
Can you explain how to use the ignore loader? I tried ` new webpackreg.IgnorePlugin(/\.\/locale\/.+.js$/, [])` but that didn't work. Also the contextReplacementPlugin still included the files in my bundle I just think it wasn't using them.Topee
You can take a look at this issue (github.com/webpack/webpack/issues/198) which contains a detailed discussion about moment+webpack.Limbourg
Thank you I think new webpack.IgnorePlugin(/^\.\/lang$/, /moment$/) from your comment on github will work.Topee
In the webpack docs, the second argument is an array of regexes. I tried plugins: [ new webpack.IgnorePlugin(/^\.\/locale$/, [/moment$/]) ], which worked just fine.Triquetrous
@AlexKinnee The bracket notation in the docs means it's an optional argument, not an array.Iambic
You're right. Not sure why it would have worked when I tried it.Triquetrous
This will load all of the locales for that language (ex: French, French (Canada), French (Switzerland)). You can tweak the regex to exclude those if you want like so fr(%|\.js)Deicide
In Webpack 2, #25384860Connective
I was getting errors trying IgnorePlugin as it seems the syntax has changed: new webpack.IgnorePlugin({ resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/, }); Source: webpack.js.org/plugins/ignore-pluginDavena
K
9

In our project, I include moment like this: import moment from 'moment/src/moment'; and that seems to do the trick. Our usage of moment is very simple though, so I'm not sure if there will be any inconsistencies with the SDK. I think this works because WebPack doesn't know how to find the locale files statically, so you get a warning (that's easy to hide by adding an empty folder at moment/src/lib/locale/locale) but no locale includes.

Kandacekandahar answered 11/5, 2016 at 20:22 Comment(1)
Not sure guys how this one is working for you... I just was tried to fight that github.com/angular/angular-cli/issues/6137 and then just ended up by using github.com/ksloan/moment-mini. The proper modular moment library will come up with Version 3 github.com/moment/moment/milestone/15 at some point.Respite
R
5

UPDATE: 2021
There are many other libs that you may want to checkout:

ORIGINAL ANSWER:
Seems like the proper modular moment library will never come up However, I just ended up of using https://github.com/ksloan/moment-mini like import * as moment from 'moment-mini';

Respite answered 2/5, 2017 at 0:36 Comment(2)
does moment-mini provides pipes of angular ?Gefell
@Gefell check this out github.com/urish/ngx-momentRespite
T
4

As of moment 2.18, all locales are bundled together with the core library (see this GitHub issue).

The resourceRegExp parameter passed to IgnorePlugin is not tested against the resolved file names or absolute module names being imported or required, but rather against the string passed to require or import within the source code where the import is taking place. For example, if you're trying to exclude node_modules/moment/locale/*.js, this won't work:

new webpack.IgnorePlugin({ resourceRegExp: /moment\/locale\// });

Rather, because moment imports with this code:

require('./locale/' + name);

your first regexp must match that './locale/' string. The second contextRegExp parameter is then used to select specific directories from where the import took place. The following will cause those locale files to be ignored:

plugins:[
    new webpack.IgnorePlugin({
      resourceRegExp: /^\.\/locale$/,
      contextRegExp: /moment$/,
    }),
]

which means "any require statement matching './locale' from any directories ending with 'moment' will be ignored.

Towline answered 13/9, 2022 at 17:57 Comment(0)
H
3

Based on Adam McCrmick's answer, you were close, change your alias to:

resolve: {
    alias: {
        moment: 'moment/src/moment'
    },
},
Hoffmann answered 20/10, 2016 at 3:6 Comment(3)
Just a word of warning, that this will change the behavior for all of the modules you include and could cause interesting issues with third-party libraries. It should work in general thoughKandacekandahar
Can you elaborate ? I'm not experienced with all alias use cases. In my understanding, it'd only matter if you touched that module, in this case 'moment'Hoffmann
When you set resolve aliases, they apply to any usage of import or require in your system (including libraries you depend on). So if a module you depend on required moment, you would be changing the result for that module as well. This comes up if you happen to set an alias that conflicts with a node module in your dependency tree (like "events" for example if your dependencies use that library). In practice, I have only had an issue with name conflicts, not behavior refinements like this, but this is a more dangerous approach than changing one import statement.Kandacekandahar
C
2

With webpack2 and recent versions of moment you can do:

import {fn as moment} from 'moment'

And then in webpack.config.js you do:

resolve: {
    packageMains: ['jsnext:main', 'main']
}
Cam answered 30/12, 2016 at 17:18 Comment(2)
I guess you mean mainFields: ...Loginov
Look at webpack2, pretty sure that field name changed.Cam
K
2

Here's another solution using postinstall script in NPM installer.

You can put a line to your package.json file:

{
  "scripts": {
    ...
    "postinstall": "find node_modules/moment/locale -type f -not -name 'en-gb.js' -not -name 'pl.js' -printf '%p\\n' | xargs rm"
    ...
  }
}

In result unwanted locales will be removed immediately after npm install finish installing packages.

In my case only en-gb and pl locales will remain in bundle.

In case you already have postinstall script, you can add script to existing commands:

{
  "scripts": {
    ...
    "postinstall": "previous_command && find node_modules/moment/locale -type f -not -name 'en-gb.js' -not -name 'pl.js' -printf '%p\\n' | xargs rm"
    ...
  }
}
Kibler answered 8/9, 2020 at 20:2 Comment(0)
S
0

For the web and if you are going to use very few locales of moment.You can choose this solution. Use the externals configuration. Besides, we need to add the moment library and specific locales script to the HTML template.

Pros:

  • Increase the speed of webpack compilation.
  • Decrease the bundle size.
  • Don't need require('moment/locale/zh-tw') statement. (V.S. webpack.IgnorePlugin solution)

Cons:

  • Each locale script has to be added manually.
  • The locale scripts are always downloaded from CDN even if you don't use them in your code. This means there are more HTTP requests.

The following example only use zh-tw locale of moment.

webpack.config.js:

const HTMLWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  mode: 'production',
  entry: './src/index.js',
  externals: {
    moment: 'moment'
  },
  plugins: [
    new HTMLWebpackPlugin({
      template: './src/index.html'
    }),
  ]
};

src/index.html:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/locale/zh-tw.min.js"></script>
</body>
</html>

src/index.js:

import moment from 'moment';
moment.locale('zh-tw');

console.log(moment().fromNow())
Schurman answered 7/8, 2023 at 8:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.