Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ export class ImportManager
}
> = new Map();

/**
* Keeps track of imports marked for removal. The root-level key is the file from which the
* import should be removed, the inner map key is the name of the module from which the symbol
* is being imported. The value of the inner map is a set of symbol names that should be removed.
* Note! the inner map tracks the original names of the imported symbols, not their local aliases.
*/
private removedImports: Map<ts.SourceFile, Map<ModuleName, Set<string>>> = new Map();
Comment thread
crisbeto marked this conversation as resolved.
Outdated
private nextUniqueIndex = 0;
private config: ImportManagerConfig;

Expand Down Expand Up @@ -142,6 +149,14 @@ export class ImportManager
);
}

// Remove the newly-added import from the set of removed imports.
if (request.exportSymbolName !== null && !request.asTypeReference) {
this.removedImports
.get(request.requestedFile)
?.get(request.exportModuleSpecifier as ModuleName)
?.delete(request.exportSymbolName);
}

// Attempt to re-use previous identical import requests.
const previousGeneratedImportRef = attemptToReuseGeneratedImports(
this.reuseGeneratedImportsTracker,
Expand All @@ -157,6 +172,33 @@ export class ImportManager
return createImportReference(!!request.asTypeReference, resultImportRef);
}

/**
* Marks all imported symbols with a specific name for removal.
* Call `addImport` to undo this operation.
Comment thread
devversion marked this conversation as resolved.
Outdated
* @param requestedFile File from which to remove the imports.
* @param exportSymbolName Declared name of the symbol being removed.
* @param moduleSpecifier Module from which the symbol is being imported.
*/
removeImport(
requestedFile: ts.SourceFile,
exportSymbolName: string,
moduleSpecifier: string,
): void {
let moduleMap = this.removedImports.get(requestedFile);
if (!moduleMap) {
moduleMap = new Map();
this.removedImports.set(requestedFile, moduleMap);
}

let removedSymbols = moduleMap.get(moduleSpecifier as ModuleName);
if (!removedSymbols) {
removedSymbols = new Set();
moduleMap.set(moduleSpecifier as ModuleName, removedSymbols);
}

removedSymbols.add(exportSymbolName);
}

private _generateNewImport(
request: ImportRequest<ts.SourceFile>,
): ts.Identifier | [ts.Identifier, ts.Identifier] {
Expand Down Expand Up @@ -255,10 +297,13 @@ export class ImportManager
updatedImports: Map<ts.NamedImports, ts.NamedImports>;
newImports: Map<string, ts.ImportDeclaration[]>;
reusedOriginalAliasDeclarations: Set<AliasImportDeclaration>;
deletedImports: Set<ts.ImportDeclaration>;
} {
const affectedFiles = new Set<string>();
const updatedImportsResult = new Map<ts.NamedImports, ts.NamedImports>();
const newImportsResult = new Map<string, ts.ImportDeclaration[]>();
const deletedImports = new Set<ts.ImportDeclaration>();
const importDeclarationsPerFile = new Map<ts.SourceFile, ts.ImportDeclaration[]>();
Comment thread
devversion marked this conversation as resolved.
Outdated

const addNewImport = (fileName: string, importDecl: ts.ImportDeclaration) => {
affectedFiles.add(fileName);
Expand All @@ -271,22 +316,72 @@ export class ImportManager

// Collect original source file imports that need to be updated.
this.reuseSourceFileImportsTracker.updatedImports.forEach((expressions, importDecl) => {
const sourceFile = importDecl.getSourceFile();
const namedBindings = importDecl.importClause!.namedBindings as ts.NamedImports;
const newNamedBindings = ts.factory.updateNamedImports(
namedBindings,
namedBindings.elements.concat(
const moduleName = (importDecl.moduleSpecifier as ts.StringLiteral).text as ModuleName;
const newElements = namedBindings.elements
.concat(
expressions.map(({propertyName, fileUniqueAlias}) =>
ts.factory.createImportSpecifier(
false,
fileUniqueAlias !== null ? propertyName : undefined,
fileUniqueAlias ?? propertyName,
),
),
),
);
)
.filter((specifier) => this._canAddSpecifier(sourceFile, moduleName, specifier));

affectedFiles.add(sourceFile.fileName);

if (newElements.length === 0) {
deletedImports.add(importDecl);
} else {
updatedImportsResult.set(
namedBindings,
ts.factory.updateNamedImports(namedBindings, newElements),
);
}
});

affectedFiles.add(importDecl.getSourceFile().fileName);
updatedImportsResult.set(namedBindings, newNamedBindings);
this.removedImports.forEach((removeMap, sourceFile) => {
if (removeMap.size === 0) {
return;
}

let allImports = importDeclarationsPerFile.get(sourceFile);

if (!allImports) {
allImports = sourceFile.statements.filter(ts.isImportDeclaration);
importDeclarationsPerFile.set(sourceFile, allImports);
}

for (const node of allImports) {
if (
!node.importClause?.namedBindings ||
!ts.isNamedImports(node.importClause.namedBindings) ||
this.reuseSourceFileImportsTracker.updatedImports.has(node) ||
deletedImports.has(node)
) {
continue;
}

const namedBindings = node.importClause.namedBindings;
const moduleName = (node.moduleSpecifier as ts.StringLiteral).text as ModuleName;
const newImports = namedBindings.elements.filter((specifier) =>
this._canAddSpecifier(sourceFile, moduleName, specifier),
);

if (newImports.length === 0) {
affectedFiles.add(sourceFile.fileName);
deletedImports.add(node);
} else if (newImports.length !== namedBindings.elements.length) {
affectedFiles.add(sourceFile.fileName);
updatedImportsResult.set(
namedBindings,
ts.factory.updateNamedImports(namedBindings, newImports),
);
}
}
});

// Collect all new imports to be added. Named imports, namespace imports or side-effects.
Expand Down Expand Up @@ -324,17 +419,23 @@ export class ImportManager
});

namedImports.forEach((specifiers, moduleName) => {
const newImport = ts.factory.createImportDeclaration(
undefined,
ts.factory.createImportClause(
false,
undefined,
ts.factory.createNamedImports(specifiers),
),
ts.factory.createStringLiteral(moduleName, useSingleQuotes),
const filteredSpecifiers = specifiers.filter((specifier) =>
this._canAddSpecifier(sourceFile, moduleName, specifier),
);

addNewImport(fileName, newImport);
if (filteredSpecifiers.length > 0) {
const newImport = ts.factory.createImportDeclaration(
undefined,
ts.factory.createImportClause(
false,
undefined,
ts.factory.createNamedImports(filteredSpecifiers),
),
ts.factory.createStringLiteral(moduleName, useSingleQuotes),
);

addNewImport(fileName, newImport);
}
});
});

Expand All @@ -343,6 +444,7 @@ export class ImportManager
newImports: newImportsResult,
updatedImports: updatedImportsResult,
reusedOriginalAliasDeclarations: this.reuseSourceFileImportsTracker.reusedAliasDeclarations,
deletedImports,
};
}

Expand Down Expand Up @@ -387,6 +489,17 @@ export class ImportManager
}
return this.newImports.get(file)!;
}

private _canAddSpecifier(
sourceFile: ts.SourceFile,
moduleSpecifier: ModuleName,
specifier: ts.ImportSpecifier,
): boolean {
return !this.removedImports
.get(sourceFile)
?.get(moduleSpecifier)
?.has((specifier.propertyName || specifier.name).text);
}
}

/** Creates an import reference based on the given identifier, or nested access. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,20 @@ import type {ImportManager} from './import_manager';
* - The transform updates existing imports with new symbols to be added.
* - The transform adds new necessary imports.
* - The transform inserts additional optional statements after imports.
* - The transform deletes any nodes that are marked for deletion by the manager.
*/
export function createTsTransformForImportManager(
manager: ImportManager,
extraStatementsForFiles?: Map<string, ts.Statement[]>,
): ts.TransformerFactory<ts.SourceFile> {
return (ctx) => {
const {affectedFiles, newImports, updatedImports, reusedOriginalAliasDeclarations} =
manager.finalize();
const {
affectedFiles,
newImports,
updatedImports,
reusedOriginalAliasDeclarations,
deletedImports,
} = manager.finalize();

// If we re-used existing source file alias declarations, mark those as referenced so TypeScript
// doesn't drop these thinking they are unused.
Expand All @@ -45,12 +51,16 @@ export function createTsTransformForImportManager(
}
}

const visitStatement: ts.Visitor<ts.Node> = (node) => {
if (
!ts.isImportDeclaration(node) ||
node.importClause === undefined ||
!ts.isImportClause(node.importClause)
) {
const visitStatement: ts.Visitor<ts.Node, ts.Node | undefined> = (node) => {
if (!ts.isImportDeclaration(node)) {
return node;
}

if (deletedImports.has(node)) {
return undefined;
}

if (node.importClause === undefined || !ts.isImportClause(node.importClause)) {
return node;
}

Expand Down
Loading