Sage 9 w/ React

This set up seems to be working well for me getting React components working with Webpack HMR and ESLint.

Hopefully this is of some use to others.

WEBPACK.CONFIG.JS
’use strict’ // eslint-disable-line

const webpack = require('webpack')
const merge = require('webpack-merge')
const autoprefixer = require('autoprefixer')
const CleanPlugin = require('clean-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const StyleLintPlugin = require('stylelint-webpack-plugin')

const CopyGlobsPlugin = require('copy-globs-webpack-plugin')
const config = require('./config')

const assetsFilenames = config.enabled.cacheBusting ? config.cacheBusting : '[name]'
const sourceMapQueryStr = config.enabled.sourceMaps ? '+sourceMap' : '-sourceMap'

let webpackConfig = {
  context: config.paths.assets,
  entry: config.entry,
  devtool: config.enabled.sourceMaps ? '#source-map' : undefined,
  output: {
    path: config.paths.dist,
    publicPath: config.publicPath,
    filename: `scripts/${assetsFilenames}.js`,
  },
  module: {
    rules: [
      {
        enforce: 'pre',
        test: /\.js?$/,
        include: config.paths.assets,
        use: 'eslint',
      },
      {
        test: /\.jsx?$/,
        exclude: [/(node_modules|bower_components)(?![/|\\](bootstrap|foundation-sites))/],
        loader: 'babel',
        query: {
          presets: ['env', 'es2015', 'react'],
        },
      },
      {
        test: /\.css$/,
        include: config.paths.assets,
        use: ExtractTextPlugin.extract({
          fallback: 'style',
          publicPath: '../',
          use: [`css?${sourceMapQueryStr}`, 'postcss'],
        }),
      },
      {
        test: /\.scss$/,
        include: config.paths.assets,
        use: ExtractTextPlugin.extract({
          fallback: 'style',
          publicPath: '../',
          use: [
            `css?${sourceMapQueryStr}`,
            'postcss',
            `resolve-url?${sourceMapQueryStr}`,
            `sass?${sourceMapQueryStr}`,
          ],
        }),
      },
      {
        test: /\.(ttf|eot|png|jpe?g|gif|svg|ico)$/,
        include: config.paths.assets,
        loader: 'file',
        options: {
          name: `[path]${assetsFilenames}.[ext]`,
        },
      },
      {
        test: /\.woff2?$/,
        include: config.paths.assets,
        loader: 'url',
        options: {
          limit: 10000,
          mimetype: 'application/font-woff',
          name: `[path]${assetsFilenames}.[ext]`,
        },
      },
      {
        test: /\.(ttf|eot|woff2?|png|jpe?g|gif|svg)$/,
        include: /node_modules|bower_components/,
        loader: 'file',
        options: {
          name: `vendor/${config.cacheBusting}.[ext]`,
        },
      },
    ],
  },
  resolve: {
    modules: [config.paths.assets, 'node_modules', 'bower_components'],
    enforceExtension: false,
  },
  resolveLoader: {
    moduleExtensions: ['-loader'],
  },
  externals: {
    jquery: 'jQuery',
  },
  plugins: [
    new CleanPlugin([config.paths.dist], {
      root: config.paths.root,
      verbose: false,
    }),
    /**
         * It would be nice to switch to copy-webpack-plugin, but
         * unfortunately it doesn't provide a reliable way of
         * tracking the before/after file names
         */
    new CopyGlobsPlugin({
      pattern: config.copy,
      output: `[path]${assetsFilenames}.[ext]`,
      manifest: config.manifest,
    }),
    new ExtractTextPlugin({
      filename: `styles/${assetsFilenames}.css`,
      allChunks: true,
      disable: config.enabled.watcher,
    }),
    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery',
      'window.jQuery': 'jquery',
      Tether: 'tether',
      'window.Tether': 'tether',
    }),
    new webpack.LoaderOptionsPlugin({
      minimize: config.enabled.optimize,
      debug: config.enabled.watcher,
      stats: { colors: true },
    }),
    new webpack.LoaderOptionsPlugin({
      test: /\.s?css$/,
      options: {
        output: { path: config.paths.dist },
        context: config.paths.assets,
        postcss: [autoprefixer()],
      },
    }),
    new webpack.LoaderOptionsPlugin({
      test: /\.js$/,
      options: {
        eslint: { failOnWarning: false, failOnError: true },
      },
    }),
    new StyleLintPlugin({
      failOnError: !config.enabled.watcher,
      syntax: 'scss',
    }),
  ],
}

/* eslint-disable global-require */
/** Let's only load dependencies as needed */

if (config.enabled.optimize) {
  webpackConfig = merge(webpackConfig, require('./webpack.config.optimize'))
}

if (config.env.production) {
  webpackConfig.plugins.push(new webpack.NoEmitOnErrorsPlugin())
}

if (config.enabled.cacheBusting) {
  const WebpackAssetsManifest = require('webpack-assets-manifest')

  webpackConfig.plugins.push(
    new WebpackAssetsManifest({
      output: 'assets.json',
      space: 2,
      writeToDisk: false,
      assets: config.manifest,
      replacer: require('./util/assetManifestsFormatter'),
    })
  )
}

if (config.enabled.watcher) {
  webpackConfig.entry = require('./util/addHotMiddleware')(webpackConfig.entry)
  webpackConfig = merge(webpackConfig, require('./webpack.config.watch'))
}

module.exports = webpackConfig

BABEL.RC

{
    "presets": ["transform-runtime", "es2015", "stage-0", "react"]
}

ESLINT.RC

{
  "root": true,
  "extends": [
        "eslint:recommended",
        "plugin:react/recommended"
    ],
  "globals": {
    "wp": true
  },
  "env": {
    "node": true,
    "es6": true,
    "amd": true,
    "browser": true,
    "jquery": true
  },
  "parser": "babel-eslint",
  "parserOptions": {
    "ecmaFeatures": {
      "globalReturn": true,
      "jsx": true,
      "modules": true,
      "generators": false,
      "objectLiteralDuplicateProperties": false,
      "experimentalObjectRestSpread": true
    },
    "ecmaVersion": 2017,
    "sourceType": "module"
  },
  "plugins": [
    "import",
    "react",
  ],
  "settings": {
    "import/core-modules": [],
    "import/ignore": [
      "node_modules",
      "\\.(coffee|scss|css|less|hbs|svg|json)$"
    ]
  },
  "rules": {
    "comma-dangle": ["error", {
      "arrays": "always-multiline",
      "objects": "always-multiline",
      "imports": "always-multiline",
      "exports": "always-multiline",
      "functions": "ignore"
    }],
    "semi": 0,
  }
}
3 Likes

Unexpected token : you are using brackets instead of parenthesis in your return statement.

  render() {
    return (
      <span/>
    );
  }

Hope that helps!

For a working sample of intergation between sage and react you can refer to https://github.com/raminv80/bedrock-sage-react/tree/master/web/app/themes/bemore

1 Like

I just wanted to thank you @raminv80 for your repo, it really helped me to get setup with React on the Roots stack and likely saved me days of effort.

I’ve got an issue with this setup. I can’t import .jsx files, but I can import .js files containing jsx code.

What I’ve done so far:

$ yarn add react react-dom
$ yarn add --dev eslint-plugin-react
//.eslintrc.js
module.exports = {
  root: true,
  extends: ['eslint:recommended', 'plugin:react/recommended'],
  //...
  parserOptions: {
    ecmaFeatures: {
      // ...
      jsx: true,
    }
  }
}
//home.js
import React from 'react';
import ReactDOM from 'react-dom';
import Calculator from './../components/calculator.jsx';

export default {
  init() {
    ReactDOM.render(<Calculator />,document.getElementById('calculator')
    );
  },
};

This throws following error:

ERROR in ./scripts/components/calculator.jsx 6:12
Module parse failed: Unexpected token (6:12)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
|   // state = {  }
|   render() {
>     return (<h1>Calculator</h1>);
|   }
| }
 @ ./scripts/routes/home.js 3:0-56 10:29-39
 @ ./scripts/main.js 10:0-33 18:8-12

When renaming the react component to calculator.js (instead of .jsx), and updating the import accordingly, the react component is properly rendered. I tried changing the parser to bable-eslint instead of buble, but that didn’t change anything.

By the way, if you need webpack 5 and latest dependencies, it may be helpful to use this Sage 9.x update branch:

And yes, I’m using the update branch :slight_smile:

I replicated the setup. To get it working with jsx you also have to configure the loader (in this case we use buble-loader for buble). For this, in resources/assets/build/webpack.config.js, just replace

      {
        test: /\.js$/,
        exclude: [/node_modules(?![/|\\](bootstrap|foundation-sites))/],
        use: [
          { loader: 'buble-loader', options: { objectAssign: 'Object.assign' } },
        ],
      },

with

      {
        test: /\.(js|jsx)$/,
        exclude: [/node_modules(?![/|\\](bootstrap|foundation-sites))/],
        use: [
          { loader: 'buble-loader', options: { objectAssign: 'Object.assign' } },
        ],
      },

The regexp is changed to match js and jsx files.

Note: For external components that use jsx one would have to adjust the exclude filter so it doesn’t exclude these folders in node_modules/.

1 Like

It’s funny, the problem you are describing is what want! I much prefer not to give the jsx extension.

So I just removed the jsx regexp modification. Then I renamed components/calculator.jsx to components/calculator.js and also adjusted the import in routes/home.js so its path uses .js.
npm run build just works fine, so apparently no changes to configuration were necessary with the default setup.

Thanks once again strarsis. Looks like bublé has some issues with ES6 features, for example the property initializer syntax which is a common pattern in React Components.

Since Bublé doesn’t fancy plugins and has not yet implemented this (see https://github.com/bublejs/buble/issues/123), I’ve either have to change to babel, or being limited regarding js syntax.

It seems nothing much is happening with Bublé (no updates in a year). Isn’t babel the better choice for transpiling js?

Yes, buble is used mostly for supporting old browsers like IE11 (which is quite dead now luckily) and for improved build-performance (for less features), It is maintained only minimally and its developers also now recommend switching to another transpiler, like Babel.

With webpack (as in Sage 9.x update branch) luckily it is trivially easy to replace buble with babel.
See this commit/diff of webpack-babel branch:

npm install --save-dev @babel/core @babel/preset-env babel-loader
npm uninstall --save-dev buble-loader
npm uninstall --save buble

2 Likes

It took me a while to figure out what I was doing wrong. I had a few errors that were caused by the eslint plugin from vscode, which was throwing errors in vscode although webpack was able to build. After disabling the vscode plugin, I got things up and running like this (similar to strarsis’ setup as described above, but with react and jsx support).

So, for future reference, here’s the current Sage9 + react setup:

$ yarn add react react-dom
$ yarn add --dev eslint-plugin-react @babel/core @babel/preset-env @babel/preset-react babel-loader 
// .babelrc
{
  "presets": [ "@babel/preset-env", "@babel/preset-react"]
}
// .eslintrc.js
module.exports = {
  root: true,
  extends: ['eslint:recommended', 'plugin:react/recommended'],
  // ...
  parser: '@babel/eslint-parser',
  //...
}
// webpack.config.js
// add to webpackconfig.module.rules
{
  test: /\.(js|jsx)$/,
  exclude: [/node_modules(?![/|\\](bootstrap|foundation-sites))/],
  use: ['babel-loader'],
},