Description
We recently noticed that repeated runs of our webpack build do not always produce the same compiled javascript. After much digging, we eventually traced it back to this plugin.
The problem is related to the order in which webpack invokes the nmf.hooks.afterResolve
callback this plugin registers (the callback fires in different orders based on when filesystem reads finish). I put together a simple reproducible example over in https://github.com/jfly/2019-07-lodash-webpack-plugin-nondeterminism. With the following 3 files:
index.js
import './a';
import './b';
a.js
import _ from 'lodash';
function duplicate(n) {
return [n, n];
}
_.flatMap([1, 2], duplicate);
b.js
import _ from 'lodash';
let b = _.omit({'magickey': 1}, 'magickey');
console.log(b);
It will invoke webpack two times, once with no delays on filesytems reads, and once with a 1 second delay before returning the contents of a.js
:
$ git clone https://github.com/jfly/2019-07-lodash-webpack-plugin-nondeterminism.git
...
$ cd 2019-07-lodash-webpack-plugin-nondeterminism
$ make run
...
Compiling (with no delay for accessing 'a.js')
Running dist/main.nodelay.js (this will print an empty object)
{}
Compiling (with 1 second delay for accessing 'a.js')
Running dist/main.withdelay.js (this will print a non-empty object)
{ magickey: 1 }
Note that output differs between these two compiled artifacts!
The root cause seems to be related to the fact that lodash-webpack-plugin updates some internal state (this.patterns
) inside of the callback that gets fired in a non-deterministic order. In the example above, if a.js
is read first, then the overrides for flatMap
get loaded, which enables flattening
. However, if b.js
is read first, then flattening
will not have been enabled yet, and omit
will behave differently.
One workaround is to explicitly enable flattening
in our lodash-webpack-plugin
configuration, but it does feel to me like this plugin should be rewritten to behave deterministically, regardless of the order in which webpack reads files.