How to generate a module from a webpack plugin and hot replace it

696 views Asked by At

I’m trying to write a webpack plugin for https://github.com/martinandert/babel-plugin-css-in-js, with the following 2 requirements during development:

  • I don’t want a file to be written to disk, but instead have webpack-dev-server host the file in-memory.
  • I want hot reloading to load the newly extracted CSS.

(Questions are at the bottom.)


Don’t write to disk

I’ve been able to do this with code like the following, but after reading many different examples I’m still not feeling 100% sure that this is the right way.

compiler.plugin('emit', function(compilation, callback) {
  compilation.chunks.forEach(function(chunk) {
    chunk.modules.forEach(function(module) {
      if (module.resource) {
        var css = extractCSSFromFile(module.resource)
        if (css) {
          cache[module.resource] = css
        }
      }
    })
  })
  var source = Object.values(cache).join('')
  compilation.assets[bundleFile] = {
    source: function() { return source },
    size: function() { return source.length },
  }
})

Hot reload

My thinking is that whenever the CSS changes, I would render a new version of a tiny module which forces the stylesheet to reload. I would then have web pack’s Hot Module Replacement replace that module, thus in effect have HMR for the extracted stylesheet. This reload module looks like:

if (module.hot) {
  module.hot.accept()
  module.hot.dispose(function() {
    document.querySelectorAll('link[href="' + WEBPACK_BABEL_CSS_IN_JS_STYLESHEET_PATH + '"]').forEach(link => {
      link.href = link.href.replace(/(\?\d+)?$/, '?' + WEBPACK_BABEL_CSS_IN_JS_STYLESHEET_HASH)
    })
  })
}

And the webpack plugin code to generate the file with the hash of the latest CSS looks like:

compiler.plugin('emit', function(compilation, callback) {
  // All of the previous code to extract the CSS...

  var source = Object.values(cache).join('')
  var sourceHash = crypto.createHash('md5').update(source).digest('hex')

  var cssReloader = path.basename(bundleFile, '.css') + '_webpack-reloader.js'
  var childCompiler = compilation.createChildCompiler('babel-css-in-js', {
    filename: cssReloader,
  })

  childCompiler.apply(
    new SingleEntryPlugin(
      compiler.context,
      path.join(__dirname, 'webpack-babel-css-in-js-client-template.js'),
      path.join(publicPath, cssReloader))
  )

  childCompiler.apply(
    new webpack.DefinePlugin({
      WEBPACK_BABEL_CSS_IN_JS_STYLESHEET_PATH: JSON.stringify(path.join(publicPath, bundleFile)),
      WEBPACK_BABEL_CSS_IN_JS_STYLESHEET_HASH: JSON.stringify(sourceHash),
    })
  )

  // Completely cargo-culted from http://stackoverflow.com/a/38284256
  // It is supposedly required for HMR to work.
  childCompiler.plugin('compilation', (compilation) => {
    if (compilation.cache) {
      if (!compilation.cache[cssReloader]) {
        compilation.cache[cssReloader] = {}
      }
      compilation.cache = compilation.cache[cssReloader]
    }
  })

  childCompiler.runAsChild(function(err) {
    if (err) {
      callback(err)
    }
    callback()
  })
})

The problem, however, is that my reloader JS module doesn’t get regenerated, which I think is because the input source doesn’t change, it would only change during compilation.


My questions basically are:

  • Am I approaching this the right way? And do you know of the right examples that I should look at?
  • Alternatively, is there a way for me to compile a file without using an external input file, but fully from source? That way I can just re-generate the source and embed the hash myself, after which webpack should notice a difference and HMR it, right?
0

There are 0 answers