432 lines
12 KiB
TypeScript
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()
|
|
]);
|
|
}
|