├── 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 |
--------------------------------------------------------------------------------