bananaoomarang / isomorphic-redux

Isomorphic Redux demo, with routing and async actions
https://medium.com/@bananaoomarang/handcrafting-an-isomorphic-redux-application-with-love-40ada4468af4
MIT License
455 stars 87 forks source link

Adding css loader to webpack setup #38

Closed Conrad777 closed 8 years ago

Conrad777 commented 8 years ago

Hey, first off I want to thank you for this article... It really helped me to understand universal web apps a lot better. I do however have a question:

How do I add the css loader in Webpack so that it will work on the server as well?

So what I did was I installed and added the loaders inside webpack.dev.js like this:

...
{
    test:    /\.jsx?$/,
    exclude: /node_modules/,
    loader:  'babel',
    query:   BABEL_QUERY
},
{
    test:   /\.css$/,
    loader: 'style!css'
}
...

Then when I import the file into my Home.jsx like this

import 'css/style.css';

It returns this error when running npm run dev:

> isomorphic-redux@1.0.0 dev /Users/conradh/Desktop/environments/server/www/delete-this/isomorphic-redux-babel6
> NODE_PATH=$NODE_PATH:./shared node --harmony .

/Users/conradh/Desktop/environments/server/www/delete-this/isomorphic-redux-babel6/node_modules/babel-core/lib/transformation/file/index.js:548
      throw err;
      ^

SyntaxError: /Users/conradh/Desktop/environments/server/www/delete-this/isomorphic-redux-babel6/shared/css/style.css: Unexpected token (1:5)
> 1 | body {
    |      ^
  2 |   background: skyblue;
  3 | }
    at Parser.pp.raise (/Users/conradh/Desktop/environments/server/www/delete-this/isomorphic-redux-babel6/node_modules/babylon/lib/parser/location.js:22:13)
    at Parser.pp.unexpected (/Users/conradh/Desktop/environments/server/www/delete-this/isomorphic-redux-babel6/node_modules/babylon/lib/parser/util.js:91:8)
    at Parser.pp.semicolon (/Users/conradh/Desktop/environments/server/www/delete-this/isomorphic-redux-babel6/node_modules/babylon/lib/parser/util.js:78:38)
    at Parser.pp.parseExpressionStatement (/Users/conradh/Desktop/environments/server/www/delete-this/isomorphic-redux-babel6/node_modules/babylon/lib/parser/statement.js:470:8)
    at Parser.<anonymous> (/Users/conradh/Desktop/environments/server/www/delete-this/isomorphic-redux-babel6/node_modules/babylon/lib/plugins/flow.js:658:20)
    at Parser.parseExpressionStatement (/Users/conradh/Desktop/environments/server/www/delete-this/isomorphic-redux-babel6/node_modules/babylon/lib/plugins/flow.js:658:20)
    at Parser.pp.parseStatement (/Users/conradh/Desktop/environments/server/www/delete-this/isomorphic-redux-babel6/node_modules/babylon/lib/parser/statement.js:161:17)
    at Parser.<anonymous> (/Users/conradh/Desktop/environments/server/www/delete-this/isomorphic-redux-babel6/node_modules/babylon/lib/plugins/flow.js:636:22)
    at Parser.parseStatement (/Users/conradh/Desktop/environments/server/www/delete-this/isomorphic-redux-babel6/node_modules/babylon/lib/plugins/flow.js:636:22)
    at Parser.pp.parseBlockBody (/Users/conradh/Desktop/environments/server/www/delete-this/isomorphic-redux-babel6/node_modules/babylon/lib/parser/statement.js:500:21)

npm ERR! Darwin 14.4.0
npm ERR! argv "/Users/conradh/.nvm/versions/node/v5.1.1/bin/node" "/Users/conradh/.nvm/versions/node/v5.1.1/bin/npm" "run" "dev"
npm ERR! node v5.1.1
npm ERR! npm  v3.3.12
npm ERR! code ELIFECYCLE
npm ERR! isomorphic-redux@1.0.0 dev: `NODE_PATH=$NODE_PATH:./shared node --harmony .`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the isomorphic-redux@1.0.0 dev script 'NODE_PATH=$NODE_PATH:./shared node --harmony .'.
npm ERR! Make sure you have the latest version of node.js and npm installed.
npm ERR! If you do, this is most likely a problem with the isomorphic-redux package,
npm ERR! not with npm itself.
npm ERR! Tell the author that this fails on your system:
npm ERR!     NODE_PATH=$NODE_PATH:./shared node --harmony .
npm ERR! You can get their info via:
npm ERR!     npm owner ls isomorphic-redux
npm ERR! There is likely additional logging output above.

npm ERR! Please include the following file with any support request:
npm ERR!     /Users/conradh/Desktop/environments/server/www/delete-this/isomorphic-redux-babel6/npm-debug.log

It works when I import the css on the client/index.jsx file, cause then the server doesn't have to deal with it at all. Problem with this is that I want to make the styles modular and add only the necessary styles to their components. My aim is to move towards using CSS Modules... But before I can I need to know why and how to add loaders to the setup.

I would truly appreciate you're help...

bananaoomarang commented 8 years ago

The easiest way to do this is to wrap CSS requires in a conditional. Something like:

if (window)
    require('module.css');

Slightly cleaner would be to use Webpack.DefinePlugin() to set process.env.CLIENT to true, then use that.

There might be a better way of doing it, because I know this seems a little messy. There is probably a way of telling babel not to process certain requires, I just don’t know it :)

Conrad777 commented 8 years ago

Yeah, I did exactly that and it at least does half the job by loading chunks of css only when necessary. The only issue with this is that it doesn't make it possible to then do inline styling like this:

let styles = require('module.css')

...

render() {
    return (
      <div className={styles.container} />

...

But for now I guess this will have to do until I can find a better way of doing it. I will dig a little into babel and see if I can find something there.

Thanks for your reply!

Dattaya commented 8 years ago

@Conrad777, webpack-isomorphic-tools will do what you want, the only downside is that it requires a lot of configuration and that configuration makes the code look uglier, but it works. I use it in my own project.

Conrad777 commented 8 years ago

Awesome!! This is perfect... Thanks @Dattaya!!

Dattaya commented 8 years ago

Alternatively, there is https://github.com/gajus/react-css-modules but I think webpack-isomorphic-tools is the easiest path. There was an attempt to integrate react-css-modules into react-redux-universal-hot-example, see erikras/react-redux-universal-hot-example#655 but chrisblossom had some trouble with hot reloading and error handling. Let me know if you have any trouble with integrating it. Well, with this current setup—one express server for everything—you most likely will. If you want to stick with it, you'll need to add the following code in your index.js

// Since one express server serves the app and dev assets,
// workaround is needed (in the dev mode) for webpack-isomorphic-tools
if (process.env.NODE_ENV !== 'production') {
  var fs = require('fs');
  fs.closeSync(fs.openSync(path.join(__dirname, '..', 'webpack-assets.json'), 'w'));
}

I have plans to create a separate branch in my fork of this repository with wepack-isomorphic-tools integrated, it's already on my todo list. I can speed this process up in case it takes you too long to properly configure it.

Dattaya commented 8 years ago

In my previous post I believe I confused react-css-modules with css-modules-require-hook

Conrad777 commented 8 years ago

@Dattaya I will need help with this after all! It might just be a really silly rookie thing, but I don't have the level of expertise to even know where to start looking. So I apologise in advance if the code below makes you lose all hope for humanity!!

Here is what I've added so far:

webpack.dev.js

...
var WebpackIsomorphicTools = require('webpack-isomorphic-tools');

var Webpack_isomorphic_tools_plugin = require('webpack-isomorphic-tools/plugin')
var webpack_isomorphic_tools_plugin = new Webpack_isomorphic_tools_plugin(require('./webpack-isomorphic-tools-configuration'))

...

export default function(app) {
  const config = Object.assign(prodCfg, {
    ...
    context: path.resolve(__dirname, '..'),
    output: {
      path: path.join(__dirname, 'dist'),
      publicPath: '/',
      filename: 'bundle.js'
    },
    module: {
      loaders: [
        {
          test: /\.scss$/,
          loader: 'style!css?modules&importLoaders=2&sourceMap&localIdentName=[local]___[hash:base64:5]!autoprefixer?browsers=last 2 version!sass?outputStyle=expanded&sourceMap'
        },
...
},
    plugins: [
      webpack_isomorphic_tools_plugin.development(),
...

webpack.prod.config.js

var WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin');
var webpackIsomorphicToolsPlugin = new WebpackIsomorphicToolsPlugin(require('./webpack-isomorphic-tools-configuration'));

...

  module: {
    loaders: [
        test: /\.scss$/,
        loader: ExtractTextPlugin.extract('style-loader', 'css?modules&importLoaders=2&sourceMap!autoprefixer?browsers=last 2 version!sass?outputStyle=expanded&sourceMap=true&sourceMapContents=true')
      },
...
  plugins: [
    webpackIsomorphicToolsPlugin
...

webpack-isomorphic-tools-configuration.js (I basically just copied this from here)

var WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin');

module.exports = {

  assets: {
    images: {
      extensions: ['jpeg','jpg','png','gif'],
      parser: WebpackIsomorphicToolsPlugin.url_loader_parser
    },
    fonts: {
      extensions: ['woff','woff2','ttf','eot'],
      parser: WebpackIsomorphicToolsPlugin.url_loader_parser
    },
    svg: {
      extension: 'svg',
      parser: WebpackIsomorphicToolsPlugin.url_loader_parser
    },
    style_modules: {
      extensions: ['scss'],
      filter: function(module, regex, options, log) {
        if (options.development) {
          return WebpackIsomorphicToolsPlugin.style_loader_filter(module, regex, options, log);
        } else {
          return regex.test(module.name);
        }
      },
      path: function(module, options, log) {
        if (options.development) {
          return WebpackIsomorphicToolsPlugin.style_loader_path_extractor(module, options, log);
        } else {
          return module.name;
        }
      },
      parser: function(module, options, log) {
        if (options.development) {
          return WebpackIsomorphicToolsPlugin.css_modules_loader_parser(module, options, log);
        } else {
          // in production mode there's Extract Text Loader which extracts CSS text away
          return module.source;
        }
      }
    }
  }
}

index.js

if (process.env.NODE_ENV !== 'production') {
  var fs = require('fs');
  fs.closeSync(fs.openSync(path.join(__dirname, '..', 'webpack-assets.json'), 'w'));
}

var Webpack_isomorphic_tools = require('webpack-isomorphic-tools')
var project_base_path = path.resolve(__dirname, '..');

global.webpack_isomorphic_tools = new Webpack_isomorphic_tools(require('./webpack-isomorphic-tools-configuration'))
.development(process.env.NODE_ENV !== 'production')
.server(project_base_path, function() {

  var server = require('./server')

  const PORT = process.env.PORT || 3000;

  server.listen(PORT, function() {
    console.log('Listening on port %s...', PORT);
  })

})

viewComponent.js

let styles = require('./homeView.scss');

Dattaya commented 8 years ago

@Dattaya I will need help with this after all! It might just be a really silly rookie thing, but I don't have the level of expertise to even know where to start looking. So I apologise in advance if the code below makes you lose all hope for humanity!!

ha ha, no way is it going to happen (I'm talking about hope for humanity) :smiley: I'm also kind of a rookie with this stuff too, because I didn't want to delve into this library too much, so I copied some config files from react-redux-universal-hot-example as you did :).

  1. The first thing I see, but it's only relevant for production is missing or maybe you just haven't copied that code here, but make sure you iterate over assets in your template component (it's probably convenient to create one) like so {Object.keys(assets.styles).map((style, i) => ... take a look at this file.
  2. Second, this context: path.resolve(__dirname, '..'), and this path.resolve(__dirname, '..') context is the path to resolve your entries, so in your case for ./client file it would be 'path_where_dev_config_is_located' + '..' + '/client'. If you use this project as a boilerplate, you need to remove '..' in both.
  3. var server = require('./server') => var server = require('./server').default; (this is how babel 6 works)
  4. In server js I have a code that if I understand it correctly, responsible for the correct work of hot reloading of assets:
app.use( (req, res) => {
  if (process.env.NODE_ENV !== 'production') {
    global.webpackIsomorphicTools.refresh();
  }
  const location = createLocation(req.url);
  const reducer  = combineReducers(reducers);
  const store    = applyMiddleware(promiseMiddleware)(createStore)(reducer);
  ...

5 let styles = require('./homeView.scss');–I think it doesn't work globally so ensure it's inside render method. I hope it helps.

Conrad777 commented 8 years ago

@Dattaya I added all these changes (except for 1. but I think it should still work without it) and still not resolving the css. I forked this repo and made the changes to it quickly... Would you mind looking at it real quick and let me know what I am missing?

https://github.com/Conrad777/isomorphic-redux

I would understand if you are a bit busy, so only if you can please!

Dattaya commented 8 years ago

@Conrad777 take a look at this branch https://github.com/Dattaya/isomorphic-redux/tree/integrate-iso-tools , it should work nicely, the only thing missing is # 1 from the list—iterating over assets.

Dattaya commented 8 years ago

btw, I gave the code not completely suitable for this project fs.closeSync(fs.openSync(path.join(__dirname, '..', 'webpack-assets.json'), 'w'));—redundant .. again, oops...

Conrad777 commented 8 years ago

@Dattaya This is perfect! # 1 was pretty quick to implement as well. Thank you for all your help... it is much appreciated!!

ryanbelt commented 7 years ago

@Dattaya , i saw your repository. It does help me to setup the css as well. But the only question is. The css are not able to run on production server.

ryanbelt commented 7 years ago

i found a way to fix it. under webpack.prod.config.js

new ExtractTextPlugin('[name].css', {allChunks: true}),

under server.js

<body>
          <div id="react-view">${componentHTML}</div>
          <link rel='stylesheet' type='text/css' href='/main.css' />
          <script type="application/javascript" src="/bundle.js"></script>
        </body>