import {isDeclaration} from "./parser/tokenizer"; import {ContextualKeyword} from "./parser/tokenizer/keywords"; import {TokenType as tt} from "./parser/tokenizer/types"; import getImportExportSpecifierInfo from "./util/getImportExportSpecifierInfo"; import {getNonTypeIdentifiers} from "./util/getNonTypeIdentifiers"; /** * Class responsible for preprocessing and bookkeeping import and export declarations within the * file. * * TypeScript uses a simpler mechanism that does not use functions like interopRequireDefault and * interopRequireWildcard, so we also allow that mode for compatibility. */ export default class CJSImportProcessor { __init() {this.nonTypeIdentifiers = new Set()} __init2() {this.importInfoByPath = new Map()} __init3() {this.importsToReplace = new Map()} __init4() {this.identifierReplacements = new Map()} __init5() {this.exportBindingsByLocalName = new Map()} constructor( nameManager, tokens, enableLegacyTypeScriptModuleInterop, options, isTypeScriptTransformEnabled, keepUnusedImports, helperManager, ) {;this.nameManager = nameManager;this.tokens = tokens;this.enableLegacyTypeScriptModuleInterop = enableLegacyTypeScriptModuleInterop;this.options = options;this.isTypeScriptTransformEnabled = isTypeScriptTransformEnabled;this.keepUnusedImports = keepUnusedImports;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);} preprocessTokens() { for (let i = 0; i < this.tokens.tokens.length; i++) { if ( this.tokens.matches1AtIndex(i, tt._import) && !this.tokens.matches3AtIndex(i, tt._import, tt.name, tt.eq) ) { this.preprocessImportAtIndex(i); } if ( this.tokens.matches1AtIndex(i, tt._export) && !this.tokens.matches2AtIndex(i, tt._export, tt.eq) ) { this.preprocessExportAtIndex(i); } } this.generateImportReplacements(); } /** * In TypeScript, import statements that only import types should be removed. * This includes `import {} from 'foo';`, but not `import 'foo';`. */ pruneTypeOnlyImports() { this.nonTypeIdentifiers = getNonTypeIdentifiers(this.tokens, this.options); for (const [path, importInfo] of this.importInfoByPath.entries()) { if ( importInfo.hasBareImport || importInfo.hasStarExport || importInfo.exportStarNames.length > 0 || importInfo.namedExports.length > 0 ) { continue; } const names = [ ...importInfo.defaultNames, ...importInfo.wildcardNames, ...importInfo.namedImports.map(({localName}) => localName), ]; if (names.every((name) => this.shouldAutomaticallyElideImportedName(name))) { this.importsToReplace.set(path, ""); } } } shouldAutomaticallyElideImportedName(name) { return ( this.isTypeScriptTransformEnabled && !this.keepUnusedImports && !this.nonTypeIdentifiers.has(name) ); } generateImportReplacements() { for (const [path, importInfo] of this.importInfoByPath.entries()) { const { defaultNames, wildcardNames, namedImports, namedExports, exportStarNames, hasStarExport, } = importInfo; if ( defaultNames.length === 0 && wildcardNames.length === 0 && namedImports.length === 0 && namedExports.length === 0 && exportStarNames.length === 0 && !hasStarExport ) { // Import is never used, so don't even assign a name. this.importsToReplace.set(path, `require('${path}');`); continue; } const primaryImportName = this.getFreeIdentifierForPath(path); let secondaryImportName; if (this.enableLegacyTypeScriptModuleInterop) { secondaryImportName = primaryImportName; } else { secondaryImportName = wildcardNames.length > 0 ? wildcardNames[0] : this.getFreeIdentifierForPath(path); } let requireCode = `var ${primaryImportName} = require('${path}');`; if (wildcardNames.length > 0) { for (const wildcardName of wildcardNames) { const moduleExpr = this.enableLegacyTypeScriptModuleInterop ? primaryImportName : `${this.helperManager.getHelperName("interopRequireWildcard")}(${primaryImportName})`; requireCode += ` var ${wildcardName} = ${moduleExpr};`; } } else if (exportStarNames.length > 0 && secondaryImportName !== primaryImportName) { requireCode += ` var ${secondaryImportName} = ${this.helperManager.getHelperName( "interopRequireWildcard", )}(${primaryImportName});`; } else if (defaultNames.length > 0 && secondaryImportName !== primaryImportName) { requireCode += ` var ${secondaryImportName} = ${this.helperManager.getHelperName( "interopRequireDefault", )}(${primaryImportName});`; } for (const {importedName, localName} of namedExports) { requireCode += ` ${this.helperManager.getHelperName( "createNamedExportFrom", )}(${primaryImportName}, '${localName}', '${importedName}');`; } for (const exportStarName of exportStarNames) { requireCode += ` exports.${exportStarName} = ${secondaryImportName};`; } if (hasStarExport) { requireCode += ` ${this.helperManager.getHelperName( "createStarExport", )}(${primaryImportName});`; } this.importsToReplace.set(path, requireCode); for (const defaultName of defaultNames) { this.identifierReplacements.set(defaultName, `${secondaryImportName}.default`); } for (const {importedName, localName} of namedImports) { this.identifierReplacements.set(localName, `${primaryImportName}.${importedName}`); } } } getFreeIdentifierForPath(path) { const components = path.split("/"); const lastComponent = components[components.length - 1]; const baseName = lastComponent.replace(/\W/g, ""); return this.nameManager.claimFreeName(`_${baseName}`); } preprocessImportAtIndex(index) { const defaultNames = []; const wildcardNames = []; const namedImports = []; index++; if ( (this.tokens.matchesContextualAtIndex(index, ContextualKeyword._type) || this.tokens.matches1AtIndex(index, tt._typeof)) && !this.tokens.matches1AtIndex(index + 1, tt.comma) && !this.tokens.matchesContextualAtIndex(index + 1, ContextualKeyword._from) ) { // import type declaration, so no need to process anything. return; } if (this.tokens.matches1AtIndex(index, tt.parenL)) { // Dynamic import, so nothing to do return; } if (this.tokens.matches1AtIndex(index, tt.name)) { defaultNames.push(this.tokens.identifierNameAtIndex(index)); index++; if (this.tokens.matches1AtIndex(index, tt.comma)) { index++; } } if (this.tokens.matches1AtIndex(index, tt.star)) { // * as index += 2; wildcardNames.push(this.tokens.identifierNameAtIndex(index)); index++; } if (this.tokens.matches1AtIndex(index, tt.braceL)) { const result = this.getNamedImports(index + 1); index = result.newIndex; for (const namedImport of result.namedImports) { // Treat {default as X} as a default import to ensure usage of require interop helper if (namedImport.importedName === "default") { defaultNames.push(namedImport.localName); } else { namedImports.push(namedImport); } } } if (this.tokens.matchesContextualAtIndex(index, ContextualKeyword._from)) { index++; } if (!this.tokens.matches1AtIndex(index, tt.string)) { throw new Error("Expected string token at the end of import statement."); } const path = this.tokens.stringValueAtIndex(index); const importInfo = this.getImportInfo(path); importInfo.defaultNames.push(...defaultNames); importInfo.wildcardNames.push(...wildcardNames); importInfo.namedImports.push(...namedImports); if (defaultNames.length === 0 && wildcardNames.length === 0 && namedImports.length === 0) { importInfo.hasBareImport = true; } } preprocessExportAtIndex(index) { if ( this.tokens.matches2AtIndex(index, tt._export, tt._var) || this.tokens.matches2AtIndex(index, tt._export, tt._let) || this.tokens.matches2AtIndex(index, tt._export, tt._const) ) { this.preprocessVarExportAtIndex(index); } else if ( this.tokens.matches2AtIndex(index, tt._export, tt._function) || this.tokens.matches2AtIndex(index, tt._export, tt._class) ) { const exportName = this.tokens.identifierNameAtIndex(index + 2); this.addExportBinding(exportName, exportName); } else if (this.tokens.matches3AtIndex(index, tt._export, tt.name, tt._function)) { const exportName = this.tokens.identifierNameAtIndex(index + 3); this.addExportBinding(exportName, exportName); } else if (this.tokens.matches2AtIndex(index, tt._export, tt.braceL)) { this.preprocessNamedExportAtIndex(index); } else if (this.tokens.matches2AtIndex(index, tt._export, tt.star)) { this.preprocessExportStarAtIndex(index); } } preprocessVarExportAtIndex(index) { let depth = 0; // Handle cases like `export let {x} = y;`, starting at the open-brace in that case. for (let i = index + 2; ; i++) { if ( this.tokens.matches1AtIndex(i, tt.braceL) || this.tokens.matches1AtIndex(i, tt.dollarBraceL) || this.tokens.matches1AtIndex(i, tt.bracketL) ) { depth++; } else if ( this.tokens.matches1AtIndex(i, tt.braceR) || this.tokens.matches1AtIndex(i, tt.bracketR) ) { depth--; } else if (depth === 0 && !this.tokens.matches1AtIndex(i, tt.name)) { break; } else if (this.tokens.matches1AtIndex(1, tt.eq)) { const endIndex = this.tokens.currentToken().rhsEndIndex; if (endIndex == null) { throw new Error("Expected = token with an end index."); } i = endIndex - 1; } else { const token = this.tokens.tokens[i]; if (isDeclaration(token)) { const exportName = this.tokens.identifierNameAtIndex(i); this.identifierReplacements.set(exportName, `exports.${exportName}`); } } } } /** * Walk this export statement just in case it's an export...from statement. * If it is, combine it into the import info for that path. Otherwise, just * bail out; it'll be handled later. */ preprocessNamedExportAtIndex(index) { // export { index += 2; const {newIndex, namedImports} = this.getNamedImports(index); index = newIndex; if (this.tokens.matchesContextualAtIndex(index, ContextualKeyword._from)) { index++; } else { // Reinterpret "a as b" to be local/exported rather than imported/local. for (const {importedName: localName, localName: exportedName} of namedImports) { this.addExportBinding(localName, exportedName); } return; } if (!this.tokens.matches1AtIndex(index, tt.string)) { throw new Error("Expected string token at the end of import statement."); } const path = this.tokens.stringValueAtIndex(index); const importInfo = this.getImportInfo(path); importInfo.namedExports.push(...namedImports); } preprocessExportStarAtIndex(index) { let exportedName = null; if (this.tokens.matches3AtIndex(index, tt._export, tt.star, tt._as)) { // export * as index += 3; exportedName = this.tokens.identifierNameAtIndex(index); // foo from index += 2; } else { // export * from index += 3; } if (!this.tokens.matches1AtIndex(index, tt.string)) { throw new Error("Expected string token at the end of star export statement."); } const path = this.tokens.stringValueAtIndex(index); const importInfo = this.getImportInfo(path); if (exportedName !== null) { importInfo.exportStarNames.push(exportedName); } else { importInfo.hasStarExport = true; } } getNamedImports(index) { const namedImports = []; while (true) { if (this.tokens.matches1AtIndex(index, tt.braceR)) { index++; break; } const specifierInfo = getImportExportSpecifierInfo(this.tokens, index); index = specifierInfo.endIndex; if (!specifierInfo.isType) { namedImports.push({ importedName: specifierInfo.leftName, localName: specifierInfo.rightName, }); } if (this.tokens.matches2AtIndex(index, tt.comma, tt.braceR)) { index += 2; break; } else if (this.tokens.matches1AtIndex(index, tt.braceR)) { index++; break; } else if (this.tokens.matches1AtIndex(index, tt.comma)) { index++; } else { throw new Error(`Unexpected token: ${JSON.stringify(this.tokens.tokens[index])}`); } } return {newIndex: index, namedImports}; } /** * Get a mutable import info object for this path, creating one if it doesn't * exist yet. */ getImportInfo(path) { const existingInfo = this.importInfoByPath.get(path); if (existingInfo) { return existingInfo; } const newInfo = { defaultNames: [], wildcardNames: [], namedImports: [], namedExports: [], hasBareImport: false, exportStarNames: [], hasStarExport: false, }; this.importInfoByPath.set(path, newInfo); return newInfo; } addExportBinding(localName, exportedName) { if (!this.exportBindingsByLocalName.has(localName)) { this.exportBindingsByLocalName.set(localName, []); } this.exportBindingsByLocalName.get(localName).push(exportedName); } /** * Return the code to use for the import for this path, or the empty string if * the code has already been "claimed" by a previous import. */ claimImportCode(importPath) { const result = this.importsToReplace.get(importPath); this.importsToReplace.set(importPath, ""); return result || ""; } getIdentifierReplacement(identifierName) { return this.identifierReplacements.get(identifierName) || null; } /** * Return a string like `exports.foo = exports.bar`. */ resolveExportBinding(assignedName) { const exportedNames = this.exportBindingsByLocalName.get(assignedName); if (!exportedNames || exportedNames.length === 0) { return null; } return exportedNames.map((exportedName) => `exports.${exportedName}`).join(" = "); } /** * Return all imported/exported names where we might be interested in whether usages of those * names are shadowed. */ getGlobalNames() { return new Set([ ...this.identifierReplacements.keys(), ...this.exportBindingsByLocalName.keys(), ]); } }