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 82c0f47
Show file tree
Hide file tree
Showing 5 changed files with 256 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);
}
});
});
103 changes: 103 additions & 0 deletions modules/effects/migrations/9_0_0/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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';

const OLD_TEXT = 'resubscribeOnError';
const NEW_TEXT = 'useEffectsErrorHandler';

function renameErrorHandlerConfig(): Rule {
return (tree: Tree, ctx: SchematicContext) => {
visitTSSourceFiles(tree, sourceFile => {
const createChange = (node: ts.Node) =>
createReplaceChange(sourceFile, node, OLD_TEXT, NEW_TEXT);

const changes: ReplaceChange[] = [
...findCreateEffectsConfigKey(sourceFile, createChange),
...findEffectDecoratorConfigKey(sourceFile, createChange),
];

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 findCreateEffectsConfigKey(
sourceFile: ts.SourceFile,
createChange: (node: ts.Node) => ReplaceChange
) {
const changes: ReplaceChange[] = [];
ts.forEachChild(sourceFile, node => findKey(node, changes));
return changes;

function findKey(node: ts.Node, changes: ReplaceChange[]) {
if (
ts.isIdentifier(node) &&
node.text === OLD_TEXT &&
isEffectDecorator(node.parent.parent.parent.parent)
) {
changes.push(createChange(node));
}

ts.forEachChild(node, childNode => findKey(childNode, changes));
}
}

function findEffectDecoratorConfigKey(
sourceFile: ts.SourceFile,
createChange: (node: ts.Node) => ReplaceChange
) {
const changes: ReplaceChange[] = [];
ts.forEachChild(sourceFile, node => findKey(node, changes));
return changes;

function findKey(node: ts.Node, changes: ReplaceChange[]) {
if (
ts.isIdentifier(node) &&
node.text === OLD_TEXT &&
isCreateEffectFunction(node.parent.parent.parent)
) {
changes.push(createChange(node));
}

ts.forEachChild(node, childNode => findKey(childNode, changes));
}
}

function isCreateEffectFunction(node: ts.Node): node is ts.CallExpression {
return (
ts.isCallExpression(node) &&
ts.isIdentifier(node.expression) &&
node.expression.text === 'createEffect'
);
}

function isEffectDecorator(node: ts.Node) {
return (
ts.isDecorator(node) &&
ts.isCallExpression(node.expression) &&
ts.isIdentifier(node.expression.expression) &&
node.expression.expression.text === 'Effect'
);
}

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 82c0f47

Please sign in to comment.