match-tasks.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. /**
  2. * @module match-tasks
  3. * @author Toru Nagashima
  4. * @copyright 2015 Toru Nagashima. All rights reserved.
  5. * See LICENSE file in root directory for full license.
  6. */
  7. 'use strict'
  8. // ------------------------------------------------------------------------------
  9. // Requirements
  10. // ------------------------------------------------------------------------------
  11. const { minimatch } = require('minimatch')
  12. const Minimatch = minimatch.Minimatch
  13. // ------------------------------------------------------------------------------
  14. // Helpers
  15. // ------------------------------------------------------------------------------
  16. const COLON_OR_SLASH = /[:/]/g
  17. const CONVERT_MAP = { ':': '/', '/': ':' }
  18. /**
  19. * Swaps ":" and "/", in order to use ":" as the separator in minimatch.
  20. *
  21. * @param {string} s - A text to swap.
  22. * @returns {string} The text which was swapped.
  23. */
  24. function swapColonAndSlash (s) {
  25. return s.replace(COLON_OR_SLASH, (matched) => CONVERT_MAP[matched])
  26. }
  27. /**
  28. * Creates a filter from user-specified pattern text.
  29. *
  30. * The task name is the part until the first space.
  31. * The rest part is the arguments for this task.
  32. *
  33. * @param {string} pattern - A pattern to create filter.
  34. * @returns {{match: function, task: string, args: string}} The filter object of the pattern.
  35. */
  36. function createFilter (pattern) {
  37. const trimmed = pattern.trim()
  38. const spacePos = trimmed.indexOf(' ')
  39. const task = spacePos < 0 ? trimmed : trimmed.slice(0, spacePos)
  40. const args = spacePos < 0 ? '' : trimmed.slice(spacePos)
  41. const matcher = new Minimatch(swapColonAndSlash(task), { nonegate: true })
  42. const match = matcher.match.bind(matcher)
  43. return { match, task, args }
  44. }
  45. /**
  46. * The set to remove overlapped task.
  47. */
  48. class TaskSet {
  49. /**
  50. * Creates a instance.
  51. */
  52. constructor () {
  53. this.result = []
  54. this.sourceMap = Object.create(null)
  55. }
  56. /**
  57. * Adds a command (a pattern) into this set if it's not overlapped.
  58. * "Overlapped" is meaning that the command was added from a different source.
  59. *
  60. * @param {string} command - A pattern text to add.
  61. * @param {string} source - A task name to check.
  62. * @returns {void}
  63. */
  64. add (command, source) {
  65. const sourceList = this.sourceMap[command] || (this.sourceMap[command] = [])
  66. if (sourceList.length === 0 || sourceList.indexOf(source) !== -1) {
  67. this.result.push(command)
  68. }
  69. sourceList.push(source)
  70. }
  71. }
  72. // ------------------------------------------------------------------------------
  73. // Public Interface
  74. // ------------------------------------------------------------------------------
  75. /**
  76. * Enumerates tasks which matches with given patterns.
  77. *
  78. * @param {string[]} taskList - A list of actual task names.
  79. * @param {string[]} patterns - Pattern texts to match.
  80. * @returns {string[]} Tasks which matches with the patterns.
  81. * @private
  82. */
  83. module.exports = function matchTasks (taskList, patterns) {
  84. const filters = patterns.map(createFilter)
  85. const candidates = taskList.map(swapColonAndSlash)
  86. const taskSet = new TaskSet()
  87. const unknownSet = Object.create(null)
  88. // Take tasks while keep the order of patterns.
  89. for (const filter of filters) {
  90. let found = false
  91. for (const candidate of candidates) {
  92. if (filter.match(candidate)) {
  93. found = true
  94. taskSet.add(
  95. swapColonAndSlash(candidate) + filter.args,
  96. filter.task
  97. )
  98. }
  99. }
  100. // Built-in tasks should be allowed.
  101. if (!found && (filter.task === 'restart' || filter.task === 'env')) {
  102. taskSet.add(filter.task + filter.args, filter.task)
  103. found = true
  104. }
  105. if (!found) {
  106. unknownSet[filter.task] = true
  107. }
  108. }
  109. const unknownTasks = Object.keys(unknownSet)
  110. if (unknownTasks.length > 0) {
  111. throw new Error(`Task not found: "${unknownTasks.join('", ')}"`)
  112. }
  113. return taskSet.result
  114. }