@creditkarma/hapi-galaxy

2.0.0 • Public • Published

Hapi Galaxy

Front-end component rendering plugin for hapi.js.

Build Status

Introduction

Use hapi-galaxy to transform any front-end component into server-rendered output.

Motivations

  1. Provide a bridge between Credit Karma's server side JavaScript rendering conventions and our Web App servers.

Front End code ships on-demand and often, so rendering servers need to be able to execute newly discovered modules safely and consistently.

  1. Isolated, multi-tennant rendering.

We need to render many versions of the same module at the same time, so every front end application gets rendered in a dedicated process using worker-farm.

  1. Programmatic integration with any framework that supports server side rendering.

Next.js, create-react-app, Vue.js, Ember, or even vanilla string templates will work. Not tied to any client side framework.

Assumptions

  • Your front end code lives in some other module
  • You have somehow packaged and exported that module so that is an entry point intended for server-side rendering
    • e.g., a React app compiled with Babel and Webpack, without any references to browser specific globals in the runtime
  • "Bring your own render method"
    • Runtime errors are bad and make universal rendering unpredictable
    • Requiring your entire application's dependency tree is onerous
    • All renders are wrapped in a promise
    • Dependencies for render belong to the front end application
      • We use React today, but we may use something else tomorrow

Usage

Install hapi-galaxy and add it to your Hapi.js project's dependencies

npm install @creditkarma/hapi-galaxy --save

Then, register the plugin with your Hapi.js server:

server.register(require('hapi-galaxy'), pluginErr => {
  if (pluginErr) throw pluginErr

  server.route({
    path: '/',
    method: 'GET',
    handler: {
      galaxy: {
        component: require.resolve('./frontend/component')
      }
    }
  })

  server.start((err) => {
    console.log(`Server started at: ${server.info.uri}`);
  })
})

Where require.resolve('./frontend/component') is the absolute path to any module that uses the client interface, which means any async function will work. This means that the minimum viable "front end component" would look like this:

module.exports = (props, path) => Promise.resolve('<h1>Hello World</h1>')

Options

The galaxy handler object has the following properties:

  • component — a string with the name of the module to use when require'ing your FE code, or function with the signature function(props, location) which returns a promise that resolves with the rendered output of your component.
  • layout – function with the signature function(props, content) which wraps your component output, presumably with in an HTML document with head and body tags
    • If no layout function is provided a generic HTML 5 layout will be used.
    • layout: false will disable the default view.
    • Will be ignored if view option is provided.
  • path — current url being requested. Useful for handling route logic within a bundled component.
  • props — object containing any additional properties to be passed to the view.
  • view – string corresponding to the vision view template to be used. The view template with be the output of the render and props used as content and props, respectively.
  • workerFarm — object containing any configuration options for worker-farm

reply.galaxy(component, options, errorCallback)

  • component — a string or function, following the same restrictions as the component option defined above
  • options — an object including the same keys and restrictions defined by the route galaxy handler options, excluding the component
  • errorCallback — a function with the signature function(err) which should handle the error returned

Client Interface

So, here's the thing:

Client code can throw errors when you try to render it on the server.

Obviously, in a perfect world this doesn't happen very often. But since it can, and since we don't want the output of random client errors manifesting themselves on a production server, your render method needs to be a Promise that resolves with the successful output of your render.

Here's what a simple Client would look like:

In frontend/component.js

'use strict'
import React from 'react'
import { renderToString } from 'react-dom/server'
import MyApp from './app'

export default props =>
  new Promise((resolve, reject) => {
    resolve(renderToString(<MyApp {...props} />))
  })

In server.js

server.route({
  path: '/',
  method: 'GET',
  handler: {
    galaxy: {
      component: require.resolve('./frontend/component')
    }
  }
})

This has several benefits over error-first callbacks for our use-case:

  1. It provides a consistent way of preventing client code from throwing errors in your server process. Consider the following:
  • The server is a secondary runtime for the code being rendered and therefore it would be inappropriate to let the client code throw errors.
  • We're delegating the render method to the client code, error-first callbacks would require a try/catch to do correctly.
  • Error-swallowing becomes a feature as it provides de facto encapsulation.
  1. Promises are a requirement for using fetch and associated libraries).
  2. Async/await will be ready soon without compilation and it's interface is compatible with promises. Next.js uses this pattern to great effect and it contributes to a really nice developer experience.

Package Sidebar

Install

npm i @creditkarma/hapi-galaxy

Weekly Downloads

1

Version

2.0.0

License

ISC

Last publish

Collaborators

  • hightimesteddy
  • ck-npm
  • ouranosskia
  • riley-stroud-ck
  • wookiehangover
  • nnanceck