TrueCar / gluestick

GlueStick is a command line interface for quickly developing universal web applications using React and Redux.
MIT License
361 stars 43 forks source link

Write plugin for Radium #379

Closed toddw closed 7 years ago

toddw commented 7 years ago

I know it is possible to use Aphrodite with GlueStick but it would be nice if it were easier to set up. Currently you have to override renderMethod in src/config/application.server.js then you have to run through the StyleSheetServer.renderStaticMethod as documented in the aphrodite docs which gives you the html and the css then you expose the html as bodyContent in the output of the render method and you have to set up the stylesheet head tag with the css exported from StyleSheetServer.renderStaticMethod and then you need to add code to src/config/init.browser.js to rehydrate aphrodite before the app is mounted.

It's all possible and we have done it but it would be nice if it were easier.

toddw commented 7 years ago

It would be nice to have a render-style interface and then you would have a Radium style interface an aphrodite style interface or people could write their own.

michalchudziak commented 7 years ago

Do you have any concepts on how should it look like in your mind?

Also, I've seen Radium integration is being removed in this PR https://github.com/TrueCar/gluestick/pull/386 - what's the status here, should we proceed with work here?

toddw commented 7 years ago

386 is a start, you can definitely pick up from where that one leaves off if it helps but it really only does some (possibly all) of the work for removing Radium but none of the work to make Radium or Aphrodite easy to turn on.

The way I think this should work is that a developer can specify a style engine in src/config/application.js

{..., styleEngine: gluestickRadiumEngine, ...}

And things should work with Radium. I don't know if we want to call it an engine or an interface.

What do you think about this approach? Definitely open to suggestions.

michalchudziak commented 7 years ago

Do you think that it is that important to have style engine integrated into GlueStick? I think it's not that bad if we let the user handle it by himself, but that's just an idea. If you really want to have it integrated then I like your proposal, but I'm afraid that Radium, Aphrodite or StyledComponents have a bit different API, and it might be tricky to have it universal.

toddw commented 7 years ago

My goal is to make the simple stuff easy and the complex stuff possible… I do believe there is a way we can pull this off even if it will be tricky.

lukewalczak commented 7 years ago

What do you think about an idea to give user option argument to new command to choose radium or aphrodite?

toddw commented 7 years ago

Allowing the user to specify the style engine in the new command sounds good to me, as long as it doesn't make it difficult for people to write their own style engine.

I'm going to spend some time today planning some possibilities for this functionality today. After thinking about it some more I have an idea of what might make this pretty easy to implement and give us everything we need.

toddw commented 7 years ago

First off I would like to say that I wish we could make this an app level concern but so far both Radium and Aphrodite have proven to need some changes high up in GlueStick in order to work. Because both Radium and Aphrodite require some extra work to setup, it would be nice if they were easily supported in GlueStick apps without too much work. One of our goals it to make building apps simple so if we can eliminate work in setting up the style engine then I think it is worth going after.

When we first started Radium seemed like a great tool (and it is!) but now there are tons of great tools and Aphrodite has proven to be something else we really want to use.

I think it will be pretty easy to support both and all of the other options going forward. I'm not sure if StyledComponents needs any integration at the Gluestick level but I would be happy to add that as an option as well.

Below is the breakdown for both Radium and Aphrodite regarding requirements.

In order to support radium (with server side rendering)

https://github.com/FormidableLabs/radium

If we didn't need the user

In order to support Aphrodite (with server side rendering)

https://github.com/Khan/aphrodite

Style Driver

Both Aphrodite and Radium have different requirements but there is no reason we couldn't just create a plugable system that supports all of their needs.

For Radium the plugin could be as simple as this:

// gluestick-radium-plugin
{
  getRootComponent: (userAgent) => {…},
}

The function getRootComponent will be passed the server request object so that it can get the user agent. https://github.com/TrueCar/gluestick/blob/627e346d3e0ff27f9f4fdd1f35c24c0be25bc2cd/src/lib/server/RequestHandler.js#L139

The function would look like

function getRadiumRootComponent (userAgent) {
  return function RadiumRootComponent({children}) {
    return (
      <StyleRoot radiumConfig={{userAgent}}>
        {children}
      </StyleRoot>
    );
  };
}

For Radium you would want to change: https://github.com/TrueCar/gluestick/blob/8ccd512ad8deb89c8f7fd948013c550db3b7573e/templates/new/src/config/.entry.js#L39

Instead of passing radiumConfig it would just pass userAgent. Now if any other style engine needed a custom root component at the top of the app, it could be done. It would be tempting to pass something other than userAgent but you'll notice that this is built on the server for server side rendering and in the client. So sometimes we get the user agent from req.headers and then in the browser we get it from window.navigator.userAgent. I also can't think of anything else that style engine might need. Radium uses this for browser specific prefixes on styles.

For Aphrodite the plugin

Aphrodite takes a bit more work to make a plugin. All of the functionality to support Aphrodite already exists in a GlueStick app but there are a lot of steps so it be nice to make it easier.

// gluestick-aphrodite-plugin
{
  renderMethod: (render, main) => {…},
  preRenderMethod: () => {…}
}

renderMethod is supported in src/config/application.server.js https://github.com/TrueCar/gluestick/blob/627e346d3e0ff27f9f4fdd1f35c24c0be25bc2cd/src/lib/server/RequestHandler.js#L163-L173

An example Aphrodite render method would look like this:

import React from "react";
import { StyleSheetServer } from "aphrodite";
import { renderToString } from "react-dom/server";

export default function renderWithAphrodite (render, main) {
  const { html: body, css } = StyleSheetServer.renderStatic(() => {
    return renderToString(main);
  });

  const head = [
    <style key="style" data-aphrodite>{css.content}</style>,
    <meta key="meta" name="aphrodite-styles" content={JSON.stringify(css.renderedClassNames)} />
  ];

  return {
    body,
    head
  };
}

An example Aphrodite preRenderMethod will look like this:

import { StyleSheet } from "aphrodite";
const styles = JSON.parse(document.querySelector("meta[name='aphrodite-styles']").getAttribute("content"));
StyleSheet.rehydrate(styles);

The renderMethod function follows the instructions for generating styles with server side rendering as described on Aphrodite. With the slight difference being the head and body tags will be passed back to GlueStick to handle. The head tags will be embeded in the head of the document and body will be the HTML.

With the custom render method, then we can make sure Aphrodite can rehydrate the styles based on the data that was passed to the meta tag during the render method.

Dependencies

In order to use Radium or Aphrodite apps will also need to include the node_module in the package.json. I'm not sure of a great way to solve this yet.

An idea: The interface supports:

{
  // …,
  // …,
  dependencies: {
    "radium": ">=1.0.0"
  }
}

When you start up the app we could modify src/autoUpgrade/updatePackage.js so it looks at the style engine's dependencies and makes sure they are installed when you start the app.

Conclusion

Hopefully the specific details of how to support Radium or more likely how to support Aphrodite didn't make this seem to indimidating. I'm proposing a GlueStyle style engine interface looks like the following:

{
  getRootComponent: (userAgent) => {…}, // optional (needed for Radium engine)
  renderMethod: (render, main) => {…}, // optional (needed for Aphrodite engine)
  preRenderMethod: () => {…} // optional (needed for Aphrodite engine),
  dependencies: {…}
}

Defaults

If any of the properites are missing we can have a sensible fallback: getRootComponent can be a simple component that renders <div>{children}</div>

renderMethod we can simply check if it exists on this line: https://github.com/TrueCar/gluestick/blob/8ccd512ad8deb89c8f7fd948013c550db3b7573e/src/lib/server/RequestHandler.js#L163

const renderMethod = config.server && config.server.renderMethod || styleEngine.renderMethod;

preRenderMethod - We can just add a line above this one: https://github.com/TrueCar/gluestick/blob/8ccd512ad8deb89c8f7fd948013c550db3b7573e/templates/new/src/config/.entry.js#L33 That simply says styleEngine.preRenderMethod && styleEngine.preRenderMethod();

In the future

As most style engines for React are created, and when demand for supporting other currently existing engines arises we can extend this interface if we ever need to.

toddw commented 7 years ago

The style engine plugins should probably live on their own outside of the main gluestick repo. Maybe we should create modules for gluestick-style-radium, gluestick-style-aphrodite?

What do you all think?

Also, this leaves open the question: What should be the default? I personally think glustick-style-aphrodite should be the default moving forward.

And yes I agree with @lukewalczak that it would be nice to specify the style plugin when running gluestick new. Maybe it would look like gluestick new --style-engine=gluestick-style-aphrodite ?