Anidetrix / rollup-plugin-styles

🎨 Universal Rollup plugin for styles: PostCSS, Sass, Less, Stylus and more.
https://anidetrix.github.io/rollup-plugin-styles
MIT License
242 stars 43 forks source link

Add Support for CSS Module Tree Shaking [feature request] #168

Closed danlevy1 closed 3 years ago

danlevy1 commented 3 years ago

Feature Request

While CSS is technically considered a side effect, I would like a way to explicitly state that my CSS Modules are not side effects.

Background

I am building a library that stores a bunch of CSS Module files. The library will be consumed by my component library. The advantage of separating the CSS from the components is that I can write component libraries using multiple frameworks (e.g. React, Angular, and Vue) that all use the same exact styles. Since I am guaranteeing that my styles in any given file are locally scoped to only one component, I can guarantee that my CSS files are not side effects.

Currently, the output from this plugin looks something like the following:

var css$some_hash = ".random_hash{background-color:#000000;font-size:24px;color:#ffffff;cursor:pointer}";
var modules_some_hash = {"button":"some_hash"};
styleInject(css$some_hash, {});

export { modules_some_hash as ButtonStyles }

Now, let's assume that I have 10 CSS files that I want to inject using CSS Modules. I essentially get 10 styleInject function calls and 10 named exports. Once I consume the build file, I'll want to do something like the following in my component file:

import React from "react"; // Assuming React just as an example
import { ButtonStyles } from "./index.es.js"; // "index.es.js" is the build file

const Button = ({ title }) => {
    return <button className={ButtonStyles.button}>{title}</button>
}

This works as expected, meaning that the button styles will be injected into the <head> element via styleInject. The problem is that I have 9 other style files that resulted in 9 other styleInject(..) function calls in the build file (i.e. index.es.js). Because these styleInject calls are not wrapped in a function with a /*#__PURE__*/ annotation, just one import will cause all 10 CSS files to be injected into the <head> element. This is not what I want because the other 9 CSS files (other than the one that I import) are not actually being used. This results in a larger build file for my component library, and thus a larger download for consumers of the component library.

Tree Shaking/Dead Code Elimination

Bundlers like Rollup and Webpack understand the /*#__PURE__*/ annotation preceding a function call. Webpack does a good job explaining this (see https://webpack.js.org/guides/tree-shaking/#mark-a-function-call-as-side-effect-free).

The /*#__PURE__*/ annotation tells the bundler "don't call this function unless it is used somewhere else." In the case of this Rollup plugin, we don't want styleInject(..) to be called unless the styles are actually imported somewhere.

Implementation

For reference, here is the current output:

var css = ".random_hash{background-color:#000000;font-size:24px;color:#ffffff;cursor:pointer}";
var modules = {"button":".random_hash"};
styleInject(css, {});

export { modules as ButtonStyles }

If a user of the plugin states "I guarantee that my CSS has no side effects," we want to transform the above code into the following:

var css = ".random_hash{background-color:#000000;font-size:24px;color:#ffffff;cursor:pointer}";
var styleMapping = {"button":".random_hash"};
var modules = /*#__PURE__*/(function() {
  styleInject(css, {});
  return styleMapping;
})();

export { modules as ButtonStyles }

Here is what I changed in the second code snippet:

  1. Wrap the styleInject(..) function call in an IIFE
  2. Use the /*#__PURE__*/ annotation
  3. Instead of exporting var modules = {"button":".random_hash"};, we need to return mapping from the IIFE. Since we are introducing a third variable to the mix, I set the mapping {"button":".random_hash"}; to a variable called styleMapping instead of modules. Now, I can set the result of the IIFE call to the variable modules. Note that modules is still the variable being exported from the file.

We'll also need an option name to tell this plugin to use the /*#__PURE__*/ annotation and IIFE. Some ideas are pure, noSideEffects, and treeShakable (assuming the value would be a boolean).

danlevy1 commented 3 years ago

Oops, it looks like tree shaking is already available (see https://anidetrix.github.io/rollup-plugin-styles/interfaces/types.injectoptions.html#treeshakeable)

814k31 commented 1 year ago

Updated link for anyone looking for it in the future: https://anidetrix.github.io/rollup-plugin-styles/interfaces/types.InjectOptions.html#treeshakeable