/
create-resolver.js
167 lines (151 loc) · 7.26 KB
/
create-resolver.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
const path = require('path');
const babylon = require('react-docgen/dist/babylon').default;
const resolve = require('resolve').sync;
const isExportsOrModuleAssignment = require('react-docgen/dist/utils/isExportsOrModuleAssignment').default;
const isReactComponentClass = require('react-docgen/dist/utils/isReactComponentClass').default;
const isReactCreateClassCall = require('react-docgen/dist/utils/isReactCreateClassCall').default;
const isStatelessComponent = require('react-docgen/dist/utils/isStatelessComponent').default;
const normalizeClassDefinition = require('react-docgen/dist/utils/normalizeClassDefinition').default;
const getMemberValuePath = require('react-docgen/dist/utils/getMemberValuePath').default;
const resolveToValue = require('react-docgen/dist/utils/resolveToValue').default;
const resolveHOC = require('react-docgen/dist/utils/resolveHOC').default;
const resolveToModule = require('react-docgen/dist/utils/resolveToModule').default;
const getSourceFileContent = require('./get-source-file-content');
const resolveExportDeclaration = require('./docgen/resolve-export-declaration');
const isDecoratedBy = require('./docgen/is-decorated-by');
const ERROR_MULTIPLE_DEFINITIONS = 'Multiple exported component definitions found.';
function isComponentDefinition(path) {
return isReactCreateClassCall(path) || isReactComponentClass(path) || isStatelessComponent(path);
}
function resolveDefinition(definition, types) {
if (isReactCreateClassCall(definition)) {
// return argument
const resolvedPath = resolveToValue(definition.get('arguments', 0));
if (types.ObjectExpression.check(resolvedPath.node)) {
return resolvedPath;
}
} else if (isReactComponentClass(definition) || isDecoratedBy(definition, 'cn')) {
normalizeClassDefinition(definition);
return definition;
} else if (isStatelessComponent(definition)) {
return definition;
}
return null;
}
function findExportedComponentDefinition(ast, recast, filePath) {
const types = recast.types.namedTypes;
const importedModules = {};
let originalClassName;
let definition;
function exportDeclaration(nodePath) {
let linkedFile;
const definitions = resolveExportDeclaration(nodePath, types)
.reduce((acc, def) => {
if (isComponentDefinition(def)) {
acc.push(def);
return acc;
}
const resolved = resolveToValue(resolveHOC(def));
if (isComponentDefinition(resolved)) {
acc.push(resolved);
return acc;
}
if (isDecoratedBy(def, 'cn') && def.get('superClass')) {
const superClass = def.get('superClass');
if (!originalClassName) { // save original component name and use it to patch parent class
originalClassName = def.get('id').value.name;
}
const src = getSourceFileContent(importedModules[superClass.value.name], filePath);
filePath = src.filePath; // update file path, so we can correctly resolve imports
linkedFile = recast.parse(src.content, { esprima: babylon });
return acc;
}
if (def.get('value') && def.get('value').value) {
// if we found reexported file - parse it with recast and return
const src = getSourceFileContent(def.get('value').value, filePath);
filePath = src.filePath; // update file path, so we can correctly resolve imports
linkedFile = recast.parse(src.content, { esprima: babylon });
}
return acc;
}, []);
if (linkedFile) {
return linkedFile;
}
if (definitions.length === 0) {
return false;
}
if (definitions.length > 1 || definition) {
// If a file exports multiple components, ... complain!
throw new Error(ERROR_MULTIPLE_DEFINITIONS);
}
definition = resolveDefinition(definitions[0], types);
return false;
}
recast.visit(ast, {
visitExportDeclaration: exportDeclaration,
visitExportNamedDeclaration: exportDeclaration,
visitExportDefaultDeclaration: exportDeclaration,
visitClassDeclaration(node) {
if (originalClassName) {
// we inside super class, so we create `displayName` static property that handlers can read
// and override original class display name
let originalPath = getMemberValuePath(node, 'displayName');
if (originalPath) { // replace existing displayName
originalPath.value = recast.types.builders.literal(originalClassName);
} else { // create new property
const propertyDefinition = recast.types.builders.classProperty(
recast.types.builders.identifier('displayName'),
recast.types.builders.literal(originalClassName),
null,
true
);
node.get('body').value.body.push(propertyDefinition);
}
}
return false;
},
visitImportDeclaration(node) {
const specifiers = node.value.specifiers;
const moduleName = resolveToModule(node);
if (moduleName !== 'react' && moduleName !== 'prop-types') {
// resolve path to file here, because this is the only place where we've got actual source path
// but skip `react` and `prop-types` modules, because docgen will not be able to detect types otherwise
node.value.source.value = resolve(
node.value.source.value,
{ basedir: path.dirname(filePath), extensions: ['.js', '.jsx', '.ts', '.tsx'] }
);
}
if (specifiers && specifiers.length > 0) {
importedModules[specifiers[0].local.name] = node.value.source.value;
}
return false;
},
visitAssignmentExpression(path) {
// Ignore anything that is not `exports.X = ...;` or
// `module.exports = ...;`
if (!isExportsOrModuleAssignment(path)) {
return false;
}
// Resolve the value of the right hand side. It should resolve to a call
// expression, something like React.createClass
path = resolveToValue(path.get('right'));
if (!isComponentDefinition(path)) {
path = resolveToValue(resolveHOC(path));
if (!isComponentDefinition(path)) {
return false;
}
}
if (definition) {
// If a file exports multiple components, ... complain!
throw new Error(ERROR_MULTIPLE_DEFINITIONS);
}
definition = resolveDefinition(path, types);
return false;
}
});
return definition;
}
function createResolver(filePath) {
return (ast, recast) => findExportedComponentDefinition(ast, recast, filePath);
}
module.exports = createResolver;