@workday/canvas-kit-docs
Version:
Documentation components of Canvas Kit components
228 lines (227 loc) • 9.9 kB
JavaScript
import ts from 'typescript';
import { createParserPlugin, filterObjectProperties, getValueDeclaration, getDefaultFromTags, isObject, isExportedSymbol, getValidDefaultFromNode, } from '../docParser';
import t from '../traverse';
export const componentParser = createParserPlugin((node, parser) => {
var _a, _b;
/**
* We'll look for class components that extend from `React.Component` or `Component`
*
* ```tsx
* class MyComponent extends React.Component<Props> {
* }
* ```
*/
if (t.isClassDeclaration(node)) {
// check if this is a `React.Component`
if (node.heritageClauses &&
node.heritageClauses[0] &&
t.isHeritageClause(node.heritageClauses[0]) &&
node.heritageClauses[0].types[0]) {
const clause = node.heritageClauses[0].types[0];
if ((clause &&
// `class A extends Component`
t.isExpressionWithTypeArguments(clause) &&
t.isIdentifier(clause.expression) &&
clause.expression.text === 'Component') ||
// `class A extends React.Component`
(t.isPropertyAccessExpression(clause.expression) &&
t.isIdentifier(clause.expression.expression) &&
clause.expression.expression.text === 'React' &&
t.isIdentifier(clause.expression.name) &&
clause.expression.name.text === 'Component')) {
// The `typeArguments` are the generics: `React.Component<P>`
if (clause.typeArguments &&
clause.typeArguments[0] &&
t.isTypeReference(clause.typeArguments[0])) {
const type = parser.checker.getTypeAtLocation(clause.typeArguments[0]);
const defaultPropNode = (_a = node.members.find(m => t.isPropertyDeclaration(m) &&
t.isIdentifier(m.name) &&
m.name.text === 'defaultProps')) === null || _a === void 0 ? void 0 : _a.initializer;
const defaultProps = getDefaultsFromDefaultProps(parser, defaultPropNode);
const props = getComponentProps(parser, type, defaultProps);
/**
* Get a displayName if there is one
*/
const displayName = (_b = node.members.find(m => t.isPropertyDeclaration(m) &&
t.isIdentifier(m.name) &&
m.name.text === 'displayName' &&
m.initializer &&
t.isStringLiteral(m.initializer))) === null || _b === void 0 ? void 0 : _b.initializer.text;
return { kind: 'component', props, displayName };
}
// We couldn't find any props
return {
kind: 'component',
props: [],
};
}
}
}
/**
* ```tsx
* export function A(props: Props) { return <div /> }
* ```
*/
if (ts.isFunctionLike(node) && isExportedSymbol(parser, node)) {
return getComponentFromFunction(parser, node);
}
/**
* ```tsx
* export const A = (props: Props) => <div />
* ```
*/
if (t.isVariableDeclaration(node) && node.initializer && ts.isFunctionLike(node.initializer)) {
return getComponentFromFunction(parser, node.initializer);
}
/**
* The docParser would return `React.FC<Props>` as a type, but we want to evaluate `Props`
* ```tsx
* const A: React.FC<Props> = (props) => {
* }
* ```
*/
if (t.isVariableDeclaration(node) &&
node.type &&
t.isTypeReference(node.type) &&
t.isQualifiedName(node.type.typeName) &&
t.isIdentifier(node.type.typeName.left) &&
node.type.typeName.left.text === 'React' &&
t.isIdentifier(node.type.typeName.right) &&
(node.type.typeName.right.text === 'FC' ||
node.type.typeName.right.text === 'FunctionComponent') &&
node.initializer) {
// Force evaluation of the initializer. This will result in the function being evaluated as a
// component
return getComponentFromFunction(parser, node.initializer);
}
/**
* ```tsx
* export const A = React.forwardRef((props: Props) => <div />)
* ```
*/
if (t.isVariableDeclaration(node) &&
node.initializer &&
t.isCallExpression(node.initializer) &&
((t.isIdentifier(node.initializer.expression) &&
node.initializer.expression.text === 'forwardRef') ||
(t.isPropertyAccessExpression(node.initializer.expression) &&
t.isIdentifier(node.initializer.expression.expression) &&
node.initializer.expression.expression.text === 'React' &&
t.isIdentifier(node.initializer.expression.name) &&
node.initializer.expression.name.text === 'forwardRef')) &&
node.initializer.arguments.length &&
ts.isFunctionLike(node.initializer.arguments[0])) {
// Force evaluation of the initializer. This will result in the function being evaluated as a
// component
return getComponentFromFunction(parser, node.initializer.arguments[0]);
}
return undefined;
});
/**
* Evaluate the return type looking for a `React.ReactElement` type based on the `props`.
*/
export function isComponent(returnType) {
if (returnType.isUnion()) {
return returnType.types.some(isComponent);
}
if (isObject(returnType)) {
return returnType.getProperties().some(s => s.name === 'props');
}
return false;
}
/**
* Get the props of an interface. It is expected the type of the `Prop` interface has already been
* created and passed down. Where props are defined is very specific to how a React component is
* created.
*
* @param parser
* @param signature The function like component containing the component's implementation
* @param type The type of the props. We use a type because there are many ways the prop interface
* @param defaultProps Optional record of found default props could be created
*/
export function getComponentProps(parser, type, defaultProps = {}) {
const props = type
.getProperties()
.map(symbol => {
const defaultValue = defaultProps[symbol.name] || getDefaultFromTags(symbol.getJsDocTags());
const value = parser.getValueFromNode(getValueDeclaration(symbol));
if (value.kind === 'property') {
value.defaultValue = defaultValue;
}
return value;
})
.filter(filterObjectProperties);
return props;
}
/**
* A component might have a `defaultProps` literal. The `node` passed should be this node. If this
* node is an `ObjectLiteralExpression`, we'll try to parse to extract default props
*/
export function getDefaultsFromDefaultProps(parser, node) {
if (node && t.isObjectLiteralExpression(node)) {
return node.properties.reduce((result, property) => {
if (t.isPropertyAssignment(property) && t.isIdentifier(property.name)) {
result[property.name.text] = parser.getValueFromNode(property.initializer);
}
return result;
}, {});
}
return {};
}
/**
* A parameter might represent a `ObjectBindingPattern` which can be used to set defaults. This will
* return all defaults found within the `ObjectBindingPattern` and return them as a map of the
* property name to the `Value`. These defaults can be used to piece together a default. Also
* `getDefaultFromTags` can be used to get defaults from JSDoc tags.
*/
export function getDefaultsFromObjectBindingPattern(parser, node) {
if (t.isObjectBindingPattern(node)) {
return node.elements.reduce((result, element) => {
if (t.isBindingElement(element) && t.isIdentifier(element.name) && element.initializer) {
const defaultValue = getValidDefaultFromNode(parser, element.initializer);
if (defaultValue) {
result[element.name.text] = defaultValue;
}
}
return result;
}, {});
}
return {};
}
function getComponentFromFunction(parser, node) {
/**
* Check all functions for a signature of a component
*/
if (ts.isFunctionLike(node)) {
// Get a signature and test the return type for JSX props
const signature = parser.checker.getSignatureFromDeclaration(node);
if (signature) {
const returnType = signature.getReturnType();
if (isComponent(returnType)) {
if (signature.parameters[0]) {
const declaration = getValueDeclaration(signature.parameters[0]);
if (declaration && t.isParameter(declaration)) {
const type = parser.checker.getTypeAtLocation(declaration);
if (!isObject(type) && !type.isIntersection()) {
// This is not a component. We may still get some false positives, but this decreases
// the likelihood
return undefined;
}
const defaults = getDefaultsFromObjectBindingPattern(parser, declaration.name);
const props = getComponentProps(parser, type, defaults);
return {
kind: 'component',
props,
};
}
}
// We couldn't find props
return {
kind: 'component',
props: [],
};
}
}
}
return undefined;
}