devsite/node_modules/sucrase/dist/esm/CJSImportProcessor.js

457 lines
15 KiB
JavaScript
Raw Normal View History

2024-07-08 01:49:38 +00:00
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(),
]);
}
}