diff --git a/modules/data/schematics-core/index.ts b/modules/data/schematics-core/index.ts index 4f59388898..327bb6460d 100644 --- a/modules/data/schematics-core/index.ts +++ b/modules/data/schematics-core/index.ts @@ -81,4 +81,12 @@ export { addPackageToPackageJson } from './utility/package'; export { platformVersion } from './utility/libs-version'; -export { visitTSSourceFiles, visitNgModuleImports } from './utility/visitors'; +export { + visitTSSourceFiles, + visitNgModuleImports, + visitNgModuleExports, + visitComponents, + visitDecorator, + visitNgModules, + visitTemplates, +} from './utility/visitors'; diff --git a/modules/data/schematics-core/utility/visitors.ts b/modules/data/schematics-core/utility/visitors.ts index a8c2b4b3d7..beb2c43a0b 100644 --- a/modules/data/schematics-core/utility/visitors.ts +++ b/modules/data/schematics-core/utility/visitors.ts @@ -1,4 +1,5 @@ import * as ts from 'typescript'; +import { normalize, resolve } from '@angular-devkit/core'; import { Tree, DirEntry } from '@angular-devkit/schematics'; export function visitTSSourceFiles( @@ -17,6 +18,190 @@ export function visitTSSourceFiles( return result; } +export function visitTemplates( + tree: Tree, + visitor: ( + template: { + fileName: string; + content: string; + inline: boolean; + start: number; + }, + tree: Tree + ) => void +): void { + visitTSSourceFiles(tree, source => { + visitComponents(source, (_, decoratorExpressionNode) => { + ts.forEachChild(decoratorExpressionNode, function findTemplates(n) { + if (ts.isPropertyAssignment(n) && ts.isIdentifier(n.name)) { + if ( + n.name.text === 'template' && + ts.isStringLiteralLike(n.initializer) + ) { + // Need to add an offset of one to the start because the template quotes are + // not part of the template content. + const templateStartIdx = n.initializer.getStart() + 1; + visitor( + { + fileName: source.fileName, + content: n.initializer.text, + inline: true, + start: templateStartIdx, + }, + tree + ); + return; + } else if ( + n.name.text === 'templateUrl' && + ts.isStringLiteralLike(n.initializer) + ) { + const parts = normalize(source.fileName) + .split('/') + .slice(0, -1); + const templatePath = resolve( + normalize(parts.join('/')), + normalize(n.initializer.text) + ); + if (!tree.exists(templatePath)) { + return; + } + + const fileContent = tree.read(templatePath); + if (!fileContent) { + return; + } + + visitor( + { + fileName: templatePath, + content: fileContent.toString(), + inline: false, + start: 0, + }, + tree + ); + return; + } + } + + ts.forEachChild(n, findTemplates); + }); + }); + }); +} + +export function visitNgModuleImports( + sourceFile: ts.SourceFile, + callback: ( + importNode: ts.PropertyAssignment, + elementExpressions: ts.NodeArray + ) => void +) { + visitNgModuleProperty(sourceFile, callback, 'imports'); +} + +export function visitNgModuleExports( + sourceFile: ts.SourceFile, + callback: ( + exportNode: ts.PropertyAssignment, + elementExpressions: ts.NodeArray + ) => void +) { + visitNgModuleProperty(sourceFile, callback, 'exports'); +} + +function visitNgModuleProperty( + sourceFile: ts.SourceFile, + callback: ( + nodes: ts.PropertyAssignment, + elementExpressions: ts.NodeArray + ) => void, + property: string +) { + visitNgModules(sourceFile, (_, decoratorExpressionNode) => { + ts.forEachChild(decoratorExpressionNode, function findTemplates(n) { + if ( + ts.isPropertyAssignment(n) && + ts.isIdentifier(n.name) && + n.name.text === property && + ts.isArrayLiteralExpression(n.initializer) + ) { + callback(n, n.initializer.elements); + return; + } + + ts.forEachChild(n, findTemplates); + }); + }); +} +export function visitComponents( + sourceFile: ts.SourceFile, + callback: ( + classDeclarationNode: ts.ClassDeclaration, + decoratorExpressionNode: ts.ObjectLiteralExpression + ) => void +) { + visitDecorator(sourceFile, 'Component', callback); +} + +export function visitNgModules( + sourceFile: ts.SourceFile, + callback: ( + classDeclarationNode: ts.ClassDeclaration, + decoratorExpressionNode: ts.ObjectLiteralExpression + ) => void +) { + visitDecorator(sourceFile, 'NgModule', callback); +} + +export function visitDecorator( + sourceFile: ts.SourceFile, + decoratorName: string, + callback: ( + classDeclarationNode: ts.ClassDeclaration, + decoratorExpressionNode: ts.ObjectLiteralExpression + ) => void +) { + ts.forEachChild(sourceFile, function findClassDeclaration(node) { + if (!ts.isClassDeclaration(node)) { + ts.forEachChild(node, findClassDeclaration); + } + + const classDeclarationNode = node as ts.ClassDeclaration; + + if ( + !classDeclarationNode.decorators || + !classDeclarationNode.decorators.length + ) { + return; + } + + const componentDecorator = classDeclarationNode.decorators.find(d => { + return ( + ts.isCallExpression(d.expression) && + ts.isIdentifier(d.expression.expression) && + d.expression.expression.text === decoratorName + ); + }); + + if (!componentDecorator) { + return; + } + + const { expression } = componentDecorator; + if (!ts.isCallExpression(expression)) { + return; + } + + const [arg] = expression.arguments; + if (!ts.isObjectLiteralExpression(arg)) { + return; + } + + callback(classDeclarationNode, arg); + }); +} + function* visit(directory: DirEntry): IterableIterator { for (const path of directory.subfiles) { if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { @@ -42,31 +227,3 @@ function* visit(directory: DirEntry): IterableIterator { yield* visit(directory.dir(path)); } } - -export function visitNgModuleImports( - sourceFile: ts.SourceFile, - callback: ( - importNode: ts.PropertyAssignment, - elementExpressions: ts.NodeArray - ) => void -) { - ts.forEachChild(sourceFile, function findDecorator(node) { - if (!ts.isDecorator(node)) { - ts.forEachChild(node, findDecorator); - return; - } - - ts.forEachChild(node, function findImportsNode(n) { - if ( - ts.isPropertyAssignment(n) && - ts.isArrayLiteralExpression(n.initializer) && - ts.isIdentifier(n.name) && - n.name.text === 'imports' - ) { - callback(n, n.initializer.elements); - } - - ts.forEachChild(n, findImportsNode); - }); - }); -} diff --git a/modules/effects/schematics-core/index.ts b/modules/effects/schematics-core/index.ts index 4f59388898..327bb6460d 100644 --- a/modules/effects/schematics-core/index.ts +++ b/modules/effects/schematics-core/index.ts @@ -81,4 +81,12 @@ export { addPackageToPackageJson } from './utility/package'; export { platformVersion } from './utility/libs-version'; -export { visitTSSourceFiles, visitNgModuleImports } from './utility/visitors'; +export { + visitTSSourceFiles, + visitNgModuleImports, + visitNgModuleExports, + visitComponents, + visitDecorator, + visitNgModules, + visitTemplates, +} from './utility/visitors'; diff --git a/modules/effects/schematics-core/utility/visitors.ts b/modules/effects/schematics-core/utility/visitors.ts index a8c2b4b3d7..beb2c43a0b 100644 --- a/modules/effects/schematics-core/utility/visitors.ts +++ b/modules/effects/schematics-core/utility/visitors.ts @@ -1,4 +1,5 @@ import * as ts from 'typescript'; +import { normalize, resolve } from '@angular-devkit/core'; import { Tree, DirEntry } from '@angular-devkit/schematics'; export function visitTSSourceFiles( @@ -17,6 +18,190 @@ export function visitTSSourceFiles( return result; } +export function visitTemplates( + tree: Tree, + visitor: ( + template: { + fileName: string; + content: string; + inline: boolean; + start: number; + }, + tree: Tree + ) => void +): void { + visitTSSourceFiles(tree, source => { + visitComponents(source, (_, decoratorExpressionNode) => { + ts.forEachChild(decoratorExpressionNode, function findTemplates(n) { + if (ts.isPropertyAssignment(n) && ts.isIdentifier(n.name)) { + if ( + n.name.text === 'template' && + ts.isStringLiteralLike(n.initializer) + ) { + // Need to add an offset of one to the start because the template quotes are + // not part of the template content. + const templateStartIdx = n.initializer.getStart() + 1; + visitor( + { + fileName: source.fileName, + content: n.initializer.text, + inline: true, + start: templateStartIdx, + }, + tree + ); + return; + } else if ( + n.name.text === 'templateUrl' && + ts.isStringLiteralLike(n.initializer) + ) { + const parts = normalize(source.fileName) + .split('/') + .slice(0, -1); + const templatePath = resolve( + normalize(parts.join('/')), + normalize(n.initializer.text) + ); + if (!tree.exists(templatePath)) { + return; + } + + const fileContent = tree.read(templatePath); + if (!fileContent) { + return; + } + + visitor( + { + fileName: templatePath, + content: fileContent.toString(), + inline: false, + start: 0, + }, + tree + ); + return; + } + } + + ts.forEachChild(n, findTemplates); + }); + }); + }); +} + +export function visitNgModuleImports( + sourceFile: ts.SourceFile, + callback: ( + importNode: ts.PropertyAssignment, + elementExpressions: ts.NodeArray + ) => void +) { + visitNgModuleProperty(sourceFile, callback, 'imports'); +} + +export function visitNgModuleExports( + sourceFile: ts.SourceFile, + callback: ( + exportNode: ts.PropertyAssignment, + elementExpressions: ts.NodeArray + ) => void +) { + visitNgModuleProperty(sourceFile, callback, 'exports'); +} + +function visitNgModuleProperty( + sourceFile: ts.SourceFile, + callback: ( + nodes: ts.PropertyAssignment, + elementExpressions: ts.NodeArray + ) => void, + property: string +) { + visitNgModules(sourceFile, (_, decoratorExpressionNode) => { + ts.forEachChild(decoratorExpressionNode, function findTemplates(n) { + if ( + ts.isPropertyAssignment(n) && + ts.isIdentifier(n.name) && + n.name.text === property && + ts.isArrayLiteralExpression(n.initializer) + ) { + callback(n, n.initializer.elements); + return; + } + + ts.forEachChild(n, findTemplates); + }); + }); +} +export function visitComponents( + sourceFile: ts.SourceFile, + callback: ( + classDeclarationNode: ts.ClassDeclaration, + decoratorExpressionNode: ts.ObjectLiteralExpression + ) => void +) { + visitDecorator(sourceFile, 'Component', callback); +} + +export function visitNgModules( + sourceFile: ts.SourceFile, + callback: ( + classDeclarationNode: ts.ClassDeclaration, + decoratorExpressionNode: ts.ObjectLiteralExpression + ) => void +) { + visitDecorator(sourceFile, 'NgModule', callback); +} + +export function visitDecorator( + sourceFile: ts.SourceFile, + decoratorName: string, + callback: ( + classDeclarationNode: ts.ClassDeclaration, + decoratorExpressionNode: ts.ObjectLiteralExpression + ) => void +) { + ts.forEachChild(sourceFile, function findClassDeclaration(node) { + if (!ts.isClassDeclaration(node)) { + ts.forEachChild(node, findClassDeclaration); + } + + const classDeclarationNode = node as ts.ClassDeclaration; + + if ( + !classDeclarationNode.decorators || + !classDeclarationNode.decorators.length + ) { + return; + } + + const componentDecorator = classDeclarationNode.decorators.find(d => { + return ( + ts.isCallExpression(d.expression) && + ts.isIdentifier(d.expression.expression) && + d.expression.expression.text === decoratorName + ); + }); + + if (!componentDecorator) { + return; + } + + const { expression } = componentDecorator; + if (!ts.isCallExpression(expression)) { + return; + } + + const [arg] = expression.arguments; + if (!ts.isObjectLiteralExpression(arg)) { + return; + } + + callback(classDeclarationNode, arg); + }); +} + function* visit(directory: DirEntry): IterableIterator { for (const path of directory.subfiles) { if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { @@ -42,31 +227,3 @@ function* visit(directory: DirEntry): IterableIterator { yield* visit(directory.dir(path)); } } - -export function visitNgModuleImports( - sourceFile: ts.SourceFile, - callback: ( - importNode: ts.PropertyAssignment, - elementExpressions: ts.NodeArray - ) => void -) { - ts.forEachChild(sourceFile, function findDecorator(node) { - if (!ts.isDecorator(node)) { - ts.forEachChild(node, findDecorator); - return; - } - - ts.forEachChild(node, function findImportsNode(n) { - if ( - ts.isPropertyAssignment(n) && - ts.isArrayLiteralExpression(n.initializer) && - ts.isIdentifier(n.name) && - n.name.text === 'imports' - ) { - callback(n, n.initializer.elements); - } - - ts.forEachChild(n, findImportsNode); - }); - }); -} diff --git a/modules/entity/schematics-core/index.ts b/modules/entity/schematics-core/index.ts index 4f59388898..327bb6460d 100644 --- a/modules/entity/schematics-core/index.ts +++ b/modules/entity/schematics-core/index.ts @@ -81,4 +81,12 @@ export { addPackageToPackageJson } from './utility/package'; export { platformVersion } from './utility/libs-version'; -export { visitTSSourceFiles, visitNgModuleImports } from './utility/visitors'; +export { + visitTSSourceFiles, + visitNgModuleImports, + visitNgModuleExports, + visitComponents, + visitDecorator, + visitNgModules, + visitTemplates, +} from './utility/visitors'; diff --git a/modules/entity/schematics-core/utility/visitors.ts b/modules/entity/schematics-core/utility/visitors.ts index a8c2b4b3d7..beb2c43a0b 100644 --- a/modules/entity/schematics-core/utility/visitors.ts +++ b/modules/entity/schematics-core/utility/visitors.ts @@ -1,4 +1,5 @@ import * as ts from 'typescript'; +import { normalize, resolve } from '@angular-devkit/core'; import { Tree, DirEntry } from '@angular-devkit/schematics'; export function visitTSSourceFiles( @@ -17,6 +18,190 @@ export function visitTSSourceFiles( return result; } +export function visitTemplates( + tree: Tree, + visitor: ( + template: { + fileName: string; + content: string; + inline: boolean; + start: number; + }, + tree: Tree + ) => void +): void { + visitTSSourceFiles(tree, source => { + visitComponents(source, (_, decoratorExpressionNode) => { + ts.forEachChild(decoratorExpressionNode, function findTemplates(n) { + if (ts.isPropertyAssignment(n) && ts.isIdentifier(n.name)) { + if ( + n.name.text === 'template' && + ts.isStringLiteralLike(n.initializer) + ) { + // Need to add an offset of one to the start because the template quotes are + // not part of the template content. + const templateStartIdx = n.initializer.getStart() + 1; + visitor( + { + fileName: source.fileName, + content: n.initializer.text, + inline: true, + start: templateStartIdx, + }, + tree + ); + return; + } else if ( + n.name.text === 'templateUrl' && + ts.isStringLiteralLike(n.initializer) + ) { + const parts = normalize(source.fileName) + .split('/') + .slice(0, -1); + const templatePath = resolve( + normalize(parts.join('/')), + normalize(n.initializer.text) + ); + if (!tree.exists(templatePath)) { + return; + } + + const fileContent = tree.read(templatePath); + if (!fileContent) { + return; + } + + visitor( + { + fileName: templatePath, + content: fileContent.toString(), + inline: false, + start: 0, + }, + tree + ); + return; + } + } + + ts.forEachChild(n, findTemplates); + }); + }); + }); +} + +export function visitNgModuleImports( + sourceFile: ts.SourceFile, + callback: ( + importNode: ts.PropertyAssignment, + elementExpressions: ts.NodeArray + ) => void +) { + visitNgModuleProperty(sourceFile, callback, 'imports'); +} + +export function visitNgModuleExports( + sourceFile: ts.SourceFile, + callback: ( + exportNode: ts.PropertyAssignment, + elementExpressions: ts.NodeArray + ) => void +) { + visitNgModuleProperty(sourceFile, callback, 'exports'); +} + +function visitNgModuleProperty( + sourceFile: ts.SourceFile, + callback: ( + nodes: ts.PropertyAssignment, + elementExpressions: ts.NodeArray + ) => void, + property: string +) { + visitNgModules(sourceFile, (_, decoratorExpressionNode) => { + ts.forEachChild(decoratorExpressionNode, function findTemplates(n) { + if ( + ts.isPropertyAssignment(n) && + ts.isIdentifier(n.name) && + n.name.text === property && + ts.isArrayLiteralExpression(n.initializer) + ) { + callback(n, n.initializer.elements); + return; + } + + ts.forEachChild(n, findTemplates); + }); + }); +} +export function visitComponents( + sourceFile: ts.SourceFile, + callback: ( + classDeclarationNode: ts.ClassDeclaration, + decoratorExpressionNode: ts.ObjectLiteralExpression + ) => void +) { + visitDecorator(sourceFile, 'Component', callback); +} + +export function visitNgModules( + sourceFile: ts.SourceFile, + callback: ( + classDeclarationNode: ts.ClassDeclaration, + decoratorExpressionNode: ts.ObjectLiteralExpression + ) => void +) { + visitDecorator(sourceFile, 'NgModule', callback); +} + +export function visitDecorator( + sourceFile: ts.SourceFile, + decoratorName: string, + callback: ( + classDeclarationNode: ts.ClassDeclaration, + decoratorExpressionNode: ts.ObjectLiteralExpression + ) => void +) { + ts.forEachChild(sourceFile, function findClassDeclaration(node) { + if (!ts.isClassDeclaration(node)) { + ts.forEachChild(node, findClassDeclaration); + } + + const classDeclarationNode = node as ts.ClassDeclaration; + + if ( + !classDeclarationNode.decorators || + !classDeclarationNode.decorators.length + ) { + return; + } + + const componentDecorator = classDeclarationNode.decorators.find(d => { + return ( + ts.isCallExpression(d.expression) && + ts.isIdentifier(d.expression.expression) && + d.expression.expression.text === decoratorName + ); + }); + + if (!componentDecorator) { + return; + } + + const { expression } = componentDecorator; + if (!ts.isCallExpression(expression)) { + return; + } + + const [arg] = expression.arguments; + if (!ts.isObjectLiteralExpression(arg)) { + return; + } + + callback(classDeclarationNode, arg); + }); +} + function* visit(directory: DirEntry): IterableIterator { for (const path of directory.subfiles) { if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { @@ -42,31 +227,3 @@ function* visit(directory: DirEntry): IterableIterator { yield* visit(directory.dir(path)); } } - -export function visitNgModuleImports( - sourceFile: ts.SourceFile, - callback: ( - importNode: ts.PropertyAssignment, - elementExpressions: ts.NodeArray - ) => void -) { - ts.forEachChild(sourceFile, function findDecorator(node) { - if (!ts.isDecorator(node)) { - ts.forEachChild(node, findDecorator); - return; - } - - ts.forEachChild(node, function findImportsNode(n) { - if ( - ts.isPropertyAssignment(n) && - ts.isArrayLiteralExpression(n.initializer) && - ts.isIdentifier(n.name) && - n.name.text === 'imports' - ) { - callback(n, n.initializer.elements); - } - - ts.forEachChild(n, findImportsNode); - }); - }); -} diff --git a/modules/router-store/schematics-core/index.ts b/modules/router-store/schematics-core/index.ts index 4f59388898..327bb6460d 100644 --- a/modules/router-store/schematics-core/index.ts +++ b/modules/router-store/schematics-core/index.ts @@ -81,4 +81,12 @@ export { addPackageToPackageJson } from './utility/package'; export { platformVersion } from './utility/libs-version'; -export { visitTSSourceFiles, visitNgModuleImports } from './utility/visitors'; +export { + visitTSSourceFiles, + visitNgModuleImports, + visitNgModuleExports, + visitComponents, + visitDecorator, + visitNgModules, + visitTemplates, +} from './utility/visitors'; diff --git a/modules/router-store/schematics-core/utility/visitors.ts b/modules/router-store/schematics-core/utility/visitors.ts index a8c2b4b3d7..beb2c43a0b 100644 --- a/modules/router-store/schematics-core/utility/visitors.ts +++ b/modules/router-store/schematics-core/utility/visitors.ts @@ -1,4 +1,5 @@ import * as ts from 'typescript'; +import { normalize, resolve } from '@angular-devkit/core'; import { Tree, DirEntry } from '@angular-devkit/schematics'; export function visitTSSourceFiles( @@ -17,6 +18,190 @@ export function visitTSSourceFiles( return result; } +export function visitTemplates( + tree: Tree, + visitor: ( + template: { + fileName: string; + content: string; + inline: boolean; + start: number; + }, + tree: Tree + ) => void +): void { + visitTSSourceFiles(tree, source => { + visitComponents(source, (_, decoratorExpressionNode) => { + ts.forEachChild(decoratorExpressionNode, function findTemplates(n) { + if (ts.isPropertyAssignment(n) && ts.isIdentifier(n.name)) { + if ( + n.name.text === 'template' && + ts.isStringLiteralLike(n.initializer) + ) { + // Need to add an offset of one to the start because the template quotes are + // not part of the template content. + const templateStartIdx = n.initializer.getStart() + 1; + visitor( + { + fileName: source.fileName, + content: n.initializer.text, + inline: true, + start: templateStartIdx, + }, + tree + ); + return; + } else if ( + n.name.text === 'templateUrl' && + ts.isStringLiteralLike(n.initializer) + ) { + const parts = normalize(source.fileName) + .split('/') + .slice(0, -1); + const templatePath = resolve( + normalize(parts.join('/')), + normalize(n.initializer.text) + ); + if (!tree.exists(templatePath)) { + return; + } + + const fileContent = tree.read(templatePath); + if (!fileContent) { + return; + } + + visitor( + { + fileName: templatePath, + content: fileContent.toString(), + inline: false, + start: 0, + }, + tree + ); + return; + } + } + + ts.forEachChild(n, findTemplates); + }); + }); + }); +} + +export function visitNgModuleImports( + sourceFile: ts.SourceFile, + callback: ( + importNode: ts.PropertyAssignment, + elementExpressions: ts.NodeArray + ) => void +) { + visitNgModuleProperty(sourceFile, callback, 'imports'); +} + +export function visitNgModuleExports( + sourceFile: ts.SourceFile, + callback: ( + exportNode: ts.PropertyAssignment, + elementExpressions: ts.NodeArray + ) => void +) { + visitNgModuleProperty(sourceFile, callback, 'exports'); +} + +function visitNgModuleProperty( + sourceFile: ts.SourceFile, + callback: ( + nodes: ts.PropertyAssignment, + elementExpressions: ts.NodeArray + ) => void, + property: string +) { + visitNgModules(sourceFile, (_, decoratorExpressionNode) => { + ts.forEachChild(decoratorExpressionNode, function findTemplates(n) { + if ( + ts.isPropertyAssignment(n) && + ts.isIdentifier(n.name) && + n.name.text === property && + ts.isArrayLiteralExpression(n.initializer) + ) { + callback(n, n.initializer.elements); + return; + } + + ts.forEachChild(n, findTemplates); + }); + }); +} +export function visitComponents( + sourceFile: ts.SourceFile, + callback: ( + classDeclarationNode: ts.ClassDeclaration, + decoratorExpressionNode: ts.ObjectLiteralExpression + ) => void +) { + visitDecorator(sourceFile, 'Component', callback); +} + +export function visitNgModules( + sourceFile: ts.SourceFile, + callback: ( + classDeclarationNode: ts.ClassDeclaration, + decoratorExpressionNode: ts.ObjectLiteralExpression + ) => void +) { + visitDecorator(sourceFile, 'NgModule', callback); +} + +export function visitDecorator( + sourceFile: ts.SourceFile, + decoratorName: string, + callback: ( + classDeclarationNode: ts.ClassDeclaration, + decoratorExpressionNode: ts.ObjectLiteralExpression + ) => void +) { + ts.forEachChild(sourceFile, function findClassDeclaration(node) { + if (!ts.isClassDeclaration(node)) { + ts.forEachChild(node, findClassDeclaration); + } + + const classDeclarationNode = node as ts.ClassDeclaration; + + if ( + !classDeclarationNode.decorators || + !classDeclarationNode.decorators.length + ) { + return; + } + + const componentDecorator = classDeclarationNode.decorators.find(d => { + return ( + ts.isCallExpression(d.expression) && + ts.isIdentifier(d.expression.expression) && + d.expression.expression.text === decoratorName + ); + }); + + if (!componentDecorator) { + return; + } + + const { expression } = componentDecorator; + if (!ts.isCallExpression(expression)) { + return; + } + + const [arg] = expression.arguments; + if (!ts.isObjectLiteralExpression(arg)) { + return; + } + + callback(classDeclarationNode, arg); + }); +} + function* visit(directory: DirEntry): IterableIterator { for (const path of directory.subfiles) { if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { @@ -42,31 +227,3 @@ function* visit(directory: DirEntry): IterableIterator { yield* visit(directory.dir(path)); } } - -export function visitNgModuleImports( - sourceFile: ts.SourceFile, - callback: ( - importNode: ts.PropertyAssignment, - elementExpressions: ts.NodeArray - ) => void -) { - ts.forEachChild(sourceFile, function findDecorator(node) { - if (!ts.isDecorator(node)) { - ts.forEachChild(node, findDecorator); - return; - } - - ts.forEachChild(node, function findImportsNode(n) { - if ( - ts.isPropertyAssignment(n) && - ts.isArrayLiteralExpression(n.initializer) && - ts.isIdentifier(n.name) && - n.name.text === 'imports' - ) { - callback(n, n.initializer.elements); - } - - ts.forEachChild(n, findImportsNode); - }); - }); -} diff --git a/modules/schematics-core/index.ts b/modules/schematics-core/index.ts index 4f59388898..327bb6460d 100644 --- a/modules/schematics-core/index.ts +++ b/modules/schematics-core/index.ts @@ -81,4 +81,12 @@ export { addPackageToPackageJson } from './utility/package'; export { platformVersion } from './utility/libs-version'; -export { visitTSSourceFiles, visitNgModuleImports } from './utility/visitors'; +export { + visitTSSourceFiles, + visitNgModuleImports, + visitNgModuleExports, + visitComponents, + visitDecorator, + visitNgModules, + visitTemplates, +} from './utility/visitors'; diff --git a/modules/schematics-core/utility/visitors.ts b/modules/schematics-core/utility/visitors.ts index a8c2b4b3d7..beb2c43a0b 100644 --- a/modules/schematics-core/utility/visitors.ts +++ b/modules/schematics-core/utility/visitors.ts @@ -1,4 +1,5 @@ import * as ts from 'typescript'; +import { normalize, resolve } from '@angular-devkit/core'; import { Tree, DirEntry } from '@angular-devkit/schematics'; export function visitTSSourceFiles( @@ -17,6 +18,190 @@ export function visitTSSourceFiles( return result; } +export function visitTemplates( + tree: Tree, + visitor: ( + template: { + fileName: string; + content: string; + inline: boolean; + start: number; + }, + tree: Tree + ) => void +): void { + visitTSSourceFiles(tree, source => { + visitComponents(source, (_, decoratorExpressionNode) => { + ts.forEachChild(decoratorExpressionNode, function findTemplates(n) { + if (ts.isPropertyAssignment(n) && ts.isIdentifier(n.name)) { + if ( + n.name.text === 'template' && + ts.isStringLiteralLike(n.initializer) + ) { + // Need to add an offset of one to the start because the template quotes are + // not part of the template content. + const templateStartIdx = n.initializer.getStart() + 1; + visitor( + { + fileName: source.fileName, + content: n.initializer.text, + inline: true, + start: templateStartIdx, + }, + tree + ); + return; + } else if ( + n.name.text === 'templateUrl' && + ts.isStringLiteralLike(n.initializer) + ) { + const parts = normalize(source.fileName) + .split('/') + .slice(0, -1); + const templatePath = resolve( + normalize(parts.join('/')), + normalize(n.initializer.text) + ); + if (!tree.exists(templatePath)) { + return; + } + + const fileContent = tree.read(templatePath); + if (!fileContent) { + return; + } + + visitor( + { + fileName: templatePath, + content: fileContent.toString(), + inline: false, + start: 0, + }, + tree + ); + return; + } + } + + ts.forEachChild(n, findTemplates); + }); + }); + }); +} + +export function visitNgModuleImports( + sourceFile: ts.SourceFile, + callback: ( + importNode: ts.PropertyAssignment, + elementExpressions: ts.NodeArray + ) => void +) { + visitNgModuleProperty(sourceFile, callback, 'imports'); +} + +export function visitNgModuleExports( + sourceFile: ts.SourceFile, + callback: ( + exportNode: ts.PropertyAssignment, + elementExpressions: ts.NodeArray + ) => void +) { + visitNgModuleProperty(sourceFile, callback, 'exports'); +} + +function visitNgModuleProperty( + sourceFile: ts.SourceFile, + callback: ( + nodes: ts.PropertyAssignment, + elementExpressions: ts.NodeArray + ) => void, + property: string +) { + visitNgModules(sourceFile, (_, decoratorExpressionNode) => { + ts.forEachChild(decoratorExpressionNode, function findTemplates(n) { + if ( + ts.isPropertyAssignment(n) && + ts.isIdentifier(n.name) && + n.name.text === property && + ts.isArrayLiteralExpression(n.initializer) + ) { + callback(n, n.initializer.elements); + return; + } + + ts.forEachChild(n, findTemplates); + }); + }); +} +export function visitComponents( + sourceFile: ts.SourceFile, + callback: ( + classDeclarationNode: ts.ClassDeclaration, + decoratorExpressionNode: ts.ObjectLiteralExpression + ) => void +) { + visitDecorator(sourceFile, 'Component', callback); +} + +export function visitNgModules( + sourceFile: ts.SourceFile, + callback: ( + classDeclarationNode: ts.ClassDeclaration, + decoratorExpressionNode: ts.ObjectLiteralExpression + ) => void +) { + visitDecorator(sourceFile, 'NgModule', callback); +} + +export function visitDecorator( + sourceFile: ts.SourceFile, + decoratorName: string, + callback: ( + classDeclarationNode: ts.ClassDeclaration, + decoratorExpressionNode: ts.ObjectLiteralExpression + ) => void +) { + ts.forEachChild(sourceFile, function findClassDeclaration(node) { + if (!ts.isClassDeclaration(node)) { + ts.forEachChild(node, findClassDeclaration); + } + + const classDeclarationNode = node as ts.ClassDeclaration; + + if ( + !classDeclarationNode.decorators || + !classDeclarationNode.decorators.length + ) { + return; + } + + const componentDecorator = classDeclarationNode.decorators.find(d => { + return ( + ts.isCallExpression(d.expression) && + ts.isIdentifier(d.expression.expression) && + d.expression.expression.text === decoratorName + ); + }); + + if (!componentDecorator) { + return; + } + + const { expression } = componentDecorator; + if (!ts.isCallExpression(expression)) { + return; + } + + const [arg] = expression.arguments; + if (!ts.isObjectLiteralExpression(arg)) { + return; + } + + callback(classDeclarationNode, arg); + }); +} + function* visit(directory: DirEntry): IterableIterator { for (const path of directory.subfiles) { if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { @@ -42,31 +227,3 @@ function* visit(directory: DirEntry): IterableIterator { yield* visit(directory.dir(path)); } } - -export function visitNgModuleImports( - sourceFile: ts.SourceFile, - callback: ( - importNode: ts.PropertyAssignment, - elementExpressions: ts.NodeArray - ) => void -) { - ts.forEachChild(sourceFile, function findDecorator(node) { - if (!ts.isDecorator(node)) { - ts.forEachChild(node, findDecorator); - return; - } - - ts.forEachChild(node, function findImportsNode(n) { - if ( - ts.isPropertyAssignment(n) && - ts.isArrayLiteralExpression(n.initializer) && - ts.isIdentifier(n.name) && - n.name.text === 'imports' - ) { - callback(n, n.initializer.elements); - } - - ts.forEachChild(n, findImportsNode); - }); - }); -} diff --git a/modules/schematics/collection.json b/modules/schematics/collection.json index 7cf5eba7ed..81ab16fe1b 100644 --- a/modules/schematics/collection.json +++ b/modules/schematics/collection.json @@ -43,6 +43,13 @@ "description": "Add feature state" }, + "ngrx-push-migration": { + "aliases": ["ngrxpush"], + "factory": "./src/ngrx-push-migration", + "schema": "./src/ngrx-push-migration/schema.json", + "description": "Migration to replace the `async` pipe with `ngrxPush`" + }, + "reducer": { "aliases": ["r"], "factory": "./src/reducer", diff --git a/modules/schematics/schematics-core/index.ts b/modules/schematics/schematics-core/index.ts index 4f59388898..327bb6460d 100644 --- a/modules/schematics/schematics-core/index.ts +++ b/modules/schematics/schematics-core/index.ts @@ -81,4 +81,12 @@ export { addPackageToPackageJson } from './utility/package'; export { platformVersion } from './utility/libs-version'; -export { visitTSSourceFiles, visitNgModuleImports } from './utility/visitors'; +export { + visitTSSourceFiles, + visitNgModuleImports, + visitNgModuleExports, + visitComponents, + visitDecorator, + visitNgModules, + visitTemplates, +} from './utility/visitors'; diff --git a/modules/schematics/schematics-core/utility/visitors.ts b/modules/schematics/schematics-core/utility/visitors.ts index a8c2b4b3d7..beb2c43a0b 100644 --- a/modules/schematics/schematics-core/utility/visitors.ts +++ b/modules/schematics/schematics-core/utility/visitors.ts @@ -1,4 +1,5 @@ import * as ts from 'typescript'; +import { normalize, resolve } from '@angular-devkit/core'; import { Tree, DirEntry } from '@angular-devkit/schematics'; export function visitTSSourceFiles( @@ -17,6 +18,190 @@ export function visitTSSourceFiles( return result; } +export function visitTemplates( + tree: Tree, + visitor: ( + template: { + fileName: string; + content: string; + inline: boolean; + start: number; + }, + tree: Tree + ) => void +): void { + visitTSSourceFiles(tree, source => { + visitComponents(source, (_, decoratorExpressionNode) => { + ts.forEachChild(decoratorExpressionNode, function findTemplates(n) { + if (ts.isPropertyAssignment(n) && ts.isIdentifier(n.name)) { + if ( + n.name.text === 'template' && + ts.isStringLiteralLike(n.initializer) + ) { + // Need to add an offset of one to the start because the template quotes are + // not part of the template content. + const templateStartIdx = n.initializer.getStart() + 1; + visitor( + { + fileName: source.fileName, + content: n.initializer.text, + inline: true, + start: templateStartIdx, + }, + tree + ); + return; + } else if ( + n.name.text === 'templateUrl' && + ts.isStringLiteralLike(n.initializer) + ) { + const parts = normalize(source.fileName) + .split('/') + .slice(0, -1); + const templatePath = resolve( + normalize(parts.join('/')), + normalize(n.initializer.text) + ); + if (!tree.exists(templatePath)) { + return; + } + + const fileContent = tree.read(templatePath); + if (!fileContent) { + return; + } + + visitor( + { + fileName: templatePath, + content: fileContent.toString(), + inline: false, + start: 0, + }, + tree + ); + return; + } + } + + ts.forEachChild(n, findTemplates); + }); + }); + }); +} + +export function visitNgModuleImports( + sourceFile: ts.SourceFile, + callback: ( + importNode: ts.PropertyAssignment, + elementExpressions: ts.NodeArray + ) => void +) { + visitNgModuleProperty(sourceFile, callback, 'imports'); +} + +export function visitNgModuleExports( + sourceFile: ts.SourceFile, + callback: ( + exportNode: ts.PropertyAssignment, + elementExpressions: ts.NodeArray + ) => void +) { + visitNgModuleProperty(sourceFile, callback, 'exports'); +} + +function visitNgModuleProperty( + sourceFile: ts.SourceFile, + callback: ( + nodes: ts.PropertyAssignment, + elementExpressions: ts.NodeArray + ) => void, + property: string +) { + visitNgModules(sourceFile, (_, decoratorExpressionNode) => { + ts.forEachChild(decoratorExpressionNode, function findTemplates(n) { + if ( + ts.isPropertyAssignment(n) && + ts.isIdentifier(n.name) && + n.name.text === property && + ts.isArrayLiteralExpression(n.initializer) + ) { + callback(n, n.initializer.elements); + return; + } + + ts.forEachChild(n, findTemplates); + }); + }); +} +export function visitComponents( + sourceFile: ts.SourceFile, + callback: ( + classDeclarationNode: ts.ClassDeclaration, + decoratorExpressionNode: ts.ObjectLiteralExpression + ) => void +) { + visitDecorator(sourceFile, 'Component', callback); +} + +export function visitNgModules( + sourceFile: ts.SourceFile, + callback: ( + classDeclarationNode: ts.ClassDeclaration, + decoratorExpressionNode: ts.ObjectLiteralExpression + ) => void +) { + visitDecorator(sourceFile, 'NgModule', callback); +} + +export function visitDecorator( + sourceFile: ts.SourceFile, + decoratorName: string, + callback: ( + classDeclarationNode: ts.ClassDeclaration, + decoratorExpressionNode: ts.ObjectLiteralExpression + ) => void +) { + ts.forEachChild(sourceFile, function findClassDeclaration(node) { + if (!ts.isClassDeclaration(node)) { + ts.forEachChild(node, findClassDeclaration); + } + + const classDeclarationNode = node as ts.ClassDeclaration; + + if ( + !classDeclarationNode.decorators || + !classDeclarationNode.decorators.length + ) { + return; + } + + const componentDecorator = classDeclarationNode.decorators.find(d => { + return ( + ts.isCallExpression(d.expression) && + ts.isIdentifier(d.expression.expression) && + d.expression.expression.text === decoratorName + ); + }); + + if (!componentDecorator) { + return; + } + + const { expression } = componentDecorator; + if (!ts.isCallExpression(expression)) { + return; + } + + const [arg] = expression.arguments; + if (!ts.isObjectLiteralExpression(arg)) { + return; + } + + callback(classDeclarationNode, arg); + }); +} + function* visit(directory: DirEntry): IterableIterator { for (const path of directory.subfiles) { if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { @@ -42,31 +227,3 @@ function* visit(directory: DirEntry): IterableIterator { yield* visit(directory.dir(path)); } } - -export function visitNgModuleImports( - sourceFile: ts.SourceFile, - callback: ( - importNode: ts.PropertyAssignment, - elementExpressions: ts.NodeArray - ) => void -) { - ts.forEachChild(sourceFile, function findDecorator(node) { - if (!ts.isDecorator(node)) { - ts.forEachChild(node, findDecorator); - return; - } - - ts.forEachChild(node, function findImportsNode(n) { - if ( - ts.isPropertyAssignment(n) && - ts.isArrayLiteralExpression(n.initializer) && - ts.isIdentifier(n.name) && - n.name.text === 'imports' - ) { - callback(n, n.initializer.elements); - } - - ts.forEachChild(n, findImportsNode); - }); - }); -} diff --git a/modules/schematics/src/ngrx-push-migration/index.spec.ts b/modules/schematics/src/ngrx-push-migration/index.spec.ts new file mode 100644 index 0000000000..1ae6d01f71 --- /dev/null +++ b/modules/schematics/src/ngrx-push-migration/index.spec.ts @@ -0,0 +1,260 @@ +import { + SchematicTestRunner, + UnitTestTree, +} from '@angular-devkit/schematics/testing'; +import * as path from 'path'; +import { createWorkspace } from '../../../schematics-core/testing'; + +describe('NgrxPush migration', () => { + const schematicRunner = new SchematicTestRunner( + '@ngrx/schematics', + path.join(__dirname, '../../collection.json') + ); + + let appTree: UnitTestTree; + + beforeEach(async () => { + appTree = await createWorkspace(schematicRunner, appTree); + }); + + describe('migrateToNgrxPush', () => { + const TEMPLATE = ` + promise|async + One whitespace {{ greeting | async }} + No whitespace {{ greeting |async }} + Multiple whitespace {{ greeting | async }} + `; + + it('should replace an inline template', async () => { + appTree.create( + './sut.component.ts', + `@Component({ + selector: 'sut', + template: \`${TEMPLATE}\` + }) + export class SUTComponent { }` + ); + + const tree = await schematicRunner + .runSchematicAsync('ngrx-push-migration', {}, appTree) + .toPromise(); + + const actual = tree.readContent('./sut.component.ts'); + expect(actual).not.toContain('async'); + expect(actual).toContain('ngrxPush'); + }); + + it('should replace a file template', async () => { + appTree.create( + './sut.component.ts', + `@Component({ + selector: 'sut', + templateUrl: './sut.component.html' + }) + export class SUTComponent { }` + ); + appTree.create('./sut.component.html', TEMPLATE); + + const tree = await schematicRunner + .runSchematicAsync('ngrx-push-migration', {}, appTree) + .toPromise(); + + const actual = tree.readContent('./sut.component.html'); + expect(actual).not.toContain('async'); + expect(actual).toContain('ngrxPush'); + }); + + it('should not touch templates that are not referenced', async () => { + appTree.create('./sut.component.html', TEMPLATE); + + const tree = await schematicRunner + .runSchematicAsync('ngrx-push-migration', {}, appTree) + .toPromise(); + + const actual = tree.readContent('./sut.component.html'); + expect(actual).toBe(TEMPLATE); + }); + }); + + describe('importReactiveComponentModule', () => { + it('should import ReactiveComponentModule when BrowserModule is imported', async () => { + appTree.create( + './sut.module.ts', + ` + import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [ AppComponent ], + imports: [ BrowserModule ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + ` + ); + const tree = await schematicRunner + .runSchematicAsync('ngrx-push-migration', {}, appTree) + .toPromise(); + + const actual = tree.readContent('./sut.module.ts'); + expect(actual).toMatch( + /imports: \[ BrowserModule, ReactiveComponentModule \],/ + ); + expect(actual).toMatch( + /import { ReactiveComponentModule } from '@ngrx\/component'/ + ); + }); + + it('should import ReactiveComponentModule when CommonModule is imported', async () => { + appTree.create( + './sut.module.ts', + ` + import { CommonModule } from '@angular/common'; + import { NgModule } from '@angular/core'; + + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [ AppComponent ], + imports: [ CommonModule ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + ` + ); + const tree = await schematicRunner + .runSchematicAsync('ngrx-push-migration', {}, appTree) + .toPromise(); + + const actual = tree.readContent('./sut.module.ts'); + expect(actual).toMatch( + /imports: \[ CommonModule, ReactiveComponentModule \],/ + ); + expect(actual).toMatch( + /import { ReactiveComponentModule } from '@ngrx\/component'/ + ); + }); + + it("should not import ReactiveComponentModule when it doesn't need to", async () => { + appTree.create( + './sut.module.ts', + ` + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [ AppComponent ], + imports: [], + providers: [], + }) + export class AppModule { } + ` + ); + const tree = await schematicRunner + .runSchematicAsync('ngrx-push-migration', {}, appTree) + .toPromise(); + + const actual = tree.readContent('./sut.module.ts'); + expect(actual).not.toMatch( + /imports: \[ CommonModule, ReactiveComponentModule \],/ + ); + expect(actual).not.toMatch( + /import { ReactiveComponentModule } from '@ngrx\/component'/ + ); + }); + }); + + describe('exportReactiveComponentModule', () => { + it('should export ReactiveComponentModule when BrowserModule is exported', async () => { + appTree.create( + './sut.module.ts', + ` + import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [ AppComponent ], + exports: [ BrowserModule ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + ` + ); + const tree = await schematicRunner + .runSchematicAsync('ngrx-push-migration', {}, appTree) + .toPromise(); + + const actual = tree.readContent('./sut.module.ts'); + expect(actual).toMatch( + /exports: \[ BrowserModule, ReactiveComponentModule \],/ + ); + expect(actual).toMatch( + /import { ReactiveComponentModule } from '@ngrx\/component'/ + ); + }); + + it('should export ReactiveComponentModule when CommonModule is exported', async () => { + appTree.create( + './sut.module.ts', + ` + import { CommonModule } from '@angular/common'; + import { NgModule } from '@angular/core'; + + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [ AppComponent ], + exports: [ CommonModule ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + ` + ); + const tree = await schematicRunner + .runSchematicAsync('ngrx-push-migration', {}, appTree) + .toPromise(); + + const actual = tree.readContent('./sut.module.ts'); + expect(actual).toMatch( + /exports: \[ CommonModule, ReactiveComponentModule \],/ + ); + expect(actual).toMatch( + /import { ReactiveComponentModule } from '@ngrx\/component'/ + ); + }); + + it("should not export ReactiveComponentModule when it doesn't need to", async () => { + appTree.create( + './sut.module.ts', + ` + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [ AppComponent ], + exports: [], + providers: [], + }) + export class AppModule { } + ` + ); + const tree = await schematicRunner + .runSchematicAsync('ngrx-push-migration', {}, appTree) + .toPromise(); + + const actual = tree.readContent('./sut.module.ts'); + expect(actual).not.toMatch( + /exports: \[ CommonModule, ReactiveComponentModule \],/ + ); + expect(actual).not.toMatch( + /import { ReactiveComponentModule } from '@ngrx\/component'/ + ); + }); + }); +}); diff --git a/modules/schematics/src/ngrx-push-migration/index.ts b/modules/schematics/src/ngrx-push-migration/index.ts new file mode 100644 index 0000000000..60745f3af1 --- /dev/null +++ b/modules/schematics/src/ngrx-push-migration/index.ts @@ -0,0 +1,102 @@ +import * as ts from 'typescript'; +import { Tree, Rule, chain } from '@angular-devkit/schematics'; +import { + commitChanges, + visitTemplates, + ReplaceChange, + Change, + visitTSSourceFiles, + visitNgModuleImports, + visitNgModuleExports, + addImportToModule, + addExportToModule, +} from '@ngrx/schematics/schematics-core'; + +const ASYNC_REGEXP = /\| {0,}async/g; +const REACTIVE_MODULE = 'ReactiveComponentModule'; +const COMPONENT_MODULE = '@ngrx/component'; + +const reactiveModuleToFind = (node: ts.Node) => + ts.isIdentifier(node) && node.text === REACTIVE_MODULE; + +const ngModulesToFind = (node: ts.Node) => + ts.isIdentifier(node) && + (node.text === 'CommonModule' || node.text === 'BrowserModule'); + +export function migrateToNgrxPush(): Rule { + return (host: Tree) => + visitTemplates(host, template => { + let match: RegExpMatchArray | null; + let changes: Change[] = []; + while ((match = ASYNC_REGEXP.exec(template.content)) !== null) { + const m = match.toString(); + + changes.push( + new ReplaceChange( + template.fileName, + template.start + match.index!, + m, + m.replace('async', 'ngrxPush') + ) + ); + } + + return commitChanges(host, template.fileName, changes); + }); +} + +export function importReactiveComponentModule(): Rule { + return (host: Tree) => { + visitTSSourceFiles(host, sourceFile => { + let hasCommonModuleOrBrowserModule = false; + let hasReactiveComponentModule = false; + + visitNgModuleImports(sourceFile, (_, importNodes) => { + hasCommonModuleOrBrowserModule = importNodes.some(ngModulesToFind); + hasReactiveComponentModule = importNodes.some(reactiveModuleToFind); + }); + + if (hasCommonModuleOrBrowserModule && !hasReactiveComponentModule) { + const changes: Change[] = addImportToModule( + sourceFile, + sourceFile.fileName, + REACTIVE_MODULE, + COMPONENT_MODULE + ); + commitChanges(host, sourceFile.fileName, changes); + } + }); + }; +} + +export function exportReactiveComponentModule(): Rule { + return (host: Tree) => { + visitTSSourceFiles(host, sourceFile => { + let hasCommonModuleOrBrowserModule = false; + let hasReactiveComponentModule = false; + + visitNgModuleExports(sourceFile, (_, exportNodes) => { + hasCommonModuleOrBrowserModule = exportNodes.some(ngModulesToFind); + hasReactiveComponentModule = exportNodes.some(reactiveModuleToFind); + }); + + if (hasCommonModuleOrBrowserModule && !hasReactiveComponentModule) { + const changes: Change[] = addExportToModule( + sourceFile, + sourceFile.fileName, + REACTIVE_MODULE, + COMPONENT_MODULE + ); + commitChanges(host, sourceFile.fileName, changes); + } + }); + }; +} + +export default function(): Rule { + return chain([ + migrateToNgrxPush(), + importReactiveComponentModule(), + exportReactiveComponentModule(), + ]); +} diff --git a/modules/schematics/src/ngrx-push-migration/schema.json b/modules/schematics/src/ngrx-push-migration/schema.json new file mode 100644 index 0000000000..4c4c0fad02 --- /dev/null +++ b/modules/schematics/src/ngrx-push-migration/schema.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "SchematicsNgRxNgRxPushMigration", + "title": "NgRx Component NgRxPush Migration from async to ngrxPush", + "type": "object", + "properties": {}, + "required": [] +} diff --git a/modules/schematics/src/ngrx-push-migration/schema.ts b/modules/schematics/src/ngrx-push-migration/schema.ts new file mode 100644 index 0000000000..e53f1202a2 --- /dev/null +++ b/modules/schematics/src/ngrx-push-migration/schema.ts @@ -0,0 +1 @@ +export interface Schema {} diff --git a/modules/store-devtools/schematics-core/index.ts b/modules/store-devtools/schematics-core/index.ts index 4f59388898..327bb6460d 100644 --- a/modules/store-devtools/schematics-core/index.ts +++ b/modules/store-devtools/schematics-core/index.ts @@ -81,4 +81,12 @@ export { addPackageToPackageJson } from './utility/package'; export { platformVersion } from './utility/libs-version'; -export { visitTSSourceFiles, visitNgModuleImports } from './utility/visitors'; +export { + visitTSSourceFiles, + visitNgModuleImports, + visitNgModuleExports, + visitComponents, + visitDecorator, + visitNgModules, + visitTemplates, +} from './utility/visitors'; diff --git a/modules/store-devtools/schematics-core/utility/visitors.ts b/modules/store-devtools/schematics-core/utility/visitors.ts index a8c2b4b3d7..beb2c43a0b 100644 --- a/modules/store-devtools/schematics-core/utility/visitors.ts +++ b/modules/store-devtools/schematics-core/utility/visitors.ts @@ -1,4 +1,5 @@ import * as ts from 'typescript'; +import { normalize, resolve } from '@angular-devkit/core'; import { Tree, DirEntry } from '@angular-devkit/schematics'; export function visitTSSourceFiles( @@ -17,6 +18,190 @@ export function visitTSSourceFiles( return result; } +export function visitTemplates( + tree: Tree, + visitor: ( + template: { + fileName: string; + content: string; + inline: boolean; + start: number; + }, + tree: Tree + ) => void +): void { + visitTSSourceFiles(tree, source => { + visitComponents(source, (_, decoratorExpressionNode) => { + ts.forEachChild(decoratorExpressionNode, function findTemplates(n) { + if (ts.isPropertyAssignment(n) && ts.isIdentifier(n.name)) { + if ( + n.name.text === 'template' && + ts.isStringLiteralLike(n.initializer) + ) { + // Need to add an offset of one to the start because the template quotes are + // not part of the template content. + const templateStartIdx = n.initializer.getStart() + 1; + visitor( + { + fileName: source.fileName, + content: n.initializer.text, + inline: true, + start: templateStartIdx, + }, + tree + ); + return; + } else if ( + n.name.text === 'templateUrl' && + ts.isStringLiteralLike(n.initializer) + ) { + const parts = normalize(source.fileName) + .split('/') + .slice(0, -1); + const templatePath = resolve( + normalize(parts.join('/')), + normalize(n.initializer.text) + ); + if (!tree.exists(templatePath)) { + return; + } + + const fileContent = tree.read(templatePath); + if (!fileContent) { + return; + } + + visitor( + { + fileName: templatePath, + content: fileContent.toString(), + inline: false, + start: 0, + }, + tree + ); + return; + } + } + + ts.forEachChild(n, findTemplates); + }); + }); + }); +} + +export function visitNgModuleImports( + sourceFile: ts.SourceFile, + callback: ( + importNode: ts.PropertyAssignment, + elementExpressions: ts.NodeArray + ) => void +) { + visitNgModuleProperty(sourceFile, callback, 'imports'); +} + +export function visitNgModuleExports( + sourceFile: ts.SourceFile, + callback: ( + exportNode: ts.PropertyAssignment, + elementExpressions: ts.NodeArray + ) => void +) { + visitNgModuleProperty(sourceFile, callback, 'exports'); +} + +function visitNgModuleProperty( + sourceFile: ts.SourceFile, + callback: ( + nodes: ts.PropertyAssignment, + elementExpressions: ts.NodeArray + ) => void, + property: string +) { + visitNgModules(sourceFile, (_, decoratorExpressionNode) => { + ts.forEachChild(decoratorExpressionNode, function findTemplates(n) { + if ( + ts.isPropertyAssignment(n) && + ts.isIdentifier(n.name) && + n.name.text === property && + ts.isArrayLiteralExpression(n.initializer) + ) { + callback(n, n.initializer.elements); + return; + } + + ts.forEachChild(n, findTemplates); + }); + }); +} +export function visitComponents( + sourceFile: ts.SourceFile, + callback: ( + classDeclarationNode: ts.ClassDeclaration, + decoratorExpressionNode: ts.ObjectLiteralExpression + ) => void +) { + visitDecorator(sourceFile, 'Component', callback); +} + +export function visitNgModules( + sourceFile: ts.SourceFile, + callback: ( + classDeclarationNode: ts.ClassDeclaration, + decoratorExpressionNode: ts.ObjectLiteralExpression + ) => void +) { + visitDecorator(sourceFile, 'NgModule', callback); +} + +export function visitDecorator( + sourceFile: ts.SourceFile, + decoratorName: string, + callback: ( + classDeclarationNode: ts.ClassDeclaration, + decoratorExpressionNode: ts.ObjectLiteralExpression + ) => void +) { + ts.forEachChild(sourceFile, function findClassDeclaration(node) { + if (!ts.isClassDeclaration(node)) { + ts.forEachChild(node, findClassDeclaration); + } + + const classDeclarationNode = node as ts.ClassDeclaration; + + if ( + !classDeclarationNode.decorators || + !classDeclarationNode.decorators.length + ) { + return; + } + + const componentDecorator = classDeclarationNode.decorators.find(d => { + return ( + ts.isCallExpression(d.expression) && + ts.isIdentifier(d.expression.expression) && + d.expression.expression.text === decoratorName + ); + }); + + if (!componentDecorator) { + return; + } + + const { expression } = componentDecorator; + if (!ts.isCallExpression(expression)) { + return; + } + + const [arg] = expression.arguments; + if (!ts.isObjectLiteralExpression(arg)) { + return; + } + + callback(classDeclarationNode, arg); + }); +} + function* visit(directory: DirEntry): IterableIterator { for (const path of directory.subfiles) { if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { @@ -42,31 +227,3 @@ function* visit(directory: DirEntry): IterableIterator { yield* visit(directory.dir(path)); } } - -export function visitNgModuleImports( - sourceFile: ts.SourceFile, - callback: ( - importNode: ts.PropertyAssignment, - elementExpressions: ts.NodeArray - ) => void -) { - ts.forEachChild(sourceFile, function findDecorator(node) { - if (!ts.isDecorator(node)) { - ts.forEachChild(node, findDecorator); - return; - } - - ts.forEachChild(node, function findImportsNode(n) { - if ( - ts.isPropertyAssignment(n) && - ts.isArrayLiteralExpression(n.initializer) && - ts.isIdentifier(n.name) && - n.name.text === 'imports' - ) { - callback(n, n.initializer.elements); - } - - ts.forEachChild(n, findImportsNode); - }); - }); -} diff --git a/modules/store/schematics-core/index.ts b/modules/store/schematics-core/index.ts index 4f59388898..327bb6460d 100644 --- a/modules/store/schematics-core/index.ts +++ b/modules/store/schematics-core/index.ts @@ -81,4 +81,12 @@ export { addPackageToPackageJson } from './utility/package'; export { platformVersion } from './utility/libs-version'; -export { visitTSSourceFiles, visitNgModuleImports } from './utility/visitors'; +export { + visitTSSourceFiles, + visitNgModuleImports, + visitNgModuleExports, + visitComponents, + visitDecorator, + visitNgModules, + visitTemplates, +} from './utility/visitors'; diff --git a/modules/store/schematics-core/utility/visitors.ts b/modules/store/schematics-core/utility/visitors.ts index a8c2b4b3d7..beb2c43a0b 100644 --- a/modules/store/schematics-core/utility/visitors.ts +++ b/modules/store/schematics-core/utility/visitors.ts @@ -1,4 +1,5 @@ import * as ts from 'typescript'; +import { normalize, resolve } from '@angular-devkit/core'; import { Tree, DirEntry } from '@angular-devkit/schematics'; export function visitTSSourceFiles( @@ -17,6 +18,190 @@ export function visitTSSourceFiles( return result; } +export function visitTemplates( + tree: Tree, + visitor: ( + template: { + fileName: string; + content: string; + inline: boolean; + start: number; + }, + tree: Tree + ) => void +): void { + visitTSSourceFiles(tree, source => { + visitComponents(source, (_, decoratorExpressionNode) => { + ts.forEachChild(decoratorExpressionNode, function findTemplates(n) { + if (ts.isPropertyAssignment(n) && ts.isIdentifier(n.name)) { + if ( + n.name.text === 'template' && + ts.isStringLiteralLike(n.initializer) + ) { + // Need to add an offset of one to the start because the template quotes are + // not part of the template content. + const templateStartIdx = n.initializer.getStart() + 1; + visitor( + { + fileName: source.fileName, + content: n.initializer.text, + inline: true, + start: templateStartIdx, + }, + tree + ); + return; + } else if ( + n.name.text === 'templateUrl' && + ts.isStringLiteralLike(n.initializer) + ) { + const parts = normalize(source.fileName) + .split('/') + .slice(0, -1); + const templatePath = resolve( + normalize(parts.join('/')), + normalize(n.initializer.text) + ); + if (!tree.exists(templatePath)) { + return; + } + + const fileContent = tree.read(templatePath); + if (!fileContent) { + return; + } + + visitor( + { + fileName: templatePath, + content: fileContent.toString(), + inline: false, + start: 0, + }, + tree + ); + return; + } + } + + ts.forEachChild(n, findTemplates); + }); + }); + }); +} + +export function visitNgModuleImports( + sourceFile: ts.SourceFile, + callback: ( + importNode: ts.PropertyAssignment, + elementExpressions: ts.NodeArray + ) => void +) { + visitNgModuleProperty(sourceFile, callback, 'imports'); +} + +export function visitNgModuleExports( + sourceFile: ts.SourceFile, + callback: ( + exportNode: ts.PropertyAssignment, + elementExpressions: ts.NodeArray + ) => void +) { + visitNgModuleProperty(sourceFile, callback, 'exports'); +} + +function visitNgModuleProperty( + sourceFile: ts.SourceFile, + callback: ( + nodes: ts.PropertyAssignment, + elementExpressions: ts.NodeArray + ) => void, + property: string +) { + visitNgModules(sourceFile, (_, decoratorExpressionNode) => { + ts.forEachChild(decoratorExpressionNode, function findTemplates(n) { + if ( + ts.isPropertyAssignment(n) && + ts.isIdentifier(n.name) && + n.name.text === property && + ts.isArrayLiteralExpression(n.initializer) + ) { + callback(n, n.initializer.elements); + return; + } + + ts.forEachChild(n, findTemplates); + }); + }); +} +export function visitComponents( + sourceFile: ts.SourceFile, + callback: ( + classDeclarationNode: ts.ClassDeclaration, + decoratorExpressionNode: ts.ObjectLiteralExpression + ) => void +) { + visitDecorator(sourceFile, 'Component', callback); +} + +export function visitNgModules( + sourceFile: ts.SourceFile, + callback: ( + classDeclarationNode: ts.ClassDeclaration, + decoratorExpressionNode: ts.ObjectLiteralExpression + ) => void +) { + visitDecorator(sourceFile, 'NgModule', callback); +} + +export function visitDecorator( + sourceFile: ts.SourceFile, + decoratorName: string, + callback: ( + classDeclarationNode: ts.ClassDeclaration, + decoratorExpressionNode: ts.ObjectLiteralExpression + ) => void +) { + ts.forEachChild(sourceFile, function findClassDeclaration(node) { + if (!ts.isClassDeclaration(node)) { + ts.forEachChild(node, findClassDeclaration); + } + + const classDeclarationNode = node as ts.ClassDeclaration; + + if ( + !classDeclarationNode.decorators || + !classDeclarationNode.decorators.length + ) { + return; + } + + const componentDecorator = classDeclarationNode.decorators.find(d => { + return ( + ts.isCallExpression(d.expression) && + ts.isIdentifier(d.expression.expression) && + d.expression.expression.text === decoratorName + ); + }); + + if (!componentDecorator) { + return; + } + + const { expression } = componentDecorator; + if (!ts.isCallExpression(expression)) { + return; + } + + const [arg] = expression.arguments; + if (!ts.isObjectLiteralExpression(arg)) { + return; + } + + callback(classDeclarationNode, arg); + }); +} + function* visit(directory: DirEntry): IterableIterator { for (const path of directory.subfiles) { if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { @@ -42,31 +227,3 @@ function* visit(directory: DirEntry): IterableIterator { yield* visit(directory.dir(path)); } } - -export function visitNgModuleImports( - sourceFile: ts.SourceFile, - callback: ( - importNode: ts.PropertyAssignment, - elementExpressions: ts.NodeArray - ) => void -) { - ts.forEachChild(sourceFile, function findDecorator(node) { - if (!ts.isDecorator(node)) { - ts.forEachChild(node, findDecorator); - return; - } - - ts.forEachChild(node, function findImportsNode(n) { - if ( - ts.isPropertyAssignment(n) && - ts.isArrayLiteralExpression(n.initializer) && - ts.isIdentifier(n.name) && - n.name.text === 'imports' - ) { - callback(n, n.initializer.elements); - } - - ts.forEachChild(n, findImportsNode); - }); - }); -}