angular-cn/packages/core/schematics/migrations/injectable-pipe/index.ts

90 lines
3.6 KiB
TypeScript

/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
import {dirname, relative} from 'path';
import * as ts from 'typescript';
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
import {parseTsconfigFile} from '../../utils/typescript/parse_tsconfig';
import {InjectablePipeVisitor} from './angular/injectable_pipe_visitor';
import {INJECTABLE_DECORATOR_NAME, addNamedImport, getNamedImports} from './util';
/**
* Runs a migration over a TypeScript project that adds an `@Injectable`
* annotation to all classes that have `@Pipe`.
*/
export default function(): Rule {
return (tree: Tree) => {
const {buildPaths, testPaths} = getProjectTsConfigPaths(tree);
const basePath = process.cwd();
const allPaths = [...buildPaths, ...testPaths];
if (!allPaths.length) {
throw new SchematicsException(
'Could not find any tsconfig file. Cannot add Injectable annotation to pipes.');
}
for (const tsconfigPath of allPaths) {
runInjectablePipeMigration(tree, tsconfigPath, basePath);
}
};
}
function runInjectablePipeMigration(tree: Tree, tsconfigPath: string, basePath: string) {
const parsed = parseTsconfigFile(tsconfigPath, dirname(tsconfigPath));
const host = ts.createCompilerHost(parsed.options, true);
// We need to overwrite the host "readFile" method, as we want the TypeScript
// program to be based on the file contents in the virtual file tree. Otherwise
// if we run the migration for multiple tsconfig files which have intersecting
// source files, it can end up updating query definitions multiple times.
host.readFile = fileName => {
const buffer = tree.read(relative(basePath, fileName));
return buffer ? buffer.toString() : undefined;
};
const program = ts.createProgram(parsed.fileNames, parsed.options, host);
const typeChecker = program.getTypeChecker();
const visitor = new InjectablePipeVisitor(typeChecker);
const rootSourceFiles = program.getRootFileNames().map(f => program.getSourceFile(f) !);
const printer = ts.createPrinter();
rootSourceFiles.forEach(sourceFile => visitor.visitNode(sourceFile));
visitor.missingInjectablePipes.forEach(data => {
const {classDeclaration, importDeclarationMissingImport} = data;
const sourceFile = classDeclaration.getSourceFile();
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
// Note that we don't need to go through the AST to insert the decorator, because the change
// is pretty basic. Also this has a better chance of preserving the user's formatting.
update.insertLeft(classDeclaration.getStart(), `@${INJECTABLE_DECORATOR_NAME}()\n`);
// Add @Injectable to the imports if it isn't imported already. Note that this doesn't deal with
// the case where there aren't any imports for `@angular/core` at all. We don't need to handle
// it because the Pipe decorator won't be recognized if it hasn't been imported from Angular.
if (importDeclarationMissingImport) {
const namedImports = getNamedImports(importDeclarationMissingImport);
if (namedImports) {
update.remove(namedImports.getStart(), namedImports.getWidth());
update.insertRight(
namedImports.getStart(),
printer.printNode(
ts.EmitHint.Unspecified,
addNamedImport(importDeclarationMissingImport, INJECTABLE_DECORATOR_NAME),
sourceFile));
}
}
tree.commitUpdate(update);
});
}