@openrewrite/recipes-angular 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/package.json +37 -0
  2. package/src/index.ts +321 -0
  3. package/src/migration/add-default-configuration.ts +121 -0
  4. package/src/migration/add-localize-polyfill.ts +51 -0
  5. package/src/migration/add-module-with-providers-generic.ts +102 -0
  6. package/src/migration/add-static-false-to-view-queries.ts +92 -0
  7. package/src/migration/add-testbed-teardown.ts +41 -0
  8. package/src/migration/enable-aot-build.ts +132 -0
  9. package/src/migration/explicit-standalone-flag.ts +82 -0
  10. package/src/migration/migrate-constructor-to-inject.ts +172 -0
  11. package/src/migration/migrate-input-to-signal.ts +320 -0
  12. package/src/migration/migrate-output-to-signal.ts +268 -0
  13. package/src/migration/migrate-query-to-signal.ts +276 -0
  14. package/src/migration/migrate-to-solution-style-tsconfig.ts +139 -0
  15. package/src/migration/move-document-to-core.ts +40 -0
  16. package/src/migration/remove-aot-summaries.ts +72 -0
  17. package/src/migration/remove-browser-module-with-server-transition.ts +185 -0
  18. package/src/migration/remove-component-factory-resolver.ts +48 -0
  19. package/src/migration/remove-default-project.ts +52 -0
  20. package/src/migration/remove-empty-ng-on-init.ts +80 -0
  21. package/src/migration/remove-enable-ivy.ts +63 -0
  22. package/src/migration/remove-entry-components.ts +75 -0
  23. package/src/migration/remove-es5-browser-support.ts +59 -0
  24. package/src/migration/remove-extract-css.ts +60 -0
  25. package/src/migration/remove-ie-polyfills.ts +118 -0
  26. package/src/migration/remove-module-id.ts +59 -0
  27. package/src/migration/remove-relative-link-resolution.ts +64 -0
  28. package/src/migration/remove-standalone-true.ts +50 -0
  29. package/src/migration/remove-static-false.ts +71 -0
  30. package/src/migration/remove-zone-js-polyfill.ts +55 -0
  31. package/src/migration/rename-after-render.ts +32 -0
  32. package/src/migration/rename-check-no-changes.ts +29 -0
  33. package/src/migration/rename-file.ts +72 -0
  34. package/src/migration/rename-pending-tasks.ts +30 -0
  35. package/src/migration/rename-zoneless-provider.ts +29 -0
  36. package/src/migration/replace-async-with-wait-for-async.ts +32 -0
  37. package/src/migration/replace-deep-zone-js-imports.ts +118 -0
  38. package/src/migration/replace-http-client-module.ts +276 -0
  39. package/src/migration/replace-initial-navigation.ts +73 -0
  40. package/src/migration/replace-inject-flags.ts +83 -0
  41. package/src/migration/replace-load-children-string.ts +48 -0
  42. package/src/migration/replace-node-sass-with-sass.ts +22 -0
  43. package/src/migration/replace-router-link-with-href.ts +37 -0
  44. package/src/migration/replace-testbed-get-with-inject.ts +33 -0
  45. package/src/migration/replace-untyped-forms.ts +59 -0
  46. package/src/migration/replace-validator-with-validators.ts +41 -0
  47. package/src/migration/replace-view-encapsulation-native.ts +51 -0
  48. package/src/migration/update-component-template-url.ts +186 -0
  49. package/src/migration/update-tsconfig-module.ts +75 -0
  50. package/src/migration/update-tsconfig-target.ts +61 -0
  51. package/src/migration/upgrade-to-angular-10.ts +52 -0
  52. package/src/migration/upgrade-to-angular-11.ts +52 -0
  53. package/src/migration/upgrade-to-angular-12.ts +43 -0
  54. package/src/migration/upgrade-to-angular-13.ts +45 -0
  55. package/src/migration/upgrade-to-angular-14.ts +44 -0
  56. package/src/migration/upgrade-to-angular-15.ts +43 -0
  57. package/src/migration/upgrade-to-angular-16.ts +57 -0
  58. package/src/migration/upgrade-to-angular-17.ts +43 -0
  59. package/src/migration/upgrade-to-angular-18.ts +69 -0
  60. package/src/migration/upgrade-to-angular-19.ts +52 -0
  61. package/src/migration/upgrade-to-angular-20.ts +47 -0
  62. package/src/migration/upgrade-to-angular-21.ts +53 -0
  63. package/src/migration/upgrade-to-angular-8.ts +54 -0
  64. package/src/migration/upgrade-to-angular-9.ts +69 -0
  65. package/src/search/find-analyze-for-entry-components-usage.ts +46 -0
  66. package/src/search/find-angular-decorator.ts +58 -0
  67. package/src/search/find-angular-http-usage.ts +35 -0
  68. package/src/search/find-animation-driver-matches-element.ts +38 -0
  69. package/src/search/find-async-test-helper-usage.ts +45 -0
  70. package/src/search/find-bare-module-with-providers.ts +47 -0
  71. package/src/search/find-browser-transfer-state-module-usage.ts +45 -0
  72. package/src/search/find-common-module-usage.ts +47 -0
  73. package/src/search/find-compiler-factory-usage.ts +51 -0
  74. package/src/search/find-date-pipe-default-timezone-usage.ts +46 -0
  75. package/src/search/find-effect-timing-usage.ts +28 -0
  76. package/src/search/find-empty-projectable-nodes.ts +68 -0
  77. package/src/search/find-fake-async-usage.ts +37 -0
  78. package/src/search/find-hammer-js-usage.ts +48 -0
  79. package/src/search/find-i18n-usage.ts +94 -0
  80. package/src/search/find-karma-usage.ts +47 -0
  81. package/src/search/find-load-children-string-usage.ts +43 -0
  82. package/src/search/find-missing-injectable.ts +75 -0
  83. package/src/search/find-ng-class-usage.ts +45 -0
  84. package/src/search/find-ng-style-usage.ts +45 -0
  85. package/src/search/find-path-match-type-usage.ts +44 -0
  86. package/src/search/find-platform-dynamic-server-usage.ts +38 -0
  87. package/src/search/find-platform-webworker-usage.ts +34 -0
  88. package/src/search/find-platform-worker-usage.ts +39 -0
  89. package/src/search/find-preserve-fragment-usage.ts +32 -0
  90. package/src/search/find-preserve-query-params-usage.ts +32 -0
  91. package/src/search/find-provided-in-deprecated-usage.ts +65 -0
  92. package/src/search/find-reflective-injector-usage.ts +45 -0
  93. package/src/search/find-render-application-usage.ts +47 -0
  94. package/src/search/find-render-component-type-usage.ts +46 -0
  95. package/src/search/find-render-module-factory-usage.ts +45 -0
  96. package/src/search/find-renderer-usage.ts +46 -0
  97. package/src/search/find-resource-cache-provider-usage.ts +38 -0
  98. package/src/search/find-root-renderer-usage.ts +47 -0
  99. package/src/search/find-rxjs-compat-usage.ts +40 -0
  100. package/src/search/find-server-transfer-state-module-usage.ts +38 -0
  101. package/src/search/find-setup-testing-router-usage.ts +45 -0
  102. package/src/search/find-testability-pending-request-usage.ts +38 -0
  103. package/src/search/find-undecorated-angular-class.ts +78 -0
  104. package/src/search/find-with-no-dom-reuse-usage.ts +46 -0
  105. package/src/search/find-wrapped-value-usage.ts +46 -0
  106. package/src/search/find-zone-js-usage.ts +43 -0
@@ -0,0 +1,94 @@
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, foundSearchResult, 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} from "@openrewrite/rewrite/json";
11
+
12
+ const I18N_CONFIG_KEYS = new Set(['i18nLocale', 'i18nFile', 'i18nFormat', 'i18nMissingTranslation']);
13
+
14
+ class FindI18nConfigInAngularJson extends Recipe {
15
+ readonly name = "org.openrewrite.angular.search.find-i18n-usage.angular-json";
16
+ readonly displayName = "Find i18n configuration in `angular.json`";
17
+ readonly description = "Finds legacy i18n configuration keys (`i18nLocale`, `i18nFile`, `i18nFormat`, `i18nMissingTranslation`) in `angular.json`.";
18
+
19
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
20
+ return new class extends JsonVisitor<ExecutionContext> {
21
+ protected async visitDocument(doc: Json.Document, p: ExecutionContext): Promise<Json | undefined> {
22
+ if (!doc.sourcePath.endsWith('angular.json')) return doc;
23
+ return super.visitDocument(doc, p);
24
+ }
25
+
26
+ protected async visitMember(member: Json.Member, p: ExecutionContext): Promise<Json | undefined> {
27
+ const m = await super.visitMember(member, p) as Json.Member;
28
+ if (!m) return m;
29
+
30
+ const keyName = getMemberKeyName(m);
31
+ if (keyName && I18N_CONFIG_KEYS.has(keyName)) {
32
+ return foundSearchResult(m,
33
+ `Legacy i18n config \`${keyName}\`. Angular 9 requires \`@angular/localize\` for i18n support.`);
34
+ }
35
+
36
+ return m;
37
+ }
38
+ }();
39
+ }
40
+ }
41
+
42
+ class FindLocalizeUsageInTypeScript extends Recipe {
43
+ readonly name = "org.openrewrite.angular.search.find-i18n-usage.typescript";
44
+ readonly displayName = "Find `$localize` usage in TypeScript";
45
+ readonly description = "Finds `$localize` tagged template literal usage and `@angular/localize` imports.";
46
+
47
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
48
+ return new class extends JavaScriptVisitor<ExecutionContext> {
49
+ protected async visitImportDeclaration(jsImport: JS.Import, p: ExecutionContext): Promise<J | undefined> {
50
+ const imp = await super.visitImportDeclaration(jsImport, p) as JS.Import;
51
+ if (!imp.moduleSpecifier) return imp;
52
+
53
+ const moduleSpec = imp.moduleSpecifier.element;
54
+ if (moduleSpec.kind !== J.Kind.Literal) return imp;
55
+
56
+ const moduleName = (moduleSpec as J.Literal).value as string;
57
+ if (moduleName === '@angular/localize' || moduleName.startsWith('@angular/localize/')) {
58
+ return foundSearchResult(imp,
59
+ "`@angular/localize` import. Ensure `import '@angular/localize/init'` is in `polyfills.ts`.");
60
+ }
61
+
62
+ return imp;
63
+ }
64
+
65
+ override async visitIdentifier(identifier: J.Identifier, p: ExecutionContext): Promise<J | undefined> {
66
+ const id = await super.visitIdentifier(identifier, p) as J.Identifier;
67
+ if (!id) return id;
68
+
69
+ if (id.simpleName === '$localize') {
70
+ return foundSearchResult(id,
71
+ "`$localize` usage. Ensure `@angular/localize` is installed and `import '@angular/localize/init'` is in `polyfills.ts`.");
72
+ }
73
+
74
+ return id;
75
+ }
76
+ };
77
+ }
78
+ }
79
+
80
+ export class FindI18nUsage extends Recipe {
81
+ readonly name = "org.openrewrite.angular.search.find-i18n-usage";
82
+ readonly displayName: string = "Find i18n usage";
83
+ readonly description: string = "Finds i18n usage indicators: legacy i18n configuration in `angular.json` " +
84
+ "(`i18nLocale`, `i18nFile`, `i18nFormat`, `i18nMissingTranslation`), `$localize` tagged template literals, " +
85
+ "and `@angular/localize` imports. Projects with these markers need `@angular/localize` installed and " +
86
+ "`import '@angular/localize/init'` in `polyfills.ts` for Angular 9+.";
87
+
88
+ async recipeList(): Promise<Recipe[]> {
89
+ return [
90
+ new FindI18nConfigInAngularJson(),
91
+ new FindLocalizeUsageInTypeScript(),
92
+ ];
93
+ }
94
+ }
@@ -0,0 +1,47 @@
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, foundSearchResult, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {JsonVisitor, Json, getMemberKeyName, isLiteral} from "@openrewrite/rewrite/json";
9
+
10
+ export class FindKarmaUsage extends Recipe {
11
+ readonly name = "org.openrewrite.angular.search.find-karma-usage";
12
+ readonly displayName: string = "Find Karma test runner usage";
13
+ readonly description: string = "Finds Karma test runner configuration in package.json dependencies and angular.json test builder. " +
14
+ "Angular 21 replaces Karma with Vitest as the default test runner.";
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('package.json') && !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 key = getMemberKeyName(m);
28
+ if (!key) return m;
29
+
30
+ if (key.startsWith('karma')) {
31
+ return foundSearchResult(m,
32
+ `Karma dependency '${key}'. Angular 21 uses Vitest as the default test runner. See https://angular.dev/guide/testing`);
33
+ }
34
+
35
+ if (key === 'builder' && isLiteral(m.value)) {
36
+ const val = m.value.value as string;
37
+ if (typeof val === 'string' && val.includes('karma')) {
38
+ return foundSearchResult(m,
39
+ "Karma test builder reference. Angular 21 uses Vitest as the default test runner. See https://angular.dev/guide/testing");
40
+ }
41
+ }
42
+
43
+ return m;
44
+ }
45
+ }();
46
+ }
47
+ }
@@ -0,0 +1,43 @@
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, foundSearchResult, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {JavaScriptVisitor, JS} from "@openrewrite/rewrite/javascript";
9
+ import {J, isIdentifier, isLiteral} from "@openrewrite/rewrite/java";
10
+
11
+ export class FindLoadChildrenStringUsage extends Recipe {
12
+ readonly name = "org.openrewrite.angular.search.find-load-children-string-usage";
13
+ readonly displayName: string = "Find deprecated string-based `loadChildren` usage";
14
+ readonly description: string = "Finds usages of the deprecated string-based `loadChildren` syntax " +
15
+ "(e.g. `loadChildren: './path/to/module#ModuleName'`). " +
16
+ "String-based lazy loading was deprecated in Angular 8 and removed in Angular 11. " +
17
+ "Use dynamic imports instead: `loadChildren: () => import('./path/to/module').then(m => m.ModuleName)`.";
18
+
19
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
20
+ return new class extends JavaScriptVisitor<ExecutionContext> {
21
+ override async visitPropertyAssignment(prop: JS.PropertyAssignment, p: ExecutionContext): Promise<J | undefined> {
22
+ let pa = await super.visitPropertyAssignment(prop, p) as JS.PropertyAssignment;
23
+ if (!pa) return pa;
24
+
25
+ const nameExpr = pa.name.element;
26
+ if (!isIdentifier(nameExpr) || nameExpr.simpleName !== 'loadChildren') return pa;
27
+
28
+ if (!pa.initializer) return pa;
29
+
30
+ if (isLiteral(pa.initializer)) {
31
+ const lit = pa.initializer as J.Literal;
32
+ if (typeof lit.value === 'string' && lit.value.includes('#')) {
33
+ return foundSearchResult(pa,
34
+ "String-based `loadChildren` was removed in Angular 11. " +
35
+ "Use dynamic imports: `loadChildren: () => import('...').then(m => m.ModuleName)`.");
36
+ }
37
+ }
38
+
39
+ return pa;
40
+ }
41
+ }();
42
+ }
43
+ }
@@ -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, foundSearchResult, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {JavaScriptVisitor, JS} from "@openrewrite/rewrite/javascript";
9
+ import {J, isIdentifier} from "@openrewrite/rewrite/java";
10
+
11
+ const ANGULAR_DECORATORS = new Set(['Component', 'Directive', 'Pipe', 'Injectable', 'NgModule']);
12
+
13
+ export class FindMissingInjectable extends Recipe {
14
+ readonly name = "org.openrewrite.angular.search.find-missing-injectable";
15
+ readonly displayName: string = "Find classes with DI dependencies but missing `@Injectable()`";
16
+ readonly description: string = "Finds classes that have constructor parameters (suggesting dependency injection) " +
17
+ "but lack an `@Injectable()` or other Angular class-level decorator. " +
18
+ "Angular 9 with Ivy requires an explicit `@Injectable()` decorator for all services that use dependency injection.";
19
+
20
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
21
+ return new class extends JavaScriptVisitor<ExecutionContext> {
22
+ private importsFromAngular = false;
23
+
24
+ protected async visitImportDeclaration(jsImport: JS.Import, p: ExecutionContext): Promise<J | undefined> {
25
+ const imp = await super.visitImportDeclaration(jsImport, p) as JS.Import;
26
+ if (!imp?.moduleSpecifier) return imp;
27
+
28
+ const moduleSpec = imp.moduleSpecifier.element;
29
+ if (moduleSpec.kind === J.Kind.Literal) {
30
+ const moduleName = (moduleSpec as J.Literal).value as string;
31
+ if (moduleName.startsWith('@angular/')) {
32
+ this.importsFromAngular = true;
33
+ }
34
+ }
35
+
36
+ return imp;
37
+ }
38
+
39
+ override async visitClassDeclaration(classDecl: J.ClassDeclaration, p: ExecutionContext): Promise<J | undefined> {
40
+ const cd = await super.visitClassDeclaration(classDecl, p) as J.ClassDeclaration;
41
+ if (!cd) return cd;
42
+
43
+ if (!this.importsFromAngular) return cd;
44
+
45
+ const hasAngularDecorator = cd.leadingAnnotations.some(a =>
46
+ isIdentifier(a.annotationType) && ANGULAR_DECORATORS.has(a.annotationType.simpleName)
47
+ );
48
+ if (hasAngularDecorator) return cd;
49
+
50
+ let hasConstructorParams = false;
51
+ for (const stmt of cd.body.statements) {
52
+ const el = stmt.element;
53
+ if (el.kind !== J.Kind.MethodDeclaration) continue;
54
+ const method = el as J.MethodDeclaration;
55
+ if (method.name.simpleName !== 'constructor') continue;
56
+
57
+ const params = method.parameters?.elements;
58
+ if (params && params.length > 0) {
59
+ const hasRealParams = params.some(rp => rp.element.kind !== J.Kind.Empty);
60
+ if (hasRealParams) {
61
+ hasConstructorParams = true;
62
+ }
63
+ }
64
+ break;
65
+ }
66
+
67
+ if (!hasConstructorParams) return cd;
68
+
69
+ return foundSearchResult(cd,
70
+ "Class has constructor dependencies but no `@Injectable()` decorator. " +
71
+ "Angular 9 with Ivy requires an explicit `@Injectable()` for dependency injection to work.");
72
+ }
73
+ }();
74
+ }
75
+ }
@@ -0,0 +1,45 @@
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, foundSearchResult, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {JavaScriptVisitor, JS} from "@openrewrite/rewrite/javascript";
9
+ import {J, isIdentifier} from "@openrewrite/rewrite/java";
10
+
11
+ export class FindNgClassUsage extends Recipe {
12
+ readonly name = "org.openrewrite.angular.search.find-ng-class-usage";
13
+ readonly displayName: string = "Find `NgClass` usage";
14
+ readonly description: string = "Finds imports of `NgClass` from `@angular/common`. " +
15
+ "The `ngClass` directive is soft deprecated in Angular 21 in favor of native `[class.*]` bindings.";
16
+
17
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
18
+ return new class extends JavaScriptVisitor<ExecutionContext> {
19
+ protected async visitImportDeclaration(jsImport: JS.Import, p: ExecutionContext): Promise<J | undefined> {
20
+ const imp = await super.visitImportDeclaration(jsImport, p) as JS.Import;
21
+ if (!imp.moduleSpecifier) return imp;
22
+
23
+ const moduleSpec = imp.moduleSpecifier.element;
24
+ if (moduleSpec.kind !== J.Kind.Literal) return imp;
25
+
26
+ const moduleName = (moduleSpec as J.Literal).value as string;
27
+ if (moduleName !== '@angular/common') return imp;
28
+
29
+ const namedBindings = imp.importClause?.namedBindings;
30
+ if (namedBindings?.kind === JS.Kind.NamedImports) {
31
+ const named = namedBindings as JS.NamedImports;
32
+ for (const specifier of named.elements.elements) {
33
+ const spec = specifier.element as JS.ImportSpecifier;
34
+ if (isIdentifier(spec.specifier) && spec.specifier.simpleName === 'NgClass') {
35
+ return foundSearchResult(imp,
36
+ "NgClass is soft deprecated in Angular 21. Use native [class.*] bindings instead. See https://angular.dev/api/common/NgClass");
37
+ }
38
+ }
39
+ }
40
+
41
+ return imp;
42
+ }
43
+ };
44
+ }
45
+ }
@@ -0,0 +1,45 @@
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, foundSearchResult, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {JavaScriptVisitor, JS} from "@openrewrite/rewrite/javascript";
9
+ import {J, isIdentifier} from "@openrewrite/rewrite/java";
10
+
11
+ export class FindNgStyleUsage extends Recipe {
12
+ readonly name = "org.openrewrite.angular.search.find-ng-style-usage";
13
+ readonly displayName: string = "Find `NgStyle` usage";
14
+ readonly description: string = "Finds imports of `NgStyle` from `@angular/common`. " +
15
+ "The `ngStyle` directive is soft deprecated in Angular 21 in favor of native `[style.*]` bindings.";
16
+
17
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
18
+ return new class extends JavaScriptVisitor<ExecutionContext> {
19
+ protected async visitImportDeclaration(jsImport: JS.Import, p: ExecutionContext): Promise<J | undefined> {
20
+ const imp = await super.visitImportDeclaration(jsImport, p) as JS.Import;
21
+ if (!imp.moduleSpecifier) return imp;
22
+
23
+ const moduleSpec = imp.moduleSpecifier.element;
24
+ if (moduleSpec.kind !== J.Kind.Literal) return imp;
25
+
26
+ const moduleName = (moduleSpec as J.Literal).value as string;
27
+ if (moduleName !== '@angular/common') return imp;
28
+
29
+ const namedBindings = imp.importClause?.namedBindings;
30
+ if (namedBindings?.kind === JS.Kind.NamedImports) {
31
+ const named = namedBindings as JS.NamedImports;
32
+ for (const specifier of named.elements.elements) {
33
+ const spec = specifier.element as JS.ImportSpecifier;
34
+ if (isIdentifier(spec.specifier) && spec.specifier.simpleName === 'NgStyle') {
35
+ return foundSearchResult(imp,
36
+ "NgStyle is soft deprecated in Angular 21. Use native [style.*] bindings instead. See https://angular.dev/api/common/NgStyle");
37
+ }
38
+ }
39
+ }
40
+
41
+ return imp;
42
+ }
43
+ };
44
+ }
45
+ }
@@ -0,0 +1,44 @@
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, foundSearchResult, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {JavaScriptVisitor, JS} from "@openrewrite/rewrite/javascript";
9
+ import {J, isIdentifier, isLiteral} from "@openrewrite/rewrite/java";
10
+
11
+ const VALID_VALUES = ['full', 'prefix'];
12
+
13
+ export class FindPathMatchTypeUsage extends Recipe {
14
+ readonly name = "org.openrewrite.angular.search.find-path-match-type-usage";
15
+ readonly displayName: string = "Find `pathMatch` route properties that may need type narrowing";
16
+ readonly description: string = "Finds `pathMatch` property assignments in route configurations. " +
17
+ "In Angular 14, the `pathMatch` type was narrowed from `string` to `'full' | 'prefix'`. " +
18
+ "Routes defined as plain objects without explicit `Route` or `Routes` typing may fail type checking.";
19
+
20
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
21
+ return new class extends JavaScriptVisitor<ExecutionContext> {
22
+ override async visitPropertyAssignment(prop: JS.PropertyAssignment, p: ExecutionContext): Promise<J | undefined> {
23
+ let pa = await super.visitPropertyAssignment(prop, p) as JS.PropertyAssignment;
24
+ if (!pa) return pa;
25
+
26
+ const nameExpr = pa.name.element;
27
+ if (!isIdentifier(nameExpr) || nameExpr.simpleName !== 'pathMatch') return pa;
28
+
29
+ if (!pa.initializer) return pa;
30
+
31
+ if (isLiteral(pa.initializer)) {
32
+ const lit = pa.initializer as J.Literal;
33
+ if (typeof lit.value === 'string' && VALID_VALUES.includes(lit.value)) {
34
+ return pa;
35
+ }
36
+ }
37
+
38
+ return foundSearchResult(pa,
39
+ "pathMatch type narrowed to 'full' | 'prefix' in Angular 14. " +
40
+ "Ensure the route object is typed as Route or Routes, or use an explicit literal value.");
41
+ }
42
+ }();
43
+ }
44
+ }
@@ -0,0 +1,38 @@
1
+ import {ExecutionContext, foundSearchResult, Recipe, TreeVisitor} from "@openrewrite/rewrite";
2
+ import {JavaScriptVisitor, JS} from "@openrewrite/rewrite/javascript";
3
+ import {J, isIdentifier} from "@openrewrite/rewrite/java";
4
+
5
+ export class FindPlatformDynamicServerUsage extends Recipe {
6
+ readonly name = "org.openrewrite.angular.search.find-platform-dynamic-server-usage";
7
+ readonly displayName: string = "Find `platformDynamicServer` usage";
8
+ readonly description: string = "Finds usages of the removed `platformDynamicServer` API from `@angular/platform-server`. In Angular 18, replace with `platformServer` and add `import '@angular/compiler'`.";
9
+
10
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
11
+ return new class extends JavaScriptVisitor<ExecutionContext> {
12
+ protected async visitImportDeclaration(jsImport: JS.Import, p: ExecutionContext): Promise<J | undefined> {
13
+ const imp = await super.visitImportDeclaration(jsImport, p) as JS.Import;
14
+ if (!imp.moduleSpecifier) return imp;
15
+
16
+ const moduleSpec = imp.moduleSpecifier.element;
17
+ if (moduleSpec.kind !== J.Kind.Literal) return imp;
18
+
19
+ const moduleName = (moduleSpec as J.Literal).value as string;
20
+ if (moduleName !== '@angular/platform-server') return imp;
21
+
22
+ const namedBindings = imp.importClause?.namedBindings;
23
+ if (namedBindings?.kind === JS.Kind.NamedImports) {
24
+ const named = namedBindings as JS.NamedImports;
25
+ for (const specifier of named.elements.elements) {
26
+ const spec = specifier.element as JS.ImportSpecifier;
27
+ if (isIdentifier(spec.specifier) && spec.specifier.simpleName === 'platformDynamicServer') {
28
+ return foundSearchResult(imp,
29
+ "platformDynamicServer has been removed in Angular 18. Replace with platformServer and add import '@angular/compiler'.");
30
+ }
31
+ }
32
+ }
33
+
34
+ return imp;
35
+ }
36
+ };
37
+ }
38
+ }
@@ -0,0 +1,34 @@
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, foundSearchResult, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {JavaScriptVisitor, JS} from "@openrewrite/rewrite/javascript";
9
+ import {J} from "@openrewrite/rewrite/java";
10
+
11
+ export class FindPlatformWebworkerUsage extends Recipe {
12
+ readonly name = "org.openrewrite.angular.search.find-platform-webworker-usage";
13
+ readonly displayName: string = "Find removed `@angular/platform-webworker` usage";
14
+ readonly description: string = "Finds imports from `@angular/platform-webworker` and `@angular/platform-webworker-dynamic`, " +
15
+ "which were removed in Angular 8 with no direct replacement.";
16
+
17
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
18
+ return new class extends JavaScriptVisitor<ExecutionContext> {
19
+ protected async visitImportDeclaration(jsImport: JS.Import, p: ExecutionContext): Promise<J | undefined> {
20
+ const imp = await super.visitImportDeclaration(jsImport, p) as JS.Import;
21
+ if (!imp.moduleSpecifier) return imp;
22
+
23
+ const moduleSpec = imp.moduleSpecifier.element;
24
+ if (moduleSpec.kind !== J.Kind.Literal) return imp;
25
+
26
+ const moduleName = (moduleSpec as J.Literal).value as string;
27
+ if (moduleName !== '@angular/platform-webworker' && moduleName !== '@angular/platform-webworker-dynamic') return imp;
28
+
29
+ return foundSearchResult(imp,
30
+ `\`${moduleName}\` was removed in Angular 8 with no direct replacement.`);
31
+ }
32
+ };
33
+ }
34
+ }
@@ -0,0 +1,39 @@
1
+ import {ExecutionContext, foundSearchResult, Recipe, TreeVisitor} from "@openrewrite/rewrite";
2
+ import {JavaScriptVisitor, JS} from "@openrewrite/rewrite/javascript";
3
+ import {J, isIdentifier} from "@openrewrite/rewrite/java";
4
+
5
+ export class FindPlatformWorkerUsage extends Recipe {
6
+ readonly name = "org.openrewrite.angular.search.find-platform-worker-usage";
7
+ readonly displayName: string = "Find `isPlatformWorkerUi` and `isPlatformWorkerApp` usage";
8
+ readonly description: string = "Finds usages of the removed `isPlatformWorkerUi` and `isPlatformWorkerApp` APIs from `@angular/common`. These were removed in Angular 18 with no replacement, as they served no purpose since the removal of the WebWorker platform.";
9
+
10
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
11
+ const removedApis = new Set(['isPlatformWorkerUi', 'isPlatformWorkerApp']);
12
+ return new class extends JavaScriptVisitor<ExecutionContext> {
13
+ protected async visitImportDeclaration(jsImport: JS.Import, p: ExecutionContext): Promise<J | undefined> {
14
+ const imp = await super.visitImportDeclaration(jsImport, p) as JS.Import;
15
+ if (!imp.moduleSpecifier) return imp;
16
+
17
+ const moduleSpec = imp.moduleSpecifier.element;
18
+ if (moduleSpec.kind !== J.Kind.Literal) return imp;
19
+
20
+ const moduleName = (moduleSpec as J.Literal).value as string;
21
+ if (moduleName !== '@angular/common') return imp;
22
+
23
+ const namedBindings = imp.importClause?.namedBindings;
24
+ if (namedBindings?.kind === JS.Kind.NamedImports) {
25
+ const named = namedBindings as JS.NamedImports;
26
+ for (const specifier of named.elements.elements) {
27
+ const spec = specifier.element as JS.ImportSpecifier;
28
+ if (isIdentifier(spec.specifier) && removedApis.has(spec.specifier.simpleName)) {
29
+ return foundSearchResult(imp,
30
+ `${spec.specifier.simpleName} has been removed from @angular/common in Angular 18 with no replacement.`);
31
+ }
32
+ }
33
+ }
34
+
35
+ return imp;
36
+ }
37
+ };
38
+ }
39
+ }
@@ -0,0 +1,32 @@
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, foundSearchResult, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {JavaScriptVisitor, JS} from "@openrewrite/rewrite/javascript";
9
+ import {J, isIdentifier} from "@openrewrite/rewrite/java";
10
+
11
+ export class FindPreserveFragmentUsage extends Recipe {
12
+ readonly name = "org.openrewrite.angular.search.find-preserve-fragment-usage";
13
+ readonly displayName: string = "Find deprecated `preserveFragment` usage";
14
+ readonly description: string = "Finds usages of the deprecated `preserveFragment` navigation option. " +
15
+ "`preserveFragment` was deprecated in Angular 4 and removed in Angular 11. " +
16
+ "Fragments are now preserved by default.";
17
+
18
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
19
+ return new class extends JavaScriptVisitor<ExecutionContext> {
20
+ override async visitPropertyAssignment(prop: JS.PropertyAssignment, p: ExecutionContext): Promise<J | undefined> {
21
+ let pa = await super.visitPropertyAssignment(prop, p) as JS.PropertyAssignment;
22
+ if (!pa) return pa;
23
+
24
+ const nameExpr = pa.name.element;
25
+ if (!isIdentifier(nameExpr) || nameExpr.simpleName !== 'preserveFragment') return pa;
26
+
27
+ return foundSearchResult(pa,
28
+ "`preserveFragment` was removed in Angular 11. Fragments are now preserved by default.");
29
+ }
30
+ }();
31
+ }
32
+ }
@@ -0,0 +1,32 @@
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, foundSearchResult, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {JavaScriptVisitor, JS} from "@openrewrite/rewrite/javascript";
9
+ import {J, isIdentifier} from "@openrewrite/rewrite/java";
10
+
11
+ export class FindPreserveQueryParamsUsage extends Recipe {
12
+ readonly name = "org.openrewrite.angular.search.find-preserve-query-params-usage";
13
+ readonly displayName: string = "Find deprecated `preserveQueryParams` usage";
14
+ readonly description: string = "Finds usages of the deprecated `preserveQueryParams` navigation option. " +
15
+ "`preserveQueryParams` was deprecated in Angular 4 and removed in Angular 11. " +
16
+ "Use `queryParamsHandling: 'preserve'` instead.";
17
+
18
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
19
+ return new class extends JavaScriptVisitor<ExecutionContext> {
20
+ override async visitPropertyAssignment(prop: JS.PropertyAssignment, p: ExecutionContext): Promise<J | undefined> {
21
+ let pa = await super.visitPropertyAssignment(prop, p) as JS.PropertyAssignment;
22
+ if (!pa) return pa;
23
+
24
+ const nameExpr = pa.name.element;
25
+ if (!isIdentifier(nameExpr) || nameExpr.simpleName !== 'preserveQueryParams') return pa;
26
+
27
+ return foundSearchResult(pa,
28
+ "`preserveQueryParams` was removed in Angular 11. Use `queryParamsHandling: 'preserve'` instead.");
29
+ }
30
+ }();
31
+ }
32
+ }
@@ -0,0 +1,65 @@
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, foundSearchResult, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {JavaScriptVisitor, JS} from "@openrewrite/rewrite/javascript";
9
+ import {J, isIdentifier, isLiteral} from "@openrewrite/rewrite/java";
10
+
11
+ const VALID_STRING_VALUES = new Set(['root', 'platform']);
12
+
13
+ export class FindProvidedInDeprecatedUsage extends Recipe {
14
+ readonly name = "org.openrewrite.angular.search.find-provided-in-deprecated-usage";
15
+ readonly displayName: string = "Find deprecated `providedIn` values";
16
+ readonly description: string = "Finds usages of `providedIn: 'any'` and `providedIn: NgModule` " +
17
+ "in `@Injectable` and `InjectionToken` declarations. These were deprecated in Angular 15. " +
18
+ "Use `providedIn: 'root'` or add the service to `NgModule.providers` instead.";
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 (annotType.simpleName !== 'Injectable') 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
+ for (const stmtPadded of newClass.body.statements) {
38
+ const stmt = stmtPadded.element;
39
+ if (stmt.kind !== JS.Kind.PropertyAssignment) continue;
40
+
41
+ const prop = stmt as JS.PropertyAssignment;
42
+ const nameExpr = prop.name.element;
43
+ if (!isIdentifier(nameExpr) || nameExpr.simpleName !== 'providedIn') continue;
44
+ if (!prop.initializer) continue;
45
+
46
+ if (isLiteral(prop.initializer)) {
47
+ const value = prop.initializer.value as string;
48
+ if (value === 'any') {
49
+ return foundSearchResult(a,
50
+ "providedIn: 'any' is deprecated. Use providedIn: 'root' or add the service to NgModule.providers instead.");
51
+ }
52
+ } else if (isIdentifier(prop.initializer)) {
53
+ const name = prop.initializer.simpleName;
54
+ if (!VALID_STRING_VALUES.has(name)) {
55
+ return foundSearchResult(a,
56
+ `providedIn: ${name} (NgModule) is deprecated. Add the service to ${name}.providers instead.`);
57
+ }
58
+ }
59
+ }
60
+
61
+ return a;
62
+ }
63
+ }();
64
+ }
65
+ }