Created
September 6, 2016 01:00
-
-
Save girvo/0038796cd979bdc6f4ba4e529ebed5e7 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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