Skip to content

Commit 93b4081

Browse files
authored
feat(effects): add migration for breaking change that renames effects error handler config key (#2335)
1 parent 3a9ad63 commit 93b4081

File tree

4 files changed

+264
-0
lines changed

4 files changed

+264
-0
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { Tree } from '@angular-devkit/schematics';
2+
import {
3+
SchematicTestRunner,
4+
UnitTestTree,
5+
} from '@angular-devkit/schematics/testing';
6+
import * as path from 'path';
7+
import { createPackageJson } from '../../../schematics-core/testing/create-package';
8+
9+
describe('Effects Migration 9_0_0', () => {
10+
let appTree: UnitTestTree;
11+
const collectionPath = path.join(__dirname, '../migration.json');
12+
const pkgName = 'effects';
13+
14+
beforeEach(() => {
15+
appTree = new UnitTestTree(Tree.empty());
16+
appTree.create(
17+
'/tsconfig.json',
18+
`
19+
{
20+
"include": [**./*.ts"]
21+
}
22+
`
23+
);
24+
createPackageJson('', pkgName, appTree);
25+
});
26+
27+
describe('Replaces resubscribeOnError with useEffectsErrorHandler in effect options', () => {
28+
describe('should replace resubscribeOnError configuration key with useEffectsErrorHandler', () => {
29+
it('in createEffect() effect creator', () => {
30+
const input = `
31+
import { Injectable } from '@angular/core';
32+
import { Actions, ofType, createEffect } from '@ngrx/effects';
33+
import { tap } from 'rxjs/operators';
34+
35+
@Injectable()
36+
export class LogEffects {
37+
constructor(private actions$: Actions) {}
38+
39+
logActions$ = createEffect(() =>
40+
this.actions$.pipe(
41+
tap(action => console.log(action))
42+
), { resubscribeOnError: false });
43+
}
44+
`;
45+
46+
const expected = `
47+
import { Injectable } from '@angular/core';
48+
import { Actions, ofType, createEffect } from '@ngrx/effects';
49+
import { tap } from 'rxjs/operators';
50+
51+
@Injectable()
52+
export class LogEffects {
53+
constructor(private actions$: Actions) {}
54+
55+
logActions$ = createEffect(() =>
56+
this.actions$.pipe(
57+
tap(action => console.log(action))
58+
), { useEffectsErrorHandler: false });
59+
}
60+
`;
61+
62+
test(input, expected);
63+
});
64+
65+
it('in @Effect() decorator', () => {
66+
const input = `
67+
import { Injectable } from '@angular/core';
68+
import { Actions, Effect, ofType } from '@ngrx/effects';
69+
import { tap } from 'rxjs/operators';
70+
71+
@Injectable()
72+
export class LogEffects {
73+
constructor(private actions$: Actions) {}
74+
75+
@Effect({ resubscribeOnError: false })
76+
logActions$ = this.actions$.pipe(
77+
tap(action => console.log(action))
78+
)
79+
}
80+
`;
81+
82+
const expected = `
83+
import { Injectable } from '@angular/core';
84+
import { Actions, Effect, ofType } from '@ngrx/effects';
85+
import { tap } from 'rxjs/operators';
86+
87+
@Injectable()
88+
export class LogEffects {
89+
constructor(private actions$: Actions) {}
90+
91+
@Effect({ useEffectsErrorHandler: false })
92+
logActions$ = this.actions$.pipe(
93+
tap(action => console.log(action))
94+
)
95+
}
96+
`;
97+
98+
test(input, expected);
99+
});
100+
});
101+
102+
describe('should not replace non-ngrx identifiers', () => {
103+
it('in module scope', () => {
104+
const input = `
105+
export const resubscribeOnError = null;
106+
`;
107+
108+
test(input, input);
109+
});
110+
111+
it('within create effect callback', () => {
112+
const input = `
113+
import { Injectable } from '@angular/core';
114+
import { Actions, ofType, createEffect } from '@ngrx/effects';
115+
import { tap } from 'rxjs/operators';
116+
117+
@Injectable()
118+
export class LogEffects {
119+
constructor(private actions$: Actions) {}
120+
121+
logActions$ = createEffect(() =>
122+
this.actions$.pipe(
123+
tap(resubscribeOnError => console.log(resubscribeOnError))
124+
));
125+
}
126+
`;
127+
128+
test(input, input);
129+
});
130+
});
131+
132+
function test(input: string, expected: string) {
133+
appTree.create('./app.module.ts', input);
134+
const runner = new SchematicTestRunner('schematics', collectionPath);
135+
136+
const newTree = runner.runSchematic(
137+
`ngrx-${pkgName}-migration-02`,
138+
{},
139+
appTree
140+
);
141+
const file = newTree.readContent('app.module.ts');
142+
143+
expect(file).toBe(expected);
144+
}
145+
});
146+
});
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import {
2+
chain,
3+
Rule,
4+
SchematicContext,
5+
Tree,
6+
} from '@angular-devkit/schematics';
7+
import {
8+
commitChanges,
9+
visitTSSourceFiles,
10+
} from '@ngrx/effects/schematics-core';
11+
import {
12+
createReplaceChange,
13+
ReplaceChange,
14+
} from '@ngrx/effects/schematics-core';
15+
import * as ts from 'typescript';
16+
17+
function renameErrorHandlerConfig(): Rule {
18+
return (tree: Tree, ctx: SchematicContext) => {
19+
visitTSSourceFiles(tree, sourceFile => {
20+
const changes: ReplaceChange[] = replaceEffectConfigKeys(
21+
sourceFile,
22+
'resubscribeOnError',
23+
'useEffectsErrorHandler'
24+
);
25+
26+
commitChanges(tree, sourceFile.fileName, changes);
27+
28+
if (changes.length) {
29+
ctx.logger.info(
30+
`[@ngrx/effects] Updated Effects configuration, see the migration guide (https://ngrx.io/guide/migration/v9#effects) for more info`
31+
);
32+
}
33+
});
34+
};
35+
}
36+
37+
function replaceEffectConfigKeys(
38+
sourceFile: ts.SourceFile,
39+
oldText: string,
40+
newText: string
41+
): ReplaceChange[] {
42+
const changes: ReplaceChange[] = [];
43+
44+
ts.forEachChild(sourceFile, node => {
45+
visitCreateEffectFunctionCreator(node, createEffectNode => {
46+
const [effectDeclaration, configNode] = createEffectNode.arguments;
47+
if (configNode) {
48+
findAndReplaceText(configNode);
49+
}
50+
});
51+
52+
visitEffectDecorator(node, effectDecoratorNode => {
53+
findAndReplaceText(effectDecoratorNode);
54+
});
55+
});
56+
57+
return changes;
58+
59+
function findAndReplaceText(node: ts.Node): void {
60+
visitIdentifierWithText(node, oldText, match => {
61+
changes.push(createReplaceChange(sourceFile, match, oldText, newText));
62+
});
63+
}
64+
}
65+
66+
function visitIdentifierWithText(
67+
node: ts.Node,
68+
text: string,
69+
visitor: (node: ts.Node) => void
70+
) {
71+
if (ts.isIdentifier(node) && node.text === text) {
72+
visitor(node);
73+
}
74+
75+
ts.forEachChild(node, childNode =>
76+
visitIdentifierWithText(childNode, text, visitor)
77+
);
78+
}
79+
80+
function visitEffectDecorator(node: ts.Node, visitor: (node: ts.Node) => void) {
81+
if (
82+
ts.isDecorator(node) &&
83+
ts.isCallExpression(node.expression) &&
84+
ts.isIdentifier(node.expression.expression) &&
85+
node.expression.expression.text === 'Effect'
86+
) {
87+
visitor(node);
88+
}
89+
90+
ts.forEachChild(node, childNode => visitEffectDecorator(childNode, visitor));
91+
}
92+
93+
function visitCreateEffectFunctionCreator(
94+
node: ts.Node,
95+
visitor: (node: ts.CallExpression) => void
96+
) {
97+
if (
98+
ts.isCallExpression(node) &&
99+
ts.isIdentifier(node.expression) &&
100+
node.expression.text === 'createEffect'
101+
) {
102+
visitor(node);
103+
}
104+
105+
ts.forEachChild(node, childNode =>
106+
visitCreateEffectFunctionCreator(childNode, visitor)
107+
);
108+
}
109+
110+
export default function(): Rule {
111+
return chain([renameErrorHandlerConfig()]);
112+
}

modules/effects/migrations/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ ts_library(
1717
deps = [
1818
"//modules/effects/schematics-core",
1919
"@npm//@angular-devkit/schematics",
20+
"@npm//typescript",
2021
],
2122
)
2223

modules/effects/migrations/migration.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
"description": "The road to v6",
77
"version": "5.2",
88
"factory": "./6_0_0/index"
9+
},
10+
"ngrx-effects-migration-02": {
11+
"description": "The road to v9",
12+
"version": "9-beta",
13+
"factory": "./9_0_0/index"
914
}
1015
}
1116
}

0 commit comments

Comments
 (0)