161 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			161 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
 | 
						|
 | 
						|
import {IdentifierRole} from "../parser/tokenizer";
 | 
						|
import {TokenType as tt} from "../parser/tokenizer/types";
 | 
						|
 | 
						|
 | 
						|
import Transformer from "./Transformer";
 | 
						|
 | 
						|
/**
 | 
						|
 * Implementation of babel-plugin-transform-react-display-name, which adds a
 | 
						|
 * display name to usages of React.createClass and createReactClass.
 | 
						|
 */
 | 
						|
export default class ReactDisplayNameTransformer extends Transformer {
 | 
						|
  constructor(
 | 
						|
     rootTransformer,
 | 
						|
     tokens,
 | 
						|
     importProcessor,
 | 
						|
     options,
 | 
						|
  ) {
 | 
						|
    super();this.rootTransformer = rootTransformer;this.tokens = tokens;this.importProcessor = importProcessor;this.options = options;;
 | 
						|
  }
 | 
						|
 | 
						|
  process() {
 | 
						|
    const startIndex = this.tokens.currentIndex();
 | 
						|
    if (this.tokens.identifierName() === "createReactClass") {
 | 
						|
      const newName =
 | 
						|
        this.importProcessor && this.importProcessor.getIdentifierReplacement("createReactClass");
 | 
						|
      if (newName) {
 | 
						|
        this.tokens.replaceToken(`(0, ${newName})`);
 | 
						|
      } else {
 | 
						|
        this.tokens.copyToken();
 | 
						|
      }
 | 
						|
      this.tryProcessCreateClassCall(startIndex);
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
    if (
 | 
						|
      this.tokens.matches3(tt.name, tt.dot, tt.name) &&
 | 
						|
      this.tokens.identifierName() === "React" &&
 | 
						|
      this.tokens.identifierNameAtIndex(this.tokens.currentIndex() + 2) === "createClass"
 | 
						|
    ) {
 | 
						|
      const newName = this.importProcessor
 | 
						|
        ? this.importProcessor.getIdentifierReplacement("React") || "React"
 | 
						|
        : "React";
 | 
						|
      if (newName) {
 | 
						|
        this.tokens.replaceToken(newName);
 | 
						|
        this.tokens.copyToken();
 | 
						|
        this.tokens.copyToken();
 | 
						|
      } else {
 | 
						|
        this.tokens.copyToken();
 | 
						|
        this.tokens.copyToken();
 | 
						|
        this.tokens.copyToken();
 | 
						|
      }
 | 
						|
      this.tryProcessCreateClassCall(startIndex);
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * This is called with the token position at the open-paren.
 | 
						|
   */
 | 
						|
   tryProcessCreateClassCall(startIndex) {
 | 
						|
    const displayName = this.findDisplayName(startIndex);
 | 
						|
    if (!displayName) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (this.classNeedsDisplayName()) {
 | 
						|
      this.tokens.copyExpectedToken(tt.parenL);
 | 
						|
      this.tokens.copyExpectedToken(tt.braceL);
 | 
						|
      this.tokens.appendCode(`displayName: '${displayName}',`);
 | 
						|
      this.rootTransformer.processBalancedCode();
 | 
						|
      this.tokens.copyExpectedToken(tt.braceR);
 | 
						|
      this.tokens.copyExpectedToken(tt.parenR);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
   findDisplayName(startIndex) {
 | 
						|
    if (startIndex < 2) {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
    if (this.tokens.matches2AtIndex(startIndex - 2, tt.name, tt.eq)) {
 | 
						|
      // This is an assignment (or declaration) and the LHS is either an identifier or a member
 | 
						|
      // expression ending in an identifier, so use that identifier name.
 | 
						|
      return this.tokens.identifierNameAtIndex(startIndex - 2);
 | 
						|
    }
 | 
						|
    if (
 | 
						|
      startIndex >= 2 &&
 | 
						|
      this.tokens.tokens[startIndex - 2].identifierRole === IdentifierRole.ObjectKey
 | 
						|
    ) {
 | 
						|
      // This is an object literal value.
 | 
						|
      return this.tokens.identifierNameAtIndex(startIndex - 2);
 | 
						|
    }
 | 
						|
    if (this.tokens.matches2AtIndex(startIndex - 2, tt._export, tt._default)) {
 | 
						|
      return this.getDisplayNameFromFilename();
 | 
						|
    }
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
 | 
						|
   getDisplayNameFromFilename() {
 | 
						|
    const filePath = this.options.filePath || "unknown";
 | 
						|
    const pathSegments = filePath.split("/");
 | 
						|
    const filename = pathSegments[pathSegments.length - 1];
 | 
						|
    const dotIndex = filename.lastIndexOf(".");
 | 
						|
    const baseFilename = dotIndex === -1 ? filename : filename.slice(0, dotIndex);
 | 
						|
    if (baseFilename === "index" && pathSegments[pathSegments.length - 2]) {
 | 
						|
      return pathSegments[pathSegments.length - 2];
 | 
						|
    } else {
 | 
						|
      return baseFilename;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * We only want to add a display name when this is a function call containing
 | 
						|
   * one argument, which is an object literal without `displayName` as an
 | 
						|
   * existing key.
 | 
						|
   */
 | 
						|
   classNeedsDisplayName() {
 | 
						|
    let index = this.tokens.currentIndex();
 | 
						|
    if (!this.tokens.matches2(tt.parenL, tt.braceL)) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    // The block starts on the {, and we expect any displayName key to be in
 | 
						|
    // that context. We need to ignore other other contexts to avoid matching
 | 
						|
    // nested displayName keys.
 | 
						|
    const objectStartIndex = index + 1;
 | 
						|
    const objectContextId = this.tokens.tokens[objectStartIndex].contextId;
 | 
						|
    if (objectContextId == null) {
 | 
						|
      throw new Error("Expected non-null context ID on object open-brace.");
 | 
						|
    }
 | 
						|
 | 
						|
    for (; index < this.tokens.tokens.length; index++) {
 | 
						|
      const token = this.tokens.tokens[index];
 | 
						|
      if (token.type === tt.braceR && token.contextId === objectContextId) {
 | 
						|
        index++;
 | 
						|
        break;
 | 
						|
      }
 | 
						|
 | 
						|
      if (
 | 
						|
        this.tokens.identifierNameAtIndex(index) === "displayName" &&
 | 
						|
        this.tokens.tokens[index].identifierRole === IdentifierRole.ObjectKey &&
 | 
						|
        token.contextId === objectContextId
 | 
						|
      ) {
 | 
						|
        // We found a displayName key, so bail out.
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (index === this.tokens.tokens.length) {
 | 
						|
      throw new Error("Unexpected end of input when processing React class.");
 | 
						|
    }
 | 
						|
 | 
						|
    // If we got this far, we know we have createClass with an object with no
 | 
						|
    // display name, so we want to proceed as long as that was the only argument.
 | 
						|
    return (
 | 
						|
      this.tokens.matches1AtIndex(index, tt.parenR) ||
 | 
						|
      this.tokens.matches2AtIndex(index, tt.comma, tt.parenR)
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 |