This repository has been archived by the owner on Jul 21, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 10
/
make.js
184 lines (148 loc) · 5.23 KB
/
make.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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
'use strict';
// TypeScript definitions are generated here.
// AVA allows chaining of function names, like `test.after.cb.always`.
// The order of these names is not important.
// Writing these definitions by hand is hard. Because of chaining,
// the number of combinations grows fast (2^n). To reduce this number,
// illegal combinations are filtered out in `verify`.
// The order of the options is not important. We could generate full
// definitions for each possible order, but that would give a very big
// output. Instead, we write an alias for different orders. For instance,
// `after.cb` is fully written, and `cb.after` is emitted as an alias
// using `typeof after.cb`.
const path = require('path');
const fs = require('fs');
const isArraySorted = require('is-array-sorted');
const Runner = require('../lib/runner');
const arrayHas = parts => part => parts.indexOf(part) !== -1;
const base = fs.readFileSync(path.join(__dirname, 'base.d.ts'), 'utf8');
// All suported function names
const allParts = Object.keys(new Runner({}).chain).filter(name => name !== 'test');
// The output consists of the base declarations, the actual 'test' function declarations,
// and the namespaced chainable methods.
const output = base + generatePrefixed([]);
fs.writeFileSync(path.join(__dirname, 'generated.d.ts'), output);
// Generates type definitions, for the specified prefix
// The prefix is an array of function names
function generatePrefixed(prefix) {
let output = '';
let children = '';
for (const part of allParts) {
const parts = prefix.concat([part]);
if (prefix.indexOf(part) !== -1 || !verify(parts, true)) {
// Function already in prefix or not allowed here
continue;
}
// If `parts` is not sorted, we alias it to the sorted chain
if (!isArraySorted(parts)) {
if (exists(parts)) {
parts.sort();
let chain;
if (hasChildren(parts)) {
chain = parts.join('_') + '<T>';
} else {
// This is a single function, not a namespace, so there's no type associated
// and we need to dereference it as a property type
const last = parts.pop();
const joined = parts.join('_');
chain = `${joined}<T>['${last}']`;
}
output += `\t${part}: Register_${chain};\n`;
}
continue;
}
// Check that `part` is a valid function name.
// `always` is a valid prefix, for instance of `always.after`,
// but not a valid function name.
if (verify(parts, false)) {
if (arrayHas(parts)('todo')) {
// 'todo' functions don't have a function argument, just a string
output += `\t${part}: (name: string) => void;\n`;
} else {
if (arrayHas(parts)('cb')) {
output += `\t${part}: CallbackRegisterBase<T>`;
} else {
output += `\t${part}: RegisterBase<T>`;
}
if (hasChildren(parts)) {
// This chain can be continued, make the property an intersection type with the chain continuation
const joined = parts.join('_');
output += ` & Register_${joined}<T>`;
}
output += ';\n';
}
}
children += generatePrefixed(parts);
}
if (output === '') {
return children;
}
const typeBody = `{\n${output}}\n${children}`;
if (prefix.length === 0) {
// No prefix, so this is the type for the default export
return `export interface Register<T> extends RegisterBase<T> ${typeBody}`;
}
const namespace = ['Register'].concat(prefix).join('_');
return `interface ${namespace}<T> ${typeBody}`;
}
// Checks whether a chain is a valid function name (when `asPrefix === false`)
// or a valid prefix that could contain members.
// For instance, `test.always` is not a valid function name, but it is a valid
// prefix of `test.always.after`.
function verify(parts, asPrefix) {
const has = arrayHas(parts);
if (has('only') + has('skip') + has('todo') > 1) {
return false;
}
const beforeAfterCount = has('before') + has('beforeEach') + has('after') + has('afterEach');
if (beforeAfterCount > 1) {
return false;
}
if (beforeAfterCount === 1) {
if (has('only')) {
return false;
}
}
if (has('always')) {
// `always` can only be used with `after` or `afterEach`.
// Without it can still be a valid prefix
if (has('after') || has('afterEach')) {
return true;
}
if (!verify(parts.concat(['after']), false) && !verify(parts.concat(['afterEach']), false)) {
// If `after` nor `afterEach` cannot be added to this prefix,
// `always` is not allowed here.
return false;
}
// Only allowed as a prefix
return asPrefix;
}
return true;
}
// Returns true if a chain can have any child properties
function hasChildren(parts) {
// Concatenate the chain with each other part, and see if any concatenations are valid functions
const validChildren = allParts
.filter(newPart => parts.indexOf(newPart) === -1)
.map(newPart => parts.concat([newPart]))
.filter(longer => verify(longer, false));
return validChildren.length > 0;
}
// Checks whether a chain is a valid function name or a valid prefix with some member
function exists(parts) {
if (verify(parts, false)) {
// Valid function name
return true;
}
if (!verify(parts, true)) {
// Not valid prefix
return false;
}
// Valid prefix, check whether it has members
for (const prefix of allParts) {
if (parts.indexOf(prefix) === -1 && exists(parts.concat([prefix]))) {
return true;
}
}
return false;
}