egoist / rollup-plugin-postcss

Seamless integration between Rollup and PostCSS.
MIT License
677 stars 217 forks source link

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

Open bogas04 opened 5 years ago

bogas04 commented 5 years ago

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!

IchabodDee commented 5 years ago

@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.

bogas04 commented 5 years ago

@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 commented 5 years ago

@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;
        },
    };
}
abhinavpreetu commented 3 years ago

@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.