|
filePath
|
filePath: WorkspacePath
|
Type : WorkspacePath
|
|
|
import { FileSystem, getDecoratorMetadata, getMetadataField, WorkspacePath } from '@angular/cdk/schematics';
import { SyntaxKind } from 'typescript';
// We explicitely use the same type defined by the angular schematic function signature, to avoid typescript version mismatch
type SourceFile = Parameters<typeof getDecoratorMetadata>[0];
type Node = Parameters<typeof getMetadataField>[0];
/**
* This is a rather basic implementation of a ngModule import removal.
* It collects the necessary changes and returns them. If the NgModule or the import was not found an empty changes array is returned.
*
* It currently does not handle edge cases, like it being the only import. (Which simply leaves an empty array of imports)
*/
export function removeSymbolFromNgModuleImports(sourceFile: SourceFile, symbolName: string, fileSystem: FileSystem): RemovalChange[] {
const filePath = fileSystem.resolve(sourceFile.getSourceFile().fileName);
const changes: RemovalChange[] = [];
const nodes = getDecoratorMetadata(sourceFile, 'NgModule', '@angular/core');
const node = nodes[0];
// Find the decorator declaration.
if (!node) {
return [];
}
// Get all the children property assignment of object literals.
const matchingProperties = getMetadataField(node as Node, 'imports');
// Get the last node of the array literal.
if (!matchingProperties || !matchingProperties[0]) {
return [];
}
const children = matchingProperties[0].getChildren();
const importsChildren = children.find((value) => value.kind === SyntaxKind.ArrayLiteralExpression);
if (!importsChildren) {
return [];
}
const importTokens = importsChildren
.getChildren()
?.find((value) => value.kind === SyntaxKind.SyntaxList)
?.getChildren();
if (!importTokens) {
return [];
}
const symbolIndex = importTokens.findIndex((value) => value.kind === SyntaxKind.Identifier && value.getText() === symbolName);
const symbolIdentifier = importTokens[symbolIndex];
const previousToken = importTokens[symbolIndex - 1];
const previousTokenComma = previousToken !== undefined && previousToken.kind === SyntaxKind.CommaToken;
const nextToken = importTokens[symbolIndex + 1];
const nextTokenComma = nextToken !== undefined && nextToken.kind === SyntaxKind.CommaToken;
if (previousTokenComma && nextTokenComma) {
// Perfect! We're surrounded by commas, so it doesn't matter which one we remove...
changes.push({
filePath,
index: nextToken.getStart(),
width: nextToken.getWidth(),
});
} else if (!nextToken || (!nextTokenComma && previousTokenComma)) {
// There is no comma behind us, but there is one in the front.
// Make sure that this is the last entry in the array, then we can remove the comma with us, or leave it, or...
if (!nextToken) {
changes.push({
filePath,
index: previousToken.getStart(),
width: previousToken.getWidth(),
});
} else {
// There is a next token, but it is not a comma. This doesn't look like valid code in the first place...
return [];
}
}
if (symbolIdentifier) {
changes.push({
filePath,
index: symbolIdentifier.getStart(),
width: symbolIdentifier.getWidth(),
});
}
return changes;
}
export interface RemovalChange {
filePath: WorkspacePath;
index: number;
width: number;
}