Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/__tests__/data/StatelessStaticComponentsExportVariation1.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as React from 'react';

interface LabelProps {
/** title description */
title: string;
}

/** StatelessStaticComponents.Label description */
const SubComponent = (props: LabelProps) => (
<div>My Property = {props.title}</div>
);

interface StatelessStaticComponentsProps {
/** myProp description */
myProp: string;
}

/** StatelessStaticComponents description */
const StatelessStaticComponents = (props: StatelessStaticComponentsProps) => (
<div>My Property = {props.myProp}</div>
);

StatelessStaticComponents.Label = SubComponent;

export { StatelessStaticComponents };
25 changes: 25 additions & 0 deletions src/__tests__/data/StatelessStaticComponentsExportVariation2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as React from 'react';

interface LabelProps {
/** title description */
title: string;
}

/** StatelessStaticComponents.Label description */
const SubComponent = (props: LabelProps) => (
<div>My Property = {props.title}</div>
);

interface StatelessStaticComponentsProps {
/** myProp description */
myProp: string;
}

/** StatelessStaticComponents description */
function StatelessStaticComponents(props: StatelessStaticComponentsProps) {
return <div>My Property = {props.myProp}</div>;
}

StatelessStaticComponents.Label = SubComponent;

export { StatelessStaticComponents };
26 changes: 26 additions & 0 deletions src/__tests__/data/StatelessStaticComponentsNamedObjectExport.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as React from 'react';

interface LabelProps {
/** title description */
title: string;
}

/** SubComponent description */
const SubComponent = (props: LabelProps) => (
<div>My Property = {props.title}</div>
);

interface StatelessStaticComponentsProps {
/** myProp description */
myProp: string;
}

/** StatelessStaticComponents description */
const StatelessStaticComponents = (props: StatelessStaticComponentsProps) => (
<div>My Property = {props.myProp}</div>
);

export const Record = {
StatelessStaticComponents,
SubComponent
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as React from 'react';

interface LabelProps {
/** title description */
title: string;
}

/** SubComponent description */
const SubComponent = (props: LabelProps) => (
<div>My Property = {props.title}</div>
);

interface StatelessStaticComponentsProps {
/** myProp description */
myProp: string;
}

/** StatelessStaticComponents description */
const StatelessStaticComponents = (props: StatelessStaticComponentsProps) => (
<div>My Property = {props.myProp}</div>
);

export const Record = {
Comp1: StatelessStaticComponents,
Comp2: SubComponent
};
49 changes: 49 additions & 0 deletions src/__tests__/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,55 @@ describe('parser', () => {
});
});

it('should parse static exported components variation1', () => {
check('StatelessStaticComponentsExportVariation1', {
StatelessStaticComponents: {
myProp: { type: 'string' }
},
'StatelessStaticComponents.Label': {
title: { type: 'string' }
}
});
});

it('should parse static exported components variation2', () => {
check('StatelessStaticComponentsExportVariation2', {
StatelessStaticComponents: {
myProp: { type: 'string' }
},
'StatelessStaticComponents.Label': {
title: { type: 'string' }
}
});
});

it('should parse static sub components exported from named object', () => {
check(
'StatelessStaticComponentsNamedObjectExport',
{
StatelessStaticComponents: {
myProp: { type: 'string' }
},
SubComponent: {
title: { type: 'string' }
}
},
true,
''
);
});

it('should parse static sub components exported from named object with keys', () => {
check('StatelessStaticComponentsNamedObjectExportAsKeys', {
StatelessStaticComponents: {
myProp: { type: 'string' }
},
SubComponent: {
title: { type: 'string' }
}
});
});

it('should parse static sub components on class components', () => {
check('ColumnWithStaticComponents', {
Column: {
Expand Down
123 changes: 112 additions & 11 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,76 @@ export class Parser {
this.shouldIncludeExpression = Boolean(shouldIncludeExpression);
}

private getComponentFromExpression(exp: ts.Symbol) {
public getTypeSymbol(exp: ts.Symbol) {
const declaration = exp.valueDeclaration || exp.declarations![0];
const type = this.checker.getTypeOfSymbolAtLocation(exp, declaration);
const typeSymbol = type.symbol || type.aliasSymbol;
return typeSymbol;
}

public isPlainObjectType(exp: ts.Symbol) {
let targetSymbol = exp;
if (exp.flags & ts.SymbolFlags.Alias) {
targetSymbol = this.checker.getAliasedSymbol(exp);
}
const declaration =
targetSymbol.valueDeclaration || targetSymbol.declarations![0];

if (ts.isClassDeclaration(declaration)) {
return false;
}

const type = this.checker.getTypeOfSymbolAtLocation(
targetSymbol,
declaration
);
// Confirm it's an object type
if (!(type.flags & ts.TypeFlags.Object)) {
return false;
}
const objectType = type as ts.ObjectType;
const isPlain = !!(
objectType.objectFlags &
(ts.ObjectFlags.Anonymous | ts.ObjectFlags.ObjectLiteral)
);
return isPlain;
}

/**
* Attempts to gather a symbol's exports.
* Some symbol's like `default` exports are aliased, so we need to get the real symbol.
* @param exp symbol
*/
public getComponentExports(exp: ts.Symbol) {
let targetSymbol = exp;

if (targetSymbol.exports) {
return { symbol: targetSymbol, exports: targetSymbol.exports! };
}

if (exp.flags & ts.SymbolFlags.Alias) {
targetSymbol = this.checker.getAliasedSymbol(exp);
}
if (targetSymbol.exports) {
return { symbol: targetSymbol, exports: targetSymbol.exports };
}
}

private getComponentFromExpression(exp: ts.Symbol) {
let declaration = exp.valueDeclaration || exp.declarations![0];
// Lookup component if it's a property assignment
if (declaration && ts.isPropertyAssignment(declaration)) {
if (ts.isIdentifier(declaration.initializer)) {
const newSymbol = this.checker.getSymbolAtLocation(
declaration.initializer
);
if (newSymbol) {
exp = newSymbol;
declaration = exp.valueDeclaration || exp.declarations![0];
}
}
}

const type = this.checker.getTypeOfSymbolAtLocation(exp, declaration);
const typeSymbol = type.symbol || type.aliasSymbol;

Expand All @@ -261,7 +329,6 @@ export class Parser {
}

const symbolName = typeSymbol.getName();

if (
(symbolName === 'MemoExoticComponent' ||
symbolName === 'ForwardRefExoticComponent') &&
Expand Down Expand Up @@ -1206,7 +1273,7 @@ function getTextValueOfFunctionProperty(
source: ts.SourceFile,
propertyName: string
) {
const [textValue] = source.statements
const identifierStatements: [ts.__String, string][] = source.statements
.filter(statement => ts.isExpressionStatement(statement))
.filter(statement => {
const expr = (statement as ts.ExpressionStatement)
Expand All @@ -1225,11 +1292,25 @@ function getTextValueOfFunctionProperty(
);
})
.map(statement => {
return (((statement as ts.ExpressionStatement)
.expression as ts.BinaryExpression).right as ts.Identifier).text;
const expressionStatement = (statement as ts.ExpressionStatement)
.expression as ts.BinaryExpression;
const name = ((expressionStatement.left as ts.PropertyAccessExpression)
.expression as ts.Identifier).escapedText;
const value = (expressionStatement.right as ts.Identifier).text;
return [name, value];
});

return textValue || '';
if (identifierStatements.length > 0) {
const locatedStatement = identifierStatements.find(
statement => statement[0] === exp.escapedName
);
if (locatedStatement) {
return locatedStatement[1];
}
return identifierStatements[0][1] || '';
}

return '';
}

function computeComponentName(
Expand Down Expand Up @@ -1395,11 +1476,28 @@ function parseWithProgramProvider(
return docs;
}

const components = checker.getExportsOfModule(moduleSymbol);
const exports = checker.getExportsOfModule(moduleSymbol);
const componentDocs: ComponentDoc[] = [];
const exportsAndMembers: ts.Symbol[] = [];

// Examine each export to determine if it's on object which may contain components
exports.forEach(exp => {
// Push symbol for extraction to maintain existing behavior
exportsAndMembers.push(exp);
// Determine if the export symbol is an object
if (!parser.isPlainObjectType(exp)) {
return;
}
const typeSymbol = parser.getTypeSymbol(exp);
if (typeSymbol?.members) {
typeSymbol.members.forEach(member => {
exportsAndMembers.push(member);
});
}
});

// First document all components
components.forEach(exp => {
exportsAndMembers.forEach(exp => {
const doc = parser.getComponentInfo(
exp,
sourceFile,
Expand All @@ -1411,12 +1509,13 @@ function parseWithProgramProvider(
componentDocs.push(doc);
}

if (!exp.exports) {
const componentExports = parser.getComponentExports(exp);
if (!componentExports) {
return;
}

// Then document any static sub-components
exp.exports.forEach(symbol => {
componentExports.exports.forEach(symbol => {
if (symbol.flags & ts.SymbolFlags.Prototype) {
return;
}
Expand All @@ -1439,7 +1538,9 @@ function parseWithProgramProvider(

if (doc) {
const prefix =
exp.escapedName === 'default' ? '' : `${exp.escapedName}.`;
componentExports.symbol.escapedName === 'default'
? ''
: `${componentExports.symbol.escapedName}.`;

componentDocs.push({
...doc,
Expand Down