Skip to content

Allow customization of prerender DOM #253

@yepitschunked

Description

@yepitschunked

Happo has a stylesheets config option to ensure styles are applied at render time. We'd like these styles to also be injected at prerender time. This allows components such as react-autosize-textarea, which call window.getComputedStyle while rendering, to behave more realistically.

Furthermore, we've run into some execution ordering bugs in JSDOM which result in the <script> tags after a stylesheet <link> tag executing before the stylesheet is loaded. This should not happen in a real browser, since styles ought to be parser-blocking. Our solution was to inline the stylesheet contents in a <style> tag.

We were able to work around all of these problems by using the plugins API, which allows providing a custom DomProvider. We extended the JSDOMDomProvider and modified this.dom.

Implementation ideas

Prerender currently operates on a static JSDOM template:

this.dom = new JSDOM(
`
<!DOCTYPE html>
<html>
<head>
<script src='file://${webpackBundle}'></script>
</head>
<body>
</body>
</html>
`.trim(),

  • A very basic solution would be to just inject the stylesheets param in here as a tag, the same way that the render environment works. However, this won't support the JSDOM workaround.
  • Another option would be to automatically inline the styles. However, users might want to link to an external stylesheet that isn't on the filesystem, which would require some work to fetch the stylesheet.
  • A third option would be to allow customizing the JSDOM template in addition to the existing jsdomOptions.
  • Lastly, we could just document the fact that DomProviders exist and can be overridden by a plugin. We could use our custom DomProvider as an example:
module.exports = function happoPrerenderWithStylesheetsPlugin(
  stylesheetAbsolutePaths,
  // the regular jsdomOptions from the config aren't passed to a plugin DOMProvider, maybe we can fix this
  jsdomOptions,
) {
  class JSDOMProviderWithStylesheets extends HappoJSDOMProvider {
    constructor(jsdomOpts, providerOptions) {
      super(jsdomOpts, providerOptions);
      // this.dom was created in the superclass
      const {
        window: { document },
      } = this.dom;

      stylesheetAbsolutePaths.forEach((href) => {
        const cssSource = fs.readFileSync(href, 'utf-8');
        const style = document.createElement('style');
        style.textContent = cssSource;

        document.head.prepend(style);
      });
    }
  }

  return {
    DomProvider: JSDOMProviderWithStylesheets.bind(JSDOMProviderWithStylesheets, jsdomOptions),
  };
};

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions