The story of how we changed the project build from grunt to webpack
You come to work, you open IDE, you write
npm start
, starting the build system, you start working. It is convenient for you to navigate the structure of the project, it is convenient to debug the code and styles, obviously, exactly how and in what order the project is going.
It takes two years. In the development process, you periodically think about where to put the files with the new module correctly, how to deal with shared resources, and it is not always straightforward to answer the Junior's question “how does this file get into the bundle at all?”. Or you answer the sacred “so historically” and yearn for what was two years ago.
As it turned out, this happens if you do not upgrade the assembly system with the growth of the project. The good news is that this is successfully treated! In the summer we confirmed this in battle and want to share our experience.
')

initial situation
We have been developing the office application package MoyOffice since 2013, the web version (which will be discussed later) - since 2014.
There are several related projects (file manager, authorization and profile, web-editor of documents) with common sub-repositories, each of which is a SPA-part of a large application MyOffice. Development is carried out on
angular 1.5
, jenkins is used for continuous integration.
The original grunt build system, consisting of complex interdependent tasks, was created at the dawn of the project and has changed little since then. To indicate the scale: the dev-build launched about 30 grunt-tasks, 30% of which collected modules and styles, 70% - shifted images and fonts, updating the links to them. The order of execution was crucial, however, information about interdependence could only be obtained from colleagues.
Why migrate and why webpack
Collecting an angular project is actually not so difficult: you just need to combine all the source files into one, not forgetting that the module was announced before its controller. We made it even easier: we collected all the files from the src folder (using `_` at the beginning of the file name to ensure the correct connection order), added an array of external packages, and then connected the files directly to the head (of course, only for the dev-build, for production code was concatenated into a bundle followed by obfuscation and minification.

The assembly is obviously morally obsolete. The latest critical changes were dated 2015, which in the conditions of the modern frontend can be equated to a web-covered corner, in which, frankly, unfashionable grunt stores its intermediate files.
Plus, she had only one: the reassembly of the project was conditionally free due to the direct connection of files to the head.
Cons are much more:
- Collecting files by mask, we could not develop independent plug-ins.
- The number of HTTP requests in the dev mode was measured in the hundreds, and the page reload time in this mode was several seconds longer than in the compiled application.
- Because of the connection by
*.js
mask, unused modules got into the project. - When you add a new js-file, you had to restart the entire assembly.
- To connect third-party dependencies, we stored a separate json with module names.
- Grunt forced us to create a large number of intermediate and config files, due to which our
.hgignore
contained more than 50 lines.
And the more our project expanded, the more strongly the shortcomings of the build system interfered.
Taking a step back, looking at ourselves, at others, at the trends, recalling the experience of previous projects, we chose a webpack that effectively solves the problems described above.
Work organization

The main secret of successful refactoring is to clearly define the steps in advance and make a plan.
- Make a list of all requirements.
- Implement the proof of concept on a small project site. The idea is to collect all possible rakes cheaply and in the background, without risks for the main development.
- To make a complete transition, taking into account all the fine points identified in paragraph 2. Knowing all the problems and having experience transferring part of the code to new rails, one can fairly accurately estimate the labor intensity.
Reverse engineering requirements
The new system should not only solve the existing problems of assembly, but also support several new and long-desired features (and of course, not lose the old ones). Compiled a list of what should be able to webpack in a finished form:
- Incremental build .
- Watch mode.
- Source map support (by flag).
- Minification (by flag).
- Hot module replacement.
- Babel support.
- Dead code elimination.
- Split vendor code and our two bundle.
- Add hash to file names.
In this case, grunt cannot be excluded from the process, since it is responsible for assembling styles, working with images and fonts, and generating documentation. For consistency, we even want to run the webpack via grunt, and not via npm-task, in order not to change the command to build the project at all and not to reconfigure anything to CI.
Proof of concept
At the mercy was given one of our applications - SPA, responsible for all manipulations with authentication and user profile. At the end of the work with him could be taken for the rest.
In a good way, all the work was divided into three parts:
1. Create a config for webpack.
2. Prepare files for such an assembly. File Formats:
- html
- js,
- css,
- media (pictures and fonts),
- a large number of configs stored in
json
and integrated into the assembly somewhere in the middle.
3. Rewrite unit tests.
Css along with the media temporarily postponed, since they are not integrated into angular and can continue to live their lives.
js modules
For those who have not looked into angular for a long time, let us recall how it looks from the inside:
The main thing that disturbs us in the case of the webpack: all dependencies are indicated simply by the string-name of the required module. To build a dependency graph in a webpack, you must explicitly specify which file to include.
Over time, such a plan was formed:
By using es6
spread syntax, we were able to elegantly avoid duplicating the name of the module when declaring a component.
Since the dependency connection format changed critically, it was not possible to touch common sub-repositories within the POC framework in order not to hook on other projects. Therefore, all common files had to be connected manually by a long-long list.
HTML templates
Templates are divided into two categories:
index.html
and all others. Collecting
index.html
easy with the
html-webpack-plugin . Everyone else used to do
grunt-ng-template . I had to search for webpack-plugin to work with templates. There were only two requirements for it:
- So that all the templates mentioned in the modules immediately fall into $ templateCache.
- So that all internal template connections (ng-include) are also processed.
The first item was
easy to handle, and the second one had problems. Until now, there is no suitable solution, and although it is easy to write it, it was faster for us to connect all such templates in js. In the future we want to develop a webpack-loader for this purpose. If you have already written one yourself, share the github link with us in the comments.
With an hit in
$templateCache
interesting nuance: if you do
require
within a directive or controller, then it will try to add itself to the cache only in runtime, without getting into the bundle in advance. With the advent of
angular components, this was corrected; in other places, templates had to be connected before the controllers were announced.
In order to easily detect missed template connections, we added a
webpack-dev-middleware
in
webpack-dev-middleware
that prohibits downloading any nested
html
.
function blockLocalTemplatesMiddleware(req, res, next) { var urlPath = parseUrl(req).pathname; if (/[^\/]+\/[^\/]+\.html$/g.test(urlPath)) { res.statusCode = 404; res.end('Request to .html template denied'); } else { next(); } }
Configs
Each of our projects has configurations that are sewn into the project at the assembly stage. Previously, all configs were stored in several json-files,
grunt-ng-constant wrapped them in an angular-module and connected to the project at the assembly stage, reducing the transparency of reading and debugging. Using
DefinePlugin made it much more convenient and easier.
Unit tests
- In order not to slow down the test build, ignore-loader was used to connect everything except js.
- In unit tests I had to directly contact angular.mock because of the webpack .
- Tests using
angular.element
began to fall massively. Breaking our head notably, we remembered that angular.element
uses jQuery, but does not pull it with us, so the library should be connected separately in karma.config.js
.
Final migration
Three weeks of careful POC later, we were ready for the final migration of the entire application.

Work with css
We migrated in June-July, thought about the article in August, resolutely started writing in December, and during this time we already had time to get used to the convenience of the modular structure and decided to transfer the sass-style assembly to a webpack.
And although this article initially assumed the story only about the first stage of migration, we cannot but share the experience of giving up
grunt-sass
.
The process of such migration is in fact quite trivial: just connect all the necessary styles in the modules where they are used. However, without pitfalls also not done.
How did the build work before? Like building js modules. According to the mask, all
*.scss
were collected and imported in one file. Then
sass
worked on it alone, all mixins and helpers connected once were available everywhere, there were practically no cross-imports.
To implement the modular structure, we started importing variables, mixins,
node-bourbon
, lunaparks, blackjack in each style file. Because of this, two troubles happened:
- Due to the lack of
import-once
(there was no special need before), our final .css
so swollen that IE (in Chrome, Firefox, and even Safari, of course, didn’t have such problems) could not parse them. That is, the page was loaded, the .css
file was loaded, but to realize that it is full of styles, IE was not able to. This issue was resolved by simply adding import-once
. sass-loader
, which does not have an incremental build, rebuilt the project every time, and because of the abundance of entry points and imports into them, spent about 5 seconds rebuilding. Dealing with this without changing the architecture was not possible.
However, the update to the recently released
node-sass@4.0.0
accelerated the reassembly by about 1.5 times, and we decided to postpone the massive processing of styles.
Debugging and Testing
The main thing in development is not to write, but to debug, according to the results of debugging, we have compiled a cheat sheet for those who decide to repeat the migration path (by the way, it doesn’t matter where to get off - with gulp or grunt). Basically, all the defects encountered on the way and their diagnostics looked like:
- “Nothing is going to, in the IDE console is full of letters”: we are trying to connect a dependency that does not exist (wrong path or incorrect export).
- “Gathered, but nothing works, there are very long errors in the browser console”: the module lacks dependency. In the old assembly, there was no such problem, because all the modules got into the assembly and you can easily not mention the necessary dependency without getting any side effects. Now, the files that are not mentioned anywhere in the bundle are not included.
- “Everything was loaded, but when you click or click - it drops”: there are three options to choose from — the previous one, the lack of a template in
$templateCache
or forgot to add webworker to the assembly. - “The application does not look like this”: due to a change in the order of styles, we still for a long time found small defects caused by the initial calculation for a certain (alphabetical) order of connecting files.
Separate manual testing from the QA-team in this task was not required, it was easy enough to run autotests. The only thing we asked testers to check is that jenkins is successfully and correctly built with all possible flags.
Technical points
There are a lot of possibilities to optimize the process of working angular using a webpack. On github, you can find dozens of loaders (curiously, from the moment we completed the migration to the day when this paragraph was started, there were already some plug-ins that we lacked then). However, a third of them do not have documentation and contain only minified code, so it is not possible to use them, the second third works ambiguously (for example, there are three loaders for templates, they do the same thing, and we have earned correctly only one).
Inevitable difficulties
- In this section we will tell about the difficulties we encountered on the way of working with the selected tools.
- It is inconvenient to export the name of the module, it is not clear how to solve this problem on angular 1.x.
- You can not connect the necessary dependency and remain uncatchable if it is used in another module. This reveals unit tests that run in isolation, but the overall trend does not seem healthy.
- Some external angular modules of dependencies do not have exports, this causes suffering and reduces transparency.
For example:
require('ng-file-upload'); angular.module('app', ['ngFileUpload'])
You can cope with this with the help of a pull-requests battery on github, we sometimes send them in spare time.
- If you write modules in the format of the export function, you must mention
@ngInject
. If this is not done, the minified version does not work, and this situation cannot be tracked by linders. - Webpack, it turns out, panics, when it has two ways to otrezolvit file.
For example, the project structure is as follows:
├── src │ └── module.js └── common └── src └── module.js
If you change the file, change detection cannot work correctly and the project is completely unpredictable, it is re-assembled, then no. Since we had several input points with the same internal structure, we had to abandon it.
- Not all minification methods work 100% the same. We used to use grunt-contrib-uglify , now we have switched to UglifyJsPlugin . Despite the same settings, during the transition, a problem arose with the fact that one of the libraries began to consider Russian characters in HTML templates to be insecure and turned them into double-shielded HTML entities. Such cases do not lend themselves to a logical explanation, but illustrate the benefits of frequent testing of the code compiled with the settings used for production.
Unexpected Benefits
- We always created two bundles - the code of third-party libraries and ours. With webpack, sharing occurs through CommonsChunksPlugin . At first, we kept two entry points, on which we used ommonsChunkPlugin, but found an excellent tricky solution.
new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', chunks: ['app'], filename: 'vendor.[hash].js', minChunks: function minChunks(module) { return module.resource && module.resource.indexOf('node_modules') > 0; } })
Why divide the code into two parts? To use a
DLL , speeding up rebuilding. Plus, with fairly frequent releases (and we strive to increase their frequency) the list of dependencies does not have time to change, while maintaining the same hash. This allows the user not to download an extra file, but simply to take it from the browser's cache.
- Using opensource libraries, we are obliged to specify the names of their authors. With webpack, it has become very convenient to collect this information with the help of license-webpack-plugin , which is oriented along the path to the plug-in.
Did not go to work
Automatic loading of modules
Of course, we didn’t really want to rewrite all module dependencies from strings to
require
. It would be cool to fasten the
loader
, which would analyze the code and itself substitute the necessary
require
!
However, such an approach requires a strict project structure so that it is possible to unambiguously match the path to the module with its name. In fact, the string with the module name would uniquely transform into the file path. At the time of the migration, the project structure did not have this approach, and the reorganization of files would take no less time and threaten a large number of conflicts when merging branches.
Now we are starting the path to strict organization of the source code and, when we are finished, we will be able to take advantage of such opportunities. Although this, most likely, it does not want to, because going through
ctrl-click
immediately to the dependent module is extremely convenient.
Hot module replacement
Unfortunately, we had to refuse HMR for js-code. There are
two plug-ins , but both of them require not only a very strict project structure, but also an exact export format, and also work only with controllers, but not with directives. Even with a suitable structure, using the update only for a part of the code is completely inconvenient. However, for styles HMR works correctly.
Tips for the past
The migration process went quite smoothly and step by step, however, as it usually happens, having completed the work, we figured out how to facilitate it:
- Instead of manually replacing all string dependency names with
require()
, it is easier to write a one-time nodejs script that analyzes the current code base and replaces the module names on the way to them. - Unverified advice! Perhaps it makes sense to first rewrite the code using browserify , and then attach the webpack to it, so as not to figure out exactly what of the tons of changes made, the problem is in the wrong paths of the included files or in the collector itself.
About numbers
The most interesting is, of course, the numbers. Interesting logs on tasks:

Re-and underestimations take place, however, in general, we managed to predict the labor intensity rather accurately. The key to this success, we believe a clear wording of the requirements prior to the implementation of the task. The experience of previous and subsequent mass refactorings confirms this: if we take on the task, planning to make a list of requirements in the development process, forgetting something important is easier than easy.
Assembly time
On the old build, when detecting changes in js, the page started reloading right away. If changes were made to the styles, rebuilding css took about 3.5 seconds.
After moving reassembly occurs in 5 seconds regardless of where the changes were made.The loading time of the page in the dev-version on the old build took about 1.5 seconds due to the large number of js-files connected. After the transition to the webpack, it was reduced to 0.8 seconds. When you change styles, both then and now, no reload is required.Thus, the following data is obtained. The table shows the time from making changes to applying them on the page:
findings

Minuses:
- time from change to page reload increased
Pros:
- project scalability has grown - now add a new plugin or loader (connect babel or postcss) is much easier
- transition to a modular structure has finally become possible
- easy to navigate by module dependencies using ctr + click
- no extra files get into the bundle
- It has become more convenient to collect information about third-party licenses and separate the opensource code from our
- when adding new files do not need to restart the entire assembly again
- got rid of a long, tangled list of grunt-tasok, replacing it with a list of webpack-plug-ins, which are much more convenient to use
- you can switch from branch to branch without restarting a working assembly
Future plans:- speed up the build
- learn how to collect assets used in styles using postcss plugins, and the rest using webpacks
- take steps to maintain the HMR for any changes
In general, it became easier to navigate the project, the entry threshold for a new employee became lower, refactoring is more accessible, dependency tracking is more convenient. Now you can develop individual modules and not be afraid that part of the code or css will fall into a common bundle.It would be a shame to read such a long article and not get a bonus at the end! We put ready for you configs for webpack and karma !