-
-
Notifications
You must be signed in to change notification settings - Fork 263
Expand file tree
/
Copy pathexpand-matrix.ts
More file actions
108 lines (92 loc) · 3.46 KB
/
expand-matrix.ts
File metadata and controls
108 lines (92 loc) · 3.46 KB
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
import { quote } from 'shell-quote';
import { CommandInfo } from '../command';
import { CommandParser } from './command-parser';
/**
* Replace placeholders with new commands for each binding in the matrix expansion.
*/
export class ExpandMatrix implements CommandParser {
/**
* The matrix as defined by a mapping of dimension names to their possible values.
*/
private readonly matrix: Record<string, string[]>;
/**
* All combinations of the matrix dimensions.
*/
private readonly bindings: Record<string, string>[];
constructor(matrix: Record<string, string[]>) {
this.matrix = matrix;
this.bindings = Array.from(combinations(matrix));
}
parse(commandInfo: CommandInfo) {
return this.bindings.map((binding) => this.replacePlaceholders(commandInfo, binding));
}
private replacePlaceholders(
commandInfo: CommandInfo,
binding: Record<string, string>,
): CommandInfo {
const command = commandInfo.command.replace(
/\\?\{M:([^}]+)\}/g,
(match, placeholderTarget) => {
// Don't replace the placeholder if it is escaped by a backslash.
if (match.startsWith('\\')) {
return match.slice(1);
}
if (placeholderTarget && !(placeholderTarget in this.matrix)) {
throw new Error(
`[concurrently] Matrix placeholder '{M:${placeholderTarget}}' does not match any defined matrix.`,
);
}
// Replace dimension name with binding value
return quote([binding[placeholderTarget]]);
},
);
return { ...commandInfo, command };
}
}
/**
* Returns all possible combinations of the given dimensions.
*
* @param dimensions An object where keys are dimension names and values are arrays of possible values.
* eg `{os: ['windows', 'linux'], env: ['dev', 'staging']}`
*/
export function* combinations(
dimensions: Record<string, string[]>,
): Generator<Record<string, string>> {
const buildCurBinding = (): Record<string, string> => {
return Object.fromEntries(
Object.entries(dimensions).map(([dimName, dimValues], i) => [
dimName,
dimValues[curBindingIndices[i]],
]),
);
};
const totalDimensions = Object.keys(dimensions).length;
const curBindingIndices = Object.values(dimensions).map(() => 0);
const dimensionSizes = Object.values(dimensions).map((dimValues) => dimValues.length);
// If any dimension is empty, there are no combinations.
if (totalDimensions === 0 || dimensionSizes.some((size) => size === 0)) {
return;
}
let curDimension = 0;
while (curDimension >= 0) {
if (curDimension === totalDimensions - 1) {
yield buildCurBinding();
// Exhausted last dimension, backtrack
while (
curDimension >= 0 &&
curBindingIndices[curDimension] === dimensionSizes[curDimension] - 1
) {
curBindingIndices[curDimension] = 0;
curDimension--;
}
// All dimensions exhausted, done
if (curDimension < 0) {
break;
}
// Move to next value in current dimension
curBindingIndices[curDimension]++;
} else {
curDimension++;
}
}
}