@openrewrite/recipes-angular 1.9.0 → 1.10.0-20260813-195315
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/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +84 -2
- package/dist/index.js.map +1 -1
- package/dist/migration/add-eager-change-detection.d.ts +8 -0
- package/dist/migration/add-eager-change-detection.d.ts.map +1 -0
- package/dist/migration/add-eager-change-detection.js +130 -0
- package/dist/migration/add-eager-change-detection.js.map +1 -0
- package/dist/migration/add-istanbul-instrumenter.d.ts +8 -0
- package/dist/migration/add-istanbul-instrumenter.d.ts.map +1 -0
- package/dist/migration/add-istanbul-instrumenter.js +80 -0
- package/dist/migration/add-istanbul-instrumenter.js.map +1 -0
- package/dist/migration/add-no-incremental-hydration.d.ts +8 -0
- package/dist/migration/add-no-incremental-hydration.d.ts.map +1 -0
- package/dist/migration/add-no-incremental-hydration.js +69 -0
- package/dist/migration/add-no-incremental-hydration.js.map +1 -0
- package/dist/migration/add-strict-templates-false.d.ts +8 -0
- package/dist/migration/add-strict-templates-false.d.ts.map +1 -0
- package/dist/migration/add-strict-templates-false.js +84 -0
- package/dist/migration/add-strict-templates-false.js.map +1 -0
- package/dist/migration/add-trust-proxy-headers.d.ts +8 -0
- package/dist/migration/add-trust-proxy-headers.d.ts.map +1 -0
- package/dist/migration/add-trust-proxy-headers.js +82 -0
- package/dist/migration/add-trust-proxy-headers.js.map +1 -0
- package/dist/migration/add-with-xhr.d.ts +8 -0
- package/dist/migration/add-with-xhr.d.ts.map +1 -0
- package/dist/migration/add-with-xhr.js +70 -0
- package/dist/migration/add-with-xhr.js.map +1 -0
- package/dist/migration/js-support.d.ts +6 -0
- package/dist/migration/js-support.d.ts.map +1 -0
- package/dist/migration/js-support.js +94 -0
- package/dist/migration/js-support.js.map +1 -0
- package/dist/migration/json-support.d.ts +19 -0
- package/dist/migration/json-support.d.ts.map +1 -0
- package/dist/migration/json-support.js +117 -0
- package/dist/migration/json-support.js.map +1 -0
- package/dist/migration/migrate-input-to-signal.d.ts.map +1 -1
- package/dist/migration/migrate-input-to-signal.js +3 -67
- package/dist/migration/migrate-input-to-signal.js.map +1 -1
- package/dist/migration/migrate-model-with-output.d.ts +8 -0
- package/dist/migration/migrate-model-with-output.d.ts.map +1 -0
- package/dist/migration/migrate-model-with-output.js +192 -0
- package/dist/migration/migrate-model-with-output.js.map +1 -0
- package/dist/migration/remove-with-fetch.d.ts +8 -0
- package/dist/migration/remove-with-fetch.d.ts.map +1 -0
- package/dist/migration/remove-with-fetch.js +73 -0
- package/dist/migration/remove-with-fetch.js.map +1 -0
- package/dist/migration/rename-angular-js-globals.d.ts +8 -0
- package/dist/migration/rename-angular-js-globals.d.ts.map +1 -0
- package/dist/migration/rename-angular-js-globals.js +56 -0
- package/dist/migration/rename-angular-js-globals.js.map +1 -0
- package/dist/migration/rename-default-change-detection.d.ts +8 -0
- package/dist/migration/rename-default-change-detection.d.ts.map +1 -0
- package/dist/migration/rename-default-change-detection.js +52 -0
- package/dist/migration/rename-default-change-detection.js.map +1 -0
- package/dist/migration/rename-ssr-experimental-platform.d.ts +8 -0
- package/dist/migration/rename-ssr-experimental-platform.d.ts.map +1 -0
- package/dist/migration/rename-ssr-experimental-platform.js +124 -0
- package/dist/migration/rename-ssr-experimental-platform.js.map +1 -0
- package/dist/migration/replace-create-ng-module-ref.d.ts +8 -0
- package/dist/migration/replace-create-ng-module-ref.d.ts.map +1 -0
- package/dist/migration/replace-create-ng-module-ref.js +51 -0
- package/dist/migration/replace-create-ng-module-ref.js.map +1 -0
- package/dist/migration/suppress-safe-navigation-diagnostics.d.ts +8 -0
- package/dist/migration/suppress-safe-navigation-diagnostics.d.ts.map +1 -0
- package/dist/migration/suppress-safe-navigation-diagnostics.js +55 -0
- package/dist/migration/suppress-safe-navigation-diagnostics.js.map +1 -0
- package/dist/migration/upgrade-to-angular-22.d.ts +14 -0
- package/dist/migration/upgrade-to-angular-22.d.ts.map +1 -0
- package/dist/migration/upgrade-to-angular-22.js +112 -0
- package/dist/migration/upgrade-to-angular-22.js.map +1 -0
- package/dist/search/find-can-match-usage.d.ts +8 -0
- package/dist/search/find-can-match-usage.d.ts.map +1 -0
- package/dist/search/find-can-match-usage.js +53 -0
- package/dist/search/find-can-match-usage.js.map +1 -0
- package/dist/search/find-check-no-changes-usage.d.ts +8 -0
- package/dist/search/find-check-no-changes-usage.d.ts.map +1 -0
- package/dist/search/find-check-no-changes-usage.js +50 -0
- package/dist/search/find-check-no-changes-usage.js.map +1 -0
- package/dist/search/find-http-xhr-backend-usage.d.ts +8 -0
- package/dist/search/find-http-xhr-backend-usage.d.ts.map +1 -0
- package/dist/search/find-http-xhr-backend-usage.js +65 -0
- package/dist/search/find-http-xhr-backend-usage.js.map +1 -0
- package/dist/search/find-implicit-params-inheritance.d.ts +8 -0
- package/dist/search/find-implicit-params-inheritance.d.ts.map +1 -0
- package/dist/search/find-implicit-params-inheritance.js +74 -0
- package/dist/search/find-implicit-params-inheritance.js.map +1 -0
- package/dist/search/find-provide-routes-usage.d.ts +8 -0
- package/dist/search/find-provide-routes-usage.d.ts.map +1 -0
- package/dist/search/find-provide-routes-usage.js +64 -0
- package/dist/search/find-provide-routes-usage.js.map +1 -0
- package/dist/search/find-template-safe-navigation.d.ts +8 -0
- package/dist/search/find-template-safe-navigation.d.ts.map +1 -0
- package/dist/search/find-template-safe-navigation.js +105 -0
- package/dist/search/find-template-safe-navigation.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +60 -0
- package/src/migration/add-eager-change-detection.ts +109 -0
- package/src/migration/add-istanbul-instrumenter.ts +62 -0
- package/src/migration/add-no-incremental-hydration.ts +49 -0
- package/src/migration/add-strict-templates-false.ts +62 -0
- package/src/migration/add-trust-proxy-headers.ts +61 -0
- package/src/migration/add-with-xhr.ts +50 -0
- package/src/migration/js-support.ts +92 -0
- package/src/migration/json-support.ts +119 -0
- package/src/migration/migrate-input-to-signal.ts +1 -67
- package/src/migration/migrate-model-with-output.ts +201 -0
- package/src/migration/remove-with-fetch.ts +53 -0
- package/src/migration/rename-angular-js-globals.ts +37 -0
- package/src/migration/rename-default-change-detection.ts +33 -0
- package/src/migration/rename-ssr-experimental-platform.ts +108 -0
- package/src/migration/replace-create-ng-module-ref.ts +32 -0
- package/src/migration/suppress-safe-navigation-diagnostics.ts +42 -0
- package/src/migration/upgrade-to-angular-22.ts +92 -0
- package/src/search/find-can-match-usage.ts +32 -0
- package/src/search/find-check-no-changes-usage.ts +29 -0
- package/src/search/find-http-xhr-backend-usage.ts +44 -0
- package/src/search/find-implicit-params-inheritance.ts +49 -0
- package/src/search/find-provide-routes-usage.ts +43 -0
- package/src/search/find-template-safe-navigation.ts +72 -0
|
@@ -0,0 +1,62 @@
|
|
|
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 {detectIndent, getMemberKeyName, isObject, Json, JsonVisitor} from "@openrewrite/rewrite/json";
|
|
9
|
+
import {addMemberPath, literal, memberIndent} from "./json-support";
|
|
10
|
+
|
|
11
|
+
const PACKAGE = 'istanbul-lib-instrument';
|
|
12
|
+
const VERSION = '^6.0.3';
|
|
13
|
+
|
|
14
|
+
const DEPENDENCY_SCOPES = ['dependencies', 'devDependencies'];
|
|
15
|
+
|
|
16
|
+
export class AddIstanbulInstrumenter extends Recipe {
|
|
17
|
+
readonly name = "org.openrewrite.angular.migration.add-istanbul-instrumenter";
|
|
18
|
+
readonly displayName: string = "Add `istanbul-lib-instrument` for Karma code coverage";
|
|
19
|
+
readonly description: string = "Adds `istanbul-lib-instrument` to `devDependencies` of projects that still depend on Karma. " +
|
|
20
|
+
"Angular 22 stopped bundling the instrumenter, so Karma code coverage fails without it. " +
|
|
21
|
+
"Run an install afterwards to refresh the lock file.";
|
|
22
|
+
|
|
23
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
24
|
+
return new class extends JsonVisitor<ExecutionContext> {
|
|
25
|
+
protected async visitDocument(doc: Json.Document, p: ExecutionContext): Promise<Json | undefined> {
|
|
26
|
+
if (!doc.sourcePath.endsWith('package.json')) return doc;
|
|
27
|
+
|
|
28
|
+
const root = doc.value;
|
|
29
|
+
if (!isObject(root)) return doc;
|
|
30
|
+
|
|
31
|
+
const declared = declaredPackages(root);
|
|
32
|
+
if (declared.has(PACKAGE)) return doc;
|
|
33
|
+
if (![...declared].some(name => name === 'karma' || name.startsWith('karma-'))) return doc;
|
|
34
|
+
|
|
35
|
+
const indentUnit = detectIndent(doc);
|
|
36
|
+
const value = addMemberPath(root, ['devDependencies', PACKAGE],
|
|
37
|
+
literal(`"${VERSION}"`, VERSION), memberIndent(root, indentUnit), indentUnit);
|
|
38
|
+
|
|
39
|
+
return value === root ? doc : {...doc, value} as Json.Document;
|
|
40
|
+
}
|
|
41
|
+
}();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function declaredPackages(root: Json.Object): Set<string> {
|
|
46
|
+
const declared = new Set<string>();
|
|
47
|
+
for (const rp of root.members) {
|
|
48
|
+
const member = rp.element;
|
|
49
|
+
if (member.kind !== Json.Kind.Member) continue;
|
|
50
|
+
if (!DEPENDENCY_SCOPES.includes(getMemberKeyName(member as Json.Member) ?? '')) continue;
|
|
51
|
+
|
|
52
|
+
const scope = (member as Json.Member).value;
|
|
53
|
+
if (!isObject(scope)) continue;
|
|
54
|
+
|
|
55
|
+
for (const dep of scope.members) {
|
|
56
|
+
if (dep.element.kind !== Json.Kind.Member) continue;
|
|
57
|
+
const name = getMemberKeyName(dep.element as Json.Member);
|
|
58
|
+
if (name) declared.add(name);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return declared;
|
|
62
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
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, maybeAddImport, Template} from "@openrewrite/rewrite/javascript";
|
|
9
|
+
import {isIdentifier, J} from "@openrewrite/rewrite/java";
|
|
10
|
+
|
|
11
|
+
const HYDRATION_FEATURES = ['withIncrementalHydration', 'withNoIncrementalHydration'];
|
|
12
|
+
|
|
13
|
+
export class AddNoIncrementalHydration extends Recipe {
|
|
14
|
+
readonly name = "org.openrewrite.angular.migration.add-no-incremental-hydration";
|
|
15
|
+
readonly displayName: string = "Add `withNoIncrementalHydration()` to `provideClientHydration()`";
|
|
16
|
+
readonly description: string = "Adds the `withNoIncrementalHydration()` opt out to `provideClientHydration()` calls that do not " +
|
|
17
|
+
"already configure incremental hydration. Angular 22 enables incremental hydration by default; the opt out retains the previous behavior.";
|
|
18
|
+
|
|
19
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
20
|
+
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
21
|
+
override async visitMethodInvocation(method: J.MethodInvocation, p: ExecutionContext): Promise<J | undefined> {
|
|
22
|
+
const m = await super.visitMethodInvocation(method, p) as J.MethodInvocation;
|
|
23
|
+
if (!m || m.name.simpleName !== 'provideClientHydration') return m;
|
|
24
|
+
|
|
25
|
+
const elements = (m.arguments?.elements ?? []).filter(rp => rp.element.kind !== J.Kind.Empty);
|
|
26
|
+
if (elements.some(rp => isHydrationFeatureCall(rp.element))) return m;
|
|
27
|
+
|
|
28
|
+
maybeAddImport(this, {
|
|
29
|
+
module: '@angular/platform-browser',
|
|
30
|
+
member: 'withNoIncrementalHydration',
|
|
31
|
+
onlyIfReferenced: false
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
let builder = Template.builder().code('provideClientHydration(');
|
|
35
|
+
for (const element of elements) {
|
|
36
|
+
builder = builder.param(element.element).code(', ');
|
|
37
|
+
}
|
|
38
|
+
const t = builder.code('withNoIncrementalHydration())').build();
|
|
39
|
+
return await t.apply(m, this.cursor);
|
|
40
|
+
}
|
|
41
|
+
}();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isHydrationFeatureCall(expression: J): boolean {
|
|
46
|
+
if (expression.kind !== J.Kind.MethodInvocation) return false;
|
|
47
|
+
const invocation = expression as J.MethodInvocation;
|
|
48
|
+
return isIdentifier(invocation.name) && HYDRATION_FEATURES.includes(invocation.name.simpleName);
|
|
49
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 the original author or authors.
|
|
3
|
+
*
|
|
4
|
+
* Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {emptyMarkers, ExecutionContext, randomId, Recipe, TreeVisitor} from "@openrewrite/rewrite";
|
|
8
|
+
import {detectIndent, getMemberKeyName, isMember, isObject, Json, JsonVisitor, rightPadded, space} from "@openrewrite/rewrite/json";
|
|
9
|
+
import {appendMember, buildMember, declaresAngularCompilerOptions, isRootTsconfig, literal, memberIndent} from "./json-support";
|
|
10
|
+
|
|
11
|
+
export class AddStrictTemplatesFalse extends Recipe {
|
|
12
|
+
readonly name = "org.openrewrite.angular.migration.add-strict-templates-false";
|
|
13
|
+
readonly displayName: string = "Add `strictTemplates: false` to `tsconfig.json`";
|
|
14
|
+
readonly description: string = "Adds `\"strictTemplates\": false` to `angularCompilerOptions` in `tsconfig.json` when the option is not set. " +
|
|
15
|
+
"Angular 22 turned strict template type checking on by default; setting it explicitly avoids new compilation errors.";
|
|
16
|
+
|
|
17
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
18
|
+
return new class extends JsonVisitor<ExecutionContext> {
|
|
19
|
+
private baseIndent = ' ';
|
|
20
|
+
|
|
21
|
+
protected async visitDocument(doc: Json.Document, p: ExecutionContext): Promise<Json | undefined> {
|
|
22
|
+
this.baseIndent = detectIndent(doc);
|
|
23
|
+
|
|
24
|
+
if (declaresAngularCompilerOptions(doc)) return super.visitDocument(doc, p);
|
|
25
|
+
if (!isRootTsconfig(doc.sourcePath)) return doc;
|
|
26
|
+
|
|
27
|
+
const root = doc.value;
|
|
28
|
+
if (!isObject(root)) return doc;
|
|
29
|
+
|
|
30
|
+
const indent = memberIndent(root, this.baseIndent);
|
|
31
|
+
const newMember = buildMember('angularCompilerOptions', {
|
|
32
|
+
kind: Json.Kind.Object,
|
|
33
|
+
id: randomId(),
|
|
34
|
+
prefix: space(' '),
|
|
35
|
+
markers: emptyMarkers,
|
|
36
|
+
members: [rightPadded(
|
|
37
|
+
buildMember('strictTemplates', literal('false', false), '\n' + indent + this.baseIndent),
|
|
38
|
+
space('\n' + indent))],
|
|
39
|
+
} as Json.Object, '\n' + indent);
|
|
40
|
+
|
|
41
|
+
return {...doc, value: appendMember(root, newMember, '\n')} as Json.Document;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
protected async visitMember(member: Json.Member, p: ExecutionContext): Promise<Json | undefined> {
|
|
45
|
+
const m = await super.visitMember(member, p) as Json.Member;
|
|
46
|
+
if (!m) return m;
|
|
47
|
+
|
|
48
|
+
if (getMemberKeyName(m) !== 'angularCompilerOptions' || !isObject(m.value)) return m;
|
|
49
|
+
|
|
50
|
+
const options = m.value as Json.Object;
|
|
51
|
+
const hasStrictTemplates = options.members.some(rp =>
|
|
52
|
+
isMember(rp.element) && getMemberKeyName(rp.element as Json.Member) === 'strictTemplates');
|
|
53
|
+
if (hasStrictTemplates) return m;
|
|
54
|
+
|
|
55
|
+
const indent = memberIndent(options, this.baseIndent.repeat(2));
|
|
56
|
+
const newMember = buildMember('strictTemplates', literal('false', false), '\n' + indent);
|
|
57
|
+
|
|
58
|
+
return {...m, value: appendMember(options, newMember, '\n' + this.baseIndent)} as Json.Member;
|
|
59
|
+
}
|
|
60
|
+
}();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
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, Template} from "@openrewrite/rewrite/javascript";
|
|
9
|
+
import {isIdentifier, J} from "@openrewrite/rewrite/java";
|
|
10
|
+
|
|
11
|
+
const APP_ENGINES = ['AngularNodeAppEngine', 'AngularAppEngine'];
|
|
12
|
+
|
|
13
|
+
const TODO_COMMENT = "// TODO: This is a security-sensitive option. Remove if not needed. " +
|
|
14
|
+
"For more information, see https://angular.dev/best-practices/security#configuring-trusted-proxy-headers";
|
|
15
|
+
|
|
16
|
+
export class AddTrustProxyHeaders extends Recipe {
|
|
17
|
+
readonly name = "org.openrewrite.angular.migration.add-trust-proxy-headers";
|
|
18
|
+
readonly displayName: string = "Add `trustProxyHeaders` to the Angular app engine";
|
|
19
|
+
readonly description: string = "Adds `trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto']` to " +
|
|
20
|
+
"`new AngularNodeAppEngine()` and `new AngularAppEngine()`. Angular 22 stopped trusting forwarded headers " +
|
|
21
|
+
"by default; the option restores the previous behavior and carries a TODO to remove it when the server " +
|
|
22
|
+
"does not sit behind a trusted proxy.";
|
|
23
|
+
|
|
24
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
25
|
+
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
26
|
+
protected async visitNewClass(newClass: J.NewClass, p: ExecutionContext): Promise<J | undefined> {
|
|
27
|
+
const n = await super.visitNewClass(newClass, p) as J.NewClass;
|
|
28
|
+
if (!n || !n.class || !isIdentifier(n.class) || !APP_ENGINES.includes(n.class.simpleName)) return n;
|
|
29
|
+
|
|
30
|
+
const args = (n.arguments?.elements ?? []).filter(rp => rp.element.kind !== J.Kind.Empty);
|
|
31
|
+
if (args.length > 1) return n;
|
|
32
|
+
|
|
33
|
+
const options = args.length === 1 ? args[0].element : undefined;
|
|
34
|
+
if (options && options.kind !== J.Kind.NewClass) return n;
|
|
35
|
+
if (options && declaresTrustProxyHeaders(options as J.NewClass)) return n;
|
|
36
|
+
|
|
37
|
+
const engine = n.class.simpleName;
|
|
38
|
+
const existing = (options as J.NewClass | undefined)?.body?.statements
|
|
39
|
+
.filter(rp => rp.element.kind !== J.Kind.Empty) ?? [];
|
|
40
|
+
|
|
41
|
+
let builder = Template.builder().code(`new ${engine}({\n ${TODO_COMMENT}\n trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],`);
|
|
42
|
+
for (const statement of existing) {
|
|
43
|
+
builder = builder.code('\n ').param(statement.element).code(',');
|
|
44
|
+
}
|
|
45
|
+
const t = builder.code('\n})').build();
|
|
46
|
+
return await t.apply(n, this.cursor);
|
|
47
|
+
}
|
|
48
|
+
}();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function declaresTrustProxyHeaders(options: J.NewClass): boolean {
|
|
53
|
+
return (options.body?.statements ?? []).some(rp => {
|
|
54
|
+
const element = rp.element;
|
|
55
|
+
// A spread may supply the option from elsewhere, which we cannot see from here
|
|
56
|
+
if (element.kind === JS.Kind.Spread) return true;
|
|
57
|
+
if (element.kind !== JS.Kind.PropertyAssignment) return false;
|
|
58
|
+
const name = (element as JS.PropertyAssignment).name.element;
|
|
59
|
+
return isIdentifier(name) && name.simpleName === 'trustProxyHeaders';
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
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, maybeAddImport, Template} from "@openrewrite/rewrite/javascript";
|
|
9
|
+
import {isIdentifier, J} from "@openrewrite/rewrite/java";
|
|
10
|
+
|
|
11
|
+
const BACKEND_FEATURES = ['withFetch', 'withXhr'];
|
|
12
|
+
|
|
13
|
+
export class AddWithXhr extends Recipe {
|
|
14
|
+
readonly name = "org.openrewrite.angular.migration.add-with-xhr";
|
|
15
|
+
readonly displayName: string = "Add `withXhr()` to `provideHttpClient()`";
|
|
16
|
+
readonly description: string = "Adds `withXhr()` to `provideHttpClient()` calls that select neither a `withFetch()` nor a " +
|
|
17
|
+
"`withXhr()` backend. Angular 22 made `FetchBackend` the default `HttpBackend`; `withXhr()` keeps the previous " +
|
|
18
|
+
"`XMLHttpRequest` based backend, which is also the only one that reports upload progress.";
|
|
19
|
+
|
|
20
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
21
|
+
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
22
|
+
override async visitMethodInvocation(method: J.MethodInvocation, p: ExecutionContext): Promise<J | undefined> {
|
|
23
|
+
const m = await super.visitMethodInvocation(method, p) as J.MethodInvocation;
|
|
24
|
+
if (!m || m.select || !isIdentifier(m.name) || m.name.simpleName !== 'provideHttpClient') return m;
|
|
25
|
+
|
|
26
|
+
const elements = (m.arguments?.elements ?? []).filter(rp => rp.element.kind !== J.Kind.Empty);
|
|
27
|
+
if (elements.some(rp => isBackendFeatureCall(rp.element))) return m;
|
|
28
|
+
|
|
29
|
+
maybeAddImport(this, {
|
|
30
|
+
module: '@angular/common/http',
|
|
31
|
+
member: 'withXhr',
|
|
32
|
+
onlyIfReferenced: false
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
let builder = Template.builder().code('provideHttpClient(');
|
|
36
|
+
for (const element of elements) {
|
|
37
|
+
builder = builder.param(element.element).code(', ');
|
|
38
|
+
}
|
|
39
|
+
const t = builder.code('withXhr())').build();
|
|
40
|
+
return await t.apply(m, this.cursor);
|
|
41
|
+
}
|
|
42
|
+
}();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isBackendFeatureCall(expression: J): boolean {
|
|
47
|
+
if (expression.kind !== J.Kind.MethodInvocation) return false;
|
|
48
|
+
const invocation = expression as J.MethodInvocation;
|
|
49
|
+
return !invocation.select && isIdentifier(invocation.name) && BACKEND_FEATURES.includes(invocation.name.simpleName);
|
|
50
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 the original author or authors.
|
|
3
|
+
*
|
|
4
|
+
* Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {emptyMarkers, randomId} from "@openrewrite/rewrite";
|
|
8
|
+
import {JS} from "@openrewrite/rewrite/javascript";
|
|
9
|
+
import {emptySpace, J, singleSpace} from "@openrewrite/rewrite/java";
|
|
10
|
+
|
|
11
|
+
export function buildPropertyAssignment(key: string, value: string): any {
|
|
12
|
+
return {
|
|
13
|
+
kind: JS.Kind.PropertyAssignment,
|
|
14
|
+
id: randomId(),
|
|
15
|
+
prefix: singleSpace,
|
|
16
|
+
markers: emptyMarkers,
|
|
17
|
+
name: {
|
|
18
|
+
kind: J.Kind.RightPadded,
|
|
19
|
+
element: {
|
|
20
|
+
kind: J.Kind.Identifier,
|
|
21
|
+
id: randomId(),
|
|
22
|
+
prefix: emptySpace,
|
|
23
|
+
markers: emptyMarkers,
|
|
24
|
+
simpleName: key,
|
|
25
|
+
annotations: [],
|
|
26
|
+
type: undefined,
|
|
27
|
+
},
|
|
28
|
+
after: emptySpace,
|
|
29
|
+
markers: emptyMarkers,
|
|
30
|
+
},
|
|
31
|
+
assigmentToken: 'Colon',
|
|
32
|
+
initializer: {
|
|
33
|
+
kind: J.Kind.Literal,
|
|
34
|
+
id: randomId(),
|
|
35
|
+
prefix: singleSpace,
|
|
36
|
+
markers: emptyMarkers,
|
|
37
|
+
value: value,
|
|
38
|
+
valueSource: `'${value}'`,
|
|
39
|
+
unicodeEscapes: undefined,
|
|
40
|
+
type: undefined,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildOptionsObject(properties: any[]): any {
|
|
46
|
+
return {
|
|
47
|
+
kind: J.Kind.NewClass,
|
|
48
|
+
id: randomId(),
|
|
49
|
+
prefix: emptySpace,
|
|
50
|
+
markers: emptyMarkers,
|
|
51
|
+
enclosing: undefined,
|
|
52
|
+
new: emptySpace,
|
|
53
|
+
clazz: undefined,
|
|
54
|
+
arguments: {
|
|
55
|
+
kind: 'org.openrewrite.java.tree.JContainer',
|
|
56
|
+
before: emptySpace,
|
|
57
|
+
markers: emptyMarkers,
|
|
58
|
+
elements: [],
|
|
59
|
+
},
|
|
60
|
+
body: {
|
|
61
|
+
kind: J.Kind.Block,
|
|
62
|
+
id: randomId(),
|
|
63
|
+
prefix: emptySpace,
|
|
64
|
+
markers: emptyMarkers,
|
|
65
|
+
static: {kind: J.Kind.RightPadded, element: false, after: emptySpace, markers: emptyMarkers},
|
|
66
|
+
statements: properties.map(p => ({
|
|
67
|
+
kind: J.Kind.RightPadded,
|
|
68
|
+
element: p,
|
|
69
|
+
after: emptySpace,
|
|
70
|
+
markers: emptyMarkers,
|
|
71
|
+
})),
|
|
72
|
+
end: singleSpace,
|
|
73
|
+
},
|
|
74
|
+
constructorType: undefined,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function identifier(simpleName: string, prefix = emptySpace): J.Identifier {
|
|
79
|
+
return {
|
|
80
|
+
kind: J.Kind.Identifier,
|
|
81
|
+
id: randomId(),
|
|
82
|
+
prefix,
|
|
83
|
+
markers: emptyMarkers,
|
|
84
|
+
simpleName,
|
|
85
|
+
annotations: [],
|
|
86
|
+
type: undefined,
|
|
87
|
+
} as J.Identifier;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function rightPadded<T>(element: T, prefix = emptySpace): any {
|
|
91
|
+
return {kind: J.Kind.RightPadded, element, after: prefix, markers: emptyMarkers};
|
|
92
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 the original author or authors.
|
|
3
|
+
*
|
|
4
|
+
* Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {emptyMarkers, randomId} from "@openrewrite/rewrite";
|
|
8
|
+
import {getMemberKeyName, isMember, isObject, Json, rightPadded, space} from "@openrewrite/rewrite/json";
|
|
9
|
+
|
|
10
|
+
export function isRootTsconfig(sourcePath: string): boolean {
|
|
11
|
+
return (sourcePath.split('/').pop() ?? '') === 'tsconfig.json';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `extends` replaces `angularCompilerOptions` wholesale rather than merging it, so a derived
|
|
16
|
+
* tsconfig that declares the block needs its own copy of every option set in the root.
|
|
17
|
+
*/
|
|
18
|
+
export function declaresAngularCompilerOptions(doc: Json.Document): boolean {
|
|
19
|
+
const root = doc.value;
|
|
20
|
+
return isObject(root) && findMemberIndex(root, 'angularCompilerOptions') !== -1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function literal(source: string, value: unknown): Json.Literal {
|
|
24
|
+
return {
|
|
25
|
+
kind: Json.Kind.Literal,
|
|
26
|
+
id: randomId(),
|
|
27
|
+
prefix: space(' '),
|
|
28
|
+
markers: emptyMarkers,
|
|
29
|
+
source,
|
|
30
|
+
value
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function buildMember(key: string, value: Json.Value, keyPrefix = ''): Json.Member {
|
|
35
|
+
return {
|
|
36
|
+
kind: Json.Kind.Member,
|
|
37
|
+
id: randomId(),
|
|
38
|
+
prefix: space(''),
|
|
39
|
+
markers: emptyMarkers,
|
|
40
|
+
key: rightPadded({
|
|
41
|
+
kind: Json.Kind.Literal,
|
|
42
|
+
id: randomId(),
|
|
43
|
+
prefix: space(keyPrefix),
|
|
44
|
+
markers: emptyMarkers,
|
|
45
|
+
source: `"${key}"`,
|
|
46
|
+
value: key
|
|
47
|
+
} as Json.Literal, space('')),
|
|
48
|
+
value
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function memberIndent(obj: Json.Object, fallback: string): string {
|
|
53
|
+
const first = obj.members.find(rp => isMember(rp.element));
|
|
54
|
+
if (!first) return fallback;
|
|
55
|
+
const firstPrefix = (first.element as Json.Member).key.element.prefix.whitespace;
|
|
56
|
+
const match = firstPrefix.match(/\n([ \t]+)/);
|
|
57
|
+
return match ? match[1] : fallback;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function findMemberIndex(obj: Json.Object, key: string): number {
|
|
61
|
+
return obj.members.findIndex(rp => isMember(rp.element) && getMemberKeyName(rp.element as Json.Member) === key);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function appendMember(obj: Json.Object, newMember: Json.Member, fallbackClosingWhitespace: string): Json.Object {
|
|
65
|
+
// An empty object holds a single Json.Empty placeholder rather than any members
|
|
66
|
+
const members = obj.members.filter(rp => isMember(rp.element));
|
|
67
|
+
const closing = members.length > 0
|
|
68
|
+
? members[members.length - 1].after
|
|
69
|
+
: space(fallbackClosingWhitespace);
|
|
70
|
+
|
|
71
|
+
if (members.length > 0) {
|
|
72
|
+
members[members.length - 1] = {...members[members.length - 1], after: space('')};
|
|
73
|
+
}
|
|
74
|
+
members.push(rightPadded(newMember, closing));
|
|
75
|
+
|
|
76
|
+
return {...obj, members};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** `indent` is the indentation of the member the returned value is assigned to. */
|
|
80
|
+
function buildNestedValue(keys: string[], leaf: Json.Value, indent: string, indentUnit: string): Json.Value {
|
|
81
|
+
if (keys.length === 0) return leaf;
|
|
82
|
+
|
|
83
|
+
const childIndent = indent + indentUnit;
|
|
84
|
+
return {
|
|
85
|
+
kind: Json.Kind.Object,
|
|
86
|
+
id: randomId(),
|
|
87
|
+
prefix: space(' '),
|
|
88
|
+
markers: emptyMarkers,
|
|
89
|
+
members: [rightPadded(
|
|
90
|
+
buildMember(keys[0], buildNestedValue(keys.slice(1), leaf, childIndent, indentUnit), '\n' + childIndent),
|
|
91
|
+
space('\n' + indent))],
|
|
92
|
+
} as Json.Object;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Adds `leaf` at `keys`, creating missing intermediate objects. Returns `obj` unchanged when the
|
|
97
|
+
* path already exists, or when an intermediate key holds something other than an object.
|
|
98
|
+
* `indent` is the indentation of the members of `obj`.
|
|
99
|
+
*/
|
|
100
|
+
export function addMemberPath(obj: Json.Object, keys: string[], leaf: Json.Value, indent: string, indentUnit: string): Json.Object {
|
|
101
|
+
const index = findMemberIndex(obj, keys[0]);
|
|
102
|
+
if (index === -1) {
|
|
103
|
+
const newMember = buildMember(keys[0], buildNestedValue(keys.slice(1), leaf, indent, indentUnit), '\n' + indent);
|
|
104
|
+
return appendMember(obj, newMember, '\n' + indent.slice(0, Math.max(0, indent.length - indentUnit.length)));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (keys.length === 1) return obj;
|
|
108
|
+
|
|
109
|
+
const member = obj.members[index].element as Json.Member;
|
|
110
|
+
if (!isObject(member.value)) return obj;
|
|
111
|
+
|
|
112
|
+
const child = member.value as Json.Object;
|
|
113
|
+
const updated = addMemberPath(child, keys.slice(1), leaf, memberIndent(child, indent + indentUnit), indentUnit);
|
|
114
|
+
if (updated === child) return obj;
|
|
115
|
+
|
|
116
|
+
const members = [...obj.members];
|
|
117
|
+
members[index] = {...members[index], element: {...member, value: updated} as Json.Member};
|
|
118
|
+
return {...obj, members};
|
|
119
|
+
}
|
|
@@ -8,6 +8,7 @@ import {ExecutionContext, randomId, emptyMarkers, markers as mkMarkers, Recipe,
|
|
|
8
8
|
import {JavaScriptVisitor, JS, maybeAddImport, maybeRemoveImport} from "@openrewrite/rewrite/javascript";
|
|
9
9
|
import {J, isIdentifier, isLiteral, emptySpace, singleSpace} from "@openrewrite/rewrite/java";
|
|
10
10
|
import {create} from "mutative";
|
|
11
|
+
import {buildOptionsObject, buildPropertyAssignment} from "./js-support";
|
|
11
12
|
|
|
12
13
|
const ANGULAR_DECORATORS = ['Component', 'Directive', 'Pipe', 'Injectable'];
|
|
13
14
|
|
|
@@ -220,73 +221,6 @@ function buildInputCall(typeExpr: any, existingInitializer: any, config: InputCo
|
|
|
220
221
|
return call;
|
|
221
222
|
}
|
|
222
223
|
|
|
223
|
-
function buildPropertyAssignment(key: string, value: string): any {
|
|
224
|
-
return {
|
|
225
|
-
kind: JS.Kind.PropertyAssignment,
|
|
226
|
-
id: randomId(),
|
|
227
|
-
prefix: singleSpace,
|
|
228
|
-
markers: emptyMarkers,
|
|
229
|
-
name: {
|
|
230
|
-
kind: J.Kind.RightPadded,
|
|
231
|
-
element: {
|
|
232
|
-
kind: J.Kind.Identifier,
|
|
233
|
-
id: randomId(),
|
|
234
|
-
prefix: emptySpace,
|
|
235
|
-
markers: emptyMarkers,
|
|
236
|
-
simpleName: key,
|
|
237
|
-
annotations: [],
|
|
238
|
-
type: undefined,
|
|
239
|
-
},
|
|
240
|
-
after: emptySpace,
|
|
241
|
-
markers: emptyMarkers,
|
|
242
|
-
},
|
|
243
|
-
assigmentToken: 'Colon',
|
|
244
|
-
initializer: {
|
|
245
|
-
kind: J.Kind.Literal,
|
|
246
|
-
id: randomId(),
|
|
247
|
-
prefix: singleSpace,
|
|
248
|
-
markers: emptyMarkers,
|
|
249
|
-
value: value,
|
|
250
|
-
valueSource: `'${value}'`,
|
|
251
|
-
unicodeEscapes: undefined,
|
|
252
|
-
type: undefined,
|
|
253
|
-
},
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function buildOptionsObject(properties: any[]): any {
|
|
258
|
-
return {
|
|
259
|
-
kind: J.Kind.NewClass,
|
|
260
|
-
id: randomId(),
|
|
261
|
-
prefix: emptySpace,
|
|
262
|
-
markers: emptyMarkers,
|
|
263
|
-
enclosing: undefined,
|
|
264
|
-
new: emptySpace,
|
|
265
|
-
clazz: undefined,
|
|
266
|
-
arguments: {
|
|
267
|
-
kind: 'org.openrewrite.java.tree.JContainer',
|
|
268
|
-
before: emptySpace,
|
|
269
|
-
markers: emptyMarkers,
|
|
270
|
-
elements: [],
|
|
271
|
-
},
|
|
272
|
-
body: {
|
|
273
|
-
kind: J.Kind.Block,
|
|
274
|
-
id: randomId(),
|
|
275
|
-
prefix: emptySpace,
|
|
276
|
-
markers: emptyMarkers,
|
|
277
|
-
static: {kind: J.Kind.RightPadded, element: false, after: emptySpace, markers: emptyMarkers},
|
|
278
|
-
statements: properties.map(p => ({
|
|
279
|
-
kind: J.Kind.RightPadded,
|
|
280
|
-
element: p,
|
|
281
|
-
after: emptySpace,
|
|
282
|
-
markers: emptyMarkers,
|
|
283
|
-
})),
|
|
284
|
-
end: singleSpace,
|
|
285
|
-
},
|
|
286
|
-
constructorType: undefined,
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
|
|
290
224
|
function buildInputField(varDecl: J.VariableDeclarations, inputAnnotationIdx: number, config: InputConfig): any {
|
|
291
225
|
const namedVar = varDecl.variables[0].element;
|
|
292
226
|
const inputCall = buildInputCall(
|