UNPKG

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