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