432 lines
12 KiB
TypeScript

import {
Rule,
chain,
externalSchematic,
SchematicContext,
Tree
} from '@angular-devkit/schematics';
import * as ts from 'typescript';
import { updateJsonInTree, readJsonInTree, insert } from '@nrwl/workspace';
import { formatFiles } from '@nrwl/workspace';
import {
getSourceNodes,
ReplaceChange
} from '@nrwl/workspace/src/utils/ast-utils';
const addExtensionRecommendations = updateJsonInTree(
'.vscode/extensions.json',
(json: { recommendations?: string[] }) => {
json.recommendations = json.recommendations || [];
[
'nrwl.angular-console',
'angular.ng-template',
'esbenp.prettier-vscode'
].forEach(extension => {
if (!json.recommendations.includes(extension)) {
json.recommendations.push(extension);
}
});
return json;
}
);
function addItemToImport(
path: string,
sourceFile: ts.SourceFile,
printer: ts.Printer,
importStatement: ts.ImportDeclaration,
symbol: string
) {
const newImport = ts.createImportDeclaration(
importStatement.decorators,
importStatement.modifiers,
ts.createImportClause(
importStatement.importClause.name,
ts.createNamedImports([
...(importStatement.importClause.namedBindings as ts.NamedImports)
.elements,
ts.createImportSpecifier(undefined, ts.createIdentifier(symbol))
])
),
importStatement.moduleSpecifier
);
return new ReplaceChange(
path,
importStatement.getStart(sourceFile),
importStatement.getText(sourceFile),
printer.printNode(ts.EmitHint.Unspecified, newImport, sourceFile)
);
}
function isEffectDecorator(decorator: ts.Decorator) {
return (
ts.isCallExpression(decorator.expression) &&
ts.isIdentifier(decorator.expression.expression) &&
decorator.expression.expression.text === 'Effect'
);
}
function getImport(sourceFile: ts.SourceFile, path: string, symbol: string) {
return sourceFile.statements
.filter(ts.isImportDeclaration)
.filter(statement =>
statement.moduleSpecifier.getText(sourceFile).includes(path)
)
.find(statement => {
if (!ts.isNamedImports(statement.importClause.namedBindings)) {
return false;
}
return statement.importClause.namedBindings.elements.some(
element => element.getText(sourceFile) === symbol
);
});
}
function updateOfTypeCode(path: string, sourceFile: ts.SourceFile) {
const effectsImport = getImport(sourceFile, '@ngrx/effects', 'Effect');
if (!effectsImport) {
return [];
}
const effects: ts.PropertyDeclaration[] = [];
const changes: ReplaceChange[] = [];
const printer = ts.createPrinter();
sourceFile.statements
.filter(ts.isClassDeclaration)
.map(clazz =>
clazz.members
.filter(ts.isPropertyDeclaration)
.filter(
member =>
member.decorators && member.decorators.some(isEffectDecorator)
)
)
.forEach(properties => {
effects.push(...properties);
});
effects.forEach(effect => {
if (
ts.isCallExpression(effect.initializer) &&
ts.isPropertyAccessExpression(effect.initializer.expression) &&
effect.initializer.expression.name.text === 'pipe' &&
ts.isCallExpression(effect.initializer.expression.expression) &&
ts.isPropertyAccessExpression(
effect.initializer.expression.expression.expression
) &&
effect.initializer.expression.expression.expression.name.text === 'ofType'
) {
const originalText = effect.initializer.getText(sourceFile);
const ofTypeExpression = ts.createCall(
ts.createIdentifier('ofType'),
effect.initializer.expression.expression.typeArguments,
effect.initializer.expression.expression.arguments
);
const node = ts.createCall(
ts.createPropertyAccess(
effect.initializer.expression.expression.expression.expression,
'pipe'
),
effect.initializer.typeArguments,
ts.createNodeArray([
ofTypeExpression,
...(effect.initializer as ts.CallExpression).arguments
])
);
const newEffect = printer.printNode(
ts.EmitHint.Expression,
node,
sourceFile
);
const change = new ReplaceChange(
path,
effect.initializer.getStart(sourceFile),
originalText,
newEffect
);
changes.push(change);
}
});
if (changes.length > 0) {
changes.unshift(
addItemToImport(path, sourceFile, printer, effectsImport, 'ofType')
);
}
return changes;
}
function getConstructor(
classDeclaration: ts.ClassDeclaration
): ts.ConstructorDeclaration {
return classDeclaration.members.find(ts.isConstructorDeclaration);
}
function getStoreProperty(
sourceFile: ts.SourceFile,
constructor: ts.ConstructorDeclaration
): string {
const storeParameter = constructor.parameters.find(
parameter =>
parameter.type && parameter.type.getText(sourceFile).includes('Store')
);
return storeParameter ? storeParameter.name.getText(sourceFile) : null;
}
function updateSelectorCode(path: string, sourceFile: ts.SourceFile) {
const storeImport = getImport(sourceFile, '@ngrx/store', 'Store');
if (!storeImport) {
return [];
}
const changes: ReplaceChange[] = [];
const printer = ts.createPrinter();
sourceFile.statements
.filter(ts.isClassDeclaration)
.forEach(classDeclaration => {
const constructor = getConstructor(classDeclaration);
if (!constructor) {
return;
}
const storeProperty = getStoreProperty(sourceFile, constructor);
getSourceNodes(sourceFile).forEach(node => {
if (
ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
ts.isPropertyAccessExpression(node.expression.expression) &&
ts.isIdentifier(node.expression.name) &&
ts.isIdentifier(node.expression.expression.name) &&
node.expression.name.getText(sourceFile) === 'select' &&
node.expression.expression.name.getText(sourceFile) ===
storeProperty &&
node.expression.expression.expression.kind ===
ts.SyntaxKind.ThisKeyword
) {
const newExpression = ts.createCall(
ts.createPropertyAccess(
ts.createPropertyAccess(
ts.createIdentifier('this'),
ts.createIdentifier(storeProperty)
),
ts.createIdentifier('pipe')
),
[],
[
ts.createCall(
ts.createIdentifier('select'),
node.typeArguments,
node.arguments
)
]
);
const newNode = printer.printNode(
ts.EmitHint.Expression,
newExpression,
sourceFile
);
changes.push(
new ReplaceChange(
path,
node.getStart(sourceFile),
node.getText(sourceFile),
newNode
)
);
}
});
});
if (changes.length > 0) {
changes.unshift(
addItemToImport(path, sourceFile, printer, storeImport, 'select')
);
}
return changes;
}
function migrateNgrx(host: Tree) {
const ngrxVersion = readJsonInTree(host, 'package.json').dependencies[
'@ngrx/store'
];
if (
ngrxVersion &&
!(
ngrxVersion.startsWith('6.') ||
ngrxVersion.startsWith('~6.') ||
ngrxVersion.startsWith('^6.')
)
) {
return host;
}
host.visit(path => {
if (!path.endsWith('.ts')) {
return;
}
let sourceFile = ts.createSourceFile(
path,
host.read(path).toString(),
ts.ScriptTarget.Latest
);
if (sourceFile.isDeclarationFile) {
return;
}
insert(host, path, updateOfTypeCode(path, sourceFile));
sourceFile = ts.createSourceFile(
path,
host.read(path).toString(),
ts.ScriptTarget.Latest
);
insert(host, path, updateSelectorCode(path, sourceFile));
sourceFile = ts.createSourceFile(
path,
host.read(path).toString(),
ts.ScriptTarget.Latest
);
insert(host, path, cleanUpDoublePipes(path, sourceFile));
});
}
function cleanUpDoublePipes(
path: string,
sourceFile: ts.SourceFile
): ReplaceChange[] {
const changes: ReplaceChange[] = [];
const printer = ts.createPrinter();
getSourceNodes(sourceFile).forEach(node => {
if (
ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
ts.isCallExpression(node.expression.expression) &&
ts.isPropertyAccessExpression(node.expression.expression.expression) &&
node.expression.name.text === 'pipe' &&
node.expression.expression.expression.name.text === 'pipe'
) {
const singlePipe = ts.createCall(
node.expression.expression.expression,
node.typeArguments,
[...node.expression.expression.arguments, ...node.arguments]
);
changes.push(
new ReplaceChange(
path,
node.getStart(sourceFile),
node.getText(sourceFile),
printer.printNode(ts.EmitHint.Expression, singlePipe, sourceFile)
)
);
}
});
return changes;
}
const updateNgrx = updateJsonInTree('package.json', json => {
json.devDependencies = json.devDependencies || {};
json.dependencies = json.dependencies || {};
json.dependencies = {
...json.dependencies,
'@ngrx/effects': '7.2.0',
'@ngrx/router-store': '7.2.0',
'@ngrx/store': '7.2.0'
};
json.devDependencies = {
...json.devDependencies,
'@ngrx/schematics': '7.2.0',
'@ngrx/store-devtools': '7.2.0'
};
return json;
});
const addDotEnv = updateJsonInTree('package.json', json => {
json.devDependencies = json.devDependencies || {};
json.devDependencies = {
...json.devDependencies,
dotenv: '6.2.0'
};
return json;
});
const setDefaults = updateJsonInTree('angular.json', json => {
if (!json.schematics) {
json.schematics = {};
}
if (!json.schematics['@nrwl/schematics:library']) {
json.schematics['@nrwl/schematics:library'] = {};
}
if (!json.schematics['@nrwl/schematics:library'].unitTestRunner) {
json.schematics['@nrwl/schematics:library'].unitTestRunner = 'karma';
}
if (!json.schematics['@nrwl/schematics:application']) {
json.schematics['@nrwl/schematics:application'] = {};
}
if (!json.schematics['@nrwl/schematics:application'].unitTestRunner) {
json.schematics['@nrwl/schematics:application'].unitTestRunner = 'karma';
}
if (!json.schematics['@nrwl/schematics:application'].e2eTestRunner) {
json.schematics['@nrwl/schematics:application'].e2eTestRunner =
'protractor';
}
if (!json.schematics['@nrwl/schematics:node-application']) {
json.schematics['@nrwl/schematics:node-application'] = {};
}
if (!json.schematics['@nrwl/schematics:node-application'].framework) {
json.schematics['@nrwl/schematics:node-application'].framework = 'express';
}
return json;
});
const updateAngularCLI = chain([
externalSchematic('@schematics/update', 'update', {
packages: ['@angular/cli'],
from: '7.2.2',
to: '7.3.1',
force: true
}),
updateJsonInTree('package.json', json => {
json.devDependencies = json.devDependencies || {};
json.devDependencies = {
...json.devDependencies,
'@angular/cli': '7.3.1',
'@angular-devkit/build-angular': '~0.13.1'
};
return json;
})
]);
export default function(): Rule {
return chain([
addExtensionRecommendations,
addDotEnv,
updateAngularCLI,
migrateNgrx,
updateNgrx,
setDefaults,
formatFiles()
]);
}