UNPKG

15.2 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 let 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 index++;
216 ({newIndex: index, namedImports} = this.getNamedImports(index));
217 }
218
219 if (this.tokens.matchesContextualAtIndex(index, ContextualKeyword._from)) {
220 index++;
221 }
222
223 if (!this.tokens.matches1AtIndex(index, tt.string)) {
224 throw new Error("Expected string token at the end of import statement.");
225 }
226 const path = this.tokens.stringValueAtIndex(index);
227 const importInfo = this.getImportInfo(path);
228 importInfo.defaultNames.push(...defaultNames);
229 importInfo.wildcardNames.push(...wildcardNames);
230 importInfo.namedImports.push(...namedImports);
231 if (defaultNames.length === 0 && wildcardNames.length === 0 && namedImports.length === 0) {
232 importInfo.hasBareImport = true;
233 }
234 }
235
236 preprocessExportAtIndex(index) {
237 if (
238 this.tokens.matches2AtIndex(index, tt._export, tt._var) ||
239 this.tokens.matches2AtIndex(index, tt._export, tt._let) ||
240 this.tokens.matches2AtIndex(index, tt._export, tt._const)
241 ) {
242 this.preprocessVarExportAtIndex(index);
243 } else if (
244 this.tokens.matches2AtIndex(index, tt._export, tt._function) ||
245 this.tokens.matches2AtIndex(index, tt._export, tt._class)
246 ) {
247 const exportName = this.tokens.identifierNameAtIndex(index + 2);
248 this.addExportBinding(exportName, exportName);
249 } else if (this.tokens.matches3AtIndex(index, tt._export, tt.name, tt._function)) {
250 const exportName = this.tokens.identifierNameAtIndex(index + 3);
251 this.addExportBinding(exportName, exportName);
252 } else if (this.tokens.matches2AtIndex(index, tt._export, tt.braceL)) {
253 this.preprocessNamedExportAtIndex(index);
254 } else if (this.tokens.matches2AtIndex(index, tt._export, tt.star)) {
255 this.preprocessExportStarAtIndex(index);
256 }
257 }
258
259 preprocessVarExportAtIndex(index) {
260 let depth = 0;
261 // Handle cases like `export let {x} = y;`, starting at the open-brace in that case.
262 for (let i = index + 2; ; i++) {
263 if (
264 this.tokens.matches1AtIndex(i, tt.braceL) ||
265 this.tokens.matches1AtIndex(i, tt.dollarBraceL) ||
266 this.tokens.matches1AtIndex(i, tt.bracketL)
267 ) {
268 depth++;
269 } else if (
270 this.tokens.matches1AtIndex(i, tt.braceR) ||
271 this.tokens.matches1AtIndex(i, tt.bracketR)
272 ) {
273 depth--;
274 } else if (depth === 0 && !this.tokens.matches1AtIndex(i, tt.name)) {
275 break;
276 } else if (this.tokens.matches1AtIndex(1, tt.eq)) {
277 const endIndex = this.tokens.currentToken().rhsEndIndex;
278 if (endIndex == null) {
279 throw new Error("Expected = token with an end index.");
280 }
281 i = endIndex - 1;
282 } else {
283 const token = this.tokens.tokens[i];
284 if (isDeclaration(token)) {
285 const exportName = this.tokens.identifierNameAtIndex(i);
286 this.identifierReplacements.set(exportName, `exports.${exportName}`);
287 }
288 }
289 }
290 }
291
292 /**
293 * Walk this export statement just in case it's an export...from statement.
294 * If it is, combine it into the import info for that path. Otherwise, just
295 * bail out; it'll be handled later.
296 */
297 preprocessNamedExportAtIndex(index) {
298 // export {
299 index += 2;
300 const {newIndex, namedImports} = this.getNamedImports(index);
301 index = newIndex;
302
303 if (this.tokens.matchesContextualAtIndex(index, ContextualKeyword._from)) {
304 index++;
305 } else {
306 // Reinterpret "a as b" to be local/exported rather than imported/local.
307 for (const {importedName: localName, localName: exportedName} of namedImports) {
308 this.addExportBinding(localName, exportedName);
309 }
310 return;
311 }
312
313 if (!this.tokens.matches1AtIndex(index, tt.string)) {
314 throw new Error("Expected string token at the end of import statement.");
315 }
316 const path = this.tokens.stringValueAtIndex(index);
317 const importInfo = this.getImportInfo(path);
318 importInfo.namedExports.push(...namedImports);
319 }
320
321 preprocessExportStarAtIndex(index) {
322 let exportedName = null;
323 if (this.tokens.matches3AtIndex(index, tt._export, tt.star, tt._as)) {
324 // export * as
325 index += 3;
326 exportedName = this.tokens.identifierNameAtIndex(index);
327 // foo from
328 index += 2;
329 } else {
330 // export * from
331 index += 3;
332 }
333 if (!this.tokens.matches1AtIndex(index, tt.string)) {
334 throw new Error("Expected string token at the end of star export statement.");
335 }
336 const path = this.tokens.stringValueAtIndex(index);
337 const importInfo = this.getImportInfo(path);
338 if (exportedName !== null) {
339 importInfo.exportStarNames.push(exportedName);
340 } else {
341 importInfo.hasStarExport = true;
342 }
343 }
344
345 getNamedImports(index) {
346 const namedImports = [];
347 while (true) {
348 if (this.tokens.matches1AtIndex(index, tt.braceR)) {
349 index++;
350 break;
351 }
352
353 // Flow type imports should just be ignored.
354 let isTypeImport = false;
355 if (
356 (this.tokens.matchesContextualAtIndex(index, ContextualKeyword._type) ||
357 this.tokens.matches1AtIndex(index, tt._typeof)) &&
358 this.tokens.matches1AtIndex(index + 1, tt.name) &&
359 !this.tokens.matchesContextualAtIndex(index + 1, ContextualKeyword._as)
360 ) {
361 isTypeImport = true;
362 index++;
363 }
364
365 const importedName = this.tokens.identifierNameAtIndex(index);
366 let localName;
367 index++;
368 if (this.tokens.matchesContextualAtIndex(index, ContextualKeyword._as)) {
369 index++;
370 localName = this.tokens.identifierNameAtIndex(index);
371 index++;
372 } else {
373 localName = importedName;
374 }
375 if (!isTypeImport) {
376 namedImports.push({importedName, localName});
377 }
378 if (this.tokens.matches2AtIndex(index, tt.comma, tt.braceR)) {
379 index += 2;
380 break;
381 } else if (this.tokens.matches1AtIndex(index, tt.braceR)) {
382 index++;
383 break;
384 } else if (this.tokens.matches1AtIndex(index, tt.comma)) {
385 index++;
386 } else {
387 throw new Error(`Unexpected token: ${JSON.stringify(this.tokens.tokens[index])}`);
388 }
389 }
390 return {newIndex: index, namedImports};
391 }
392
393 /**
394 * Get a mutable import info object for this path, creating one if it doesn't
395 * exist yet.
396 */
397 getImportInfo(path) {
398 const existingInfo = this.importInfoByPath.get(path);
399 if (existingInfo) {
400 return existingInfo;
401 }
402 const newInfo = {
403 defaultNames: [],
404 wildcardNames: [],
405 namedImports: [],
406 namedExports: [],
407 hasBareImport: false,
408 exportStarNames: [],
409 hasStarExport: false,
410 };
411 this.importInfoByPath.set(path, newInfo);
412 return newInfo;
413 }
414
415 addExportBinding(localName, exportedName) {
416 if (!this.exportBindingsByLocalName.has(localName)) {
417 this.exportBindingsByLocalName.set(localName, []);
418 }
419 this.exportBindingsByLocalName.get(localName).push(exportedName);
420 }
421
422 /**
423 * Return the code to use for the import for this path, or the empty string if
424 * the code has already been "claimed" by a previous import.
425 */
426 claimImportCode(importPath) {
427 const result = this.importsToReplace.get(importPath);
428 this.importsToReplace.set(importPath, "");
429 return result || "";
430 }
431
432 getIdentifierReplacement(identifierName) {
433 return this.identifierReplacements.get(identifierName) || null;
434 }
435
436 /**
437 * Return a string like `exports.foo = exports.bar`.
438 */
439 resolveExportBinding(assignedName) {
440 const exportedNames = this.exportBindingsByLocalName.get(assignedName);
441 if (!exportedNames || exportedNames.length === 0) {
442 return null;
443 }
444 return exportedNames.map((exportedName) => `exports.${exportedName}`).join(" = ");
445 }
446
447 /**
448 * Return all imported/exported names where we might be interested in whether usages of those
449 * names are shadowed.
450 */
451 getGlobalNames() {
452 return new Set([
453 ...this.identifierReplacements.keys(),
454 ...this.exportBindingsByLocalName.keys(),
455 ]);
456 }
457}