jalla

0.11.3 • Public • Published

jalla

stability experimental npm version build status downloads js-standard-style

Jalla is an opinionated compiler and server in one. It makes web development fast, fun and exceptionally performant.

Jalla is an excellent choice when static files just don't cut it. Perhaps you need to render views dynamically, push (HTTP/2) assets or integrate with back-end services.

In short: a Koa server, a Browserify bundler for scripts and a PostCSS for styles. Documents are compiled using Documentify. Jalla is built with Choo in mind and is heavily inspired by Bankai.

Usage

$ jalla index.js

Working With Jalla

Jalla has a watch mode and a production mode. Setting the environment variable NODE_ENV to anything but development will cause jalla to perform more expensive compilation and optimizations on your code.

If the environment variable NODE_ENV is missing, jalla assumes you are in development and will default to watch mode which observes files for changes and recompiles them on the fly.

$ NODE_ENV=production jalla index.js

JavaScript

Scripts are compiled using Browserify. Custom transforms can be added using the browserify.transform field in your package.json file.

Example browserify config
// package.json
"browserify": {
  "transform": [
    "some-browserify-transform"
  ]
}
Included Browserify optimizations
split-require

Lazily load parts of your codebase. Jalla will transform dynamic imports into calls to split-require automatically (using a babel plugin), meaning you only have to call import('./some-file') to get bundle splitting right out of the box without any tooling footprint in your source code.

babelify

Run babel on your sourcecode. Will respect local .babelrc files for configuring the babel transform.

The following babel plugins are added by default:

brfs

Inline static assets in your application using the Node.js fs module.

envify

Use environment variables in your code.

nanohtml (not used in watch mode)

Choo-specific optimization which transpiles html templates for increased browser performance.

tinyify (not used in watch mode)

A while suite of optimizations and minifications removing unused code, significantly reducing file size.

CSS

CSS files are located and included automaticly. Whenever a JavaScript module is used in your application, jalla will try and find an adjacent index.css file in the same location. Jalla will also respect the style field in a modules package.json to determine which CSS file to include.

All CSS files are transpiled using PostCSS. To add PostCSS plugins, either add a postcss field to your package.json or, if you need to conditionally configure PostCSS, create a .postcssrc.js in the root of your project. See postcss-load-config for details.

Example PostCSS config
// package.json
"postcss": {
  "plugins": {
    "some-postcss-plugin": {}
  }
}
// .postcssrc.js
module.exports = config
 
function config (ctx) {
  var plugins = []
  if (ctx.env === 'production') {
    plugins.push(require('some-postcss-plugin'))
  }
  return { plugins }
}
The included PostCSS plugins
postcss-url

Rewrite URLs and copy assets from their source location. This means you can reference e.g. background images and the like using relative URLs and it'll just work™.

postcss-import

Inline files imported with @import. Works for both local files as well as for files in node_modules, just like it does in Node.js.

autoprefixer (not used in watch mode)

Automatically add vendor prefixes. Respects .browserlist to determine which browsers to support.

HTML

Jalla uses Documentify to compile server-rendered markup. Documentify can be configured in the package.json (see Documentify documentation). By default, jalla only applies HTML minification using posthtml-minifier.

Example Documentify config
// package.json
"documentify": {
  "transform": [
    [
      "./my-document.js",
      {
        "order": "end"
      }
    ]
  ]
}
// my-document.js
var hyperstream = require('hstream')
 
module.exports = document
 
function document () {
  return hyperstream({
    'html': {
      // add a class to the root html element
      class: 'Root'
    },
    'meta[name="viewport"]': {
      // instruct Mobile Safari to expand under the iPhone X notch
      content: 'width=device-width, initial-scale=1, viewport-fit=cover'
    },
    head: {
      // add some tracking script to the header
      _appendHtml: `
        <script async src="https://www.tracking-service.com/tracker.js?id=abc123"></script>
        <script>
          window.dataLayer = window.dataLayer || [];
          function track () { dataLayer.push(arguments); }
          track('js', new Date());
          track('config', 'abc123');
        </script>
      `
    }
  })
}

Assets

All files located in the root folder ./assets are automatically being served under the webpage root.

CLI Options

  • --service-worker, --sw entry point for a service worker, uses a subset of the optimization used for the entry file.
  • --css explicitly include a css file in the build
  • --quiet, -q disable printing to console
  • --debug, -d enable the node inspector, accepts a port as value
  • --base, -b base path where app will be served
  • --port, -p port to use for server

Service Workers

By supplying the path to a service worker entry file with the sw option, jalla will build and serve it's bundle from that path.

Registering a service worker with a Choo app is easily done using choo-service-worker.

app.use(require('choo-service-worker')('/sw.js'))

And then starting jalla with the sw option.

$ jalla index.js --sw sw.js

Information about application bundles and assets are exposed to the service worker during its build and can be accessed as environment variables.

  • process.env.ASSET_LIST a list of URLs to all included assets
Example service worker
// index.json
var choo = require('choo')
var app = choo()
 
app.route('/', require('./views/home'))
app.use(require('choo-service-worker')('/sw.js'))
 
module.exports = app.mount('body')
// sw.js
// use package.json version field as cache key
var CACHE_KEY = process.env.npm_package_version
var FILES = [
  '/',
  '/manifest.json'
].concat(process.env.ASSET_LIST)
 
self.addEventListener('install', function oninstall (event) {
  // cache landing page and all assets once service worker is installed
  event.waitUntil(
    caches
      .open(CACHE_KEY)
      .then((cache) => cache.addAll(FILES))
      .then(() => self.skipWaiting())
  )
})
 
self.addEventListener('activate', function onactivate (event) {
  // clear old caches on activate
  event.waitUntil(clear().then(() => self.clients.claim()))
})
 
self.addEventListener('fetch', function onfetch (event) {
  event.respondWith(
    caches.open(CACHE_KEY).then(function (cache) {
      return cache.match(req).then(function (cached) {
        if (req.cache === 'only-if-cached' && req.mode !== 'same-origin') {
          return cached
        }
 
        // try and fetch response and fallback to cache
        return self.fetch(event.request).then(function (response) {
          if (!response.ok) {
            if (fallback) return fallback
            else return response
          }
          cache.put(req, response.clone())
          return response
        }, function (err) {
          if (fallback) return fallback
          return err
        })
      })
    })
  )
})
 
// clear application cache
// () -> Promise
function clear () {
  return caches.keys().then(function (keys) {
    var caches = keys.filter((key) => key !== CACHE_KEY)
    return Promise.all(keys.map((key) => caches.delete(key)))
  })
}

Manifest

A bare-bones application manifest is generated based on the projects package.json. You could either place a manifest.json in the assets folder or you can generate one using a custom middleware.

API

After instantiating the jalla server, middleware can be added just like you'd do with any Koa app. The application is an instance of Koa and supports all Koa middleware.

Jalla will await all middleware to finish before trying to render a HTML response. If the response has been redirected (i.e. calling ctx.redirect) or if a value has been assigned to ctx.body jalla will not render any HTML response.

var mount = require('koa-mount')
var jalla = require('jalla')
var app = jalla('index.js')
 
// only allow robots in production
app.use(mount('/robots.txt', function (ctx, next) {
  ctx.type = 'text/plain'
  ctx.body = `
    User-agent: *
    Disallow: ${process.env.NODE_ENV === 'production' ? '' : '/'}
  `
}))
 
app.listen(8080)

API Options

Options can be supplied as the second argument (jalla('index.js', opts)).

  • sw entry point for a service worker
  • css explicitly include a css file in the build
  • quiet disable printing to console
  • base base path where app will be served

SSR (Server side render)

When rendering HTML, jalla will make two render passes; once to allow your views to fetch the content it needs and once again to generate the resulting HTML. On the application state there will be an prefetch property which is an array for you to push promises into. Once all promises are resolved, the second render will commence.

Example using state.prefetch
var fetch = require('node-fetch')
var html = require('choo/html')
var choo = require('choo')
var app = choo()
 
app.route('/', main)
app.use(store)
 
module.exports = app.mount('body')
 
function main (state, emit) {
  if (!state.name) {
    emit('fetch')
    return html`<body>Loading…</body>`
  }
 
  return html`
    <body>
      <h1>Hello ${state.name}!</h1>
    </body>
  `
}
 
function store (state, emitter) {
  state.name = state.name || null
 
  emitter.on('fetch', function () {
    var promise = fetch('https://some-api.com')
      .then((res) => res.text())
      .then(function (name) {
        state.name = name
        emitter.emit('render')
      })
 
    if (state.prefetch) {
      // ask jalla to wait for this promise before rendering the resulting HTML
      state.prefetch.push(promise)
    }
  })
}

Caching HTML

Jalla will render HTML for every request, which is excellent for dynamic content but might not be what you need for all your views and endpoints. You will probably want to add custom caching middleware or an external caching layer ontop of your server for optimal performance.

Setting up Cloudflare caching with jalla

Cloudflares free tier is an excellent complement to jalla for caching HTML responses. You'll need to setup Cloudflare to cache everything and to respect existing cache headers. This means you'll be able to tell Cloudflare which responses to cache and for how long by setting the s-maxage header.

However, when publishing a new version of your webpage or when the cache should be invalidated due to some external service update, you'll need to purge the Cloudflare cache. For that purpose, there's cccpurge.

Example purging cache on server startup
var purge = require('cccpurge')
var jalla = require('jalla')
var app = jalla('index.js')
 
app.use(function (ctx, next) {
  if (ctx.accepts('html')) {
    // cache all html responses on Cloudflare for a week
    ctx.set('Cache-Control', `s-maxage=${60 * 60 * 24 * 7}, max-age=0`)
  }
  return next()
})
 
if (app.env === 'production') {
  // purge cache before starting production server
  cccpurge(require('./index'), {
    root: 'https://www.my-blog.com',
    email: 'foo@my-blog.com',
    zone: '<CLOUDFLARE_ZONE_ID>',
    key: '<CLOUDFLARE_API_KEY>'
  }, function (err) {
    if (err) process.exit(1)
    app.listen(8080)
  })
} else {
  app.listen(8080)
}

ctx.state

Whatever is stored in the state object after all middleware has run will be used as state when rendering the HTML response. The resulting application state will be exposed to the client as window.initialState and will be automatically picked up by Choo. Using ctx.state is how you bootstrap your client with server generated content.

Meta data for the page being rendered can be added to ctx.state.meta. A <meta> tag will be added to the header for every property therein.

Example decorating ctx.state
var geoip = require('geoip-lite')
 
app.use(function (ctx, next) {
  if (ctx.accepts('html')) {
    // add meta data
    ctx.state.meta = { 'og:url': 'https://webpage.com' + ctx.url }
 
    // expose user location on state
    ctx.state.location = geoip.lookup(ctx.ip)
  }
  return next()
})

ctx.assets

Compiled assets (scripts and styles) are exposed on the koa ctx object as an object with the properties file, map, buffer and url.

Example adding Link headers for all JS assets
app.use(function (ctx, next) {
  if (!ctx.accepts('html')) return next()
 
  // find all javascript assets
  var bundles = Object.values(ctx.assets)
    .filter((asset) => /\.js$/.test(asset.url))
    .map((asset) => `<${asset.url}>; rel=preload; as=script`)
 
  // HTTP/2 push all bundles
  ctx.append('Link', bundles)
 
  return next()
})

Events

Most of the internal workings are exposed as events on the application (Koa) instance.

app.on('error', callback(err))

When an internal error occurs or a route could not be served. If an HTTP error was encountered, the status code is available on the error object.

app.on('warning', callback(warning))

When a non-critical error was encountered, e.g. a postcss plugin failed to parse a rule.

app.on('update', callback(file))

When a file has been changed.

app.on('progress', callback(file, uri))

When an entry file is being bundled.

app.on('bundle:script', callback(file, uri, buff)

When a script file finishes bundling.

app.on('bundle:style', callback(file, uri, buff)

When a css file finishes bundling.

app.on('bundle:file', callback(file))

When a file is being included in a bundle.

app.on('timing', callback(time, ctx))

When a HTTP response has been sent.

app.on('start', callback(port))

When the server has started and in listening.

Todo

  • Fix CSS asset handling
  • Add bundle splitting for CSS
  • Export compiled files to disc
  • Pretty UI

Package Sidebar

Install

npm i jalla

Weekly Downloads

10

Version

0.11.3

License

MIT

Unpacked Size

50 kB

Total Files

15

Last publish

Collaborators

  • tornqvist
  • antontrollback