sucrase
Version:
Super-fast alternative to Babel for when you can target modern JS runtimes
122 lines (121 loc) • 5.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tokenizer_1 = require("../../sucrase-babylon/tokenizer");
const Transformer_1 = require("./Transformer");
/**
* Implementation of babel-plugin-transform-react-display-name, which adds a
* display name to usages of React.createClass and createReactClass.
*
* This implementation has the following limitations compared with the
* - It does not handle `export default React.createClass`, using the filename,
* since Sucrase currently does not know the name of the current file.
*/
class ReactDisplayNameTransformer extends Transformer_1.default {
constructor(rootTransformer, tokens, importProcessor) {
super();
this.rootTransformer = rootTransformer;
this.tokens = tokens;
this.importProcessor = importProcessor;
}
process() {
const startIndex = this.tokens.currentIndex();
if (this.tokens.matchesName("createReactClass")) {
const newName = this.importProcessor.getIdentifierReplacement("createReactClass");
if (newName) {
this.tokens.replaceToken(`(0, ${newName})`);
}
else {
this.tokens.copyToken();
}
this.tryProcessCreateClassCall(startIndex);
return true;
}
if (this.tokens.matches(["name", ".", "name"]) &&
this.tokens.matchesName("React") &&
this.tokens.matchesNameAtIndex(this.tokens.currentIndex() + 2, "createClass")) {
const newName = this.importProcessor.getIdentifierReplacement("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("(");
this.tokens.copyExpectedToken("{");
this.tokens.appendCode(`displayName: '${displayName}',`);
this.rootTransformer.processBalancedCode();
this.tokens.copyExpectedToken("}");
this.tokens.copyExpectedToken(")");
}
}
findDisplayName(startIndex) {
if (this.tokens.matchesAtIndex(startIndex - 2, ["name", "="]) &&
!this.tokens.matchesAtIndex(startIndex - 3, ["."])) {
// This is an assignment (or declaration) with an identifier LHS, so use
// that identifier name.
return this.tokens.tokens[startIndex - 2].value;
}
if (this.tokens.tokens[startIndex - 2].identifierRole === tokenizer_1.IdentifierRole.ObjectKey) {
// This is an object literal value.
return this.tokens.tokens[startIndex - 2].value;
}
return null;
}
/**
* 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.matches(["(", "{"])) {
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.label === "}" && token.contextId === objectContextId) {
index++;
break;
}
if (this.tokens.matchesNameAtIndex(index, "displayName") &&
this.tokens.tokens[index].identifierRole === tokenizer_1.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.matchesAtIndex(index, [")"]) || this.tokens.matchesAtIndex(index, [",", ")"]));
}
}
exports.default = ReactDisplayNameTransformer;