diff --git a/packages/compiler-cli/src/ngtsc/translator/src/api/import_generator.ts b/packages/compiler-cli/src/ngtsc/translator/src/api/import_generator.ts index d61b776bb75..79a6bb70c64 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/api/import_generator.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/api/import_generator.ts @@ -30,6 +30,17 @@ export interface ImportRequest { * imports are never re-used. E.g. in the linker generator. */ requestedFile: TFile; + + /** + * Specifies an alias under which the symbol can be referenced within + * the file (e.g. `import { symbol as alias } from 'module'`). + * + * !!!Warning!!! passing in this alias is considered unsafe, because the import manager won't + * try to avoid conflicts with existing identifiers in the file if it is specified. As such, + * this option should only be used if the caller has verified that the alias won't conflict + * with anything in the file. + */ + unsafeAliasOverride?: string; } /** diff --git a/packages/compiler-cli/src/ngtsc/translator/src/import_manager/import_manager.ts b/packages/compiler-cli/src/ngtsc/translator/src/import_manager/import_manager.ts index e0d0858481e..0d25a3b9820 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/import_manager/import_manager.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/import_manager/import_manager.ts @@ -212,12 +212,23 @@ export class ImportManager } const exportSymbolName = ts.factory.createIdentifier(request.exportSymbolName); - const fileUniqueName = this.config.generateUniqueIdentifier( - sourceFile, - request.exportSymbolName, - ); - const needsAlias = fileUniqueName !== null; - const specifierName = needsAlias ? fileUniqueName : exportSymbolName; + const fileUniqueName = request.unsafeAliasOverride + ? null + : this.config.generateUniqueIdentifier(sourceFile, request.exportSymbolName); + + let needsAlias: boolean; + let specifierName: ts.Identifier; + + if (request.unsafeAliasOverride) { + needsAlias = true; + specifierName = ts.factory.createIdentifier(request.unsafeAliasOverride); + } else if (fileUniqueName !== null) { + needsAlias = true; + specifierName = fileUniqueName; + } else { + needsAlias = false; + specifierName = exportSymbolName; + } namedImports .get(request.exportModuleSpecifier as ModuleName)! diff --git a/packages/compiler-cli/src/ngtsc/translator/src/import_manager/reuse_generated_imports.ts b/packages/compiler-cli/src/ngtsc/translator/src/import_manager/reuse_generated_imports.ts index 1f1b98a29f8..ff49dacb462 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/import_manager/reuse_generated_imports.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/import_manager/reuse_generated_imports.ts @@ -76,5 +76,5 @@ export function captureGeneratedImport( /** Generates a unique hash for the given import request. */ function hashImportRequest(req: ImportRequest): ImportRequestHash { - return `${req.requestedFile.fileName}:${req.exportModuleSpecifier}:${req.exportSymbolName}` as ImportRequestHash; + return `${req.requestedFile.fileName}:${req.exportModuleSpecifier}:${req.exportSymbolName}${req.unsafeAliasOverride ? ':' + req.unsafeAliasOverride : ''}` as ImportRequestHash; } diff --git a/packages/compiler-cli/src/ngtsc/translator/src/import_manager/reuse_source_file_imports.ts b/packages/compiler-cli/src/ngtsc/translator/src/import_manager/reuse_source_file_imports.ts index 34ce5491a54..6bfaa48c8d8 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/import_manager/reuse_source_file_imports.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/import_manager/reuse_source_file_imports.ts @@ -91,12 +91,20 @@ export function attemptToReuseExistingSourceFileImports( if (ts.isNamedImports(namedBindings) && request.exportSymbolName !== null) { const existingElement = namedBindings.elements.find((e) => { // TODO: Consider re-using type-only imports efficiently. - return ( - !e.isTypeOnly && - (e.propertyName + let nameMatches: boolean; + + if (request.unsafeAliasOverride) { + // If a specific alias is passed, both the original name and alias have to match. + nameMatches = + e.propertyName?.text === request.exportSymbolName && + e.name.text === request.unsafeAliasOverride; + } else { + nameMatches = e.propertyName ? e.propertyName.text === request.exportSymbolName - : e.name.text === request.exportSymbolName) - ); + : e.name.text === request.exportSymbolName; + } + + return !e.isTypeOnly && nameMatches; }); if (existingElement !== undefined) { @@ -122,7 +130,9 @@ export function attemptToReuseExistingSourceFileImports( } const symbolsToBeImported = tracker.updatedImports.get(candidateImportToBeUpdated)!; const propertyName = ts.factory.createIdentifier(request.exportSymbolName); - const fileUniqueAlias = tracker.generateUniqueIdentifier(sourceFile, request.exportSymbolName); + const fileUniqueAlias = request.unsafeAliasOverride + ? ts.factory.createIdentifier(request.unsafeAliasOverride) + : tracker.generateUniqueIdentifier(sourceFile, request.exportSymbolName); // Since it can happen that multiple classes need to be imported within the // specified source file and we want to add the identifiers to the existing diff --git a/packages/compiler-cli/src/ngtsc/translator/test/import_manager_spec.ts b/packages/compiler-cli/src/ngtsc/translator/test/import_manager_spec.ts index 8aafe005599..ea7847fc712 100644 --- a/packages/compiler-cli/src/ngtsc/translator/test/import_manager_spec.ts +++ b/packages/compiler-cli/src/ngtsc/translator/test/import_manager_spec.ts @@ -716,6 +716,181 @@ describe('import manager', () => { `), ); }); + + it('should allow for a specific alias to be passed in', () => { + const {testFile, emit} = createTestProgram(` + import { input } from "@angular/core"; + + input(); + `); + const manager = new ImportManager(); + + const fooRef = manager.addImport({ + exportModuleSpecifier: '@angular/core', + exportSymbolName: 'foo', + unsafeAliasOverride: 'bar', + requestedFile: testFile, + }); + + const res = emit(manager, [ts.factory.createExpressionStatement(fooRef)]); + + expect(res).toBe( + omitLeadingWhitespace(` + import { input, foo as bar } from "@angular/core"; + bar; + input(); + `), + ); + }); + + it('should allow for a specific alias to be passed in when reuse is disabled', () => { + const {testFile, emit} = createTestProgram(` + import { input } from "@angular/core"; + + input(); + `); + const manager = new ImportManager({ + disableOriginalSourceFileReuse: true, + }); + + const fooRef = manager.addImport({ + exportModuleSpecifier: '@angular/core', + exportSymbolName: 'foo', + unsafeAliasOverride: 'bar', + requestedFile: testFile, + }); + + const res = emit(manager, [ts.factory.createExpressionStatement(fooRef)]); + + expect(res).toBe( + omitLeadingWhitespace(` + import { input } from "@angular/core"; + import { foo as bar } from "@angular/core"; + bar; + input(); + `), + ); + }); + + it('should reuse a pre-existing import that has the same name and alias', () => { + const {testFile, emit} = createTestProgram(` + import { foo as bar } from "@angular/core"; + bar(); + `); + const manager = new ImportManager(); + + const fooRef = manager.addImport({ + exportModuleSpecifier: '@angular/core', + exportSymbolName: 'foo', + unsafeAliasOverride: 'bar', + requestedFile: testFile, + }); + + const res = emit(manager, [ts.factory.createExpressionStatement(fooRef)]); + + expect(res).toBe( + omitLeadingWhitespace(` + import { foo as bar } from "@angular/core"; + bar; + bar(); + `), + ); + }); + + it('should reuse import if both the name and alias are the same when added through `addImport`', () => { + const {testFile, emit} = createTestProgram(''); + const manager = new ImportManager(); + + const firstRef = manager.addImport({ + exportModuleSpecifier: '@angular/core', + exportSymbolName: 'foo', + unsafeAliasOverride: 'bar', + requestedFile: testFile, + }); + + const secondRef = manager.addImport({ + exportModuleSpecifier: '@angular/core', + exportSymbolName: 'foo', + unsafeAliasOverride: 'bar', + requestedFile: testFile, + }); + + const res = emit(manager, [ + ts.factory.createExpressionStatement(firstRef), + ts.factory.createExpressionStatement(secondRef), + ]); + + expect(res).toBe( + omitLeadingWhitespace(` + import { foo as bar } from "@angular/core"; + bar; + bar; + `), + ); + }); + + it('should not reuse import if symbol is imported under a different alias', () => { + const {testFile, emit} = createTestProgram(''); + const manager = new ImportManager(); + + const barRef = manager.addImport({ + exportModuleSpecifier: '@angular/core', + exportSymbolName: 'foo', + unsafeAliasOverride: 'bar', + requestedFile: testFile, + }); + + const bazRef = manager.addImport({ + exportModuleSpecifier: '@angular/core', + exportSymbolName: 'foo', + unsafeAliasOverride: 'baz', + requestedFile: testFile, + }); + + const res = emit(manager, [ + ts.factory.createExpressionStatement(barRef), + ts.factory.createExpressionStatement(bazRef), + ]); + + expect(res).toBe( + omitLeadingWhitespace(` + import { foo as bar, foo as baz } from "@angular/core"; + bar; + baz; + `), + ); + }); + + it('should not attempt to de-duplicate imports with an explicit alias', () => { + const {testFile, emit} = createTestProgram(''); + const manager = new ImportManager(); + + const fooRef = manager.addImport({ + exportModuleSpecifier: '@angular/core', + exportSymbolName: 'foo', + requestedFile: testFile, + }); + + const barRef = manager.addImport({ + exportModuleSpecifier: '@angular/core', + exportSymbolName: 'bar', + unsafeAliasOverride: 'foo', + requestedFile: testFile, + }); + + const res = emit(manager, [ + ts.factory.createExpressionStatement(fooRef), + ts.factory.createExpressionStatement(barRef), + ]); + + expect(res).toBe( + omitLeadingWhitespace(` + import { foo, bar as foo } from "@angular/core"; + foo; + foo; + `), + ); + }); }); function createTestProgram(text: string): {