Skip to content

Instantly share code, notes, and snippets.

@girvo
Created September 6, 2016 01:00
Show Gist options
  • Save girvo/0038796cd979bdc6f4ba4e529ebed5e7 to your computer and use it in GitHub Desktop.
Save girvo/0038796cd979bdc6f4ba4e529ebed5e7 to your computer and use it in GitHub Desktop.
/**
* Tiny module to generate an Express middleware that maps the request path to
* a specified folder, allowing for "semi-static" routes without needing to
* define them ahead of time.
*
* Configuration can be passed to the `semiStatic` function when registering the
* middleware. The format is described below, with the "context" property being
* passed to the template's render call, and `context.req` being the request
* object itself.
*
* ```json
* {
* defaultFile: 'index',
* fileExt: '.html',
* templateExt: '.swig',
* staticDir: '/static'
* }
* ```
*
* Install required dependencies:
* npm install --save creed ramda
*
* Usage:
* ```js
* const express = require('express')
* const swig = require('swig')
* const semiStatic = require('./semi-static')
*
* const app = express()
*
* app.engine('swig', swig.renderFile)
* app.set('view engine', 'swig')
* app.set('views', resolve(__dirname, '../views'))
*
* app.use(semiStatic())
*
* app.listen(3000, () => console.log('Listening on port 3000...'))
* ```
*/
'use strict'
const R = require('ramda')
const { stat } = require('fs')
const { resolve } = require('path')
const { coroutine, fromNode } = require('creed')
// Promisified path and fs functions
const resolveP = fromNode(resolve)
const statP = fromNode(stat)
/**
* Internal functions
*/
// Function to check if a file exists, returns a promised bool. Swallows errors
// fileExistsP :: String -> Promise e Boolean
const fileExistsP = coroutine(function * (path) {
try {
const stats = yield statP(path)
return stats.isFile()
} catch (e) {
return false
}
})
// appendSlash :: String -> String
const appendSlash = R.ifElse(
f => f.charAt(f.length - 1) !== '/',
f => `${f}/`,
R.identity
)
// stripSlash :: String -> String
const stripSlash = R.ifElse(
f => f.charAt(f.length - 1) === '/',
f => f.slice(0, f.length - 1),
R.identity
)
// Path-to-template middleware generator
// Takes config object and returns configured middleware
//
// semiStatic :: Object -> Function
function semiStatic (userConfig = {}) {
// Define our configuration
const config = Object.assign({}, {
defaultFile: 'index',
fileExt: '.html',
templateExt: '.swig',
staticDir: '/static'
}, userConfig)
// Build our context object
const context = R.defaultTo({})(R.prop('context', userConfig))
// getTemplateFile :: String -> String
const getTemplateFile = (folder) => folder + config.fileExt + config.templateExt
// Return the middleware
return function (req, res, next) {
const initialPath = req.app.get('views') + config.staticDir + req.path
const exactPath = getTemplateFile(stripSlash(initialPath))
const defaultPath = getTemplateFile(appendSlash(initialPath) + config.defaultFile)
// Promise-returning coroutine to check which path exists on the fs
// exactPath takes precedence over defaultPath
const whichPath = coroutine(function * () {
// Return exact path if it exists
const exactPathExists = yield fileExistsP(exactPath)
if (exactPathExists) return exactPath
// Return default path if it exists
const defaultPathExists = yield fileExistsP(defaultPath)
if (defaultPathExists) return defaultPath
// Neither match, throw an error so the promises catch() can move to next middleware
throw new Error('No path matched')
})
// Execute our coroutine, render template if it returns a path
whichPath().then(verifiedPath => {
// Configure the context object
context.req = req
// Load the template and render it
return res.render(verifiedPath, context)
}).catch(() => {
// Return to next middleware if the path did not exist
return next()
})
}
}
module.exports = semiStatic
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment