jalla
![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)
Jalla is a Choo compiler and server in one, making 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, set custom headers or integrate an API.
In short, Jalla is a Koa server, a Browserify bundler
for scripts and a PostCSS processor for styles. Documents are
compiled using Documentify. And it's all configured for you.
Usage
Jalla performs a series of optimizations when compiling your code. By default
it will enter development mode – meaning fast compilation times and automatic
recompilation when files are updated.
The fastes way to get up and running is by using the CLI and pointing it to your
Choo app entry point. If you name your CSS files index.css
and place them
adjacent to your script files, they will be automatically detected and included.
$ jalla index.js
Setting the environment variable NODE_ENV
to anything other than
development
will cause jalla to perform more expensive compilation and optimizations on your code. Taking longer to compile but making it faster to
run.
$ NODE_ENV=production jalla index.js
Options
--css
explicitly include a css file in the build--service-worker, --sw
entry point for a service worker--base, -b
base path where app will be served--watch, -w
watch files for changes (default in development
)--dir, -d
output directory, use with build and serve--quiet, -q
disable printing to console--inspect, -i
enable the node inspector, accepts a port as value--port, -p
port to use for server
Build
Jalla can write all assets to disk, and then serve them statically. This greatly
increases the server startup times and makes the server more resilient to
failure or sleep. This is especially usefull for serverless plarforms, such as
now or AWS Lambda
et. al.
By default files will be written to the folder dist
, but this can be changed
using the dir
option.
$ NODE_ENV=production jalla build index.js --dir output
Serve
For fast server start up times, use the serve
command. In serve mode, jalla
will not compile any assets but instead serve built assets produced by the
build command.
By default jalla will look for built files in the dist
folder. Use the dir
option to change this.
$ NODE_ENV=production jalla serve --dir output
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.
Just like the CLI, the programatic API accepts a Choo app entry point
as it's first argument, followed by options.
var jalla = require('jalla')
var app = jalla('index.js', {
sw: 'sw.js',
serve: process.env.NODE_ENV === 'production'
})
app.listen(8080)
Server Side Rendering
For every request that comes in (which accepts HTML and is not an asset), unless
handeled by custom middleware, jalla will try and render an HTML response. Jalla
will await all custom 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.
During server side rendering a status
code can be added to the state which
will be used for the HTTP response. This is usefull to set proper 404
or error
codes.
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)
Custom HTML
By default, Jalla will render your app in an empty HTML document, injecting
assets and initial state. You can override the default empty document by adding
an index.html
file adjacent to the application entry file.
You can inform jalla of where in the document you'd like to mount the
application by exporting the Choo app instance after calling .mount()
.
module.exports = app.mount('#app')
<body>
<div id="app"></div>
<footer>© 2019</footer>
</body>
Prefetching data
Often times you'll need to fetch some data to render the application views. For
this, jalla will expose an array, prefetch
, on the application state. Jalla
will render the app once and then wait for the promises in the array to resolve
before issuing another render pass using the state generated the first time.
module.exports = function (state, emitter) {
state.data = state.data || null
emitter.on('fetch', function () {
var request = window.fetch('/my/api')
.then((res) => res.json())
.then(function (data) {
state.data = data
emitter.emit('render')
})
if (state.prefetch) state.prefetch.push(request)
})
}
Apart from prefetch
, jalla also exposes the HTTP req
and res
objects.
They can be usefull to read cookies or set headers. Writing to the response
stream, however, is not recommended.
ctx.state
The data 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.location = geoip.lookup(ctx.ip)
}
return next()
})
ctx.assets
Compiled assets are exposed on ctx.assets
as a Map
object. The assets hold
data such as the asset url, size and hash. There's also a read
method for
retreiving the asset buffer.
Example adding Link headers for all JS assets
app.use(function (ctx, next) {
if (!ctx.accepts('html')) return next()
for (let [id, asset] of ctx.assets) {
if (id !== 'bundle.js' && /\.js$/.test(id)) {
ctx.append('Link', `<${asset.url}>; rel=preload; as=script`)
}
}
return next()
})
Assets
Static assets can be placed in an assets
folder adjacent to the Choo app entry
file. Files in the assets folder will be served statically by jalla.
Manifest
A bare-bones application manifest is generated based on the projects
package.json
. You can either place a custom manifest.json
in the
assets folder or you can generate one using a custom middleware.
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 assets are exposed to the service worker during
its build and can be accessed as an environment variable.
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 = ['/'].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(async function (cache) {
try {
var cached = await cache.match(req)
var response = self.fetch(event.request)
if (req.method.toUpperCase() === 'GET') {
await cache.put(req, response.clone())
}
return response
} catch (err) {
if (cached) return cached
throw 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)))
})
}
Advanced Usage
If you need to jack into the compilation and build pipeline of jalla, there's a
pipeline
utility attached to the app instance. The pipline is labeled so that
you can hook into any specific step of the compilation to add or inspect assets.
Using the method get
you can retrieve an array that holds the differnt steps
associated with a specific compilation step. You may push your own functions to
this array to have them added to the pipeline.
The labels on the pipeline are:
scripts
compiles the main bundle and any dynamic bundlesstyles
detect CSS files and compile into single bundleassets
locate static assetsmanifest
generate manifest.json file unless one already existsservice-worker
compile the service workerbuild
write files to disk
The functions in the pipeline have a similar signature to that of Choo routes.
They are instantiated with a state object and a function for emitting events.
A pipline function should return a function which will be called whenever jalla
is compiling the app. The pipline steps are called in series, and have access
to the assets and dependencies of all prior steps.
var fs = require('fs')
var jalla = require('jalla')
var app = jalla('index.js')
app.pipeline.get('assets').push(function (state, emit) {
return function (cb) {
emit('progress', 'data.csv')
fs.readFile('data.csv', function (err, buffer) {
if (err) return cb(err)
emit('asset', 'data.json', buffer)
cb()
})
}
})
app.listen(8080)
Configuration
The bundling is handled by tested and reliable tools which can be configured
just as you are used to.
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": [
["aliasify", {
"aliases": {
"d3": "./shims/d3.js",
"underscore": "lodash"
}
}]
]
}
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.
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 wide suite of optimizations and minifications removing unused code,
significantly reducing file size.
CSS
CSS files are looked up 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": {
"postcss-custom-properties": {}
}
}
module.exports = config
function config (ctx) {
var plugins = []
if (ctx.env !== 'development') {
plugins.push(require('postcss-custom-properties'))
}
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-transform.js"]
]
}
var hyperstream = require('hstream')
module.exports = transform
function transform () {
return hyperstream({
'html': {
class: 'page-root'
},
'meta[name="viewport"]': {
content: 'width=device-width, initial-scale=1, viewport-fit=cover'
},
head: {
_appendHtml: `
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-XXXXXXXX-X"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-XXXXXXXX-X');
</script>
`
}
})
}