@openrewrite/recipes-angular 0.0.0

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 (106) hide show
  1. package/package.json +37 -0
  2. package/src/index.ts +321 -0
  3. package/src/migration/add-default-configuration.ts +121 -0
  4. package/src/migration/add-localize-polyfill.ts +51 -0
  5. package/src/migration/add-module-with-providers-generic.ts +102 -0
  6. package/src/migration/add-static-false-to-view-queries.ts +92 -0
  7. package/src/migration/add-testbed-teardown.ts +41 -0
  8. package/src/migration/enable-aot-build.ts +132 -0
  9. package/src/migration/explicit-standalone-flag.ts +82 -0
  10. package/src/migration/migrate-constructor-to-inject.ts +172 -0
  11. package/src/migration/migrate-input-to-signal.ts +320 -0
  12. package/src/migration/migrate-output-to-signal.ts +268 -0
  13. package/src/migration/migrate-query-to-signal.ts +276 -0
  14. package/src/migration/migrate-to-solution-style-tsconfig.ts +139 -0
  15. package/src/migration/move-document-to-core.ts +40 -0
  16. package/src/migration/remove-aot-summaries.ts +72 -0
  17. package/src/migration/remove-browser-module-with-server-transition.ts +185 -0
  18. package/src/migration/remove-component-factory-resolver.ts +48 -0
  19. package/src/migration/remove-default-project.ts +52 -0
  20. package/src/migration/remove-empty-ng-on-init.ts +80 -0
  21. package/src/migration/remove-enable-ivy.ts +63 -0
  22. package/src/migration/remove-entry-components.ts +75 -0
  23. package/src/migration/remove-es5-browser-support.ts +59 -0
  24. package/src/migration/remove-extract-css.ts +60 -0
  25. package/src/migration/remove-ie-polyfills.ts +118 -0
  26. package/src/migration/remove-module-id.ts +59 -0
  27. package/src/migration/remove-relative-link-resolution.ts +64 -0
  28. package/src/migration/remove-standalone-true.ts +50 -0
  29. package/src/migration/remove-static-false.ts +71 -0
  30. package/src/migration/remove-zone-js-polyfill.ts +55 -0
  31. package/src/migration/rename-after-render.ts +32 -0
  32. package/src/migration/rename-check-no-changes.ts +29 -0
  33. package/src/migration/rename-file.ts +72 -0
  34. package/src/migration/rename-pending-tasks.ts +30 -0
  35. package/src/migration/rename-zoneless-provider.ts +29 -0
  36. package/src/migration/replace-async-with-wait-for-async.ts +32 -0
  37. package/src/migration/replace-deep-zone-js-imports.ts +118 -0
  38. package/src/migration/replace-http-client-module.ts +276 -0
  39. package/src/migration/replace-initial-navigation.ts +73 -0
  40. package/src/migration/replace-inject-flags.ts +83 -0
  41. package/src/migration/replace-load-children-string.ts +48 -0
  42. package/src/migration/replace-node-sass-with-sass.ts +22 -0
  43. package/src/migration/replace-router-link-with-href.ts +37 -0
  44. package/src/migration/replace-testbed-get-with-inject.ts +33 -0
  45. package/src/migration/replace-untyped-forms.ts +59 -0
  46. package/src/migration/replace-validator-with-validators.ts +41 -0
  47. package/src/migration/replace-view-encapsulation-native.ts +51 -0
  48. package/src/migration/update-component-template-url.ts +186 -0
  49. package/src/migration/update-tsconfig-module.ts +75 -0
  50. package/src/migration/update-tsconfig-target.ts +61 -0
  51. package/src/migration/upgrade-to-angular-10.ts +52 -0
  52. package/src/migration/upgrade-to-angular-11.ts +52 -0
  53. package/src/migration/upgrade-to-angular-12.ts +43 -0
  54. package/src/migration/upgrade-to-angular-13.ts +45 -0
  55. package/src/migration/upgrade-to-angular-14.ts +44 -0
  56. package/src/migration/upgrade-to-angular-15.ts +43 -0
  57. package/src/migration/upgrade-to-angular-16.ts +57 -0
  58. package/src/migration/upgrade-to-angular-17.ts +43 -0
  59. package/src/migration/upgrade-to-angular-18.ts +69 -0
  60. package/src/migration/upgrade-to-angular-19.ts +52 -0
  61. package/src/migration/upgrade-to-angular-20.ts +47 -0
  62. package/src/migration/upgrade-to-angular-21.ts +53 -0
  63. package/src/migration/upgrade-to-angular-8.ts +54 -0
  64. package/src/migration/upgrade-to-angular-9.ts +69 -0
  65. package/src/search/find-analyze-for-entry-components-usage.ts +46 -0
  66. package/src/search/find-angular-decorator.ts +58 -0
  67. package/src/search/find-angular-http-usage.ts +35 -0
  68. package/src/search/find-animation-driver-matches-element.ts +38 -0
  69. package/src/search/find-async-test-helper-usage.ts +45 -0
  70. package/src/search/find-bare-module-with-providers.ts +47 -0
  71. package/src/search/find-browser-transfer-state-module-usage.ts +45 -0
  72. package/src/search/find-common-module-usage.ts +47 -0
  73. package/src/search/find-compiler-factory-usage.ts +51 -0
  74. package/src/search/find-date-pipe-default-timezone-usage.ts +46 -0
  75. package/src/search/find-effect-timing-usage.ts +28 -0
  76. package/src/search/find-empty-projectable-nodes.ts +68 -0
  77. package/src/search/find-fake-async-usage.ts +37 -0
  78. package/src/search/find-hammer-js-usage.ts +48 -0
  79. package/src/search/find-i18n-usage.ts +94 -0
  80. package/src/search/find-karma-usage.ts +47 -0
  81. package/src/search/find-load-children-string-usage.ts +43 -0
  82. package/src/search/find-missing-injectable.ts +75 -0
  83. package/src/search/find-ng-class-usage.ts +45 -0
  84. package/src/search/find-ng-style-usage.ts +45 -0
  85. package/src/search/find-path-match-type-usage.ts +44 -0
  86. package/src/search/find-platform-dynamic-server-usage.ts +38 -0
  87. package/src/search/find-platform-webworker-usage.ts +34 -0
  88. package/src/search/find-platform-worker-usage.ts +39 -0
  89. package/src/search/find-preserve-fragment-usage.ts +32 -0
  90. package/src/search/find-preserve-query-params-usage.ts +32 -0
  91. package/src/search/find-provided-in-deprecated-usage.ts +65 -0
  92. package/src/search/find-reflective-injector-usage.ts +45 -0
  93. package/src/search/find-render-application-usage.ts +47 -0
  94. package/src/search/find-render-component-type-usage.ts +46 -0
  95. package/src/search/find-render-module-factory-usage.ts +45 -0
  96. package/src/search/find-renderer-usage.ts +46 -0
  97. package/src/search/find-resource-cache-provider-usage.ts +38 -0
  98. package/src/search/find-root-renderer-usage.ts +47 -0
  99. package/src/search/find-rxjs-compat-usage.ts +40 -0
  100. package/src/search/find-server-transfer-state-module-usage.ts +38 -0
  101. package/src/search/find-setup-testing-router-usage.ts +45 -0
  102. package/src/search/find-testability-pending-request-usage.ts +38 -0
  103. package/src/search/find-undecorated-angular-class.ts +78 -0
  104. package/src/search/find-with-no-dom-reuse-usage.ts +46 -0
  105. package/src/search/find-wrapped-value-usage.ts +46 -0
  106. package/src/search/find-zone-js-usage.ts +43 -0
@@ -0,0 +1,118 @@
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, JS} from "@openrewrite/rewrite/javascript";
9
+ import {J} from "@openrewrite/rewrite/java";
10
+ import {create} from "mutative";
11
+ import {JsonVisitor, Json, getMemberKeyName, isLiteral, isArray} from "@openrewrite/rewrite/json";
12
+
13
+ function resolveDeepZoneImport(path: string): string | undefined {
14
+ if (!path.startsWith('zone.js/dist/') && !path.startsWith('zone.js/bundles/')) {
15
+ return undefined;
16
+ }
17
+ return path.includes('testing') ? 'zone.js/testing' : 'zone.js';
18
+ }
19
+
20
+ class ReplaceDeepZoneJsTypeScriptImports extends Recipe {
21
+ readonly name = "org.openrewrite.angular.migration.replace-deep-zone-js-imports.typescript";
22
+ readonly displayName = "Replace deep `zone.js` TypeScript imports";
23
+ readonly description = "Replaces legacy deep TypeScript imports from `zone.js`.";
24
+
25
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
26
+ return new class extends JavaScriptVisitor<ExecutionContext> {
27
+ protected async visitImportDeclaration(jsImport: JS.Import, p: ExecutionContext): Promise<J | undefined> {
28
+ const imp = await super.visitImportDeclaration(jsImport, p) as JS.Import;
29
+ if (!imp.moduleSpecifier) return imp;
30
+
31
+ const moduleSpec = imp.moduleSpecifier.element;
32
+ if (moduleSpec.kind !== J.Kind.Literal) return imp;
33
+
34
+ const literal = moduleSpec as J.Literal;
35
+ const newModule = resolveDeepZoneImport(literal.value as string);
36
+ if (!newModule) return imp;
37
+
38
+ const quote = literal.valueSource?.charAt(0) ?? "'";
39
+ return create(imp, draft => {
40
+ const specDraft: any = draft.moduleSpecifier!.element;
41
+ specDraft.value = newModule;
42
+ specDraft.valueSource = `${quote}${newModule}${quote}`;
43
+ }) as JS.Import;
44
+ }
45
+ };
46
+ }
47
+ }
48
+
49
+ class ReplaceDeepZoneJsAngularJsonPolyfills extends Recipe {
50
+ readonly name = "org.openrewrite.angular.migration.replace-deep-zone-js-imports.angular-json";
51
+ readonly displayName = "Replace deep `zone.js` polyfills in `angular.json`";
52
+ readonly description = "Replaces legacy deep `zone.js` entries in the `polyfills` array of `angular.json`.";
53
+
54
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
55
+ return new class extends JsonVisitor<ExecutionContext> {
56
+ protected async visitDocument(doc: Json.Document, p: ExecutionContext): Promise<Json | undefined> {
57
+ if (!doc.sourcePath.endsWith('angular.json')) return doc;
58
+ return super.visitDocument(doc, p);
59
+ }
60
+
61
+ protected async visitMember(member: Json.Member, p: ExecutionContext): Promise<Json | undefined> {
62
+ const m = await super.visitMember(member, p) as Json.Member;
63
+ if (!m) return m;
64
+
65
+ if (getMemberKeyName(m) !== 'polyfills') return m;
66
+ if (!isArray(m.value)) return m;
67
+
68
+ let changed = false;
69
+ const newValues = m.value.values.map(rp => {
70
+ const elem = rp.element;
71
+ if (!isLiteral(elem)) return rp;
72
+ const val = elem.value as string;
73
+ if (typeof val !== 'string') return rp;
74
+
75
+ const newVal = resolveDeepZoneImport(val);
76
+ if (!newVal) return rp;
77
+
78
+ changed = true;
79
+ const src = elem.source as string;
80
+ const quote = src?.charAt(0) ?? '"';
81
+ return {
82
+ ...rp,
83
+ element: {
84
+ ...elem,
85
+ value: newVal,
86
+ source: `${quote}${newVal}${quote}`,
87
+ } as Json.Literal,
88
+ };
89
+ });
90
+
91
+ if (!changed) return m;
92
+
93
+ return {
94
+ ...m,
95
+ value: {
96
+ ...m.value,
97
+ values: newValues,
98
+ } as Json.Array,
99
+ } as Json.Member;
100
+ }
101
+ }();
102
+ }
103
+ }
104
+
105
+ export class ReplaceDeepZoneJsImports extends Recipe {
106
+ readonly name = "org.openrewrite.angular.migration.replace-deep-zone-js-imports";
107
+ readonly displayName: string = "Replace deep `zone.js` imports";
108
+ readonly description: string = "Replaces legacy deep imports from `zone.js` such as `zone.js/dist/zone` or `zone.js/bundles/zone-testing.js` " +
109
+ "with the standard `zone.js` or `zone.js/testing` imports, in both TypeScript files and `angular.json` polyfills. " +
110
+ "Deep imports are no longer allowed in Angular 17.";
111
+
112
+ async recipeList(): Promise<Recipe[]> {
113
+ return [
114
+ new ReplaceDeepZoneJsTypeScriptImports(),
115
+ new ReplaceDeepZoneJsAngularJsonPolyfills(),
116
+ ];
117
+ }
118
+ }
@@ -0,0 +1,276 @@
1
+ import {ExecutionContext, Recipe, TreeVisitor} from "@openrewrite/rewrite";
2
+ import {randomId, emptyMarkers} from "@openrewrite/rewrite";
3
+ import {JavaScriptVisitor, JS, maybeAddImport, maybeRemoveImport, template, Template, capture, pattern} from "@openrewrite/rewrite/javascript";
4
+ import {J, isIdentifier, emptySpace, singleSpace} from "@openrewrite/rewrite/java";
5
+
6
+ const HTTP_MODULES = new Set([
7
+ 'HttpClientModule',
8
+ 'HttpClientJsonpModule',
9
+ 'HttpClientXsrfModule',
10
+ ]);
11
+
12
+ const TESTING_MODULES = new Set([
13
+ 'HttpClientTestingModule',
14
+ ]);
15
+
16
+ interface HttpModuleInfo {
17
+ index: number;
18
+ name: string;
19
+ xsrfOptions?: any;
20
+ }
21
+
22
+ export class ReplaceHttpClientModule extends Recipe {
23
+ readonly name = "org.openrewrite.angular.migration.replace-http-client-module";
24
+ readonly displayName: string = "Replace `HttpClientModule` with `provideHttpClient()`";
25
+ readonly description: string = "Replaces deprecated `HttpClientModule`, `HttpClientJsonpModule`, `HttpClientXsrfModule`, and `HttpClientTestingModule` with their functional equivalents: `provideHttpClient()` with feature functions and `provideHttpClientTesting()`.";
26
+
27
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
28
+ return new class extends JavaScriptVisitor<ExecutionContext> {
29
+ override async visitAnnotation(annotation: J.Annotation, p: ExecutionContext): Promise<J | undefined> {
30
+ let a = await super.visitAnnotation(annotation, p) as J.Annotation;
31
+ if (!a) return a;
32
+
33
+ const annotType = a.annotationType;
34
+ if (!isIdentifier(annotType)) return a;
35
+ if (annotType.simpleName !== 'NgModule') return a;
36
+
37
+ if (!a.arguments?.elements?.length) return a;
38
+ const firstArg = a.arguments.elements[0].element;
39
+ if (firstArg.kind !== J.Kind.NewClass) return a;
40
+
41
+ const newClass = firstArg as J.NewClass;
42
+ if (!newClass.body) return a;
43
+
44
+ let importsIndex = -1;
45
+ let providersIndex = -1;
46
+
47
+ for (let i = 0; i < newClass.body.statements.length; i++) {
48
+ const stmt = newClass.body.statements[i].element;
49
+ if (stmt.kind !== JS.Kind.PropertyAssignment) continue;
50
+
51
+ const prop = stmt as JS.PropertyAssignment;
52
+ const nameExpr = prop.name.element;
53
+ if (!isIdentifier(nameExpr)) continue;
54
+
55
+ if (nameExpr.simpleName === 'imports') {
56
+ importsIndex = i;
57
+ } else if (nameExpr.simpleName === 'providers') {
58
+ providersIndex = i;
59
+ }
60
+ }
61
+
62
+ if (importsIndex === -1) return a;
63
+
64
+ const importsStmt = newClass.body.statements[importsIndex].element as any;
65
+ const importsArray = importsStmt.initializer;
66
+ if (!importsArray?.initializer?.elements) return a;
67
+
68
+ const elements = importsArray.initializer.elements;
69
+ const httpModules: HttpModuleInfo[] = [];
70
+ const testingModules: HttpModuleInfo[] = [];
71
+
72
+ for (let i = 0; i < elements.length; i++) {
73
+ const elem = elements[i].element;
74
+ if (elem.kind === J.Kind.Identifier) {
75
+ const name = (elem as J.Identifier).simpleName;
76
+ if (HTTP_MODULES.has(name)) {
77
+ httpModules.push({index: i, name});
78
+ } else if (TESTING_MODULES.has(name)) {
79
+ testingModules.push({index: i, name});
80
+ }
81
+ } else if (elem.kind === J.Kind.MethodInvocation) {
82
+ const xsrfOpts = capture<J>('xsrfOpts');
83
+ const match = await pattern`HttpClientXsrfModule.withOptions(${xsrfOpts})`.match(elem, this.cursor);
84
+ if (match) {
85
+ httpModules.push({
86
+ index: i,
87
+ name: 'HttpClientXsrfModule',
88
+ xsrfOptions: match.get(xsrfOpts),
89
+ });
90
+ }
91
+ }
92
+ }
93
+
94
+ if (httpModules.length === 0 && testingModules.length === 0) return a;
95
+
96
+ const indicesToRemove = new Set([
97
+ ...httpModules.map(m => m.index),
98
+ ...testingModules.map(m => m.index),
99
+ ]);
100
+
101
+ for (const mod of httpModules) {
102
+ maybeRemoveImport(this, '@angular/common/http', mod.name);
103
+ }
104
+ for (const mod of testingModules) {
105
+ maybeRemoveImport(this, '@angular/common/http/testing', mod.name);
106
+ }
107
+
108
+ const features: string[] = [];
109
+ let hasXsrfOptions = false;
110
+ let xsrfOptionsNode: any = null;
111
+
112
+ if (httpModules.length > 0) {
113
+ maybeAddImport(this, {module: '@angular/common/http', member: 'provideHttpClient', onlyIfReferenced: false});
114
+
115
+ for (const mod of httpModules) {
116
+ if (mod.name === 'HttpClientJsonpModule') {
117
+ features.push('withJsonpSupport');
118
+ maybeAddImport(this, {module: '@angular/common/http', member: 'withJsonpSupport', onlyIfReferenced: false});
119
+ } else if (mod.name === 'HttpClientXsrfModule') {
120
+ if (mod.xsrfOptions) {
121
+ features.push('withXsrfConfiguration');
122
+ hasXsrfOptions = true;
123
+ xsrfOptionsNode = mod.xsrfOptions;
124
+ }
125
+ maybeAddImport(this, {module: '@angular/common/http', member: 'withXsrfConfiguration', onlyIfReferenced: false});
126
+ }
127
+ }
128
+ }
129
+
130
+ if (testingModules.length > 0) {
131
+ maybeAddImport(this, {module: '@angular/common/http/testing', member: 'provideHttpClientTesting', onlyIfReferenced: false});
132
+ }
133
+
134
+ // Build new provider nodes using templates
135
+ const newProviders: J.RightPadded<any>[] = [];
136
+
137
+ if (httpModules.length > 0) {
138
+ const builder = Template.builder().code('provideHttpClient(');
139
+ for (let i = 0; i < features.length; i++) {
140
+ if (i > 0) builder.code(', ');
141
+ if (features[i] === 'withXsrfConfiguration' && hasXsrfOptions && xsrfOptionsNode) {
142
+ builder.code('withXsrfConfiguration(').param(xsrfOptionsNode).code(')');
143
+ } else {
144
+ builder.code(`${features[i]}()`);
145
+ }
146
+ }
147
+ builder.code(')');
148
+ const provideCall = await builder.build().apply(a, this.cursor);
149
+ newProviders.push({
150
+ kind: J.Kind.RightPadded,
151
+ element: {...provideCall, prefix: singleSpace},
152
+ after: emptySpace,
153
+ markers: emptyMarkers,
154
+ });
155
+ }
156
+
157
+ if (testingModules.length > 0) {
158
+ const testingCall = await template`provideHttpClientTesting()`.apply(a, this.cursor);
159
+ newProviders.push({
160
+ kind: J.Kind.RightPadded,
161
+ element: {...testingCall, prefix: singleSpace},
162
+ after: emptySpace,
163
+ markers: emptyMarkers,
164
+ });
165
+ }
166
+
167
+ const existingStmts = newClass.body.statements;
168
+ const newStatements = [...existingStmts];
169
+
170
+ // 1. Remove HTTP modules from imports array
171
+ const origContainer = importsArray.initializer;
172
+ const filteredElements = origContainer.elements.filter((_: any, i: number) => !indicesToRemove.has(i));
173
+
174
+ const newImportsStmt = {
175
+ ...importsStmt,
176
+ initializer: {
177
+ ...importsArray,
178
+ initializer: {
179
+ ...origContainer,
180
+ elements: filteredElements,
181
+ },
182
+ },
183
+ };
184
+ newStatements[importsIndex] = {
185
+ ...newStatements[importsIndex],
186
+ element: newImportsStmt,
187
+ };
188
+
189
+ // 2. Add providers
190
+ if (providersIndex !== -1) {
191
+ const providersStmt = newStatements[providersIndex].element as any;
192
+ const provArray = providersStmt.initializer;
193
+ const provContainer = provArray.initializer;
194
+ const hasRealElements = provContainer.elements?.some(
195
+ (e: any) => e.element && (e.element.kind === J.Kind.Identifier
196
+ || e.element.kind === J.Kind.MethodInvocation
197
+ || e.element.kind === J.Kind.NewClass
198
+ || e.element.kind === JS.Kind.PropertyAssignment));
199
+ const existingElements = hasRealElements ? provContainer.elements : [];
200
+ const newProvStmt = {
201
+ ...providersStmt,
202
+ initializer: {
203
+ ...provArray,
204
+ initializer: {
205
+ ...provContainer,
206
+ elements: [...existingElements, ...newProviders],
207
+ },
208
+ },
209
+ };
210
+ newStatements[providersIndex] = {
211
+ ...newStatements[providersIndex],
212
+ element: newProvStmt,
213
+ };
214
+ } else {
215
+ const refProp = existingStmts[0].element as JS.PropertyAssignment;
216
+ newStatements.push({
217
+ kind: J.Kind.RightPadded,
218
+ element: buildProvidersProperty(newProviders, refProp.prefix),
219
+ after: emptySpace,
220
+ markers: emptyMarkers,
221
+ } as J.RightPadded<any>);
222
+ }
223
+
224
+ const newBody = {...newClass.body, statements: newStatements};
225
+ const newNewClass = {...newClass, body: newBody};
226
+ return {
227
+ ...a,
228
+ arguments: {
229
+ ...a.arguments!,
230
+ elements: [{
231
+ ...a.arguments!.elements[0],
232
+ element: newNewClass,
233
+ }],
234
+ },
235
+ } as J.Annotation;
236
+ }
237
+ }();
238
+ }
239
+ }
240
+
241
+ function buildProvidersProperty(providerElements: J.RightPadded<any>[], propPrefix: any): JS.PropertyAssignment {
242
+ return {
243
+ kind: JS.Kind.PropertyAssignment,
244
+ id: randomId(),
245
+ markers: emptyMarkers,
246
+ prefix: propPrefix,
247
+ name: {
248
+ kind: J.Kind.RightPadded,
249
+ element: {
250
+ kind: J.Kind.Identifier,
251
+ id: randomId(),
252
+ markers: emptyMarkers,
253
+ prefix: emptySpace,
254
+ annotations: [],
255
+ simpleName: 'providers',
256
+ } as J.Identifier,
257
+ after: emptySpace,
258
+ markers: emptyMarkers,
259
+ },
260
+ assigmentToken: "Colon" as JS.PropertyAssignment.Token,
261
+ initializer: {
262
+ kind: J.Kind.NewArray,
263
+ id: randomId(),
264
+ markers: emptyMarkers,
265
+ prefix: singleSpace,
266
+ typeExpression: null,
267
+ dimensions: [],
268
+ initializer: {
269
+ kind: 'org.openrewrite.java.tree.JContainer',
270
+ before: emptySpace,
271
+ markers: emptyMarkers,
272
+ elements: providerElements,
273
+ },
274
+ } as any,
275
+ };
276
+ }
@@ -0,0 +1,73 @@
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, JS} from "@openrewrite/rewrite/javascript";
9
+ import {J, isIdentifier, isLiteral} from "@openrewrite/rewrite/java";
10
+ import {create} from "mutative";
11
+
12
+ export class ReplaceInitialNavigation extends Recipe {
13
+ readonly name = "org.openrewrite.angular.migration.replace-initial-navigation";
14
+ readonly displayName: string = "Replace `initialNavigation` option values";
15
+ readonly description: string = "Replaces deprecated `initialNavigation` router option values: " +
16
+ "`'legacy_enabled'` and `true` become `'enabledBlocking'`, `'legacy_disabled'` and `false` become `'disabled'`, " +
17
+ "and `'enabled'` becomes `'enabledNonBlocking'`. " +
18
+ "The legacy values were removed in Angular 11; `'enabled'` was renamed in Angular 14.";
19
+
20
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
21
+ return new class extends JavaScriptVisitor<ExecutionContext> {
22
+ override async visitPropertyAssignment(prop: JS.PropertyAssignment, p: ExecutionContext): Promise<J | undefined> {
23
+ let pa = await super.visitPropertyAssignment(prop, p) as JS.PropertyAssignment;
24
+ if (!pa) return pa;
25
+
26
+ const nameExpr = pa.name.element;
27
+ if (!isIdentifier(nameExpr) || nameExpr.simpleName !== 'initialNavigation') return pa;
28
+
29
+ if (!pa.initializer) return pa;
30
+
31
+ if (isLiteral(pa.initializer)) {
32
+ const lit = pa.initializer as J.Literal;
33
+ const replacements: Record<string, string> = {
34
+ 'legacy_enabled': 'enabledBlocking',
35
+ 'legacy_disabled': 'disabled',
36
+ 'enabled': 'enabledNonBlocking',
37
+ };
38
+
39
+ if (typeof lit.value === 'string' && lit.value in replacements) {
40
+ const newValue = replacements[lit.value];
41
+ return create(pa, draft => {
42
+ (draft as any).initializer = {
43
+ ...lit,
44
+ value: newValue,
45
+ valueSource: `'${newValue}'`,
46
+ };
47
+ }) as JS.PropertyAssignment;
48
+ }
49
+ if (lit.value === true) {
50
+ return create(pa, draft => {
51
+ (draft as any).initializer = {
52
+ ...lit,
53
+ value: 'enabledBlocking',
54
+ valueSource: `'enabledBlocking'`,
55
+ };
56
+ }) as JS.PropertyAssignment;
57
+ }
58
+ if (lit.value === false) {
59
+ return create(pa, draft => {
60
+ (draft as any).initializer = {
61
+ ...lit,
62
+ value: 'disabled',
63
+ valueSource: `'disabled'`,
64
+ };
65
+ }) as JS.PropertyAssignment;
66
+ }
67
+ }
68
+
69
+ return pa;
70
+ }
71
+ }();
72
+ }
73
+ }
@@ -0,0 +1,83 @@
1
+ import {ExecutionContext, Recipe, TreeVisitor} from "@openrewrite/rewrite";
2
+ import {JavaScriptVisitor, maybeRemoveImport, template, Template} from "@openrewrite/rewrite/javascript";
3
+ import {J, Type} from "@openrewrite/rewrite/java";
4
+
5
+ const FLAG_TO_OPTION: Record<string, string> = {
6
+ 'Optional': 'optional',
7
+ 'Host': 'host',
8
+ 'Self': 'self',
9
+ 'SkipSelf': 'skipSelf',
10
+ };
11
+
12
+ function collectInjectFlags(expr: J): string[] | undefined {
13
+ if (expr.kind === J.Kind.FieldAccess) {
14
+ const fieldAccess = expr as J.FieldAccess;
15
+ if (fieldAccess.target.kind !== J.Kind.Identifier) return undefined;
16
+ if ((fieldAccess.target as J.Identifier).simpleName !== 'InjectFlags') return undefined;
17
+
18
+ const flagName = fieldAccess.name.element.simpleName;
19
+ if (flagName === 'Default') return [];
20
+ const option = FLAG_TO_OPTION[flagName];
21
+ return option ? [option] : undefined;
22
+ }
23
+
24
+ if (expr.kind === J.Kind.Binary) {
25
+ const binary = expr as J.Binary;
26
+ if (binary.operator.element !== J.Binary.Type.BitOr) return undefined;
27
+
28
+ const left = collectInjectFlags(binary.left);
29
+ if (!left) return undefined;
30
+ const right = collectInjectFlags(binary.right);
31
+ if (!right) return undefined;
32
+
33
+ return [...left, ...right];
34
+ }
35
+
36
+ return undefined;
37
+ }
38
+
39
+ function buildOptionsTemplate(injectName: J.Identifier, firstArg: J, options: string[]): Template {
40
+ if (options.length === 0) {
41
+ return template`${injectName}(${firstArg})`;
42
+ }
43
+ const objectLiteral = '{' + options.map(o => `${o}: true`).join(', ') + '}';
44
+ return Template.builder()
45
+ .param(injectName)
46
+ .code('(')
47
+ .param(firstArg)
48
+ .code(`, ${objectLiteral})`)
49
+ .build();
50
+ }
51
+
52
+ export class ReplaceInjectFlags extends Recipe {
53
+ readonly name = "org.openrewrite.angular.migration.replace-inject-flags";
54
+ readonly displayName: string = "Replace `InjectFlags` with options object";
55
+ readonly description: string = "Replaces deprecated `InjectFlags` enum usage in `inject()` calls with the corresponding options object. For example, `inject(MyService, InjectFlags.Optional)` becomes `inject(MyService, { optional: true })`.";
56
+
57
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
58
+ return new class extends JavaScriptVisitor<ExecutionContext> {
59
+ protected async visitMethodInvocation(method: J.MethodInvocation, p: ExecutionContext): Promise<J | undefined> {
60
+ const m = await super.visitMethodInvocation(method, p) as J.MethodInvocation;
61
+
62
+ if (m.name.simpleName !== 'inject') return m;
63
+
64
+ const methodType = m.methodType;
65
+ if (methodType?.declaringType?.kind !== Type.Kind.Class
66
+ || (methodType.declaringType as Type.Class).fullyQualifiedName !== '@angular/core') {
67
+ return m;
68
+ }
69
+
70
+ const args = m.arguments?.elements;
71
+ if (!args || args.length !== 2) return m;
72
+
73
+ const options = collectInjectFlags(args[1].element);
74
+ if (!options) return m;
75
+
76
+ maybeRemoveImport(this, '@angular/core', 'InjectFlags');
77
+
78
+ const t = buildOptionsTemplate(m.name, args[0].element, options);
79
+ return await t.apply(m, this.cursor);
80
+ }
81
+ };
82
+ }
83
+ }
@@ -0,0 +1,48 @@
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, JS, Template} from "@openrewrite/rewrite/javascript";
9
+ import {J, isIdentifier, isLiteral} from "@openrewrite/rewrite/java";
10
+ import {create} from "mutative";
11
+
12
+ export class ReplaceLoadChildrenString extends Recipe {
13
+ readonly name = "org.openrewrite.angular.migration.replace-load-children-string";
14
+ readonly displayName = "Replace string-based `loadChildren` with dynamic `import()`";
15
+ readonly description = "Converts the deprecated string-based `loadChildren: 'path#Module'` syntax " +
16
+ "to dynamic imports: `loadChildren: () => import('path').then(m => m.Module)`.";
17
+
18
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
19
+ return new class extends JavaScriptVisitor<ExecutionContext> {
20
+ override async visitPropertyAssignment(prop: JS.PropertyAssignment, p: ExecutionContext): Promise<J | undefined> {
21
+ let pa = await super.visitPropertyAssignment(prop, p) as JS.PropertyAssignment;
22
+ if (!pa) return pa;
23
+
24
+ const nameExpr = pa.name.element;
25
+ if (!isIdentifier(nameExpr) || nameExpr.simpleName !== 'loadChildren') return pa;
26
+ if (!pa.initializer) return pa;
27
+
28
+ if (!isLiteral(pa.initializer)) return pa;
29
+ const lit = pa.initializer as J.Literal;
30
+ if (typeof lit.value !== 'string' || !lit.value.includes('#')) return pa;
31
+
32
+ const hashIndex = lit.value.indexOf('#');
33
+ const path = lit.value.substring(0, hashIndex);
34
+ const moduleName = lit.value.substring(hashIndex + 1);
35
+ if (!path || !moduleName) return pa;
36
+
37
+ const arrowFn = await Template.builder()
38
+ .code(`() => import('${path}').then(m => m.${moduleName})`)
39
+ .build()
40
+ .apply(lit, this.cursor);
41
+
42
+ return create(pa, draft => {
43
+ (draft as any).initializer = arrowFn;
44
+ }) as JS.PropertyAssignment;
45
+ }
46
+ }();
47
+ }
48
+ }
@@ -0,0 +1,22 @@
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 {AddDependency, RemoveDependency} from "@openrewrite/rewrite/javascript";
9
+
10
+ export class ReplaceNodeSassWithSass extends Recipe {
11
+ readonly name = "org.openrewrite.angular.migration.replace-node-sass-with-sass";
12
+ readonly displayName: string = "Replace `node-sass` with `sass`";
13
+ readonly description: string = "Replaces the deprecated `node-sass` package with `sass` (Dart Sass). " +
14
+ "Angular 12 requires Dart Sass; `node-sass` is no longer supported.";
15
+
16
+ async recipeList(): Promise<Recipe[]> {
17
+ return [
18
+ new RemoveDependency({packageName: "node-sass"}),
19
+ new AddDependency({packageName: "sass", version: "^1.35.0", scope: "devDependencies"}),
20
+ ];
21
+ }
22
+ }
@@ -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, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {JavaScriptVisitor, maybeAddImport, maybeRemoveImport} from "@openrewrite/rewrite/javascript";
9
+ import {J} from "@openrewrite/rewrite/java";
10
+ import {create} from "mutative";
11
+
12
+ export class ReplaceRouterLinkWithHref extends Recipe {
13
+ readonly name = "org.openrewrite.angular.migration.replace-router-link-with-href";
14
+ readonly displayName: string = "Replace `RouterLinkWithHref` with `RouterLink`";
15
+ readonly description: string = "Replaces `RouterLinkWithHref` with `RouterLink` in imports and usages. " +
16
+ "`RouterLinkWithHref` was merged into `RouterLink` in Angular 16.";
17
+
18
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
19
+ return new class extends JavaScriptVisitor<ExecutionContext> {
20
+ override async visitIdentifier(identifier: J.Identifier, p: ExecutionContext): Promise<J | undefined> {
21
+ let id = await super.visitIdentifier(identifier, p) as J.Identifier;
22
+ if (!id) return id;
23
+
24
+ if (id.simpleName === 'RouterLinkWithHref') {
25
+ maybeAddImport(this, {module: '@angular/router', member: 'RouterLink'});
26
+ maybeRemoveImport(this, '@angular/router', 'RouterLinkWithHref');
27
+
28
+ return create(id, draft => {
29
+ draft.simpleName = 'RouterLink';
30
+ }) as J.Identifier;
31
+ }
32
+
33
+ return id;
34
+ }
35
+ }();
36
+ }
37
+ }