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

css-loader + tree-shaking + transpiled imports (babel) not working as it should. #769

Closed
magnusriga opened this issue Sep 10, 2018 · 10 comments

Comments

@magnusriga
Copy link

magnusriga commented Sep 10, 2018

Do you want to request a feature or report a bug?
Bug

What is the current behavior?
When compiling a bundle with Webpack and css-loader in development mode, all css files in component-style subfolders are included. When doing a production build however, only the top level CSS is included (CSS directly within src folder), while all other css (in various subfolders of src) are missing from final bundle.

I tried removing all conditional statements based on isProduction from the webpack config. It still did not work. So, I assumed it had to do with tree shaking somehow.

Changing sideEffects in package.json did not work (it already had: ".css", ".scss").

By random chance I tried adding the following into babel-loader:

"plugins": ["@babel/plugin-transform-modules-commonjs"]

Suddenly it worked.

What is going on here?

If the current behavior is a bug, please provide the steps to reproduce.

Webpack config:
const path = require('path');
const webpack = require('webpack'); // for webpack built-in plugins
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
// const WriteFilePlugin = require('write-file-webpack-plugin');
// const ManifestPlugin = require('webpack-manifest-plugin');
// const InlineManifestWebpackPlugin = require('inline-manifest-webpack-plugin');

// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

const PATHS = {
  // when using __dirname, resolve and join gives same result,
  // because __dirname is absolute path to directory of this file.
  // OK to use no slashes,
  // both resolve and join adds platform-specific separators by default
  src: path.resolve(__dirname, 'src'),
  dist: path.resolve(__dirname, 'dist'),
  build: path.resolve(__dirname, 'build'),
  test: path.resolve(__dirname, 'test')
};

const NAMES = {
  // JS FILES
  main: 'main',
  print: 'print',
  // Chrome Extension Development
  popup: 'popup',
  options: 'options',
  background: 'background',
  contentScript: 'contentScript',

  // FOLDERS
  assets: 'assets',
  utilities: 'utilities',
  images: 'images',
  fonts: 'fonts',
  include: 'include'
};

const FILE_PATHS = {
  // JS
  mainJs: `${path.join(PATHS.src, NAMES.main)}.js`,
  printJs: `${path.join(PATHS.src, NAMES.print)}.js`,
  // Chrome Extension Development
  popupJs: `${path.join(PATHS.src, NAMES.popup)}.js`,
  optionsJs: `${path.join(PATHS.src, NAMES.options)}.js`,
  backgroundJs: `${path.join(PATHS.src, NAMES.background)}.js`,
  contentScriptJs: `${path.join(
    PATHS.src,
    NAMES.include,
    NAMES.contentScript
  )}.js`,

  // HTML
  mainHtml: `${path.join(PATHS.src, 'index')}.html`,
  printHtml: `${path.join(PATHS.src, NAMES.print)}.html`,
  // Chrome Extension Development
  popupHtml: `${path.join(PATHS.src, NAMES.popup)}.html`,
  optionsHtml: `${path.join(PATHS.src, NAMES.options)}.html`,
  backgroundHtml: `${path.join(PATHS.src, NAMES.background)}.html`
};

// Third-party (vendor) libraries to include
// const VENDORS = ['react', 'bootstrap', 'lodash', 'jQuery']; // Relative paths to node_modules

// Note: These are relative
const ASSETS = {
  images: path.join(NAMES.assets, NAMES.images),
  fonts: path.join(NAMES.assets, NAMES.fonts)
};

// CleanWebpackPlugin config
const pathsToClean = [PATHS.dist, PATHS.build];
const cleanOptions = {
  root: __dirname,
  exclude: ['shared.js'],
  verbose: true,
  dry: false
};

// CopyWebpackPlugin config
const copyPattern = [];
const copyOptions = {
  // ignore: ['*.js'],
  context: PATHS.src
};

module.exports = (env = {}) => {
  // webpack injects env variable, into webpack config.
  // perfect to check for production.
  // remember to specify --env.production in command
  // (if in production mode).
  const isProduction = env.production === true;

  return {
    entry: {
      main: FILE_PATHS.mainJs
      // vendor: VENDORS
    },
    mode: isProduction ? 'production' : 'development',
    devtool: isProduction ? 'source-map' : 'inline-source-map',
    optimization: {
      splitChunks: {
        chunks: 'all'
      }
    },
    output: {
      filename: isProduction ? '[name].[chunkhash:8].js' : '[name].js',
      // chunkFilename determine name of non-entry chunk files,
      // for example dynamic imports in the app
      chunkFilename: isProduction ? '[name].[chunkhash:8].js' : '[name].js',
      path: PATHS.dist
    },
    plugins: [
      // new webpack.SourceMapDevToolPlugin({
      // filename: '[file].map',
      // exclude: ['vendor', 'runtime']
      // }),
      new webpack.DefinePlugin({
        // specifies environment variable for dependencies.
        // does not apply to browser runtime environment
        // (process.env is provisioned by Node)
        'process.env.NODE_ENV': isProduction
          ? JSON.stringify('production')
          : JSON.stringify('development')
      }),
      // new BundleAnalyzerPlugin(),
      new CleanWebpackPlugin(pathsToClean, cleanOptions),
      new MiniCssExtractPlugin({
        // Options similar to the same options in webpackOptions.output
        // both options are optional
        // does not work with Hot Module Replacement (HMR)
        // allows HMR in development (will only use this plugin in production)
        filename: isProduction ? '[name].[contenthash].css' : '[name].css',
        chunkFilename: isProduction ? '[id].[contenthash].css' : '[id].css'
      }),
      new webpack.HashedModuleIdsPlugin(),
      isProduction
        ? new UglifyJSPlugin({
            cache: true,
            parallel: true,
            sourceMap: true // set to true if you want JS source maps
          })
        : () => {},
      new CopyWebpackPlugin(copyPattern, copyOptions),
      // new WriteFilePlugin(),
      new HtmlWebpackPlugin({
        template: FILE_PATHS.mainHtml,
        filename: `index.html`
      })
    ],
    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /(node_modules|bower_components)/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env'],
              "plugins": ["@babel/plugin-transform-modules-commonjs"] // THIS MADE IT WORK
            }
          }
        },
        {
          test: /\.s?[ac]ss$/,
          exclude: /node_modules/,
          use: [
            isProduction
              ? MiniCssExtractPlugin.loader
              : {
                  // creates style nodes from JS strings
                  loader: 'style-loader',
                  options: {
                    sourceMap: true,
                    convertToAbsoluteUrls: true
                  }
                },
            {
              // CSS to CommonJS (resolves CSS imports into exported CSS string in JS bundle)
              loader: 'css-loader',
              options: {
                sourceMap: true,
                importLoaders: 3
                // url: false,
                // import: false
              }
            },
            {
              loader: 'postcss-loader',
              options: {
                config: {
                  ctx: {
                    cssnext: {},
                    cssnano: {},
                    autoprefixer: {}
                  }
                },
                sourceMap: true
              }
            },
            {
              loader: 'resolve-url-loader',
              options: {
                attempts: 1,
                sourceMap: true
              }
            },
            {
              // compiles Sass to CSS
              loader: 'sass-loader',
              options: { sourceMap: true }
            }
          ]
        },
        {
          test: /\.(png|svg|jpg|gif)$/,
          use: [
            {
              loader: 'file-loader',
              options: {
                name: '[name].[hash:4].[ext]',
                outputPath: ASSETS.images
              }
            }
          ]
        },
        {
          test: /\.(woff|woff2|eot|ttf|otf)$/,
          use: [
            {
              loader: 'file-loader',
              options: {
                name: '[name].[hash:4].[ext]',
                outputPath: ASSETS.fonts
              }
            }
          ]
        },
        {
          test: /\.(csv|tsv)$/,
          use: ['csv-loader']
        },
        {
          test: /\.xml$/,
          use: ['xml-loader']
        },
        {
          test: /\.(html)$/,
          use: {
            loader: 'html-loader',
            options: {
              interpolate: 'require',
              minimize: true
            }
          }
        }
        // {
        // test: /\.tsx?$/,
        // exclude: /(node_modules|bower_components)/,
        // use: 'ts-loader'
        // }
      ]
    },
    devServer: {
      // contentBase: path.join(__dirname, 'dist'),
      contentBase: PATHS.dist,
      compress: false,
      port: 8080,
      open: false
    }
  };
};

What is the expected behavior?
See above.

@magnusriga magnusriga changed the title css-loader seems to be missing css files in sub-folders, in production mode (tree shaking?) css-loader + tree-shaking + transpiled imports (babel) not working as it should. Sep 10, 2018
@alexander-akait
Copy link
Member

@magnusriga please create minimum reproducible test repo with minimum webpack config

@magnusriga
Copy link
Author

@evilebottnawi I have now created a small test project. In the process, I discovered that the problem occurs when sideEffects: ["*.css"] is set in package.json. Any idea why? If I remove that part, all CSS gets included. This only applies to folders that are one level down from src (src css always gets included).

Repo: https://github.com/magnusriga/css-loader

@alexander-akait
Copy link
Member

@magnusriga all css have sideEffects. What your expected (can you add minimum readme and provide what you have and what you expected)?

@magnusriga
Copy link
Author

magnusriga commented Sep 10, 2018

@evilebottnawi

What I expected: When adding sideEffects: ["*.css"] to package.json, local css files should be protected against tree shaking (i.e. not be lost). That expected behavior is explained for instance here and here.

What happened instead: When adding sideEffects: ["*.css"] to package.json, local css files were removed by tree shaking, instead of being protected from it.

I have included a minimal working example above, to demonstrate the effect (https://github.com/magnusriga/css-loader). Just build once with sideEffects: ["*.css"] included in package.json and once with sideEffects: ["*.css"] not included, to see that all styles are only bundled in the latter build.

@adamburgess
Copy link

adamburgess commented Sep 11, 2018

I don't think this is a problem with babel, but some difference between import and require. I've recently ran into this problem myself - not sure if its exactly the same as OP, but very similar.
Here's a small repo: https://github.com/adamburgess/css-side-effects the meat is in test.js
I wrote a test case on a matrix of different mode, sideEffects, import/require, and babel: https://travis-ci.org/adamburgess/css-side-effects/jobs/427420870#L450

babel has no effect.
when side effects is false and you use require, the css is not removed from the bundle (I think it should be)
when side effects is ["*.css"] and you use import, css directly imported from the entry is in the bundle, but css imported from submodules is removed (this issue)

@joselcc
Copy link

joselcc commented Sep 27, 2018

@magnusriga webpack treeshaking only works with ES6 module syntax, have you tried disabling the module transformation?

Example:

presets: ["@babel/preset-env", { "modules": false }]

@magnusriga
Copy link
Author

@joselcc I have not tried that yet. I basically removed sideEffects in package.json and instead just imported all related css in each js component.

@joselcc
Copy link

joselcc commented Oct 7, 2018

here is an example of Webpack tree shaking working with babel and css loader
https://github.com/pp3nd3x/webpack-treeshaking-css-modules

@alexander-akait
Copy link
Member

Answer above with example, feel free to feedback

@mbonaci
Copy link

mbonaci commented Jul 22, 2021

@joselcc I've just lost a day on this issue (Webpack 4), so here's what worked for me, in case someone else stumbles on this (and his Webpack contains the ability to specify a glob in sideEffects):

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

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

5 participants