@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.
- package/package.json +37 -0
- package/src/index.ts +321 -0
- package/src/migration/add-default-configuration.ts +121 -0
- package/src/migration/add-localize-polyfill.ts +51 -0
- package/src/migration/add-module-with-providers-generic.ts +102 -0
- package/src/migration/add-static-false-to-view-queries.ts +92 -0
- package/src/migration/add-testbed-teardown.ts +41 -0
- package/src/migration/enable-aot-build.ts +132 -0
- package/src/migration/explicit-standalone-flag.ts +82 -0
- package/src/migration/migrate-constructor-to-inject.ts +172 -0
- package/src/migration/migrate-input-to-signal.ts +320 -0
- package/src/migration/migrate-output-to-signal.ts +268 -0
- package/src/migration/migrate-query-to-signal.ts +276 -0
- package/src/migration/migrate-to-solution-style-tsconfig.ts +139 -0
- package/src/migration/move-document-to-core.ts +40 -0
- package/src/migration/remove-aot-summaries.ts +72 -0
- package/src/migration/remove-browser-module-with-server-transition.ts +185 -0
- package/src/migration/remove-component-factory-resolver.ts +48 -0
- package/src/migration/remove-default-project.ts +52 -0
- package/src/migration/remove-empty-ng-on-init.ts +80 -0
- package/src/migration/remove-enable-ivy.ts +63 -0
- package/src/migration/remove-entry-components.ts +75 -0
- package/src/migration/remove-es5-browser-support.ts +59 -0
- package/src/migration/remove-extract-css.ts +60 -0
- package/src/migration/remove-ie-polyfills.ts +118 -0
- package/src/migration/remove-module-id.ts +59 -0
- package/src/migration/remove-relative-link-resolution.ts +64 -0
- package/src/migration/remove-standalone-true.ts +50 -0
- package/src/migration/remove-static-false.ts +71 -0
- package/src/migration/remove-zone-js-polyfill.ts +55 -0
- package/src/migration/rename-after-render.ts +32 -0
- package/src/migration/rename-check-no-changes.ts +29 -0
- package/src/migration/rename-file.ts +72 -0
- package/src/migration/rename-pending-tasks.ts +30 -0
- package/src/migration/rename-zoneless-provider.ts +29 -0
- package/src/migration/replace-async-with-wait-for-async.ts +32 -0
- package/src/migration/replace-deep-zone-js-imports.ts +118 -0
- package/src/migration/replace-http-client-module.ts +276 -0
- package/src/migration/replace-initial-navigation.ts +73 -0
- package/src/migration/replace-inject-flags.ts +83 -0
- package/src/migration/replace-load-children-string.ts +48 -0
- package/src/migration/replace-node-sass-with-sass.ts +22 -0
- package/src/migration/replace-router-link-with-href.ts +37 -0
- package/src/migration/replace-testbed-get-with-inject.ts +33 -0
- package/src/migration/replace-untyped-forms.ts +59 -0
- package/src/migration/replace-validator-with-validators.ts +41 -0
- package/src/migration/replace-view-encapsulation-native.ts +51 -0
- package/src/migration/update-component-template-url.ts +186 -0
- package/src/migration/update-tsconfig-module.ts +75 -0
- package/src/migration/update-tsconfig-target.ts +61 -0
- package/src/migration/upgrade-to-angular-10.ts +52 -0
- package/src/migration/upgrade-to-angular-11.ts +52 -0
- package/src/migration/upgrade-to-angular-12.ts +43 -0
- package/src/migration/upgrade-to-angular-13.ts +45 -0
- package/src/migration/upgrade-to-angular-14.ts +44 -0
- package/src/migration/upgrade-to-angular-15.ts +43 -0
- package/src/migration/upgrade-to-angular-16.ts +57 -0
- package/src/migration/upgrade-to-angular-17.ts +43 -0
- package/src/migration/upgrade-to-angular-18.ts +69 -0
- package/src/migration/upgrade-to-angular-19.ts +52 -0
- package/src/migration/upgrade-to-angular-20.ts +47 -0
- package/src/migration/upgrade-to-angular-21.ts +53 -0
- package/src/migration/upgrade-to-angular-8.ts +54 -0
- package/src/migration/upgrade-to-angular-9.ts +69 -0
- package/src/search/find-analyze-for-entry-components-usage.ts +46 -0
- package/src/search/find-angular-decorator.ts +58 -0
- package/src/search/find-angular-http-usage.ts +35 -0
- package/src/search/find-animation-driver-matches-element.ts +38 -0
- package/src/search/find-async-test-helper-usage.ts +45 -0
- package/src/search/find-bare-module-with-providers.ts +47 -0
- package/src/search/find-browser-transfer-state-module-usage.ts +45 -0
- package/src/search/find-common-module-usage.ts +47 -0
- package/src/search/find-compiler-factory-usage.ts +51 -0
- package/src/search/find-date-pipe-default-timezone-usage.ts +46 -0
- package/src/search/find-effect-timing-usage.ts +28 -0
- package/src/search/find-empty-projectable-nodes.ts +68 -0
- package/src/search/find-fake-async-usage.ts +37 -0
- package/src/search/find-hammer-js-usage.ts +48 -0
- package/src/search/find-i18n-usage.ts +94 -0
- package/src/search/find-karma-usage.ts +47 -0
- package/src/search/find-load-children-string-usage.ts +43 -0
- package/src/search/find-missing-injectable.ts +75 -0
- package/src/search/find-ng-class-usage.ts +45 -0
- package/src/search/find-ng-style-usage.ts +45 -0
- package/src/search/find-path-match-type-usage.ts +44 -0
- package/src/search/find-platform-dynamic-server-usage.ts +38 -0
- package/src/search/find-platform-webworker-usage.ts +34 -0
- package/src/search/find-platform-worker-usage.ts +39 -0
- package/src/search/find-preserve-fragment-usage.ts +32 -0
- package/src/search/find-preserve-query-params-usage.ts +32 -0
- package/src/search/find-provided-in-deprecated-usage.ts +65 -0
- package/src/search/find-reflective-injector-usage.ts +45 -0
- package/src/search/find-render-application-usage.ts +47 -0
- package/src/search/find-render-component-type-usage.ts +46 -0
- package/src/search/find-render-module-factory-usage.ts +45 -0
- package/src/search/find-renderer-usage.ts +46 -0
- package/src/search/find-resource-cache-provider-usage.ts +38 -0
- package/src/search/find-root-renderer-usage.ts +47 -0
- package/src/search/find-rxjs-compat-usage.ts +40 -0
- package/src/search/find-server-transfer-state-module-usage.ts +38 -0
- package/src/search/find-setup-testing-router-usage.ts +45 -0
- package/src/search/find-testability-pending-request-usage.ts +38 -0
- package/src/search/find-undecorated-angular-class.ts +78 -0
- package/src/search/find-with-no-dom-reuse-usage.ts +46 -0
- package/src/search/find-wrapped-value-usage.ts +46 -0
- 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
|
+
}
|