UNPKG

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