/
getProps.ts
185 lines (153 loc) · 5.81 KB
/
getProps.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
import path from 'path';
import fs from 'fs';
import { TagProps, TagParamObject, DocumentationObject, utils, TagObject } from 'react-docgen';
import _ from 'lodash';
import doctrine, { Annotation } from 'doctrine';
import createLogger from 'glogg';
import highlightCodeInMarkdown from './highlightCodeInMarkdown';
import removeDoclets from './removeDoclets';
import requireIt from './requireIt';
import getNameFromFilePath from './getNameFromFilePath';
import * as Rsg from '../../typings';
const logger = createLogger('rsg');
const examplesLoader = path.resolve(__dirname, '../examples-loader.js');
const JS_DOC_METHOD_PARAM_TAG_SYNONYMS: (keyof TagProps)[] = ['param', 'arg', 'argument'];
const JS_DOC_METHOD_RETURN_TAG_SYNONYMS: (keyof TagProps)[] = ['return', 'returns'];
const JS_DOC_ALL_SYNONYMS: (keyof TagProps)[] = [
...JS_DOC_METHOD_PARAM_TAG_SYNONYMS,
...JS_DOC_METHOD_RETURN_TAG_SYNONYMS,
];
// HACK: We have to make sure that doclets is a proper object with correct prototype to
// work around an issue in react-docgen that breaks the build if a component has JSDoc tags
// like @see in its description, see https://github.com/reactjs/react-docgen/issues/155
// and https://github.com/styleguidist/react-styleguidist/issues/298
const getDocletsObject = (str?: string) => ({ ...utils.docblock.getDoclets(str) });
const getDoctrineTags = (documentation: Annotation) => {
return _.groupBy(documentation.tags, 'title');
};
const doesExternalExampleFileExist = (componentPath: string, exampleFile: string) => {
const exampleFilepath = path.resolve(path.dirname(componentPath), exampleFile);
const doesFileExist = fs.existsSync(exampleFilepath);
if (!doesFileExist) {
logger.warn(`An example file ${exampleFile} defined in ${componentPath} component not found.`);
}
return doesFileExist;
};
const getMergedTag = (tags: TagProps, names: (keyof TagProps)[]): TagObject[] => {
return names.reduce((params: TagObject[], name) => [...params, ...(tags[name] || [])], []);
};
/**
* 1. Remove non-public methods.
* 2. Extract doclets.
* 3. Highlight code in descriptions.
* 4. Extract @example doclet (load linked file with examples-loader).
*
* @param {object} doc
* @param {string} filepath
* @returns {object}
*/
export default function getProps(doc: DocumentationObject, filepath?: string): Rsg.TempPropsObject {
const outDocs: Rsg.TempPropsObject = { doclets: {}, displayName: '', ...doc, methods: undefined };
// Keep only public methods
outDocs.methods = (doc.methods || []).filter(method => {
const doclets = method.docblock && utils.docblock.getDoclets(method.docblock);
return doclets && doclets.public;
}) as Rsg.MethodWithDocblock[];
// Parse the docblock of the remaining methods with doctrine to retrieve
// the JSDoc tags
// if a method is visible it must have a docblock
outDocs.methods = outDocs.methods.map(method => {
const allTags = getDoctrineTags(
doctrine.parse(method.docblock, { sloppy: true, unwrap: true })
);
// Merge with react-docgen information about arguments and return value
// with information from JSDoc
const paramTags = getMergedTag(
allTags as TagProps,
JS_DOC_METHOD_PARAM_TAG_SYNONYMS
) as TagParamObject[];
const params =
method.params &&
method.params.map(param => ({
...param,
...paramTags.find(tagParam => tagParam.name === param.name),
}));
if (params) {
method.params = params;
}
const returnTags = getMergedTag(
allTags as TagProps,
JS_DOC_METHOD_RETURN_TAG_SYNONYMS
) as TagParamObject[];
const returns = method.returns
? {
...method.returns,
type: {
type: 'NameExpression',
...method.returns.type,
},
}
: returnTags[0];
if (returns) {
method.returns = returns;
}
// Remove tag synonyms
method.tags = _.omit(allTags, JS_DOC_ALL_SYNONYMS);
return method;
});
if (doc.description) {
// Read doclets from the description and remove them
outDocs.doclets = getDocletsObject(doc.description);
const documentation = doctrine.parse(doc.description);
outDocs.tags = getDoctrineTags(documentation) as TagProps;
outDocs.description = highlightCodeInMarkdown(removeDoclets(doc.description));
let exampleFileExists = false;
let exampleFile = outDocs.doclets.example;
// doc.doclets.example might be a boolean or undefined
if (typeof outDocs.doclets.example === 'string' && filepath) {
exampleFile = outDocs.doclets.example.trim();
exampleFileExists = doesExternalExampleFileExist(filepath, exampleFile);
}
if (exampleFileExists) {
outDocs.example = requireIt(`!!${examplesLoader}!${exampleFile}`);
delete outDocs.doclets.example;
}
} else {
outDocs.doclets = {};
}
if (doc.props) {
// Read doclets of props
Object.keys(doc.props).forEach(propName => {
if (!doc.props) {
return;
}
const prop = doc.props[propName];
const doclets = getDocletsObject(prop.description);
// When a prop is listed in defaultProps but not in props the prop.description is undefined
const documentation = doctrine.parse(prop.description || '');
// documentation.description is the description without tags
prop.description = documentation.description;
prop.tags = getDoctrineTags(documentation) as TagProps;
// Remove ignored props
if (doclets && doclets.ignore && outDocs.props) {
delete outDocs.props[propName];
} else if (outDocs.props) {
outDocs.props[propName] = prop;
}
});
}
if (!doc.displayName && filepath) {
// Guess the exported component's display name based on the file path
outDocs.displayName = getNameFromFilePath(filepath);
}
if (outDocs.doclets && outDocs.doclets.visibleName) {
outDocs.visibleName = outDocs.doclets.visibleName;
// Custom tag is added both to doclets and tags
// Removing from both locations
delete outDocs.doclets.visibleName;
if (outDocs.tags) {
delete outDocs.tags.visibleName;
}
}
return outDocs;
}