jalla

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.
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.
$ jalla index.js
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
"browserify": {
"transform": [
"some-browserify-transform"
]
}
Included Browserify optimizations
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.
Run babel on your sourcecode. Will respect local .babelrc
files for
configuring the babel transform.
The following babel plugins are added by default:
Inline static assets in your application using the Node.js fs
module.
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
"postcss": {
"plugins": {
"some-postcss-plugin": {}
}
}
module.exports = config
function config (ctx) {
var plugins = []
if (ctx.env === 'production') {
plugins.push(require('some-postcss-plugin'))
}
return { plugins }
}
The included PostCSS plugins
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™.
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.
postcss-csso (not used in watch mode)
Cleans, compresses and restructures CSS for optimal performance and file size.
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
"documentify": {
"transform": [
[
"./my-document.js",
{
"order": "end"
}
]
]
}
var hyperstream = require('hstream')
module.exports = document
function document () {
return hyperstream({
'html': {
class: 'Root'
},
'meta[name="viewport"]': {
content: 'width=device-width, initial-scale=1, viewport-fit=cover'
},
head: {
_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--build, -b
write assets to disc and exit--serve, -s
serve built files from disk--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
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')
var CACHE_KEY = process.env.npm_package_version
var FILES = [
'/',
'/manifest.json'
].concat(process.env.ASSET_LIST)
self.addEventListener('install', function oninstall (event) {
event.waitUntil(
caches
.open(CACHE_KEY)
.then((cache) => cache.addAll(FILES))
.then(() => self.skipWaiting())
)
})
self.addEventListener('activate', function onactivate (event) {
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
}
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
})
})
})
)
})
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')
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 workercss
explicitly include a css file in the buildquiet
disable printing to consolebase
base path where app will be servedserve
serve built files from disk (path or bool)
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) {
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')) {
ctx.set('Cache-Control', `s-maxage=${60 * 60 * 24 * 7}, max-age=0`)
}
return next()
})
if (app.env === 'production') {
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')) {
ctx.state.meta = { 'og:url': 'https://webpage.com' + ctx.url }
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()
var bundles = Object.values(ctx.assets)
.filter((asset) => /\.js$/.test(asset.url))
.map((asset) => `<${asset.url}>; rel=preload; as=script`)
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