Skip to content

Commit

Permalink
fix(migrations): combine newly-added imports in import manager (angul…
Browse files Browse the repository at this point in the history
…ar#48620)

Fixes that imports weren't being combined in the `ImportManager` when multiple new imports are added for the same file. This wasn't a problem for previous schematics that used the manager, but it'll come up in some of the new ones.

Also moves the logic for writing new imports into `recordChanges`, instead of `addImportToSourceFile`.

PR Close angular#48620
  • Loading branch information
crisbeto authored and devversion committed Jan 2, 2023
1 parent 27da733 commit cc284af
Showing 1 changed file with 61 additions and 20 deletions.
81 changes: 61 additions & 20 deletions packages/core/schematics/utils/import_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ export class ImportManager {
new Map<ts.ImportDeclaration, {propertyName?: ts.Identifier, importName: ts.Identifier}[]>();
/** Map of source-files and their previously used identifier names. */
private usedIdentifierNames = new Map<ts.SourceFile, string[]>();
/** Map of source files and the new imports that have to be added to them. */
private newImports: Map<ts.SourceFile, {
importStartIndex: number,
defaultImports: Map<string, ts.Identifier>,
namedImports: Map<string, ts.ImportSpecifier[]>,
}> = new Map();

/**
* Array of previously resolved symbol imports. Cache can be re-used to return
* the same identifier without checking the source-file again.
Expand Down Expand Up @@ -142,37 +149,34 @@ export class ImportManager {
}

let identifier: ts.Identifier|null = null;
let newImport: ts.ImportDeclaration|null = null;

if (!this.newImports.has(sourceFile)) {
this.newImports.set(sourceFile, {
importStartIndex,
defaultImports: new Map(),
namedImports: new Map(),
});
}

if (symbolName) {
const propertyIdentifier = ts.factory.createIdentifier(symbolName);
const generatedUniqueIdentifier = this._getUniqueIdentifier(sourceFile, symbolName);
const needsGeneratedUniqueName = generatedUniqueIdentifier.text !== symbolName;
const importMap = this.newImports.get(sourceFile)!.namedImports;
identifier = needsGeneratedUniqueName ? generatedUniqueIdentifier : propertyIdentifier;

newImport = createImportDeclaration(
undefined,
ts.factory.createImportClause(
false, undefined,
ts.factory.createNamedImports([ts.factory.createImportSpecifier(
false, needsGeneratedUniqueName ? propertyIdentifier : undefined, identifier)])),
ts.factory.createStringLiteral(moduleName));
if (!importMap.has(moduleName)) {
importMap.set(moduleName, []);
}

importMap.get(moduleName)!.push(ts.factory.createImportSpecifier(
false, needsGeneratedUniqueName ? propertyIdentifier : undefined, identifier));
} else {
const importMap = this.newImports.get(sourceFile)!.defaultImports;
identifier = this._getUniqueIdentifier(sourceFile, 'defaultExport');
newImport = createImportDeclaration(
undefined, ts.factory.createImportClause(false, identifier, undefined),
ts.factory.createStringLiteral(moduleName));
importMap.set(moduleName, identifier);
}

const newImportText = this.printer.printNode(ts.EmitHint.Unspecified, newImport, sourceFile);
// If the import is generated at the start of the source file, we want to add
// a new-line after the import. Otherwise if the import is generated after an
// existing import, we need to prepend a new-line so that the import is not on
// the same line as the existing import anchor.
this.getUpdateRecorder(sourceFile)
.addNewImport(
importStartIndex, importStartIndex === 0 ? `${newImportText}\n` : `\n${newImportText}`);

// Keep track of all generated imports so that we don't generate duplicate
// similar imports as these can't be statically analyzed in the source-file yet.
this.importCache.push({sourceFile, symbolName, moduleName, identifier});
Expand Down Expand Up @@ -200,6 +204,30 @@ export class ImportManager {
this.printer.printNode(ts.EmitHint.Unspecified, newNamedBindings, sourceFile);
recorder.updateExistingImport(namedBindings, newNamedBindingsText);
});

this.newImports.forEach(({importStartIndex, defaultImports, namedImports}, sourceFile) => {
const recorder = this.getUpdateRecorder(sourceFile);

defaultImports.forEach((identifier, moduleName) => {
const newImport = createImportDeclaration(
undefined, ts.factory.createImportClause(false, identifier, undefined),
ts.factory.createStringLiteral(moduleName));

recorder.addNewImport(
importStartIndex, this._getNewImportText(importStartIndex, newImport, sourceFile));
});

namedImports.forEach((specifiers, moduleName) => {
const newImport = createImportDeclaration(
undefined,
ts.factory.createImportClause(
false, undefined, ts.factory.createNamedImports(specifiers)),
ts.factory.createStringLiteral(moduleName));

recorder.addNewImport(
importStartIndex, this._getNewImportText(importStartIndex, newImport, sourceFile));
});
});
}

/** Gets an unique identifier with a base name for the given source file. */
Expand Down Expand Up @@ -260,6 +288,19 @@ export class ImportManager {
}
return commentRanges[commentRanges.length - 1]!.end;
}

/** Gets the text that should be added to the file for a newly-created import declaration. */
private _getNewImportText(
importStartIndex: number, newImport: ts.ImportDeclaration,
sourceFile: ts.SourceFile): string {
const text = this.printer.printNode(ts.EmitHint.Unspecified, newImport, sourceFile);

// If the import is generated at the start of the source file, we want to add
// a new-line after the import. Otherwise if the import is generated after an
// existing import, we need to prepend a new-line so that the import is not on
// the same line as the existing import anchor
return importStartIndex === 0 ? `${text}\n` : `\n${text}`;
}
}

/**
Expand Down

0 comments on commit cc284af

Please sign in to comment.