@openrewrite/recipes-angular 1.9.0 → 1.10.0-20260813-195315

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dist/index.d.ts +20 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +84 -2
  4. package/dist/index.js.map +1 -1
  5. package/dist/migration/add-eager-change-detection.d.ts +8 -0
  6. package/dist/migration/add-eager-change-detection.d.ts.map +1 -0
  7. package/dist/migration/add-eager-change-detection.js +130 -0
  8. package/dist/migration/add-eager-change-detection.js.map +1 -0
  9. package/dist/migration/add-istanbul-instrumenter.d.ts +8 -0
  10. package/dist/migration/add-istanbul-instrumenter.d.ts.map +1 -0
  11. package/dist/migration/add-istanbul-instrumenter.js +80 -0
  12. package/dist/migration/add-istanbul-instrumenter.js.map +1 -0
  13. package/dist/migration/add-no-incremental-hydration.d.ts +8 -0
  14. package/dist/migration/add-no-incremental-hydration.d.ts.map +1 -0
  15. package/dist/migration/add-no-incremental-hydration.js +69 -0
  16. package/dist/migration/add-no-incremental-hydration.js.map +1 -0
  17. package/dist/migration/add-strict-templates-false.d.ts +8 -0
  18. package/dist/migration/add-strict-templates-false.d.ts.map +1 -0
  19. package/dist/migration/add-strict-templates-false.js +84 -0
  20. package/dist/migration/add-strict-templates-false.js.map +1 -0
  21. package/dist/migration/add-trust-proxy-headers.d.ts +8 -0
  22. package/dist/migration/add-trust-proxy-headers.d.ts.map +1 -0
  23. package/dist/migration/add-trust-proxy-headers.js +82 -0
  24. package/dist/migration/add-trust-proxy-headers.js.map +1 -0
  25. package/dist/migration/add-with-xhr.d.ts +8 -0
  26. package/dist/migration/add-with-xhr.d.ts.map +1 -0
  27. package/dist/migration/add-with-xhr.js +70 -0
  28. package/dist/migration/add-with-xhr.js.map +1 -0
  29. package/dist/migration/js-support.d.ts +6 -0
  30. package/dist/migration/js-support.d.ts.map +1 -0
  31. package/dist/migration/js-support.js +94 -0
  32. package/dist/migration/js-support.js.map +1 -0
  33. package/dist/migration/json-support.d.ts +19 -0
  34. package/dist/migration/json-support.d.ts.map +1 -0
  35. package/dist/migration/json-support.js +117 -0
  36. package/dist/migration/json-support.js.map +1 -0
  37. package/dist/migration/migrate-input-to-signal.d.ts.map +1 -1
  38. package/dist/migration/migrate-input-to-signal.js +3 -67
  39. package/dist/migration/migrate-input-to-signal.js.map +1 -1
  40. package/dist/migration/migrate-model-with-output.d.ts +8 -0
  41. package/dist/migration/migrate-model-with-output.d.ts.map +1 -0
  42. package/dist/migration/migrate-model-with-output.js +192 -0
  43. package/dist/migration/migrate-model-with-output.js.map +1 -0
  44. package/dist/migration/remove-with-fetch.d.ts +8 -0
  45. package/dist/migration/remove-with-fetch.d.ts.map +1 -0
  46. package/dist/migration/remove-with-fetch.js +73 -0
  47. package/dist/migration/remove-with-fetch.js.map +1 -0
  48. package/dist/migration/rename-angular-js-globals.d.ts +8 -0
  49. package/dist/migration/rename-angular-js-globals.d.ts.map +1 -0
  50. package/dist/migration/rename-angular-js-globals.js +56 -0
  51. package/dist/migration/rename-angular-js-globals.js.map +1 -0
  52. package/dist/migration/rename-default-change-detection.d.ts +8 -0
  53. package/dist/migration/rename-default-change-detection.d.ts.map +1 -0
  54. package/dist/migration/rename-default-change-detection.js +52 -0
  55. package/dist/migration/rename-default-change-detection.js.map +1 -0
  56. package/dist/migration/rename-ssr-experimental-platform.d.ts +8 -0
  57. package/dist/migration/rename-ssr-experimental-platform.d.ts.map +1 -0
  58. package/dist/migration/rename-ssr-experimental-platform.js +124 -0
  59. package/dist/migration/rename-ssr-experimental-platform.js.map +1 -0
  60. package/dist/migration/replace-create-ng-module-ref.d.ts +8 -0
  61. package/dist/migration/replace-create-ng-module-ref.d.ts.map +1 -0
  62. package/dist/migration/replace-create-ng-module-ref.js +51 -0
  63. package/dist/migration/replace-create-ng-module-ref.js.map +1 -0
  64. package/dist/migration/suppress-safe-navigation-diagnostics.d.ts +8 -0
  65. package/dist/migration/suppress-safe-navigation-diagnostics.d.ts.map +1 -0
  66. package/dist/migration/suppress-safe-navigation-diagnostics.js +55 -0
  67. package/dist/migration/suppress-safe-navigation-diagnostics.js.map +1 -0
  68. package/dist/migration/upgrade-to-angular-22.d.ts +14 -0
  69. package/dist/migration/upgrade-to-angular-22.d.ts.map +1 -0
  70. package/dist/migration/upgrade-to-angular-22.js +112 -0
  71. package/dist/migration/upgrade-to-angular-22.js.map +1 -0
  72. package/dist/search/find-can-match-usage.d.ts +8 -0
  73. package/dist/search/find-can-match-usage.d.ts.map +1 -0
  74. package/dist/search/find-can-match-usage.js +53 -0
  75. package/dist/search/find-can-match-usage.js.map +1 -0
  76. package/dist/search/find-check-no-changes-usage.d.ts +8 -0
  77. package/dist/search/find-check-no-changes-usage.d.ts.map +1 -0
  78. package/dist/search/find-check-no-changes-usage.js +50 -0
  79. package/dist/search/find-check-no-changes-usage.js.map +1 -0
  80. package/dist/search/find-http-xhr-backend-usage.d.ts +8 -0
  81. package/dist/search/find-http-xhr-backend-usage.d.ts.map +1 -0
  82. package/dist/search/find-http-xhr-backend-usage.js +65 -0
  83. package/dist/search/find-http-xhr-backend-usage.js.map +1 -0
  84. package/dist/search/find-implicit-params-inheritance.d.ts +8 -0
  85. package/dist/search/find-implicit-params-inheritance.d.ts.map +1 -0
  86. package/dist/search/find-implicit-params-inheritance.js +74 -0
  87. package/dist/search/find-implicit-params-inheritance.js.map +1 -0
  88. package/dist/search/find-provide-routes-usage.d.ts +8 -0
  89. package/dist/search/find-provide-routes-usage.d.ts.map +1 -0
  90. package/dist/search/find-provide-routes-usage.js +64 -0
  91. package/dist/search/find-provide-routes-usage.js.map +1 -0
  92. package/dist/search/find-template-safe-navigation.d.ts +8 -0
  93. package/dist/search/find-template-safe-navigation.d.ts.map +1 -0
  94. package/dist/search/find-template-safe-navigation.js +105 -0
  95. package/dist/search/find-template-safe-navigation.js.map +1 -0
  96. package/package.json +1 -1
  97. package/src/index.ts +60 -0
  98. package/src/migration/add-eager-change-detection.ts +109 -0
  99. package/src/migration/add-istanbul-instrumenter.ts +62 -0
  100. package/src/migration/add-no-incremental-hydration.ts +49 -0
  101. package/src/migration/add-strict-templates-false.ts +62 -0
  102. package/src/migration/add-trust-proxy-headers.ts +61 -0
  103. package/src/migration/add-with-xhr.ts +50 -0
  104. package/src/migration/js-support.ts +92 -0
  105. package/src/migration/json-support.ts +119 -0
  106. package/src/migration/migrate-input-to-signal.ts +1 -67
  107. package/src/migration/migrate-model-with-output.ts +201 -0
  108. package/src/migration/remove-with-fetch.ts +53 -0
  109. package/src/migration/rename-angular-js-globals.ts +37 -0
  110. package/src/migration/rename-default-change-detection.ts +33 -0
  111. package/src/migration/rename-ssr-experimental-platform.ts +108 -0
  112. package/src/migration/replace-create-ng-module-ref.ts +32 -0
  113. package/src/migration/suppress-safe-navigation-diagnostics.ts +42 -0
  114. package/src/migration/upgrade-to-angular-22.ts +92 -0
  115. package/src/search/find-can-match-usage.ts +32 -0
  116. package/src/search/find-check-no-changes-usage.ts +29 -0
  117. package/src/search/find-http-xhr-backend-usage.ts +44 -0
  118. package/src/search/find-implicit-params-inheritance.ts +49 -0
  119. package/src/search/find-provide-routes-usage.ts +43 -0
  120. package/src/search/find-template-safe-navigation.ts +72 -0
@@ -0,0 +1,201 @@
1
+ /*
2
+ * Copyright 2026 the original author or authors.
3
+ *
4
+ * Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
5
+ */
6
+
7
+ import {emptyMarkers, ExecutionContext, markers as mkMarkers, randomId, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {JavaScriptVisitor, JS, maybeAddImport, maybeRemoveImport} from "@openrewrite/rewrite/javascript";
9
+ import {emptySpace, isIdentifier, J, singleSpace} from "@openrewrite/rewrite/java";
10
+ import {create} from "mutative";
11
+ import {buildOptionsObject, buildPropertyAssignment, identifier, rightPadded} from "./js-support";
12
+
13
+ export class MigrateModelWithOutput extends Recipe {
14
+ readonly name = "org.openrewrite.angular.migration.migrate-model-with-output";
15
+ readonly displayName: string = "Split a `model()` that has a matching explicit output";
16
+ readonly description: string = "Rewrites `foo = model()` to `fooInput = input({alias: 'foo'})` plus " +
17
+ "`foo = linkedSignal(this.fooInput)` when the same class also declares a `fooChange` output. " +
18
+ "`model()` already contributes a `fooChange` output, and Angular 22 rejects the resulting duplicate.";
19
+
20
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
21
+ return new class extends JavaScriptVisitor<ExecutionContext> {
22
+ protected async visitClassDeclaration(classDecl: J.ClassDeclaration, p: ExecutionContext): Promise<J | undefined> {
23
+ const c = await super.visitClassDeclaration(classDecl, p) as J.ClassDeclaration;
24
+ if (!c) return c;
25
+
26
+ const outputs = new Set<string>();
27
+ for (const stmt of c.body.statements) {
28
+ const name = declaredName(stmt.element);
29
+ if (name && declaresOutput(stmt.element)) outputs.add(name);
30
+ }
31
+ if (outputs.size === 0) return c;
32
+
33
+ let changed = false;
34
+ const newStatements = c.body.statements.flatMap(stmt => {
35
+ const name = declaredName(stmt.element);
36
+ if (!name || !outputs.has(`${name}Change`)) return [stmt];
37
+
38
+ const varDecl = stmt.element as J.VariableDeclarations;
39
+ const modelCall = modelInvocation(varDecl);
40
+ if (!modelCall) return [stmt];
41
+
42
+ const inputCall = toInputCall(modelCall, name);
43
+ if (!inputCall) return [stmt];
44
+
45
+ changed = true;
46
+ const semicolon = mkMarkers({kind: 'org.openrewrite.java.marker.Semicolon' as const, id: randomId()});
47
+ return [
48
+ {...stmt, element: withNameAndInitializer(varDecl, `${name}Input`, inputCall), markers: semicolon},
49
+ {...stmt, element: withNameAndInitializer(varDecl, name, linkedSignalCall(`${name}Input`)), markers: semicolon},
50
+ ];
51
+ });
52
+
53
+ if (!changed) return c;
54
+
55
+ maybeAddImport(this, {module: '@angular/core', member: 'input'});
56
+ maybeAddImport(this, {module: '@angular/core', member: 'linkedSignal'});
57
+ maybeRemoveImport(this, '@angular/core', 'model');
58
+
59
+ return create(c, (draft: any) => {
60
+ draft.body.statements = newStatements;
61
+ }) as J.ClassDeclaration;
62
+ }
63
+ };
64
+ }
65
+ }
66
+
67
+ function declaredName(statement: J): string | undefined {
68
+ if (statement.kind !== J.Kind.VariableDeclarations) return undefined;
69
+ const variables = (statement as J.VariableDeclarations).variables;
70
+ if (variables.length !== 1) return undefined;
71
+ const name = variables[0].element.name;
72
+ return isIdentifier(name) ? name.simpleName : undefined;
73
+ }
74
+
75
+ function declaresOutput(statement: J): boolean {
76
+ if (statement.kind !== J.Kind.VariableDeclarations) return false;
77
+ const varDecl = statement as J.VariableDeclarations;
78
+
79
+ if (varDecl.leadingAnnotations.some(a => isIdentifier(a.annotationType)
80
+ && (a.annotationType as J.Identifier).simpleName === 'Output')) {
81
+ return true;
82
+ }
83
+
84
+ return !!factoryInvocation(varDecl, 'output');
85
+ }
86
+
87
+ function modelInvocation(varDecl: J.VariableDeclarations): J.MethodInvocation | undefined {
88
+ return factoryInvocation(varDecl, 'model');
89
+ }
90
+
91
+ /** Matches `factory(...)` and `factory.required(...)` initializers. */
92
+ function factoryInvocation(varDecl: J.VariableDeclarations, factory: string): J.MethodInvocation | undefined {
93
+ const initializer = varDecl.variables[0]?.element.initializer?.element;
94
+ if (!initializer || initializer.kind !== J.Kind.MethodInvocation) return undefined;
95
+
96
+ const call = initializer as J.MethodInvocation;
97
+ if (!isIdentifier(call.name)) return undefined;
98
+
99
+ if (!call.select) {
100
+ return call.name.simpleName === factory ? call : undefined;
101
+ }
102
+ const select = call.select.element;
103
+ return call.name.simpleName === 'required' && isIdentifier(select) && select.simpleName === factory ? call : undefined;
104
+ }
105
+
106
+ /**
107
+ * Turns the `model()` call into the matching `input()` call, aliased back to the original
108
+ * property name so templates keep binding to it. Returns undefined when the options argument
109
+ * is something other than an object literal, which we cannot merge an alias into.
110
+ */
111
+ function toInputCall(modelCall: J.MethodInvocation, name: string): J.MethodInvocation | undefined {
112
+ const required = !!modelCall.select;
113
+ const args = (modelCall.arguments?.elements ?? []).filter(rp => rp.element.kind !== J.Kind.Empty);
114
+ const optionsIndex = required ? 0 : 1;
115
+ const options = args[optionsIndex]?.element;
116
+
117
+ let newArgs;
118
+ if (!options) {
119
+ newArgs = [...args, rightPadded({
120
+ ...buildOptionsObject([buildPropertyAssignment('alias', name)]),
121
+ prefix: args.length > 0 ? singleSpace : emptySpace,
122
+ })];
123
+ } else if (options.kind === J.Kind.NewClass && (options as J.NewClass).body) {
124
+ const body = (options as J.NewClass).body!;
125
+ const properties = body.statements.filter(rp => rp.element.kind !== J.Kind.Empty);
126
+ const hasAlias = properties.some(rp => {
127
+ const element = rp.element;
128
+ if (element.kind !== JS.Kind.PropertyAssignment) return false;
129
+ const propName = (element as JS.PropertyAssignment).name.element;
130
+ return isIdentifier(propName) && propName.simpleName === 'alias';
131
+ });
132
+ if (hasAlias) {
133
+ newArgs = args;
134
+ } else {
135
+ const withAlias = {
136
+ ...options,
137
+ body: {...body, statements: [rightPadded(buildPropertyAssignment('alias', name)), ...properties]},
138
+ };
139
+ newArgs = [...args];
140
+ newArgs[optionsIndex] = {...args[optionsIndex], element: withAlias};
141
+ }
142
+ } else {
143
+ return undefined;
144
+ }
145
+
146
+ return {
147
+ ...modelCall,
148
+ select: required ? {...modelCall.select!, element: identifier('input')} : undefined,
149
+ name: required ? modelCall.name : identifier('input'),
150
+ arguments: {...modelCall.arguments!, elements: newArgs},
151
+ methodType: undefined,
152
+ } as J.MethodInvocation;
153
+ }
154
+
155
+ function linkedSignalCall(inputName: string): J.MethodInvocation {
156
+ const thisAccess: J.FieldAccess = {
157
+ kind: J.Kind.FieldAccess,
158
+ id: randomId(),
159
+ prefix: emptySpace,
160
+ markers: emptyMarkers,
161
+ target: identifier('this'),
162
+ name: {kind: J.Kind.LeftPadded, before: emptySpace, element: identifier(inputName), markers: emptyMarkers},
163
+ type: undefined,
164
+ } as J.FieldAccess;
165
+
166
+ return {
167
+ kind: J.Kind.MethodInvocation,
168
+ id: randomId(),
169
+ prefix: singleSpace,
170
+ markers: emptyMarkers,
171
+ select: undefined,
172
+ typeParameters: undefined,
173
+ name: identifier('linkedSignal'),
174
+ arguments: {
175
+ kind: 'org.openrewrite.java.tree.JContainer',
176
+ before: emptySpace,
177
+ markers: emptyMarkers,
178
+ elements: [rightPadded(thisAccess)],
179
+ },
180
+ methodType: undefined,
181
+ } as unknown as J.MethodInvocation;
182
+ }
183
+
184
+ function withNameAndInitializer(varDecl: J.VariableDeclarations, name: string, call: J.MethodInvocation): any {
185
+ const namedVar = varDecl.variables[0].element;
186
+ return {
187
+ ...varDecl,
188
+ id: randomId(),
189
+ typeExpression: undefined,
190
+ variables: [{
191
+ ...varDecl.variables[0],
192
+ element: {
193
+ ...namedVar,
194
+ id: randomId(),
195
+ name: identifier(name, (namedVar.name as J.Identifier).prefix),
196
+ dimensionsAfterName: [],
197
+ initializer: {before: singleSpace, element: call, markers: emptyMarkers},
198
+ },
199
+ }],
200
+ };
201
+ }
@@ -0,0 +1,53 @@
1
+ /*
2
+ * Copyright 2026 the original author or authors.
3
+ *
4
+ * Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
5
+ */
6
+
7
+ import {emptyMarkers, ExecutionContext, randomId, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {JavaScriptVisitor, maybeRemoveImport} from "@openrewrite/rewrite/javascript";
9
+ import {emptySpace, isIdentifier, J} from "@openrewrite/rewrite/java";
10
+
11
+ export class RemoveWithFetch extends Recipe {
12
+ readonly name = "org.openrewrite.angular.migration.remove-with-fetch";
13
+ readonly displayName: string = "Remove `withFetch()` from `provideHttpClient()`";
14
+ readonly description: string = "Removes the `withFetch()` feature from `provideHttpClient()` calls. " +
15
+ "Angular 22 uses the Fetch API by default and deprecated `withFetch()`, which can be safely removed.";
16
+
17
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
18
+ return new class extends JavaScriptVisitor<ExecutionContext> {
19
+ override async visitMethodInvocation(method: J.MethodInvocation, p: ExecutionContext): Promise<J | undefined> {
20
+ const m = await super.visitMethodInvocation(method, p) as J.MethodInvocation;
21
+ if (!m || m.name.simpleName !== 'provideHttpClient') return m;
22
+
23
+ const elements = m.arguments?.elements ?? [];
24
+ const retained = elements.filter(rp => !isWithFetchCall(rp.element));
25
+ if (retained.length === elements.length) return m;
26
+
27
+ maybeRemoveImport(this, '@angular/common/http', 'withFetch');
28
+
29
+ const newElements = retained.length > 0
30
+ ? [{...retained[0], element: {...retained[0].element, prefix: elements[0].element.prefix}}, ...retained.slice(1)]
31
+ : [{
32
+ kind: J.Kind.RightPadded,
33
+ element: {
34
+ kind: J.Kind.Empty,
35
+ id: randomId(),
36
+ prefix: emptySpace,
37
+ markers: emptyMarkers,
38
+ } as J.Empty,
39
+ after: emptySpace,
40
+ markers: emptyMarkers,
41
+ } as J.RightPadded<any>];
42
+
43
+ return {...m, arguments: {...m.arguments!, elements: newElements}} as J.MethodInvocation;
44
+ }
45
+ }();
46
+ }
47
+ }
48
+
49
+ function isWithFetchCall(expression: J): boolean {
50
+ if (expression.kind !== J.Kind.MethodInvocation) return false;
51
+ const invocation = expression as J.MethodInvocation;
52
+ return !invocation.select && isIdentifier(invocation.name) && invocation.name.simpleName === 'withFetch';
53
+ }
@@ -0,0 +1,37 @@
1
+ /*
2
+ * Copyright 2026 the original author or authors.
3
+ *
4
+ * Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
5
+ */
6
+
7
+ import {ExecutionContext, produceAsync, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {JavaScriptVisitor} from "@openrewrite/rewrite/javascript";
9
+ import {J} from "@openrewrite/rewrite/java";
10
+
11
+ const RENAMES: Record<string, string> = {
12
+ getAngularLib: 'getAngularJSGlobal',
13
+ setAngularLib: 'setAngularJSGlobal',
14
+ };
15
+
16
+ export class RenameAngularJsGlobals extends Recipe {
17
+ readonly name = "org.openrewrite.angular.migration.rename-angular-js-globals";
18
+ readonly displayName: string = "Rename `getAngularLib`/`setAngularLib` to `getAngularJSGlobal`/`setAngularJSGlobal`";
19
+ readonly description: string = "Renames the `getAngularLib` and `setAngularLib` functions from `@angular/upgrade` to " +
20
+ "`getAngularJSGlobal` and `setAngularJSGlobal`. The deprecated aliases were removed in Angular 22.";
21
+
22
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
23
+ return new class extends JavaScriptVisitor<ExecutionContext> {
24
+ override async visitIdentifier(identifier: J.Identifier, p: ExecutionContext): Promise<J | undefined> {
25
+ const id = await super.visitIdentifier(identifier, p) as J.Identifier;
26
+ if (!id) return id;
27
+
28
+ const replacement = RENAMES[id.simpleName];
29
+ if (!replacement) return id;
30
+
31
+ return produceAsync(id, async draft => {
32
+ draft.simpleName = replacement;
33
+ });
34
+ }
35
+ }();
36
+ }
37
+ }
@@ -0,0 +1,33 @@
1
+ /*
2
+ * Copyright 2026 the original author or authors.
3
+ *
4
+ * Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
5
+ */
6
+
7
+ import {ExecutionContext, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {JavaScriptVisitor} from "@openrewrite/rewrite/javascript";
9
+ import {isIdentifier, J} from "@openrewrite/rewrite/java";
10
+
11
+ export class RenameDefaultChangeDetection extends Recipe {
12
+ readonly name = "org.openrewrite.angular.migration.rename-default-change-detection";
13
+ readonly displayName: string = "Rename `ChangeDetectionStrategy.Default` to `ChangeDetectionStrategy.Eager`";
14
+ readonly description: string = "Renames the deprecated `ChangeDetectionStrategy.Default` enum member to its Angular 22 " +
15
+ "replacement `ChangeDetectionStrategy.Eager`. Both members have the same value, so the change is behavior preserving.";
16
+
17
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
18
+ return new class extends JavaScriptVisitor<ExecutionContext> {
19
+ protected async visitFieldAccess(fieldAccess: J.FieldAccess, p: ExecutionContext): Promise<J | undefined> {
20
+ const f = await super.visitFieldAccess(fieldAccess, p) as J.FieldAccess;
21
+ if (!f) return f;
22
+
23
+ if (!isIdentifier(f.target) || f.target.simpleName !== 'ChangeDetectionStrategy') return f;
24
+ if (f.name.element.simpleName !== 'Default') return f;
25
+
26
+ return {
27
+ ...f,
28
+ name: {...f.name, element: {...f.name.element, simpleName: 'Eager'}}
29
+ } as J.FieldAccess;
30
+ }
31
+ }();
32
+ }
33
+ }
@@ -0,0 +1,108 @@
1
+ /*
2
+ * Copyright 2026 the original author or authors.
3
+ *
4
+ * Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
5
+ */
6
+
7
+ import {ExecutionContext, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {getMemberKeyName, isMember, isObject, Json, JsonVisitor} from "@openrewrite/rewrite/json";
9
+
10
+ const APPLICATION_BUILDERS = new Set([
11
+ '@angular-devkit/build-angular:application',
12
+ '@angular-devkit/build-angular:build-application',
13
+ '@angular/build:application',
14
+ '@angular/build:build-application',
15
+ ]);
16
+
17
+ export class RenameSsrExperimentalPlatform extends Recipe {
18
+ readonly name = "org.openrewrite.angular.migration.rename-ssr-experimental-platform";
19
+ readonly displayName: string = "Rename `ssr.experimentalPlatform` to `ssr.platform` in `angular.json`";
20
+ readonly description: string = "Angular 22 renamed the `ssr.experimentalPlatform` builder option to `ssr.platform` on the " +
21
+ "`application` and `build-application` builders. Matches the `update-workspace-config` schematic.";
22
+
23
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
24
+ return new class extends JsonVisitor<ExecutionContext> {
25
+ protected async visitDocument(doc: Json.Document, p: ExecutionContext): Promise<Json | undefined> {
26
+ if (!doc.sourcePath.endsWith('angular.json')) return doc;
27
+
28
+ const root = doc.value;
29
+ if (!isObject(root)) return doc;
30
+
31
+ const updated = mapProjects(root);
32
+ return updated === root ? doc : {...doc, value: updated} as Json.Document;
33
+ }
34
+ }();
35
+ }
36
+ }
37
+
38
+ function mapProjects(root: Json.Object): Json.Object {
39
+ return mapObjectMember(root, 'projects', projects =>
40
+ mapObjectMembers(projects, project => mapProject(project)));
41
+ }
42
+
43
+ function mapProject(project: Json.Object): Json.Object {
44
+ if (findMember(project, 'projectType')?.value !== 'application') return project;
45
+ return mapObjectMember(project, 'architect', architect =>
46
+ mapObjectMembers(architect, target => mapTarget(target)));
47
+ }
48
+
49
+ function mapTarget(target: Json.Object): Json.Object {
50
+ const builder = findMember(target, 'builder')?.value;
51
+ if (typeof builder !== 'string' || !APPLICATION_BUILDERS.has(builder)) return target;
52
+
53
+ let updated = mapObjectMember(target, 'options', renameSsrPlatform);
54
+ updated = mapObjectMember(updated, 'configurations', configurations =>
55
+ mapObjectMembers(configurations, renameSsrPlatform));
56
+ return updated;
57
+ }
58
+
59
+ function renameSsrPlatform(options: Json.Object): Json.Object {
60
+ return mapObjectMember(options, 'ssr', ssr => renameMember(ssr, 'experimentalPlatform', 'platform'));
61
+ }
62
+
63
+ /* ---------- small object helpers ---------- */
64
+
65
+ function findMember(obj: Json.Object, key: string): {value: unknown; member: Json.Member} | undefined {
66
+ for (const rp of obj.members) {
67
+ if (!isMember(rp.element)) continue;
68
+ const m = rp.element as Json.Member;
69
+ if (getMemberKeyName(m) === key) return {value: (m.value as Json.Literal).value, member: m};
70
+ }
71
+ return undefined;
72
+ }
73
+
74
+ function mapObjectMember(obj: Json.Object, key: string, fn: (child: Json.Object) => Json.Object): Json.Object {
75
+ const members = obj.members.map(rp => {
76
+ if (!isMember(rp.element)) return rp;
77
+ const m = rp.element as Json.Member;
78
+ if (getMemberKeyName(m) !== key || !isObject(m.value)) return rp;
79
+ const updated = fn(m.value as Json.Object);
80
+ return updated === m.value ? rp : {...rp, element: {...m, value: updated} as Json.Member};
81
+ });
82
+ return members.every((rp, i) => rp === obj.members[i]) ? obj : {...obj, members};
83
+ }
84
+
85
+ function mapObjectMembers(obj: Json.Object, fn: (child: Json.Object) => Json.Object): Json.Object {
86
+ const members = obj.members.map(rp => {
87
+ if (!isMember(rp.element)) return rp;
88
+ const m = rp.element as Json.Member;
89
+ if (!isObject(m.value)) return rp;
90
+ const updated = fn(m.value as Json.Object);
91
+ return updated === m.value ? rp : {...rp, element: {...m, value: updated} as Json.Member};
92
+ });
93
+ return members.every((rp, i) => rp === obj.members[i]) ? obj : {...obj, members};
94
+ }
95
+
96
+ function renameMember(obj: Json.Object, from: string, to: string): Json.Object {
97
+ // If the target key already exists, leave the object alone rather than clobbering it.
98
+ if (findMember(obj, to)) return obj;
99
+
100
+ const members = obj.members.map(rp => {
101
+ if (!isMember(rp.element)) return rp;
102
+ const m = rp.element as Json.Member;
103
+ if (getMemberKeyName(m) !== from) return rp;
104
+ const newKey = {...m.key.element, source: `"${to}"`, value: to} as Json.Literal;
105
+ return {...rp, element: {...m, key: {...m.key, element: newKey}} as Json.Member};
106
+ });
107
+ return members.every((rp, i) => rp === obj.members[i]) ? obj : {...obj, members};
108
+ }
@@ -0,0 +1,32 @@
1
+ /*
2
+ * Copyright 2026 the original author or authors.
3
+ *
4
+ * Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
5
+ */
6
+
7
+ import {ExecutionContext, produceAsync, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {JavaScriptVisitor, maybeAddImport, maybeRemoveImport} from "@openrewrite/rewrite/javascript";
9
+ import {J} from "@openrewrite/rewrite/java";
10
+
11
+ export class ReplaceCreateNgModuleRef extends Recipe {
12
+ readonly name = "org.openrewrite.angular.migration.replace-create-ng-module-ref";
13
+ readonly displayName: string = "Replace `createNgModuleRef` with `createNgModule`";
14
+ readonly description: string = "Renames `createNgModuleRef` to `createNgModule` in imports and usages. " +
15
+ "The deprecated `createNgModuleRef` alias was removed in Angular 22.";
16
+
17
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
18
+ return new class extends JavaScriptVisitor<ExecutionContext> {
19
+ override async visitIdentifier(identifier: J.Identifier, p: ExecutionContext): Promise<J | undefined> {
20
+ const id = await super.visitIdentifier(identifier, p) as J.Identifier;
21
+ if (!id || id.simpleName !== 'createNgModuleRef') return id;
22
+
23
+ maybeAddImport(this, {module: '@angular/core', member: 'createNgModule'});
24
+ maybeRemoveImport(this, '@angular/core', 'createNgModuleRef');
25
+
26
+ return produceAsync(id, async draft => {
27
+ draft.simpleName = 'createNgModule';
28
+ });
29
+ }
30
+ }();
31
+ }
32
+ }
@@ -0,0 +1,42 @@
1
+ /*
2
+ * Copyright 2026 the original author or authors.
3
+ *
4
+ * Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
5
+ */
6
+
7
+ import {ExecutionContext, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {detectIndent, isObject, Json, JsonVisitor} from "@openrewrite/rewrite/json";
9
+ import {addMemberPath, declaresAngularCompilerOptions, isRootTsconfig, literal, memberIndent} from "./json-support";
10
+
11
+ const SUPPRESSED_CHECKS = ['nullishCoalescingNotNullable', 'optionalChainNotNullable'];
12
+
13
+ export class SuppressSafeNavigationDiagnostics extends Recipe {
14
+ readonly name = "org.openrewrite.angular.migration.suppress-safe-navigation-diagnostics";
15
+ readonly displayName: string = "Suppress the safe navigation extended diagnostics";
16
+ readonly description: string = "Sets the `nullishCoalescingNotNullable` and `optionalChainNotNullable` extended diagnostics to " +
17
+ "`suppress` in `angularCompilerOptions` in `tsconfig.json`. Angular 22 narrows types across safe navigation and " +
18
+ "nullish coalescing in templates, which makes both diagnostics fire on templates that previously compiled cleanly.";
19
+
20
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
21
+ return new class extends JsonVisitor<ExecutionContext> {
22
+ protected async visitDocument(doc: Json.Document, p: ExecutionContext): Promise<Json | undefined> {
23
+ if (!isRootTsconfig(doc.sourcePath) && !declaresAngularCompilerOptions(doc)) return doc;
24
+
25
+ const root = doc.value;
26
+ if (!isObject(root)) return doc;
27
+
28
+ const indentUnit = detectIndent(doc);
29
+ const indent = memberIndent(root, indentUnit);
30
+
31
+ let value = root;
32
+ for (const check of SUPPRESSED_CHECKS) {
33
+ value = addMemberPath(value,
34
+ ['angularCompilerOptions', 'extendedDiagnostics', 'checks', check],
35
+ literal('"suppress"', 'suppress'), indent, indentUnit);
36
+ }
37
+
38
+ return value === root ? doc : {...doc, value} as Json.Document;
39
+ }
40
+ }();
41
+ }
42
+ }
@@ -0,0 +1,92 @@
1
+ /*
2
+ * Copyright 2026 the original author or authors.
3
+ *
4
+ * Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
5
+ */
6
+
7
+ import {Recipe} from "@openrewrite/rewrite";
8
+ import {upgradeDependencyVersion} from "@openrewrite/rewrite/javascript";
9
+ import {UpgradeToAngular21CodeChanges} from "./upgrade-to-angular-21";
10
+ import {FindProvideRoutesUsage} from "../search/find-provide-routes-usage";
11
+ import {FindCheckNoChangesUsage} from "../search/find-check-no-changes-usage";
12
+ import {FindCanMatchUsage} from "../search/find-can-match-usage";
13
+ import {FindHttpXhrBackendUsage} from "../search/find-http-xhr-backend-usage";
14
+ import {FindImplicitParamsInheritance} from "../search/find-implicit-params-inheritance";
15
+ import {AddEagerChangeDetection} from "./add-eager-change-detection";
16
+ import {RenameDefaultChangeDetection} from "./rename-default-change-detection";
17
+ import {AddNoIncrementalHydration} from "./add-no-incremental-hydration";
18
+ import {AddStrictTemplatesFalse} from "./add-strict-templates-false";
19
+ import {SuppressSafeNavigationDiagnostics} from "./suppress-safe-navigation-diagnostics";
20
+ import {AddWithXhr} from "./add-with-xhr";
21
+ import {AddTrustProxyHeaders} from "./add-trust-proxy-headers";
22
+ import {AddIstanbulInstrumenter} from "./add-istanbul-instrumenter";
23
+ import {MigrateModelWithOutput} from "./migrate-model-with-output";
24
+ import {FindTemplateSafeNavigation} from "../search/find-template-safe-navigation";
25
+ import {ReplaceCreateNgModuleRef} from "./replace-create-ng-module-ref";
26
+ import {RenameAngularJsGlobals} from "./rename-angular-js-globals";
27
+ import {RenameSsrExperimentalPlatform} from "./rename-ssr-experimental-platform";
28
+
29
+ export class UpgradeToAngular22CodeChanges extends Recipe {
30
+ readonly name = "org.openrewrite.angular.UpgradeToAngular22CodeChanges";
31
+ readonly displayName = "Upgrade to Angular 22 (code changes)";
32
+ readonly description = "The source changes of the Angular 22 migration, without the dependency version bumps. " +
33
+ "Use `UpgradeToAngular22` unless you are composing a migration to a later major.";
34
+
35
+ async recipeList(): Promise<Recipe[]> {
36
+ return [
37
+ new UpgradeToAngular21CodeChanges(),
38
+ // Removed and behaviorally changed APIs that need human review
39
+ new FindProvideRoutesUsage(),
40
+ new FindCheckNoChangesUsage(),
41
+ new FindCanMatchUsage(),
42
+ new FindHttpXhrBackendUsage(),
43
+ new FindImplicitParamsInheritance(),
44
+ new FindTemplateSafeNavigation(),
45
+ // Change detection defaults
46
+ new AddEagerChangeDetection(),
47
+ new RenameDefaultChangeDetection(),
48
+ // `HttpClient` backend default. `RemoveWithFetch` deliberately does not run here: dropping
49
+ // `withFetch()` would leave a call that `AddWithXhr` then flips onto the XHR backend.
50
+ new AddWithXhr(),
51
+ // Hydration and template type checking defaults
52
+ new AddNoIncrementalHydration(),
53
+ new AddStrictTemplatesFalse(),
54
+ new SuppressSafeNavigationDiagnostics(),
55
+ // Server rendering and test tooling
56
+ new AddTrustProxyHeaders(),
57
+ new RenameSsrExperimentalPlatform(),
58
+ new AddIstanbulInstrumenter(),
59
+ // Removed and deprecated APIs
60
+ new MigrateModelWithOutput(),
61
+ new ReplaceCreateNgModuleRef(),
62
+ new RenameAngularJsGlobals(),
63
+ ];
64
+ }
65
+ }
66
+
67
+ export class UpgradeToAngular22 extends Recipe {
68
+ readonly name = "org.openrewrite.angular.UpgradeToAngular22";
69
+ readonly displayName = "Upgrade to Angular 22";
70
+ readonly description = "Migrates Angular 21.x applications to Angular 22. " +
71
+ "This includes running the Angular 21 migration first, making the pre-existing change detection, " +
72
+ "`HttpClient` backend, incremental hydration, strict template, and safe navigation diagnostic defaults " +
73
+ "explicit, replacing APIs removed in Angular 22, flagging the router behavior changes for review, and " +
74
+ "upgrading TypeScript to 6.0.x.";
75
+
76
+ async recipeList(): Promise<Recipe[]> {
77
+ return [
78
+ new UpgradeToAngular22CodeChanges(),
79
+ // Angular core packages
80
+ await upgradeDependencyVersion({packagePattern: "@angular/*", newVersion: "22.x"}),
81
+ // Angular DevKit; `architect` and `build-webpack` kept the 0.<major><minor>.x CLI scheme
82
+ await upgradeDependencyVersion({packageName: "@angular-devkit/core", newVersion: "22.x"}),
83
+ await upgradeDependencyVersion({packagePattern: "@angular-devkit/s*", newVersion: "22.x"}),
84
+ await upgradeDependencyVersion({packageName: "@angular-devkit/build-angular", newVersion: "22.x"}),
85
+ await upgradeDependencyVersion({packagePattern: "@angular-devkit/architect*", newVersion: "0.2201.x"}),
86
+ await upgradeDependencyVersion({packageName: "@angular-devkit/build-webpack", newVersion: "0.2201.x"}),
87
+ await upgradeDependencyVersion({packageName: "@schematics/angular", newVersion: "22.x"}),
88
+ // TypeScript
89
+ await upgradeDependencyVersion({packageName: "typescript", newVersion: "6.0.x"}),
90
+ ];
91
+ }
92
+ }
@@ -0,0 +1,32 @@
1
+ /*
2
+ * Copyright 2026 the original author or authors.
3
+ *
4
+ * Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
5
+ */
6
+
7
+ import {ExecutionContext, foundSearchResult, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {JavaScriptVisitor} from "@openrewrite/rewrite/javascript";
9
+ import {isIdentifier, J} from "@openrewrite/rewrite/java";
10
+
11
+ export class FindCanMatchUsage extends Recipe {
12
+ readonly name = "org.openrewrite.angular.search.find-can-match-usage";
13
+ readonly displayName: string = "Find two argument `canMatch` guard calls";
14
+ readonly description: string = "Finds calls to a `canMatch` guard that pass only a route and its segments. " +
15
+ "Angular 22 made the `currentSnapshot` parameter of `CanMatch` and `CanMatchFn` required, so those calls no longer compile. " +
16
+ "Guards that declare fewer parameters keep working and are not reported.";
17
+
18
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
19
+ return new class extends JavaScriptVisitor<ExecutionContext> {
20
+ override async visitMethodInvocation(method: J.MethodInvocation, p: ExecutionContext): Promise<J | undefined> {
21
+ const m = await super.visitMethodInvocation(method, p) as J.MethodInvocation;
22
+ if (!m || !isIdentifier(m.name) || m.name.simpleName !== 'canMatch') return m;
23
+
24
+ const args = (m.arguments?.elements ?? []).filter(rp => rp.element.kind !== J.Kind.Empty);
25
+ if (args.length !== 2) return m;
26
+
27
+ return foundSearchResult(m,
28
+ "The currentSnapshot parameter of CanMatch and CanMatchFn is required as of Angular 22. Pass the route snapshot as a third argument.");
29
+ }
30
+ };
31
+ }
32
+ }
@@ -0,0 +1,29 @@
1
+ /*
2
+ * Copyright 2026 the original author or authors.
3
+ *
4
+ * Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
5
+ */
6
+
7
+ import {ExecutionContext, foundSearchResult, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {JavaScriptVisitor} from "@openrewrite/rewrite/javascript";
9
+ import {isIdentifier, J} from "@openrewrite/rewrite/java";
10
+
11
+ export class FindCheckNoChangesUsage extends Recipe {
12
+ readonly name = "org.openrewrite.angular.search.find-check-no-changes-usage";
13
+ readonly displayName: string = "Find `ChangeDetectorRef.checkNoChanges` usage";
14
+ readonly description: string = "Finds calls to `checkNoChanges()`, which was removed from `ChangeDetectorRef` in Angular 22. " +
15
+ "In tests, use `fixture.detectChanges()` instead.";
16
+
17
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
18
+ return new class extends JavaScriptVisitor<ExecutionContext> {
19
+ override async visitMethodInvocation(method: J.MethodInvocation, p: ExecutionContext): Promise<J | undefined> {
20
+ const m = await super.visitMethodInvocation(method, p) as J.MethodInvocation;
21
+ if (!m || !m.select) return m;
22
+ if (!isIdentifier(m.name) || m.name.simpleName !== 'checkNoChanges') return m;
23
+
24
+ return foundSearchResult(m,
25
+ "ChangeDetectorRef.checkNoChanges() was removed in Angular 22. Use fixture.detectChanges() in tests instead.");
26
+ }
27
+ };
28
+ }
29
+ }