Skip to content

Commit 90e69ba

Browse files
crisbetoalan-agius4
authored andcommitted
refactor(@schematics/angular): allow for imports to be inserted under an alias
Expands the `insertImport` utility to allow for imports to be inserted wuth an alias. Also adds unit tests and reworks the internals to be a bit more precise in where they look for matching nodes since previously they could've broken in some cases.
1 parent aaf9ee9 commit 90e69ba

File tree

2 files changed

+111
-25
lines changed

2 files changed

+111
-25
lines changed

packages/schematics/angular/utility/ast-utils.ts

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ import { Change, InsertChange, NoopChange } from './change';
1313
/**
1414
* Add Import `import { symbolName } from fileName` if the import doesn't exit
1515
* already. Assumes fileToEdit can be resolved and accessed.
16-
* @param fileToEdit (file we want to add import to)
17-
* @param symbolName (item to import)
18-
* @param fileName (path to the file)
19-
* @param isDefault (if true, import follows style for importing default exports)
16+
* @param fileToEdit File we want to add import to.
17+
* @param symbolName Item to import.
18+
* @param fileName Path to the file.
19+
* @param isDefault If true, import follows style for importing default exports.
20+
* @param alias Alias that the symbol should be inserted under.
2021
* @return Change
2122
*/
2223
export function insertImport(
@@ -25,46 +26,40 @@ export function insertImport(
2526
symbolName: string,
2627
fileName: string,
2728
isDefault = false,
29+
alias?: string,
2830
): Change {
2931
const rootNode = source;
30-
const allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
32+
const allImports = findNodes(rootNode, ts.isImportDeclaration);
33+
const importExpression = alias ? `${symbolName} as ${alias}` : symbolName;
3134

3235
// get nodes that map to import statements from the file fileName
3336
const relevantImports = allImports.filter((node) => {
34-
// StringLiteral of the ImportDeclaration is the import file (fileName in this case).
35-
const importFiles = node
36-
.getChildren()
37-
.filter(ts.isStringLiteral)
38-
.map((n) => n.text);
39-
40-
return importFiles.filter((file) => file === fileName).length === 1;
37+
return ts.isStringLiteralLike(node.moduleSpecifier) && node.moduleSpecifier.text === fileName;
4138
});
4239

4340
if (relevantImports.length > 0) {
44-
let importsAsterisk = false;
45-
// imports from import file
46-
const imports: ts.Node[] = [];
47-
relevantImports.forEach((n) => {
48-
Array.prototype.push.apply(imports, findNodes(n, ts.SyntaxKind.Identifier));
49-
if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) {
50-
importsAsterisk = true;
51-
}
41+
const hasNamespaceImport = relevantImports.some((node) => {
42+
return node.importClause?.namedBindings?.kind === ts.SyntaxKind.NamespaceImport;
5243
});
5344

5445
// if imports * from fileName, don't add symbolName
55-
if (importsAsterisk) {
46+
if (hasNamespaceImport) {
5647
return new NoopChange();
5748
}
5849

59-
const importTextNodes = imports.filter((n) => (n as ts.Identifier).text === symbolName);
50+
const imports = relevantImports.flatMap((node) => {
51+
return node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)
52+
? node.importClause.namedBindings.elements
53+
: [];
54+
});
6055

6156
// insert import if it's not there
62-
if (importTextNodes.length === 0) {
57+
if (!imports.some((node) => (node.propertyName || node.name).text === symbolName)) {
6358
const fallbackPos =
6459
findNodes(relevantImports[0], ts.SyntaxKind.CloseBraceToken)[0].getStart() ||
6560
findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].getStart();
6661

67-
return insertAfterLastOccurrence(imports, `, ${symbolName}`, fileToEdit, fallbackPos);
62+
return insertAfterLastOccurrence(imports, `, ${importExpression}`, fileToEdit, fallbackPos);
6863
}
6964

7065
return new NoopChange();
@@ -82,7 +77,7 @@ export function insertImport(
8277
const insertAtBeginning = allImports.length === 0 && useStrict.length === 0;
8378
const separator = insertAtBeginning ? '' : ';\n';
8479
const toInsert =
85-
`${separator}import ${open}${symbolName}${close}` +
80+
`${separator}import ${open}${importExpression}${close}` +
8681
` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`;
8782

8883
return insertAfterLastOccurrence(

packages/schematics/angular/utility/ast-utils_spec.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
addSymbolToNgModuleMetadata,
2020
findNodes,
2121
insertAfterLastOccurrence,
22+
insertImport,
2223
} from './ast-utils';
2324

2425
function getTsSource(path: string, content: string): ts.SourceFile {
@@ -685,4 +686,94 @@ describe('ast utils', () => {
685686
});
686687
});
687688
});
689+
690+
describe('insertImport', () => {
691+
const filePath = './src/foo.ts';
692+
693+
it('should insert a new import into a file', () => {
694+
const fileContent = '';
695+
const source = getTsSource(filePath, fileContent);
696+
const change = insertImport(source, filePath, 'Component', '@angular/core');
697+
const result = applyChanges(filePath, fileContent, [change]).trim();
698+
699+
expect(result).toBe(`import { Component } from '@angular/core';`);
700+
});
701+
702+
it('should insert a new import under an alias into a file', () => {
703+
const fileContent = '';
704+
const source = getTsSource(filePath, fileContent);
705+
const change = insertImport(
706+
source,
707+
filePath,
708+
'Component',
709+
'@angular/core',
710+
false,
711+
'NgComponent',
712+
);
713+
const result = applyChanges(filePath, fileContent, [change]).trim();
714+
715+
expect(result).toBe(`import { Component as NgComponent } from '@angular/core';`);
716+
});
717+
718+
it('should reuse imports from the same module without an alias', () => {
719+
const fileContent = `import { Pipe } from '@angular/core';`;
720+
const source = getTsSource(filePath, fileContent);
721+
const change = insertImport(source, filePath, 'Component', '@angular/core');
722+
const result = applyChanges(filePath, fileContent, [change]).trim();
723+
724+
expect(result).toBe(`import { Pipe, Component } from '@angular/core';`);
725+
});
726+
727+
it('should reuse imports from the same module with an alias', () => {
728+
const fileContent = `import { Pipe } from '@angular/core';`;
729+
const source = getTsSource(filePath, fileContent);
730+
const change = insertImport(
731+
source,
732+
filePath,
733+
'Component',
734+
'@angular/core',
735+
false,
736+
'NgComponent',
737+
);
738+
const result = applyChanges(filePath, fileContent, [change]).trim();
739+
740+
expect(result).toBe(`import { Pipe, Component as NgComponent } from '@angular/core';`);
741+
});
742+
743+
it('should reuse imports for the same symbol', () => {
744+
const fileContent = `import { Component } from '@angular/core';`;
745+
const source = getTsSource(filePath, fileContent);
746+
const change = insertImport(source, filePath, 'Component', '@angular/core');
747+
const result = applyChanges(filePath, fileContent, [change]).trim();
748+
749+
expect(result).toBe(fileContent);
750+
});
751+
752+
it('should not insert a new import if the symbol is imported under an alias', () => {
753+
const fileContent = `import { Component as NgComponent } from '@angular/core';`;
754+
const source = getTsSource(filePath, fileContent);
755+
const change = insertImport(source, filePath, 'Component', '@angular/core');
756+
const result = applyChanges(filePath, fileContent, [change]).trim();
757+
758+
expect(result).toBe(fileContent);
759+
});
760+
761+
it('should insert a new default import into a file', () => {
762+
const fileContent = '';
763+
const source = getTsSource(filePath, fileContent);
764+
const change = insertImport(source, filePath, 'core', '@angular/core', true);
765+
const result = applyChanges(filePath, fileContent, [change]).trim();
766+
767+
expect(result).toBe(`import core from '@angular/core';`);
768+
});
769+
770+
it('should not insert an import if there is a namespace import', () => {
771+
const fileContent = `import * as foo from '@angular/core';`;
772+
const source = getTsSource(filePath, fileContent);
773+
const change = insertImport(source, filePath, 'Component', '@angular/core');
774+
const result = applyChanges(filePath, fileContent, [change]).trim();
775+
776+
expect(result).toBe(fileContent);
777+
});
778+
});
688779
});

0 commit comments

Comments
 (0)