@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,185 @@
|
|
|
1
|
+
import {ExecutionContext, Recipe, TreeVisitor} from "@openrewrite/rewrite";
|
|
2
|
+
import {randomId, emptyMarkers} from "@openrewrite/rewrite";
|
|
3
|
+
import {JavaScriptVisitor, JS, maybeAddImport, template, Template, capture, pattern} from "@openrewrite/rewrite/javascript";
|
|
4
|
+
import {J, isIdentifier, isLiteral, emptySpace, singleSpace} from "@openrewrite/rewrite/java";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export class RemoveBrowserModuleWithServerTransition extends Recipe {
|
|
8
|
+
readonly name = "org.openrewrite.angular.migration.remove-browser-module-with-server-transition";
|
|
9
|
+
readonly displayName: string = "Remove `BrowserModule.withServerTransition`";
|
|
10
|
+
readonly description: string = "Replaces `BrowserModule.withServerTransition({ appId: '...' })` with `BrowserModule` and adds `{ provide: APP_ID, useValue: '...' }` to the NgModule providers. The `withServerTransition` method was removed in Angular 19.";
|
|
11
|
+
|
|
12
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
13
|
+
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
14
|
+
private appIdValue: string | null = null;
|
|
15
|
+
|
|
16
|
+
protected async visitMethodInvocation(method: J.MethodInvocation, p: ExecutionContext): Promise<J | undefined> {
|
|
17
|
+
const m = await super.visitMethodInvocation(method, p) as J.MethodInvocation;
|
|
18
|
+
|
|
19
|
+
if (!await pattern`BrowserModule.withServerTransition(${capture({variadic: true})})`.match(m, this.cursor)) return m;
|
|
20
|
+
|
|
21
|
+
const selectElem = (m as any).select?.element || (m as any).select;
|
|
22
|
+
|
|
23
|
+
this.appIdValue = extractAppId(m);
|
|
24
|
+
if (!this.appIdValue) return m;
|
|
25
|
+
|
|
26
|
+
return {...selectElem, prefix: m.prefix};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
override async visitAnnotation(annotation: J.Annotation, p: ExecutionContext): Promise<J | undefined> {
|
|
30
|
+
this.appIdValue = null;
|
|
31
|
+
let a = await super.visitAnnotation(annotation, p) as J.Annotation;
|
|
32
|
+
if (!a) return a;
|
|
33
|
+
|
|
34
|
+
if (!this.appIdValue) return a;
|
|
35
|
+
|
|
36
|
+
const annotType = a.annotationType;
|
|
37
|
+
if (!isIdentifier(annotType)) return a;
|
|
38
|
+
if (annotType.simpleName !== 'NgModule') return a;
|
|
39
|
+
|
|
40
|
+
if (!a.arguments?.elements?.length) return a;
|
|
41
|
+
const firstArg = a.arguments.elements[0].element;
|
|
42
|
+
if (firstArg.kind !== J.Kind.NewClass) return a;
|
|
43
|
+
|
|
44
|
+
const newClass = firstArg as J.NewClass;
|
|
45
|
+
if (!newClass.body) return a;
|
|
46
|
+
|
|
47
|
+
const appIdValue = this.appIdValue!;
|
|
48
|
+
maybeAddImport(this, {module: '@angular/core', member: 'APP_ID', onlyIfReferenced: false});
|
|
49
|
+
|
|
50
|
+
let providersIndex = -1;
|
|
51
|
+
for (let i = 0; i < newClass.body.statements.length; i++) {
|
|
52
|
+
const stmt = newClass.body.statements[i].element;
|
|
53
|
+
if (stmt.kind !== JS.Kind.PropertyAssignment) continue;
|
|
54
|
+
const prop = stmt as JS.PropertyAssignment;
|
|
55
|
+
const nameExpr = prop.name.element;
|
|
56
|
+
if (isIdentifier(nameExpr) && nameExpr.simpleName === 'providers') {
|
|
57
|
+
providersIndex = i;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const providerArray = await Template.builder()
|
|
62
|
+
.code(`[{ provide: APP_ID, useValue: '${appIdValue}' }]`)
|
|
63
|
+
.build()
|
|
64
|
+
.apply(a, this.cursor) as any;
|
|
65
|
+
const providerRp = providerArray.initializer.elements[0];
|
|
66
|
+
|
|
67
|
+
const existingStmts = newClass.body.statements;
|
|
68
|
+
const newStatements = [...existingStmts];
|
|
69
|
+
|
|
70
|
+
if (providersIndex !== -1) {
|
|
71
|
+
const providersStmt = newStatements[providersIndex].element as any;
|
|
72
|
+
const provArray = providersStmt.initializer;
|
|
73
|
+
const provContainer = provArray.initializer;
|
|
74
|
+
const spacedProviderRp = addObjectSpacing(providerRp);
|
|
75
|
+
newStatements[providersIndex] = {
|
|
76
|
+
...newStatements[providersIndex],
|
|
77
|
+
element: {
|
|
78
|
+
...providersStmt,
|
|
79
|
+
initializer: {
|
|
80
|
+
...provArray,
|
|
81
|
+
initializer: {
|
|
82
|
+
...provContainer,
|
|
83
|
+
elements: [...provContainer.elements, spacedProviderRp],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
} else {
|
|
89
|
+
const refProp = existingStmts[0].element as JS.PropertyAssignment;
|
|
90
|
+
const spacedProvider = addObjectSpacing(providerRp);
|
|
91
|
+
const newArray = {...providerArray, prefix: singleSpace, initializer: {
|
|
92
|
+
...providerArray.initializer,
|
|
93
|
+
elements: [spacedProvider],
|
|
94
|
+
}};
|
|
95
|
+
newStatements.push({
|
|
96
|
+
kind: J.Kind.RightPadded,
|
|
97
|
+
element: buildProvidersProperty(newArray, refProp.prefix),
|
|
98
|
+
after: emptySpace,
|
|
99
|
+
markers: emptyMarkers,
|
|
100
|
+
} as J.RightPadded<any>);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const newBody = {...newClass.body, statements: newStatements};
|
|
104
|
+
const newNewClass = {...newClass, body: newBody};
|
|
105
|
+
return {
|
|
106
|
+
...a,
|
|
107
|
+
arguments: {
|
|
108
|
+
...a.arguments!,
|
|
109
|
+
elements: [{
|
|
110
|
+
...a.arguments!.elements[0],
|
|
111
|
+
element: newNewClass,
|
|
112
|
+
}],
|
|
113
|
+
},
|
|
114
|
+
} as J.Annotation;
|
|
115
|
+
}
|
|
116
|
+
}();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function extractAppId(methodInv: any): string | null {
|
|
121
|
+
const args = methodInv.arguments?.elements;
|
|
122
|
+
if (!args?.length) return null;
|
|
123
|
+
|
|
124
|
+
const firstArg = args[0].element;
|
|
125
|
+
if (firstArg.kind !== J.Kind.NewClass) return null;
|
|
126
|
+
|
|
127
|
+
const argObj = firstArg as J.NewClass;
|
|
128
|
+
if (!argObj.body) return null;
|
|
129
|
+
|
|
130
|
+
for (const stmt of argObj.body.statements) {
|
|
131
|
+
if (stmt.element.kind === JS.Kind.PropertyAssignment) {
|
|
132
|
+
const prop = stmt.element as JS.PropertyAssignment;
|
|
133
|
+
const nameExpr = prop.name.element;
|
|
134
|
+
if (isIdentifier(nameExpr) && nameExpr.simpleName === 'appId' &&
|
|
135
|
+
prop.initializer && isLiteral(prop.initializer)) {
|
|
136
|
+
return prop.initializer.value as string;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function addObjectSpacing(rp: any): any {
|
|
144
|
+
const obj = rp.element;
|
|
145
|
+
if (!obj.body) return rp;
|
|
146
|
+
return {
|
|
147
|
+
...rp,
|
|
148
|
+
element: {
|
|
149
|
+
...obj,
|
|
150
|
+
prefix: singleSpace,
|
|
151
|
+
body: {
|
|
152
|
+
...obj.body,
|
|
153
|
+
end: singleSpace,
|
|
154
|
+
statements: obj.body.statements.map((s: any) => ({
|
|
155
|
+
...s,
|
|
156
|
+
element: {...s.element, prefix: singleSpace},
|
|
157
|
+
})),
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function buildProvidersProperty(providerArray: any, propPrefix: any): JS.PropertyAssignment {
|
|
164
|
+
return {
|
|
165
|
+
kind: JS.Kind.PropertyAssignment,
|
|
166
|
+
id: randomId(),
|
|
167
|
+
markers: emptyMarkers,
|
|
168
|
+
prefix: propPrefix,
|
|
169
|
+
name: {
|
|
170
|
+
kind: J.Kind.RightPadded,
|
|
171
|
+
element: {
|
|
172
|
+
kind: J.Kind.Identifier,
|
|
173
|
+
id: randomId(),
|
|
174
|
+
markers: emptyMarkers,
|
|
175
|
+
prefix: emptySpace,
|
|
176
|
+
annotations: [],
|
|
177
|
+
simpleName: 'providers',
|
|
178
|
+
} as J.Identifier,
|
|
179
|
+
after: emptySpace,
|
|
180
|
+
markers: emptyMarkers,
|
|
181
|
+
},
|
|
182
|
+
assigmentToken: "Colon" as JS.PropertyAssignment.Token,
|
|
183
|
+
initializer: providerArray,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -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, maybeRemoveImport} from "@openrewrite/rewrite/javascript";
|
|
9
|
+
import {J, isIdentifier} from "@openrewrite/rewrite/java";
|
|
10
|
+
import {create} from "mutative";
|
|
11
|
+
|
|
12
|
+
export class RemoveComponentFactoryResolver extends Recipe {
|
|
13
|
+
readonly name = "org.openrewrite.angular.migration.remove-component-factory-resolver";
|
|
14
|
+
readonly displayName: string = "Remove `ComponentFactoryResolver`";
|
|
15
|
+
readonly description: string = "Replaces `resolver.resolveComponentFactory(Component)` with just `Component` " +
|
|
16
|
+
"and removes the `ComponentFactoryResolver` import. Since Ivy, `ViewContainerRef.createComponent` " +
|
|
17
|
+
"accepts the component class directly. `ComponentFactoryResolver` was deprecated in Angular 13 and removed in Angular 16.";
|
|
18
|
+
|
|
19
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
20
|
+
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
21
|
+
protected async visitMethodInvocation(method: J.MethodInvocation, p: ExecutionContext): Promise<J | undefined> {
|
|
22
|
+
const m = await super.visitMethodInvocation(method, p) as J.MethodInvocation;
|
|
23
|
+
|
|
24
|
+
if (m.name.simpleName !== 'resolveComponentFactory') return m;
|
|
25
|
+
if (!m.arguments?.elements?.length) return m;
|
|
26
|
+
|
|
27
|
+
const componentArg = m.arguments.elements[0].element;
|
|
28
|
+
|
|
29
|
+
maybeRemoveImport(this, '@angular/core', 'ComponentFactoryResolver');
|
|
30
|
+
|
|
31
|
+
return create(componentArg, draft => {
|
|
32
|
+
draft.prefix = m.prefix;
|
|
33
|
+
}) as J;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
override async visitIdentifier(identifier: J.Identifier, p: ExecutionContext): Promise<J | undefined> {
|
|
37
|
+
let id = await super.visitIdentifier(identifier, p) as J.Identifier;
|
|
38
|
+
if (!id) return id;
|
|
39
|
+
|
|
40
|
+
if (id.simpleName === 'ComponentFactoryResolver') {
|
|
41
|
+
maybeRemoveImport(this, '@angular/core', 'ComponentFactoryResolver');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return id;
|
|
45
|
+
}
|
|
46
|
+
}();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
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 {JsonVisitor, Json, getMemberKeyName, isObject} from "@openrewrite/rewrite/json";
|
|
9
|
+
|
|
10
|
+
export class RemoveDefaultProject extends Recipe {
|
|
11
|
+
readonly name = "org.openrewrite.angular.migration.remove-default-project";
|
|
12
|
+
readonly displayName: string = "Remove `defaultProject` from `angular.json`";
|
|
13
|
+
readonly description: string = "Removes the deprecated `defaultProject` property from `angular.json`. " +
|
|
14
|
+
"The `defaultProject` option was deprecated in Angular 13 and the CLI infers the default project from the workspace.";
|
|
15
|
+
|
|
16
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
17
|
+
return new class extends JsonVisitor<ExecutionContext> {
|
|
18
|
+
protected async visitDocument(doc: Json.Document, p: ExecutionContext): Promise<Json | undefined> {
|
|
19
|
+
if (!doc.sourcePath.endsWith('angular.json')) return doc;
|
|
20
|
+
|
|
21
|
+
if (!isObject(doc.value)) return doc;
|
|
22
|
+
const obj = doc.value as Json.Object;
|
|
23
|
+
|
|
24
|
+
const filtered = obj.members.filter(rp => {
|
|
25
|
+
return getMemberKeyName(rp.element as Json.Member) !== 'defaultProject';
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (filtered.length === obj.members.length) return doc;
|
|
29
|
+
|
|
30
|
+
const values = [...filtered];
|
|
31
|
+
if (values.length > 0 && obj.members.length > 0) {
|
|
32
|
+
const originalFirstPrefix = obj.members[0].element.prefix;
|
|
33
|
+
const newFirst = values[0];
|
|
34
|
+
if (newFirst.element.prefix !== originalFirstPrefix) {
|
|
35
|
+
values[0] = {
|
|
36
|
+
...newFirst,
|
|
37
|
+
element: {...newFirst.element, prefix: originalFirstPrefix}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
...doc,
|
|
44
|
+
value: {
|
|
45
|
+
...obj,
|
|
46
|
+
members: values
|
|
47
|
+
} as Json.Object
|
|
48
|
+
} as Json.Document;
|
|
49
|
+
}
|
|
50
|
+
}();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {ExecutionContext, Recipe, TreeVisitor} from "@openrewrite/rewrite";
|
|
2
|
+
import {JavaScriptVisitor, maybeRemoveImport} from "@openrewrite/rewrite/javascript";
|
|
3
|
+
import {J, isIdentifier, isMethodDeclaration} from "@openrewrite/rewrite/java";
|
|
4
|
+
import {create} from "mutative";
|
|
5
|
+
|
|
6
|
+
export class RemoveEmptyNgOnInit extends Recipe {
|
|
7
|
+
readonly name = "org.openrewrite.angular.migration.remove-empty-ng-on-init";
|
|
8
|
+
readonly displayName: string = "Remove empty `ngOnInit` lifecycle hooks";
|
|
9
|
+
readonly description: string = "Removes empty `ngOnInit` lifecycle hook methods and OnInit interface from Angular components.";
|
|
10
|
+
|
|
11
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
12
|
+
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
13
|
+
override async visitClassDeclaration(classDecl: J.ClassDeclaration, p: ExecutionContext): Promise<J | undefined> {
|
|
14
|
+
let c = await super.visitClassDeclaration(classDecl, p) as J.ClassDeclaration;
|
|
15
|
+
|
|
16
|
+
if (!c) {
|
|
17
|
+
return c;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Check if class has an empty ngOnInit method
|
|
21
|
+
const hasEmptyNgOnInit = c.body.statements.some(stmtPadded => {
|
|
22
|
+
const stmt = stmtPadded.element;
|
|
23
|
+
if (!isMethodDeclaration(stmt)) return false;
|
|
24
|
+
if (!isIdentifier(stmt.name) || stmt.name.simpleName !== 'ngOnInit') return false;
|
|
25
|
+
return stmt.body && stmt.body.statements.length === 0;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!hasEmptyNgOnInit) {
|
|
29
|
+
return c;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Remove the empty ngOnInit method
|
|
33
|
+
c = create(c, draft => {
|
|
34
|
+
draft.body.statements = draft.body.statements.filter(stmtPadded => {
|
|
35
|
+
const stmt = stmtPadded.element;
|
|
36
|
+
if (!isMethodDeclaration(stmt)) return false;
|
|
37
|
+
if (!isIdentifier(stmt.name) || stmt.name.simpleName !== 'ngOnInit') return true;
|
|
38
|
+
// Keep non-empty ngOnInit
|
|
39
|
+
return !(stmt.body && stmt.body.statements.length === 0);
|
|
40
|
+
});
|
|
41
|
+
}) as J.ClassDeclaration;
|
|
42
|
+
|
|
43
|
+
// Check if class implements OnInit
|
|
44
|
+
if (c.implements) {
|
|
45
|
+
const implementsOnInit = c.implements.elements.some(typeTreePadded => {
|
|
46
|
+
const typeTree = typeTreePadded.element;
|
|
47
|
+
if (isIdentifier(typeTree)) {
|
|
48
|
+
return typeTree.simpleName === 'OnInit';
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (implementsOnInit) {
|
|
54
|
+
// Remove OnInit from implements clause
|
|
55
|
+
c = create(c, draft => {
|
|
56
|
+
if (draft.implements) {
|
|
57
|
+
draft.implements.elements = draft.implements.elements.filter(typeTreePadded => {
|
|
58
|
+
const typeTree = typeTreePadded.element;
|
|
59
|
+
if (isIdentifier(typeTree)) {
|
|
60
|
+
return typeTree.simpleName !== 'OnInit';
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
});
|
|
64
|
+
// If no implements left, set to undefined
|
|
65
|
+
if (draft.implements.elements.length === 0) {
|
|
66
|
+
draft.implements = undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}) as J.ClassDeclaration;
|
|
70
|
+
|
|
71
|
+
// Remove OnInit import if no longer used
|
|
72
|
+
maybeRemoveImport(this, "@angular/core", "OnInit");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return c;
|
|
77
|
+
}
|
|
78
|
+
}();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
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 {JsonVisitor, Json, getMemberKeyName, isObject} from "@openrewrite/rewrite/json";
|
|
9
|
+
|
|
10
|
+
export class RemoveEnableIvy extends Recipe {
|
|
11
|
+
readonly name = "org.openrewrite.angular.migration.remove-enable-ivy";
|
|
12
|
+
readonly displayName: string = "Remove `enableIvy` compiler option";
|
|
13
|
+
readonly description: string = "Removes the `enableIvy` option from `angularCompilerOptions` in `tsconfig.json`. " +
|
|
14
|
+
"Ivy is the only rendering engine since Angular 12, and the option was removed in Angular 15.";
|
|
15
|
+
|
|
16
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
17
|
+
return new class extends JsonVisitor<ExecutionContext> {
|
|
18
|
+
protected async visitDocument(doc: Json.Document, p: ExecutionContext): Promise<Json | undefined> {
|
|
19
|
+
if (!doc.sourcePath.endsWith('tsconfig.json') &&
|
|
20
|
+
!doc.sourcePath.endsWith('tsconfig.app.json') &&
|
|
21
|
+
!doc.sourcePath.endsWith('tsconfig.lib.json')) {
|
|
22
|
+
return doc;
|
|
23
|
+
}
|
|
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
|
+
if (getMemberKeyName(m) !== 'angularCompilerOptions') return m;
|
|
32
|
+
if (!isObject(m.value)) return m;
|
|
33
|
+
|
|
34
|
+
const obj = m.value as Json.Object;
|
|
35
|
+
const filtered = obj.members.filter(rp => {
|
|
36
|
+
return getMemberKeyName(rp.element as Json.Member) !== 'enableIvy';
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (filtered.length === obj.members.length) return m;
|
|
40
|
+
|
|
41
|
+
const values = [...filtered];
|
|
42
|
+
if (values.length > 0 && obj.members.length > 0) {
|
|
43
|
+
const originalFirstPrefix = obj.members[0].element.prefix;
|
|
44
|
+
const newFirst = values[0];
|
|
45
|
+
if (newFirst.element.prefix !== originalFirstPrefix) {
|
|
46
|
+
values[0] = {
|
|
47
|
+
...newFirst,
|
|
48
|
+
element: {...newFirst.element, prefix: originalFirstPrefix}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
...m,
|
|
55
|
+
value: {
|
|
56
|
+
...obj,
|
|
57
|
+
members: values
|
|
58
|
+
} as Json.Object
|
|
59
|
+
} as Json.Member;
|
|
60
|
+
}
|
|
61
|
+
}();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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, maybeRemoveImport} from "@openrewrite/rewrite/javascript";
|
|
9
|
+
import {J, isIdentifier} from "@openrewrite/rewrite/java";
|
|
10
|
+
import {create} from "mutative";
|
|
11
|
+
|
|
12
|
+
const DECORATORS_WITH_ENTRY_COMPONENTS = ['NgModule', 'Component'];
|
|
13
|
+
|
|
14
|
+
export class RemoveEntryComponents extends Recipe {
|
|
15
|
+
readonly name = "org.openrewrite.angular.migration.remove-entry-components";
|
|
16
|
+
readonly displayName: string = "Remove `entryComponents`";
|
|
17
|
+
readonly description: string = "Removes the `entryComponents` property from `@NgModule` and `@Component` decorators, " +
|
|
18
|
+
"and removes the `ANALYZE_FOR_ENTRY_COMPONENTS` import. " +
|
|
19
|
+
"These were removed in Angular 16 as they served no purpose since Ivy.";
|
|
20
|
+
|
|
21
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
22
|
+
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
23
|
+
private removedEntryComponents = false;
|
|
24
|
+
|
|
25
|
+
override async visitAnnotation(annotation: J.Annotation, p: ExecutionContext): Promise<J | undefined> {
|
|
26
|
+
let a = await super.visitAnnotation(annotation, p) as J.Annotation;
|
|
27
|
+
if (!a) return a;
|
|
28
|
+
|
|
29
|
+
const annotType = a.annotationType;
|
|
30
|
+
if (!isIdentifier(annotType)) return a;
|
|
31
|
+
if (!DECORATORS_WITH_ENTRY_COMPONENTS.includes(annotType.simpleName)) return a;
|
|
32
|
+
|
|
33
|
+
if (!a.arguments?.elements?.length) return a;
|
|
34
|
+
const firstArg = a.arguments.elements[0].element;
|
|
35
|
+
if (firstArg.kind !== J.Kind.NewClass) return a;
|
|
36
|
+
|
|
37
|
+
const newClass = firstArg as J.NewClass;
|
|
38
|
+
if (!newClass.body) return a;
|
|
39
|
+
|
|
40
|
+
let entryComponentsIndex = -1;
|
|
41
|
+
for (let i = 0; i < newClass.body.statements.length; i++) {
|
|
42
|
+
const stmt = newClass.body.statements[i].element;
|
|
43
|
+
if (stmt.kind === JS.Kind.PropertyAssignment) {
|
|
44
|
+
const prop = stmt as JS.PropertyAssignment;
|
|
45
|
+
const nameExpr = prop.name.element;
|
|
46
|
+
if (isIdentifier(nameExpr) && nameExpr.simpleName === 'entryComponents') {
|
|
47
|
+
entryComponentsIndex = i;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (entryComponentsIndex === -1) return a;
|
|
54
|
+
|
|
55
|
+
this.removedEntryComponents = true;
|
|
56
|
+
|
|
57
|
+
return create(a, draft => {
|
|
58
|
+
const body = (draft.arguments!.elements[0].element as any).body!;
|
|
59
|
+
body.statements = body.statements.filter((_: any, i: number) => i !== entryComponentsIndex);
|
|
60
|
+
}) as J.Annotation;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
override async visitIdentifier(identifier: J.Identifier, p: ExecutionContext): Promise<J | undefined> {
|
|
64
|
+
let id = await super.visitIdentifier(identifier, p) as J.Identifier;
|
|
65
|
+
if (!id) return id;
|
|
66
|
+
|
|
67
|
+
if (id.simpleName === 'ANALYZE_FOR_ENTRY_COMPONENTS') {
|
|
68
|
+
maybeRemoveImport(this, '@angular/core', 'ANALYZE_FOR_ENTRY_COMPONENTS');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return id;
|
|
72
|
+
}
|
|
73
|
+
}();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
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 {JsonVisitor, Json, getMemberKeyName, isObject} from "@openrewrite/rewrite/json";
|
|
9
|
+
|
|
10
|
+
export class RemoveEs5BrowserSupport extends Recipe {
|
|
11
|
+
readonly name = "org.openrewrite.angular.migration.remove-es5-browser-support";
|
|
12
|
+
readonly displayName: string = "Remove `es5BrowserSupport` from `angular.json`";
|
|
13
|
+
readonly description: string = "Removes the deprecated `es5BrowserSupport` option from `angular.json`. " +
|
|
14
|
+
"`es5BrowserSupport` was deprecated in Angular 7.3 and removed in Angular 10. " +
|
|
15
|
+
"Differential loading is now handled automatically by the Angular CLI based on the project's browserslist configuration.";
|
|
16
|
+
|
|
17
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
18
|
+
return new class extends JsonVisitor<ExecutionContext> {
|
|
19
|
+
protected async visitDocument(doc: Json.Document, p: ExecutionContext): Promise<Json | undefined> {
|
|
20
|
+
if (!doc.sourcePath.endsWith('angular.json')) return doc;
|
|
21
|
+
return super.visitDocument(doc, p);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
protected async visitMember(member: Json.Member, p: ExecutionContext): Promise<Json | undefined> {
|
|
25
|
+
const m = await super.visitMember(member, p) as Json.Member;
|
|
26
|
+
if (!m) return m;
|
|
27
|
+
|
|
28
|
+
if (!isObject(m.value)) return m;
|
|
29
|
+
|
|
30
|
+
const obj = m.value as Json.Object;
|
|
31
|
+
const filtered = obj.members.filter(rp => {
|
|
32
|
+
return getMemberKeyName(rp.element as Json.Member) !== 'es5BrowserSupport';
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (filtered.length === obj.members.length) return m;
|
|
36
|
+
|
|
37
|
+
const values = [...filtered];
|
|
38
|
+
if (values.length > 0 && obj.members.length > 0) {
|
|
39
|
+
const originalFirstPrefix = obj.members[0].element.prefix;
|
|
40
|
+
const newFirst = values[0];
|
|
41
|
+
if (newFirst.element.prefix !== originalFirstPrefix) {
|
|
42
|
+
values[0] = {
|
|
43
|
+
...newFirst,
|
|
44
|
+
element: {...newFirst.element, prefix: originalFirstPrefix}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
...m,
|
|
51
|
+
value: {
|
|
52
|
+
...obj,
|
|
53
|
+
members: values
|
|
54
|
+
} as Json.Object
|
|
55
|
+
} as Json.Member;
|
|
56
|
+
}
|
|
57
|
+
}();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
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 {JsonVisitor, Json, getMemberKeyName, isObject} from "@openrewrite/rewrite/json";
|
|
9
|
+
|
|
10
|
+
export class RemoveExtractCss extends Recipe {
|
|
11
|
+
readonly name = "org.openrewrite.angular.migration.remove-extract-css";
|
|
12
|
+
readonly displayName: string = "Remove `extractCss` from `angular.json`";
|
|
13
|
+
readonly description: string = "Removes the deprecated `extractCss` build option from `angular.json`. " +
|
|
14
|
+
"In Angular 11, CSS extraction became the default behavior for production builds and the option was deprecated.";
|
|
15
|
+
|
|
16
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
17
|
+
return new class extends JsonVisitor<ExecutionContext> {
|
|
18
|
+
protected async visitDocument(doc: Json.Document, p: ExecutionContext): Promise<Json | undefined> {
|
|
19
|
+
if (!doc.sourcePath.endsWith('angular.json')) return doc;
|
|
20
|
+
return super.visitDocument(doc, p);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
protected async visitMember(member: Json.Member, p: ExecutionContext): Promise<Json | undefined> {
|
|
24
|
+
const m = await super.visitMember(member, p) as Json.Member;
|
|
25
|
+
if (!m) return m;
|
|
26
|
+
|
|
27
|
+
const keyName = getMemberKeyName(m);
|
|
28
|
+
if (keyName !== 'options' && keyName !== 'production' && keyName !== 'development') return m;
|
|
29
|
+
if (!isObject(m.value)) return m;
|
|
30
|
+
|
|
31
|
+
const obj = m.value as Json.Object;
|
|
32
|
+
const filtered = obj.members.filter(rp => {
|
|
33
|
+
return getMemberKeyName(rp.element as Json.Member) !== 'extractCss';
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (filtered.length === obj.members.length) return m;
|
|
37
|
+
|
|
38
|
+
const values = [...filtered];
|
|
39
|
+
if (values.length > 0 && obj.members.length > 0) {
|
|
40
|
+
const originalFirstPrefix = obj.members[0].element.prefix;
|
|
41
|
+
const newFirst = values[0];
|
|
42
|
+
if (newFirst.element.prefix !== originalFirstPrefix) {
|
|
43
|
+
values[0] = {
|
|
44
|
+
...newFirst,
|
|
45
|
+
element: {...newFirst.element, prefix: originalFirstPrefix}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
...m,
|
|
52
|
+
value: {
|
|
53
|
+
...obj,
|
|
54
|
+
members: values
|
|
55
|
+
} as Json.Object
|
|
56
|
+
} as Json.Member;
|
|
57
|
+
}
|
|
58
|
+
}();
|
|
59
|
+
}
|
|
60
|
+
}
|