UNPKG

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