Introducing Full-stack Plugins
Tags: javascript, webdev, tooling
Published: 2022-04-21 15:20:01

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](https://styled-components.com/). 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](https://styled-components.com/docs/tooling#babel-plugin) in your bundler _du jour_.
2. [Wire up server-side rendering](https://styled-components.com/docs/advanced#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](https://vitejs.dev), here's what a Full-stack Plugin might look like:

```ts
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;
```

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](https://nostalgie.dev). With the power and speed of [Vite](https://vitejs.dev), the experience might just blow your mind.

_Cover image by Clark Van Der Beken: https://unsplash.com/photos/Tk0B3Dfkf_4_