@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,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 {JsonVisitor, Json, getMemberKeyName, isLiteral, isArray} from "@openrewrite/rewrite/json";
|
|
11
|
+
|
|
12
|
+
const IE_POLYFILL_PREFIXES = ['core-js/', 'core-js'];
|
|
13
|
+
const IE_POLYFILL_EXACT = ['classlist.js', 'web-animations-js'];
|
|
14
|
+
|
|
15
|
+
function isIePolyfill(moduleSpecifier: string): boolean {
|
|
16
|
+
if (IE_POLYFILL_EXACT.includes(moduleSpecifier)) return true;
|
|
17
|
+
return IE_POLYFILL_PREFIXES.some(prefix => moduleSpecifier.startsWith(prefix));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class RemoveIePolyfillImports extends Recipe {
|
|
21
|
+
readonly name = "org.openrewrite.angular.migration.remove-ie-polyfills.typescript";
|
|
22
|
+
readonly displayName = "Remove IE polyfill imports from `polyfills.ts`";
|
|
23
|
+
readonly description = "Removes IE11-specific polyfill imports from `polyfills.ts`.";
|
|
24
|
+
|
|
25
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
26
|
+
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
27
|
+
private inPolyfillsFile = false;
|
|
28
|
+
|
|
29
|
+
protected async visitJsCompilationUnit(cu: JS.CompilationUnit, p: ExecutionContext): Promise<J | undefined> {
|
|
30
|
+
this.inPolyfillsFile = cu.sourcePath.endsWith('polyfills.ts') || cu.sourcePath.endsWith('polyfills.js');
|
|
31
|
+
return super.visitJsCompilationUnit(cu, p);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
protected async visitImportDeclaration(jsImport: JS.Import, p: ExecutionContext): Promise<J | undefined> {
|
|
35
|
+
const imp = await super.visitImportDeclaration(jsImport, p) as JS.Import;
|
|
36
|
+
if (!this.inPolyfillsFile) return imp;
|
|
37
|
+
if (!imp.moduleSpecifier) return imp;
|
|
38
|
+
|
|
39
|
+
const moduleSpec = imp.moduleSpecifier.element;
|
|
40
|
+
if (moduleSpec.kind !== J.Kind.Literal) return imp;
|
|
41
|
+
|
|
42
|
+
const moduleName = (moduleSpec as J.Literal).value as string;
|
|
43
|
+
if (isIePolyfill(moduleName)) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return imp;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
class RemoveIePolyfillsFromAngularJson extends Recipe {
|
|
54
|
+
readonly name = "org.openrewrite.angular.migration.remove-ie-polyfills.angular-json";
|
|
55
|
+
readonly displayName = "Remove IE polyfills from `angular.json`";
|
|
56
|
+
readonly description = "Removes IE11-specific polyfill entries from the `polyfills` array in `angular.json`.";
|
|
57
|
+
|
|
58
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
59
|
+
return new class extends JsonVisitor<ExecutionContext> {
|
|
60
|
+
protected async visitDocument(doc: Json.Document, p: ExecutionContext): Promise<Json | undefined> {
|
|
61
|
+
if (!doc.sourcePath.endsWith('angular.json')) return doc;
|
|
62
|
+
return super.visitDocument(doc, p);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
protected async visitMember(member: Json.Member, p: ExecutionContext): Promise<Json | undefined> {
|
|
66
|
+
const m = await super.visitMember(member, p) as Json.Member;
|
|
67
|
+
if (!m) return m;
|
|
68
|
+
|
|
69
|
+
if (getMemberKeyName(m) !== 'polyfills') return m;
|
|
70
|
+
if (!isArray(m.value)) return m;
|
|
71
|
+
|
|
72
|
+
const filtered = m.value.values.filter(rp => {
|
|
73
|
+
const elem = rp.element;
|
|
74
|
+
if (!isLiteral(elem)) return true;
|
|
75
|
+
const val = elem.value as string;
|
|
76
|
+
return typeof val !== 'string' || !isIePolyfill(val);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (filtered.length === m.value.values.length) return m;
|
|
80
|
+
|
|
81
|
+
const values = [...filtered];
|
|
82
|
+
if (values.length > 0 && m.value.values.length > 0) {
|
|
83
|
+
const originalFirstPrefix = m.value.values[0].element.prefix;
|
|
84
|
+
const newFirst = values[0];
|
|
85
|
+
if (newFirst.element.prefix !== originalFirstPrefix) {
|
|
86
|
+
values[0] = {
|
|
87
|
+
...newFirst,
|
|
88
|
+
element: {...newFirst.element, prefix: originalFirstPrefix}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const arr = m.value as Json.Array;
|
|
94
|
+
return {
|
|
95
|
+
...m,
|
|
96
|
+
value: {
|
|
97
|
+
...arr,
|
|
98
|
+
values
|
|
99
|
+
} as Json.Array
|
|
100
|
+
} as Json.Member;
|
|
101
|
+
}
|
|
102
|
+
}();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class RemoveIePolyfills extends Recipe {
|
|
107
|
+
readonly name = "org.openrewrite.angular.migration.remove-ie-polyfills";
|
|
108
|
+
readonly displayName: string = "Remove IE11 polyfills";
|
|
109
|
+
readonly description: string = "Removes IE11-specific polyfill imports (`core-js`, `classlist.js`, `web-animations-js`) " +
|
|
110
|
+
"from `polyfills.ts` and `angular.json`. Angular 13 dropped IE11 support, making these polyfills unnecessary.";
|
|
111
|
+
|
|
112
|
+
async recipeList(): Promise<Recipe[]> {
|
|
113
|
+
return [
|
|
114
|
+
new RemoveIePolyfillImports(),
|
|
115
|
+
new RemoveIePolyfillsFromAngularJson(),
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -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 {JavaScriptVisitor, JS} from "@openrewrite/rewrite/javascript";
|
|
9
|
+
import {J, isIdentifier} from "@openrewrite/rewrite/java";
|
|
10
|
+
import {create} from "mutative";
|
|
11
|
+
|
|
12
|
+
const DECORATORS_WITH_MODULE_ID = ['Component', 'Directive'];
|
|
13
|
+
|
|
14
|
+
export class RemoveModuleId extends Recipe {
|
|
15
|
+
readonly name = "org.openrewrite.angular.migration.remove-module-id";
|
|
16
|
+
readonly displayName: string = "Remove `moduleId`";
|
|
17
|
+
readonly description: string = "Removes the `moduleId` property from `@Component` and `@Directive` decorators. " +
|
|
18
|
+
"`moduleId` was deprecated in Angular 16 and removed in Angular 17 as it served no purpose since Ivy.";
|
|
19
|
+
|
|
20
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
21
|
+
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
22
|
+
override async visitAnnotation(annotation: J.Annotation, p: ExecutionContext): Promise<J | undefined> {
|
|
23
|
+
let a = await super.visitAnnotation(annotation, p) as J.Annotation;
|
|
24
|
+
if (!a) return a;
|
|
25
|
+
|
|
26
|
+
const annotType = a.annotationType;
|
|
27
|
+
if (!isIdentifier(annotType)) return a;
|
|
28
|
+
if (!DECORATORS_WITH_MODULE_ID.includes(annotType.simpleName)) return a;
|
|
29
|
+
|
|
30
|
+
if (!a.arguments?.elements?.length) return a;
|
|
31
|
+
const firstArg = a.arguments.elements[0].element;
|
|
32
|
+
if (firstArg.kind !== J.Kind.NewClass) return a;
|
|
33
|
+
|
|
34
|
+
const newClass = firstArg as J.NewClass;
|
|
35
|
+
if (!newClass.body) return a;
|
|
36
|
+
|
|
37
|
+
let moduleIdIndex = -1;
|
|
38
|
+
for (let i = 0; i < newClass.body.statements.length; i++) {
|
|
39
|
+
const stmt = newClass.body.statements[i].element;
|
|
40
|
+
if (stmt.kind === JS.Kind.PropertyAssignment) {
|
|
41
|
+
const prop = stmt as JS.PropertyAssignment;
|
|
42
|
+
const nameExpr = prop.name.element;
|
|
43
|
+
if (isIdentifier(nameExpr) && nameExpr.simpleName === 'moduleId') {
|
|
44
|
+
moduleIdIndex = i;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (moduleIdIndex === -1) return a;
|
|
51
|
+
|
|
52
|
+
return create(a, draft => {
|
|
53
|
+
const body = (draft.arguments!.elements[0].element as any).body!;
|
|
54
|
+
body.statements = body.statements.filter((_: any, i: number) => i !== moduleIdIndex);
|
|
55
|
+
}) as J.Annotation;
|
|
56
|
+
}
|
|
57
|
+
}();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
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} from "@openrewrite/rewrite/javascript";
|
|
9
|
+
import {J, isIdentifier} from "@openrewrite/rewrite/java";
|
|
10
|
+
import {create} from "mutative";
|
|
11
|
+
|
|
12
|
+
export class RemoveRelativeLinkResolution extends Recipe {
|
|
13
|
+
readonly name = "org.openrewrite.angular.migration.remove-relative-link-resolution";
|
|
14
|
+
readonly displayName: string = "Remove `relativeLinkResolution`";
|
|
15
|
+
readonly description: string = "Removes the `relativeLinkResolution` option from `RouterModule.forRoot()` calls. " +
|
|
16
|
+
"This option was deprecated in Angular 14 and removed in Angular 15.";
|
|
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 callArgs = capture({variadic: true});
|
|
25
|
+
if (!await pattern`RouterModule.forRoot(${callArgs})`.match(m, this.cursor)) return m;
|
|
26
|
+
|
|
27
|
+
const args = (m as any).arguments?.elements;
|
|
28
|
+
if (!args || args.length < 2) return m;
|
|
29
|
+
|
|
30
|
+
const optionsArg = args[1].element;
|
|
31
|
+
if (optionsArg.kind !== J.Kind.NewClass) return m;
|
|
32
|
+
|
|
33
|
+
const optionsObj = optionsArg as J.NewClass;
|
|
34
|
+
if (!optionsObj.body) return m;
|
|
35
|
+
|
|
36
|
+
let rlrIndex = -1;
|
|
37
|
+
for (let i = 0; i < optionsObj.body.statements.length; i++) {
|
|
38
|
+
const stmt = optionsObj.body.statements[i].element;
|
|
39
|
+
if (stmt.kind === JS.Kind.PropertyAssignment) {
|
|
40
|
+
const prop = stmt as JS.PropertyAssignment;
|
|
41
|
+
const nameExpr = prop.name.element;
|
|
42
|
+
if (isIdentifier(nameExpr) && nameExpr.simpleName === 'relativeLinkResolution') {
|
|
43
|
+
rlrIndex = i;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (rlrIndex === -1) return m;
|
|
50
|
+
|
|
51
|
+
if (optionsObj.body.statements.length === 1) {
|
|
52
|
+
return create(m, draft => {
|
|
53
|
+
(draft as any).arguments.elements = (draft as any).arguments.elements.filter((_: any, i: number) => i !== 1);
|
|
54
|
+
}) as J.MethodInvocation;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return create(m, draft => {
|
|
58
|
+
const body = ((draft as any).arguments.elements[1].element as any).body;
|
|
59
|
+
body.statements = body.statements.filter((_: any, i: number) => i !== rlrIndex);
|
|
60
|
+
}) as J.MethodInvocation;
|
|
61
|
+
}
|
|
62
|
+
}();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {ExecutionContext, Recipe, TreeVisitor} from "@openrewrite/rewrite";
|
|
2
|
+
import {JavaScriptVisitor, JS} from "@openrewrite/rewrite/javascript";
|
|
3
|
+
import {isIdentifier, isLiteral, J} from "@openrewrite/rewrite/java";
|
|
4
|
+
import {create} from "mutative";
|
|
5
|
+
|
|
6
|
+
const ANGULAR_DECORATORS = ['Component', 'Directive', 'Pipe'];
|
|
7
|
+
|
|
8
|
+
export class RemoveStandaloneTrue extends Recipe {
|
|
9
|
+
readonly name = "org.openrewrite.angular.migration.remove-standalone-true";
|
|
10
|
+
readonly displayName: string = "Remove redundant `standalone: true`";
|
|
11
|
+
readonly description: string = "Removes the `standalone: true` property from Angular component, directive, and pipe decorators since standalone is the default in Angular 19+.";
|
|
12
|
+
|
|
13
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
14
|
+
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
15
|
+
override async visitAnnotation(annotation: J.Annotation, p: ExecutionContext): Promise<J | undefined> {
|
|
16
|
+
let a = await super.visitAnnotation(annotation, p) as J.Annotation;
|
|
17
|
+
if (!a) return a;
|
|
18
|
+
|
|
19
|
+
const annotType = a.annotationType;
|
|
20
|
+
if (!isIdentifier(annotType)) return a;
|
|
21
|
+
if (!ANGULAR_DECORATORS.includes(annotType.simpleName)) return a;
|
|
22
|
+
|
|
23
|
+
if (!a.arguments?.elements?.length) return a;
|
|
24
|
+
const firstArg = a.arguments.elements[0].element;
|
|
25
|
+
if (firstArg.kind !== J.Kind.NewClass) return a;
|
|
26
|
+
|
|
27
|
+
const newClass = firstArg as J.NewClass;
|
|
28
|
+
if (!newClass.body) return a;
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < newClass.body.statements.length; i++) {
|
|
31
|
+
const stmt = newClass.body.statements[i].element;
|
|
32
|
+
if (stmt.kind === JS.Kind.PropertyAssignment) {
|
|
33
|
+
const prop = stmt as JS.PropertyAssignment;
|
|
34
|
+
const nameExpr = prop.name.element;
|
|
35
|
+
if (isIdentifier(nameExpr) && nameExpr.simpleName === 'standalone'
|
|
36
|
+
&& prop.initializer && isLiteral(prop.initializer)
|
|
37
|
+
&& prop.initializer.value === true) {
|
|
38
|
+
return create(a, draft => {
|
|
39
|
+
const body = (draft.arguments!.elements[0].element as any).body!;
|
|
40
|
+
body.statements = body.statements.filter((_: any, idx: number) => idx !== i);
|
|
41
|
+
}) as J.Annotation;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return a;
|
|
47
|
+
}
|
|
48
|
+
}();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
const QUERY_DECORATORS = ['ViewChild', 'ContentChild', 'ViewChildren', 'ContentChildren'];
|
|
13
|
+
|
|
14
|
+
export class RemoveStaticFalse extends Recipe {
|
|
15
|
+
readonly name = "org.openrewrite.angular.migration.remove-static-false";
|
|
16
|
+
readonly displayName: string = "Remove `static: false` from view queries";
|
|
17
|
+
readonly description: string = "Removes `static: false` from `@ViewChild`, `@ContentChild`, `@ViewChildren`, and `@ContentChildren` decorators. " +
|
|
18
|
+
"In Angular 9 with Ivy, `static: false` became the default behavior, making the explicit option unnecessary.";
|
|
19
|
+
|
|
20
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
21
|
+
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
22
|
+
override async visitAnnotation(annotation: J.Annotation, p: ExecutionContext): Promise<J | undefined> {
|
|
23
|
+
let a = await super.visitAnnotation(annotation, p) as J.Annotation;
|
|
24
|
+
if (!a) return a;
|
|
25
|
+
|
|
26
|
+
const annotType = a.annotationType;
|
|
27
|
+
if (!isIdentifier(annotType)) return a;
|
|
28
|
+
if (!QUERY_DECORATORS.includes(annotType.simpleName)) return a;
|
|
29
|
+
|
|
30
|
+
if (!a.arguments?.elements || a.arguments.elements.length < 2) return a;
|
|
31
|
+
|
|
32
|
+
const optionsArg = a.arguments.elements[1].element;
|
|
33
|
+
if (optionsArg.kind !== J.Kind.NewClass) return a;
|
|
34
|
+
|
|
35
|
+
const optionsObj = optionsArg as J.NewClass;
|
|
36
|
+
if (!optionsObj.body) return a;
|
|
37
|
+
|
|
38
|
+
let staticIndex = -1;
|
|
39
|
+
let staticValue: boolean | null = null;
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < optionsObj.body.statements.length; i++) {
|
|
42
|
+
const stmt = optionsObj.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 === 'static') {
|
|
47
|
+
staticIndex = i;
|
|
48
|
+
if (prop.initializer && isLiteral(prop.initializer)) {
|
|
49
|
+
staticValue = prop.initializer.value as boolean;
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (staticIndex === -1 || staticValue !== false) return a;
|
|
57
|
+
|
|
58
|
+
if (optionsObj.body.statements.length === 1) {
|
|
59
|
+
return create(a, draft => {
|
|
60
|
+
draft.arguments!.elements = draft.arguments!.elements.filter((_: any, i: number) => i !== 1);
|
|
61
|
+
}) as J.Annotation;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return create(a, draft => {
|
|
65
|
+
const body = (draft.arguments!.elements[1].element as any).body;
|
|
66
|
+
body.statements = body.statements.filter((_: any, i: number) => i !== staticIndex);
|
|
67
|
+
}) as J.Annotation;
|
|
68
|
+
}
|
|
69
|
+
}();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {ExecutionContext, Recipe, TreeVisitor} from "@openrewrite/rewrite";
|
|
2
|
+
import {JsonVisitor, Json, getMemberKeyName, isLiteral, isArray} from "@openrewrite/rewrite/json";
|
|
3
|
+
|
|
4
|
+
export class RemoveZoneJsPolyfill extends Recipe {
|
|
5
|
+
readonly name = "org.openrewrite.angular.migration.remove-zone-js-polyfill";
|
|
6
|
+
readonly displayName: string = "Remove zone.js polyfill from angular.json";
|
|
7
|
+
readonly description: string = "Removes zone.js entries from the `polyfills` array in `angular.json`. Angular 20 supports zoneless change detection via `provideZonelessChangeDetection()`, making the zone.js polyfill unnecessary.";
|
|
8
|
+
|
|
9
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
10
|
+
return new class extends JsonVisitor<ExecutionContext> {
|
|
11
|
+
protected async visitDocument(doc: Json.Document, p: ExecutionContext): Promise<Json | undefined> {
|
|
12
|
+
if (!doc.sourcePath.endsWith('angular.json')) return doc;
|
|
13
|
+
return super.visitDocument(doc, p);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
protected async visitMember(member: Json.Member, p: ExecutionContext): Promise<Json | undefined> {
|
|
17
|
+
const m = await super.visitMember(member, p) as Json.Member;
|
|
18
|
+
if (!m) return m;
|
|
19
|
+
|
|
20
|
+
if (getMemberKeyName(m) !== 'polyfills') return m;
|
|
21
|
+
if (!isArray(m.value)) return m;
|
|
22
|
+
|
|
23
|
+
const filtered = m.value.values.filter(rp => {
|
|
24
|
+
const elem = rp.element;
|
|
25
|
+
if (!isLiteral(elem)) return true;
|
|
26
|
+
const val = elem.value as string;
|
|
27
|
+
return val !== 'zone.js' && !val?.startsWith('zone.js/');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (filtered.length === m.value.values.length) return m;
|
|
31
|
+
|
|
32
|
+
const values = [...filtered];
|
|
33
|
+
if (values.length > 0 && m.value.values.length > 0) {
|
|
34
|
+
const originalFirstPrefix = m.value.values[0].element.prefix;
|
|
35
|
+
const newFirst = values[0];
|
|
36
|
+
if (newFirst.element.prefix !== originalFirstPrefix) {
|
|
37
|
+
values[0] = {
|
|
38
|
+
...newFirst,
|
|
39
|
+
element: {...newFirst.element, prefix: originalFirstPrefix}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const arr = m.value as Json.Array;
|
|
45
|
+
return {
|
|
46
|
+
...m,
|
|
47
|
+
value: {
|
|
48
|
+
...arr,
|
|
49
|
+
values
|
|
50
|
+
} as Json.Array
|
|
51
|
+
} as Json.Member;
|
|
52
|
+
}
|
|
53
|
+
}();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {ExecutionContext, produceAsync, Recipe, TreeVisitor} from "@openrewrite/rewrite";
|
|
2
|
+
import {JavaScriptVisitor, maybeAddImport, maybeRemoveImport} from "@openrewrite/rewrite/javascript";
|
|
3
|
+
import {J, Type} from "@openrewrite/rewrite/java";
|
|
4
|
+
|
|
5
|
+
export class RenameAfterRender extends Recipe {
|
|
6
|
+
readonly name = "org.openrewrite.angular.migration.rename-after-render";
|
|
7
|
+
readonly displayName: string = "Rename `afterRender` to `afterEveryRender`";
|
|
8
|
+
readonly description: string = "Renames `afterRender` to `afterEveryRender` in imports and usages. The `afterRender` function was renamed to `afterEveryRender` in Angular 20, and Angular provides no migration schematic for this change.";
|
|
9
|
+
|
|
10
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
11
|
+
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
12
|
+
protected async visitMethodInvocation(method: J.MethodInvocation, p: ExecutionContext): Promise<J | undefined> {
|
|
13
|
+
const m = await super.visitMethodInvocation(method, p) as J.MethodInvocation;
|
|
14
|
+
|
|
15
|
+
if (m.name.simpleName === 'afterRender') {
|
|
16
|
+
const methodType = m.methodType;
|
|
17
|
+
if (methodType?.declaringType?.kind === Type.Kind.Class
|
|
18
|
+
&& (methodType.declaringType as Type.Class).fullyQualifiedName === '@angular/core') {
|
|
19
|
+
maybeAddImport(this, {module: '@angular/core', member: 'afterEveryRender'});
|
|
20
|
+
maybeRemoveImport(this, '@angular/core', 'afterRender');
|
|
21
|
+
|
|
22
|
+
return produceAsync(m, async draft => {
|
|
23
|
+
draft.name.simpleName = 'afterEveryRender';
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return m;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {ExecutionContext, produceAsync, Recipe, TreeVisitor} from "@openrewrite/rewrite";
|
|
2
|
+
import {JavaScriptVisitor, maybeAddImport, maybeRemoveImport} from "@openrewrite/rewrite/javascript";
|
|
3
|
+
import {J} from "@openrewrite/rewrite/java";
|
|
4
|
+
|
|
5
|
+
export class RenameCheckNoChanges extends Recipe {
|
|
6
|
+
readonly name = "org.openrewrite.angular.migration.rename-check-no-changes";
|
|
7
|
+
readonly displayName: string = "Rename `provideExperimentalCheckNoChangesForDebug` to `provideCheckNoChangesForDebug`";
|
|
8
|
+
readonly description: string = "Renames `provideExperimentalCheckNoChangesForDebug` to `provideCheckNoChangesForDebug` in imports and usages. The experimental API was promoted to developer preview in Angular 20.";
|
|
9
|
+
|
|
10
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
11
|
+
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
12
|
+
override async visitIdentifier(identifier: J.Identifier, p: ExecutionContext): Promise<J | undefined> {
|
|
13
|
+
let id = await super.visitIdentifier(identifier, p) as J.Identifier;
|
|
14
|
+
if (!id) return id;
|
|
15
|
+
|
|
16
|
+
if (id.simpleName === 'provideExperimentalCheckNoChangesForDebug') {
|
|
17
|
+
maybeAddImport(this, {module: '@angular/core', member: 'provideCheckNoChangesForDebug'});
|
|
18
|
+
maybeRemoveImport(this, '@angular/core', 'provideExperimentalCheckNoChangesForDebug');
|
|
19
|
+
|
|
20
|
+
return produceAsync(id, async draft => {
|
|
21
|
+
draft.simpleName = 'provideCheckNoChangesForDebug';
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return id;
|
|
26
|
+
}
|
|
27
|
+
}();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
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, Option, Recipe, TreeVisitor} from "@openrewrite/rewrite";
|
|
8
|
+
import {isSourceFile} from "@openrewrite/rewrite";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
|
|
11
|
+
// TODO Replace with org.openrewrite.RenameFile once RpcRecipe supports no-arg construction.
|
|
12
|
+
// Previously this used rpc.prepareRecipe("org.openrewrite.RenameFile", ...) but
|
|
13
|
+
// PrepareRecipe.installSubRecipes calls marketplace.install(subRecipe.constructor, [])
|
|
14
|
+
// which fails for RpcRecipe because its constructor requires positional parameters.
|
|
15
|
+
export class RenameFile extends Recipe {
|
|
16
|
+
readonly name = "org.openrewrite.angular.migration.rename-file";
|
|
17
|
+
readonly displayName = "Rename file";
|
|
18
|
+
readonly description = "Renames files matching a glob pattern to a new file name, preserving the directory.";
|
|
19
|
+
|
|
20
|
+
@Option({
|
|
21
|
+
displayName: "File matcher",
|
|
22
|
+
description: "Glob pattern to match files (e.g., `**/browserslist`). " +
|
|
23
|
+
"Supports `**` prefix to match in any directory.",
|
|
24
|
+
example: "**/browserslist"
|
|
25
|
+
})
|
|
26
|
+
fileMatcher!: string;
|
|
27
|
+
|
|
28
|
+
@Option({
|
|
29
|
+
displayName: "File name",
|
|
30
|
+
description: "The new file name (just the basename, e.g., `.browserslistrc`).",
|
|
31
|
+
example: ".browserslistrc"
|
|
32
|
+
})
|
|
33
|
+
fileName!: string;
|
|
34
|
+
|
|
35
|
+
constructor(options?: { fileMatcher?: string; fileName?: string }) {
|
|
36
|
+
super(options);
|
|
37
|
+
this.fileMatcher ??= '';
|
|
38
|
+
this.fileName ??= '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
42
|
+
const pattern = this.fileMatcher;
|
|
43
|
+
const newName = this.fileName;
|
|
44
|
+
const matchFn = globToMatcher(pattern);
|
|
45
|
+
|
|
46
|
+
return new class extends TreeVisitor<any, ExecutionContext> {
|
|
47
|
+
protected async preVisit(tree: any, _: ExecutionContext): Promise<any> {
|
|
48
|
+
this.stopAfterPreVisit();
|
|
49
|
+
if (isSourceFile(tree)) {
|
|
50
|
+
const normalized = tree.sourcePath.replace(/\\/g, '/');
|
|
51
|
+
if (matchFn(normalized)) {
|
|
52
|
+
const dir = path.posix.dirname(normalized);
|
|
53
|
+
const newPath = dir === '.' ? newName : `${dir}/${newName}`;
|
|
54
|
+
return {...tree, sourcePath: newPath};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return tree;
|
|
58
|
+
}
|
|
59
|
+
}();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function globToMatcher(pattern: string): (filePath: string) => boolean {
|
|
64
|
+
if (pattern.startsWith('**/')) {
|
|
65
|
+
const suffix = pattern.slice(3);
|
|
66
|
+
return (filePath: string) => {
|
|
67
|
+
const basename = path.posix.basename(filePath);
|
|
68
|
+
return basename === suffix || filePath === suffix || filePath.endsWith('/' + suffix);
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return (filePath: string) => filePath === pattern;
|
|
72
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {ExecutionContext, Recipe, TreeVisitor} from "@openrewrite/rewrite";
|
|
2
|
+
import {JavaScriptVisitor, maybeAddImport, maybeRemoveImport} from "@openrewrite/rewrite/javascript";
|
|
3
|
+
import {J, isIdentifier} from "@openrewrite/rewrite/java";
|
|
4
|
+
import {create} from "mutative";
|
|
5
|
+
|
|
6
|
+
export class RenamePendingTasks extends Recipe {
|
|
7
|
+
readonly name = "org.openrewrite.angular.migration.rename-pending-tasks";
|
|
8
|
+
readonly displayName: string = "Rename `ExperimentalPendingTasks` to `PendingTasks`";
|
|
9
|
+
readonly description: string = "Renames `ExperimentalPendingTasks` to `PendingTasks` in imports and usages. `ExperimentalPendingTasks` was renamed in Angular 19.";
|
|
10
|
+
|
|
11
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
12
|
+
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
13
|
+
override async visitIdentifier(identifier: J.Identifier, p: ExecutionContext): Promise<J | undefined> {
|
|
14
|
+
let id = await super.visitIdentifier(identifier, p) as J.Identifier;
|
|
15
|
+
if (!id) return id;
|
|
16
|
+
|
|
17
|
+
if (id.simpleName === 'ExperimentalPendingTasks') {
|
|
18
|
+
maybeAddImport(this, {module: '@angular/core', member: 'PendingTasks'});
|
|
19
|
+
maybeRemoveImport(this, '@angular/core', 'ExperimentalPendingTasks');
|
|
20
|
+
|
|
21
|
+
return create(id, draft => {
|
|
22
|
+
draft.simpleName = 'PendingTasks';
|
|
23
|
+
}) as J.Identifier;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return id;
|
|
27
|
+
}
|
|
28
|
+
}();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {ExecutionContext, produceAsync, Recipe, TreeVisitor} from "@openrewrite/rewrite";
|
|
2
|
+
import {JavaScriptVisitor, maybeAddImport, maybeRemoveImport} from "@openrewrite/rewrite/javascript";
|
|
3
|
+
import {J} from "@openrewrite/rewrite/java";
|
|
4
|
+
|
|
5
|
+
export class RenameZonelessProvider extends Recipe {
|
|
6
|
+
readonly name = "org.openrewrite.angular.migration.rename-zoneless-provider";
|
|
7
|
+
readonly displayName: string = "Rename `provideExperimentalZonelessChangeDetection` to `provideZonelessChangeDetection`";
|
|
8
|
+
readonly description: string = "Renames `provideExperimentalZonelessChangeDetection` to `provideZonelessChangeDetection` in imports and usages. The experimental API was promoted to developer preview in Angular 20.";
|
|
9
|
+
|
|
10
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
11
|
+
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
12
|
+
override async visitIdentifier(identifier: J.Identifier, p: ExecutionContext): Promise<J | undefined> {
|
|
13
|
+
let id = await super.visitIdentifier(identifier, p) as J.Identifier;
|
|
14
|
+
if (!id) return id;
|
|
15
|
+
|
|
16
|
+
if (id.simpleName === 'provideExperimentalZonelessChangeDetection') {
|
|
17
|
+
maybeAddImport(this, {module: '@angular/core', member: 'provideZonelessChangeDetection'});
|
|
18
|
+
maybeRemoveImport(this, '@angular/core', 'provideExperimentalZonelessChangeDetection');
|
|
19
|
+
|
|
20
|
+
return produceAsync(id, async draft => {
|
|
21
|
+
draft.simpleName = 'provideZonelessChangeDetection';
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return id;
|
|
26
|
+
}
|
|
27
|
+
}();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {ExecutionContext, produceAsync, Recipe, TreeVisitor} from "@openrewrite/rewrite";
|
|
2
|
+
import {JavaScriptVisitor, maybeAddImport, maybeRemoveImport} from "@openrewrite/rewrite/javascript";
|
|
3
|
+
import {J, Type} from "@openrewrite/rewrite/java";
|
|
4
|
+
|
|
5
|
+
export class ReplaceAsyncWithWaitForAsync extends Recipe {
|
|
6
|
+
readonly name = "org.openrewrite.angular.migration.replace-async-with-wait-for-async";
|
|
7
|
+
readonly displayName: string = "Replace `async` with `waitForAsync`";
|
|
8
|
+
readonly description: string = "Replaces the removed `async` test helper from `@angular/core/testing` with `waitForAsync`. The `async` function was deprecated in Angular 11 and removed in Angular 18.";
|
|
9
|
+
|
|
10
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
11
|
+
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
12
|
+
protected async visitMethodInvocation(method: J.MethodInvocation, p: ExecutionContext): Promise<J | undefined> {
|
|
13
|
+
const m = await super.visitMethodInvocation(method, p) as J.MethodInvocation;
|
|
14
|
+
|
|
15
|
+
if (m.name.simpleName === 'async') {
|
|
16
|
+
const methodType = m.methodType;
|
|
17
|
+
if (methodType?.declaringType?.kind === Type.Kind.Class
|
|
18
|
+
&& (methodType.declaringType as Type.Class).fullyQualifiedName === '@angular/core/testing') {
|
|
19
|
+
maybeAddImport(this, {module: '@angular/core/testing', member: 'waitForAsync'});
|
|
20
|
+
maybeRemoveImport(this, '@angular/core/testing', 'async');
|
|
21
|
+
|
|
22
|
+
return produceAsync(m, async draft => {
|
|
23
|
+
draft.name.simpleName = 'waitForAsync';
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return m;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|