mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(compiler-cli): add alias option to the import manager (#57096)
Updates the import manager to allow for a specific alias to be passed in. This is a prerequisite for switching schematics to the new import manager. Note that passing in an alias disables identifier conflict resolution in order to avoid rewriting the alias that was passed in explicitly. For now this is fine since we have a very narrow use case for it, but we may want to revisit it in the future. PR Close #57096
This commit is contained in:
parent
a50a81cb1a
commit
6cbcef237e
5 changed files with 220 additions and 13 deletions
|
|
@ -30,6 +30,17 @@ export interface ImportRequest<TFile> {
|
|||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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)!
|
||||
|
|
|
|||
|
|
@ -76,5 +76,5 @@ export function captureGeneratedImport(
|
|||
|
||||
/** Generates a unique hash for the given import request. */
|
||||
function hashImportRequest(req: ImportRequest<ts.SourceFile>): 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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): {
|
||||
|
|
|
|||
Loading…
Reference in a new issue