UNPKG

sucrase

Version:

Super-fast alternative to Babel for when you can target modern JS runtimes

353 lines (321 loc) 12.2 kB
import {TokenType as tt} from "../parser/tokenizer/types"; import getClassInfo, {} from "../util/getClassInfo"; import CJSImportTransformer from "./CJSImportTransformer"; import ESMImportTransformer from "./ESMImportTransformer"; import FlowTransformer from "./FlowTransformer"; import JSXTransformer from "./JSXTransformer"; import NumericSeparatorTransformer from "./NumericSeparatorTransformer"; import OptionalCatchBindingTransformer from "./OptionalCatchBindingTransformer"; import ReactDisplayNameTransformer from "./ReactDisplayNameTransformer"; import ReactHotLoaderTransformer from "./ReactHotLoaderTransformer"; import TypeScriptTransformer from "./TypeScriptTransformer"; export default class RootTransformer { __init() {this.transformers = []} __init2() {this.generatedVariables = []} constructor( sucraseContext, transforms, enableLegacyBabel5ModuleInterop, options, ) {;RootTransformer.prototype.__init.call(this);RootTransformer.prototype.__init2.call(this); this.nameManager = sucraseContext.nameManager; const {tokenProcessor, importProcessor} = sucraseContext; this.tokens = tokenProcessor; this.isImportsTransformEnabled = transforms.includes("imports"); this.isReactHotLoaderTransformEnabled = transforms.includes("react-hot-loader"); 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, this.nameManager, options), ); this.transformers.push( new ReactDisplayNameTransformer(this, tokenProcessor, importProcessor, options), ); } let reactHotLoaderTransformer = null; if (transforms.includes("react-hot-loader")) { if (!options.filePath) { throw new Error("filePath is required when using the react-hot-loader transform."); } reactHotLoaderTransformer = new ReactHotLoaderTransformer(tokenProcessor, options.filePath); this.transformers.push(reactHotLoaderTransformer); } // Note that we always want to enable the imports transformer, even when the import transform // itself isn't enabled, since we need to do type-only import pruning for both Flow and // TypeScript. if (transforms.includes("imports")) { if (importProcessor === null) { throw new Error("Expected non-null importProcessor with imports transform enabled."); } this.transformers.push( new CJSImportTransformer( this, tokenProcessor, importProcessor, this.nameManager, reactHotLoaderTransformer, enableLegacyBabel5ModuleInterop, ), ); } else { this.transformers.push( new ESMImportTransformer( tokenProcessor, this.nameManager, reactHotLoaderTransformer, transforms.includes("typescript"), options, ), ); } if (transforms.includes("flow")) { this.transformers.push(new FlowTransformer(this, tokenProcessor)); } if (transforms.includes("typescript")) { this.transformers.push( new TypeScriptTransformer(this, tokenProcessor, transforms.includes("imports")), ); } } transform() { this.tokens.reset(); this.processBalancedCode(); const shouldAddUseStrict = this.isImportsTransformEnabled; // "use strict" always needs to be first, so override the normal transformer order. let prefix = shouldAddUseStrict ? '"use strict";' : ""; 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()) { if (this.tokens.matches1(tt.braceL) || this.tokens.matches1(tt.dollarBraceL)) { braceDepth++; } else if (this.tokens.matches1(tt.braceR)) { if (braceDepth === 0) { return; } braceDepth--; } if (this.tokens.matches1(tt.parenL)) { parenDepth++; } else if (this.tokens.matches1(tt.parenR)) { if (parenDepth === 0) { return; } parenDepth--; } this.processToken(); } } processToken() { if (this.tokens.matches1(tt._class)) { this.processClass(); return; } 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.matches2(tt._class, tt.name)) { throw new Error("Expected identifier for exported class name."); } const name = this.tokens.identifierNameAtIndex(this.tokens.currentIndex() + 1); this.processClass(); return name; } processClass() { const classInfo = getClassInfo(this, this.tokens, this.nameManager); // Both static and instance initializers need a class name to use to invoke the initializer, so // assign to one if necessary. const needsCommaExpression = classInfo.headerInfo.isExpression && classInfo.staticInitializerNames.length + classInfo.instanceInitializerNames.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(tt._class); while (!this.tokens.matchesContextIdAndLabel(tt.braceL, contextId)) { this.processToken(); } this.processClassBody(classInfo, className); const staticInitializerStatements = classInfo.staticInitializerNames.map( (name) => `${className}.${name}()`, ); if (needsCommaExpression) { this.tokens.appendCode( `, ${staticInitializerStatements.map((s) => `${s}, `).join("")}${className})`, ); } else if (classInfo.staticInitializerNames.length > 0) { this.tokens.appendCode(` ${staticInitializerStatements.map((s) => `${s};`).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, className) { const { headerInfo, constructorInsertPos, constructorInitializerStatements, fields, instanceInitializerNames, rangesToRemove, } = classInfo; let fieldIndex = 0; let rangeToRemoveIndex = 0; const classContextId = this.tokens.currentToken().contextId; if (classContextId == null) { throw new Error("Expected non-null context ID on class."); } this.tokens.copyExpectedToken(tt.braceL); if (this.isReactHotLoaderTransformEnabled) { this.tokens.appendCode( "__reactstandin__regenerateByEval(key, code) {this[key] = eval(code);}", ); } const needsConstructorInit = constructorInitializerStatements.length + instanceInitializerNames.length > 0; if (constructorInsertPos === null && needsConstructorInit) { const constructorInitializersCode = this.makeConstructorInitCode( constructorInitializerStatements, instanceInitializerNames, className, ); if (headerInfo.hasSuperclass) { const argsName = this.nameManager.claimFreeName("args"); this.tokens.appendCode( `constructor(...${argsName}) { super(...${argsName}); ${constructorInitializersCode}; }`, ); } else { this.tokens.appendCode(`constructor() { ${constructorInitializersCode}; }`); } } while (!this.tokens.matchesContextIdAndLabel(tt.braceR, classContextId)) { if (fieldIndex < fields.length && this.tokens.currentIndex() === fields[fieldIndex].start) { let needsCloseBrace = false; if (this.tokens.matches1(tt.bracketL)) { this.tokens.copyTokenWithPrefix(`${fields[fieldIndex].initializerName}() {this`); } else if (this.tokens.matches1(tt.string) || this.tokens.matches1(tt.num)) { this.tokens.copyTokenWithPrefix(`${fields[fieldIndex].initializerName}() {this[`); needsCloseBrace = true; } else { this.tokens.copyTokenWithPrefix(`${fields[fieldIndex].initializerName}() {this.`); } while (this.tokens.currentIndex() < fields[fieldIndex].end) { if (needsCloseBrace && this.tokens.currentIndex() === fields[fieldIndex].equalsIndex) { this.tokens.appendCode("]"); } this.processToken(); } this.tokens.appendCode("}"); fieldIndex++; } else if ( rangeToRemoveIndex < rangesToRemove.length && this.tokens.currentIndex() === rangesToRemove[rangeToRemoveIndex].start ) { this.tokens.removeInitialToken(); while (this.tokens.currentIndex() < rangesToRemove[rangeToRemoveIndex].end) { this.tokens.removeToken(); } rangeToRemoveIndex++; } else if (this.tokens.currentIndex() === constructorInsertPos) { this.tokens.copyToken(); if (needsConstructorInit) { this.tokens.appendCode( `;${this.makeConstructorInitCode( constructorInitializerStatements, instanceInitializerNames, className, )};`, ); } this.processToken(); } else { this.processToken(); } } this.tokens.copyExpectedToken(tt.braceR); } makeConstructorInitCode( constructorInitializerStatements, instanceInitializerNames, className, ) { return [ ...constructorInitializerStatements, ...instanceInitializerNames.map((name) => `${className}.prototype.${name}.call(this)`), ].join(";"); } /** * Normally it's ok to simply remove type tokens, but we need to be more careful when dealing with * arrow function return types since they can confuse the parser. In that case, we want to move * the close-paren to the same line as the arrow. * * See https://github.com/alangpierce/sucrase/issues/391 for more details. */ processPossibleArrowParamEnd() { if (this.tokens.matches2(tt.parenR, tt.colon) && this.tokens.tokenAtRelativeIndex(1).isType) { let nextNonTypeIndex = this.tokens.currentIndex() + 1; // Look ahead to see if this is an arrow function or something else. while (this.tokens.tokens[nextNonTypeIndex].isType) { nextNonTypeIndex++; } if (this.tokens.matches1AtIndex(nextNonTypeIndex, tt.arrow)) { this.tokens.removeInitialToken(); while (this.tokens.currentIndex() < nextNonTypeIndex) { this.tokens.removeToken(); } this.tokens.replaceTokenTrimmingLeftWhitespace(") =>"); return true; } } return false; } processPossibleTypeRange() { if (this.tokens.currentToken().isType) { this.tokens.removeInitialToken(); while (this.tokens.currentToken().isType) { this.tokens.removeToken(); } return true; } return false; } }