457 lines
15 KiB
JavaScript
457 lines
15 KiB
JavaScript
|
|
|
|
|
|
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(),
|
|
]);
|
|
}
|
|
}
|