sucrase
Version:
Super-fast alternative to Babel for when you can target modern JS runtimes
201 lines (200 loc) • 8.47 kB
JavaScript
import getClassInfo from "../util/getClassInfo";
import FlowTransformer from "./FlowTransformer";
import ImportTransformer from "./ImportTransformer";
import JSXTransformer from "./JSXTransformer";
import NumericSeparatorTransformer from "./NumericSeparatorTransformer";
import OptionalCatchBindingTransformer from "./OptionalCatchBindingTransformer";
import ReactDisplayNameTransformer from "./ReactDisplayNameTransformer";
import TypeScriptTransformer from "./TypeScriptTransformer";
export default class RootTransformer {
constructor(sucraseContext, transforms) {
this.transformers = [];
this.generatedVariables = [];
this.nameManager = sucraseContext.nameManager;
const { tokenProcessor, importProcessor } = sucraseContext;
this.tokens = tokenProcessor;
this.transformers.push(new NumericSeparatorTransformer(tokenProcessor));
this.transformers.push(new OptionalCatchBindingTransformer(tokenProcessor, this.nameManager));
if (transforms.includes("jsx")) {
this.transformers.push(new JSXTransformer(this, tokenProcessor, importProcessor));
}
// react-display-name must come before imports since otherwise imports will
// claim a normal `React` name token.
if (transforms.includes("react-display-name")) {
this.transformers.push(new ReactDisplayNameTransformer(this, tokenProcessor, importProcessor));
}
if (transforms.includes("imports")) {
const shouldAddModuleExports = transforms.includes("add-module-exports");
this.transformers.push(new ImportTransformer(this, tokenProcessor, importProcessor, shouldAddModuleExports));
}
if (transforms.includes("flow")) {
this.transformers.push(new FlowTransformer(this, tokenProcessor));
}
if (transforms.includes("typescript")) {
if (!transforms.includes("imports")) {
throw new Error("The TypeScript transform without the import transform is not yet supported.");
}
this.transformers.push(new TypeScriptTransformer(this, tokenProcessor));
}
}
transform() {
this.tokens.reset();
this.processBalancedCode();
let prefix = "";
for (const transformer of this.transformers) {
prefix += transformer.getPrefixCode();
}
prefix += this.generatedVariables.map((v) => ` var ${v};`).join("");
let suffix = "";
for (const transformer of this.transformers) {
suffix += transformer.getSuffixCode();
}
let code = this.tokens.finish();
if (code.startsWith("#!")) {
let newlineIndex = code.indexOf("\n");
if (newlineIndex === -1) {
newlineIndex = code.length;
code += "\n";
}
return code.slice(0, newlineIndex + 1) + prefix + code.slice(newlineIndex + 1) + suffix;
}
else {
return prefix + this.tokens.finish() + suffix;
}
}
processBalancedCode() {
let braceDepth = 0;
let parenDepth = 0;
while (!this.tokens.isAtEnd()) {
let wasProcessed = false;
for (const transformer of this.transformers) {
wasProcessed = transformer.process();
if (wasProcessed) {
break;
}
}
if (!wasProcessed) {
if (this.tokens.matches(["{"]) || this.tokens.matches(["${"])) {
braceDepth++;
}
else if (this.tokens.matches(["}"])) {
if (braceDepth === 0) {
return;
}
braceDepth--;
}
if (this.tokens.matches(["("])) {
parenDepth++;
}
else if (this.tokens.matches([")"])) {
if (parenDepth === 0) {
return;
}
parenDepth--;
}
this.tokens.copyToken();
}
}
}
processToken() {
for (const transformer of this.transformers) {
const wasProcessed = transformer.process();
if (wasProcessed) {
return;
}
}
this.tokens.copyToken();
}
/**
* Skip past a class with a name and return that name.
*/
processNamedClass() {
if (!this.tokens.matches(["class", "name"])) {
throw new Error("Expected identifier for exported class name.");
}
const name = this.tokens.tokens[this.tokens.currentIndex() + 1].value;
this.processClass();
return name;
}
processClass() {
const classInfo = getClassInfo(this, this.tokens);
const needsCommaExpression = classInfo.headerInfo.isExpression && classInfo.staticInitializerSuffixes.length > 0;
let className = classInfo.headerInfo.className;
if (needsCommaExpression) {
className = this.nameManager.claimFreeName("_class");
this.generatedVariables.push(className);
this.tokens.appendCode(` (${className} =`);
}
const classToken = this.tokens.currentToken();
const contextId = classToken.contextId;
if (contextId == null) {
throw new Error("Expected class to have a context ID.");
}
this.tokens.copyExpectedToken("class");
while (!this.tokens.matchesContextIdAndLabel("{", contextId)) {
this.processToken();
}
this.processClassBody(classInfo);
const staticInitializerStatements = classInfo.staticInitializerSuffixes.map((suffix) => `${className}${suffix}`);
if (needsCommaExpression) {
this.tokens.appendCode(`, ${staticInitializerStatements.join(", ")}, ${className})`);
}
else if (classInfo.staticInitializerSuffixes.length > 0) {
this.tokens.appendCode(` ${staticInitializerStatements.join("; ")};`);
}
}
/**
* We want to just handle class fields in all contexts, since TypeScript supports them. Later,
* when some JS implementations support class fields, this should be made optional.
*/
processClassBody(classInfo) {
const { headerInfo, constructorInsertPos, initializerStatements, fieldRanges } = classInfo;
let fieldIndex = 0;
const classContextId = this.tokens.currentToken().contextId;
if (classContextId == null) {
throw new Error("Expected non-null context ID on class.");
}
this.tokens.copyExpectedToken("{");
if (constructorInsertPos === null && initializerStatements.length > 0) {
const initializersCode = initializerStatements.join(";");
if (headerInfo.hasSuperclass) {
const argsName = this.nameManager.claimFreeName("args");
this.tokens.appendCode(`constructor(...${argsName}) { super(...${argsName}); ${initializersCode}; }`);
}
else {
this.tokens.appendCode(`constructor() { ${initializersCode}; }`);
}
}
while (!this.tokens.matchesContextIdAndLabel("}", classContextId)) {
if (fieldIndex < fieldRanges.length &&
this.tokens.currentIndex() === fieldRanges[fieldIndex].start) {
this.tokens.removeInitialToken();
while (this.tokens.currentIndex() < fieldRanges[fieldIndex].end) {
this.tokens.removeToken();
}
fieldIndex++;
}
else if (this.tokens.currentIndex() === constructorInsertPos) {
this.tokens.copyToken();
if (initializerStatements.length > 0) {
this.tokens.appendCode(`;${initializerStatements.join(";")};`);
}
this.processToken();
}
else {
this.processToken();
}
}
this.tokens.copyExpectedToken("}");
}
processPossibleTypeRange() {
if (this.tokens.currentToken().isType) {
this.tokens.removeInitialToken();
while (this.tokens.currentToken().isType) {
this.tokens.removeToken();
}
return true;
}
return false;
}
}