birdofpreyru / babel-plugin-react-css-modules

Transforms styleName to className using compile time CSS module resolution.
https://dr.pogodin.studio/docs/babel-plugin-react-css-modules
Other
30 stars 12 forks source link
babel css javascript modules plugin react web

Babel Plugin: React CSS Modules

Latest NPM Release NPM monthly downloads CircleCI GitHub Repo stars Dr. Pogodin Studio

Babel plugin for advanced CSS modules support in React:

Sponsor

Content

Usage Examples

Assuming style.css in the following examples is compiled as CSS Module.

Without this plugin

import S from './styles.css';

export default function Component() {
  return (
    <div className={S.container}>
      <h1 className={S.title}>Example</div>
      <p styleName={S.text}>Sample text paragraph.</p>
      <p className={`${S.text} ${S.special}`}>
        Sample text paragraph with special style.
      </p>
    </div>
  );
}

With this plugin

import './styles.css';

export default function Component() {
  return (
    <div styleName="container">
      <h1 styleName="title">Example</div>
      <p styleName="text">Sample text paragraph.</p>
      <p styleName="text special">
        Sample text paragraph with special style.
      </p>
    </div>
  );
}

With this plugin and multiple stylesheets

Assuming:

import './styles-01.css';
import './styles-02.css';

export default function Component() {
  return (
    <div styleName="container">
      <h1 styleName="title">Example</div>
      <p styleName="text">Sample text paragraph.</p>
      <p styleName="text special">
        Sample text paragraph with special style.
      </p>
    </div>
  );
}

If both files, styles-01.css and styles-02.css contain styles with the same names, thus making auto resolution impossible, this plugin allows explicit stylesheet prefixes:

import S1 from './styles-01.css';
import S2 from './styles-02.css';

export default function Component() {
  return (
    <div styleName="S1.container">
      <h1 styleName="S1.title">Example</div>
      <p styleName="S1.text">Sample text paragraph.</p>
      <p styleName="S1.text S2.special">
        Sample text paragraph with special style.
      </p>
    </div>
  );
}

With this plugin and runtime resolution

import './styles-01.css';
import './styles-02.css';

export default function Component({ special }) {
  let textStyle = 'text';
  if (special) textStyle += ' special';

  return (
    <div styleName="container">
      <h1 styleName="title">Example</div>
      <p styleName={textStyle}>Sample text paragraph.</p>
      <p styleName={textStyle}>
        Sample text paragraph with special style.
      </p>
    </div>
  );
}

In the case when the exact style value is not known at the compile time, like in this example, the plugin will inject necessary code to correctly resolve the styleName at runtime (which is somewhat less performant, but otherwise works fine).

SSR scenario

Consider such component, which uses a named stylesheet import in order to use it in some other ways, beside simple styling, e.g. to also display the classname mapping:

import S from './style.css';

export default function Component() {
  return (
    <div styleName="container">
      {JSON.stringify(S)}
    </div>
  )
}

While by default this plugin transforms it into (leaving it to the Webpack's css-loader to handle ./style.scss import for the actual CSS bundling, and leaving a correct JS in place of it):

import S from './style.css';

export default function Component() {
  return (
    <div className="12345">
      {JSON.stringify(S)}
    </div>
  )
}

For server-side environment, if you don't compile server-side code with Webpack, you'll need to replace ./style.css with valid JS code. That is exactly what this plugin does with replaceImport option enabled, it outputs:

const S = {
  container: '12345',
  // Other stylesheet keys, if any.
};

export default function Component() {
  return (
    <div className="12345">
      {JSON.stringify(S)}
    </div>
  )
}

CommonJS require() support

The plugin works the same with require('./style.css') CSS imports.

Installation

React Native

If you'd like to get this working in React Native, you're going to have to allow custom import extensions, via a rn-cli.config.js file:

module.exports = {
  getAssetExts() {
    return ["scss"];
  }
}

Remember, also, that the bundler caches things like plugins and presets. If you want to change your .babelrc (to add this plugin) then you'll want to add the --reset-cache flag to the end of the package command.

Configuration

Plugin Options

These are valid plugin options. All are optional, but the overall configuration should be compatible with that of css-loader, thus defaults may not work for you.

Deprecated Plugin Options

Configurate syntax loaders

To add support for different CSS syntaxes (e.g. SCSS), perform the following two steps:

  1. Add the postcss syntax loader as a development dependency:

    npm install postcss-scss --save-dev
  2. Add a filetypes syntax mapping to the Babel plugin configuration. For example for SCSS:

    "filetypes": {
      ".scss": {
        "syntax": "postcss-scss"
      }
    }

    And optionally specify extra plugins:

    "filetypes": {
      ".scss": {
        "syntax": "postcss-scss",
        "plugins": [
          "postcss-nested"
        ]
      }
    }

    NOTE: postcss-nested is added as an extra plugin for demonstration purposes only. It's not needed with postcss-scss because SCSS already supports nesting.

    Postcss plugins can have options specified by wrapping the name and an options object in an array inside your config:

      "plugins": [
        ["postcss-import-sync2", {
          "path": ["src/styles", "shared/styles"]
        }],
        "postcss-nested"
      ]

Hot Module Reloading

If you don't know what is Hot Module Reloading (HMR), refer to the Webpack documentation.

If you use HMR in your development setup (you probably should), depending on your particular configuration you might need to enable webpackHotModuleReloading option of this plugin, or you may need to leave it disabled (default), as other loaders / plugins in your Webpack pipeline for CSS may already inject required HMR code.

In case you decide to enable it in this plugin, webpackHotModuleReloading option may be set equal:

The default value is false - this plugin does not inject HMR accept code.

transform

function transform(cssSource, cssSourceFilePath, pluginOptions): string

The transform function, if provided as the transform option of this plugin, will be called for each loaded CSS source with three arguments:

It should return a string, the actual CSS code to use.

Custom Attribute

You can set your own attribute mapping rules using the attributeNames option.

It's an object, where keys are source attribute names and values are destination attribute names.

For example, the <NavLink> component from React Router has an activeClassName attribute to accept an additional class name. You can set "attributeNames": { "activeStyleName": "activeClassName" } to transform it.

The default styleName -> className transformation will not be affected by an attributeNames value without a styleName key. Of course you can use { "styleName": "somethingOther" } to change it, or use { "styleName": null } to disable it.

Server-Side Rendering

If replaceImport flag is set, this plugin will remove or replace original stylesheet imports, which is needed for server-side rendering:

// Anonymous imports are removed from the code:
import 'path/to/style.css';

// Default and named imports are replaced in the following manner:

// Before:
import styles, {
  className,
  otherClassName as alias,
} from 'path/to/style.css';

// After:
const styles = {
  className: 'generatedClassName',
  otherClassName: 'otherGeneratedClassName',
},
className = 'generatedClassName',
alias = 'otherGeneratedClassName';

// Also this kind of import:
import * as style from 'path/to/style.css';

// is replaced by:
const style = {
  className: 'generatedClassName',
  otherClassName: 'otherGeneratedClassName',
};

Under the hood

How does it work?

This plugin does the following:

  1. Builds index of all stylesheet imports per file (imports of files with .css or .scss extension).
  2. Uses postcss to parse the matching CSS files into a lookup of CSS module references.
  3. Iterates through all JSX element declarations.
  4. Parses the styleName attribute value into anonymous and named CSS module references.
  5. Finds the CSS class name matching the CSS module reference:
    • If styleName value is a string literal, generates a string literal value.
    • If styleName value is a jSXExpressionContainer, uses a helper function (getClassName) to construct the className value at the runtime.
  6. Removes the styleName attribute from the element.
  7. Appends the resulting className to the existing className value (creates className attribute if one does not exist).

Project history

This plugin is an up-to-date, well-maintained fork of the original babel-plugin-react-css-modules:

The original babel-plugin-react-css-modules plugin is largely abandoned by its author since March 2019. When an year later updates of css-loader and Webpack broke dependant projects, with no reaction from babel-plugin-react-css-modules author on emerging issue reports in GitHub, I (birdofpreyru) created this fork to ensure stability of my own projects relying on it.

I am banned from commenting in the original project repo since I tried a little self-promo, trying to encourage people to switch over to my fork. If you read this, consider to spread the word to encourage more users to move to this fork.

Migration from babel-plugin-react-css-modules

css-loader compatibility

css-loader versions this plugin versions
7.0.07.1.2 (latest) 6.13.06.13.2 (latest)
6.7.16.11.0 6.7.06.12.0
6.5.06.7.0 6.5.16.6.1
6.4.0 6.4.06.4.1
6.0.06.3.0 6.2.16.3.1
5.2.55.2.7 6.1.1
5.2.4 6.1.0
5.1.35.2.3 6.0.11 / 6.1.0(1)
5.0.05.1.2 6.0.76.0.11
4.2.04.3.0 6.0.36.0.6
3.6.0 original plugin

1) There might be some corner-case differences in class name transformation between these versions of css-loader and this plugin, but most probably they won't break compatibility for most users.