UNPKG

5.36 kBJavaScriptView Raw
1'use strict';
2
3// TypeScript definitions are generated here.
4// AVA allows chaining of function names, like `test.after.cb.always`.
5// The order of these names is not important.
6// Writing these definitions by hand is hard. Because of chaining,
7// the number of combinations grows fast (2^n). To reduce this number,
8// illegal combinations are filtered out in `verify`.
9// The order of the options is not important. We could generate full
10// definitions for each possible order, but that would give a very big
11// output. Instead, we write an alias for different orders. For instance,
12// `after.cb` is fully written, and `cb.after` is emitted as an alias
13// using `typeof after.cb`.
14
15const path = require('path');
16const fs = require('fs');
17const isArraySorted = require('is-array-sorted');
18const Runner = require('../lib/runner');
19
20const arrayHas = parts => part => parts.indexOf(part) !== -1;
21
22const base = fs.readFileSync(path.join(__dirname, 'base.d.ts'), 'utf8');
23
24// All suported function names
25const allParts = Object.keys(new Runner({}).chain).filter(name => name !== 'test');
26
27// The output consists of the base declarations, the actual 'test' function declarations,
28// and the namespaced chainable methods.
29const output = base + generatePrefixed([]);
30
31fs.writeFileSync(path.join(__dirname, 'generated.d.ts'), output);
32
33// Generates type definitions, for the specified prefix
34// The prefix is an array of function names
35function generatePrefixed(prefix) {
36 let output = '';
37 let children = '';
38
39 for (const part of allParts) {
40 const parts = prefix.concat([part]);
41
42 if (prefix.indexOf(part) !== -1 || !verify(parts, true)) {
43 // Function already in prefix or not allowed here
44 continue;
45 }
46
47 // If `parts` is not sorted, we alias it to the sorted chain
48 if (!isArraySorted(parts)) {
49 if (exists(parts)) {
50 parts.sort();
51
52 let chain;
53 if (hasChildren(parts)) {
54 chain = parts.join('_') + '<T>';
55 } else {
56 // This is a single function, not a namespace, so there's no type associated
57 // and we need to dereference it as a property type
58 const last = parts.pop();
59 const joined = parts.join('_');
60 chain = `${joined}<T>['${last}']`;
61 }
62
63 output += `\t${part}: Register_${chain};\n`;
64 }
65
66 continue;
67 }
68
69 // Check that `part` is a valid function name.
70 // `always` is a valid prefix, for instance of `always.after`,
71 // but not a valid function name.
72 if (verify(parts, false)) {
73 if (arrayHas(parts)('todo')) {
74 // 'todo' functions don't have a function argument, just a string
75 output += `\t${part}: (name: string) => void;\n`;
76 } else {
77 if (arrayHas(parts)('cb')) {
78 output += `\t${part}: CallbackRegisterBase<T>`;
79 } else {
80 output += `\t${part}: RegisterBase<T>`;
81 }
82
83 if (hasChildren(parts)) {
84 // This chain can be continued, make the property an intersection type with the chain continuation
85 const joined = parts.join('_');
86 output += ` & Register_${joined}<T>`;
87 }
88
89 output += ';\n';
90 }
91 }
92
93 children += generatePrefixed(parts);
94 }
95
96 if (output === '') {
97 return children;
98 }
99
100 const typeBody = `{\n${output}}\n${children}`;
101
102 if (prefix.length === 0) {
103 // No prefix, so this is the type for the default export
104 return `export interface Register<T> extends RegisterBase<T> ${typeBody}`;
105 }
106 const namespace = ['Register'].concat(prefix).join('_');
107 return `interface ${namespace}<T> ${typeBody}`;
108}
109
110// Checks whether a chain is a valid function name (when `asPrefix === false`)
111// or a valid prefix that could contain members.
112// For instance, `test.always` is not a valid function name, but it is a valid
113// prefix of `test.always.after`.
114function verify(parts, asPrefix) {
115 const has = arrayHas(parts);
116
117 if (has('only') + has('skip') + has('todo') > 1) {
118 return false;
119 }
120
121 const beforeAfterCount = has('before') + has('beforeEach') + has('after') + has('afterEach');
122
123 if (beforeAfterCount > 1) {
124 return false;
125 }
126
127 if (beforeAfterCount === 1) {
128 if (has('only')) {
129 return false;
130 }
131 }
132
133 if (has('always')) {
134 // `always` can only be used with `after` or `afterEach`.
135 // Without it can still be a valid prefix
136 if (has('after') || has('afterEach')) {
137 return true;
138 }
139
140 if (!verify(parts.concat(['after']), false) && !verify(parts.concat(['afterEach']), false)) {
141 // If `after` nor `afterEach` cannot be added to this prefix,
142 // `always` is not allowed here.
143 return false;
144 }
145
146 // Only allowed as a prefix
147 return asPrefix;
148 }
149
150 return true;
151}
152
153// Returns true if a chain can have any child properties
154function hasChildren(parts) {
155 // Concatenate the chain with each other part, and see if any concatenations are valid functions
156 const validChildren = allParts
157 .filter(newPart => parts.indexOf(newPart) === -1)
158 .map(newPart => parts.concat([newPart]))
159 .filter(longer => verify(longer, false));
160
161 return validChildren.length > 0;
162}
163
164// Checks whether a chain is a valid function name or a valid prefix with some member
165function exists(parts) {
166 if (verify(parts, false)) {
167 // Valid function name
168 return true;
169 }
170
171 if (!verify(parts, true)) {
172 // Not valid prefix
173 return false;
174 }
175
176 // Valid prefix, check whether it has members
177 for (const prefix of allParts) {
178 if (parts.indexOf(prefix) === -1 && exists(parts.concat([prefix]))) {
179 return true;
180 }
181 }
182
183 return false;
184}