@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,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 {ExecutionContext, Recipe, TreeVisitor, emptyMarkers} from "@openrewrite/rewrite";
8
+ import {JavaScriptVisitor, JS, template} from "@openrewrite/rewrite/javascript";
9
+ import {J, isIdentifier, emptySpace, singleSpace} from "@openrewrite/rewrite/java";
10
+ import {create} from "mutative";
11
+
12
+ const QUERY_DECORATORS = ['ViewChild', 'ContentChild'];
13
+
14
+ export class AddStaticFalseToViewQueries extends Recipe {
15
+ readonly name = "org.openrewrite.angular.migration.add-static-false-to-view-queries";
16
+ readonly displayName: string = "Add `static: false` to view queries";
17
+ readonly description: string = "Adds `static: false` to `@ViewChild` and `@ContentChild` decorators that don't have the `static` property. " +
18
+ "Angular 8 requires an explicit `static` flag for view query decorators. " +
19
+ "Using `static: false` preserves the Angular 7 default behavior (queries resolved after change detection).";
20
+
21
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
22
+ return new class extends JavaScriptVisitor<ExecutionContext> {
23
+ override async visitAnnotation(annotation: J.Annotation, p: ExecutionContext): Promise<J | undefined> {
24
+ let a = await super.visitAnnotation(annotation, p) as J.Annotation;
25
+ if (!a) return a;
26
+
27
+ const annotType = a.annotationType;
28
+ if (!isIdentifier(annotType)) return a;
29
+ if (!QUERY_DECORATORS.includes(annotType.simpleName)) return a;
30
+
31
+ if (!a.arguments?.elements || a.arguments.elements.length === 0) return a;
32
+
33
+ if (a.arguments.elements.length >= 2) {
34
+ const optionsArg = a.arguments.elements[1].element;
35
+ if (optionsArg.kind !== J.Kind.NewClass) return a;
36
+ const optionsObj = optionsArg as J.NewClass;
37
+ if (!optionsObj.body) return a;
38
+
39
+ for (const stmt of optionsObj.body.statements) {
40
+ if (stmt.element.kind === JS.Kind.PropertyAssignment) {
41
+ const prop = stmt.element as JS.PropertyAssignment;
42
+ if (isIdentifier(prop.name.element) && prop.name.element.simpleName === 'static') {
43
+ return a;
44
+ }
45
+ }
46
+ }
47
+
48
+ const obj = await template`({ static: false })`.apply(a, this.cursor) as any;
49
+ const staticProp = obj.tree.element.body.statements[0].element;
50
+ const refProp = optionsObj.body.statements[0]?.element as JS.PropertyAssignment | undefined;
51
+
52
+ const wrappedProp: J.RightPadded<any> = {
53
+ kind: J.Kind.RightPadded,
54
+ element: refProp ? {...staticProp, prefix: refProp.prefix} : staticProp,
55
+ after: emptySpace,
56
+ markers: emptyMarkers,
57
+ };
58
+
59
+ return create(a, draft => {
60
+ const body = (draft.arguments!.elements[1].element as any).body!;
61
+ body.statements = [...body.statements, wrappedProp];
62
+ }) as J.Annotation;
63
+ }
64
+
65
+ const obj = await template`({ static: false })`.apply(a, this.cursor) as any;
66
+ const optionsNode = obj.tree.element;
67
+
68
+ const fixedStatements = optionsNode.body.statements.map((s: any, i: number, arr: any[]) => ({
69
+ ...s,
70
+ element: {...s.element, prefix: singleSpace},
71
+ after: i === arr.length - 1 ? singleSpace : s.after,
72
+ }));
73
+ const fixedOptionsNode = {
74
+ ...optionsNode,
75
+ prefix: singleSpace,
76
+ body: {...optionsNode.body, statements: fixedStatements},
77
+ };
78
+
79
+ const wrappedOptions: J.RightPadded<any> = {
80
+ kind: J.Kind.RightPadded,
81
+ element: fixedOptionsNode,
82
+ after: emptySpace,
83
+ markers: emptyMarkers,
84
+ };
85
+
86
+ return create(a, draft => {
87
+ draft.arguments!.elements = [...draft.arguments!.elements, wrappedOptions];
88
+ }) as J.Annotation;
89
+ }
90
+ }();
91
+ }
92
+ }
@@ -0,0 +1,41 @@
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, capture, pattern, Template} from "@openrewrite/rewrite/javascript";
9
+ import {J} from "@openrewrite/rewrite/java";
10
+
11
+ export class AddTestBedTeardown extends Recipe {
12
+ readonly name = "org.openrewrite.angular.migration.add-testbed-teardown";
13
+ readonly displayName: string = "Add TestBed module teardown";
14
+ readonly description: string = "Adds `{ teardown: { destroyAfterEach: true } }` as the third argument to " +
15
+ "`TestBed.initTestEnvironment()` calls. Angular 13 changed the default teardown behavior, and this " +
16
+ "ensures explicit opt-in for module teardown after each test.";
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
+ let m = await super.visitMethodInvocation(method, p) as J.MethodInvocation;
22
+ if (!m) return m;
23
+
24
+ const args = capture<J>({variadic: true});
25
+ if (!await pattern`TestBed.initTestEnvironment(${args})`.match(m, this.cursor)) return m;
26
+
27
+ const elements = (m as any).arguments?.elements;
28
+ if (!elements || elements.length !== 2) return m;
29
+
30
+ const t = Template.builder()
31
+ .code('TestBed.initTestEnvironment(')
32
+ .param(elements[0].element)
33
+ .code(', ')
34
+ .param(elements[1].element)
35
+ .code(', { teardown: { destroyAfterEach: true } })')
36
+ .build();
37
+ return await t.apply(m, this.cursor);
38
+ }
39
+ }();
40
+ }
41
+ }
@@ -0,0 +1,132 @@
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, randomId, ExecutionContext, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {JsonVisitor, Json, getMemberKeyName, isObject, detectIndent, rightPadded, space} from "@openrewrite/rewrite/json";
9
+
10
+ export class EnableAotBuild extends Recipe {
11
+ readonly name = "org.openrewrite.angular.migration.enable-aot-build";
12
+ readonly displayName: string = "Enable AOT compilation in `angular.json`";
13
+ readonly description: string = "Adds `\"aot\": true` to build options in `angular.json`. " +
14
+ "Angular 9 made AOT compilation the default, and projects upgrading from Angular 8 should enable it explicitly.";
15
+
16
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
17
+ return new class extends JsonVisitor<ExecutionContext> {
18
+ private baseIndent = ' ';
19
+ private inArchitect = false;
20
+
21
+ protected async visitDocument(doc: Json.Document, p: ExecutionContext): Promise<Json | undefined> {
22
+ if (!doc.sourcePath.endsWith('angular.json')) return doc;
23
+ this.baseIndent = detectIndent(doc);
24
+ return super.visitDocument(doc, p);
25
+ }
26
+
27
+ protected async visitMember(member: Json.Member, p: ExecutionContext): Promise<Json | undefined> {
28
+ const m = await super.visitMember(member, p) as Json.Member;
29
+ if (!m) return m;
30
+
31
+ const keyName = getMemberKeyName(m);
32
+
33
+ if (keyName === 'architect' && isObject(m.value)) {
34
+ this.inArchitect = true;
35
+ const result = await super.visitMember(m, p) as Json.Member;
36
+ this.inArchitect = false;
37
+ return result;
38
+ }
39
+
40
+ if (!this.inArchitect || keyName !== 'build' || !isObject(m.value)) return m;
41
+
42
+ const buildObj = m.value as Json.Object;
43
+ const optionsMember = buildObj.members.find(rp =>
44
+ getMemberKeyName(rp.element as Json.Member) === 'options'
45
+ );
46
+
47
+ if (!optionsMember) return m;
48
+ const optionsValue = (optionsMember.element as Json.Member).value;
49
+ if (!isObject(optionsValue)) return m;
50
+
51
+ const optionsObj = optionsValue as Json.Object;
52
+ const hasAot = optionsObj.members.some(rp =>
53
+ getMemberKeyName(rp.element as Json.Member) === 'aot'
54
+ );
55
+
56
+ if (hasAot) return m;
57
+
58
+ const firstMemberPrefix = optionsObj.members.length > 0
59
+ ? (optionsObj.members[0].element as Json.Member).key.element.prefix.whitespace
60
+ : '';
61
+ const prefixMatch = firstMemberPrefix.match(/\n([ \t]+)/);
62
+ const indent = prefixMatch ? prefixMatch[1] : this.baseIndent.repeat(6);
63
+
64
+ const keyLiteral: Json.Literal = {
65
+ kind: Json.Kind.Literal,
66
+ id: randomId(),
67
+ prefix: space('\n' + indent),
68
+ markers: emptyMarkers,
69
+ source: '"aot"',
70
+ value: 'aot'
71
+ };
72
+
73
+ const valueLiteral: Json.Literal = {
74
+ kind: Json.Kind.Literal,
75
+ id: randomId(),
76
+ prefix: space(' '),
77
+ markers: emptyMarkers,
78
+ source: 'true',
79
+ value: true
80
+ };
81
+
82
+ const newMember: Json.Member = {
83
+ kind: Json.Kind.Member,
84
+ id: randomId(),
85
+ prefix: space(''),
86
+ markers: emptyMarkers,
87
+ key: rightPadded(keyLiteral, space('')),
88
+ value: valueLiteral
89
+ };
90
+
91
+ const members = [...optionsObj.members];
92
+ const closingWhitespace = members.length > 0
93
+ ? members[members.length - 1].after.whitespace
94
+ : '\n' + this.baseIndent.repeat(5);
95
+
96
+ if (members.length > 0) {
97
+ members[members.length - 1] = {
98
+ ...members[members.length - 1],
99
+ after: space('')
100
+ };
101
+ }
102
+
103
+ members.push(rightPadded(newMember, space(closingWhitespace)));
104
+
105
+ const newOptionsObj: Json.Object = {
106
+ ...optionsObj,
107
+ members
108
+ };
109
+
110
+ const newOptionsMember = {
111
+ ...optionsMember,
112
+ element: {
113
+ ...(optionsMember.element as Json.Member),
114
+ value: newOptionsObj
115
+ } as Json.Member
116
+ };
117
+
118
+ const buildMembers = buildObj.members.map(rp =>
119
+ getMemberKeyName(rp.element as Json.Member) === 'options' ? newOptionsMember : rp
120
+ );
121
+
122
+ return {
123
+ ...m,
124
+ value: {
125
+ ...buildObj,
126
+ members: buildMembers
127
+ } as Json.Object
128
+ } as Json.Member;
129
+ }
130
+ }();
131
+ }
132
+ }
@@ -0,0 +1,82 @@
1
+ import {ExecutionContext, Recipe, TreeVisitor} from "@openrewrite/rewrite";
2
+ import {emptyMarkers} from "@openrewrite/rewrite";
3
+ import {JavaScriptVisitor, JS, template} from "@openrewrite/rewrite/javascript";
4
+ import {J, isIdentifier, isLiteral, emptySpace} from "@openrewrite/rewrite/java";
5
+ import {create} from "mutative";
6
+
7
+ const ANGULAR_DECORATORS = ['Component', 'Directive', 'Pipe'];
8
+
9
+ export class ExplicitStandaloneFlag extends Recipe {
10
+ readonly name = "org.openrewrite.angular.migration.explicit-standalone-flag";
11
+ readonly displayName: string = "Make standalone flag explicit";
12
+ readonly description: string = "Adds `standalone: false` to non-standalone Angular components, directives, and pipes, and removes redundant `standalone: true` since it became the default in Angular 19.";
13
+
14
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
15
+ return new class extends JavaScriptVisitor<ExecutionContext> {
16
+ override async visitAnnotation(annotation: J.Annotation, p: ExecutionContext): Promise<J | undefined> {
17
+ let a = await super.visitAnnotation(annotation, p) as J.Annotation;
18
+ if (!a) return a;
19
+
20
+ const annotType = a.annotationType;
21
+ if (!isIdentifier(annotType)) return a;
22
+ if (!ANGULAR_DECORATORS.includes(annotType.simpleName)) return a;
23
+
24
+ if (!a.arguments?.elements?.length) return a;
25
+ const firstArg = a.arguments.elements[0].element;
26
+ if (firstArg.kind !== J.Kind.NewClass) return a;
27
+
28
+ const newClass = firstArg as J.NewClass;
29
+ if (!newClass.body) return a;
30
+
31
+ let standaloneIndex = -1;
32
+ let standaloneValue: boolean | null = null;
33
+
34
+ for (let i = 0; i < newClass.body.statements.length; i++) {
35
+ const stmt = newClass.body.statements[i].element;
36
+ if (stmt.kind === JS.Kind.PropertyAssignment) {
37
+ const prop = stmt as JS.PropertyAssignment;
38
+ const nameExpr = prop.name.element;
39
+ if (isIdentifier(nameExpr) && nameExpr.simpleName === 'standalone') {
40
+ standaloneIndex = i;
41
+ if (prop.initializer && isLiteral(prop.initializer)) {
42
+ standaloneValue = prop.initializer.value as boolean;
43
+ }
44
+ break;
45
+ }
46
+ }
47
+ }
48
+
49
+ if (standaloneValue === true) {
50
+ return create(a, draft => {
51
+ const body = (draft.arguments!.elements[0].element as any).body!;
52
+ body.statements = body.statements.filter((_: any, i: number) => i !== standaloneIndex);
53
+ }) as J.Annotation;
54
+ }
55
+
56
+ if (standaloneIndex === -1) {
57
+ const existingStmts = newClass.body.statements;
58
+ if (existingStmts.length === 0) return a;
59
+
60
+ const refProp = existingStmts[0].element as JS.PropertyAssignment;
61
+
62
+ const obj = await template`({ standalone: false })`.apply(a, this.cursor) as any;
63
+ const standaloneProp = {...obj.tree.element.body.statements[0].element, prefix: refProp.prefix};
64
+
65
+ const wrappedProp: J.RightPadded<any> = {
66
+ kind: J.Kind.RightPadded,
67
+ element: standaloneProp,
68
+ after: emptySpace,
69
+ markers: emptyMarkers,
70
+ };
71
+
72
+ return create(a, draft => {
73
+ const body = (draft.arguments!.elements[0].element as any).body!;
74
+ body.statements = [wrappedProp, ...body.statements];
75
+ }) as J.Annotation;
76
+ }
77
+
78
+ return a;
79
+ }
80
+ }();
81
+ }
82
+ }
@@ -0,0 +1,172 @@
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, randomId, emptyMarkers, markers as mkMarkers, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {JavaScriptVisitor, maybeAddImport} from "@openrewrite/rewrite/javascript";
9
+ import {J, isIdentifier, emptySpace, singleSpace} from "@openrewrite/rewrite/java";
10
+ import {create} from "mutative";
11
+
12
+ const ANGULAR_DECORATORS = ['Component', 'Directive', 'Pipe', 'Injectable'];
13
+ const ACCESS_MODIFIER_TYPES = new Set(['Private', 'Protected', 'Public']);
14
+
15
+ export class MigrateConstructorToInject extends Recipe {
16
+ readonly name = "org.openrewrite.angular.migration.migrate-constructor-to-inject";
17
+ readonly displayName: string = "Migrate constructor injection to `inject()`";
18
+ readonly description: string = "Converts constructor parameter properties in Angular classes to field declarations " +
19
+ "using the `inject()` function. For example, `constructor(private svc: MyService) {}` becomes " +
20
+ "`private svc = inject(MyService);`.";
21
+
22
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
23
+ return new class extends JavaScriptVisitor<ExecutionContext> {
24
+ protected async visitClassDeclaration(classDecl: J.ClassDeclaration, p: ExecutionContext): Promise<J | undefined> {
25
+ let c = await super.visitClassDeclaration(classDecl, p) as J.ClassDeclaration;
26
+ if (!c) return c;
27
+
28
+ if (!c.leadingAnnotations.some(a => isIdentifier(a.annotationType)
29
+ && ANGULAR_DECORATORS.includes((a.annotationType as J.Identifier).simpleName))) {
30
+ return c;
31
+ }
32
+
33
+ let constructorIdx = -1;
34
+ let constructorDecl: J.MethodDeclaration | null = null;
35
+ for (let i = 0; i < c.body.statements.length; i++) {
36
+ const stmt = c.body.statements[i].element;
37
+ if (stmt.kind === J.Kind.MethodDeclaration) {
38
+ const method = stmt as J.MethodDeclaration;
39
+ if (method.name.simpleName === 'constructor') {
40
+ constructorIdx = i;
41
+ constructorDecl = method;
42
+ break;
43
+ }
44
+ }
45
+ }
46
+
47
+ if (!constructorDecl?.parameters) return c;
48
+
49
+ const params = constructorDecl.parameters.elements;
50
+ const promoted: J.VariableDeclarations[] = [];
51
+ const remaining: any[] = [];
52
+
53
+ for (const rp of params) {
54
+ const param = rp.element;
55
+ if (param.kind === J.Kind.VariableDeclarations) {
56
+ const varDecls = param as J.VariableDeclarations;
57
+ const hasAccessMod = varDecls.modifiers.some(
58
+ m => ACCESS_MODIFIER_TYPES.has(m.type as string));
59
+ if (hasAccessMod && varDecls.typeExpression) {
60
+ promoted.push(varDecls);
61
+ continue;
62
+ }
63
+ }
64
+ remaining.push(rp);
65
+ }
66
+
67
+ if (promoted.length === 0) return c;
68
+
69
+ maybeAddImport(this, {module: '@angular/core', member: 'inject'});
70
+
71
+ const ctorPrefix = c.body.statements[constructorIdx].element.prefix;
72
+ const fields = promoted.map(param => ({
73
+ kind: J.Kind.RightPadded,
74
+ element: buildField(param, ctorPrefix),
75
+ after: emptySpace,
76
+ markers: mkMarkers({kind: 'org.openrewrite.java.marker.Semicolon' as const, id: randomId()}),
77
+ }));
78
+
79
+ const stmts = [...c.body.statements];
80
+ stmts.splice(constructorIdx, 0, ...fields);
81
+
82
+ const newCtorIdx = constructorIdx + fields.length;
83
+ const bodyEmpty = !constructorDecl.body
84
+ || constructorDecl.body.statements.length === 0;
85
+
86
+ if (remaining.length === 0 && bodyEmpty) {
87
+ stmts.splice(newCtorIdx, 1);
88
+ } else {
89
+ const updatedParams = remaining.length > 0 ? remaining : [{
90
+ kind: J.Kind.RightPadded,
91
+ element: {kind: J.Kind.Empty, id: randomId(), prefix: emptySpace, markers: emptyMarkers},
92
+ after: emptySpace,
93
+ markers: emptyMarkers,
94
+ }];
95
+ const updatedCtor = create(constructorDecl, (draft: any) => {
96
+ draft.parameters.elements = updatedParams;
97
+ });
98
+ stmts[newCtorIdx] = {
99
+ ...stmts[newCtorIdx],
100
+ element: updatedCtor,
101
+ };
102
+ }
103
+
104
+ return create(c, (draft: any) => {
105
+ draft.body.statements = stmts;
106
+ }) as J.ClassDeclaration;
107
+ }
108
+ };
109
+ }
110
+ }
111
+
112
+ function unwrapTypeIdentifier(typeExpr: any): any {
113
+ if (typeExpr?.typeIdentifier) return unwrapTypeIdentifier(typeExpr.typeIdentifier);
114
+ return typeExpr;
115
+ }
116
+
117
+ function buildField(param: J.VariableDeclarations, prefix: any): any {
118
+ const rawType = unwrapTypeIdentifier(param.typeExpression);
119
+ const typeArg: any = {
120
+ ...rawType,
121
+ id: randomId(),
122
+ prefix: emptySpace,
123
+ };
124
+ const injectCall: any = {
125
+ kind: J.Kind.MethodInvocation,
126
+ id: randomId(),
127
+ prefix: singleSpace,
128
+ markers: emptyMarkers,
129
+ select: undefined,
130
+ typeParameters: undefined,
131
+ name: {
132
+ kind: J.Kind.Identifier,
133
+ id: randomId(),
134
+ prefix: emptySpace,
135
+ markers: emptyMarkers,
136
+ simpleName: 'inject',
137
+ annotations: [],
138
+ type: undefined,
139
+ },
140
+ arguments: {
141
+ kind: 'org.openrewrite.java.tree.JContainer',
142
+ before: emptySpace,
143
+ markers: emptyMarkers,
144
+ elements: [{
145
+ kind: J.Kind.RightPadded,
146
+ element: typeArg,
147
+ after: emptySpace,
148
+ markers: emptyMarkers,
149
+ }],
150
+ },
151
+ methodType: undefined,
152
+ };
153
+
154
+ const namedVar = param.variables[0].element;
155
+ return {
156
+ ...param,
157
+ id: randomId(),
158
+ prefix,
159
+ typeExpression: undefined,
160
+ variables: [{
161
+ ...param.variables[0],
162
+ element: {
163
+ ...namedVar,
164
+ initializer: {
165
+ before: singleSpace,
166
+ element: injectCall,
167
+ markers: emptyMarkers,
168
+ },
169
+ },
170
+ }],
171
+ };
172
+ }