diff --git a/packages/babel-plugin-transform-react-display-name/src/index.js b/packages/babel-plugin-transform-react-display-name/src/index.js index f19c4dd055..7c024563aa 100644 --- a/packages/babel-plugin-transform-react-display-name/src/index.js +++ b/packages/babel-plugin-transform-react-display-name/src/index.js @@ -2,29 +2,117 @@ import { declare } from "@babel/helper-plugin-utils"; import path from "path"; import { types as t } from "@babel/core"; -export default declare(api => { - api.assertVersion(7); +function addDisplayNameInCreateClass(id, call) { + const props = call.arguments[0].properties; + let safe = true; - function addDisplayName(id, call) { - const props = call.arguments[0].properties; - let safe = true; - - for (let i = 0; i < props.length; i++) { - const prop = props[i]; - const key = t.toComputedKey(prop); - if (t.isLiteral(key, { value: "displayName" })) { - safe = false; - break; - } - } - - if (safe) { - props.unshift( - t.objectProperty(t.identifier("displayName"), t.stringLiteral(id)), - ); + for (let i = 0; i < props.length; i++) { + const prop = props[i]; + const key = t.toComputedKey(prop); + if (t.isLiteral(key, { value: "displayName" })) { + safe = false; + break; } } + if (safe) { + props.unshift( + t.objectProperty(t.identifier("displayName"), t.stringLiteral(id)), + ); + } +} + +function getDisplayNameReferenceIdentifier( + path: NodePath, +): ?t.Identifier { + let id; + + // crawl up the ancestry looking for possible candidates for displayName inference + path.find(function (path) { + if (path.isAssignmentExpression()) { + id = path.node.left; + } else if (path.isObjectProperty()) { + id = path.node.key; + } else if (path.isVariableDeclarator()) { + id = path.node.id; + } else if (path.isStatement()) { + // we've hit a statement, we should stop crawling up + return true; + } + + // we've got an id! no need to continue + if (id) return true; + }); + + // ensure that we have an identifier we can inherit from + if (!id) return; + + // foo.bar -> bar + if (t.isMemberExpression(id)) { + id = id.property; + } + + // identifiers are the only thing we can reliably get a name from + if (!t.isIdentifier(id)) return; + + return id; +} + +function isCreateContext(node) { + let callee; + return ( + t.isCallExpression(node) && + t.isMemberExpression((callee = node.callee)) && + t.isIdentifier(callee.object, { name: "React" }) && + ((!callee.computed && + t.isIdentifier(callee.property, { name: "createContext" })) || + t.isStringLiteral(callee.property, { value: "createContext" })) + ); +} + +function buildDisplayNameAssignment(ref, displayName) { + return t.assignmentExpression( + "=", + t.memberExpression(t.cloneNode(ref), t.identifier("displayName")), + t.stringLiteral(displayName), + ); +} + +function addDisplayNameAfterCreateContext( + id, + path: t.NodePath, +) { + const { parentPath } = path; + if (parentPath.isVariableDeclarator()) { + // FooContext = React.createContext() + const ref = parentPath.node.id; + // parentPath.parentPath must be a VariableDeclaration because getDisplayNameReferenceIdentifier + // does not support patterns + parentPath.parentPath.insertAfter(buildDisplayNameAssignment(ref, id)); + } else if (parentPath.isAssignmentExpression()) { + // var FooContext = React.createContext() + const ref = parentPath.node.left; + parentPath.insertAfter(buildDisplayNameAssignment(ref, id)); + } else { + // (ref = React.createContext(), ref.displayName = "id", ref) + const { scope } = path; + const ref = scope.generateUidIdentifier("ref"); + scope.push({ id: ref }); + path.replaceWith( + t.sequenceExpression([ + t.assignmentExpression("=", t.cloneNode(ref), path.node), + buildDisplayNameAssignment(ref, id), + t.cloneNode(ref), + ]), + ); + } +} + +const createContextVisited = new WeakSet(); + +export default declare(api => { + api.assertVersion(7); + const isCreateClassCallExpression = t.buildMatchMemberExpression("React.createClass"); const isCreateClassAddon = callee => callee.name === "createReactClass"; @@ -66,44 +154,27 @@ export default declare(api => { displayName = path.basename(path.dirname(filename)); } - addDisplayName(displayName, node.declaration); + addDisplayNameInCreateClass(displayName, node.declaration); } }, CallExpression(path) { const { node } = path; - if (!isCreateClass(node)) return; - - let id; - - // crawl up the ancestry looking for possible candidates for displayName inference - path.find(function (path) { - if (path.isAssignmentExpression()) { - id = path.node.left; - } else if (path.isObjectProperty()) { - id = path.node.key; - } else if (path.isVariableDeclarator()) { - id = path.node.id; - } else if (path.isStatement()) { - // we've hit a statement, we should stop crawling up - return true; + if (isCreateClass(node)) { + const id = getDisplayNameReferenceIdentifier(path); + if (id) { + addDisplayNameInCreateClass(id.name, node); } + } else if (isCreateContext(node)) { + if (createContextVisited.has(node)) { + return; + } + createContextVisited.add(node); + const id = getDisplayNameReferenceIdentifier(path); - // we've got an id! no need to continue - if (id) return true; - }); - - // ensure that we have an identifier we can inherit from - if (!id) return; - - // foo.bar -> bar - if (t.isMemberExpression(id)) { - id = id.property; - } - - // identifiers are the only thing we can reliably get a name from - if (t.isIdentifier(id)) { - addDisplayName(id.name, node); + if (id) { + addDisplayNameAfterCreateContext(id.name, path); + } } }, }, diff --git a/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/assignment-expression-and-display-name/input.js b/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/assignment-expression-and-display-name/input.js new file mode 100644 index 0000000000..ee4c559729 --- /dev/null +++ b/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/assignment-expression-and-display-name/input.js @@ -0,0 +1,2 @@ +ThemeContext = React.createContext("light"); +ThemeContext.displayName = "CustomThemeContext"; diff --git a/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/assignment-expression-and-display-name/output.js b/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/assignment-expression-and-display-name/output.js new file mode 100644 index 0000000000..dd68935643 --- /dev/null +++ b/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/assignment-expression-and-display-name/output.js @@ -0,0 +1,3 @@ +ThemeContext = React.createContext("light"); +ThemeContext.displayName = "ThemeContext"; +ThemeContext.displayName = "CustomThemeContext"; diff --git a/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/assignment-expression/input.js b/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/assignment-expression/input.js new file mode 100644 index 0000000000..e75d864191 --- /dev/null +++ b/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/assignment-expression/input.js @@ -0,0 +1 @@ +ThemeContext = React.createContext("light"); diff --git a/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/assignment-expression/output.js b/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/assignment-expression/output.js new file mode 100644 index 0000000000..ab2bd9acd8 --- /dev/null +++ b/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/assignment-expression/output.js @@ -0,0 +1,2 @@ +ThemeContext = React.createContext("light"); +ThemeContext.displayName = "ThemeContext"; diff --git a/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/nested/input.js b/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/nested/input.js new file mode 100644 index 0000000000..ebc6657153 --- /dev/null +++ b/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/nested/input.js @@ -0,0 +1 @@ +var enhancedContext = qux(React.createContext("light")); diff --git a/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/nested/output.js b/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/nested/output.js new file mode 100644 index 0000000000..7eb898174a --- /dev/null +++ b/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/nested/output.js @@ -0,0 +1,3 @@ +var _ref; + +var enhancedContext = qux((_ref = React.createContext("light"), _ref.displayName = "enhancedContext", _ref)); diff --git a/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/object-property/input.js b/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/object-property/input.js new file mode 100644 index 0000000000..4e99a0ec8c --- /dev/null +++ b/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/object-property/input.js @@ -0,0 +1,3 @@ +({ + ThemeContext: React.createContext("light") +}); diff --git a/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/object-property/output.js b/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/object-property/output.js new file mode 100644 index 0000000000..fdc05dbe85 --- /dev/null +++ b/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/object-property/output.js @@ -0,0 +1,5 @@ +var _ref; + +({ + ThemeContext: (_ref = React.createContext("light"), _ref.displayName = "ThemeContext", _ref) +}); diff --git a/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/options.json b/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/options.json new file mode 100644 index 0000000000..e21e418ca9 --- /dev/null +++ b/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["transform-react-display-name"] +} diff --git a/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/variable-declarator/input.js b/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/variable-declarator/input.js new file mode 100644 index 0000000000..e79fa34777 --- /dev/null +++ b/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/variable-declarator/input.js @@ -0,0 +1 @@ +var ThemeContext = React.createContext("light"); diff --git a/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/variable-declarator/output.js b/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/variable-declarator/output.js new file mode 100644 index 0000000000..717112ea86 --- /dev/null +++ b/packages/babel-plugin-transform-react-display-name/test/fixtures/display-name-context/variable-declarator/output.js @@ -0,0 +1,2 @@ +var ThemeContext = React.createContext("light"); +ThemeContext.displayName = "ThemeContext"