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
MIT License
4.01k stars 513 forks source link

Reloading extracted css with hot module replacement #30

Closed nickdima closed 9 years ago

nickdima commented 10 years ago

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 commented 10 years ago

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

nickdima commented 10 years ago

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

nickdima commented 10 years ago

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

andreypopp commented 10 years ago

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

nickdima commented 10 years ago

@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 commented 10 years ago

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 commented 10 years ago

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 commented 10 years ago

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

sokra commented 10 years ago

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 commented 10 years ago

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 commented 10 years ago

@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 commented 9 years ago

@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 commented 9 years ago

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

mmahalwy commented 9 years ago

@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 commented 9 years ago

@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 commented 9 years ago

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

nickdima commented 9 years ago

@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 commented 9 years ago

@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 commented 9 years ago

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 commented 9 years ago

@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 commented 9 years ago

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

WishCow commented 9 years ago

@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 commented 9 years ago

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 commented 9 years ago

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

jsrobertson commented 9 years ago

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

nickdima commented 9 years ago

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

nickdima commented 9 years ago

@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

tsheaff commented 9 years ago

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 :+1: while I still get hot-reloading on localhost by not using it there

valerybugakov commented 8 years ago

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

donaldpipowitch commented 8 years ago

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.

ipetez commented 8 years ago

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

donaldpipowitch commented 8 years ago

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

ipetez commented 8 years ago

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

donaldpipowitch commented 8 years ago

@ipeters90 I actually fallback to browser-sync for reloading the CSS with extract-text-webpack-plugin for now: https://github.com/webpack/webpack/issues/1530#issuecomment-179711478.

ipetez commented 8 years ago

@donaldpipowitch :+1:

mrtnpro commented 8 years ago

@ptahdunbar you just saved us a loooot of headache with https://github.com/webpack/extract-text-webpack-plugin/issues/30#issuecomment-125757853 This should definitely go into official docs 👍

gustavorino commented 8 years ago

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

}`

IAMtheIAM commented 8 years ago

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

hutber commented 8 years ago

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

vladkosinov commented 8 years ago

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

// ...
IAMtheIAM commented 8 years ago

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

wojas commented 8 years ago

@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>
mpavel commented 8 years ago

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.

L8D commented 8 years ago

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();
}
edmundo096 commented 8 years ago

@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')
      }
drosen0 commented 8 years ago

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();
  };
}
pedramphp commented 8 years ago

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

jmdfm commented 7 years ago

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

drosen0 commented 7 years ago

@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.
kellyrmilligan commented 7 years ago

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