UNPKG

houl

Version:

Full-contained static site workflow

166 lines (132 loc) 4.09 kB
'use strict' const fs = require('fs') const path = require('path') const url = require('url') const vfs = require('vinyl-fs') const { Readable, Writable } = require('stream') const browserSync = require('browser-sync') const proxy = require('http-proxy-middleware') const mime = require('mime') const util = require('../util') const defaultBsOptions = { ui: false, ghostMode: false, notify: false } module.exports = (config, bsOptions, depCache) => { const bs = browserSync.create() bsOptions = util.merge(defaultBsOptions, bsOptions) bsOptions = util.merge(bsOptions, { port: config.port, startPath: config.basePath }) bsOptions.server = config.output injectMiddleware(bsOptions, transformer(config, depCache)) config.proxy.forEach(p => { injectMiddleware(bsOptions, proxy(p.context, p.config)) }) bs.init(bsOptions) return bs } function injectMiddleware(options, middleware) { if (Array.isArray(options.middleware)) { options.middleware.push(middleware) return } if (options.middleware) { options.middleware = [options.middleware, middleware] return } options.middleware = [middleware] } function transformer(config, depCache) { const basePath = config.basePath return (req, res, next) => { const parsedPath = url.parse(req.url).pathname const reqPath = normalizePath(parsedPath) const outputPath = resolveBase(basePath, reqPath) if (outputPath === null) { return next() } const rule = config.findRuleByOutput(outputPath, inputPath => { return fs.existsSync(path.join(config.input, inputPath)) }) // If any rules are not matched, leave it to browsersync if (!rule) return next() const inputPath = path.join(config.input, rule.getInputPath(outputPath)) // If Input file is excluded by config, leave it to browsersync if (config.isExclude(inputPath)) return next() // If it is directory, redirect to path that added trailing slash // There should not be not found error // since it is already checked in previous process if (fs.statSync(inputPath).isDirectory()) { return redirect(res, reqPath + '/') } const responseFile = new Writable({ objectMode: true, write(file, encoding, cb) { const response = contents => { res.setHeader('Content-Type', mime.getType(outputPath)) res.end(contents) } // If the inputPath hits the cache, just use cached output const contentsStr = String(file.contents) if (depCache.test(file.path, contentsStr)) { const contents = depCache.get(file.path) response(contents) cb(null) return } const inputProxy = new Readable({ objectMode: true, read() { this.push(file) this.push(null) } }) rule .task(inputProxy) .on('data', transformed => { // Register the transformed contents into cache depCache.register(file.path, contentsStr, transformed.contents) response(transformed.contents) }) .on('end', cb) } }) vfs.src(inputPath).pipe(responseFile) } } function redirect(res, pathname) { res.statusCode = 301 res.setHeader('Location', pathname) res.end() } function resolveBase(base, target) { function loop(base, target) { const bh = base[0] if (bh === undefined) { return '/' + target.join('/') } const th = target[0] if (bh !== th) { return null } return loop(base.slice(1), target.slice(1)) } const baseList = base.split('/').filter(item => item !== '') const targetList = trimHead(target.split('/')) return loop(baseList, targetList) } function normalizePath(pathname) { if (isDirectoryPath(pathname)) { pathname = path.join(pathname, 'index.html') } return pathname } function isDirectoryPath(pathname) { return /\/$/.test(pathname) } function trimHead(list) { return util.dropWhile(list, item => item === '') }