npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

koa-ssr

v0.3.0

Published

Koa server-side-rendering middleware using JSDOM

Downloads

8

Readme

koa-ssr

Use JSDOM to evaluate (and cache) your client-side app on server before serving response as HTML in Koa.

Perfect for serving static content generated by webpack production build.

Eg. project

Install

npm i koa-ssr

Usage


import Koa from 'koa';
import koaStatic from 'koa-static';
import koaSSR from 'koa-ssr';

const app = new Koa();

const root = __dirname + '/dist'

// serve static content as usual:
app.use(koaStatic(root, {
  // DON'T let index.html be served statically
  index: false  // << important!
}))

app.use(koaSSR(root, {
  // we'll (re-)generate it here
  index: 'index.html'
}))

API

koaMiddleware = koaSSR(root, opts)
  • root root directory
  • opts options:

Options

  • index [str] (default: 'index.html') Main index file

  • html [str] Instead of index.html, provide an html string

  • timeout [num] (default: 5000) After which if JSDOM hasn't finished loading (i.e. window[opts.modulesLoadedEventLabel] hasn't been called (see below)) it throws an error (with { koaSSR: {ctx, window} } property attached).

  • jsdom [obj] Config passed to JSDOM: jsdom.jsdom(opts.html, opts.jsdom).

    Eg. for shimming unimplemented APIs:

      koaSSR(root, {
        jsdom: {
          created: (e, window) => {
            window.localStorage = new MockLocalStorage();
          },
        }
      })
  • console [obj] (default: modified debug (set DEBUG=koa-ssr:jsdom-client)) console object for JSDOM's virtualConsole used as jsdom.createVirtualConsole().sendTo(console)

    Eg.

      koaSSR(root, {
        console: console // native console object
      })

    Note: You can also do this manually in opts.jsdom.virtualConsole, this is just a shorter version. It also tries to infer the type of console (checking for .log/err etc methods) and adds the additional prefixes ('[JSDOM]') to messages.

  • resourceLoader [func] (default: (res, cb, def) => def(res, cb)) Wrapper around JSDOM's resourceLoader with an extra argument def to load resources automatically from root.

    Eg.

      koaSSR(root, {
        resourceLoader: (res, cb, def) => {
          // either load the resource manually
          fs.readFile(res.url.pathname, 'utf8', cb)
    
          // or let koaSSR handle it
          def(res, cb);
    
          // or intercept
          def(res, (err, body) => {
            cb(null, body || 'something else')
          })
        }
      })

    Note: You can also provide this option as opts.jsdom.resourceLoader but it won't have the additional third argument def.

  • modulesLoadedEventLabel [str] (default: 'onModulesLoaded') A special function is attached to window[modulesLoadedEventLabel] which **must be called** to indicate that your app has finished rendering. Failure would result in a timeout and an error thrown (with { koaSSR: {ctx, window} } property attached). See JSDOM: Dealing with asynchronous script loading as to why it needs you to do this instead of relying on default onload or other such events. This can also be used as an indicator that your app is being rendered server-side so you may choose to deal with that aspect in your app as well.

    Eg.

    client-app.js

      import {h, render} from 'preact'
    
      if (window.onModulesLoaded) {
        // rendered on server
        const userData = window.userData // as attached in render function below
      } else {
        // not rendered on server
        const userData = localStorage.get('userdata') || await fetch('/api/user...')
      }
    
      render(h('div', {...data}, ['Hello world!']), document.body)
    
      if (window.onModulesLoaded) {
          window.onModulesLoaded();
      }
  • cache [bool|obj|function] (default: true) Whether (and where/how) to cache JSDOM responses

    • false Doesn't uses a cache, JSDOM is run for every request

    • true|{} Uses an object in memory (created or provided) to store JSDOM generated response as {url: body}

    • function Delegate caching and retriving

      Called with args:

      The optional arguments (html, window, serialize) are passed only when the page was rendered with JSDOM. So when they're not passed, it expects you to return a pre-cached (if available) html string to use as a response instead. With this you can essentially control whether or not to actually invoke JSDOM for each request.

      Eg. Caching to disk selectively (this functionality is available as a helper function cacheToDisk)

        const cacheIndex = {}
        koaSSR(root, {
          cache: (ctx, html, window, serialize) => {
            // parse URL and omit query strings
            const url = URL.parse(ctx.url).pathname; // ignore '?query=xyz'
      
            // choose a sanitized filename
            const filename = '.ssr-cache/' + (_.kebabCase(url) || 'index') + '.html';
      
            // if html is provided, cache it:
            if (html) {
              fs.writeFile(filename, html);
              cacheIndex[filename] = true;
              return html; // and return it to be rendered
            }
      
            // if html isn't provided...
      
            // check if filename was cached
            if (cacheIndex[filename]) {
              ctx.type = 'html'; // (override stream's inferred type "application/octet-stream")
              return fs.createReadStream(filename);
            }
      
            // check if file exists anyways (from a previous run)
            if (await fs.exists(filename)) {
              cacheIndex[filename] = true;
              ctx.type = 'html';
              return fs.createReadStream(filename);
            }
      
            // if nothing is returned, JSDOM will be invoked
          }
        })
  • render [func] (defaut: (ctx, html) => ctx.body = html) Final function responsible for sending the final html as a response to the client by setting ctx.body=.

    Called with args:

    The optional arguments (window, serialize) are passed only when the page was rendered with JSDOM (either before caching for the first time, or when cache is set to false or cache function decides not to cache).

    Use this to customize response (even the cached response) for different users. Eg.

      koaSSR(root, {
        render: async (ctx, html) => {
    
          html = html.replace('</body>', `
            <script>
              window.userData = ${await User.findOne(ctx.user)}
              window.queryData = ${await Search.findResult(ctx.query)}
            </script>
          </body>`)
    
          ctx.body = html
        }
      })

    Note that in earlier eg. with cache we returned a stream in which case (use stream-replace because) html here would also have been the same stream object (render is called with the result of cache).

Helpers

Helper functions

  • cacheToDisk Helper function to be used as opts.cache for cahing to disk (as shown above).

      import koaSSR from 'koa-ssr'
      import {cacheToDisk} from 'koa-ssr/helpers'
    
      koaSSR(root, {
        cache: cacheToDisk(opts)
      })
    

    Options:

    • parseUrl [func] (defaut: url => URL.parse(url).pathname) Parse the url
    • dir [str] (defaut: '.ssr-cache/') Directory to use for cache files
    • filename [func] (defaut: url => Path.join(opts.dir, (_.kebabCase(url)||'index')+'.html')) Generate filename
    • invalidatePrevious [bool] (defaut: false) Do not use cache created from a previous run