ReactDisplayNameTransformer.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  2. var _tokenizer = require('../parser/tokenizer');
  3. var _types = require('../parser/tokenizer/types');
  4. var _Transformer = require('./Transformer'); var _Transformer2 = _interopRequireDefault(_Transformer);
  5. /**
  6. * Implementation of babel-plugin-transform-react-display-name, which adds a
  7. * display name to usages of React.createClass and createReactClass.
  8. */
  9. class ReactDisplayNameTransformer extends _Transformer2.default {
  10. constructor(
  11. rootTransformer,
  12. tokens,
  13. importProcessor,
  14. options,
  15. ) {
  16. super();this.rootTransformer = rootTransformer;this.tokens = tokens;this.importProcessor = importProcessor;this.options = options;;
  17. }
  18. process() {
  19. const startIndex = this.tokens.currentIndex();
  20. if (this.tokens.identifierName() === "createReactClass") {
  21. const newName =
  22. this.importProcessor && this.importProcessor.getIdentifierReplacement("createReactClass");
  23. if (newName) {
  24. this.tokens.replaceToken(`(0, ${newName})`);
  25. } else {
  26. this.tokens.copyToken();
  27. }
  28. this.tryProcessCreateClassCall(startIndex);
  29. return true;
  30. }
  31. if (
  32. this.tokens.matches3(_types.TokenType.name, _types.TokenType.dot, _types.TokenType.name) &&
  33. this.tokens.identifierName() === "React" &&
  34. this.tokens.identifierNameAtIndex(this.tokens.currentIndex() + 2) === "createClass"
  35. ) {
  36. const newName = this.importProcessor
  37. ? this.importProcessor.getIdentifierReplacement("React") || "React"
  38. : "React";
  39. if (newName) {
  40. this.tokens.replaceToken(newName);
  41. this.tokens.copyToken();
  42. this.tokens.copyToken();
  43. } else {
  44. this.tokens.copyToken();
  45. this.tokens.copyToken();
  46. this.tokens.copyToken();
  47. }
  48. this.tryProcessCreateClassCall(startIndex);
  49. return true;
  50. }
  51. return false;
  52. }
  53. /**
  54. * This is called with the token position at the open-paren.
  55. */
  56. tryProcessCreateClassCall(startIndex) {
  57. const displayName = this.findDisplayName(startIndex);
  58. if (!displayName) {
  59. return;
  60. }
  61. if (this.classNeedsDisplayName()) {
  62. this.tokens.copyExpectedToken(_types.TokenType.parenL);
  63. this.tokens.copyExpectedToken(_types.TokenType.braceL);
  64. this.tokens.appendCode(`displayName: '${displayName}',`);
  65. this.rootTransformer.processBalancedCode();
  66. this.tokens.copyExpectedToken(_types.TokenType.braceR);
  67. this.tokens.copyExpectedToken(_types.TokenType.parenR);
  68. }
  69. }
  70. findDisplayName(startIndex) {
  71. if (startIndex < 2) {
  72. return null;
  73. }
  74. if (this.tokens.matches2AtIndex(startIndex - 2, _types.TokenType.name, _types.TokenType.eq)) {
  75. // This is an assignment (or declaration) and the LHS is either an identifier or a member
  76. // expression ending in an identifier, so use that identifier name.
  77. return this.tokens.identifierNameAtIndex(startIndex - 2);
  78. }
  79. if (
  80. startIndex >= 2 &&
  81. this.tokens.tokens[startIndex - 2].identifierRole === _tokenizer.IdentifierRole.ObjectKey
  82. ) {
  83. // This is an object literal value.
  84. return this.tokens.identifierNameAtIndex(startIndex - 2);
  85. }
  86. if (this.tokens.matches2AtIndex(startIndex - 2, _types.TokenType._export, _types.TokenType._default)) {
  87. return this.getDisplayNameFromFilename();
  88. }
  89. return null;
  90. }
  91. getDisplayNameFromFilename() {
  92. const filePath = this.options.filePath || "unknown";
  93. const pathSegments = filePath.split("/");
  94. const filename = pathSegments[pathSegments.length - 1];
  95. const dotIndex = filename.lastIndexOf(".");
  96. const baseFilename = dotIndex === -1 ? filename : filename.slice(0, dotIndex);
  97. if (baseFilename === "index" && pathSegments[pathSegments.length - 2]) {
  98. return pathSegments[pathSegments.length - 2];
  99. } else {
  100. return baseFilename;
  101. }
  102. }
  103. /**
  104. * We only want to add a display name when this is a function call containing
  105. * one argument, which is an object literal without `displayName` as an
  106. * existing key.
  107. */
  108. classNeedsDisplayName() {
  109. let index = this.tokens.currentIndex();
  110. if (!this.tokens.matches2(_types.TokenType.parenL, _types.TokenType.braceL)) {
  111. return false;
  112. }
  113. // The block starts on the {, and we expect any displayName key to be in
  114. // that context. We need to ignore other other contexts to avoid matching
  115. // nested displayName keys.
  116. const objectStartIndex = index + 1;
  117. const objectContextId = this.tokens.tokens[objectStartIndex].contextId;
  118. if (objectContextId == null) {
  119. throw new Error("Expected non-null context ID on object open-brace.");
  120. }
  121. for (; index < this.tokens.tokens.length; index++) {
  122. const token = this.tokens.tokens[index];
  123. if (token.type === _types.TokenType.braceR && token.contextId === objectContextId) {
  124. index++;
  125. break;
  126. }
  127. if (
  128. this.tokens.identifierNameAtIndex(index) === "displayName" &&
  129. this.tokens.tokens[index].identifierRole === _tokenizer.IdentifierRole.ObjectKey &&
  130. token.contextId === objectContextId
  131. ) {
  132. // We found a displayName key, so bail out.
  133. return false;
  134. }
  135. }
  136. if (index === this.tokens.tokens.length) {
  137. throw new Error("Unexpected end of input when processing React class.");
  138. }
  139. // If we got this far, we know we have createClass with an object with no
  140. // display name, so we want to proceed as long as that was the only argument.
  141. return (
  142. this.tokens.matches1AtIndex(index, _types.TokenType.parenR) ||
  143. this.tokens.matches2AtIndex(index, _types.TokenType.comma, _types.TokenType.parenR)
  144. );
  145. }
  146. } exports.default = ReactDisplayNameTransformer;