Skip to content

Commit 4382762

Browse files
authored
Merge pull request #514 from cgatian/feat/improve-subcomponent-exports
Improve subcomponent and components with "parts" exports
2 parents 490ff82 + 46a75e5 commit 4382762

6 files changed

+263
-11
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+
/** SubComponent 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 Record = {
24+
StatelessStaticComponents,
25+
SubComponent
26+
};
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+
/** SubComponent 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 Record = {
24+
Comp1: StatelessStaticComponents,
25+
Comp2: SubComponent
26+
};

src/__tests__/parser.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,55 @@ 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+
281+
it('should parse static sub components exported from named object with keys', () => {
282+
check('StatelessStaticComponentsNamedObjectExportAsKeys', {
283+
StatelessStaticComponents: {
284+
myProp: { type: 'string' }
285+
},
286+
SubComponent: {
287+
title: { type: 'string' }
288+
}
289+
});
290+
});
291+
243292
it('should parse static sub components on class components', () => {
244293
check('ColumnWithStaticComponents', {
245294
Column: {

src/parser.ts

Lines changed: 112 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,76 @@ export class Parser {
254254
this.shouldIncludeExpression = Boolean(shouldIncludeExpression);
255255
}
256256

257-
private getComponentFromExpression(exp: ts.Symbol) {
257+
public getTypeSymbol(exp: ts.Symbol) {
258258
const declaration = exp.valueDeclaration || exp.declarations![0];
259+
const type = this.checker.getTypeOfSymbolAtLocation(exp, declaration);
260+
const typeSymbol = type.symbol || type.aliasSymbol;
261+
return typeSymbol;
262+
}
263+
264+
public isPlainObjectType(exp: ts.Symbol) {
265+
let targetSymbol = exp;
266+
if (exp.flags & ts.SymbolFlags.Alias) {
267+
targetSymbol = this.checker.getAliasedSymbol(exp);
268+
}
269+
const declaration =
270+
targetSymbol.valueDeclaration || targetSymbol.declarations![0];
271+
272+
if (ts.isClassDeclaration(declaration)) {
273+
return false;
274+
}
275+
276+
const type = this.checker.getTypeOfSymbolAtLocation(
277+
targetSymbol,
278+
declaration
279+
);
280+
// Confirm it's an object type
281+
if (!(type.flags & ts.TypeFlags.Object)) {
282+
return false;
283+
}
284+
const objectType = type as ts.ObjectType;
285+
const isPlain = !!(
286+
objectType.objectFlags &
287+
(ts.ObjectFlags.Anonymous | ts.ObjectFlags.ObjectLiteral)
288+
);
289+
return isPlain;
290+
}
291+
292+
/**
293+
* Attempts to gather a symbol's exports.
294+
* Some symbol's like `default` exports are aliased, so we need to get the real symbol.
295+
* @param exp symbol
296+
*/
297+
public getComponentExports(exp: ts.Symbol) {
298+
let targetSymbol = exp;
299+
300+
if (targetSymbol.exports) {
301+
return { symbol: targetSymbol, exports: targetSymbol.exports! };
302+
}
303+
304+
if (exp.flags & ts.SymbolFlags.Alias) {
305+
targetSymbol = this.checker.getAliasedSymbol(exp);
306+
}
307+
if (targetSymbol.exports) {
308+
return { symbol: targetSymbol, exports: targetSymbol.exports };
309+
}
310+
}
311+
312+
private getComponentFromExpression(exp: ts.Symbol) {
313+
let declaration = exp.valueDeclaration || exp.declarations![0];
314+
// Lookup component if it's a property assignment
315+
if (declaration && ts.isPropertyAssignment(declaration)) {
316+
if (ts.isIdentifier(declaration.initializer)) {
317+
const newSymbol = this.checker.getSymbolAtLocation(
318+
declaration.initializer
319+
);
320+
if (newSymbol) {
321+
exp = newSymbol;
322+
declaration = exp.valueDeclaration || exp.declarations![0];
323+
}
324+
}
325+
}
326+
259327
const type = this.checker.getTypeOfSymbolAtLocation(exp, declaration);
260328
const typeSymbol = type.symbol || type.aliasSymbol;
261329

@@ -264,7 +332,6 @@ export class Parser {
264332
}
265333

266334
const symbolName = typeSymbol.getName();
267-
268335
if (
269336
(symbolName === 'MemoExoticComponent' ||
270337
symbolName === 'ForwardRefExoticComponent') &&
@@ -1230,7 +1297,7 @@ function getTextValueOfFunctionProperty(
12301297
source: ts.SourceFile,
12311298
propertyName: string
12321299
) {
1233-
const [textValue] = source.statements
1300+
const identifierStatements: [ts.__String, string][] = source.statements
12341301
.filter(statement => ts.isExpressionStatement(statement))
12351302
.filter(statement => {
12361303
const expr = (statement as ts.ExpressionStatement)
@@ -1280,11 +1347,25 @@ function getTextValueOfFunctionProperty(
12801347
);
12811348
})
12821349
.map(statement => {
1283-
return (((statement as ts.ExpressionStatement)
1284-
.expression as ts.BinaryExpression).right as ts.Identifier).text;
1350+
const expressionStatement = (statement as ts.ExpressionStatement)
1351+
.expression as ts.BinaryExpression;
1352+
const name = ((expressionStatement.left as ts.PropertyAccessExpression)
1353+
.expression as ts.Identifier).escapedText;
1354+
const value = (expressionStatement.right as ts.Identifier).text;
1355+
return [name, value];
12851356
});
12861357

1287-
return textValue || '';
1358+
if (identifierStatements.length > 0) {
1359+
const locatedStatement = identifierStatements.find(
1360+
statement => statement[0] === exp.escapedName
1361+
);
1362+
if (locatedStatement) {
1363+
return locatedStatement[1];
1364+
}
1365+
return identifierStatements[0][1] || '';
1366+
}
1367+
1368+
return '';
12881369
}
12891370

12901371
function computeComponentName(
@@ -1450,11 +1531,28 @@ function parseWithProgramProvider(
14501531
return docs;
14511532
}
14521533

1453-
const components = checker.getExportsOfModule(moduleSymbol);
1534+
const exports = checker.getExportsOfModule(moduleSymbol);
14541535
const componentDocs: ComponentDoc[] = [];
1536+
const exportsAndMembers: ts.Symbol[] = [];
1537+
1538+
// Examine each export to determine if it's on object which may contain components
1539+
exports.forEach(exp => {
1540+
// Push symbol for extraction to maintain existing behavior
1541+
exportsAndMembers.push(exp);
1542+
// Determine if the export symbol is an object
1543+
if (!parser.isPlainObjectType(exp)) {
1544+
return;
1545+
}
1546+
const typeSymbol = parser.getTypeSymbol(exp);
1547+
if (typeSymbol?.members) {
1548+
typeSymbol.members.forEach(member => {
1549+
exportsAndMembers.push(member);
1550+
});
1551+
}
1552+
});
14551553

14561554
// First document all components
1457-
components.forEach(exp => {
1555+
exportsAndMembers.forEach(exp => {
14581556
const doc = parser.getComponentInfo(
14591557
exp,
14601558
sourceFile,
@@ -1466,12 +1564,13 @@ function parseWithProgramProvider(
14661564
componentDocs.push(doc);
14671565
}
14681566

1469-
if (!exp.exports) {
1567+
const componentExports = parser.getComponentExports(exp);
1568+
if (!componentExports) {
14701569
return;
14711570
}
14721571

14731572
// Then document any static sub-components
1474-
exp.exports.forEach(symbol => {
1573+
componentExports.exports.forEach(symbol => {
14751574
if (symbol.flags & ts.SymbolFlags.Prototype) {
14761575
return;
14771576
}
@@ -1494,7 +1593,9 @@ function parseWithProgramProvider(
14941593

14951594
if (doc) {
14961595
const prefix =
1497-
exp.escapedName === 'default' ? '' : `${exp.escapedName}.`;
1596+
componentExports.symbol.escapedName === 'default'
1597+
? ''
1598+
: `${componentExports.symbol.escapedName}.`;
14981599

14991600
componentDocs.push({
15001601
...doc,

0 commit comments

Comments
 (0)