UNPKG

15.3 kBJavaScriptView Raw
1import {HelperManager} from "./HelperManager";
2
3
4import {isDeclaration} from "./parser/tokenizer";
5import {ContextualKeyword} from "./parser/tokenizer/keywords";
6import {TokenType as tt} from "./parser/tokenizer/types";
7
8import {getNonTypeIdentifiers} from "./util/getNonTypeIdentifiers";
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25/**
26 * Class responsible for preprocessing and bookkeeping import and export declarations within the
27 * file.
28 *
29 * TypeScript uses a simpler mechanism that does not use functions like interopRequireDefault and
30 * interopRequireWildcard, so we also allow that mode for compatibility.
31 */
32export default class CJSImportProcessor {
33 __init() {this.nonTypeIdentifiers = new Set()}
34 __init2() {this.importInfoByPath = new Map()}
35 __init3() {this.importsToReplace = new Map()}
36 __init4() {this.identifierReplacements = new Map()}
37 __init5() {this.exportBindingsByLocalName = new Map()}
38
39
40 constructor(
41 nameManager,
42 tokens,
43 enableLegacyTypeScriptModuleInterop,
44 options,
45 isTypeScriptTransformEnabled,
46 ) {;this.nameManager = nameManager;this.tokens = tokens;this.enableLegacyTypeScriptModuleInterop = enableLegacyTypeScriptModuleInterop;this.options = options;this.isTypeScriptTransformEnabled = isTypeScriptTransformEnabled;CJSImportProcessor.prototype.__init.call(this);CJSImportProcessor.prototype.__init2.call(this);CJSImportProcessor.prototype.__init3.call(this);CJSImportProcessor.prototype.__init4.call(this);CJSImportProcessor.prototype.__init5.call(this);
47 this.helpers = new HelperManager(nameManager);
48 }
49
50 getPrefixCode() {
51 return this.helpers.emitHelpers();
52 }
53
54 preprocessTokens() {
55 for (let i = 0; i < this.tokens.tokens.length; i++) {
56 if (
57 this.tokens.matches1AtIndex(i, tt._import) &&
58 !this.tokens.matches3AtIndex(i, tt._import, tt.name, tt.eq)
59 ) {
60 this.preprocessImportAtIndex(i);
61 }
62 if (
63 this.tokens.matches1AtIndex(i, tt._export) &&
64 !this.tokens.matches2AtIndex(i, tt._export, tt.eq)
65 ) {
66 this.preprocessExportAtIndex(i);
67 }
68 }
69 this.generateImportReplacements();
70 }
71
72 /**
73 * In TypeScript, import statements that only import types should be removed. This does not count
74 * bare imports.
75 */
76 pruneTypeOnlyImports() {
77 this.nonTypeIdentifiers = getNonTypeIdentifiers(this.tokens, this.options);
78 for (const [path, importInfo] of this.importInfoByPath.entries()) {
79 if (
80 importInfo.hasBareImport ||
81 importInfo.hasStarExport ||
82 importInfo.exportStarNames.length > 0 ||
83 importInfo.namedExports.length > 0
84 ) {
85 continue;
86 }
87 const names = [
88 ...importInfo.defaultNames,
89 ...importInfo.wildcardNames,
90 ...importInfo.namedImports.map(({localName}) => localName),
91 ];
92 if (names.every((name) => this.isTypeName(name))) {
93 this.importsToReplace.set(path, "");
94 }
95 }
96 }
97
98 isTypeName(name) {
99 return this.isTypeScriptTransformEnabled && !this.nonTypeIdentifiers.has(name);
100 }
101
102 generateImportReplacements() {
103 for (const [path, importInfo] of this.importInfoByPath.entries()) {
104 const {
105 defaultNames,
106 wildcardNames,
107 namedImports,
108 namedExports,
109 exportStarNames,
110 hasStarExport,
111 } = importInfo;
112
113 if (
114 defaultNames.length === 0 &&
115 wildcardNames.length === 0 &&
116 namedImports.length === 0 &&
117 namedExports.length === 0 &&
118 exportStarNames.length === 0 &&
119 !hasStarExport
120 ) {
121 // Import is never used, so don't even assign a name.
122 this.importsToReplace.set(path, `require('${path}');`);
123 continue;
124 }
125
126 const primaryImportName = this.getFreeIdentifierForPath(path);
127 let secondaryImportName;
128 if (this.enableLegacyTypeScriptModuleInterop) {
129 secondaryImportName = primaryImportName;
130 } else {
131 secondaryImportName =
132 wildcardNames.length > 0 ? wildcardNames[0] : this.getFreeIdentifierForPath(path);
133 }
134 let requireCode = `var ${primaryImportName} = require('${path}');`;
135 if (wildcardNames.length > 0) {
136 for (const wildcardName of wildcardNames) {
137 const moduleExpr = this.enableLegacyTypeScriptModuleInterop
138 ? primaryImportName
139 : `${this.helpers.getHelperName("interopRequireWildcard")}(${primaryImportName})`;
140 requireCode += ` var ${wildcardName} = ${moduleExpr};`;
141 }
142 } else if (exportStarNames.length > 0 && secondaryImportName !== primaryImportName) {
143 requireCode += ` var ${secondaryImportName} = ${this.helpers.getHelperName(
144 "interopRequireWildcard",
145 )}(${primaryImportName});`;
146 } else if (defaultNames.length > 0 && secondaryImportName !== primaryImportName) {
147 requireCode += ` var ${secondaryImportName} = ${this.helpers.getHelperName(
148 "interopRequireDefault",
149 )}(${primaryImportName});`;
150 }
151
152 for (const {importedName, localName} of namedExports) {
153 requireCode += ` ${this.helpers.getHelperName(
154 "createNamedExportFrom",
155 )}(${primaryImportName}, '${localName}', '${importedName}');`;
156 }
157 for (const exportStarName of exportStarNames) {
158 requireCode += ` exports.${exportStarName} = ${secondaryImportName};`;
159 }
160 if (hasStarExport) {
161 requireCode += ` ${this.helpers.getHelperName("createStarExport")}(${primaryImportName});`;
162 }
163
164 this.importsToReplace.set(path, requireCode);
165
166 for (const defaultName of defaultNames) {
167 this.identifierReplacements.set(defaultName, `${secondaryImportName}.default`);
168 }
169 for (const {importedName, localName} of namedImports) {
170 this.identifierReplacements.set(localName, `${primaryImportName}.${importedName}`);
171 }
172 }
173 }
174
175 getFreeIdentifierForPath(path) {
176 const components = path.split("/");
177 const lastComponent = components[components.length - 1];
178 const baseName = lastComponent.replace(/\W/g, "");
179 return this.nameManager.claimFreeName(`_${baseName}`);
180 }
181
182 preprocessImportAtIndex(index) {
183 const defaultNames = [];
184 const wildcardNames = [];
185 let namedImports = [];
186
187 index++;
188 if (
189 (this.tokens.matchesContextualAtIndex(index, ContextualKeyword._type) ||
190 this.tokens.matches1AtIndex(index, tt._typeof)) &&
191 !this.tokens.matches1AtIndex(index + 1, tt.comma) &&
192 !this.tokens.matchesContextualAtIndex(index + 1, ContextualKeyword._from)
193 ) {
194 // import type declaration, so no need to process anything.
195 return;
196 }
197
198 if (this.tokens.matches1AtIndex(index, tt.parenL)) {
199 // Dynamic import, so nothing to do
200 return;
201 }
202
203 if (this.tokens.matches1AtIndex(index, tt.name)) {
204 defaultNames.push(this.tokens.identifierNameAtIndex(index));
205 index++;
206 if (this.tokens.matches1AtIndex(index, tt.comma)) {
207 index++;
208 }
209 }
210
211 if (this.tokens.matches1AtIndex(index, tt.star)) {
212 // * as
213 index += 2;
214 wildcardNames.push(this.tokens.identifierNameAtIndex(index));
215 index++;
216 }
217
218 if (this.tokens.matches1AtIndex(index, tt.braceL)) {
219 index++;
220 ({newIndex: index, namedImports} = this.getNamedImports(index));
221 }
222
223 if (this.tokens.matchesContextualAtIndex(index, ContextualKeyword._from)) {
224 index++;
225 }
226
227 if (!this.tokens.matches1AtIndex(index, tt.string)) {
228 throw new Error("Expected string token at the end of import statement.");
229 }
230 const path = this.tokens.stringValueAtIndex(index);
231 const importInfo = this.getImportInfo(path);
232 importInfo.defaultNames.push(...defaultNames);
233 importInfo.wildcardNames.push(...wildcardNames);
234 importInfo.namedImports.push(...namedImports);
235 if (defaultNames.length === 0 && wildcardNames.length === 0 && namedImports.length === 0) {
236 importInfo.hasBareImport = true;
237 }
238 }
239
240 preprocessExportAtIndex(index) {
241 if (
242 this.tokens.matches2AtIndex(index, tt._export, tt._var) ||
243 this.tokens.matches2AtIndex(index, tt._export, tt._let) ||
244 this.tokens.matches2AtIndex(index, tt._export, tt._const)
245 ) {
246 this.preprocessVarExportAtIndex(index);
247 } else if (
248 this.tokens.matches2AtIndex(index, tt._export, tt._function) ||
249 this.tokens.matches2AtIndex(index, tt._export, tt._class)
250 ) {
251 const exportName = this.tokens.identifierNameAtIndex(index + 2);
252 this.addExportBinding(exportName, exportName);
253 } else if (this.tokens.matches3AtIndex(index, tt._export, tt.name, tt._function)) {
254 const exportName = this.tokens.identifierNameAtIndex(index + 3);
255 this.addExportBinding(exportName, exportName);
256 } else if (this.tokens.matches2AtIndex(index, tt._export, tt.braceL)) {
257 this.preprocessNamedExportAtIndex(index);
258 } else if (this.tokens.matches2AtIndex(index, tt._export, tt.star)) {
259 this.preprocessExportStarAtIndex(index);
260 }
261 }
262
263 preprocessVarExportAtIndex(index) {
264 let depth = 0;
265 // Handle cases like `export let {x} = y;`, starting at the open-brace in that case.
266 for (let i = index + 2; ; i++) {
267 if (
268 this.tokens.matches1AtIndex(i, tt.braceL) ||
269 this.tokens.matches1AtIndex(i, tt.dollarBraceL) ||
270 this.tokens.matches1AtIndex(i, tt.bracketL)
271 ) {
272 depth++;
273 } else if (
274 this.tokens.matches1AtIndex(i, tt.braceR) ||
275 this.tokens.matches1AtIndex(i, tt.bracketR)
276 ) {
277 depth--;
278 } else if (depth === 0 && !this.tokens.matches1AtIndex(i, tt.name)) {
279 break;
280 } else if (this.tokens.matches1AtIndex(1, tt.eq)) {
281 const endIndex = this.tokens.currentToken().rhsEndIndex;
282 if (endIndex == null) {
283 throw new Error("Expected = token with an end index.");
284 }
285 i = endIndex - 1;
286 } else {
287 const token = this.tokens.tokens[i];
288 if (isDeclaration(token)) {
289 const exportName = this.tokens.identifierNameAtIndex(i);
290 this.identifierReplacements.set(exportName, `exports.${exportName}`);
291 }
292 }
293 }
294 }
295
296 /**
297 * Walk this export statement just in case it's an export...from statement.
298 * If it is, combine it into the import info for that path. Otherwise, just
299 * bail out; it'll be handled later.
300 */
301 preprocessNamedExportAtIndex(index) {
302 // export {
303 index += 2;
304 const {newIndex, namedImports} = this.getNamedImports(index);
305 index = newIndex;
306
307 if (this.tokens.matchesContextualAtIndex(index, ContextualKeyword._from)) {
308 index++;
309 } else {
310 // Reinterpret "a as b" to be local/exported rather than imported/local.
311 for (const {importedName: localName, localName: exportedName} of namedImports) {
312 this.addExportBinding(localName, exportedName);
313 }
314 return;
315 }
316
317 if (!this.tokens.matches1AtIndex(index, tt.string)) {
318 throw new Error("Expected string token at the end of import statement.");
319 }
320 const path = this.tokens.stringValueAtIndex(index);
321 const importInfo = this.getImportInfo(path);
322 importInfo.namedExports.push(...namedImports);
323 }
324
325 preprocessExportStarAtIndex(index) {
326 let exportedName = null;
327 if (this.tokens.matches3AtIndex(index, tt._export, tt.star, tt._as)) {
328 // export * as
329 index += 3;
330 exportedName = this.tokens.identifierNameAtIndex(index);
331 // foo from
332 index += 2;
333 } else {
334 // export * from
335 index += 3;
336 }
337 if (!this.tokens.matches1AtIndex(index, tt.string)) {
338 throw new Error("Expected string token at the end of star export statement.");
339 }
340 const path = this.tokens.stringValueAtIndex(index);
341 const importInfo = this.getImportInfo(path);
342 if (exportedName !== null) {
343 importInfo.exportStarNames.push(exportedName);
344 } else {
345 importInfo.hasStarExport = true;
346 }
347 }
348
349 getNamedImports(index) {
350 const namedImports = [];
351 while (true) {
352 if (this.tokens.matches1AtIndex(index, tt.braceR)) {
353 index++;
354 break;
355 }
356
357 // Flow type imports should just be ignored.
358 let isTypeImport = false;
359 if (
360 (this.tokens.matchesContextualAtIndex(index, ContextualKeyword._type) ||
361 this.tokens.matches1AtIndex(index, tt._typeof)) &&
362 this.tokens.matches1AtIndex(index + 1, tt.name) &&
363 !this.tokens.matchesContextualAtIndex(index + 1, ContextualKeyword._as)
364 ) {
365 isTypeImport = true;
366 index++;
367 }
368
369 const importedName = this.tokens.identifierNameAtIndex(index);
370 let localName;
371 index++;
372 if (this.tokens.matchesContextualAtIndex(index, ContextualKeyword._as)) {
373 index++;
374 localName = this.tokens.identifierNameAtIndex(index);
375 index++;
376 } else {
377 localName = importedName;
378 }
379 if (!isTypeImport) {
380 namedImports.push({importedName, localName});
381 }
382 if (this.tokens.matches2AtIndex(index, tt.comma, tt.braceR)) {
383 index += 2;
384 break;
385 } else if (this.tokens.matches1AtIndex(index, tt.braceR)) {
386 index++;
387 break;
388 } else if (this.tokens.matches1AtIndex(index, tt.comma)) {
389 index++;
390 } else {
391 throw new Error(`Unexpected token: ${JSON.stringify(this.tokens.tokens[index])}`);
392 }
393 }
394 return {newIndex: index, namedImports};
395 }
396
397 /**
398 * Get a mutable import info object for this path, creating one if it doesn't
399 * exist yet.
400 */
401 getImportInfo(path) {
402 const existingInfo = this.importInfoByPath.get(path);
403 if (existingInfo) {
404 return existingInfo;
405 }
406 const newInfo = {
407 defaultNames: [],
408 wildcardNames: [],
409 namedImports: [],
410 namedExports: [],
411 hasBareImport: false,
412 exportStarNames: [],
413 hasStarExport: false,
414 };
415 this.importInfoByPath.set(path, newInfo);
416 return newInfo;
417 }
418
419 addExportBinding(localName, exportedName) {
420 if (!this.exportBindingsByLocalName.has(localName)) {
421 this.exportBindingsByLocalName.set(localName, []);
422 }
423 this.exportBindingsByLocalName.get(localName).push(exportedName);
424 }
425
426 /**
427 * Return the code to use for the import for this path, or the empty string if
428 * the code has already been "claimed" by a previous import.
429 */
430 claimImportCode(importPath) {
431 const result = this.importsToReplace.get(importPath);
432 this.importsToReplace.set(importPath, "");
433 return result || "";
434 }
435
436 getIdentifierReplacement(identifierName) {
437 return this.identifierReplacements.get(identifierName) || null;
438 }
439
440 /**
441 * Return a string like `exports.foo = exports.bar`.
442 */
443 resolveExportBinding(assignedName) {
444 const exportedNames = this.exportBindingsByLocalName.get(assignedName);
445 if (!exportedNames || exportedNames.length === 0) {
446 return null;
447 }
448 return exportedNames.map((exportedName) => `exports.${exportedName}`).join(" = ");
449 }
450
451 /**
452 * Return all imported/exported names where we might be interested in whether usages of those
453 * names are shadowed.
454 */
455 getGlobalNames() {
456 return new Set([
457 ...this.identifierReplacements.keys(),
458 ...this.exportBindingsByLocalName.keys(),
459 ]);
460 }
461}