forked from merceyz/typescript-to-proptypes
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgenerator.ts
241 lines (196 loc) · 6.24 KB
/
generator.ts
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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
import * as t from './types';
import _ from 'lodash';
export interface GenerateOptions {
/**
* Enable/disable the default sorting (ascending) or provide your own sort function
* @default true
*/
sortProptypes?: boolean | ((a: t.PropTypeNode, b: t.PropTypeNode) => 0 | -1 | 1);
/**
* The name used when importing prop-types
* @default 'PropTypes'
*/
importedName?: string;
/**
* Enable/disable including JSDoc comments
* @default true
*/
includeJSDoc?: boolean;
/**
* previous source code of the validator for each prop type
*/
previousPropTypesSource?: Map<string, string>;
/**
* Given the `prop`, the `previous` source of the validator and the `generated` source:
* What source should be injected? `previous` is `undefined` if the validator
* didn't exist before
* @default Uses `generated` source
*/
reconcilePropTypes?(
proptype: t.PropTypeNode,
previous: string | undefined,
generated: string
): string;
/**
* Control which PropTypes are included in the final result
* @param proptype The current PropType about to be converted to text
*/
shouldInclude?(proptype: t.PropTypeNode): boolean | undefined;
/**
* A comment that will be added to the start of the PropTypes code block
* @example
* foo.propTypes = {
* // Comment goes here
* }
*/
comment?: string;
}
/**
* Generates code from the given node
* @param node The node to convert to code
* @param options The options used to control the way the code gets generated
*/
export function generate(node: t.Node | t.PropTypeNode[], options: GenerateOptions = {}): string {
const {
sortProptypes = true,
importedName = 'PropTypes',
includeJSDoc = true,
previousPropTypesSource = new Map<string, string>(),
reconcilePropTypes = (_prop: t.PropTypeNode, _previous: string, generated: string) => generated,
shouldInclude,
} = options;
function jsDoc(node: t.PropTypeNode | t.LiteralNode) {
if (!includeJSDoc || !node.jsDoc) {
return '';
}
return `/**\n* ${node.jsDoc.split(/\r?\n/).reduce((prev, curr) => prev + '\n* ' + curr)}\n*/\n`;
}
if (Array.isArray(node)) {
let propTypes = node;
if (typeof sortProptypes === 'function') {
propTypes = propTypes.sort(sortProptypes);
} else if (sortProptypes === true) {
propTypes = propTypes.sort((a, b) => a.name.localeCompare(b.name));
}
let filteredNodes = node;
if (shouldInclude) {
filteredNodes = filteredNodes.filter((x) => shouldInclude(x));
}
if (filteredNodes.length === 0) {
return '';
}
return filteredNodes
.map((prop) => generate(prop, options))
.reduce((prev, curr) => `${prev}\n${curr}`);
}
if (t.isProgramNode(node)) {
return node.body
.map((prop) => generate(prop, options))
.reduce((prev, curr) => `${prev}\n${curr}`);
}
if (t.isComponentNode(node)) {
const generated = generate(node.types, options);
if (generated.length === 0) {
return '';
}
const comment =
options.comment &&
`// ${options.comment.split(/\r?\n/gm).reduce((prev, curr) => `${prev}\n// ${curr}`)}\n`;
return `${node.name}.propTypes = {\n${comment ? comment : ''}${generated}\n}`;
}
if (t.isPropTypeNode(node)) {
let isOptional = false;
let propType = { ...node.propType };
if (t.isUnionNode(propType) && propType.types.some(t.isUndefinedNode)) {
isOptional = true;
propType.types = propType.types.filter(
(prop) => !t.isUndefinedNode(prop) && !(t.isLiteralNode(prop) && prop.value === 'null')
);
if (propType.types.length === 1 && t.isLiteralNode(propType.types[0]) === false) {
propType = propType.types[0];
}
}
const validatorSource = reconcilePropTypes(
node,
previousPropTypesSource.get(node.name),
`${generate(propType, options)}${isOptional ? '' : '.isRequired'},`
);
return `${jsDoc(node)}"${node.name}": ${validatorSource}`;
}
if (t.isInterfaceNode(node)) {
return `${importedName}.shape({\n${generate(node.types, {
...options,
shouldInclude: undefined,
})}\n})`;
}
if (t.isFunctionNode(node)) {
return `${importedName}.func`;
}
if (t.isStringNode(node)) {
return `${importedName}.string`;
}
if (t.isBooleanNode(node)) {
return `${importedName}.bool`;
}
if (t.isNumericNode(node)) {
return `${importedName}.number`;
}
if (t.isLiteralNode(node)) {
return `${importedName}.oneOf([${jsDoc(node)}${node.value}])`;
}
if (t.isObjectNode(node)) {
return `${importedName}.object`;
}
if (t.isAnyNode(node)) {
return `${importedName}.any`;
}
if (t.isElementNode(node)) {
return `${importedName}.${node.elementType}`;
}
if (t.isInstanceOfNode(node)) {
return `${importedName}.instanceOf(${node.instance})`;
}
if (t.isArrayNode(node)) {
if (t.isAnyNode(node.arrayType)) {
return `${importedName}.array`;
}
return `${importedName}.arrayOf(${generate(node.arrayType, options)})`;
}
if (t.isUnionNode(node)) {
let [literals, rest] = _.partition(node.types, t.isLiteralNode);
literals = _.uniqBy(literals, (x) => x.value);
rest = _.uniqBy(rest, (x) => (t.isInstanceOfNode(x) ? `${x.type}.${x.instance}` : x.type));
literals = literals.sort((a, b) => a.value.localeCompare(b.value));
const nodeToStringName = (obj: t.Node): string => {
if (t.isInstanceOfNode(obj)) {
return `${obj.type}.${obj.instance}`;
} else if (t.isInterfaceNode(obj)) {
// An interface is PropTypes.shape
// Use `ShapeNode` to get it sorted in the correct order
return `ShapeNode`;
}
return obj.type;
};
rest = rest.sort((a, b) => nodeToStringName(a).localeCompare(nodeToStringName(b)));
if (literals.find((x) => x.value === 'true') && literals.find((x) => x.value === 'false')) {
rest.push(t.booleanNode());
literals = literals.filter((x) => x.value !== 'true' && x.value !== 'false');
}
const literalProps =
literals.length !== 0
? `${importedName}.oneOf([${literals
.map((x) => `${jsDoc(x)}${x.value}`)
.reduce((prev, curr) => `${prev},${curr}`)}])`
: '';
if (rest.length === 0) {
return literalProps;
}
if (literals.length === 0 && rest.length === 1) {
return generate(rest[0], options);
}
return `${importedName}.oneOfType([${literalProps ? literalProps + ', ' : ''}${rest
.map((x) => generate(x, options))
.reduce((prev, curr) => `${prev},${curr}`)}])`;
}
throw new Error(`Nothing to handle node of type "${node.type}"`);
}