You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

309 lines
9.0 KiB

8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
  1. 'use strict'
  2. const arrayify = require('array-back')
  3. const path = require('path')
  4. const url = require('url')
  5. const debug = require('./debug')
  6. const mw = require('./middleware')
  7. const t = require('typical')
  8. const compose = require('koa-compose')
  9. const flatten = require('reduce-flatten')
  10. const MiddlewareStack = require('local-web-server-stack')
  11. const mockResponses = require('koa-mock-response')
  12. class DefaultStack extends MiddlewareStack {
  13. addAll () {
  14. this
  15. .addCors()
  16. .addJson()
  17. .addRewrite()
  18. .addBodyParser()
  19. .addBlacklist()
  20. .addCache()
  21. .addMimeOverride()
  22. .addCompression()
  23. .addLogging()
  24. .addMockResponses()
  25. .addSpa()
  26. .addStatic()
  27. .addIndex()
  28. return this
  29. }
  30. /**
  31. * allow from any origin
  32. */
  33. addCors () {
  34. this.push({ middleware: require('kcors') })
  35. return this
  36. }
  37. /* pretty print JSON */
  38. addJson () {
  39. this.push({ middleware: require('koa-json') })
  40. return this
  41. }
  42. /* rewrite rules */
  43. addRewrite (rewriteRules) {
  44. this.push({
  45. optionDefinitions: {
  46. name: 'rewrite', alias: 'r', type: String, multiple: true,
  47. typeLabel: '[underline]{expression} ...',
  48. description: "A list of URL rewrite rules. For each rule, separate the 'from' and 'to' routes with '->'. Whitespace surrounded the routes is ignored. E.g. '/from -> /to'."
  49. },
  50. middleware: function (cliOptions) {
  51. const options = parseRewriteRules(arrayify(cliOptions.rewrite || rewriteRules))
  52. if (options.length) {
  53. return options.map(route => {
  54. if (route.to) {
  55. /* `to` address is remote if the url specifies a host */
  56. if (url.parse(route.to).host) {
  57. const _ = require('koa-route')
  58. debug('proxy rewrite', `${route.from} -> ${route.to}`)
  59. return _.all(route.from, mw.proxyRequest(route))
  60. } else {
  61. const rewrite = require('koa-rewrite')
  62. const rmw = rewrite(route.from, route.to)
  63. rmw._name = 'rewrite'
  64. return rmw
  65. }
  66. }
  67. })
  68. }
  69. }
  70. })
  71. return this
  72. }
  73. /* must come after rewrite.
  74. See https://github.com/nodejitsu/node-http-proxy/issues/180. */
  75. addBodyParser () {
  76. this.push({ middleware: require('koa-bodyparser') })
  77. return this
  78. }
  79. /* path blacklist */
  80. addBlacklist (forbidList) {
  81. this.push({
  82. optionDefinitions: {
  83. name: 'forbid', alias: 'b', type: String,
  84. multiple: true, typeLabel: '[underline]{path} ...',
  85. description: 'A list of forbidden routes.'
  86. },
  87. middleware: function (cliOptions) {
  88. forbidList = arrayify(cliOptions.forbid || forbidList)
  89. if (forbidList.length) {
  90. const pathToRegexp = require('path-to-regexp')
  91. debug('forbid', forbidList.join(', '))
  92. return function blacklist (ctx, next) {
  93. if (forbidList.some(expression => pathToRegexp(expression).test(ctx.path))) {
  94. ctx.status = 403
  95. } else {
  96. return next()
  97. }
  98. }
  99. }
  100. }
  101. })
  102. return this
  103. }
  104. /* cache */
  105. addCache () {
  106. this.push({
  107. optionDefinitions: {
  108. name: 'no-cache', alias: 'n', type: Boolean,
  109. description: 'Disable etag-based caching - forces loading from disk each request.'
  110. },
  111. middleware: function (cliOptions) {
  112. const noCache = cliOptions['no-cache']
  113. if (!noCache) {
  114. return [
  115. require('koa-conditional-get')(),
  116. require('koa-etag')()
  117. ]
  118. }
  119. }
  120. })
  121. return this
  122. }
  123. /* mime-type overrides */
  124. addMimeOverride (mime) {
  125. this.push({
  126. middleware: function (cliOptions) {
  127. mime = cliOptions.mime || mime
  128. if (mime) {
  129. debug('mime override', JSON.stringify(mime))
  130. return mw.mime(mime)
  131. }
  132. }
  133. })
  134. return this
  135. }
  136. /* compress response */
  137. addCompression (compress) {
  138. this.push({
  139. optionDefinitions: {
  140. name: 'compress', alias: 'c', type: Boolean,
  141. description: 'Serve gzip-compressed resources, where applicable.'
  142. },
  143. middleware: function (cliOptions) {
  144. compress = t.isDefined(cliOptions.compress)
  145. ? cliOptions.compress
  146. : compress
  147. if (compress) {
  148. debug('compression', 'enabled')
  149. return require('koa-compress')()
  150. }
  151. }
  152. })
  153. return this
  154. }
  155. /* Logging */
  156. addLogging (format, options) {
  157. options = options || {}
  158. this.push({
  159. optionDefinitions: {
  160. name: 'log-format',
  161. alias: 'f',
  162. type: String,
  163. description: "If a format is supplied an access log is written to stdout. If not, a dynamic statistics view is displayed. Use a preset ('none', 'dev','combined', 'short', 'tiny' or 'logstalgia') or supply a custom format (e.g. ':method -> :url')."
  164. },
  165. middleware: function (cliOptions) {
  166. format = cliOptions['log-format'] || format
  167. if (cliOptions.verbose && !format) {
  168. format = 'none'
  169. }
  170. if (format !== 'none') {
  171. const morgan = require('koa-morgan')
  172. if (!format) {
  173. const streamLogStats = require('stream-log-stats')
  174. options.stream = streamLogStats({ refreshRate: 500 })
  175. return morgan('common', options)
  176. } else if (format === 'logstalgia') {
  177. morgan.token('date', () => {
  178. var d = new Date()
  179. return (`${d.getDate()}/${d.getUTCMonth()}/${d.getFullYear()}:${d.toTimeString()}`).replace('GMT', '').replace(' (BST)', '')
  180. })
  181. return morgan('combined', options)
  182. } else {
  183. return morgan(format, options)
  184. }
  185. }
  186. }
  187. })
  188. return this
  189. }
  190. /* Mock Responses */
  191. addMockResponses (mocks) {
  192. this.push({
  193. middleware: function (cliOptions) {
  194. mocks = arrayify(cliOptions.mocks || mocks)
  195. return mocks.map(mock => {
  196. if (mock.module) {
  197. const modulePath = path.resolve(path.join(cliOptions.directory, mock.module))
  198. mock.responses = require(modulePath)
  199. }
  200. if (mock.responses) {
  201. return mockResponses(mock.route, mock.responses)
  202. } else if (mock.response) {
  203. mock.target = {
  204. request: mock.request,
  205. response: mock.response
  206. }
  207. return mockResponses(mock.route, mock.target)
  208. }
  209. })
  210. }
  211. })
  212. return this
  213. }
  214. /* for any URL not matched by static (e.g. `/search`), serve the SPA */
  215. addSpa (spa, assetTest) {
  216. this.push({
  217. optionDefinitions: {
  218. name: 'spa', alias: 's', type: String, typeLabel: '[underline]{file}',
  219. description: 'Path to a Single Page App, e.g. app.html.'
  220. },
  221. middleware: function (cliOptions) {
  222. spa = cliOptions.spa || spa || 'index.html'
  223. assetTest = new RegExp(cliOptions['spa-asset-test'] || assetTest || '\\.')
  224. if (spa) {
  225. const send = require('koa-send')
  226. const _ = require('koa-route')
  227. debug('SPA', spa)
  228. return _.get('*', function spaMw (ctx, route, next) {
  229. const root = path.resolve(cliOptions.directory || process.cwd())
  230. if (ctx.accepts('text/html') && !assetTest.test(route)) {
  231. debug(`SPA request. Route: ${route}, isAsset: ${assetTest.test(route)}`)
  232. return send(ctx, spa, { root: root }).then(next)
  233. } else {
  234. return send(ctx, route, { root: root }).then(next)
  235. }
  236. })
  237. }
  238. }
  239. })
  240. return this
  241. }
  242. /* serve static files */
  243. addStatic (root, options) {
  244. this.push({
  245. optionDefinitions: {
  246. name: 'directory', alias: 'd', type: String, typeLabel: '[underline]{path}',
  247. description: 'Root directory, defaults to the current directory.'
  248. },
  249. middleware: function (cliOptions) {
  250. /* update global cliOptions */
  251. cliOptions.directory = cliOptions.directory || root || process.cwd()
  252. options = Object.assign({ hidden: true }, options)
  253. if (cliOptions.directory) {
  254. const serve = require('koa-static')
  255. return serve(cliOptions.directory, options)
  256. }
  257. }
  258. })
  259. return this
  260. }
  261. /* serve directory index */
  262. addIndex (path, options) {
  263. this.push({
  264. middleware: function (cliOptions) {
  265. path = cliOptions.directory || path || process.cwd()
  266. options = Object.assign({ icons: true, hidden: true }, options)
  267. if (path) {
  268. const serveIndex = require('koa-serve-index')
  269. return serveIndex(path, options)
  270. }
  271. }
  272. })
  273. return this
  274. }
  275. }
  276. module.exports = DefaultStack
  277. function parseRewriteRules (rules) {
  278. return rules && rules.map(rule => {
  279. if (t.isString(rule)) {
  280. const matches = rule.match(/(\S*)\s*->\s*(\S*)/)
  281. if (!(matches && matches.length >= 3)) throw new Error('Invalid rule: ' + rule)
  282. return {
  283. from: matches[1],
  284. to: matches[2]
  285. }
  286. } else {
  287. return rule
  288. }
  289. })
  290. }