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.
		
		
		
		
		
			
		
			
				
					300 lines
				
				9.5 KiB
			
		
		
			
		
	
	
					300 lines
				
				9.5 KiB
			| 
								 
											3 years ago
										 
									 | 
							
								'use strict'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/* eslint-disable no-multi-spaces */
							 | 
						||
| 
								 | 
							
								const indent              = '    '
							 | 
						||
| 
								 | 
							
								const branchIndent        = '│   '
							 | 
						||
| 
								 | 
							
								const midBranchIndent     = '├── '
							 | 
						||
| 
								 | 
							
								const endBranchIndent     = '└── '
							 | 
						||
| 
								 | 
							
								const wildcardDelimiter   = '*'
							 | 
						||
| 
								 | 
							
								const pathDelimiter       = '/'
							 | 
						||
| 
								 | 
							
								const pathRegExp          = /(?=\/)/
							 | 
						||
| 
								 | 
							
								/* eslint-enable */
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function parseFunctionName (fn) {
							 | 
						||
| 
								 | 
							
								  let fName = fn.name || ''
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  fName = fName.replace('bound', '').trim()
							 | 
						||
| 
								 | 
							
								  fName = (fName || 'anonymous') + '()'
							 | 
						||
| 
								 | 
							
								  return fName
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function parseMeta (meta) {
							 | 
						||
| 
								 | 
							
								  if (Array.isArray(meta)) return meta.map(m => parseMeta(m))
							 | 
						||
| 
								 | 
							
								  if (typeof meta === 'symbol') return meta.toString()
							 | 
						||
| 
								 | 
							
								  if (typeof meta === 'function') return parseFunctionName(meta)
							 | 
						||
| 
								 | 
							
								  return meta
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function buildMetaObject (route, metaArray) {
							 | 
						||
| 
								 | 
							
								  const out = {}
							 | 
						||
| 
								 | 
							
								  const cleanMeta = this.buildPrettyMeta(route)
							 | 
						||
| 
								 | 
							
								  if (!Array.isArray(metaArray)) metaArray = cleanMeta ? Reflect.ownKeys(cleanMeta) : []
							 | 
						||
| 
								 | 
							
								  metaArray.forEach(m => {
							 | 
						||
| 
								 | 
							
								    const metaKey = typeof m === 'symbol' ? m.toString() : m
							 | 
						||
| 
								 | 
							
								    if (cleanMeta && cleanMeta[m]) {
							 | 
						||
| 
								 | 
							
								      out[metaKey] = parseMeta(cleanMeta[m])
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  })
							 | 
						||
| 
								 | 
							
								  return out
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function prettyPrintRoutesArray (routeArray, opts = {}) {
							 | 
						||
| 
								 | 
							
								  if (!this.buildPrettyMeta) throw new Error('buildPrettyMeta not defined')
							 | 
						||
| 
								 | 
							
								  opts.includeMeta = opts.includeMeta || null // array of meta objects to display
							 | 
						||
| 
								 | 
							
								  const mergedRouteArray = []
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  let tree = ''
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  routeArray.sort((a, b) => {
							 | 
						||
| 
								 | 
							
								    if (!a.path || !b.path) return 0
							 | 
						||
| 
								 | 
							
								    return a.path.localeCompare(b.path)
							 | 
						||
| 
								 | 
							
								  })
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // merge alike paths
							 | 
						||
| 
								 | 
							
								  for (let i = 0; i < routeArray.length; i++) {
							 | 
						||
| 
								 | 
							
								    const route = routeArray[i]
							 | 
						||
| 
								 | 
							
								    const pathExists = mergedRouteArray.find(r => route.path === r.path)
							 | 
						||
| 
								 | 
							
								    if (pathExists) {
							 | 
						||
| 
								 | 
							
								      // path already declared, add new method and break out of loop
							 | 
						||
| 
								 | 
							
								      pathExists.handlers.push({
							 | 
						||
| 
								 | 
							
								        method: route.method,
							 | 
						||
| 
								 | 
							
								        opts: route.opts.constraints || undefined,
							 | 
						||
| 
								 | 
							
								        meta: opts.includeMeta ? buildMetaObject.call(this, route, opts.includeMeta) : null
							 | 
						||
| 
								 | 
							
								      })
							 | 
						||
| 
								 | 
							
								      continue
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    const routeHandler = {
							 | 
						||
| 
								 | 
							
								      method: route.method,
							 | 
						||
| 
								 | 
							
								      opts: route.opts.constraints || undefined,
							 | 
						||
| 
								 | 
							
								      meta: opts.includeMeta ? buildMetaObject.call(this, route, opts.includeMeta) : null
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    mergedRouteArray.push({
							 | 
						||
| 
								 | 
							
								      path: route.path,
							 | 
						||
| 
								 | 
							
								      methods: [route.method],
							 | 
						||
| 
								 | 
							
								      opts: [route.opts],
							 | 
						||
| 
								 | 
							
								      handlers: [routeHandler]
							 | 
						||
| 
								 | 
							
								    })
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // insert root level path if none defined
							 | 
						||
| 
								 | 
							
								  if (!mergedRouteArray.filter(r => r.path === pathDelimiter).length) {
							 | 
						||
| 
								 | 
							
								    const rootPath = {
							 | 
						||
| 
								 | 
							
								      path: pathDelimiter,
							 | 
						||
| 
								 | 
							
								      truncatedPath: '',
							 | 
						||
| 
								 | 
							
								      methods: [],
							 | 
						||
| 
								 | 
							
								      opts: [],
							 | 
						||
| 
								 | 
							
								      handlers: [{}]
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // if wildcard route exists, insert root level after wildcard
							 | 
						||
| 
								 | 
							
								    if (mergedRouteArray.filter(r => r.path === wildcardDelimiter).length) {
							 | 
						||
| 
								 | 
							
								      mergedRouteArray.splice(1, 0, rootPath)
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      mergedRouteArray.unshift(rootPath)
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // build tree
							 | 
						||
| 
								 | 
							
								  const routeTree = buildRouteTree(mergedRouteArray)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // draw tree
							 | 
						||
| 
								 | 
							
								  routeTree.forEach((rootBranch, idx) => {
							 | 
						||
| 
								 | 
							
								    tree += drawBranch(rootBranch, null, idx === routeTree.length - 1, false, true)
							 | 
						||
| 
								 | 
							
								    tree += '\n' // newline characters inserted at beginning of drawing function to allow for nested paths
							 | 
						||
| 
								 | 
							
								  })
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return tree
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function buildRouteTree (mergedRouteArray, rootPath) {
							 | 
						||
| 
								 | 
							
								  rootPath = rootPath || pathDelimiter
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const result = []
							 | 
						||
| 
								 | 
							
								  const temp = { result }
							 | 
						||
| 
								 | 
							
								  mergedRouteArray.forEach((route, idx) => {
							 | 
						||
| 
								 | 
							
								    let splitPath = route.path.split(pathRegExp)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // add preceding slash for proper nesting
							 | 
						||
| 
								 | 
							
								    if (splitPath[0] !== pathDelimiter) {
							 | 
						||
| 
								 | 
							
								      // handle wildcard route
							 | 
						||
| 
								 | 
							
								      if (splitPath[0] !== wildcardDelimiter) splitPath = [pathDelimiter, splitPath[0].slice(1), ...splitPath.slice(1)]
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // build tree
							 | 
						||
| 
								 | 
							
								    splitPath.reduce((acc, path, pidx) => {
							 | 
						||
| 
								 | 
							
								      if (!acc[path]) {
							 | 
						||
| 
								 | 
							
								        acc[path] = { result: [] }
							 | 
						||
| 
								 | 
							
								        const pathSeg = { path, children: acc[path].result }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if (pidx === splitPath.length - 1) pathSeg.handlers = route.handlers
							 | 
						||
| 
								 | 
							
								        acc.result.push(pathSeg)
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      return acc[path]
							 | 
						||
| 
								 | 
							
								    }, temp)
							 | 
						||
| 
								 | 
							
								  })
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // unfold root object from array
							 | 
						||
| 
								 | 
							
								  return result
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function drawBranch (pathSeg, prefix, endBranch, noPrefix, rootBranch) {
							 | 
						||
| 
								 | 
							
								  let branch = ''
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (!noPrefix && !rootBranch) branch += '\n'
							 | 
						||
| 
								 | 
							
								  if (!noPrefix) branch += `${prefix || ''}${endBranch ? endBranchIndent : midBranchIndent}`
							 | 
						||
| 
								 | 
							
								  branch += `${pathSeg.path}`
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (pathSeg.handlers) {
							 | 
						||
| 
								 | 
							
								    const flatHandlers = pathSeg.handlers.reduce((acc, curr) => {
							 | 
						||
| 
								 | 
							
								      const match = acc.findIndex(h => JSON.stringify(h.opts) === JSON.stringify(curr.opts))
							 | 
						||
| 
								 | 
							
								      if (match !== -1) {
							 | 
						||
| 
								 | 
							
								        acc[match].method = [acc[match].method, curr.method].join(', ')
							 | 
						||
| 
								 | 
							
								      } else {
							 | 
						||
| 
								 | 
							
								        acc.push(curr)
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      return acc
							 | 
						||
| 
								 | 
							
								    }, [])
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    flatHandlers.forEach((handler, idx) => {
							 | 
						||
| 
								 | 
							
								      if (idx > 0) branch += `${noPrefix ? '' : prefix || ''}${endBranch ? indent : branchIndent}${pathSeg.path}`
							 | 
						||
| 
								 | 
							
								      branch += ` (${handler.method || '-'})`
							 | 
						||
| 
								 | 
							
								      if (handler.opts && JSON.stringify(handler.opts) !== '{}') branch += ` ${JSON.stringify(handler.opts)}`
							 | 
						||
| 
								 | 
							
								      if (handler.meta) {
							 | 
						||
| 
								 | 
							
								        Reflect.ownKeys(handler.meta).forEach((m, hidx) => {
							 | 
						||
| 
								 | 
							
								          branch += `\n${noPrefix ? '' : prefix || ''}${endBranch ? indent : branchIndent}`
							 | 
						||
| 
								 | 
							
								          branch += `• (${m}) ${JSON.stringify(handler.meta[m])}`
							 | 
						||
| 
								 | 
							
								        })
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      if (flatHandlers.length > 1 && idx !== flatHandlers.length - 1) branch += '\n'
							 | 
						||
| 
								 | 
							
								    })
							 | 
						||
| 
								 | 
							
								  } else {
							 | 
						||
| 
								 | 
							
								    if (pathSeg.children.length > 1) branch += ' (-)'
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (!noPrefix) prefix = `${prefix || ''}${endBranch ? indent : branchIndent}`
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  pathSeg.children.forEach((child, idx) => {
							 | 
						||
| 
								 | 
							
								    const endBranch = idx === pathSeg.children.length - 1
							 | 
						||
| 
								 | 
							
								    const skipPrefix = (!pathSeg.handlers && pathSeg.children.length === 1)
							 | 
						||
| 
								 | 
							
								    branch += drawBranch(child, prefix, endBranch, skipPrefix)
							 | 
						||
| 
								 | 
							
								  })
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return branch
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function prettyPrintFlattenedNode (flattenedNode, prefix, tail, opts) {
							 | 
						||
| 
								 | 
							
								  if (!this.buildPrettyMeta) throw new Error('buildPrettyMeta not defined')
							 | 
						||
| 
								 | 
							
								  opts.includeMeta = opts.includeMeta || null // array of meta items to display
							 | 
						||
| 
								 | 
							
								  let paramName = ''
							 | 
						||
| 
								 | 
							
								  const printHandlers = []
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  for (const node of flattenedNode.nodes) {
							 | 
						||
| 
								 | 
							
								    for (const handler of node.handlers) {
							 | 
						||
| 
								 | 
							
								      printHandlers.push({ method: node.method, ...handler })
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (printHandlers.length) {
							 | 
						||
| 
								 | 
							
								    printHandlers.forEach((handler, index) => {
							 | 
						||
| 
								 | 
							
								      let suffix = `(${handler.method || '-'})`
							 | 
						||
| 
								 | 
							
								      if (Object.keys(handler.constraints).length > 0) {
							 | 
						||
| 
								 | 
							
								        suffix += ' ' + JSON.stringify(handler.constraints)
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      let name = ''
							 | 
						||
| 
								 | 
							
								      // find locations of parameters in prefix
							 | 
						||
| 
								 | 
							
								      const paramIndices = flattenedNode.prefix.split('').map((ch, idx) => ch === ':' ? idx : null).filter(idx => idx !== null)
							 | 
						||
| 
								 | 
							
								      if (paramIndices.length) {
							 | 
						||
| 
								 | 
							
								        let prevLoc = 0
							 | 
						||
| 
								 | 
							
								        paramIndices.forEach((loc, idx) => {
							 | 
						||
| 
								 | 
							
								          // find parameter in prefix
							 | 
						||
| 
								 | 
							
								          name += flattenedNode.prefix.slice(prevLoc, loc + 1)
							 | 
						||
| 
								 | 
							
								          // insert parameters
							 | 
						||
| 
								 | 
							
								          name += handler.params[handler.params.length - paramIndices.length + idx]
							 | 
						||
| 
								 | 
							
								          if (idx === paramIndices.length - 1) name += flattenedNode.prefix.slice(loc + 1)
							 | 
						||
| 
								 | 
							
								          prevLoc = loc + 1
							 | 
						||
| 
								 | 
							
								        })
							 | 
						||
| 
								 | 
							
								      } else {
							 | 
						||
| 
								 | 
							
								        // there are no parameters, return full object
							 | 
						||
| 
								 | 
							
								        name = flattenedNode.prefix
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      if (index === 0) {
							 | 
						||
| 
								 | 
							
								        paramName += `${name} ${suffix}`
							 | 
						||
| 
								 | 
							
								      } else {
							 | 
						||
| 
								 | 
							
								        paramName += `\n${prefix}${tail ? indent : branchIndent}${name} ${suffix}`
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      if (opts.includeMeta) {
							 | 
						||
| 
								 | 
							
								        const meta = buildMetaObject.call(this, handler, opts.includeMeta)
							 | 
						||
| 
								 | 
							
								        Object.keys(meta).forEach((m, hidx) => {
							 | 
						||
| 
								 | 
							
								          paramName += `\n${prefix || ''}${tail ? indent : branchIndent}`
							 | 
						||
| 
								 | 
							
								          paramName += `• (${m}) ${JSON.stringify(meta[m])}`
							 | 
						||
| 
								 | 
							
								        })
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    })
							 | 
						||
| 
								 | 
							
								  } else {
							 | 
						||
| 
								 | 
							
								    paramName = flattenedNode.prefix
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  let tree = `${prefix}${tail ? endBranchIndent : midBranchIndent}${paramName}\n`
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  prefix = `${prefix}${tail ? indent : branchIndent}`
							 | 
						||
| 
								 | 
							
								  const labels = Object.keys(flattenedNode.children)
							 | 
						||
| 
								 | 
							
								  for (let i = 0; i < labels.length; i++) {
							 | 
						||
| 
								 | 
							
								    const child = flattenedNode.children[labels[i]]
							 | 
						||
| 
								 | 
							
								    tree += prettyPrintFlattenedNode.call(this, child, prefix, i === (labels.length - 1), opts)
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  return tree
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function flattenNode (flattened, node) {
							 | 
						||
| 
								 | 
							
								  if (node.handlers.length > 0) {
							 | 
						||
| 
								 | 
							
								    flattened.nodes.push(node)
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (node.children) {
							 | 
						||
| 
								 | 
							
								    for (const child of Object.values(node.children)) {
							 | 
						||
| 
								 | 
							
								      // split on the slash separator but use a regex to lookahead and not actually match it, preserving it in the returned string segments
							 | 
						||
| 
								 | 
							
								      const childPrefixSegments = child.prefix.split(pathRegExp)
							 | 
						||
| 
								 | 
							
								      let cursor = flattened
							 | 
						||
| 
								 | 
							
								      let parent
							 | 
						||
| 
								 | 
							
								      for (const segment of childPrefixSegments) {
							 | 
						||
| 
								 | 
							
								        parent = cursor
							 | 
						||
| 
								 | 
							
								        cursor = cursor.children[segment]
							 | 
						||
| 
								 | 
							
								        if (!cursor) {
							 | 
						||
| 
								 | 
							
								          cursor = {
							 | 
						||
| 
								 | 
							
								            prefix: segment,
							 | 
						||
| 
								 | 
							
								            nodes: [],
							 | 
						||
| 
								 | 
							
								            children: {}
							 | 
						||
| 
								 | 
							
								          }
							 | 
						||
| 
								 | 
							
								          parent.children[segment] = cursor
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      flattenNode(cursor, child)
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function compressFlattenedNode (flattenedNode) {
							 | 
						||
| 
								 | 
							
								  const childKeys = Object.keys(flattenedNode.children)
							 | 
						||
| 
								 | 
							
								  if (flattenedNode.nodes.length === 0 && childKeys.length === 1) {
							 | 
						||
| 
								 | 
							
								    const child = flattenedNode.children[childKeys[0]]
							 | 
						||
| 
								 | 
							
								    if (child.nodes.length <= 1) {
							 | 
						||
| 
								 | 
							
								      compressFlattenedNode(child)
							 | 
						||
| 
								 | 
							
								      flattenedNode.nodes = child.nodes
							 | 
						||
| 
								 | 
							
								      flattenedNode.prefix += child.prefix
							 | 
						||
| 
								 | 
							
								      flattenedNode.children = child.children
							 | 
						||
| 
								 | 
							
								      return flattenedNode
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  for (const key of Object.keys(flattenedNode.children)) {
							 | 
						||
| 
								 | 
							
								    compressFlattenedNode(flattenedNode.children[key])
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return flattenedNode
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								module.exports = { flattenNode, compressFlattenedNode, prettyPrintFlattenedNode, prettyPrintRoutesArray }
							 |