Skip to content
This repository has been archived by the owner on Jan 6, 2024. It is now read-only.

k-g-a/postcss-nested-once

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PostCSS Nested for rollup-plugin-styles

Summary

This plugin allows using Sass-like nested rules in combination with css-modules by rollup-plugin-styles.

Specifically, it solves the problem of the ampersand-combined selectors, i.e.:

// styles.css
.list {
  color: red;

  &_item {
    color: green;
  }
}

Results in:

// some-module.js
import styles from "./styles.css";

// with any setup:
console.log(styles.list); // => "styles_list__HASH"

// with postcss-nested plugin:
console.log(styles.list_item); // => undefined ,

// with postcss-nested-once plugin:
console.log(styles.list_item); // => "styles_list_item__HASH"

Usage

Install:

yarn add postcss-nested-once -D

It's intended to replace postcss-nested for the following rollup configuration:

// rollup.config.js

// ...
const stylesRollupPlugin = require("rollup-plugin-styles");
const postcssNestedOncePlugin = require("postcss-nested-once");

module.exports = {
  // ...
  plugins: [
    // ...
    stylesRollupPlugin({
      // ...
      mode: "inject",
      modules: true,
      plugins: [
        // ...
        postcssNestedOnce(),
      ],
    }),
  ],
};

Assuming the following source:

// styles.css
.parent {
  color: red;

  & .child {
    color: green;
  }
}

.list {
  color: red;

  &_item {
    color: green;
  }
}

This will produce:

// styles.js
// ...
var css =
  ".styles_parent__HASH {" +
  "  color: red" +
  "}" +
  "" +
  "  .styles_parent__HASH .styles_child__HASH {" +
  "    color: green;" +
  "  }" +
  "" +
  ".styles_list__HASH {" +
  "  color: red" +
  "}" +
  "" +
  ".styles_list_item__HASH {" +
  "    color: green;" +
  "  }" +
  "";
var modules = {
  parent: "styles_parent__HASH",
  child: "styles_child__HASH",
  list: "styles_list__HASH",
  list_item: "styles_list_item__HASH",
};
injectCss["default"](css, {});

exports.css = css;
exports.default = modules;

Which in turn allows to use all the four classes in js:

// some-module.js
import styles from "./styles.css";

console.log(styles.parent); // => "styles_parent__HASH"
console.log(styles.child); // => "styles_child__HASH"
console.log(styles.list); // => "styles_list__HASH"
console.log(styles.list_item); // => "styles_list_item__HASH"

Problem Details

The rollup-plugin-styles provides an ability to use css modules by simply specifying modules: true | ModulesOptions during configuration.

Under the hood it does not rely on the postcss-modules package directly, but introduces its own plugins pipeline instead:

// built-in plugins
styles-import - internal plugin, uses 'Once' hook, used only if the 'import' option is enabled;
styles-url - internal plugin, uses 'Once' hook, used only if the 'url' option is enabled;

// bunch of plugins from options.plugins
postcss-nested - could be listed here, if specified
plugin-from-options #1
plugin-from-options #2
...

// bunch of plugins from postcss.config.js
postcss-nested - or here, if specified
plugin-from-postcss-config #1
plugin-from-postcss-config #2
...

// css-modules-related plugins
postcss-modules-values - dependency plugin, uses 'Once' hook
postcss-modules-local-by-default - dependency plugin, uses 'Once' hook
postcss-modules-extract-imports - dependency plugin, uses 'Once' hook
postcss-modules-scope - dependency plugin, uses 'Once' hook
styles-icss - internal plugin involved in resulting exports generation, uses 'OnceExit' hook

By that far it seems like everything should work as expected due to proper plugin's order.

So to make the next guess it's good to know the responsibility of every plugin. To cut the long story short:

  • postcss-modules-values extracts @value XX and @value YY from into corresponding internal :import {} / :export {} selectors and gives local names;
  • postcss-modules-local-by-default wraps every suitable css selector in internal :local directive;
  • postcss-modules-extract-imports is responsible for the compose feature;
  • postcss-modules-scope among other actions generates :export {} directives for every :local selector;
  • styles-icss fills special object from the contents of every :export {} directive.

The object formed by styles-icss is used further down the pipeline to write exports from the generated styles.js file (which are consumed by import styles from './styles.css'').

As a result, for the above input we'll get the following output:

// styles.js (generated)
var css =
  ".styles_parent__HASH {" +
  "  color: red" +
  "}" +
  "" +
  "  .styles_parent__HASH .styles_child__HASH {" +
  "    color: green;" +
  "  }" +
  "" +
  ".styles_list__HASH {" +
  "  color: red" +
  "}" +
  "" +
  ".styles_list__HASH_item {" +
  "    color: green;" +
  "  }" +
  "";
var modules = {
  parent: "styles_parent__HASH",
  child: "styles_child__HASH",
  list: "styles_list__HASH",
};
injectCss["default"](css, {});

exports.css = css;
exports.default = modules;

So we have an actual rule .styles_list__HASH_item (which will be injected during the import), but do not have the corresponding export (making styles.list_item === undefined at runtime).

The key hint is that _item suffix is added after the __HASH part, which means that postcss-nested transformation runs after the postcss-modules-scope transformation. This happens because postcss-nested plugin uses Rule hook while other ones (mostly) use Once + walk() combination which comes first.

So the most simple solution is to move postcss-nested's logic to the same Once hook, which resulted in postcss-nested-once plugin.

Implementation

For the sake of simple maintenance this plugin lists postcss-nested as dependency and reuses it by calling root.walkRules((rule) => { postcssNestedInstance.Rule(rule, postcssAPI); }); in Once hook.

It accepts (and passes down) the same options as postcss-nested.

Type definitions are copy-pasted from the original plugin.