RootTransformer.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. import {ContextualKeyword} from "../parser/tokenizer/keywords";
  2. import {TokenType as tt} from "../parser/tokenizer/types";
  3. import getClassInfo, {} from "../util/getClassInfo";
  4. import CJSImportTransformer from "./CJSImportTransformer";
  5. import ESMImportTransformer from "./ESMImportTransformer";
  6. import FlowTransformer from "./FlowTransformer";
  7. import JestHoistTransformer from "./JestHoistTransformer";
  8. import JSXTransformer from "./JSXTransformer";
  9. import NumericSeparatorTransformer from "./NumericSeparatorTransformer";
  10. import OptionalCatchBindingTransformer from "./OptionalCatchBindingTransformer";
  11. import OptionalChainingNullishTransformer from "./OptionalChainingNullishTransformer";
  12. import ReactDisplayNameTransformer from "./ReactDisplayNameTransformer";
  13. import ReactHotLoaderTransformer from "./ReactHotLoaderTransformer";
  14. import TypeScriptTransformer from "./TypeScriptTransformer";
  15. export default class RootTransformer {
  16. __init() {this.transformers = []}
  17. __init2() {this.generatedVariables = []}
  18. constructor(
  19. sucraseContext,
  20. transforms,
  21. enableLegacyBabel5ModuleInterop,
  22. options,
  23. ) {;RootTransformer.prototype.__init.call(this);RootTransformer.prototype.__init2.call(this);
  24. this.nameManager = sucraseContext.nameManager;
  25. this.helperManager = sucraseContext.helperManager;
  26. const {tokenProcessor, importProcessor} = sucraseContext;
  27. this.tokens = tokenProcessor;
  28. this.isImportsTransformEnabled = transforms.includes("imports");
  29. this.isReactHotLoaderTransformEnabled = transforms.includes("react-hot-loader");
  30. this.disableESTransforms = Boolean(options.disableESTransforms);
  31. if (!options.disableESTransforms) {
  32. this.transformers.push(
  33. new OptionalChainingNullishTransformer(tokenProcessor, this.nameManager),
  34. );
  35. this.transformers.push(new NumericSeparatorTransformer(tokenProcessor));
  36. this.transformers.push(new OptionalCatchBindingTransformer(tokenProcessor, this.nameManager));
  37. }
  38. if (transforms.includes("jsx")) {
  39. if (options.jsxRuntime !== "preserve") {
  40. this.transformers.push(
  41. new JSXTransformer(this, tokenProcessor, importProcessor, this.nameManager, options),
  42. );
  43. }
  44. this.transformers.push(
  45. new ReactDisplayNameTransformer(this, tokenProcessor, importProcessor, options),
  46. );
  47. }
  48. let reactHotLoaderTransformer = null;
  49. if (transforms.includes("react-hot-loader")) {
  50. if (!options.filePath) {
  51. throw new Error("filePath is required when using the react-hot-loader transform.");
  52. }
  53. reactHotLoaderTransformer = new ReactHotLoaderTransformer(tokenProcessor, options.filePath);
  54. this.transformers.push(reactHotLoaderTransformer);
  55. }
  56. // Note that we always want to enable the imports transformer, even when the import transform
  57. // itself isn't enabled, since we need to do type-only import pruning for both Flow and
  58. // TypeScript.
  59. if (transforms.includes("imports")) {
  60. if (importProcessor === null) {
  61. throw new Error("Expected non-null importProcessor with imports transform enabled.");
  62. }
  63. this.transformers.push(
  64. new CJSImportTransformer(
  65. this,
  66. tokenProcessor,
  67. importProcessor,
  68. this.nameManager,
  69. this.helperManager,
  70. reactHotLoaderTransformer,
  71. enableLegacyBabel5ModuleInterop,
  72. Boolean(options.enableLegacyTypeScriptModuleInterop),
  73. transforms.includes("typescript"),
  74. transforms.includes("flow"),
  75. Boolean(options.preserveDynamicImport),
  76. Boolean(options.keepUnusedImports),
  77. ),
  78. );
  79. } else {
  80. this.transformers.push(
  81. new ESMImportTransformer(
  82. tokenProcessor,
  83. this.nameManager,
  84. this.helperManager,
  85. reactHotLoaderTransformer,
  86. transforms.includes("typescript"),
  87. transforms.includes("flow"),
  88. Boolean(options.keepUnusedImports),
  89. options,
  90. ),
  91. );
  92. }
  93. if (transforms.includes("flow")) {
  94. this.transformers.push(
  95. new FlowTransformer(this, tokenProcessor, transforms.includes("imports")),
  96. );
  97. }
  98. if (transforms.includes("typescript")) {
  99. this.transformers.push(
  100. new TypeScriptTransformer(this, tokenProcessor, transforms.includes("imports")),
  101. );
  102. }
  103. if (transforms.includes("jest")) {
  104. this.transformers.push(
  105. new JestHoistTransformer(this, tokenProcessor, this.nameManager, importProcessor),
  106. );
  107. }
  108. }
  109. transform() {
  110. this.tokens.reset();
  111. this.processBalancedCode();
  112. const shouldAddUseStrict = this.isImportsTransformEnabled;
  113. // "use strict" always needs to be first, so override the normal transformer order.
  114. let prefix = shouldAddUseStrict ? '"use strict";' : "";
  115. for (const transformer of this.transformers) {
  116. prefix += transformer.getPrefixCode();
  117. }
  118. prefix += this.helperManager.emitHelpers();
  119. prefix += this.generatedVariables.map((v) => ` var ${v};`).join("");
  120. for (const transformer of this.transformers) {
  121. prefix += transformer.getHoistedCode();
  122. }
  123. let suffix = "";
  124. for (const transformer of this.transformers) {
  125. suffix += transformer.getSuffixCode();
  126. }
  127. const result = this.tokens.finish();
  128. let {code} = result;
  129. if (code.startsWith("#!")) {
  130. let newlineIndex = code.indexOf("\n");
  131. if (newlineIndex === -1) {
  132. newlineIndex = code.length;
  133. code += "\n";
  134. }
  135. return {
  136. code: code.slice(0, newlineIndex + 1) + prefix + code.slice(newlineIndex + 1) + suffix,
  137. // The hashbang line has no tokens, so shifting the tokens to account
  138. // for prefix can happen normally.
  139. mappings: this.shiftMappings(result.mappings, prefix.length),
  140. };
  141. } else {
  142. return {
  143. code: prefix + code + suffix,
  144. mappings: this.shiftMappings(result.mappings, prefix.length),
  145. };
  146. }
  147. }
  148. processBalancedCode() {
  149. let braceDepth = 0;
  150. let parenDepth = 0;
  151. while (!this.tokens.isAtEnd()) {
  152. if (this.tokens.matches1(tt.braceL) || this.tokens.matches1(tt.dollarBraceL)) {
  153. braceDepth++;
  154. } else if (this.tokens.matches1(tt.braceR)) {
  155. if (braceDepth === 0) {
  156. return;
  157. }
  158. braceDepth--;
  159. }
  160. if (this.tokens.matches1(tt.parenL)) {
  161. parenDepth++;
  162. } else if (this.tokens.matches1(tt.parenR)) {
  163. if (parenDepth === 0) {
  164. return;
  165. }
  166. parenDepth--;
  167. }
  168. this.processToken();
  169. }
  170. }
  171. processToken() {
  172. if (this.tokens.matches1(tt._class)) {
  173. this.processClass();
  174. return;
  175. }
  176. for (const transformer of this.transformers) {
  177. const wasProcessed = transformer.process();
  178. if (wasProcessed) {
  179. return;
  180. }
  181. }
  182. this.tokens.copyToken();
  183. }
  184. /**
  185. * Skip past a class with a name and return that name.
  186. */
  187. processNamedClass() {
  188. if (!this.tokens.matches2(tt._class, tt.name)) {
  189. throw new Error("Expected identifier for exported class name.");
  190. }
  191. const name = this.tokens.identifierNameAtIndex(this.tokens.currentIndex() + 1);
  192. this.processClass();
  193. return name;
  194. }
  195. processClass() {
  196. const classInfo = getClassInfo(this, this.tokens, this.nameManager, this.disableESTransforms);
  197. // Both static and instance initializers need a class name to use to invoke the initializer, so
  198. // assign to one if necessary.
  199. const needsCommaExpression =
  200. (classInfo.headerInfo.isExpression || !classInfo.headerInfo.className) &&
  201. classInfo.staticInitializerNames.length + classInfo.instanceInitializerNames.length > 0;
  202. let className = classInfo.headerInfo.className;
  203. if (needsCommaExpression) {
  204. className = this.nameManager.claimFreeName("_class");
  205. this.generatedVariables.push(className);
  206. this.tokens.appendCode(` (${className} =`);
  207. }
  208. const classToken = this.tokens.currentToken();
  209. const contextId = classToken.contextId;
  210. if (contextId == null) {
  211. throw new Error("Expected class to have a context ID.");
  212. }
  213. this.tokens.copyExpectedToken(tt._class);
  214. while (!this.tokens.matchesContextIdAndLabel(tt.braceL, contextId)) {
  215. this.processToken();
  216. }
  217. this.processClassBody(classInfo, className);
  218. const staticInitializerStatements = classInfo.staticInitializerNames.map(
  219. (name) => `${className}.${name}()`,
  220. );
  221. if (needsCommaExpression) {
  222. this.tokens.appendCode(
  223. `, ${staticInitializerStatements.map((s) => `${s}, `).join("")}${className})`,
  224. );
  225. } else if (classInfo.staticInitializerNames.length > 0) {
  226. this.tokens.appendCode(` ${staticInitializerStatements.map((s) => `${s};`).join(" ")}`);
  227. }
  228. }
  229. /**
  230. * We want to just handle class fields in all contexts, since TypeScript supports them. Later,
  231. * when some JS implementations support class fields, this should be made optional.
  232. */
  233. processClassBody(classInfo, className) {
  234. const {
  235. headerInfo,
  236. constructorInsertPos,
  237. constructorInitializerStatements,
  238. fields,
  239. instanceInitializerNames,
  240. rangesToRemove,
  241. } = classInfo;
  242. let fieldIndex = 0;
  243. let rangeToRemoveIndex = 0;
  244. const classContextId = this.tokens.currentToken().contextId;
  245. if (classContextId == null) {
  246. throw new Error("Expected non-null context ID on class.");
  247. }
  248. this.tokens.copyExpectedToken(tt.braceL);
  249. if (this.isReactHotLoaderTransformEnabled) {
  250. this.tokens.appendCode(
  251. "__reactstandin__regenerateByEval(key, code) {this[key] = eval(code);}",
  252. );
  253. }
  254. const needsConstructorInit =
  255. constructorInitializerStatements.length + instanceInitializerNames.length > 0;
  256. if (constructorInsertPos === null && needsConstructorInit) {
  257. const constructorInitializersCode = this.makeConstructorInitCode(
  258. constructorInitializerStatements,
  259. instanceInitializerNames,
  260. className,
  261. );
  262. if (headerInfo.hasSuperclass) {
  263. const argsName = this.nameManager.claimFreeName("args");
  264. this.tokens.appendCode(
  265. `constructor(...${argsName}) { super(...${argsName}); ${constructorInitializersCode}; }`,
  266. );
  267. } else {
  268. this.tokens.appendCode(`constructor() { ${constructorInitializersCode}; }`);
  269. }
  270. }
  271. while (!this.tokens.matchesContextIdAndLabel(tt.braceR, classContextId)) {
  272. if (fieldIndex < fields.length && this.tokens.currentIndex() === fields[fieldIndex].start) {
  273. let needsCloseBrace = false;
  274. if (this.tokens.matches1(tt.bracketL)) {
  275. this.tokens.copyTokenWithPrefix(`${fields[fieldIndex].initializerName}() {this`);
  276. } else if (this.tokens.matches1(tt.string) || this.tokens.matches1(tt.num)) {
  277. this.tokens.copyTokenWithPrefix(`${fields[fieldIndex].initializerName}() {this[`);
  278. needsCloseBrace = true;
  279. } else {
  280. this.tokens.copyTokenWithPrefix(`${fields[fieldIndex].initializerName}() {this.`);
  281. }
  282. while (this.tokens.currentIndex() < fields[fieldIndex].end) {
  283. if (needsCloseBrace && this.tokens.currentIndex() === fields[fieldIndex].equalsIndex) {
  284. this.tokens.appendCode("]");
  285. }
  286. this.processToken();
  287. }
  288. this.tokens.appendCode("}");
  289. fieldIndex++;
  290. } else if (
  291. rangeToRemoveIndex < rangesToRemove.length &&
  292. this.tokens.currentIndex() >= rangesToRemove[rangeToRemoveIndex].start
  293. ) {
  294. if (this.tokens.currentIndex() < rangesToRemove[rangeToRemoveIndex].end) {
  295. this.tokens.removeInitialToken();
  296. }
  297. while (this.tokens.currentIndex() < rangesToRemove[rangeToRemoveIndex].end) {
  298. this.tokens.removeToken();
  299. }
  300. rangeToRemoveIndex++;
  301. } else if (this.tokens.currentIndex() === constructorInsertPos) {
  302. this.tokens.copyToken();
  303. if (needsConstructorInit) {
  304. this.tokens.appendCode(
  305. `;${this.makeConstructorInitCode(
  306. constructorInitializerStatements,
  307. instanceInitializerNames,
  308. className,
  309. )};`,
  310. );
  311. }
  312. this.processToken();
  313. } else {
  314. this.processToken();
  315. }
  316. }
  317. this.tokens.copyExpectedToken(tt.braceR);
  318. }
  319. makeConstructorInitCode(
  320. constructorInitializerStatements,
  321. instanceInitializerNames,
  322. className,
  323. ) {
  324. return [
  325. ...constructorInitializerStatements,
  326. ...instanceInitializerNames.map((name) => `${className}.prototype.${name}.call(this)`),
  327. ].join(";");
  328. }
  329. /**
  330. * Normally it's ok to simply remove type tokens, but we need to be more careful when dealing with
  331. * arrow function return types since they can confuse the parser. In that case, we want to move
  332. * the close-paren to the same line as the arrow.
  333. *
  334. * See https://github.com/alangpierce/sucrase/issues/391 for more details.
  335. */
  336. processPossibleArrowParamEnd() {
  337. if (this.tokens.matches2(tt.parenR, tt.colon) && this.tokens.tokenAtRelativeIndex(1).isType) {
  338. let nextNonTypeIndex = this.tokens.currentIndex() + 1;
  339. // Look ahead to see if this is an arrow function or something else.
  340. while (this.tokens.tokens[nextNonTypeIndex].isType) {
  341. nextNonTypeIndex++;
  342. }
  343. if (this.tokens.matches1AtIndex(nextNonTypeIndex, tt.arrow)) {
  344. this.tokens.removeInitialToken();
  345. while (this.tokens.currentIndex() < nextNonTypeIndex) {
  346. this.tokens.removeToken();
  347. }
  348. this.tokens.replaceTokenTrimmingLeftWhitespace(") =>");
  349. return true;
  350. }
  351. }
  352. return false;
  353. }
  354. /**
  355. * An async arrow function might be of the form:
  356. *
  357. * async <
  358. * T
  359. * >() => {}
  360. *
  361. * in which case, removing the type parameters will cause a syntax error. Detect this case and
  362. * move the open-paren earlier.
  363. */
  364. processPossibleAsyncArrowWithTypeParams() {
  365. if (
  366. !this.tokens.matchesContextual(ContextualKeyword._async) &&
  367. !this.tokens.matches1(tt._async)
  368. ) {
  369. return false;
  370. }
  371. const nextToken = this.tokens.tokenAtRelativeIndex(1);
  372. if (nextToken.type !== tt.lessThan || !nextToken.isType) {
  373. return false;
  374. }
  375. let nextNonTypeIndex = this.tokens.currentIndex() + 1;
  376. // Look ahead to see if this is an arrow function or something else.
  377. while (this.tokens.tokens[nextNonTypeIndex].isType) {
  378. nextNonTypeIndex++;
  379. }
  380. if (this.tokens.matches1AtIndex(nextNonTypeIndex, tt.parenL)) {
  381. this.tokens.replaceToken("async (");
  382. this.tokens.removeInitialToken();
  383. while (this.tokens.currentIndex() < nextNonTypeIndex) {
  384. this.tokens.removeToken();
  385. }
  386. this.tokens.removeToken();
  387. // We ate a ( token, so we need to process the tokens in between and then the ) token so that
  388. // we remain balanced.
  389. this.processBalancedCode();
  390. this.processToken();
  391. return true;
  392. }
  393. return false;
  394. }
  395. processPossibleTypeRange() {
  396. if (this.tokens.currentToken().isType) {
  397. this.tokens.removeInitialToken();
  398. while (this.tokens.currentToken().isType) {
  399. this.tokens.removeToken();
  400. }
  401. return true;
  402. }
  403. return false;
  404. }
  405. shiftMappings(
  406. mappings,
  407. prefixLength,
  408. ) {
  409. for (let i = 0; i < mappings.length; i++) {
  410. const mapping = mappings[i];
  411. if (mapping !== undefined) {
  412. mappings[i] = mapping + prefixLength;
  413. }
  414. }
  415. return mappings;
  416. }
  417. }