UNPKG

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