├── test ├── mocha.opts ├── fixtures │ ├── emptyReturn │ │ ├── input.js │ │ └── expected.js │ ├── arrowFun │ │ ├── input.js │ │ └── expected.js │ ├── exportDefaultAnon │ │ ├── input.js │ │ └── expected.js │ ├── createClass │ │ ├── input.js │ │ └── expected.js │ ├── decorators │ │ ├── input.js │ │ └── expected.js │ ├── knownComponents │ │ ├── input.js │ │ └── expected.js │ ├── classComponents │ │ ├── input.js │ │ └── expected.js │ ├── functionExpr │ │ ├── input.js │ │ └── expected.js │ └── passThrough │ │ ├── input.js │ │ └── expected.js └── tests.js ├── .gitignore ├── README.md ├── package.json └── index.js /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --ui tdd -------------------------------------------------------------------------------- /test/fixtures/emptyReturn/input.js: -------------------------------------------------------------------------------- 1 | // Arrow function with empty return 2 | var emptyReturnFunction = () => { 3 | return 4 | } -------------------------------------------------------------------------------- /test/fixtures/emptyReturn/expected.js: -------------------------------------------------------------------------------- 1 | // Arrow function with empty return 2 | var emptyReturnFunction = () => { 3 | return; 4 | }; -------------------------------------------------------------------------------- /test/fixtures/arrowFun/input.js: -------------------------------------------------------------------------------- 1 | // Stateless component with an arrow function 2 | var Component2 = ({value}) => { 3 | return ( 4 |
{value}
5 | ) 6 | } -------------------------------------------------------------------------------- /test/fixtures/exportDefaultAnon/input.js: -------------------------------------------------------------------------------- 1 | // Exported default stateless component used in variable declaration 2 | export default function ({value}) { 3 | return
{value}
4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/arrowFun/expected.js: -------------------------------------------------------------------------------- 1 | // Stateless component with an arrow function 2 | var Component2 = ({ value }) => { 3 | return React.createElement( 4 | "div", 5 | null, 6 | value 7 | ); 8 | }; 9 | Component2.displayName = "Component2"; 10 | -------------------------------------------------------------------------------- /test/fixtures/createClass/input.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Component } from 'react'; 3 | 4 | // Babel already sets displayName for this one 5 | export var Component0 = React.createClass({ 6 | render: function() { 7 |
8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /test/fixtures/exportDefaultAnon/expected.js: -------------------------------------------------------------------------------- 1 | 2 | // Exported default stateless component used in variable declaration 3 | export default function _uid({ value }) { 4 | return React.createElement( 5 | "div", 6 | null, 7 | value 8 | ); 9 | } 10 | _uid.displayName = "input"; 11 | -------------------------------------------------------------------------------- /test/fixtures/decorators/input.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Component } from 'react' 3 | import connect from '../decorators/connect'; 4 | 5 | 6 | @connect(Component) 7 | export default class DecoratedComponent extends React.Component { 8 | render() { 9 | return
10 | } 11 | } -------------------------------------------------------------------------------- /test/fixtures/createClass/expected.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Component } from 'react'; 3 | 4 | // Babel already sets displayName for this one 5 | export var Component0 = React.createClass({ 6 | displayName: 'Component0', 7 | 8 | render: function () { 9 | React.createElement('div', null); 10 | } 11 | }); -------------------------------------------------------------------------------- /test/fixtures/knownComponents/input.js: -------------------------------------------------------------------------------- 1 | // Specifically configured to set name on Component5a and Component5b 2 | function Component5a() { 3 | return "some string" 4 | } 5 | 6 | var Component5b = function () { 7 | return "some string" 8 | } 9 | 10 | // Known component's name used inside another function 11 | var Component5c = function () { 12 | function Component5c() {} 13 | return Component5c 14 | }() 15 | -------------------------------------------------------------------------------- /test/fixtures/decorators/expected.js: -------------------------------------------------------------------------------- 1 | var _dec, _class; 2 | 3 | import React from 'react'; 4 | import { Component } from 'react'; 5 | import connect from '../decorators/connect'; 6 | 7 | let DecoratedComponent = (_dec = connect(Component), _dec(_class = class DecoratedComponent extends React.Component { 8 | render() { 9 | return React.createElement('div', null); 10 | } 11 | }) || _class); 12 | DecoratedComponent.displayName = 'DecoratedComponent'; 13 | export { DecoratedComponent as default }; 14 | -------------------------------------------------------------------------------- /test/fixtures/knownComponents/expected.js: -------------------------------------------------------------------------------- 1 | 2 | // Specifically configured to set name on Component5a and Component5b 3 | function Component5a() { 4 | return "some string"; 5 | } 6 | 7 | Component5a.displayName = "Component5a"; 8 | var Component5b = function () { 9 | return "some string"; 10 | }; 11 | 12 | Component5b.displayName = "Component5b"; 13 | // Known component's name used inside another function 14 | var Component5c = function () { 15 | function Component5c() {} 16 | return Component5c; 17 | }(); 18 | Component5c.displayName = "Component5c"; 19 | -------------------------------------------------------------------------------- /test/fixtures/classComponents/input.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Component } from 'react' 3 | 4 | 5 | export class Component3a extends React.Component { 6 | render() { 7 | return
8 | } 9 | } 10 | 11 | export default class Component3b extends React.Component { 12 | render() { 13 | return
14 | } 15 | } 16 | 17 | export class Component3c extends Component { 18 | render() { 19 | return
20 | } 21 | } 22 | 23 | class Component3d extends Component { 24 | static get = () => { 25 | return
; 26 | } 27 | render() { 28 | return
29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | .eslintrc 40 | .vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # babel-plugin-add-react-displayname 2 | 3 | Automatically detects and sets displayName for React components. 4 | This is useful for having real component names show up in production builds of React apps. 5 | 6 | Babel already does this for `React.createClass` style components, this adds support for the two other kinds of component definitions: 7 | * ES6-classes style components 8 | * Stateless components that return JSX 9 | 10 | 11 | ## Installation 12 | Simply add `add-react-displayname` to your `.babelrc` file: 13 | 14 | ```json 15 | { 16 | "plugins": ["add-react-displayname"] 17 | } 18 | ``` 19 | 20 | ## Troubleshooting 21 | 22 | #### Doesn't work for decorated classes 23 | 24 | If you are using the `transform-decorators-legacy` plugin, make sure it's placed *after* this plugin in your plugin list. 25 | 26 | ## Testing 27 | 28 | `npm test` 29 | -------------------------------------------------------------------------------- /test/fixtures/functionExpr/input.js: -------------------------------------------------------------------------------- 1 | // Exported stateless componenet 2 | export function Component1a(value) { 3 | return
{value}
4 | } 5 | 6 | // Stateless componenet 7 | function Component1b(value) { 8 | return
{value}
9 | } 10 | 11 | // Stateless componenet used in a variable declaration 12 | var Component1c = function (value) { 13 | return
{value}
14 | } 15 | 16 | // Exported named stateless component used in variable declaration 17 | export var Component1d = function (value) { 18 | return
{value}
19 | } 20 | 21 | // Stateless componenet used in an assignment 22 | var Component1e; 23 | Component1e = function (value) { 24 | return
{value}
25 | } 26 | 27 | // Exported default stateless *named* component used in variable declaration 28 | export default function Component1f (value) { 29 | return
{value}
30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-add-react-displayname", 3 | "version": "0.0.5", 4 | "description": "Automatically add displayName to all your components", 5 | "main": "index.js", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "babel-core": "^6.14.0", 9 | "babel-helper-plugin-test-runner": "^6.8.0", 10 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 11 | "babel-preset-react": "^6.24.1", 12 | "babel-preset-stage-0": "^6.16.0", 13 | "mocha": "^3.0.2", 14 | "standard": "^8.1.0" 15 | }, 16 | "scripts": { 17 | "test": "./node_modules/.bin/mocha" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/opbeat/babel-plugin-add-react-displayname.git" 22 | }, 23 | "author": "Ron Cohen", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/opbeat/babel-plugin-add-react-displayname/issues" 27 | }, 28 | "homepage": "https://github.com/opbeat/babel-plugin-add-react-displayname#readme" 29 | } 30 | -------------------------------------------------------------------------------- /test/fixtures/classComponents/expected.js: -------------------------------------------------------------------------------- 1 | var _class, _temp; 2 | 3 | import React from 'react'; 4 | import { Component } from 'react'; 5 | 6 | export let Component3a = class Component3a extends React.Component { 7 | render() { 8 | return React.createElement('div', null); 9 | } 10 | }; 11 | 12 | Component3a.displayName = 'Component3a'; 13 | let Component3b = class Component3b extends React.Component { 14 | render() { 15 | return React.createElement('div', null); 16 | } 17 | }; 18 | Component3b.displayName = 'Component3b'; 19 | export { Component3b as default }; 20 | 21 | 22 | export let Component3c = class Component3c extends Component { 23 | render() { 24 | return React.createElement('div', null); 25 | } 26 | }; 27 | 28 | Component3c.displayName = 'Component3c'; 29 | let Component3d = (_temp = _class = class Component3d extends Component { 30 | render() { 31 | return React.createElement('div', null); 32 | } 33 | }, _class.get = () => { 34 | return React.createElement('div', null); 35 | }, _temp); 36 | Component3d.displayName = 'Component3d'; 37 | -------------------------------------------------------------------------------- /test/fixtures/passThrough/input.js: -------------------------------------------------------------------------------- 1 | // --------------------- 2 | // Should pass through unaltered 3 | // --------------------- 4 | var f1 = function({value}) { 5 | return "somestring" 6 | } 7 | 8 | function f2({value}) { 9 | return "somestring" 10 | } 11 | 12 | class f3 { 13 | method1() { 14 | return "whatever" 15 | } 16 | } 17 | 18 | var f4 = ( 19 |
20 | {(() => )()} 21 |
22 | ) 23 | 24 | // Known component which doesn't sit directly on the `Program` node get left alone 25 | { 26 | var Component5c = function () { 27 | function Component5c () {} 28 | return Component5c 29 | }() 30 | } 31 | 32 | // --------------------- 33 | // Not supported 34 | // --------------------- 35 | 36 | // High-order things will be hard to catch 37 | var jsxChunk =
{value}
38 | function UnsupportedComponent1({value}) { 39 | return function() { 40 | return jsxChunk 41 | } 42 | } 43 | 44 | var a = { 45 | smoke: function() {}, 46 | Component1d: function ({value}) { 47 | return
{value}
48 | } 49 | } 50 | 51 | var external = function() { 52 | var internal = function() { 53 | return
54 | } 55 | return internal 56 | } 57 | 58 | -------------------------------------------------------------------------------- /test/tests.js: -------------------------------------------------------------------------------- 1 | var babel = require("babel-core") 2 | var fs = require("fs") 3 | var path = require("path") 4 | var fixturesDir = path.join(__dirname, "fixtures") 5 | 6 | // var inputFilename = path.join(fixturesDir, "input.js") 7 | // var expected = readFile(path.join(fixturesDir, "expected.js")) 8 | 9 | 10 | var pluginPath = path.join(__dirname, '../../babel-plugin-add-react-displayname') 11 | var assert = require('assert'); 12 | describe('add-react-displayname transform', function() { 13 | 14 | fs.readdirSync(fixturesDir).forEach(function (fixture) { 15 | var actual = transformFile(path.join(fixturesDir, fixture, 'input.js')) 16 | var expected = readFile((path.join(fixturesDir, fixture, 'expected.js'))) 17 | 18 | it('transforms ' + path.basename(fixture), function() { 19 | assert.equal(actual, expected) 20 | }) 21 | }) 22 | }); 23 | 24 | function readFile(filename) { 25 | var file = fs.readFileSync(filename, "utf8").trim() 26 | file = file.replace(/\r\n/g, "\n"); 27 | return file; 28 | } 29 | 30 | function transformFile(filename) { 31 | return babel.transformFileSync(filename, { 32 | presets: ['react', 'stage-1'], 33 | plugins: [ 34 | [pluginPath, {'knownComponents': ['Component5a', 'Component5b', 'Component5c']}], 35 | 'transform-decorators-legacy', 36 | ] 37 | }).code 38 | } 39 | -------------------------------------------------------------------------------- /test/fixtures/passThrough/expected.js: -------------------------------------------------------------------------------- 1 | // --------------------- 2 | // Should pass through unaltered 3 | // --------------------- 4 | var f1 = function ({ value }) { 5 | return "somestring"; 6 | }; 7 | 8 | function f2({ value }) { 9 | return "somestring"; 10 | } 11 | 12 | let f3 = class f3 { 13 | method1() { 14 | return "whatever"; 15 | } 16 | }; 17 | 18 | 19 | var f4 = React.createElement( 20 | "div", 21 | null, 22 | (() => React.createElement("span", null))() 23 | ); 24 | 25 | // Known component which doesn't sit directly on the `Program` node get left alone 26 | { 27 | var Component5c = function () { 28 | function Component5c() {} 29 | return Component5c; 30 | }(); 31 | } 32 | 33 | // --------------------- 34 | // Not supported 35 | // --------------------- 36 | 37 | // High-order things will be hard to catch 38 | var jsxChunk = React.createElement( 39 | "div", 40 | null, 41 | value 42 | ); 43 | function UnsupportedComponent1({ value }) { 44 | return function () { 45 | return jsxChunk; 46 | }; 47 | } 48 | 49 | var a = { 50 | smoke: function () {}, 51 | Component1d: function ({ value }) { 52 | return React.createElement( 53 | "div", 54 | null, 55 | value 56 | ); 57 | } 58 | }; 59 | 60 | var external = function () { 61 | var internal = function () { 62 | return React.createElement("div", null); 63 | }; 64 | return internal; 65 | }; 66 | -------------------------------------------------------------------------------- /test/fixtures/functionExpr/expected.js: -------------------------------------------------------------------------------- 1 | // Exported stateless componenet 2 | export function Component1a(value) { 3 | return React.createElement( 4 | "div", 5 | null, 6 | value 7 | ); 8 | } 9 | 10 | Component1a.displayName = "Component1a"; 11 | // Stateless componenet 12 | function Component1b(value) { 13 | return React.createElement( 14 | "div", 15 | null, 16 | value 17 | ); 18 | } 19 | 20 | Component1b.displayName = "Component1b"; 21 | // Stateless componenet used in a variable declaration 22 | var Component1c = function (value) { 23 | return React.createElement( 24 | "div", 25 | null, 26 | value 27 | ); 28 | }; 29 | 30 | Component1c.displayName = "Component1c"; 31 | // Exported named stateless component used in variable declaration 32 | export var Component1d = function (value) { 33 | return React.createElement( 34 | "div", 35 | null, 36 | value 37 | ); 38 | }; 39 | 40 | Component1d.displayName = "Component1d"; 41 | // Stateless componenet used in an assignment 42 | var Component1e; 43 | Component1e = function (value) { 44 | return React.createElement( 45 | "div", 46 | null, 47 | value 48 | ); 49 | }; 50 | 51 | Component1e.displayName = "Component1e"; 52 | // Exported default stateless *named* component used in variable declaration 53 | export default function Component1f(value) { 54 | return React.createElement( 55 | "div", 56 | null, 57 | value 58 | ); 59 | } 60 | Component1f.displayName = "Component1f"; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = transform; 2 | var pathMod = require('path') 3 | 4 | function transform (babel) { 5 | return { 6 | visitor: { 7 | ClassDeclaration: function (path, state) { 8 | if (classHasRenderMethod(path)) { 9 | setDisplayNameAfter(path, path.node.id, babel.types) 10 | } 11 | }, 12 | FunctionDeclaration: function (path, state) { 13 | if (doesReturnJSX(path.node.body) || (path.node.id && path.node.id.name && 14 | isKnownComponent(path.node.id.name, state.opts.knownComponents))) { 15 | var displayName 16 | if (path.parentPath.node.type === 'ExportDefaultDeclaration') { 17 | if (path.node.id == null) { 18 | // An anonymous function declaration in export default declaration. 19 | // Transform `export default function () { ... }` 20 | // to `var _uid1 = function () { .. }; export default __uid;` 21 | // then add displayName to _uid1 22 | var extension = pathMod.extname(state.file.opts.filename) 23 | var name = pathMod.basename(state.file.opts.filename, extension) 24 | 25 | var id = path.scope.generateUidIdentifier("uid"); 26 | path.node.id = id 27 | displayName = name 28 | } 29 | setDisplayNameAfter(path, path.node.id, babel.types, displayName) 30 | }else if(path.parentPath.node.type === 'Program' || path.parentPath.node.type == 'ExportNamedDeclaration') { 31 | setDisplayNameAfter(path, path.node.id, babel.types, displayName) 32 | } 33 | } 34 | }, 35 | FunctionExpression: function (path, state) { 36 | if(shouldSetDisplayNameForFuncExpr(path, state.opts.knownComponents)) { 37 | var id = findCandidateNameForExpression(path) 38 | if (id) { 39 | setDisplayNameAfter(path, id, babel.types) 40 | } 41 | } 42 | }, 43 | ArrowFunctionExpression: function (path, state) { 44 | if(shouldSetDisplayNameForFuncExpr(path, state.opts.knownComponents)) { 45 | var id = findCandidateNameForExpression(path) 46 | if (id) { 47 | setDisplayNameAfter(path, id, babel.types) 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | function isKnownComponent(name, knownComponents) { 56 | return (name && knownComponents && knownComponents.indexOf(name) > -1) 57 | } 58 | 59 | function componentNameFromFilename(filename) { 60 | var extension = pathMod.extname(filename); 61 | var name = pathMod.basename(filename, extension) 62 | return name 63 | } 64 | 65 | function shouldSetDisplayNameForFuncExpr(path, knownComponents) { 66 | // Parent must be either 'AssignmentExpression' or 'VariableDeclarator' or 'CallExpression' with a parent of 'VariableDeclarator' 67 | var id 68 | if (path.parentPath.node.type === 'AssignmentExpression' && 69 | path.parentPath.node.left.type !== 'MemberExpression' && // skip static members 70 | path.parentPath.parentPath.node.type == 'ExpressionStatement' && 71 | path.parentPath.parentPath.parentPath.node.type == 'Program') { 72 | id = path.parentPath.node.left 73 | }else{ 74 | // if parent is a call expression, we have something like (function () { .. })() 75 | // move up, past the call expression and run the rest of the checks as usual 76 | if(path.parentPath.node.type === 'CallExpression') { 77 | path = path.parentPath 78 | } 79 | 80 | if(path.parentPath.node.type === 'VariableDeclarator') { 81 | if (path.parentPath.parentPath.parentPath.node.type === 'ExportNamedDeclaration' || 82 | path.parentPath.parentPath.parentPath.node.type === 'Program') { 83 | id = path.parentPath.node.id 84 | } 85 | } 86 | } 87 | 88 | if (id) { 89 | if (id.name && isKnownComponent(id.name, knownComponents)) { 90 | return true 91 | } 92 | return doesReturnJSX(path.node.body) 93 | } 94 | 95 | return false 96 | } 97 | 98 | function classHasRenderMethod(path) { 99 | if(!path.node.body) { 100 | return false 101 | } 102 | var members = path.node.body.body 103 | for(var i = 0; i < members.length; i++) { 104 | if (members[i].type == 'ClassMethod' && members[i].key.name == 'render') { 105 | return true 106 | } 107 | } 108 | 109 | return false 110 | } 111 | 112 | // https://github.com/babel/babel/blob/master/packages/babel-plugin-transform-react-display-name/src/index.js#L62-L77 113 | // crawl up the ancestry looking for possible candidates for displayName inference 114 | function findCandidateNameForExpression(path) { 115 | var id 116 | path.find(function (path) { 117 | if (path.isAssignmentExpression()) { 118 | id = path.node.left; 119 | // } else if (path.isObjectProperty()) { 120 | // id = path.node.key; 121 | } else if (path.isVariableDeclarator()) { 122 | id = path.node.id; 123 | } else if (path.isStatement()) { 124 | // we've hit a statement, we should stop crawling up 125 | return true; 126 | } 127 | 128 | // we've got an id! no need to continue 129 | if (id) return true; 130 | }); 131 | return id 132 | } 133 | 134 | function doesReturnJSX (body) { 135 | if (!body) return false 136 | if (body.type === 'JSXElement') { 137 | return true 138 | } 139 | 140 | var block = body.body 141 | if (block && block.length) { 142 | var lastBlock = block.slice(0).pop() 143 | 144 | if (lastBlock.type === 'ReturnStatement') { 145 | return lastBlock.argument !== null && lastBlock.argument.type === 'JSXElement' 146 | } 147 | } 148 | 149 | return false 150 | } 151 | 152 | function setDisplayNameAfter(path, nameNodeId, t, displayName) { 153 | if (!displayName) { 154 | displayName = nameNodeId.name 155 | } 156 | 157 | var blockLevelStmnt 158 | path.find(function (path) { 159 | if (path.parentPath.isBlock()) { 160 | blockLevelStmnt = path 161 | return true 162 | } 163 | }) 164 | 165 | if (blockLevelStmnt) { 166 | var trailingComments = blockLevelStmnt.node.trailingComments 167 | delete blockLevelStmnt.node.trailingComments 168 | 169 | var setDisplayNameStmn = t.expressionStatement(t.assignmentExpression( 170 | '=', 171 | t.memberExpression(nameNodeId, t.identifier('displayName')), 172 | t.stringLiteral(displayName) 173 | )) 174 | 175 | blockLevelStmnt.insertAfter(setDisplayNameStmn) 176 | } 177 | } 178 | --------------------------------------------------------------------------------