123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462 |
- import {ContextualKeyword} from "../parser/tokenizer/keywords";
- 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 JestHoistTransformer from "./JestHoistTransformer";
- import JSXTransformer from "./JSXTransformer";
- import NumericSeparatorTransformer from "./NumericSeparatorTransformer";
- import OptionalCatchBindingTransformer from "./OptionalCatchBindingTransformer";
- import OptionalChainingNullishTransformer from "./OptionalChainingNullishTransformer";
- 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;
- this.helperManager = sucraseContext.helperManager;
- const {tokenProcessor, importProcessor} = sucraseContext;
- this.tokens = tokenProcessor;
- this.isImportsTransformEnabled = transforms.includes("imports");
- this.isReactHotLoaderTransformEnabled = transforms.includes("react-hot-loader");
- this.disableESTransforms = Boolean(options.disableESTransforms);
- if (!options.disableESTransforms) {
- this.transformers.push(
- new OptionalChainingNullishTransformer(tokenProcessor, this.nameManager),
- );
- this.transformers.push(new NumericSeparatorTransformer(tokenProcessor));
- this.transformers.push(new OptionalCatchBindingTransformer(tokenProcessor, this.nameManager));
- }
- if (transforms.includes("jsx")) {
- if (options.jsxRuntime !== "preserve") {
- 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,
- this.helperManager,
- reactHotLoaderTransformer,
- enableLegacyBabel5ModuleInterop,
- Boolean(options.enableLegacyTypeScriptModuleInterop),
- transforms.includes("typescript"),
- transforms.includes("flow"),
- Boolean(options.preserveDynamicImport),
- Boolean(options.keepUnusedImports),
- ),
- );
- } else {
- this.transformers.push(
- new ESMImportTransformer(
- tokenProcessor,
- this.nameManager,
- this.helperManager,
- reactHotLoaderTransformer,
- transforms.includes("typescript"),
- transforms.includes("flow"),
- Boolean(options.keepUnusedImports),
- options,
- ),
- );
- }
- if (transforms.includes("flow")) {
- this.transformers.push(
- new FlowTransformer(this, tokenProcessor, transforms.includes("imports")),
- );
- }
- if (transforms.includes("typescript")) {
- this.transformers.push(
- new TypeScriptTransformer(this, tokenProcessor, transforms.includes("imports")),
- );
- }
- if (transforms.includes("jest")) {
- this.transformers.push(
- new JestHoistTransformer(this, tokenProcessor, this.nameManager, importProcessor),
- );
- }
- }
- 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.helperManager.emitHelpers();
- prefix += this.generatedVariables.map((v) => ` var ${v};`).join("");
- for (const transformer of this.transformers) {
- prefix += transformer.getHoistedCode();
- }
- let suffix = "";
- for (const transformer of this.transformers) {
- suffix += transformer.getSuffixCode();
- }
- const result = this.tokens.finish();
- let {code} = result;
- if (code.startsWith("#!")) {
- let newlineIndex = code.indexOf("\n");
- if (newlineIndex === -1) {
- newlineIndex = code.length;
- code += "\n";
- }
- return {
- code: code.slice(0, newlineIndex + 1) + prefix + code.slice(newlineIndex + 1) + suffix,
- // The hashbang line has no tokens, so shifting the tokens to account
- // for prefix can happen normally.
- mappings: this.shiftMappings(result.mappings, prefix.length),
- };
- } else {
- return {
- code: prefix + code + suffix,
- mappings: this.shiftMappings(result.mappings, prefix.length),
- };
- }
- }
- 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, this.disableESTransforms);
- // 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.headerInfo.className) &&
- 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
- ) {
- if (this.tokens.currentIndex() < rangesToRemove[rangeToRemoveIndex].end) {
- 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;
- }
- /**
- * An async arrow function might be of the form:
- *
- * async <
- * T
- * >() => {}
- *
- * in which case, removing the type parameters will cause a syntax error. Detect this case and
- * move the open-paren earlier.
- */
- processPossibleAsyncArrowWithTypeParams() {
- if (
- !this.tokens.matchesContextual(ContextualKeyword._async) &&
- !this.tokens.matches1(tt._async)
- ) {
- return false;
- }
- const nextToken = this.tokens.tokenAtRelativeIndex(1);
- if (nextToken.type !== tt.lessThan || !nextToken.isType) {
- return false;
- }
- 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.parenL)) {
- this.tokens.replaceToken("async (");
- this.tokens.removeInitialToken();
- while (this.tokens.currentIndex() < nextNonTypeIndex) {
- this.tokens.removeToken();
- }
- this.tokens.removeToken();
- // We ate a ( token, so we need to process the tokens in between and then the ) token so that
- // we remain balanced.
- this.processBalancedCode();
- this.processToken();
- 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;
- }
- shiftMappings(
- mappings,
- prefixLength,
- ) {
- for (let i = 0; i < mappings.length; i++) {
- const mapping = mappings[i];
- if (mapping !== undefined) {
- mappings[i] = mapping + prefixLength;
- }
- }
- return mappings;
- }
- }
|