From a785331e2b2d14c939a24a36571002a224b846fc Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Tue, 24 Jan 2023 21:29:57 +0100 Subject: [PATCH] feat(router-store): add migration for getRouterSelectors (#3753) --- .../migrations/15_2_0/index.spec.ts | 161 ++++++++++++++++++ .../router-store/migrations/15_2_0/index.ts | 127 ++++++++++++++ .../router-store/migrations/migration.json | 5 + 3 files changed, 293 insertions(+) create mode 100644 modules/router-store/migrations/15_2_0/index.spec.ts create mode 100644 modules/router-store/migrations/15_2_0/index.ts diff --git a/modules/router-store/migrations/15_2_0/index.spec.ts b/modules/router-store/migrations/15_2_0/index.spec.ts new file mode 100644 index 0000000000..6452ed20f5 --- /dev/null +++ b/modules/router-store/migrations/15_2_0/index.spec.ts @@ -0,0 +1,161 @@ +import { Tree } from '@angular-devkit/schematics'; +import { + SchematicTestRunner, + UnitTestTree, +} from '@angular-devkit/schematics/testing'; +import * as path from 'path'; +import { createPackageJson } from '@ngrx/schematics-core/testing/create-package'; +import { waitForAsync } from '@angular/core/testing'; + +describe('Router Store Migration 15_2_0', () => { + let appTree: UnitTestTree; + const collectionPath = path.join(__dirname, '../migration.json'); + const pkgName = 'router-store'; + + beforeEach(() => { + appTree = new UnitTestTree(Tree.empty()); + appTree.create( + '/tsconfig.json', + ` + { + "include": [**./*.ts"] + } + ` + ); + createPackageJson('', pkgName, appTree); + }); + + describe('Rename selector', () => { + it(`renames getSelectors to getRouterSelectors as named imports`, waitForAsync(async () => { + const input = ` + import { getSelectors } from '@ngrx/router-store'; + export const { + selectCurrentRoute, + selectQueryParams, + selectQueryParam, + selectRouteParams, + selectRouteParam, + selectRouteData, + selectUrl, + selectTitle, + } = getSelectors(selectRouter); + `; + const expected = ` + import { getRouterSelectors } from '@ngrx/router-store'; + export const { + selectCurrentRoute, + selectQueryParams, + selectQueryParam, + selectRouteParams, + selectRouteParam, + selectRouteData, + selectUrl, + selectTitle, + } = getSelectors(selectRouter); + `; + + appTree.create('./selector.ts', input); + const runner = new SchematicTestRunner('schematics', collectionPath); + + const newTree = await runner + .runSchematicAsync(`ngrx-${pkgName}-migration-05`, {}, appTree) + .toPromise(); + const file = newTree.readContent('selector.ts'); + + expect(file).toBe(expected); + })); + + it(`renames getSelectors to getRouterSelectors as namespace import`, waitForAsync(async () => { + const input = ` + import * as routerStore from '@ngrx/router-store'; + export const selectors = routerStore.getSelectors(selectRouter); + `; + const expected = ` + import * as routerStore from '@ngrx/router-store'; + export const selectors = routerStore.getRouterSelectors(selectRouter); + `; + + appTree.create('./selector.ts', input); + const runner = new SchematicTestRunner('schematics', collectionPath); + + const newTree = await runner + .runSchematicAsync(`ngrx-${pkgName}-migration-05`, {}, appTree) + .toPromise(); + const file = newTree.readContent('selector.ts'); + + expect(file).toBe(expected); + })); + + it(`renames getSelectors to getRouterSelectors as namespace import with deconstruct`, waitForAsync(async () => { + const input = ` + import * as routerStore from '@ngrx/router-store'; + export const { + selectCurrentRoute, + selectQueryParams, + selectQueryParam, + selectRouteParams, + selectRouteParam, + selectRouteData, + selectUrl, + selectTitle, + } = routerStore.getSelectors(selectRouter); + `; + const expected = ` + import * as routerStore from '@ngrx/router-store'; + export const { + selectCurrentRoute, + selectQueryParams, + selectQueryParam, + selectRouteParams, + selectRouteParam, + selectRouteData, + selectUrl, + selectTitle, + } = routerStore.getRouterSelectors(selectRouter); + `; + + appTree.create('./selector.ts', input); + const runner = new SchematicTestRunner('schematics', collectionPath); + + const newTree = await runner + .runSchematicAsync(`ngrx-${pkgName}-migration-05`, {}, appTree) + .toPromise(); + const file = newTree.readContent('selector.ts'); + + expect(file).toBe(expected); + })); + + it(`does not rename getSelectors if not imported from router-store`, waitForAsync(async () => { + const input = ` + import { getSelectors } from '@ngrx/something'; + export const { selectCurrentRoute } = getSelectors(selectRouter); + `; + + appTree.create('./selector.ts', input); + const runner = new SchematicTestRunner('schematics', collectionPath); + + const newTree = await runner + .runSchematicAsync(`ngrx-${pkgName}-migration-05`, {}, appTree) + .toPromise(); + const file = newTree.readContent('selector.ts'); + + expect(file).toBe(input); + })); + it(`does not rename other methods on namespace import`, waitForAsync(async () => { + const input = ` + import * as routerStore from '@ngrx/router-store'; + const root = routerStore.forRoot(); + `; + + appTree.create('./selector.ts', input); + const runner = new SchematicTestRunner('schematics', collectionPath); + + const newTree = await runner + .runSchematicAsync(`ngrx-${pkgName}-migration-05`, {}, appTree) + .toPromise(); + const file = newTree.readContent('selector.ts'); + + expect(file).toBe(input); + })); + }); +}); diff --git a/modules/router-store/migrations/15_2_0/index.ts b/modules/router-store/migrations/15_2_0/index.ts new file mode 100644 index 0000000000..6ad50c16bc --- /dev/null +++ b/modules/router-store/migrations/15_2_0/index.ts @@ -0,0 +1,127 @@ +import * as ts from 'typescript'; +import { Rule, chain, Tree } from '@angular-devkit/schematics'; +import { + visitTSSourceFiles, + commitChanges, + createReplaceChange, + ReplaceChange, +} from '../../schematics-core'; + +const renames: { [key: string]: string } = { + getSelectors: 'getRouterSelectors', +}; + +function renameSelector() { + return (tree: Tree) => { + visitTSSourceFiles(tree, (sourceFile) => { + const routerStoreImports = sourceFile.statements + .filter((p): p is ts.ImportDeclaration => ts.isImportDeclaration(p)) + .filter(({ moduleSpecifier }) => + moduleSpecifier.getText(sourceFile).includes('@ngrx/router-store') + ); + const changes: ReplaceChange[] = [ + ...replaceNamedImports(routerStoreImports, sourceFile), + ...replaceNamespaceImports(routerStoreImports, sourceFile), + ]; + + if (changes.length) { + commitChanges(tree, sourceFile.fileName, changes); + } + }); + }; +} + +function replaceNamedImports( + routerStoreImports: ts.ImportDeclaration[], + sourceFile: ts.SourceFile +): ReplaceChange[] { + const changes: ReplaceChange[] = []; + + const namedImports = routerStoreImports + .flatMap((p) => + !!p.importClause && ts.isImportClause(p.importClause) + ? p.importClause.namedBindings + : [] + ) + .flatMap((p) => (!!p && ts.isNamedImports(p) ? p.elements : [])); + + for (const namedImport of namedImports) { + tryToAddReplacement(namedImport.name, sourceFile, changes); + } + return changes; +} + +function replaceNamespaceImports( + routerStoreImports: ts.ImportDeclaration[], + sourceFile: ts.SourceFile +): ReplaceChange[] { + const changes: ReplaceChange[] = []; + + const namespaceImports = routerStoreImports + .map((p) => + !!p.importClause && + ts.isImportClause(p.importClause) && + !!p.importClause.namedBindings && + ts.isNamespaceImport(p.importClause.namedBindings) + ? p.importClause.namedBindings.name.getText(sourceFile) + : null + ) + .filter((p): p is string => !!p); + + if (namespaceImports.length === 0) { + return changes; + } + + for (const statement of sourceFile.statements) { + statement.forEachChild((child) => { + if (ts.isVariableDeclarationList(child)) { + const [declaration] = child.declarations; + if ( + ts.isVariableDeclaration(declaration) && + declaration.initializer && + ts.isCallExpression(declaration.initializer) && + declaration.initializer.expression && + ts.isPropertyAccessExpression(declaration.initializer.expression) && + ts.isIdentifier(declaration.initializer.expression.expression) && + ts.isIdentifier(declaration.initializer.expression.name) + ) { + if ( + namespaceImports.includes( + declaration.initializer.expression.expression.getText(sourceFile) + ) + ) { + tryToAddReplacement( + declaration.initializer.expression.name, + sourceFile, + changes + ); + } + } + } + }); + } + + return changes; +} + +function tryToAddReplacement( + oldName: ts.Identifier, + sourceFile: ts.SourceFile, + changes: ReplaceChange[] +) { + const oldNameText = oldName.getText(sourceFile); + const newName = renames[oldNameText]; + if (newName) { + const change = createReplaceChange( + sourceFile, + oldName, + oldNameText, + newName + ); + changes.push(change); + } +} + +export default function (): Rule { + return chain([renameSelector()]); +} diff --git a/modules/router-store/migrations/migration.json b/modules/router-store/migrations/migration.json index 94805b5f77..fe85fb98f4 100644 --- a/modules/router-store/migrations/migration.json +++ b/modules/router-store/migrations/migration.json @@ -20,6 +20,11 @@ "description": "The road to v14", "version": "14-beta", "factory": "./14_0_0/index" + }, + "ngrx-router-store-migration-05": { + "description": "The road to v15.2.0", + "version": "15.2.0", + "factory": "./15_2_0/index" } } }