暂无描述

RootTransformer.js 15KB

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