rollup-plugin-postcss icon indicating copy to clipboard operation
rollup-plugin-postcss copied to clipboard

Extract "css" variable in JS for custom Server Side Rendering

Open bogas04 opened this issue 6 years ago • 4 comments

Hi,

We use css-modules in a react app. In our webpack based web app, in order to collect styles for the critical components, we basically wrap those components into a HOC. This decorator basically accumulates all the generated CSS and inlines it into the HTML while server side rendering.

Example;

// component
import styles from "./styles.scss";
import { withStyles } from "utils/with-styles";

function Component (props) {
  return <div className={styles.wrapper}>{props.children}</div>;
}

export default withStyles(Component, styles);
// utils/with-styles.js
export function withStyles(Component, styles) {
  const context = useContext(CriticalCSSContext);
  
  if(context && context.addStyles) {
    if (!context.criticalStyles.has(styles._getId) {
      context.addStyles(styles._getId(), styles._getCss());
    }
  }

  return (props) => <Component {...props} />;
}
// index.js
function App() {
  const criticalStyles = useRef({});
  const criticalCss = {
    addStyles: (id, css) => {
      criticalStyles[id].current = css
    },
    criticalStyles: {
      has: (id) => criticalStyles.current[id],
    },
  };
  return (
    <CriticalCSSContext.Provider value={criticalCss}>
      <Root />
    </CriticalCSSContext.Provider>
  );
}

While we can access the className-generatedClassName map, we can't quite get the actual "css" content. To access the generated CSS, we wrote a custom style loader that exports the CSS content.

I was wondering if the same could be achieved by getting a onExport hook/plugin/loader where we can essentially add module.exports.css = css; line in the final css module file.

This is the style loader we wrote for webpack;

// custom_webpack_style_loader.js
var stringifyRequest = require("loader-utils").stringifyRequest;

module.exports = function loader() {};

module.exports.pitch = function pitch(remainingRequest) {
    if (this.cacheable) {
        this.cacheable();
    }

    return `
      var content = require(${stringifyRequest(this, `!!${remainingRequest}`)});
      if (typeof content === 'string') {
        content = [[module.id, content, '']];
      }
      module.exports = content.locals || {};
      module.exports._getContent = () => content;
      module.exports._getCss = () => content.toString();
      module.exports._getId = () => module.id;
  `;
};

I'm sorry if this is beyond the scope of this plugin, but would love some pointers to support this. Thank you so much for this wonderful plugin!

bogas04 avatar Jun 21 '19 12:06 bogas04

@bogas04 Did you end up getting this to work? Or did you go a different direction on achieving SSR?

I'm working on a project that uses SCSS and it would be a big undertaking to switch to something that more easily supports SSR (Emotion). The injection behavior is leading to Flashes of Unstyled Content, and I was hoping to find another path. The extract option only gives one big bundle, which isn't helpful in sending critical css on the first request.

Exposing the actual css content sounds like a good plan, because then our web app will be able to pick up the styles from our components module that we are building with Rollupjs.

IchabodDee avatar Jul 12 '19 13:07 IchabodDee

@IchabodDee Nah couldn't get much time to go through plugin system of rollup/postcss. This is a great approach for css-modules, and the above solution would work really fine for webpack (swiggy.com uses it), just need to port it to rollup.

bogas04 avatar Jul 15 '19 04:07 bogas04

@IchabodDee I ended up making a plugin for this. Man rollup is so simple.

This should work for above code snippet.

/**
 * Simple rollup plugin to inject `_getCss` and `_getId` functions to default export of scss files.
 */
function injectStyleFunctions() {
    const injectFunctions = code => code.replace(
        "export default {",
        "export default {".concat([
            "_getCss: function() { return css; },",
            `_getId: function() { return "${id}"; },`,
         ].join("")
   ));

    return {
        name: "injectStyleFunctions",
        async transform(code, id) {
            if (id.includes(".scss")) {
                return {
                    id,
                    code: injectFunctions(code)
                };
            }
            return null;
        },
    };
}

bogas04 avatar Aug 14 '19 15:08 bogas04

@bogas04 @IchabodDee Hey folks, we also had a similar use case, where we want to get hold of the CSS to implement SSR. We found that we can get hold of the CSS as the named export. So in the above example, you can try:

- import styles from "./styles.css"
+ import styles, { stylesheet } from "./styles.css"

The stylesheet variable holds the stringified CSS.

abhinavpreetu avatar May 25 '21 14:05 abhinavpreetu