webpack-contrib / extract-text-webpack-plugin

[DEPRECATED] Please use https://github.com/webpack-contrib/mini-css-extract-plugin Extracts text from a bundle into a separate file

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Reloading extracted css with hot module replacement

nickdima opened this issue · comments

Is it possible to have hot module replacement for an extracted css file that I load via a css link tag in my html's head tag?
I have HMR working for my javascript but not sure how to make it work for extracted css.
This is my css related config:

entry:
    styles: ['webpack-dev-server/client?http://localhost:3001', 'webpack/hot/dev-server', './app.scss']
loaders: [
  {
    test: /\.scss$/
    loader: ExtractTextPlugin.extract "style-loader", "css-loader!sass-loader?" + JSON.stringify
      outputStyle: 'expanded'
      includePaths: [
        path.resolve __dirname, './app'
        path.resolve __dirname, './bower_components'
        require('node-bourbon').includePaths
        path.resolve __dirname, './vendor/css'
      ]
  }
plugins: [new ExtractTextPlugin "css/#{nameBundle}.css"]

Why use extract-text-webpack-plugin in development when you can use just css-loader + style-loader which can do HMR?

Good point! :)
Thanks, I'll give it a try.

@andreypopp how do you handle dev/production configuration changes? Just curious how other people do it, I'm using the NODE_ENV variable.

@nickdima just if (process.env.NODE_ENV === 'production') { ... }

@andreypopp it seems that if I require my css from my main js entry point the HMR works, but not if I want to have it as a separate bundle and load it via a script tag.
Any ideas?

These are my entries:

entry:
  common: ['webpack-dev-server/client?http://localhost:3001', 'webpack/hot/dev-server', './client.coffee']
  styles: ['webpack-dev-server/client?http://localhost:3001', 'webpack/hot/dev-server','./app.scss']

If I leave only each one of them HMR works for any of them, but if I put them both HMR works only for common.
This is how the log from the browser's consoles looks like when making a css change:

screen shot 2014-10-07 at 15 54 39

We need to change this https://github.com/webpack/webpack/blob/master/hot/dev-server.js#L39

window.onmessage = function(event) {

to something like addEventListener...

Do you want to send a PR?

I'll take a look. I'm a bit in the dark there, just starting out with HMR :)

as workaround you can do:

entry:
  common: ['webpack-dev-server/client?http://localhost:3001', 'webpack/hot/dev-server', 
             './app.scss', './client.coffee']

Best is to use only one entry per HTML page...

My goal is to have the css as a separate bundle so I can load it in the head. My html is generated server side and the javascript loaded just before closing the body tag.
I could just load everything in the head in development, but I don't want to change things to much between dev and prod. There would be just too many places to check for the env variable, so I was looking for a cleaner solution to my scenario.

@sokra I investigated the problem and now I see what you're saying about window.onmessage, it gets overwritten by the last loaded bundle.
I've made a test with addEventListener and it seems to work. I'll prepare a PR.

@nickdima Sorry if I'm not following something, but did you get HMR with the extract plugin working? Can you provide an example config?

No, I just use the script loaded css in dev and extracted only in production.

@nickdima any luck with this yet with Extract plugin? Shouldn't the CSS just reload and the browser pick up the changes or link tags don't work that :)

@mmahalwy This doesn't work. You shoudn't use the extract-text-webpack-plugin in development. Better thread the extract-text-webpack-plugin as production optimiation for the style-loader.

andreypopp: Why use extract-text-webpack-plugin in development when you can use just css-loader + style-loader which can do HMR?

@nickdima i'm trying to get something similar working. Would you mind linking your webpack.config.js?

@rxtphan it depends on you setup, what we did is we have an entry point for all our css (we use sass imports) that uses the style-loader and css-loader.
In development we just load the generated js file that adds the css at runtime while for the production build we wrap the loaders with the extract-text-webpack-plugin so that it creates a separate css file with all our styles. Basically in your html's head you need to load one or the either based on the env.

@rxtphan @mmahalwy @mysterycommand et all,

I ran into this "bug" too, wondering why my styles weren't reloading. Here's a solution that works:

// webpack.config.js
var DEBUG = process.env.NODE_ENV !== 'production' ? true : false;
var styles = 'css!csslint';

// add to loaders
{
    test: /\.css$/,
    loader: DEBUG ? 'style!' + styles : ExtractTextPlugin.extract(styles)
}

After a while of googling, I came to the realization that having live-reload (or hmr), with sass, with source maps, is currently not possible with webpack. Can anyone please confirm this, or point me in the right direction on how to get this working?

If I don't use extract-text-webpack-plugin, I can get hmr working just fine, but since that just puts the CSS inline in a <style> tag, there is no way to get sourcemaps. The sass-loader readme specifies that if you need sourcemaps, you have to use this plugin. Ok, I start using this plugin. Sourcemaps are working great, but there is no way to get hmr/livereload working, since the recommendation by @sokra here is to not use this in development.

@WishCow Just realized the exact same thing. Bonus bogus points for part of the documentation refering to devtool: 'source-map', and some other to devtool: 'sourcemap', (this is addressed by pull-request #84)). Seems like we are chasing ghosts at this time.

Doesn't style-loader!css-loader?sourceMap work?

@sokra that does not work, however messing around a bit with it, it turns out that adding sourceMap to both sass, and css loader works:

{
  test: /\.scss$/,
  loaders: [ 'style', 'css?sourceMap', 'sass?sourceMap' ]
}

It does produce a bit of a weird output in Chrome about which file the rules belong to, but clicking on the line does reveal the correct scss file (with the correct line number).

chrome sourcemaps

I'll open an issue on sass-loader, so that it has the correct information on how to enable source maps, so people don't get misled here.

Thanks a lot!

When I use autoprefixer-loader it produces even more weird source path:

zsfjq6z

Here is my webpack config:

var AUTOPREFIXER_BROWSERS = '"ie >= 10","ie_mob >= 10","ff >= 30","chrome >= 34","safari >= 7","opera >= 23","ios >= 7","android >= 4.4","bb >= 10"';

loaders: [
  "style-loader",
  "css-loader?sourceMap",
  "autoprefixer-loader?{browsers:[" + AUTOPREFIXER_BROWSERS + "]}",
  "sass-loader?sourceMap"
]

@sokra seems like saying "don't use extract-text in development" isn't a great solution for a few reasons:

(1) There are big debugging benefits to having separate CSS files that I want on dev
(2) I'd like to test my load times in development as close to production as possible
(3) more generally, the further your dev and prod become the more likely you are to have bugs

Would be great to get Hot Module Reload working from a stylesheet link tag.

@nickdima looks like you solved the multiple hot server entry points, was there another reason you weren't able to get this working?

@nickdima

Basically in your html's head you need to load one or the either based on the env.

How have you done this? Thanks!

@tsheaff I don't remember really well (one year has past) but looking at the discussion it seems I had a pull request that solved the multiple entry points issue.

@jsrobertson my index html is a handlebars template which gets the env variable passed from node.js so in a IF statement I load the css as a js script for dev and in prod I link to the stylesheet

Thanks for response @nickdima . I ended up going with just not using extract-text for my local builds, after I realized that the 12-factor dev/prod parity point is more about the difference between deployed dev (e.g. www-dev) and deployed prod (www) rather than local (localhost) so now both deployed instances match in that they use extract-text 👍 while I still get hot-reloading on localhost by not using it there

@nickdima @tsheaff did you have performance issues with big inline source maps generated by Webpack without extract-text-plugin? I'm stuck, because developer tools slows down too much with inline source maps. It's impossible to edit css directly from browser.

I had quite a hard time to figure out why I can't enable live reloading for CSS in my setup. This issue seems to indicate that the problem is ExtractTextPlugin. I still don't really understand why ExtractTextPlugin doesn't support reloading. If this is on purpose it would be great if this could be highlighted in more detail on the README.

@donaldpipowitch this part of the webpack docs should clear things up https://webpack.github.io/docs/hot-module-replacement-with-webpack.html

Essentially, the bundle file that webpack outputs which includes all your apps code (js and css files) also includes the HMR runtime. This is where the magic happens. The HMR runtime is evident if you take a look at the outputted bundle file. You will see a quite a bit of weird webpack jargon code included in that bundle. That's the HMR runtime. When running your app, webpack checks for any changes in your source files and when a change occurs, it sends a signal to the HMR runtime included in that bundle file and then the HMR runtime applies those changes and updates your app.

When using the ExtractTextPlugin, take a look at the extracted file. As you can see, the webpack HMR runtime code is not included. It only includes your app code so there's no way for webpack to signal that extracted file to make the necessary changes. This is the intended behavior of the ExtractTextPlugin since it is only meant to be used for production builds.

@ipeters90 Thank you for the detailed explanation.

As you can see, the webpack HMR runtime code is not included. It only includes your app code so there's no way for webpack to signal that extracted file to make the necessary changes.

So could I integrate live reloading, if I'd include the HMR runtime code somehow?

This is the intended behavior of the ExtractTextPlugin since it is only meant to be used for production builds.

The problem with this is that production and dev builds behave differently now. The only (?) other alternative is {{style-loader}} which causes a flash of styles initially, because the styles are injected with a slight delay.

@donaldpipowitch I'm also using {{style-loader}} and experience the quick initial flash of styles as well in my dev environment. Honestly it's just something I've put up with since the benefit of CSS hot loading while i'm developing, to me, far outweighs the this slight inconvenience. But It would be nice to find a better alternative for a more accurate comparison between dev and prod on the initial render. This issue could have something to do with the initial compilation of the css in runtime when you first load the page, whereas, when the css file is already extracted when doing a build, all the css is already precompiled so it just needs to load the file. Just my guess but I haven't found a solution yet.

@ipeters90 I actually fallback to browser-sync for reloading the CSS with extract-text-webpack-plugin for now: webpack/webpack#1530 (comment).

@ptahdunbar you just saved us a loooot of headache with #30 (comment)
This should definitely go into official docs
👍

I was able to reload the extracted CSS file with the code below. I know it's not an elegant solution, but it works pretty well.

Assuming that you are using : new ExtractTextPlugin("bundle.css")
....
Just add the following code in your browser application.

`if (process.env.NODE_ENV !== 'production') {

const cssFileName = 'bundle.css';
const originalCallback = window.webpackHotUpdate;

window.webpackHotUpdate = function (...args) {
    const links = document.getElementsByTagName("link");
    for (var i = 0; i < links.length; i++) {
        const link = links[i];
        if (link.href.search(cssFileName) !== -1) {
            let linkHref = link.href;
            link.href = 'about:blank';
            link.href = linkHref;
            originalCallback(...args);
            return;
        }
    }
}

}`

@web2style @luismreis @sokra It is now possible to use angular2, webpack with hot module replacement, sass sourcemaps, and externally loaded css. It tooks me days of playing with it but I got it working!

The dependencies are style-loader, css-loader, and sass-loader.

sass_sourcemaps _hmr _wds

In your webpack config, you need this loader:


  devtool: 'source-map',

{
        test: /\.scss$/,
        exclude: /node_modules/,
        loader: 'style!css?sourceMap!sass?sourceMap&sourceComments'
}

Then, in your component, for example, app.component.ts, you would require your .scss file OUTSIDE of the @component decorator. That is the trick. This will not work if you load styles the "angular2 way". Better to let webpack handle styles with lazy-loading anyway.

/*
 * THIS IS WHERE WE REQUIRE CSS/SCSS FILES THAT THIS COMPONENT NEEDS
 *
 * Function: To enable so-called "Lazy Loading" CSS/SCSS files "on demand" as the app views need them.
 * Do NOT add styles the "Angular2 Way" in the @Component decorator ("styles" and "styleUrls" properties)
 */
require('./app.scss');

/*
 * App Component
 * Top Level Component
 */
@Component({
  selector: 'app',
  pipes: [],
  providers: [],
  directives: [RouterActive],
  encapsulation: ViewEncapsulation.None,
  ],
  template: require('./app.html')
})

I have had no problems running this setup. Does anyone see a "problem" with doing this? If not, here you go!

I still can't get this working with this current setup @IAMtheIAM

bundle.js:33754 Refused to load the stylesheet 'blob:http%3A//localhost%3A4004/70f4cf7d-8f6c-4ac2-bb5a-5f23bd700372' because it violates the following Content Security Policy directive: "style-src 'self' 'unsafe-inline'".

This is the error I'll get in my broswer.

My Loaders are

loaders: [ { test: /\.scss$/, exclude: /node_modules/, loader: 'style!css?sourceMap!sass?sourceMap&sourceComments' }, { test: /\.css$/, loader: [ "style-loader", "css-loader?sourceMap" ]},

Hi there!
You can use webpack-hot-middleware by @glenjamin to emit custom events like 'css-file-changed' and receive them on client side to reload css.

// somewhere in your dev-server.js

const webpackHotMiddleware = require('webpack-hot-middleware');
const chokidar = require('chokidar');

const hotMiddleware = webpackHotMiddleware(clientCompiler);
app.use(hotMiddleware);

// emit 'css-file-changed' on every `build.css` change
chokidar.watch('build/css/build.css').on('change', path => {
    console.log(`Css file ${path} has been changed`);

    hotMiddleware.publish({ 
        type: 'css-file-changed' ,
        // you can pass any additional stuff for your update logic
    });
});
// at the top of your main client.js
import webpackHotMiddlewareCliient from 'webpack-hot-middleware/client';

webpackHotMiddlewareCliient.subscribe((updateEvent) => {
    if (updateEvent.type === 'css-file-changed') {
         // your style tag update logic here
         // something like:
         // styleLink.href = 'about:blank';
         // styleLink.href = path;
    }
});

// ...

@jamiehutber Interesting. Your loaders are correct, and the blob that was generated looks fine. It seems to be a browser issue. What browser and what version are you using? Did you try the latest version of Chrome or FireFox?

Here is some info on stackexchange that might help you solve that. Let me know how it goes.

commented

@gustavorino 's original code did not work with my version of the dev server, but with some changes it worked perfectly. I added this to my index.html and id="css" to my stylesheet link tag:

    <script>
     // Development: reload css on hot update
     window.addEventListener("message", function (e) {
        console.log('message:', e.data);
        if (e.data.search('webpackHotUpdate') === -1) return;
        const link = document.getElementById("css");
        console.log('reloading css');
        let linkHref = link.href;
        link.href = 'about:blank';
        link.href = linkHref;
     }, false);
    </script>

I got this to work by using something like this:

webpack.config.js:

module.exports = {
    resolve: {
        extensions: ['', '.js', '.ts', '.scss']
    },
    module: {
        loaders: [
            {
                test: /\.scss$/,
                exclude: /node_modules/,
                loaders: ["style", "css?sourceMap", "sass?sourceMap"]
                // loader: 'style!css?sourceMap!sass?sourceMap&sourceComments'
            }
        ]
    }
}

And I run it with: $ webpack-dev-server --inline --progress --hot --port 8080

I'm using this in an Angular2 project, so I followed the angular.io guide for Webpack setup, then I wanted to work with SASS and I was looking for a way to make CSS live reload, instead of refreshing the whole page. Notice the --hot option.

commented

I think I came up with a much cleaner solution for supporting live reload WITH extracted stylesheets in development. Instead of adding window.onmessage handlers and reloading the stylesheet then, I simply moved my require('<path to css file>') line from my entrypoint into a separate JavaScript module with:

require('./styles/main.scss');

if (module.hot) {
  document.getElementById('css-bundle').href = '/assets/bundle.css?t=' + Date.now();

  module.hot.accept();
}

@IAMtheIAM Yeah, I just got into that same solution after 2 nights digging and learning about all the CSS *-loaders for the dev workflow, and then I saw your comment with the same result.

     {
        test: /\.scss$/,
        loader: 'style!css?sourceMap!postcss?sourceMap!sass?sourceMap'
      }

I was not expecting that it worked (and so nicely) after adding the ?sourceMap.
I didn't expected that the style-loader were ready for any loader with ?sourceMap and added a blob instead of embedding the styles... Thank you @sokra !

For the production, I just use the ExtractTextPlugin for now...

     {
        test: /\.scss$/,
        loader: ExtractTextPlugin.extract('style', 'css!postcss!sass')
      }

Here's another (hacky) solution, inspired by @L8D's comment. In my case I'm using require.context('.', true, /\.less$/) and ExtractTextPlugin, so none of the other solutions worked.

if (module.hot) {
  const reporter = window.__webpack_hot_middleware_reporter__;
  const success = reporter.success;
  reporter.success = function () {
    document.querySelectorAll('link[href][rel=stylesheet]').forEach((link) => {
      const nextStyleHref = link.href.replace(/(\?\d+)?$/, `?${Date.now()}`);
      link.href = nextStyleHref;
    });
    success();
  };
}

@drosen0 smart way of fixing it, works like a charm.

@drosen0 Do you still get the warning in the console with this method?

process-update.js?e13e:81 [HMR] The following modules couldn't be hot updated: (Full reload needed)
This is usually because the modules which have changed (and their parents) do not know how to hot reload themselves. [..snip..]
process-update.js?e13e:89 [HMR]  - ./web/static/app/styles/app.scss

Anyone know a way to silence that message?

@johnmcdowall, I don't get that message when updating .less files. A couple things to try:

  1. If you're not already, try using require.context() to import your .scss files.
  2. Try passing true as the second argument (useSubdirectories), for example, require.context('./app/styles', true, /app\.scss$/). I believe the reason is that HMR doesn't recognize the individual files when it's not a specific file reference.
commented

@L8D , so in your layout template do you just have an empty link tag?

commented

I simply created a separate entry and required my scss file.

require('scss/main.scss')

then in my layout file, based on NODE_ENV, in the head of the document spit out script tags if in dev, or link tags for prod where extract text plugin is used.

in webpack, I have the hot module replacement plugin with style, css, postcss, and scss loaders configured.

when I update the css, it updates on the page.

since it's in a script tag in the head, it must be parsed before the rest of the document.

commented

@kellyrmilligan if you're only using external stylesheets in production, then you'd use the extract-text-webpack-plugin in your production webpack configuration, and you shouldn't need hot loading to work. This issue is for those who want to use external stylesheets in development with hot loading support (instead of using style-loader).

You shouldn't need to conditionally add any script tags if you're using style-loader, since it should add the stylesheets to your main JavaScript bundle. Are you using webpack exclusively for stylesheets?

commented

@L8D yes, that's what I am doing. I think I posted too soon. my goal is to have css with hot loading in dev, and just the external stylesheet in prod. I want the separate entry point for css, so it can be in the head of the document, since the app I am working on is a universally rendered app. I still had the regular import in my apps entry point, so need to put the code mentioned above to update the link rel for the stylesheet.

are you just updating the link that gets generated by style loader in the snippet above?

commented

I guess that's maybe a third solution. have the entry point in the head of the document for css, and import it in your entry point too. this way hot reloading just works for css. extract text plugin will do a bit more work for the build, but not a ton. you could also remove the css entry point for the build to not have it build twice.

@drosen0 Thanks, although I'm using ES6 and import statements so not really sure how that would translate. I could use require I guess but it would be inconsistent.

commented

@L8D would it be possible for you to post the relevant parts in a repo somewhere? if I just do the entry point for css, the file will hot update but not update the page, which is why i'm guessing you have the code that updates the link tag.

commented

@kirkstrobeck or @drosen0 , any way to get a full example in a gist?

  1. are you just always using extract text plugin?
  2. then have an entry point that listens for hot reloads when you change the scss and it's recompiled to update the link tag?

much obliged!

commented

@kirkstrobeck that code doesn't actually assign the new property to the <link>. It's a no-op

if (module.hot) {
  document.querySelectorAll('link[href][rel=stylesheet]').forEach((link) => {
    const nextStyleHref = link.href.replace(/(\?\d+)?$/, `?${Date.now()}`);
    link.href = nextStyleHref;
  });
}

thanks to @drosen0

Can this become a core feature of the plugin (or an extension on the plugin) so we get rid of the bicycle problem?

@kellyrmilligan

  • this code goes on the client and not inside webpack if that was a confusion point
  • you can use the DefinePlugin, which is part of webpack, to add an if guard based on your environment.
commented

@kirkstrobek I think I get it. I'll try and come up with a fully working example to post somewhere. I think that would be true most valuable thing here.

commented

@kirkstrobeck where did your code go? I see it referenced , but not on this issue anymore?

commented

so far I have my webpack 2 config using extract text plugin, and in my main client entry i'm importing the scss. I have tried a few versions of the code above, but after the first load the module.hot is not firing again to update the css with the new date/timestamp. the extracted css file is being updated when i update just the scss, but the main entry point is not firing the code to update the timestamp.

const path = require('path')
const webpack = require('webpack')
const AssetsPlugin = require('assets-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')

const NODE_ENV = process.env.NODE_ENV || 'development'
const port = 8080
const host = 'http://localhost'

module.exports = {
  entry: {
    app: [
      'webpack-dev-server/client?http://localhost:8080',
      'webpack/hot/only-dev-server',
      './src/client.js'
    ]
  },
  output: {
    filename: NODE_ENV === 'development'
      ? 'js/[name].bundle.js'
      : 'js/[name].[hash].bundle.js',
    path: path.resolve(__dirname, './build/public'),
    publicPath: NODE_ENV === 'development'
      ? host + ':' + port + '/static/'
      : '/static/'
  },
  module: {
    rules: [
      {
        test: /\.(js)$/,
        use: ['babel-loader'],
        exclude: /(node_modules)/
      },
      {
        test: /\.(json)$/,
        use: ['json-loader']
      },
      {
        test: /\.scss$/,
        loader: ExtractTextPlugin.extract({
          loader: [
            {
              loader: 'css-loader',
              query: {
                sourceMap: true
              }
            },
            {
              loader: 'postcss-loader'
            },
            {
              loader: 'sass-loader',
              query: {
                sourceMap: true
              }
            }
          ]
        })
      }
    ]
  },
  plugins: [
    new AssetsPlugin({
      path: path.resolve(__dirname, './build'),
      prettyPrint: true
    }),
    new webpack.DefinePlugin({
      'process.env': {
        'NODE_ENV': JSON.stringify(NODE_ENV)
      }
    }),
    new webpack.HotModuleReplacementPlugin(),
    new ExtractTextPlugin(NODE_ENV === 'development'
      ? 'styles/bundle.css'
      : 'styles/[contentHash].bundle.css')
  ],
  resolve: {
    extensions: [
      '.js', '.json'
    ],
    modules: [
      path.resolve(__dirname, './src'),
      'node_modules'
    ],
    alias: {
      react: path.resolve(__dirname, './node_modules/react'),
      'react-dom': path.resolve(__dirname, './node_modules/react-dom')
    }
  },
  devtool: NODE_ENV === 'development'
    ? 'eval-source-map'
    : 'source-map',
  devServer: {
    stats: 'errors-only',
    publicPath: '/static/',
    port: port,
    hot: NODE_ENV === 'development'
  }
}

entry point:

import 'scss/main.scss'
if (module.hot && process.env.NODE_ENV === 'development') {
  document.querySelectorAll('link[href][rel=stylesheet]').forEach((link) => {
    const nextStyleHref = link.href.replace(/(\?\d+)?$/, `?${Date.now()}`)
    link.href = nextStyleHref
  })
}

the first time it runs, and I see the timestamp. after an update, I do not.

commented

so I am now trying something closer to @L8D's solution, in my main entry I import a js file that just serves to update the css tag.

import './css'  //css.js module file

then in that module:

import 'scss/main.scss'

if (module.hot && process.env.NODE_ENV === 'development') {
  const cssNode = document.getElementById('css-bundle')
  cssNode.href = cssNode.href.replace(/(\?\d+)?$/, `?${Date.now()}`)
  module.hot.accept()
}

and the hot reload works only when I change a .js file, not an scss file. The console triggers App updated. recompiling... then nothing happens. is this a difference between webpack 1 and 2?

commented

@kellyrmilligan my guess is that webpack 2 is not reloading your css.js because it's trying to optimize module imports and doesn't consider css.js to depend on anything inside scss/main.scss and so it doesn't trigger a reload.

commented

but it did in webpack 1? is the implementation i'm describing what you ended up having working?

Well, let me add my 2 cents.

My configuration is following:

  • Webpack 2
  • React
  • HMR
  • Server-Sider Rendering
  • CSS Modules

I have separate webpack config for server and client:

const webpack = require('webpack');
const path = require('path');
const ManifestPlugin = require('webpack-manifest-plugin');

const outputDir   = 'dist';
const publicPath  = process.env.NODE_ENV === 'production' ? '/' : 'http://localhost:3000/';

const plugins = [
  // Always expose NODE_ENV to webpack, you can now use `process.env.NODE_ENV`
  // inside your code for any environment checks; UglifyJS will automatically
  // drop any unreachable code.
  new webpack.DefinePlugin({
    'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
  }),
  new webpack.NamedModulesPlugin(),
  new ManifestPlugin({
    publicPath,
    writeToFileEmit: true
  })
];

module.exports = {
  entry: {
    bundle: [ './client.js' ]
  },
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, outputDir),
    publicPath: '/'
    // necessary for HMR to know where to load the hot update chunks
  },
  context: path.resolve(__dirname, 'src'),
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        use: [
          'babel-loader',
        ],
        exclude: /node_modules/
      },
      {
          test: /\.less$/,
          use: [
            'style-loader',
            'css-loader?modules',
            'less-loader',
          ]
      },
            {
                test: /.ttf$/,
                loader: 'url-loader'
            },
            {
                test: /.woff$/,
                loader: 'url-loader'
            },
            {
                test: /.eot$/,
                loader: 'url-loader'
            }
    ],
  },
  plugins
};
const webpack = require('webpack');
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

const outputDir = 'dist';

const plugins = [
    // Always expose NODE_ENV to webpack, you can now use `process.env.NODE_ENV`
    // inside your code for any environment checks; UglifyJS will automatically
    // drop any unreachable code.
    new webpack.DefinePlugin({
        'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
    }),
    new ExtractTextPlugin({ filename: 'bundle.css', disable: false, allChunks: true }),
    new webpack.NamedModulesPlugin()
];

module.exports = {
    target: 'node',
    entry: {
        server: './server.js',
    },
    context: path.resolve(__dirname, 'src'),
    plugins,
    module: {
        rules: [
            {
                test: /\.jsx?$/,
                loader: 'babel-loader',
                exclude: /node_modules/
            },
            {
                test: /\.less$/,
                loader: ExtractTextPlugin.extract({
                    fallbackLoader: "style-loader",
                    loader: [
                        'css-loader?modules',
                        'less-loader',
                    ]
                })
            },
            {
                test: /.ttf$/,
                loader: 'url-loader'
            },
            {
                test: /.woff$/,
                loader: 'url-loader'
            },
            {
                test: /.eot$/,
                loader: 'url-loader'
            }
        ]
    },
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, outputDir)
    }
}

As you see, server use ExtractTextPlugin while client use simple style-loader.

I use Gulp for running both server and client build with single command:

const gulp = require('gulp');
const gutil = require('gulp-util');
const nodemon = require('gulp-nodemon');
const webpack = require('webpack');
const webpackDevServer = require('webpack-dev-server');
const clean = require('gulp-clean')

const frontendConfigDev = './webpack.client.config.dev';
const backendConfigDev = './webpack.server.config.dev';
const outputDir = 'dist';
const hostname = 'localhost';
const port = 3000;

gulp.task('clean-output', () => {
    return gulp
        .src(outputDir, { force: true })
        .pipe(clean());
});

gulp.task('frontend-build-dev', (callback) => {
    process.env.NODE_ENV = 'development';

    const cfg = Object.create(require(frontendConfigDev));

    cfg.devtool = 'eval';
    cfg.plugins.unshift(new webpack.LoaderOptionsPlugin({ debug: true }));

    webpack(cfg, function(err, stats) {
        if(err) throw new gutil.PluginError("webpack", err);
        gutil.log("[webpack]", stats.toString({
            // output options
        }));
        callback();
    });
});

gulp.task('frontend-watch', () => {
    process.env.NODE_ENV = 'development';

    const cfg = Object.create(require(frontendConfigDev));

    cfg.devtool = 'eval';

    cfg.plugins.unshift(new webpack.LoaderOptionsPlugin({ debug: true }));
    cfg.plugins.unshift(new webpack.HotModuleReplacementPlugin());
    cfg.output.publicPath = `http://${hostname}:${port}/`;

    cfg.entry.bundle.unshift('react-hot-loader/patch');
    cfg.entry.bundle.unshift(`webpack-dev-server/client?http://${hostname}:${port}/`);
    cfg.entry.bundle.unshift('webpack/hot/only-dev-server');

    const compiler = webpack(cfg);
    const server = new webpackDevServer(compiler, {
        publicPath: '/',
        stats: {
            colors: true
        },
        hot: true,
        headers: { 'Access-Control-Allow-Origin': '*' },
        contentBase: outputDir
    });

    server.listen(port, hostname, (err) => {
        if (err) {
            throw new gutil.PluginError('webpack-dev-server', err);
        }
        gutil.log('[webpack-dev-server]', `http://${hostname}:${port}/`);
    });
});

gulp.task("webpack", function(callback) {
    // run webpack
    webpack({
        // configuration
    }, function(err, stats) {
        if(err) throw new gutil.PluginError("webpack", err);
        gutil.log("[webpack]", stats.toString({
            // output options
        }));
        callback();
    });
});

gulp.task('backend-build-dev', (callback) => {
    process.env.NODE_ENV = 'development';

    const cfg = Object.create(require(backendConfigDev));

    webpack(cfg, function(err, stats) {
        if(err) throw new gutil.PluginError("webpack", err);
        gutil.log("[webpack]", stats.toString({
            // output options
        }));
        callback();
    });
});

gulp.task('backend-watch', [ 'backend-build-dev' ], () => {
    process.env.NODE_ENV = 'development';

    nodemon({
        script: 'dist/server.js',
        watch: 'src',
        ext: 'js jsx less css',
        tasks: [ 'backend-build-dev' ]
    });
});

gulp.task('watch', ['backend-build-dev', 'frontend-build-dev']);
gulp.task('watch', ['backend-watch', 'frontend-watch']);

You should notice that nodemon watches for less and css files to update bundle.css and avoid flushing on F5 in browser.

Last part is server.js:

import express from 'express';
import path from 'path';
import React from 'react';
import { renderToString } from 'react-dom/server';

import App from './components/app';

//
//  Read actual filenames from manifest
//
import { readFileSync } from 'jsonfile';
import fs from 'fs';

const manifestPath = path.resolve('dist/manifest.json');
let manifest;

fs.watchFile(manifestPath, (curr, _) => {
    if (curr.ino != 0) {
        console.log('Manifest found.');
        manifest = readFileSync(manifestPath);
    }
});

if (fs.existsSync(manifestPath)) {
    console.log('Manifest found.');
    manifest = readFileSync(manifestPath);
}

const app = express();

app.get('/', function (req, res) {
  const html = renderToString(
        <App />
    )
  res.send(renderPage(html));
});

function renderPage(html) {
  return `
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <link rel="stylesheet" href="http://localhost:3000/bundle.css"]}">
      </head>
      <body>
        <div id="root">${html}</div>
        <script src="${manifest['bundle.js']}"></script>
      </body>
    </html>
   `
}

const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
    console.log(`Server is listening on: ${PORT}`);
});

My prod configuration use ExtractTextPlugin for both client and server.

Hope, that will help someone.

commented

I write a loader(css-hot-loader), which supprot hot module replacement for an extracted css file.

Yeah, but your loader only for less, right?

commented

@LG0012 less is just a example , You can use any other loader, such as css, sass.

It's take me several hours on this problem, but finally find solution here.
Thanks

For everybody that still uses old-fashioned packages like webpack 1 and doing crazy stuff with multiple bundles and ExtractTextPlugin, I have updated @drosen0's solution. This version will not trigger a FOUT each time HMR finishes a build.

if (process.env.NODE_ENV === "development") {
  if (module.hot) {
    const reporter = window.__webpack_hot_middleware_reporter__;
    const success = reporter.success;
    const DEAD_CSS_TIMEOUT = 2000;

    reporter.success = function() {
      document.querySelectorAll("link[href][rel=stylesheet]").forEach((link) => {
        const nextStyleHref = link.href.replace(/(\?\d+)?$/, `?${Date.now()}`);
        const newLink = link.cloneNode();
        newLink.href = nextStyleHref;

        link.parentNode.appendChild(newLink);
        setTimeout(() => {
          link.parentNode.removeChild(link);
        }, DEAD_CSS_TIMEOUT);
      });
      success();
    };
  }
}

It duplicates the old nodes attaching new time-stamps and removing the old node after 2 seconds. This should be straight forward for development only.

commented

@helly0d would it be possible to create a gist with the full setup?

@helly0d thank you! this works. 😇🙏🏻

I found easier to do something like this:

webpackConfig.plugins = [
...
  new ExtractTextPlugin({
    disable: process.env.NODE_ENV === "development",
    filename  : '[name].[contenthash].css',
    allChunks : true
})
]

...

webpackConfig.module.rules.push({
  test : /\.scss$/,
  use: ExtractTextPlugin.extract({
      fallback: 'style-loader',
      use: [{
          loader: 'css-loader',
          options: {
              sourceMap: false,
              import: false,
              url: false
          }
      }, {
          loader: 'sass-loader',
          options: {
              sourceMap: true,
              outputStyle: 'expanded',
              includePaths: [...project.paths.client('styles'), './node_modules',]
          }
      }]
    })
})

For me, it works...

None of these last few suggestions worked for me mainly due to webpack 2 not seeing that my style files were true dependencies of any javascript that required them. webpack-hot-middleware didn't either. But it's possible to subscribe to all changes via webpack/hot/emitter 's webpackHotUpdate event:

if (module.hot) {
  var hotEmitter = require("webpack/hot/emitter");
  hotEmitter.on("webpackHotUpdate", function(currentHash) {
    document.querySelectorAll('link[href][rel=stylesheet]').forEach((link) => {
      const nextStyleHref = link.href.replace(/(\?\d+)?$/, `?${Date.now()}`)
      link.href = nextStyleHref
    })
  })
}

@nathanboktae it works well for me, except it refreshes the styles each time I make a change regardless if it is a change in javascript or css. I am wondering if there is a way to check if change was made in css.

Also, in chrome every time I make a change I see an annoying style flash. In firefox there is no such flash - it is all good.

@mieszko4 In order to avoid that annoying style flash ( aka FOUT ). You could combine the solution I gave above with the event based solution provided by @nathanboktae like this:

if (module.hot) {
  const hotEmitter = require("webpack/hot/emitter");
  const DEAD_CSS_TIMEOUT = 2000;

  hotEmitter.on("webpackHotUpdate", function(currentHash) {
    document.querySelectorAll("link[href][rel=stylesheet]").forEach((link) => {
      const nextStyleHref = link.href.replace(/(\?\d+)?$/, `?${Date.now()}`);
      const newLink = link.cloneNode();
      newLink.href = nextStyleHref;

      link.parentNode.appendChild(newLink);
      setTimeout(() => {
        link.parentNode.removeChild(link);
      }, DEAD_CSS_TIMEOUT);
    });
  })
}

Basically what this does, is to insert new links with the updated timestamp in the query, and remove the old link tags after 2 seconds. This way you will avoid that moment of FOUT because the new link tags will overwrite the rules from the old ones, once they have loaded their srcs. The removal of the old links is for clean-up purpose.

Thanx @helly0d! Your solution works well if there are no custom fonts in my case.

I looked more into the problem and I realized that the flash is actually caused by redownloading custom fonts that I have defined in my scss.
After each change in my scss or js, chrome redownloads the same font (the same filename).
I will post more if I figure out how to resolve that problem.

It seems that chrome does not redownload fonts if styles are defined in <style />.
I do not have enough knowledge to figure it out. It seems it would be best but also very hard (if possible at all?) if it was solved with <style /> patches instead of entire file refresh since the source map file maps to multiple source files in the project.

Great @helly0d I was about to merge our solutions too 👏

@milworm that article's solution is is 2x the lines, adds a totally unnecessary AJAX call, and keeps a 2nd copy of your stylesheets in memory in JS. This one is much better.

For the maintainers (@bebraw, @TheLarkInn, etc) the principle of keeping development and production as close as possible is extremely important, and a 2.5 year old issue with 85 comments figuring out how to do it is a testament to that. I would be great to have first class HMR support in extract-text-webpack-plugin, if not at least an official, documented solution.

@nathanboktae, @bebraw, @TheLarkInn It is not only about keeping developement and production as close as possible.
With extract-text-webpack-plugin I have source files under webpack:// in my chrome developer tools which is really awesome as they follow the tree structure of my project.
Without extract-text-webpack-plugin in my chrome developer tools they appear flat under ☁️ (no-domain and if I include resolve-url-loader source maps do not work (issue with resolve-url-loader).

@nathanboktae , well, you are right about second copy, but I don't think it's something bad. I wouldn't say that it's a bad solution. At least it works and doesn't have any issues mentioned in this thread.

@milworm No, You still have flickering as you're just setting href directly. You are polluting the network console with an XHR request (including when only JS changes). That also causes another delay.

@nathanboktae @mieszko4 PR's are welcomed! 😄 ❤️ If you think you can have a fool proof non-breaking solution why not?

The thing is that the team itself cannot and will not (currently) invest the time in tackling this vs the laundry list of user voted features that will take priority. However, it sounds that there are quite a few people here who believe that they would like this feature so I would love to see some collaboration and I'm happy to answer internal api questions, and foster any learning needed.

I think this pr is great for starters: #457

@TheLarkInn thanks for recognizing the need, and of course the hardwork on WebPack. 👏

@nathanboktae well, the ajax-call takes around 15ms on macbook, so it doesn't seem to be a lot.

Here's my updated loader config

This configuration allows for Hot Module replacement with SCSS files using webpack dev server, and extracttextplugin for production mode to emit actual .css files. Dev server mode simulated using extracted CSS because it loads the files in the when ?sourcemap=true on a previous loader

I also don't use the stylesUrl property, I import the .scss file outside of the @component decorator so that the styles load in the global context, rather than scoped by component.
Here's my working config

{
        test: /\.(scss)$/,
        use:
          isDevServer ? [
              {
                loader: 'style-loader',
              },            
              {
                loader: 'css-loader',
                options: { sourceMap: true }
              },
              {
                loader: 'postcss-loader',
                options: { postcss: [AutoPrefixer(autoPrefixerOptions)], sourceMap: true }
              },
              {
                loader: 'sass-loader',
                options: { sourceMap: true }
              },
              {
                loader: 'sass-resources-loader',
                options: {
                  resources: [
                    './src/assets/styles/variables.scss',
                    './src/assets/styles/mixins.scss']
                }
              }, 
              /**
               * The sass-vars-loader will convert the 'vars' property or any module.exports of 
               * a .JS or .JSON file into valid SASS and append to the beginning of each 
               * .scss file loaded.
               *
               * See: https://github.com/epegzz/sass-vars-loader
               */
              {
                loader: '@epegzz/sass-vars-loader?',
                options: querystring.stringify({
                  vars: JSON.stringify({
                    susyIsDevServer: susyIsDevServer
                  })
                })
              }] : // dev mode
          ExtractTextPlugin.extract({
            fallback: "css-loader",
            use: [
              {
                loader: 'css-loader',
                options: { sourceMap: true }
              },
              {
                loader: 'postcss-loader',
                options: { postcss: [AutoPrefixer(autoPrefixerOptions)], sourceMap: true }
              },
              {
                loader: 'sass-loader',
                options: { sourceMap: true }
              },
              {
                loader: 'sass-resources-loader',
                options: {
                  resources: [
                    './src/assets/styles/variables.scss',
                    './src/assets/styles/mixins.scss']
                }
              }, {
                loader: '@epegzz/sass-vars-loader?',
                options: querystring.stringify({
                  vars: JSON.stringify({
                    susyIsDevServer: susyIsDevServer
                  })
                  // // Or use 'files" object to specify vars in an external .js or .json file
                  // files: [
                  //    path.resolve(helpers.paths.appRoot + '/assets/styles/sass-js-variables.js')
                  // ],
                })
              }],
            publicPath: '/' // 'string' override the publicPath setting for this loader
          })
      },

app.component.css

import './app.style.scss'

/**
 * AppComponent Component
 * Top Level Component
 */
@Component({
   selector: 'body',
   encapsulation: ViewEncapsulation.None,
   host: { '[class.authenticated]': 'appState.state.isAuthenticated' },
   templateUrl: './app.template.html'
})

chain css-hot-loader with ExtractTextPlugin:
https://www.npmjs.com/package/css-hot-loader

I now have no Flash of Unstyled Content because of the extracted text, and my css hot reloads on save because of css-hot-loader

HMR needs css-loader and style-loader that support hot module css replacement. If you are using less or sass, use the less-loader or sass-loader and then simply use the loader configuration like below:

	module: {
		rules: [{
			test: /\.js$/,
			exclude: /node_modules|__tests__|__uitests__|__mocks__/,
			use: {
				loader: 'babel-loader'
			}
		}, {
			test: /\.less$/,
			exclude: /node_modules/,
			include: path.resolve(__dirname, 'ui/styles'),
			use: ['style-loader', 'css-loader', 'less-loader']
		}, {
			test: /\.(png|jpe?g|gif|svgz?|woff2?|eot)$/i,
			include: path.resolve(__dirname, 'ui/images'),
			use: {
				loader: 'file-loader',
				options: {
					name: '[name].[ext]'
				}
			}
		}, {
			test: /\.css$/,
			use: ['style-loader', 'css-loader']
		}]
	},

I've created https://github.com/sheerun/extracted-loader that is dedicated to this use case. Usage:

config.module.rules.push({
  test: /\.css$/,
  use: ['extracted-loader'].concat(ExtractTextPlugin.extract({
    /* Your configuration here */
  }))
})

config.plugins.push(new ExtractTextPlugin('index.css'))

No configuration necessary :)