Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to use isomorphic-style-loader effectively with component libraries? #62

Open
jussch opened this issue Oct 5, 2016 · 18 comments
Open

Comments

@jussch
Copy link

jussch commented Oct 5, 2016

I'm trying to use React Toolbox alongside isomorphic-style-loader in my project. React-Toolbox uses CSS modules for its components.

My current solution that I haven't implemented yet is to import each component and its styles, apply withStyles, and export the new component. An example file would look like:

// component_library/Button/index.js
import Button from 'react-toolbox/lib/button';
import buttonStyles from 'react-toolbox/lib/button/theme.scss';
import withStyles from 'isomorphic-style-loader/lib/withStyles';

export default withStyles(buttonStyles)(Button);

Is there a way to apply isomorphic-style-loader's withStyles() across all of a library's components without me having to manually import each file and re-export it?

--- Want to back this issue? **[Post a bounty on it!](https://www.bountysource.com/issues/38158821-how-to-use-isomorphic-style-loader-effectively-with-component-libraries?utm_campaign=plugin&utm_content=tracker%2F26439769&utm_medium=issues&utm_source=github)** We accept bounties via [Bountysource](https://www.bountysource.com/?utm_campaign=plugin&utm_content=tracker%2F26439769&utm_medium=issues&utm_source=github).
@frenzzy
Copy link
Member

frenzzy commented Oct 5, 2016

You can write a webpack-loader which will wrap all ^react-toolbox\/.*\.js$ files with the code that you have specified above.

@alex-shamshurin
Copy link

@jussch So why not to wrap the most outer component with library styles? I do.

@oliviertassinari
Copy link

oliviertassinari commented Oct 25, 2016

@javivelasco That's an issue you might be interested in. I'm really curious about how we could handle theming with lazy-loaded / injected style. I have never found time to look into this but it's often a blocker when builder large scale app.

@jussch
Copy link
Author

jussch commented Oct 25, 2016

@frenzzy Thanks for the tip, but unless I'm mistaken, there are a couple issues:

  • This relies on the library to have a standardized file-naming convention so that I can hard load the ./theme.scss file in each component's directory. (I'm unsure if React-Toolbox follows this entirely.)
  • A webpack-loader has to be written for each React component library.

I was hoping there was a more convenient solution, especially one that would translate well into integrating isomorphic-style-loader into an existing app.


@alex-shamshurin are you suggesting loading in React-Toolbox's components styles each time i use them, like so:

import Button from 'react-toolbox/lib/button';
import buttonStyles from 'react-toolbox/lib/button/theme.scss';
import FontIcon from 'react-toolbox/lib/font_icon';
import fontIconStyles from 'react-toolbox/lib/font_icon/theme.scss';
import withStyles from 'isomorphic-style-loader/lib/withStyles';

function MyOwnComponent () { 
  return (
    <div>
      <FontIcon></FontIcon>
      <Button></Button>
    </div>
  )
}

export default withStyles(buttonStyles, fontIconStyles)(MyOwnComponent)

If so, it's more inconvenient because of how often I use react-toolbox components.


I also noticed while playing around with isomorphic-style-loader and using the typical client-side setup with context like so:

// from react-starter-kit
const context = {
  insertCss: (...styles) => {
    const removeCss = styles.map(x => x._insertCss());
    return () => { removeCss.forEach(f => f()); };
  },
};

That this actually redefines the CSS order that you'd normally get with style-loader. style-loader orders the CSS in the head based on the order of import. isomorphic-style-loader orders the CSS in the head based on the order of component mounting. This makes it much harder to redefine a component library's styles because parent component's will almost always place their CSS before their children.

Example:

// File child.jsx
import childStyle from './childStyle.css';

export default withStyles(childStyle)(function ChildComponent({ className }) {
  return <div className={childStyle.normal + ' ' + className} />;
});

// File parent.jsx
import ChildComponent from './child'
import parentStyle from './parentStyle.css';

export default withStyles(parentStyle)(function ParentComponent() {
  return <ChildComponent className={parentStyle.overwrite} />;
});

With isomorphic-style-loader this will place the parentStyle.css style before the childStyle.css, where as with style-loader, the reverse is true.

This means to overwrite a third-party component styles (or any child component's styles) I have to beat the specificity of any selectors that are used. This can get nasty fast, especially if there is a large chain of style overwrites.

I can submit this problem as a separate issue, but it seemed relevant here.

@frenzzy
Copy link
Member

frenzzy commented Oct 25, 2016

If you want to override external component style maybe it is a good idea to do it like so:

/* MyComponent.css */
@import 'external/component.css';
.externalClassName {
  color: red; /* override color for class name from external/component.css */
}

ref kriasoft/react-starter-kit#920

@jussch
Copy link
Author

jussch commented Oct 25, 2016

Interesting, what do you do with MyComponent.css though? Are you saying to use it like so:

/* ./Button/index.js */
import Button from 'react-toolbox/lib/button';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import buttonStyles from './overwrite.css';

export default withStyles(buttonStyles)(Button);
/* ./Button/overwrite.css */
@import 'react-toolbox/lib/button/theme.css';

.someSelectorToOverwrite {
  color: red;
}

If so, I don't believe that works. The original button component at react-toolbox/lib/button is still referencing the old react-toolbox/lib/button/theme.css, which means that the Button would use the wrong localized class names.

@regenrek
Copy link

I would also be interestes in how to integrate this loader with react toolbox.
Is there an working example out there?

Regards

@kleinspire
Copy link

kleinspire commented Nov 20, 2016

There you go, fresh out out of the oven. Use it instead of withStyles. It expects the same arguments as themr() function from react-css-themr, except it has isomorphic-style-loader superpowers :)

Theming react-toolbox components is very easy with react-css-themr. Read react-toolbox installation guide to see how to set it up with themr. Besides themr is very nice to use for styling your own components as well. You can even switch themes at runtime with ThemeProvider. (Although there is a bug (javivelasco/react-css-themr#34), which can be fixed to make componentWillReceiveProps() method of themr() function track also changes to the context. I'm too lazy to do it myself, personally I don't need runtime theme switching right now.

The code hasn't been tested properly so please be gentle. Let me know if you spot any bugs.

import React, { Component, PropTypes } from 'react';
import hoistStatics from 'hoist-non-react-statics';
import { themr } from 'react-css-themr';

const CONTEXT_THEME = 'CONTEXT_THEME';
const PROPS_THEME = 'PROPS_THEME';
const DEFAULT_THEME = 'DEFAULT_THEME';

export default (componentName, defaultTheme, options) => (ComposedComponent) => {
  const ThemrComponent = themr(
    componentName, defaultTheme, options
  )(ComposedComponent);

  const displayName = ComposedComponent.displayName
                   || ComposedComponent.name
                   || 'Component';

  class WithThemr extends Component {
    static displayName = `WithThemr(${displayName})`;

    static contextTypes = {
      themr: PropTypes.object,
      insertCss: PropTypes.func,
    }

    static propTypes = {
      ...ThemrComponent.propTypes,
    }

    static defaultProps = {
      ...ThemrComponent.defaultProps,
    }

    constructor(props) {
      super(props);
      this.removeThemes = {};
    }

    componentWillMount() {
      if (this.props.composeTheme) {
        // composeTheme option is not false, meaning styles of the component
        // are composed of Context, Default and Props themes.

        const contextTheme = this.context.themr.theme[componentName];
        this.insertTheme(CONTEXT_THEME, contextTheme);
        this.insertTheme(DEFAULT_THEME, defaultTheme);
      }

      // Props theme has the highest priority of the three and needs to be
      // inserted regardless of the composeTheme option.
      this.insertTheme(PROPS_THEME, this.props.theme);
    }

    componentWillReceiveProps(nextProps, nextContext) {
      const contextTheme = this.context.themr.theme[componentName];
      const nextContextTheme = nextContext.themr.theme[componentName];

      if (nextProps.composeTheme) {
        // composeTheme option is not false, meaning styles of the component
        // are composed of Context, Default and Props themes.

        if (!this.props.composeTheme) {
          // composeTheme option used to be false, insert Context and Default
          // themes.
          this.insertTheme(CONTEXT_THEME, nextContextTheme);
          this.insertTheme(DEFAULT_THEME, defaultTheme);
        } else if (nextContextTheme !== contextTheme) {
          // Context theme has changed, replace it.
          this.removeTheme(CONTEXT_THEME);
          this.insertTheme(CONTEXT_THEME, nextContextTheme);
        }

        if (nextProps.theme !== this.props.theme) {
          // Props theme has changed, replace it.
          this.removeTheme(PROPS_THEME);
          this.insertTheme(PROPS_THEME, nextProps.theme);
        }
      } else {
        // composeTheme option is false, meaning styles of the component
        // are not composed and are provided by a theme with the highest priority.

        if (nextProps.theme || defaultTheme) {
          // Props theme and Default theme have higher priority than Context
          // theme. When either of them exist, we need to remove Context theme.
          this.removeTheme(CONTEXT_THEME);
        }

        if (nextProps.theme) {
          // Props theme has a higher priority than Default theme.
          // When it exists, we need to remove Default theme.
          this.removeTheme(DEFAULT_THEME);
        }
      }
    }

    componentWillUnmount() {
      this.removeTheme(CONTEXT_THEME);
      this.removeTheme(DEFAULT_THEME);
      this.removeTheme(PROPS_THEME);
    }

    insertTheme(type, theme) {
      if (theme) {
        this.removeThemes[type] = this.context.insertCss.apply(
          undefined, [theme]
        );
      }
    }

    removeTheme(type) {
      if (this.removeThemes[type]) {
        setTimeout(this.removeThemes[type], 0);
      }
    }

    render() {
      return <ThemrComponent {...this.props} />;
    }
  }

  WithThemr.ComposedComponent = ThemrComponent;

  return hoistStatics(WithThemr, ComposedComponent);
};

@kleinspire
Copy link

kleinspire commented Nov 21, 2016

Note that you would still have to wrap the react-toolbox components like this though:

import { Button } from 'react-toolbox/lib/button/Button';
import withThemr from './withThemr'; // the code I posted above

export default withThemr('RTButton')(Button);

and pass the following context theme object to ThemeProvider just like you would with react-css-themr:

{
  /* eslint-disable global-require */
  RTButton: require('react-toolbox/lib/button/theme.scss'),
  /* eslint-enable global-require */
}

import { Button } from 'react-toolbox/lib/button/Button'; line imports react-toolbox Button which is not already pre-wrapped with themr.

It's not a perfect solution, but it makes much easier to meet the styling requirements of different components in your app.

@creeperyang
Copy link

Just the same case. Maybe a babel-plugin or webpack plugin will be the best solution.

@wootencl
Copy link

wootencl commented Sep 15, 2017

@jazblueful First of all, thanks for providing that withStyles.js. It's the only approach I've come across so far that actually seems to work with isomorphic-style-loader, react-start-kit, and react-toolbox. One quick question: I'm attempting to use the Button component from RT at the moment and it somewhat works. The issue is that there doesn't seem to be any ripple effect. This make some sense as Ripple is a separate component. With that, I'm trying to figure out how to include the Ripple styles for the Button component using the above suggested approach. Would be curious if you have any advice. Thanks in advance 🙂

Code for the curious:

theme.js

export default {
  /* eslint-disable global-require */
  RTButton: require('react-toolbox/lib/button/theme.css'),
  RTInput: require('react-toolbox/lib/input/theme.css')
}

button.js

import { Button } from 'react-toolbox/lib/button/Button';
import withThemr from './withThemr'; 

export default withThemr('RTButton')(Button);

RTComponent.js

import React from 'react';
import PropTypes from 'prop-types';
import withStyles from 'isomorphic-style-loader/lib/withStyles';

import s from './Login.css';
import Button from '../../react-toolbox/button';

class RTComponent extends React.Component {
  ...
  render() {
    return (
      <div className={s.root}>
        <div className={s.container}>
          ...
          <form onSubmit={this.handleLogin}>
            ...
            <div className={s.formGroup}>
              <Button type="submit"
                      ripple={true}>
                Log in
              </Button>
            </div>
          </form>
        </div>
      </div>
    );
  }
}

export default withStyles(s)(Login);

@creeperyang
Copy link

creeperyang commented Sep 16, 2017

To solve the same problem, I checked the isomorphic-style-loader and universal-style-loader. It seems they all could not work well with react-toolbox.

And finally, I wrote my own loader iso-morphic-style-loader: It works well both server side and browser side.

It does not have to write withStyles, and nor to include react-toolbox's style manually. Just configure webpack.config.js and write some universal code if you need ssr.

(Project react-starter-kit)

// webpack.config.js

const REACT_TOOLBOX_PATH = path.resolve(__dirname, '../node_modules/react-toolbox')

// Rules for Style Sheets
      {
        test: reStyle,
        rules: [
          // Convert CSS into JS module
          {
            issuer: { not: [reStyle, REACT_TOOLBOX_PATH] },
            use: 'isomorphic-style-loader',
          },
          // Handle react-toolbox style alone since it cannot intergrate with isomorphic-style-loader well.
          {
            issuer: REACT_TOOLBOX_PATH,
            use: 'iso-morphic-style-loader'
          },

          // Process external/third-party styles
          {
            exclude: [path.resolve(__dirname, '../src'), REACT_TOOLBOX_PATH],
            loader: 'css-loader',
            options: {
              sourceMap: isDebug,
              minimize: !isDebug,
              discardComments: { removeAll: true },
            },
          },

          // Process internal/project styles (from src folder)
          {
            include: [path.resolve(__dirname, '../src'), REACT_TOOLBOX_PATH],
            use: CSS_LOADERS
          }
        ]
      },

And if you need ssr (almost the same as react-starter-kit):

// server.js
// styles handled by iso-morphic-style-loader
    if (global.__universal__) {
      global.__universal__.forEach(v => {
        data.styles.push({
          ...v.attrs,
          id: v.id,
          cssText: v.content.join('\n')
        })
      })
    }

// html.js
{styles.map(({id, cssText, ...rest}) =>
            <style
              {...rest}
              key={id}
              id={id}
              dangerouslySetInnerHTML={{ __html: cssText }}
            />
          )}

@wootencl
Copy link

wootencl commented Sep 18, 2017

@creeperyang Interesting. I'll have to take a look at it. Out of curiosity, when you need the RT component which way do you import? Like this:
import Input from 'react-toolbox/lib/input/Input';
or this:
import Input from 'react-toolbox/lib/input';

Also, when you say there's no longer a need for withStyles are you referring to the withStyles used for component styling (i.e. - export default withStyles(s)(Login);?

@creeperyang
Copy link

Yeah, it's what I mean.

iso-morphic-style-loader fully compatible with style-loader, which means all require 'xxx.css'/import 'xxx.css' will be loaded correctly. And it supports ssr via export styles to global.__universal__. So no export default withStyles(s)(Login) any more.

In my own project, I prefer import Input from 'react-toolbox/lib/input'.

@wootencl
Copy link

hmm... still getting the unexpected token error:

.../react-toolbox/lib/input/theme.css:1
(function (exports, require, module, __filename, __dirname) { :root {

SyntaxError: Unexpected token :

Here's the relevant webpack config if that helps:

      {
        test: reStyle,
        rules: [
          // Convert CSS into JS module
          {
            issuer: { not: [reStyle, REACT_TOOLBOX_PATH] },
            use: 'isomorphic-style-loader',
          },
          // Handle react-toolbox style alone since it cannot intergrate with isomorphic-style-loader well.
          {
            issuer: REACT_TOOLBOX_PATH,
            use: 'iso-morphic-style-loader'
          },
          // Process external/third-party styles
          {
            exclude: [
               path.resolve(__dirname, '../src'),
               REACT_TOOLBOX_PATH
            ],
            loader: 'css-loader',
            options: {
              sourceMap: isDebug,
              minimize: !isDebug,
              discardComments: { removeAll: true },
            },
          },

          // Process internal/project styles (from src folder)
          {
            include: [
              path.resolve(__dirname, '../src'),
              REACT_TOOLBOX_PATH
            ],
            loader: 'css-loader',
            options: {
              // CSS Loader https://github.com/webpack/css-loader
              importLoaders: 1,
              sourceMap: isDebug,
              // CSS Modules https://github.com/css-modules/css-modules
              modules: true,
              localIdentName: isDebug
                ? '[name]-[local]-[hash:base64:5]'
                : '[hash:base64:5]',
              // CSS Nano http://cssnano.co/options/
              minimize: !isDebug,
              discardComments: { removeAll: true },
            },
          },

          // Apply PostCSS plugins including autoprefixer
          {
            loader: 'postcss-loader',
            options: {
              config: {
                path: './tools/postcss.config.js',
              },
            },
          },
          {
            test: /\.scss$/,
            loader: 'sass-loader',
          },
        ],
      }

Thanks for trying to help me with this 🙂

@creeperyang
Copy link

@wootencl It looks like that you should handle node_modules/react-toolbox/*.js alone.

// Rules for JS / JSX
      {
        test: reScript,
        include: [path.resolve(__dirname, '../src'), REACT_TOOLBOX_PATH],
        loader: 'babel-loader',
        options: {
          // https://github.com/babel/babel-loader#options
          cacheDirectory: isDebug,

          // https://babeljs.io/docs/usage/options/
          babelrc: false,
          presets: [
            // A Babel preset that can automatically determine the Babel plugins and polyfills
            // https://github.com/babel/babel-preset-env
            [
              'env',
              {
                targets: {
                  browsers: BROWSER_LIST,
                  uglify: true,
                },
                modules: false,
                useBuiltIns: false,
                debug: false
              }
            ],
            // Experimental ECMAScript proposals
            // https://babeljs.io/docs/plugins/#presets-stage-x-experimental-presets-
            'stage-2',
            // JSX, Flow
            // https://github.com/babel/babel/tree/master/packages/babel-preset-react
            'react',
            // Optimize React code for the production build
            // https://github.com/thejameskyle/babel-react-optimize
            ...(isDebug ? [] : ['react-optimize']),
          ],
          plugins: [
            // Adds component stack to warning messages
            // https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-react-jsx-source
            ...(isDebug ? ['transform-react-jsx-source'] : []),
            // Adds __self attribute to JSX which React will use for some warnings
            // https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-react-jsx-self
            ...(isDebug ? ['transform-react-jsx-self'] : []),
          ]
        }
      },

My whole webpack.config.js below:

`webpack.config.js`
import path from 'path'
import webpack from 'webpack'
import AssetsPlugin from 'assets-webpack-plugin'
import nodeExternals from 'webpack-node-externals'
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'
import pkg from '../package.json'

const isDebug = !process.argv.includes('--release')
const isVerbose = process.argv.includes('--verbose')
const isAnalyze =
  process.argv.includes('--analyze') || process.argv.includes('--analyse')

const reScript = /\.jsx?$/
const reStyle = /\.(css|less|scss|sss)$/
const reFont = /\.(ttf|eot|woff2?)$/
const reImage = /\.(bmp|gif|jpe?g|png|svg)$/
const staticAssetName = isDebug
  ? '[path][name].[ext]?[hash:8]'
  : '[hash:8].[ext]'

const BROWSER_LIST = [
  '>1%',
  'last 3 versions',
  'Firefox ESR',
  'not ie < 9'
]

const REACT_TOOLBOX_PATH = path.resolve(__dirname, '../node_modules/react-toolbox')

const CSS_LOADERS = [
  {
    loader: 'css-loader',
    options: {
      // CSS Loader https://github.com/webpack/css-loader
      importLoaders: 1,
      sourceMap: isDebug,
      // CSS Modules https://github.com/css-modules/css-modules
      modules: true,
      localIdentName: isDebug
        ? '[name]-[local]-[hash:base64:5]'
        : '[hash:base64:5]',
      // CSS Nano http://cssnano.co/options/
      minimize: !isDebug,
      discardComments: { removeAll: true },
    }
  },
  {
    loader: 'postcss-loader',
    options: {
      plugins: (loader) => [
        require('postcss-cssnext')({
          // Custom react-toolbox theme
          features: {
            customProperties: {
              variables: {
                '--appbar-color': '#3293d2',
                '--tab-pointer-color': '#3293d2',
                '--input-text-highlight-color': '#3293d2',
                '--preferred-font': `-apple-system,Helvetica,Arial,Tahoma,"PingFang SC","Hiragino Sans GB","Lantinghei SC","Microsoft YaHei",sans-serif`
              }
            }
          }
        })
      ]
    }
  }
]

const config = {
  context: path.resolve(__dirname, '..'),

  output: {
    path: path.resolve(__dirname, '../build/public/assets'),
    publicPath: '/assets/',
    pathinfo: isVerbose,
    filename: isDebug ? '[name].js' : '[name].[chunkhash:8].js',
    chunkFilename: isDebug
      ? '[name].chunk.js'
      : '[name].[chunkhash:8].chunk.js',
    devtoolModuleFilenameTemplate: info =>
      path.resolve(info.absoluteResourcePath),
  },

  resolve: {
    // Allow absolute paths in imports, e.g. import Button from 'components/Button'
    // Keep in sync with .flowconfig and .eslintrc
    modules: ['node_modules', 'src'],
    alias: {
      'fonts.css': path.resolve(__dirname, '../public/styles/fonts.css')
    }
  },

  module: {
    // Make missing exports an error instead of warning
    strictExportPresence: true,
    rules: [
      // Rules for JS / JSX
      {
        test: reScript,
        include: [path.resolve(__dirname, '../src'), REACT_TOOLBOX_PATH],
        loader: 'babel-loader',
        options: {
          // https://github.com/babel/babel-loader#options
          cacheDirectory: isDebug,

          // https://babeljs.io/docs/usage/options/
          babelrc: false,
          presets: [
            // A Babel preset that can automatically determine the Babel plugins and polyfills
            // https://github.com/babel/babel-preset-env
            [
              'env',
              {
                targets: {
                  browsers: BROWSER_LIST,
                  uglify: true,
                },
                modules: false,
                useBuiltIns: false,
                debug: false
              }
            ],
            // Experimental ECMAScript proposals
            // https://babeljs.io/docs/plugins/#presets-stage-x-experimental-presets-
            'stage-2',
            // JSX, Flow
            // https://github.com/babel/babel/tree/master/packages/babel-preset-react
            'react',
            // Optimize React code for the production build
            // https://github.com/thejameskyle/babel-react-optimize
            ...(isDebug ? [] : ['react-optimize']),
          ],
          plugins: [
            // Adds component stack to warning messages
            // https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-react-jsx-source
            ...(isDebug ? ['transform-react-jsx-source'] : []),
            // Adds __self attribute to JSX which React will use for some warnings
            // https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-react-jsx-self
            ...(isDebug ? ['transform-react-jsx-self'] : []),
          ]
        }
      },
      // Rules for Style Sheets
      {
        test: reStyle,
        rules: [
          // Convert CSS into JS module
          {
            issuer: { not: [reStyle, REACT_TOOLBOX_PATH] },
            use: 'isomorphic-style-loader',
          },
          // Handle react-toolbox style since it cannot intergrate with isomorphic-style-loader well.
          {
            issuer: REACT_TOOLBOX_PATH,
            use: 'iso-morphic-style-loader'
          },

          // Process external/third-party styles
          {
            exclude: [path.resolve(__dirname, '../src'), REACT_TOOLBOX_PATH],
            loader: 'css-loader',
            options: {
              sourceMap: isDebug,
              minimize: !isDebug,
              discardComments: { removeAll: true },
            },
          },

          // Process internal/project styles (from src folder)
          {
            include: [path.resolve(__dirname, '../src'), REACT_TOOLBOX_PATH],
            use: CSS_LOADERS
          }
        ]
      },
      // Rules for fonts
      {
        test: reFont,
        use: [
          {
            loader: 'url-loader',
            options: {
              name: staticAssetName,
              limit: 1 // always use url instead of base64
            }
          }
        ]
      },
      // Rules for images
      {
        test: reImage,
        oneOf: [
          // Inline lightweight images into CSS
          {
            issuer: reStyle,
            oneOf: [
              // Inline lightweight SVGs as UTF-8 encoded DataUrl string
              {
                test: /\.svg$/,
                loader: 'svg-url-loader',
                options: {
                  name: staticAssetName,
                  limit: 4096 // 4kb
                }
              },

              // Inline lightweight images as Base64 encoded DataUrl string
              {
                loader: 'url-loader',
                options: {
                  name: staticAssetName,
                  limit: 4096 // 4kb
                }
              }
            ]
          },

          // Or return public URL to image resource
          {
            loader: 'file-loader',
            options: {
              name: staticAssetName,
            }
          }
        ]
      },
      {
        test: /lottie_files\/.+\.json$/,
        loader: 'file-loader',
        options: {
          name: staticAssetName
        }
      },
      // Exclude dev modules from production build
      ...(isDebug
        ? []
        : [
            {
              test: path.resolve(
                __dirname,
                '../node_modules/react-deep-force-update/lib/index.js',
              ),
              loader: 'null-loader',
            },
          ])
    ]
  },

  // Don't attempt to continue if there are any errors.
  bail: !isDebug,

  cache: isDebug,

  // Specify what bundle information gets displayed
  // https://webpack.js.org/configuration/stats/
  stats: {
    cached: isVerbose,
    cachedAssets: isVerbose,
    chunks: isVerbose,
    chunkModules: isVerbose,
    colors: true,
    hash: isVerbose,
    modules: isVerbose,
    reasons: isDebug,
    timings: true,
    version: isVerbose
  },

  // Choose a developer tool to enhance debugging
  // https://webpack.js.org/configuration/devtool/#devtool
  devtool: isDebug ? 'cheap-module-inline-source-map' : false
}

//
// Configuration for the client-side bundle (client.js)
// -----------------------------------------------------------------------------

const clientConfig = {
  ...config,

  name: 'client',
  target: 'web',

  entry: {
    client: ['babel-polyfill', './src/client.js']
  },

  plugins: [
    // Define free variables
    // https://webpack.js.org/plugins/define-plugin/
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': isDebug ? '"development"' : '"production"',
      'process.env.BROWSER': true,
      __DEV__: isDebug,
    }),

    // Emit a file with assets paths
    // https://github.com/sporto/assets-webpack-plugin#options
    new AssetsPlugin({
      path: path.resolve(__dirname, '../build'),
      filename: 'assets.json',
      prettyPrint: true,
    }),

    // Move modules that occur in multiple entry chunks to a new entry chunk (the commons chunk).
    // https://webpack.js.org/plugins/commons-chunk-plugin/
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: module => {
        return module.resource && /node_modules/.test(module.resource)
      }
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'client',
      async: 'chunk-vendor',
      children: true,
      minChunks: (module, count) => {
        return count >= 3
      }
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime',
      minChunks: Infinity
    }),

    ...(isDebug
      ? []
      : [
          // Decrease script evaluation time
          // https://github.com/webpack/webpack/blob/master/examples/scope-hoisting/README.md
          new webpack.optimize.ModuleConcatenationPlugin(),

          // Minimize all JavaScript output of chunks
          // https://github.com/mishoo/UglifyJS2#compressor-options
          new webpack.optimize.UglifyJsPlugin({
            sourceMap: true,
            compress: {
              screw_ie8: true, // React doesn't support IE8
              warnings: isVerbose,
              unused: true,
              dead_code: true
            },
            mangle: {
              screw_ie8: true
            },
            output: {
              comments: false,
              screw_ie8: true
            }
          })
        ]),

    // Webpack Bundle Analyzer
    // https://github.com/th0r/webpack-bundle-analyzer
    ...(isAnalyze ? [new BundleAnalyzerPlugin()] : [])
  ],

  // Some libraries import Node modules but don't use them in the browser.
  // Tell Webpack to provide empty mocks for them so importing them works.
  // https://webpack.js.org/configuration/node/
  // https://github.com/webpack/node-libs-browser/tree/master/mock
  node: {
    fs: 'empty',
    net: 'empty',
    tls: 'empty'
  }
}

//
// Configuration for the server-side bundle (server.js)
// -----------------------------------------------------------------------------

const serverConfig = {
  ...config,

  name: 'server',
  target: 'node',

  entry: {
    server: ['babel-polyfill', './src/server.js'],
  },

  output: {
    ...config.output,
    path: path.resolve(__dirname, '../build'),
    filename: '[name].js',
    chunkFilename: 'chunks/[name].js',
    libraryTarget: 'commonjs2',
  },

  // Webpack mutates resolve object, so clone it to avoid issues
  // https://github.com/webpack/webpack/issues/4817
  resolve: {
    ...config.resolve,
  },

  module: {
    ...config.module,

    rules: overrideRules(config.module.rules, rule => {
      // Override babel-preset-env configuration for Node.js
      if (rule.loader === 'babel-loader') {
        return {
          ...rule,
          options: {
            ...rule.options,
            presets: rule.options.presets.map(
              preset =>
                preset[0] !== 'env'
                  ? preset
                  : [
                      'env',
                      {
                        targets: {
                          node: pkg.engines.node.match(/(\d+\.?)+/)[0],
                        },
                        modules: false,
                        useBuiltIns: false,
                        debug: false,
                      },
                    ]
            )
          }
        };
      }
      // Override paths to static assets
      if (
        rule.loader === 'file-loader' ||
        rule.loader === 'url-loader' ||
        rule.loader === 'svg-url-loader'
      ) {
        return {
          ...rule,
          options: {
            ...rule.options,
            name: `public/assets/${rule.options.name}`,
            publicPath: url => url.replace(/^public/, '')
          }
        }
      }

      return rule
    })
  },

  externals: [
    './assets.json',
    nodeExternals({
      whitelist: [reStyle, reImage, /react-toolbox/],
    }),
  ],

  plugins: [
    // Define free variables
    // https://webpack.js.org/plugins/define-plugin/
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': isDebug ? '"development"' : '"production"',
      'process.env.BROWSER': false,
      __DEV__: isDebug,
    }),

    // Adds a banner to the top of each generated chunk
    // https://webpack.js.org/plugins/banner-plugin/
    new webpack.BannerPlugin({
      banner: 'require("source-map-support").install();',
      raw: true,
      entryOnly: false
    })
  ],

  // Do not replace node globals with polyfills
  // https://webpack.js.org/configuration/node/
  node: {
    console: false,
    global: false,
    process: false,
    Buffer: false,
    __filename: false,
    __dirname: false
  }
}

function overrideRules(rules, patch) {
  return rules.map(ruleToPatch => {
    let rule = patch(ruleToPatch);
    if (rule.rules) {
      rule = { ...rule, rules: overrideRules(rule.rules, patch) };
    }
    if (rule.oneOf) {
      rule = { ...rule, oneOf: overrideRules(rule.oneOf, patch) };
    }
    return rule;
  });
}

module.exports = [clientConfig, serverConfig]

@wootencl
Copy link

Woohoo! Thanks a ton @creeperyang! 🎉 I just swapped out my webpack config for yours (as I'm still fairly close to the initial kriasoft one) and react-toolbox components seem to be working as expected now.

Had to make some slight modifications in order to make it work (would be curious if you have thoughts as to why):

Commented out this section:

{
    loader: 'postcss-loader',
    options: {
      plugins: (loader) => [
        require('postcss-cssnext')({
          // Custom react-toolbox theme
          features: {
            customProperties: {
              variables: {
                '--appbar-color': '#3293d2',
                '--tab-pointer-color': '#3293d2',
                '--input-text-highlight-color': '#3293d2',
                '--preferred-font': `-apple-system,Helvetica,Arial,Tahoma,"PingFang SC","Hiragino Sans GB","Lantinghei SC","Microsoft YaHei",sans-serif`
              }
            }
          }
        })
      ]
    }
  }

Was getting this error at compile time: TypeError: Cannot read property 'postcssVersion' of null

Changed these:

// Move modules that occur in multiple entry chunks to a new entry chunk (the commons chunk).
    // https://webpack.js.org/plugins/commons-chunk-plugin/
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: module => {
        return module.resource && /node_modules/.test(module.resource)
      }
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'client',
      async: 'chunk-vendor',
      children: true,
      minChunks: (module, count) => {
        return count >= 3
      }
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime',
      minChunks: Infinity
    }),

into:

new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: module => /node_modules/.test(module.resource),
    }),

Thanks again!

@Rykus0
Copy link

Rykus0 commented Jun 16, 2021

The answer I was hoping to get from this question is: How does this work when you are providing a component library and you have no control over the client?

I can't require my clients to add the specified context code. So what do I do?

import React from 'react'
import ReactDOM from 'react-dom'
import StyleContext from 'isomorphic-style-loader/StyleContext'
import App from './App.js'

const insertCss = (...styles) => {
  const removeCss = styles.map(style => style._insertCss())
  return () => removeCss.forEach(dispose => dispose())
}

ReactDOM.hydrate(
  <StyleContext.Provider value={{ insertCss }}>
    <App />
  </StyleContext.Provider>,
  document.getElementById('root')
)

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

No branches or pull requests

9 participants