bikeman868
bikeman868

Reputation: 2637

Webpack works perfectly for development, but production builds do not have any styles applied

My webpack configuration mostly works, but production builds have no css styling applied.

If I run webpack using this node script webpack --mode development then open the ./dist/index.html file in my browser, the website works as expected and the content is styled correctly.

If I run webpack using this node script webpack --mode production then open the ./dist/index.html file in my browser, the website content comes up but with no styling at all applied to the content.

The webpack dev server works perfectly with styled content, live update etc.

I modified my webpack.config.js to comment out the production specific settings, so that production and development builds use the exact same webpack config. I am having a hard time explaining why the development build works but the production build does not even with identical config. Any ideas on how to debug the issue?

My webpack.config.js looks like this:

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = (env, { mode }) => {
  var config = {
    entry: {
      app: './src/app.js',
      react: 'react',
    },
    devtool: 'none',
    devServer: {
      contentBase: path.join(__dirname, 'dist'),
      compress: true,
      hot: true,
      port: 9000,
    },
    resolve: {
      extensions: ['.js', '.jsx', '.css', '.scss', '.sass'],
    },
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'index.[hash].js',
      chunkFilename: '[name].[contenthash].js',
    },
    optimization: {
      runtimeChunk: 'single',
      splitChunks: {
        cacheGroups: {
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendors',
            chunks: 'all',
          },
        },
      },
    },
    plugins: [
      new HtmlWebpackPlugin({
        minify: false,
        template: require('html-webpack-template'),
        inject: true,
        title: 'Octopedia',
        favicon: './public/images/favicon/favicon.ico',
        appMountId: 'app',
        meta: [{ name: 'viewport', content: 'width=device-width, initial-scale=1.0' }],
        links: [{ href: '/site.webmanifest', rel: 'manifest' }],
        appMountHtmlSnippet: '<div class="app-spinner"></div>',
        headHtmlSnippet:
          '<style>div.app-spinner{position:fixed;top:50%;left:50%;border:16px solid #f3f3f3;border-top:16px solid #3498db;border-radius:50%;width:120px;height:120px;margin:-60px 0 0 -60px;z-index:1;animation:spin 2s linear infinite;}@keyframes spin{0%{transform:rotate(0deg);}100%{transform:rotate(360deg);}}</style >',
      }),
      new CopyWebpackPlugin({
        patterns: [{ from: 'public' }],
      }),
    ],
    module: {
      rules: [
        {
          test: /\.(js|jsx)$/,
          exclude: /node_modules/,
          use: {
            loader: 'babel-loader',
            options: {
              cacheDirectory: true,
              cacheCompression: false,
              presets: ['@babel/preset-env', '@babel/preset-react'],
            },
          },
        },
      ],
    },
  };

  // if (mode === 'development') {
    config.mode = 'development';
    config.devtool = 'eval-source-map';
    config.plugins.push(new webpack.HotModuleReplacementPlugin());
    config.module.rules.push({
      test: /\.(sass|scss|css)/,
      exclude: /node_modules/,
      use: ['style-loader', 'css-loader', 'sass-loader'],
    });
  // } else {
  //   config.mode = 'production';
  //   config.devtool = 'none';
  //   config.plugins.push(new MiniCssExtractPlugin({ filename: 'index.css' }));
  //   config.module.rules.push({
  //     test: /\.(sass|scss|css)/,
  //     exclude: /node_modules/,
  //     use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
  //   });
  // }

  return config;
};

The output from webpack in development mode looks like this:

                                    Asset       Size   Chunks                         Chunk Names
              app.6335bee170373d18f82e.js   39.8 KiB      app  [emitted] [immutable]  app
                              favicon.ico     15 KiB           [emitted]              
                         images/.DS_Store      6 KiB           [emitted]              
                 images/favicon/.DS_Store      6 KiB           [emitted]              
images/favicon/android-chrome-192x192.png    8.1 KiB           [emitted]              
images/favicon/android-chrome-512x512.png   23.3 KiB           [emitted]              
      images/favicon/apple-touch-icon.png   7.25 KiB           [emitted]              
         images/favicon/favicon-16x16.png  534 bytes           [emitted]              
         images/favicon/favicon-32x32.png   1.06 KiB           [emitted]              
               images/favicon/favicon.ico     15 KiB           [emitted]              
            index.87da9b99395c61fa6b02.js   33.4 KiB  runtime  [emitted] [immutable]  runtime
                               index.html   1.04 KiB           [emitted]              
            react.a0fc75f932f5dd9cdda1.js  133 bytes    react  [emitted] [immutable]  react
                         site.webmanifest  447 bytes           [emitted]              
          vendors.404f5d4ed29b35b04b91.js   2.41 MiB  vendors  [emitted] [immutable]  vendors
Entrypoint app = index.87da9b99395c61fa6b02.js vendors.404f5d4ed29b35b04b91.js app.6335bee170373d18f82e.js
Entrypoint react = index.87da9b99395c61fa6b02.js vendors.404f5d4ed29b35b04b91.js react.a0fc75f932f5dd9cdda1.js

The output from webpack in production mode loolks like this:

                                    Asset       Size  Chunks                                Chunk Names
              app.170943a8008bc467199f.js   2.77 KiB       2  [emitted] [immutable]         app
                              favicon.ico     15 KiB          [emitted]                     
                         images/.DS_Store      6 KiB          [emitted]                     
                 images/favicon/.DS_Store      6 KiB          [emitted]                     
images/favicon/android-chrome-192x192.png    8.1 KiB          [emitted]                     
images/favicon/android-chrome-512x512.png   23.3 KiB          [emitted]                     
      images/favicon/apple-touch-icon.png   7.25 KiB          [emitted]                     
         images/favicon/favicon-16x16.png  534 bytes          [emitted]                     
         images/favicon/favicon-32x32.png   1.06 KiB          [emitted]                     
               images/favicon/favicon.ico     15 KiB          [emitted]                     
            index.a162f7e38dd204127c0f.js   9.03 KiB       0  [emitted] [immutable]         runtime
                               index.html   1.04 KiB          [emitted]                     
            react.f87e0cf2654ac4f74576.js   71 bytes       3  [emitted] [immutable]         react
                         site.webmanifest  447 bytes          [emitted]                     
          vendors.dd9d4a26464d8d880c74.js    352 KiB       1  [emitted] [immutable]  [big]  vendors

Upvotes: 2

Views: 7819

Answers (2)

bikeman868
bikeman868

Reputation: 2637

After several days of banging my head against a documentation brick wall, I finally tracked down the problem. I consider this to be a bug in webpack but others may disagree.

The issue comes about because in 'production' mode webpack performs optimizations that remove unreferenced code. This is a good thing of course, but it does not distinguish between javascript and other types of files, which to me is a bug.

The end result is that because none of my Javascript references anything exported from the css (how could it, css doesn't have ES6 import/export mechanism) the webpack tree shaker discards all of the css from the production build.

The solution is very simple but very hard to find. In your package.json file you need to add:

  "sideEffects": ["*.css", "*.scss", "*.sass"],

Which tells webpack that it cannot prune these modules from the tree, because just loading the module is enough to make the module provide functionality without anything in the module being referenced by the application.

It has taken three days of experimenting and reading docs and stackoverflow articles to track down this simple 1 line change. I hope this answer can save someone else from spending the same time.

Upvotes: 1

Matt Carlotta
Matt Carlotta

Reputation: 19762

Since you're using the same config, you'll need to condtionally add MiniCssExtractPlugin:


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

const { NODE_ENV } = process.env;
const inDevelopment = NODE_ENV === "development";

module.exports = {
  ...options,
  module: {
    rules: [
        ...rules,
        {
          test: /\.(scss|css)/,
          exclude: /node_modules/,
          use: [
                 inDevelopment ? "style-loader" : MiniCssExtractPlugin.loader,
                 'css-loader', 
                 'sass-loader'
               ],
        },
     ],
   },
   plugins: [
     ...plugins,
     !inDevelopment && new MiniCssExtractPlugin({ filename: "bundle.min.css" }),
   ].filter(Boolean)
}

In short, if you're in development it'll use style-loader. When you're in production, you'll use MiniCssExtractPlugin.loader with the MiniCssExtractPlugin plugin. The above assumes you'll want to bundle into a single css file. If you want to code-split it, then you'll need to add optimization.splitChunks and update the plugin for chunking.

Upvotes: 1

Related Questions