Skip to content

Commit

Permalink
feat(Effects): Add migration for breaking change that renames effects…
Browse files Browse the repository at this point in the history
… error handler config key
  • Loading branch information
zak-cloudnc committed Jan 26, 2020
1 parent f2e40f2 commit a4b7e47
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 1 deletion.
146 changes: 146 additions & 0 deletions modules/effects/migrations/9_0_0/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { Tree } from '@angular-devkit/schematics';
import {
SchematicTestRunner,
UnitTestTree,
} from '@angular-devkit/schematics/testing';
import * as path from 'path';
import { createPackageJson } from '../../../schematics-core/testing/create-package';

describe('Effects Migration 9_0_0', () => {
let appTree: UnitTestTree;
const collectionPath = path.join(__dirname, '../migration.json');
const pkgName = 'effects';

beforeEach(() => {
appTree = new UnitTestTree(Tree.empty());
appTree.create(
'/tsconfig.json',
`
{
"include": [**./*.ts"]
}
`
);
createPackageJson('', pkgName, appTree);
});

describe('Replaces resubscribeOnError with useEffectsErrorHandler in effect options', () => {
describe('should replace resubscribeOnError configuration key with useEffectsErrorHandler', () => {
it('in createEffect() effect creator', () => {
const input = `
import { Injectable } from '@angular/core';
import { Actions, ofType, createEffect } from '@ngrx/effects';
import { tap } from 'rxjs/operators';
@Injectable()
export class LogEffects {
constructor(private actions$: Actions) {}
logActions$ = createEffect(() =>
this.actions$.pipe(
tap(action => console.log(action))
), { resubscribeOnError: false });
}
`;

const expected = `
import { Injectable } from '@angular/core';
import { Actions, ofType, createEffect } from '@ngrx/effects';
import { tap } from 'rxjs/operators';
@Injectable()
export class LogEffects {
constructor(private actions$: Actions) {}
logActions$ = createEffect(() =>
this.actions$.pipe(
tap(action => console.log(action))
), { useEffectsErrorHandler: false });
}
`;

test(input, expected);
});

it('in @Effect() effect creator', () => {
const input = `
import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { tap } from 'rxjs/operators';
@Injectable()
export class LogEffects {
constructor(private actions$: Actions) {}
@Effect({ resubscribeOnError: false })
logActions$ = this.actions$.pipe(
tap(action => console.log(action))
)
}
`;

const expected = `
import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { tap } from 'rxjs/operators';
@Injectable()
export class LogEffects {
constructor(private actions$: Actions) {}
@Effect({ useEffectsErrorHandler: false })
logActions$ = this.actions$.pipe(
tap(action => console.log(action))
)
}
`;

test(input, expected);
});

describe('should not replace non-ngrx identifiers', () => {
it('in module scope', () => {
const input = `
export const resubscribeOnError = null;
`;

test(input, input);
});

it('within create effect callback', () => {
const input = `
import { Injectable } from '@angular/core';
import { Actions, ofType, createEffect } from '@ngrx/effects';
import { tap } from 'rxjs/operators';
@Injectable()
export class LogEffects {
constructor(private actions$: Actions) {}
logActions$ = createEffect(() =>
this.actions$.pipe(
tap(resubscribeOnError => console.log(resubscribeOnError))
));
}
`;

test(input, input);
});
});
});

function test(input: string, expected: string) {
appTree.create('./app.module.ts', input);
const runner = new SchematicTestRunner('schematics', collectionPath);

const newTree = runner.runSchematic(
`ngrx-${pkgName}-migration-02`,
{},
appTree
);
const file = newTree.readContent('app.module.ts');

expect(file).toBe(expected);
}
});
});
112 changes: 112 additions & 0 deletions modules/effects/migrations/9_0_0/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import {
chain,
Rule,
SchematicContext,
Tree,
} from '@angular-devkit/schematics';
import {
commitChanges,
visitTSSourceFiles,
} from '@ngrx/effects/schematics-core';
import {
createReplaceChange,
ReplaceChange,
} from '@ngrx/effects/schematics-core';
import * as ts from 'typescript';

function renameErrorHandlerConfig(): Rule {
return (tree: Tree, ctx: SchematicContext) => {
visitTSSourceFiles(tree, sourceFile => {
const changes: ReplaceChange[] = replaceEffectConfigKeys(
sourceFile,
'resubscribeOnError',
'useEffectsErrorHandler'
);

commitChanges(tree, sourceFile.fileName, changes);

if (changes.length) {
ctx.logger.info(
`[@ngrx/effects] Updated Effects configuration, see the migration guide (https://ngrx.io/guide/migration/v9#effects) for more info`
);
}
});
};
}

function replaceEffectConfigKeys(
sourceFile: ts.SourceFile,
oldText: string,
newText: string
): ReplaceChange[] {
const changes: ReplaceChange[] = [];

ts.forEachChild(sourceFile, node => {
visitCreateEffectFunctionCreator(node, createEffectNode => {
// only search the second arg to createEffect() (the config object)
if (createEffectNode.arguments[1]) {
findAndReplaceText(createEffectNode.arguments[1]);
}
});

visitEffectDecorator(node, effectDecoratorNode => {
findAndReplaceText(effectDecoratorNode);
});
});

return changes;

function findAndReplaceText(node: ts.Node): void {
visitIdentifierWithText(node, oldText, match => {
changes.push(createReplaceChange(sourceFile, match, oldText, newText));
});
}
}

function visitIdentifierWithText(
node: ts.Node,
text: string,
visitor: (node: ts.Node) => void
) {
if (ts.isIdentifier(node) && node.text === text) {
visitor(node);
}

ts.forEachChild(node, childNode =>
visitIdentifierWithText(childNode, text, visitor)
);
}

function visitEffectDecorator(node: ts.Node, visitor: (node: ts.Node) => void) {
if (
ts.isDecorator(node) &&
ts.isCallExpression(node.expression) &&
ts.isIdentifier(node.expression.expression) &&
node.expression.expression.text === 'Effect'
) {
visitor(node);
}

ts.forEachChild(node, childNode => visitEffectDecorator(childNode, visitor));
}

function visitCreateEffectFunctionCreator(
node: ts.Node,
visitor: (node: ts.CallExpression) => void
) {
if (
ts.isCallExpression(node) &&
ts.isIdentifier(node.expression) &&
node.expression.text === 'createEffect'
) {
visitor(node);
}

ts.forEachChild(node, childNode =>
visitCreateEffectFunctionCreator(childNode, visitor)
);
}

export default function(): Rule {
return chain([renameErrorHandlerConfig()]);
}
1 change: 1 addition & 0 deletions modules/effects/migrations/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ ts_library(
deps = [
"//modules/effects/schematics-core",
"@npm//@angular-devkit/schematics",
"@npm//typescript",
],
)

Expand Down
5 changes: 5 additions & 0 deletions modules/effects/migrations/migration.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
"description": "The road to v6",
"version": "5.2",
"factory": "./6_0_0/index"
},
"ngrx-effects-migration-02": {
"description": "The road to v9",
"version": "9-beta",
"factory": "./9_0_0/index"
}
}
}
2 changes: 1 addition & 1 deletion projects/ngrx.io/content/guide/effects/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Usage:

<code-example header="log.effects.ts">
import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { tap } from 'rxjs/operators';

@Injectable()
Expand Down

0 comments on commit a4b7e47

Please sign in to comment.