Skip to content

Commit d6a2c56

Browse files
authored
feat(signals): add migration schematic to rename withEffects to withEventHandlers (#5032)
Closes #5010
1 parent 86dae22 commit d6a2c56

File tree

3 files changed

+239
-0
lines changed

3 files changed

+239
-0
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import {
2+
SchematicTestRunner,
3+
UnitTestTree,
4+
} from '@angular-devkit/schematics/testing';
5+
import { createWorkspace } from '@ngrx/schematics-core/testing';
6+
import { tags, logging } from '@angular-devkit/core';
7+
import * as path from 'path';
8+
9+
describe('21_0_0-beta_0-rename-withEffects-to-withEventHandlers', () => {
10+
const collectionPath = path.join(
11+
process.cwd(),
12+
'dist/modules/signals/migrations/migration.json'
13+
);
14+
const schematicRunner = new SchematicTestRunner('schematics', collectionPath);
15+
16+
let appTree: UnitTestTree;
17+
18+
beforeEach(async () => {
19+
appTree = await createWorkspace(schematicRunner, appTree);
20+
});
21+
22+
const verify = async (input: string, output: string) => {
23+
appTree.create('main.ts', input);
24+
25+
const logs: logging.LogEntry[] = [];
26+
schematicRunner.logger.subscribe((e) => logs.push(e));
27+
28+
const tree = await schematicRunner.runSchematic(
29+
`21_0_0-beta_0-rename-withEffects-to-withEventHandlers`,
30+
{},
31+
appTree
32+
);
33+
34+
const actual = tree.readContent('main.ts');
35+
expect(actual).toBe(output);
36+
37+
return logs;
38+
};
39+
40+
it('renames named import', async () => {
41+
const input = tags.stripIndent`
42+
import { withEffects } from '@ngrx/signals/events';
43+
const S = signalStore(withEffects(() => {}));
44+
`;
45+
46+
const output = tags.stripIndent`
47+
import { withEventHandlers } from '@ngrx/signals/events';
48+
const S = signalStore(withEventHandlers(() => {}));
49+
`;
50+
51+
const logs = await verify(input, output);
52+
expect(logs[0].message).toContain(
53+
"Renamed 'withEffects' to 'withEventHandlers'"
54+
);
55+
});
56+
57+
it('preserves alias', async () => {
58+
const input = tags.stripIndent`
59+
import { withEffects as withAlias } from '@ngrx/signals/events';
60+
const S = signalStore(withAlias(() => {}));
61+
`;
62+
63+
const output = tags.stripIndent`
64+
import { withEventHandlers as withAlias } from '@ngrx/signals/events';
65+
const S = signalStore(withAlias(() => {}));
66+
`;
67+
68+
await verify(input, output);
69+
});
70+
71+
it('renames namespace usage', async () => {
72+
const input = tags.stripIndent`
73+
import * as events from '@ngrx/signals/events';
74+
const S = signalStore(events.withEffects(() => {}));
75+
`;
76+
77+
const output = tags.stripIndent`
78+
import * as events from '@ngrx/signals/events';
79+
const S = signalStore(events.withEventHandlers(() => {}));
80+
`;
81+
82+
await verify(input, output);
83+
});
84+
85+
it('ignores other packages', async () => {
86+
const input = tags.stripIndent`
87+
import { withEffects } from 'other';
88+
const S = withEffects(() => {});
89+
`;
90+
91+
await verify(input, input);
92+
});
93+
94+
it('handles multiple imports and usages', async () => {
95+
const input = tags.stripIndent`
96+
import { withEffects, event } from '@ngrx/signals/events';
97+
const S1 = signalStore(withEffects(() => {}));
98+
const S2 = signalStore(withEffects(() => {}));
99+
`;
100+
101+
const output = tags.stripIndent`
102+
import { withEventHandlers, event } from '@ngrx/signals/events';
103+
const S1 = signalStore(withEventHandlers(() => {}));
104+
const S2 = signalStore(withEventHandlers(() => {}));
105+
`;
106+
107+
await verify(input, output);
108+
});
109+
});
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import {
2+
Change,
3+
commitChanges,
4+
createReplaceChange,
5+
findNodes,
6+
visitTSSourceFiles,
7+
} from '../../schematics-core';
8+
import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
9+
import * as ts from 'typescript';
10+
11+
const EVENTS_PKG = '@ngrx/signals/events';
12+
const OLD_NAME = 'withEffects';
13+
const NEW_NAME = 'withEventHandlers';
14+
15+
export default function migrateWithEventHandlers(): Rule {
16+
return (tree: Tree, ctx: SchematicContext) => {
17+
visitTSSourceFiles(tree, (sourceFile) => {
18+
const changes: Change[] = [];
19+
const namespaceImports = new Set<string>();
20+
21+
const directImports = new Set<string>();
22+
23+
const importDeclarations = findNodes(
24+
sourceFile,
25+
ts.SyntaxKind.ImportDeclaration
26+
) as ts.ImportDeclaration[];
27+
28+
for (const decl of importDeclarations) {
29+
const moduleSpecifier = decl.moduleSpecifier as ts.StringLiteral;
30+
if (!moduleSpecifier || moduleSpecifier.text !== EVENTS_PKG) {
31+
continue;
32+
}
33+
34+
const importClause = decl.importClause;
35+
if (!importClause || !importClause.namedBindings) {
36+
continue;
37+
}
38+
39+
if (ts.isNamedImports(importClause.namedBindings)) {
40+
const named = importClause.namedBindings;
41+
42+
named.elements.forEach((spec) => {
43+
const importedName = spec.propertyName ?? spec.name;
44+
const localName = spec.name;
45+
const hasAlias = spec.propertyName !== undefined;
46+
47+
if (importedName.text === OLD_NAME) {
48+
changes.push(
49+
createReplaceChange(
50+
sourceFile,
51+
importedName,
52+
importedName.getText(),
53+
NEW_NAME
54+
)
55+
);
56+
57+
if (!hasAlias) {
58+
directImports.add(localName.text);
59+
}
60+
}
61+
});
62+
}
63+
64+
if (ts.isNamespaceImport(importClause.namedBindings)) {
65+
namespaceImports.add(importClause.namedBindings.name.text);
66+
}
67+
}
68+
69+
if (directImports.size > 0) {
70+
const callExpressions = findNodes(
71+
sourceFile,
72+
ts.SyntaxKind.CallExpression
73+
) as ts.CallExpression[];
74+
75+
callExpressions.forEach((call) => {
76+
if (
77+
ts.isIdentifier(call.expression) &&
78+
directImports.has(call.expression.text)
79+
) {
80+
changes.push(
81+
createReplaceChange(
82+
sourceFile,
83+
call.expression,
84+
call.expression.getText(),
85+
NEW_NAME
86+
)
87+
);
88+
}
89+
});
90+
}
91+
92+
if (namespaceImports.size > 0) {
93+
const propertyAccesses = findNodes(
94+
sourceFile,
95+
ts.SyntaxKind.PropertyAccessExpression
96+
) as ts.PropertyAccessExpression[];
97+
98+
propertyAccesses.forEach((pa) => {
99+
if (
100+
ts.isIdentifier(pa.expression) &&
101+
namespaceImports.has(pa.expression.text) &&
102+
ts.isIdentifier(pa.name) &&
103+
pa.name.text === OLD_NAME
104+
) {
105+
changes.push(
106+
createReplaceChange(
107+
sourceFile,
108+
pa.name,
109+
pa.name.getText(),
110+
NEW_NAME
111+
)
112+
);
113+
}
114+
});
115+
}
116+
117+
if (changes.length) {
118+
commitChanges(tree, sourceFile.fileName, changes);
119+
ctx.logger.info(
120+
`[@ngrx/signals] Renamed '${OLD_NAME}' to '${NEW_NAME}' in /${sourceFile.fileName}`
121+
);
122+
}
123+
});
124+
};
125+
}

modules/signals/migrations/migration.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
"description": "Replace several properties with a single props object",
1616
"version": "19.0.0-rc.0",
1717
"factory": "./19_0_0-rc_0-props/index"
18+
},
19+
"21_0_0-beta_0-rename-withEffects-to-withEventHandlers": {
20+
"description": "Rename withEffects to withEventHandlers",
21+
"version": "21.0.0-beta.0",
22+
"factory": "./21_0_0-beta_0-rename-withEffects-to-withEventHandlers/index"
1823
}
1924
}
2025
}

0 commit comments

Comments
 (0)