Webpack 4 chunkhash/contenthash can vary between builds

Do you want to request a feature or report a bug?
Bug

What is the current behavior?
I’m finding that the chunkhash varies between builds in v4. Using the diff utility between the the different files shows no differences. Attempting to narrow the codebase down reduces the frequency of different hashes.

If the current behavior is a bug, please provide the steps to reproduce.
I’ve been unsuccessful in creating a small test case as it’s just too inconsistent and would take such an enormous amount of time for me to figure out by slicing and dicing our codebase.

I’m happy to help as much as I can to find a repro, but I’d need some information about diagnostics I can turn on to figure out what’s happening.

What is the expected behavior?
The chunkhash should be consistent, as does in version 3.

If this is a feature request, what is motivation or use case for changing the behavior?

Please mention other relevant information such as the browser version, Node.js version, webpack version, and Operating System.

Webpack 4.6.0, Node 8.9.4, OS X 10.13.4

Here’s our webpack.config.js

const path = require('path');
const webpack = require('webpack');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const childProcess = require('child_process');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const AssetsPlugin = require('assets-webpack-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const WebpackBuildNotifierPlugin = require('webpack-build-notifier');

const root = path.resolve(__dirname, './src');
const webappPath = path.resolve(__dirname, '..');
const deployPath = path.resolve(webappPath, './public/assets');

const dev = !process.env.NODE_ENV;
const ci = process.env.NODE_ENV === 'ci';
const prod = process.env.NODE_ENV === 'production';

if (prod) {
  console.log('PRODUCTION MODE ENABLED');
}

// don't hash the filename unless we're building for prod or staging. Hashed filenames interfere with karma tests.
const outputFilename = dev || ci ? '[name].bundle.js' : '[name].[chunkhash].bundle.js';
const excludedMapSources = dev ? [/vendor.*bundle.js/, /manifest.*bundle.js/] : [/manifest.*bundle.js/];

function getPlugins() {
  let plugins = [
    new CleanWebpackPlugin(['public/assets/*'], {
      allowExternal: true,
      root: webappPath
    }),

    new webpack.SourceMapDevToolPlugin({
      filename: "[file].map",
      exclude: excludedMapSources
    }),

    new AssetsPlugin({
      includeManifest: true,
      path: deployPath,
      prettyPrint: true
    })
  ];

  plugins.push(new webpack.NamedModulesPlugin());
  plugins.push(new webpack.optimize.ModuleConcatenationPlugin());

  return plugins;
}

module.exports = {
  context: root,

  mode: prod ? 'production' : 'development',
  devtool: false, // rely on SourceMapDevToolPlugin
  cache: dev,

  stats: {
    chunks: false,
    chunkModules: false,
    entrypoints: false,
    modules: false,
    version: false,
    hash: false,
    timings: false,
  },

  entry: {
    app: './core-app/bootstrap.ts',
    embed: './core-embed/bootstrap.ts'
  },

  watchOptions: {
    aggregateTimeout: 300,
    ignored: /node_modules/
  },

  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.jsx'],
    alias: {
      handsontable: 'handsontable/dist/handsontable.full.js'
    }
  },

  output: {
    filename: outputFilename,
    path: path.resolve(__dirname, '../public/assets'),
  },

  optimization: {
    runtimeChunk: {
      name: "manifest",
    },
    splitChunks: {
      cacheGroups: {
        // default: false,
        commons: {
          test: /[\\/]node_modules[\\/]/,
          name: "vendor",
          chunks: "initial",
          enforce: true
        }
      },
    },
  },

  externals: {
    nvd3: 'window.nv',
    highcharts: "window.Highcharts",
    angular: 'window.angular',
    jquery: 'window.jQuery',
    lodash: 'window._',
    moment: 'window.moment',
    ace: 'window.ace',
    d3: 'window.d3',
    chiliPiper: 'window.ChiliPiper',
  },

  plugins: getPlugins(),

  module: {
    rules: [
      {
        test: /\.ts(x?)$/,
        exclude: [/\.(spec|e2e)\.ts$/],
        use: [{
          loader: 'ts-loader',
          options: {
            experimentalWatchApi: true // seems to work ok atm, but performance improvement varies
          }
        }]
      }, {
        test: /\.(html|css)$/,
        loader: 'raw-loader',
        exclude: /\.async\.(html|css)$/
      }, {
        test: /\.svg$/,
        loader: 'url-loader?' + JSON.stringify({
          name: '[name]_[hash]'
        })
      }
    ]
  }
};

Author: Fantashit

6 thoughts on “Webpack 4 chunkhash/contenthash can vary between builds

  1. I changed it to contenthash, but I still get different hashes and they have the same content. I even took out the sourcemap output to make sure they are exactly the same.

  2. it happens for me too and I do use contenthash but they are changed per my build 😢 (it seems that chunk’s content is different per build)

  3. I also have this issue with chunkhash but I am seeing a pattern:

    • Only the chunkhash of one “vendor” splitChunk is affected
    • Only the hash is changed and not the file
      • I am using sourcemaps so the file does change but only on the line referencing the sourcemap
    • The hash remains consistent through builds started in the same bash
      • I.e. if I run the build several times from git bash it’s all good. Only if I run the build from VS integrated terminal and then from git bash is there a difference.
  4. I should have come back and posted an update.

    Short answer, Webpack’s hashing algorithm is deterministic. But the content being sent to it may not be. I discovered a whole slew of issues:

    1. Some npm packages create absolute paths in postInstall scripts.
    2. Some packages’ error messages get sent to the hashing algorithm containing an absolute path.
    3. And we had one webpack loader generating content containing absolute paths.

    I found the easiest way to debug this issue was to override the webpack hashing algorithm and add a little script to detect for my local absolute path. Here is approximately what I used:
    https://gist.github.com/halfnibble/c9f0c3b8d61e7002c799b5d24ae05643

    And then in webpack config:

    {
      ...
      output: {
        hashFunction: HashFunction,
        ...
      }
    }
  5. @halfnibble re: #7179 (comment)

    fantastic comment, thanks to it I revisited the topic and found that it was indeed script-loader that was breaking hashing in our project (due to the usage of path.join and require.resolve which both output absolute paths). I migrated away from it to raw-loader instead, and submitted the PR to deprecate script-loader.

    The info about not using absolute paths is documented in “writing a loader” doc:

    https://webpack.js.org/contribute/writing-a-loader/#absolute-paths

Comments are closed.