Skip to content
This repository has been archived by the owner on May 29, 2019. It is now read-only.

Reloading extracted css with hot module replacement #30

Closed
nickdima opened this issue Oct 7, 2014 · 95 comments
Closed

Reloading extracted css with hot module replacement #30

nickdima opened this issue Oct 7, 2014 · 95 comments

Comments

@nickdima
Copy link

nickdima commented Oct 7, 2014

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"]
@andreypopp
Copy link
Contributor

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

@nickdima
Copy link
Author

nickdima commented Oct 7, 2014

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

@nickdima nickdima closed this as completed Oct 7, 2014
@nickdima nickdima reopened this Oct 7, 2014
@nickdima
Copy link
Author

nickdima commented Oct 7, 2014

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

@andreypopp
Copy link
Contributor

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

@nickdima
Copy link
Author

nickdima commented Oct 7, 2014

@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?

@nickdima
Copy link
Author

nickdima commented Oct 7, 2014

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

@sokra
Copy link
Member

sokra commented Oct 7, 2014

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?

@nickdima
Copy link
Author

nickdima commented Oct 7, 2014

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

@sokra
Copy link
Member

sokra commented Oct 7, 2014

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...

@nickdima
Copy link
Author

nickdima commented Oct 7, 2014

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.

@nickdima
Copy link
Author

nickdima commented Oct 7, 2014

@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.

@mysterycommand
Copy link

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

@nickdima
Copy link
Author

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

@mmahalwy
Copy link

@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 :)

@sokra
Copy link
Member

sokra commented May 31, 2015

@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?

@rxtphan
Copy link

rxtphan commented Jul 17, 2015

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

@nickdima
Copy link
Author

@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.

@ptahdunbar
Copy link

@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)
}

@WishCow
Copy link

WishCow commented Aug 16, 2015

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.

@eendeego
Copy link

@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.

@sokra
Copy link
Member

sokra commented Aug 17, 2015

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

@WishCow
Copy link

WishCow commented Aug 17, 2015

@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!

@yantakus
Copy link

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"
]

@tsheaff
Copy link

tsheaff commented Oct 21, 2015

@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?

@nathanboktae
Copy link

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
    })
  })
}

@mieszko4
Copy link

mieszko4 commented Apr 18, 2017

@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.

@helly0d
Copy link

helly0d commented Apr 18, 2017

@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.

@mieszko4
Copy link

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.

@mieszko4
Copy link

mieszko4 commented Apr 18, 2017

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.

@nathanboktae
Copy link

nathanboktae commented Apr 18, 2017

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.

@nathanboktae
Copy link

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.

@mieszko4
Copy link

@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).

@milworm
Copy link

milworm commented Apr 18, 2017

@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.

@nathanboktae
Copy link

nathanboktae commented Apr 18, 2017

@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.

@TheLarkInn
Copy link

TheLarkInn commented Apr 18, 2017

@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.

@mieszko4
Copy link

mieszko4 commented Apr 18, 2017

I think this pr is great for starters: #457

@nathanboktae
Copy link

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

@milworm
Copy link

milworm commented Apr 19, 2017

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

christopher4lis added a commit to christopher4lis/webpack.js.org that referenced this issue Jun 8, 2017
There's no way built into webpack core to reload external stylesheets automatically with HMR. There is a work around however, involving the automatic insertion and removal of link tags within a  `hotEmitter.on("webpackHotUpdate"...` function. Many people have looked for a solution for this, but the only answer is located in an issue thread near the very bottom: webpack-contrib/extract-text-webpack-plugin#30

It would be much more efficient and helpful for developers to see the solution directly on the docs page until a workaround is implemented into Webpack core.
@IAMtheIAM
Copy link

IAMtheIAM commented Aug 10, 2017

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'
})

@orpheus
Copy link

orpheus commented Oct 4, 2017

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

@doomsbuster
Copy link

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']
		}]
	},

@sheerun
Copy link

sheerun commented Dec 9, 2017

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 :)

@LG0012
Copy link

LG0012 commented Dec 9, 2017 via email

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests