Introducing Full-stack Plugins

ggoodman

Geoff Goodman

Posted on April 21, 2022

Introducing Full-stack Plugins

A full-stack plugin is a single integration point to hook into the build pipeline, server-side rendering (SSR) of requests, server-to-client handoff of data and client-side rendering (CSR). This kind of plugin offers a drop-in solution for integrating cross-stack tooling that would otherwise be terrifying for users.

Background

If you're a participant in modern web development -- willing or not -- you're undoubtedly familiar with build plugins. You might use them to convert your TypeScript to JavaScript or to allow you to seamlessly import CSS files. These are little wonders of modern developer experience (DX) tooling that let you move faster than the language or browser standards would otherwise allow.

Sometimes the ambition of these plugins exceeds the capabilities of the tooling.

But sometimes the DX is ... let's say, 'lacking'. Sometimes the ambition of these plugins exceeds the capabilities of the tooling. Sometimes integration means successfully navigating and understanding a maze of instructions in the "Advanced" section of docs.

This is where things fall apart

Let's say that you are a happy user of styled-components. Everything is great until you decide to adopt integrate server-side rendering (SSR). Getting something like this set up has huge benefits:

  • A powerful and and expressive DX to author components colocated with their styles
  • Critical CSS extraction during SSR
  • CSS pre-processing using all your favourite tools
  • Better search engine ranking and shorter first contentful paint timing

This is a very compelling set of benefits but the up-front setup is not for the faint of heart. Setting this up will require you to tweak a bunch of things.

  1. Configure the babel plugin in your bundler du jour.
  2. Wire up server-side rendering
    1. Construct a per-request ServerStyleSheet instance.
    2. Wrap your app in a <StyleSheetManager /> component and pass in the ServerStyleSheet instance.
    3. Modify the HTML served to include style tags produced by the ServerStyleSheet instance.

That's quite a lot.

But that's not styled-components' fault. That's just how our tooling works right now. You have build tooling and you have runtime tooling and never the twain shall meet.

Bridging East and West

So how do we bridge this divide? We need to rethink the relationship between our bundler and the runtime and get the two talking to each other.

Let's say you're using vite, here's what a Full-stack Plugin might look like:

interface FullStackPlugin extends VitePlugin {
  /**
   * Provide a set of references to server-side plugins.
   * 
   * A server-side plugin can participate in the server-side rendering
   * pipeline and do things like:
   * 
   * - Set up any necessary per-request context (instances).
   * - Wrap the main component in Providers wired up to the context.
   * - Modify the HTML markup for the response.
   * - Modify the HTTP headers for the response
   * - Contribute to the server-to-client hand-off of data.
   */
  getServerRendererPlugins?: () =>
    | MaybeArray<RendererPluginReference>
    | undefined;

    /**
     * Provide a set of references to client-side plugins.
     * 
     * A client-side plugin can participate in the client-side rendering
     * pipeline and do things like:
     * 
     * - Set up any necessary per-request context, having access to that
     *   request's server-to-client handoff data.
     * - Wrap the main component in Providers wired up to the context.
     * - Perform any blocking, asynchronous operations before actually
     *   doing the initial hydration.
     */
  getClientRendererPlugins?: () =>
    | MaybeArray<RendererPluginReference>
    | undefined;
}

/**
 * Describe where to find the code for the runtime plugin, including
 * which export to use (defaults to the default export) and any
 * configuration that needs to be serialized into that plugin at runtime.
 */
interface RendererPluginReference {
  readonly spec: string;
  readonly exportName?: string;
  readonly config: unknown;
}

type MaybeArray<T> = Array<T> | T;
Enter fullscreen mode Exit fullscreen mode

Imagine the power this gives you as a library author.

Here are some use-cases that are cleanly solved with Full-stack Plugins:

  1. SSR extraction of critical CSS for tools like styled-components, linaria, twind, etc...
  2. Server-side data loading with react-query and automatically setting up server-to-client hand-off of that data.
  3. Zero-cost code-splitting and lazy-loading of components, pages and data.
  4. Deep integration of react-router for simple SSR / CSR integration or even automatic filesystem-based routing like remix.

And that is just the tip of the iceberg. What else can you imagine?

Getting involved

If you find this idea compelling, come and chat on Discord: https://discord.gg/dSHCnQxbba. This is not just a pipe dream. There is working code that fulfills this promise that's making its way into the next version of Nostalgie. With the power and speed of Vite, the experience might just blow your mind.

Cover image by Clark Van Der Beken: https://unsplash.com/photos/Tk0B3Dfkf_4

馃挅 馃挭 馃檯 馃毄
ggoodman
Geoff Goodman

Posted on April 21, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related