Skip to content

Commit eff64d1

Browse files
committed
Improve subcomponent exports
Fixes #324
1 parent 6803c92 commit eff64d1

File tree

5 files changed

+194
-7
lines changed

5 files changed

+194
-7
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as React from 'react';
2+
3+
interface LabelProps {
4+
/** title description */
5+
title: string;
6+
}
7+
8+
/** StatelessStaticComponents.Label description */
9+
const SubComponent = (props: LabelProps) => (
10+
<div>My Property = {props.title}</div>
11+
);
12+
13+
interface StatelessStaticComponentsProps {
14+
/** myProp description */
15+
myProp: string;
16+
}
17+
18+
/** StatelessStaticComponents description */
19+
const StatelessStaticComponents = (props: StatelessStaticComponentsProps) => (
20+
<div>My Property = {props.myProp}</div>
21+
);
22+
23+
StatelessStaticComponents.Label = SubComponent;
24+
25+
export { StatelessStaticComponents };
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as React from 'react';
2+
3+
interface LabelProps {
4+
/** title description */
5+
title: string;
6+
}
7+
8+
/** StatelessStaticComponents.Label description */
9+
const SubComponent = (props: LabelProps) => (
10+
<div>My Property = {props.title}</div>
11+
);
12+
13+
interface StatelessStaticComponentsProps {
14+
/** myProp description */
15+
myProp: string;
16+
}
17+
18+
/** StatelessStaticComponents description */
19+
function StatelessStaticComponents(props: StatelessStaticComponentsProps) {
20+
return <div>My Property = {props.myProp}</div>;
21+
}
22+
23+
StatelessStaticComponents.Label = SubComponent;
24+
25+
export { StatelessStaticComponents };
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import * as React from 'react';
2+
3+
interface LabelProps {
4+
/** title description */
5+
title: string;
6+
}
7+
8+
/** StatelessStaticComponents.Label description */
9+
const SubComponent = (props: LabelProps) => (
10+
<div>My Property = {props.title}</div>
11+
);
12+
13+
interface StatelessStaticComponentsProps {
14+
/** myProp description */
15+
myProp: string;
16+
}
17+
18+
/** StatelessStaticComponents description */
19+
const StatelessStaticComponents = (props: StatelessStaticComponentsProps) => (
20+
<div>My Property = {props.myProp}</div>
21+
);
22+
23+
export const Components = {
24+
StatelessStaticComponents,
25+
SubComponent
26+
};

src/__tests__/parser.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,44 @@ describe('parser', () => {
240240
});
241241
});
242242

243+
it('should parse static exported components variation1', () => {
244+
check('StatelessStaticComponentsExportVariation1', {
245+
StatelessStaticComponents: {
246+
myProp: { type: 'string' }
247+
},
248+
'StatelessStaticComponents.Label': {
249+
title: { type: 'string' }
250+
}
251+
});
252+
});
253+
254+
it('should parse static exported components variation2', () => {
255+
check('StatelessStaticComponentsExportVariation2', {
256+
StatelessStaticComponents: {
257+
myProp: { type: 'string' }
258+
},
259+
'StatelessStaticComponents.Label': {
260+
title: { type: 'string' }
261+
}
262+
});
263+
});
264+
265+
it('should parse static sub components exported from named object', () => {
266+
check(
267+
'StatelessStaticComponentsNamedObjectExport',
268+
{
269+
StatelessStaticComponents: {
270+
myProp: { type: 'string' }
271+
},
272+
SubComponent: {
273+
title: { type: 'string' }
274+
}
275+
},
276+
true,
277+
''
278+
);
279+
});
280+
243281
it('should parse static sub components on class components', () => {
244282
check('ColumnWithStaticComponents', {
245283
Column: {

src/parser.ts

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,61 @@ export class Parser {
251251
this.shouldIncludeExpression = Boolean(shouldIncludeExpression);
252252
}
253253

254+
public getTypeSymbol(exp: ts.Symbol) {
255+
const declaration = exp.valueDeclaration || exp.declarations![0];
256+
const type = this.checker.getTypeOfSymbolAtLocation(exp, declaration);
257+
const typeSymbol = type.symbol || type.aliasSymbol;
258+
return typeSymbol;
259+
}
260+
261+
public isPlainObjectType(exp: ts.Symbol) {
262+
let targetSymbol = exp;
263+
if (exp.flags & ts.SymbolFlags.Alias) {
264+
targetSymbol = this.checker.getAliasedSymbol(exp);
265+
}
266+
const declaration =
267+
targetSymbol.valueDeclaration || targetSymbol.declarations![0];
268+
269+
if (ts.isClassDeclaration(declaration)) {
270+
return false;
271+
}
272+
273+
const type = this.checker.getTypeOfSymbolAtLocation(
274+
targetSymbol,
275+
declaration
276+
);
277+
// Confirm it's an object type
278+
if (!(type.flags & ts.TypeFlags.Object)) {
279+
return false;
280+
}
281+
const objectType = type as ts.ObjectType;
282+
const isPlain = !!(
283+
objectType.objectFlags &
284+
(ts.ObjectFlags.Anonymous | ts.ObjectFlags.ObjectLiteral)
285+
);
286+
return isPlain;
287+
}
288+
289+
/**
290+
* Attempts to gather a symbol's exports.
291+
* Some symbol's like `default` exports are aliased, so we need to get the real symbol.
292+
* @param exp symbol
293+
*/
294+
public getComponentExports(exp: ts.Symbol) {
295+
let targetSymbol = exp;
296+
297+
if (targetSymbol.exports) {
298+
return { symbol: targetSymbol, exports: targetSymbol.exports! };
299+
}
300+
301+
if (exp.flags & ts.SymbolFlags.Alias) {
302+
targetSymbol = this.checker.getAliasedSymbol(exp);
303+
}
304+
if (targetSymbol.exports) {
305+
return { symbol: targetSymbol, exports: targetSymbol.exports };
306+
}
307+
}
308+
254309
private getComponentFromExpression(exp: ts.Symbol) {
255310
const declaration = exp.valueDeclaration || exp.declarations![0];
256311
const type = this.checker.getTypeOfSymbolAtLocation(exp, declaration);
@@ -261,7 +316,6 @@ export class Parser {
261316
}
262317

263318
const symbolName = typeSymbol.getName();
264-
265319
if (
266320
(symbolName === 'MemoExoticComponent' ||
267321
symbolName === 'ForwardRefExoticComponent') &&
@@ -1238,7 +1292,6 @@ function computeComponentName(
12381292
customComponentTypes: ParserOptions['customComponentTypes'] = []
12391293
) {
12401294
const exportName = exp.getName();
1241-
12421295
const statelessDisplayName = getTextValueOfFunctionProperty(
12431296
exp,
12441297
source,
@@ -1395,11 +1448,28 @@ function parseWithProgramProvider(
13951448
return docs;
13961449
}
13971450

1398-
const components = checker.getExportsOfModule(moduleSymbol);
1451+
const exports = checker.getExportsOfModule(moduleSymbol);
13991452
const componentDocs: ComponentDoc[] = [];
1453+
const exportsAndMembers: ts.Symbol[] = [];
1454+
1455+
// Examine each export to determine if it's on object which may contain components
1456+
exports.forEach(exp => {
1457+
// Push symbol for extraction to maintain existing behavior
1458+
exportsAndMembers.push(exp);
1459+
// Determine if the export symbol is an object
1460+
if (!parser.isPlainObjectType(exp)) {
1461+
return;
1462+
}
1463+
const typeSymbol = parser.getTypeSymbol(exp);
1464+
if (typeSymbol?.members) {
1465+
typeSymbol.members.forEach(member => {
1466+
exportsAndMembers.push(member);
1467+
});
1468+
}
1469+
});
14001470

14011471
// First document all components
1402-
components.forEach(exp => {
1472+
exportsAndMembers.forEach(exp => {
14031473
const doc = parser.getComponentInfo(
14041474
exp,
14051475
sourceFile,
@@ -1411,12 +1481,13 @@ function parseWithProgramProvider(
14111481
componentDocs.push(doc);
14121482
}
14131483

1414-
if (!exp.exports) {
1484+
const componentExports = parser.getComponentExports(exp);
1485+
if (!componentExports) {
14151486
return;
14161487
}
14171488

14181489
// Then document any static sub-components
1419-
exp.exports.forEach(symbol => {
1490+
componentExports.exports.forEach(symbol => {
14201491
if (symbol.flags & ts.SymbolFlags.Prototype) {
14211492
return;
14221493
}
@@ -1439,7 +1510,9 @@ function parseWithProgramProvider(
14391510

14401511
if (doc) {
14411512
const prefix =
1442-
exp.escapedName === 'default' ? '' : `${exp.escapedName}.`;
1513+
componentExports.symbol.escapedName === 'default'
1514+
? ''
1515+
: `${componentExports.symbol.escapedName}.`;
14431516

14441517
componentDocs.push({
14451518
...doc,

0 commit comments

Comments
 (0)