UNPKG

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