Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#51 binomialstew's refactor #54

Merged
merged 7 commits into from
Jun 17, 2018
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
10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,23 @@
"description": "Babel plugin to add react-docgen info into your code",
"repository": "https://github.com/storybooks/babel-plugin-react-docgen",
"author": "Madushan Nishantha <[email protected]>",
"contributors": [
"Charles Lehnert"
],
"main": "lib/index.js",
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-preset-flow": "^6.23.0",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"mocha": "^4.1.0"
"mocha": "^4.1.0",
"recast": "^0.14.7"
},
"scripts": {
"clean": "rm -rf lib",
"build": "babel src -d lib",
"test": "mocha --compilers js:babel-register",
"test": "mocha --require babel-register",
"test:watch": "npm run test -- --watch",
"prepare": "npm run test && npm run clean && npm run build"
},
Expand All @@ -29,7 +33,7 @@
"dependencies": {
"babel-types": "^6.26.0",
"lodash": "^4.17.10",
"react-docgen": "^3.0.0-beta11"
"react-docgen": "^3.0.0-beta12"
},
"license": "MIT"
}
62 changes: 62 additions & 0 deletions src/actualNameHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*
* 2018 - Charles Lehnert
* This is heavily based on the react-docgen `displayNameHandler` but instead defines an `actualName` property on the
* generated docs that is taken first from the component's actual name. This addresses an issue where the name that
* the generated docs are stored under is incorrectly named with the `displayName` and not the component's actual name.
*
*/

import { utils } from 'react-docgen';
import recast from 'recast';

const { getMemberValuePath, getNameOrValue, resolveFunctionDefinitionToReturnValue, resolveToValue } = utils;
const {types: {namedTypes: types}} = recast;

export default function actualNameHandler(documentation, path) {
// Function and class declarations need special treatment. The name of the
// function / class is the displayName
if (
types.ClassDeclaration.check(path.node) ||
types.FunctionDeclaration.check(path.node)
) {
documentation.set('actualName', getNameOrValue(path.get('id')));
} else if (
types.ArrowFunctionExpression.check(path.node) ||
types.FunctionExpression.check(path.node)
) {
if (types.VariableDeclarator.check(path.parentPath.node)) {
documentation.set('actualName', getNameOrValue(path.parentPath.get('id')));
} else if (types.AssignmentExpression.check(path.parentPath.node)) {
documentation.set('actualName', getNameOrValue(path.parentPath.get('left')));
}
} else if (
// React.createClass() or createReactClass()
types.CallExpression.check(path.parentPath.node) &&
types.VariableDeclarator.check(path.parentPath.parentPath.parentPath.node)
) {
documentation.set('actualName', getNameOrValue(path.parentPath.parentPath.parentPath.get('id')));
} else {
// Could not find an actual name
documentation.set('actualName', '');
}
return;

// If display name is defined as a getter we get a function expression as
// value. In that case we try to determine the value from the return
// statement.
if (types.FunctionExpression.check(displayNamePath.node)) {
displayNamePath = resolveFunctionDefinitionToReturnValue(displayNamePath);
}
if (!displayNamePath || !types.Literal.check(displayNamePath.node)) {
return;
}
documentation.set('actualName', displayNamePath.node.value);
}
182 changes: 17 additions & 165 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,170 +1,34 @@
import * as Path from 'path';
import * as _ from 'lodash';
import * as ReactDocgen from 'react-docgen';
import isReactComponentClass from './isReactComponentClass';
import isStatelessComponent from './isStatelessComponent';
import * as Path from 'path';
import * as reactDocgenHandlers from 'react-docgen/dist/handlers';
import actualNameHandler from './actualNameHandler';

export default function ({types: t}) {
const defaultHandlers = Object.values(reactDocgenHandlers).map(handler => handler);
const handlers = [...defaultHandlers, actualNameHandler]

export default function({ types: t }) {
return {
visitor: {
Class(path, state) {
if(!isReactComponentClass(path)) {
return;
}
if(!path.node.id){
return;
Program: {
exit(path, state) {
injectReactDocgenInfo(path, state, this.file.code, t);
}
const className = path.node.id.name;

if(!isExported(path, className, t)){
return;
}
injectReactDocgenInfo(className, path, state, this.file.code, t);
},
'CallExpression'(path, state) {
const callee = path.node.callee;

const objectName = _.get(callee, 'object.name') ? callee.object.name.toLowerCase() : null;
const propertyName = _.get(callee, 'property.name') ? callee.property.name.toLowerCase() : null;
const calleeName = _.get(callee, 'name') ? callee.name.toLowerCase() : null;

// Detect `React.createClass()`
const hasReactCreateClass = (objectName === 'react' && propertyName === 'createclass');

// Detect `createReactClass()`
const hasCreateReactClass = (calleeName === 'createreactclass');

// Get React class name from variable declaration
const className = _.get(path, 'parentPath.parent.declarations[0].id.name');

// Detect `React.createElement()`
const hasReactCreateElement = (objectName === 'react' && propertyName === 'createelement');

if (className && (hasReactCreateClass || hasCreateReactClass)) {
injectReactDocgenInfo(className, path, state, this.file.code, t);
}

if (hasReactCreateElement) {
const variableDeclaration = path.findParent((path) => path.isVariableDeclaration());

if (variableDeclaration) {
const elementClassName = variableDeclaration.node.declarations[0].id.name;
if (!isExported(path, elementClassName, t)) {
return;
}

injectReactDocgenInfo(elementClassName, path, state, this.file.code, t);
}
}
},
'FunctionDeclaration|FunctionExpression|ArrowFunctionExpression'(path, state) {
if (!isStatelessComponent(path)) {
return;
}

const node = (path.node.type === 'FunctionDeclaration') ? path.node : path.parentPath.node;

if (!node.id) {
return;
}
const className = node.id.name;

if (!isExported(path, className, t)) {
return;
}
injectReactDocgenInfo(className, path, state, this.file.code, t);
},
}
};
}

function isExported(path, className, t){
const types = [
'ExportDefaultDeclaration',
'ExportNamedDeclaration'
];

function findMostRightHandArgument(args = []) {
const arg = args[0]
if (t.isIdentifier(arg)) {
return arg.name
} else if(t.isCallExpression(arg)) {
return findMostRightHandArgument(arg.arguments)
}
}

if(path.parentPath.node &&
types.some(type => {return path.parentPath.node.type === type;})) {
return true;
}

const program = path.scope.getProgramParent().path;
return program.get('body').some(path => {
if(path.node.type === 'ExportNamedDeclaration') {
if (path.node.specifiers && path.node.specifiers.length) {
return className === path.node.specifiers[0].exported.name;
} else if (path.node.declaration.declarations && path.node.declaration.declarations.length) {
return className === path.node.declaration.declarations[0].id.name;
}
} else if(path.node.type === 'ExportDefaultDeclaration') {
const decl = path.node.declaration
if (t.isCallExpression(decl)) {
return className === findMostRightHandArgument(decl.arguments);
} else {
return className === decl.name;
}
// Detect module.exports = className;
} else if(path.node.type === 'ExpressionStatement') {
const expr = path.node.expression

if (t.isAssignmentExpression(expr)) {
const left = expr.left;
const right = expr.right;

const leftIsModuleExports = t.isMemberExpression(left) &&
t.isIdentifier(left.object) &&
t.isIdentifier(left.property) &&
left.object.name === 'module' &&
left.property.name === 'exports';

const rightIsIdentifierClass = t.isIdentifier(right) && right.name === className;

return leftIsModuleExports && rightIsIdentifierClass;
}
}
return false;
});
}

function alreadyVisited(program, t) {
return program.node.body.some(node => {
if(t.isExpressionStatement(node) &&
t.isAssignmentExpression(node.expression) &&
t.isMemberExpression(node.expression.left)
) {
return node.expression.left.property.name === '__docgenInfo';
}
return false;
});
},
};
}


function injectReactDocgenInfo(className, path, state, code, t) {
function injectReactDocgenInfo(path, state, code, t) {
const program = path.scope.getProgramParent().path;

if(alreadyVisited(program, t)) {
return;
}

let docgenResults = [];
try { // all exported component definitions includes named exports
try {
let resolver = ReactDocgen.resolver.findAllExportedComponentDefinitions;

if (state.opts.resolver) {
resolver = ReactDocgen.resolver[state.opts.resolver];
}

docgenResults = ReactDocgen.parse(code, resolver);
docgenResults = ReactDocgen.parse(code, resolver, handlers);

if (state.opts.removeMethods) {
docgenResults.forEach(function(docgenResult) {
Expand All @@ -177,21 +41,8 @@ function injectReactDocgenInfo(className, path, state, code, t) {
return;
}

// docgen sometimes doesn't include 'displayName' which is the react function/class name
// the first time it's not available, we try to match it to the export name
let isDefaultClassNameUsed = false;

docgenResults.forEach(function(docgenResult, index) {
if (isDefaultClassNameUsed && !docgenResult.displayName) {
return;
}

let exportName = docgenResult.displayName;
if (!exportName) {
exportName = className;
isDefaultClassNameUsed = true;
}

let exportName = docgenResult.actualName;
const docNode = buildObjectExpression(docgenResult, t);
const docgenInfo = t.expressionStatement(
t.assignmentExpression(
Expand Down Expand Up @@ -260,6 +111,7 @@ function buildObjectExpression(obj, t){
if(_.isPlainObject(obj)) {
const children = [];
for (let key in obj) {
if (key === 'actualName') continue;
if(!obj.hasOwnProperty(key) || _.isUndefined(obj[key])) continue;
children.push(
t.objectProperty(
Expand Down
35 changes: 0 additions & 35 deletions src/isReactComponentClass.js

This file was deleted.

Loading