Can I use webpack to generate CSS and JS separately?

I have:

  1. JS files that I want to bundle.
  2. LESS files that I want to compile down to CSS (resolving @imports into a single bundle).

I was hoping to specify these as two separate inputs and have two separate outputs (likely via extract-text-webpack-plugin). Webpack has all the proper plugins/loaders to do compilation, but it doesn't seem to like the separation.

I've seen examples of people requiring their LESS files directly from JS, such as require('./app.less');, for no other reason than to tell webpack to include those files into the bundle. This allows you to only have a single entry point, but it seems really wrong to me -- why would I require LESS in my JS when it has nothing to do with my JS code?

I tried using multiple entry points, handing both the entry JS and main LESS file in, but when using multiple entry points, webpack generates a bundle that doesn't execute the JS on load -- it bundles it all, but doesn't know what should be executed on startup.

Am I just using webpack wrong? Should I run separate instances of webpack for these separate modules? Should I even be using webpack for non-JS assets if I'm not going to mix them into my JS?


Should I even be using webpack for non-JS assets if I'm not going to mix them into my JS?

Maybe not. Webpack is definitely js-centric, with the implicit assumption that what you're building is a js application. Its implementation of require() allows you to treat everything as a module (including Sass/LESS partials, JSON, pretty much anything), and automatically does your dependency management for you (everything that you require is bundled, and nothing else).

why would I require LESS in my JS when it has nothing to do with my JS code?

People do this because they're defining a piece of their application (e.g. a React component, a Backbone View) with js. That piece of the application has CSS that goes with it. Depending on some external CSS resource that's built separately and not directly referenced from the js module is fragile, harder to work with, and can lead to styles getting out of date, etc. Webpack encourages you to keep everything modular, so you have a CSS (Sass, whatever) partial that goes with that js component, and the js component require()s it to make the dependency clear (to you and to the build tool, which never builds styles you don't need).

I don't know if you could use webpack to bundle CSS on its own (when the CSS files aren't referenced from any js). I'm sure you could wire something up with plugins, etc., but not sure it's possible out of the box. If you do reference the CSS files from your js, you can easily bundle the CSS into a separate file with the Extract Text plugin, as you say.


webpack 4 solution with mini-css-extract plugin

the webpack team recommends using mini-css-extract over the extract text plugin

this solution allows you to create a separate chunk containing only your css entries:

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

function recursiveIssuer(m) {
  if (m.issuer) {
    return recursiveIssuer(m.issuer);
  } else if (m.name) {
    return m.name;
  } else {
    return false;
  }
}

module.exports = {
  entry: {
    foo: path.resolve(__dirname, 'src/foo'),
    bar: path.resolve(__dirname, 'src/bar'),
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        fooStyles: {
          name: 'foo',
          test: (m, c, entry = 'foo') =>
            m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
          chunks: 'all',
          enforce: true,
        },
        barStyles: {
          name: 'bar',
          test: (m, c, entry = 'bar') =>
            m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
          chunks: 'all',
          enforce: true,
        },
      },
    },
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
    }),
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader'],
      },
    ],
  },
};

Here is a more contrived example using mutliple entries from one of my personal projects:

const ManifestPlugin = require('webpack-manifest-plugin')
const webpack = require('webpack')
const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const VENDOR = path.join(__dirname, 'node_modules')
const LOCAL_JS = path.join(__dirname, 'app/assets/js')
const LOCAL_SCSS = path.join(__dirname, 'app/assets/scss')
const BUILD_DIR = path.join(__dirname, 'public/dist')
const EXTERNAL = path.join(__dirname, 'public/external')

function recursiveIssuer(m) {
  if (m.issuer) {
    return recursiveIssuer(m.issuer);
  } else if (m.name) {
    return m.name;
  } else {
    return false;
  }
}

module.exports = {
  entry: {
    vendor: [
      `${VENDOR}/jquery/dist/jquery.js`,
      `${VENDOR}/codemirror/lib/codemirror.js`,
      `${VENDOR}/codemirror/mode/javascript/javascript.js`,
      `${VENDOR}/codemirror/mode/yaml/yaml.js`,
      `${VENDOR}/zeroclipboard/dist/ZeroClipboard.js`,
    ],
    app: [
      `${LOCAL_JS}/utils.js`,
      `${LOCAL_JS}/editor.js`,
      `${LOCAL_JS}/clipboard.js`,
      `${LOCAL_JS}/fixtures.js`,
      `${LOCAL_JS}/ui.js`,
      `${LOCAL_JS}/data.js`,
      `${LOCAL_JS}/application.js`,
      `${LOCAL_JS}/google.js`
    ],
    'appStyles': [
      `${EXTERNAL}/montserrat.css`,
      `${EXTERNAL}/icons.css`,
      `${VENDOR}/purecss/pure-min.css`,
      `${VENDOR}/purecss/grids-core-min.css`,
      `${VENDOR}/purecss/grids-responsive-min.css`,
      `${VENDOR}/codemirror/lib/codemirror.css`,
      `${VENDOR}/codemirror/theme/monokai.css`,
    ]
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        appStyles: {
          name: 'appStyles',
          test: (m, c, entry = 'appStyles') =>
            m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
          chunks: 'all',
          enforce: true,
        },
      },
    },
  },
  module:  {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [ 'script-loader'],
      },
      {
        test: /\.(scss|css)$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
        ],
      },
    ],
  },
  mode: 'development',
  resolve: {
    extensions: ['.js', '.css', '.scss']
  },
  output: {
    path: BUILD_DIR,
    filename: "[name].[chunkhash].js",
  },
  plugins: [
    new ManifestPlugin(),
    new MiniCssExtractPlugin({
      filename: '[name].css'
    }),
  ]
};

I realize this approach is not very modular, but it should give you a foundation to build from and is an excellent strategy for adopting webpack in projects where you do not wish to inter-mix javascript and css.

The downside to this approach is that css-loader still generates an additional javascript file (whether you choose to use it or not), this will supposedly be fixed in webpack 5.

Should I even be using webpack for non-JS assets if I'm not going to mix them into my JS?

I don't see anything wrong with this but ultimately it depends on your tolerance for managing multiple build systems. To me this feels like overkill, so my preference is to remain in the webpack ecosystem.

For more information on the strategies outlined above, please see https://github.com/webpack-contrib/mini-css-extract-plugin#extracting-css-based-on-entry


A separate CSS bundle can be generated without using require('main/less) in any of your JS, but as Brendan pointed out in the first part of his answer Webpack isn't designed for a global CSS bundle to go alongside modular JS, however there are a couple of options.

The first is to add an extra entry point for main.less, then use the Extract Text plugin to create the CSS bundle:

var webpack = require('webpack'),
    ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = {
    entry: {
        home: [
            'js/common',
            'js/homepage'
        ],
        style: [
            'styles/main.less'
        ]
    },
    output: {
        path: 'dist',
        filename: "[name].min.js"
    },
    resolve: {
        extensions: ["", ".js"]
    },
    module: {
        loaders: [{
            test: /\.less$/,
            loader: ExtractTextPlugin.extract("style", "css", "less")
        }]
    },
    plugins: [
        new ExtractTextPlugin("[name].min.css", {
            allChunks: true
        })
    ]
};

The problem with this method is you also generate an unwanted JS file as well as the bundle, in this example: style.js which is just an empty Webpack module.

Another option is to add the main less file to an existing Webpack entry point:

var webpack = require('webpack'),
    ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = {
    entry: {
        home: [
            'js/common',
            'js/homepage',
            'styles/main.less'
        ],
    },
    output: {
        path: 'dist',
        filename: "[name].min.js"
    },
    resolve: {
        extensions: ["", ".js"]
    },
    module: {
        loaders: [{
            test: /\.less$/,
            loader: ExtractTextPlugin.extract("style", "css", "less")
        }]
    },
    plugins: [
        new ExtractTextPlugin("[name].min.css", {
            allChunks: true
        })
    ]
};

This is ideal if you have only 1 entry point, but if you have more, then your Webpack config will look a bit odd as you'll have to arbitrarily choose which entry point to add the main less file to.