Skip to content
  •  
  •  
  •  
68 changes: 68 additions & 0 deletions .github/skills/ng-constructor-to-inject/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
name: ng-constructor-to-inject
description: 'Migrate Angular constructor parameter injection to inject() function syntax (Angular 14+ style). Use when: migrating constructor DI, converting constructor(private svc: Svc) to inject(), replacing constructor injection with field injection, fixing no-inject-constructor lint rule.'
argument-hint: 'path/to/file.ts or glob like packages/bits/src/**/*.ts'
---

# Constructor Injection → `inject()` Migration

Migrates Angular classes from constructor parameter injection to the `inject()` function style.

## When to Use

- A file has `constructor(private|public|protected|readonly dep: Type)` params
- A spec file instantiates the class with `new MyClass(dep1, dep2)` after migration
- A lint rule (`@angular-eslint/no-inject-constructor` or similar) flags constructor injection

## Transformation Rules

See [./references/patterns.md](./references/patterns.md) for exhaustive before/after patterns.

## Procedure

### Step 1 — Identify the target file(s)

If the user provided a path or glob, use `grep_search` to list all files matching:
```
constructor\((?:private|public|protected|readonly)
```
Otherwise work on the currently open file.

### Step 2 — For each source file

1. **Read the file** to understand its full structure.
2. **Collect constructor injected params**: extract `(modifier) (readonly?) name: Type` from the constructor signature.
3. **Determine constructor body**: check whether the constructor body contains any statements beyond super() calls.
4. **Apply the transformation**:
- Add a class field `modifier (readonly?) name = inject(Type);` for every injected param, placed directly after the last decorator / class opening `{`.
- Remove the matching params from the constructor signature.
- If the constructor is now empty (no params, no body except possibly `super()`), **remove the constructor entirely**.
- If a `super(...)` call exists without args after removing params, keep the constructor with only the super call.
- If there is remaining body code, keep the constructor with only the body (no params).
5. **Update the `@angular/core` import**: add `inject` to the existing import if it is not already present.
6. **Preserve all other imports**: do not touch non-angular-core imports.
7. **Format**: match the surrounding code style (spacing, trailing commas).

### Step 3 — Update spec files

Find the corresponding `.spec.ts` file(s). See [./references/test-migration.md](./references/test-migration.md).

1. If the spec creates the class with `new MyClass(dep1, dep2)`, migrate it to `TestBed`-based instantiation.
2. If the spec already uses `TestBed.createComponent` / `TestBed.inject`, no change is needed.
3. Run the spec after changes: prefer `runTests` tool over terminal.

### Step 4 — Verify

Run `get_errors` on the modified files and fix any TypeScript errors before finishing.

## Quick Reference

| Before | After |
|--------|-------|
| `constructor(private svc: SvcClass) {}` | `private svc = inject(SvcClass);` (constructor removed) |
| `constructor(private readonly svc: SvcClass) {}` | `private readonly svc = inject(SvcClass);` |
| `constructor(public svc: SvcClass) {}` | `public svc = inject(SvcClass);` |
| `constructor(protected svc: SvcClass) {}` | `protected svc = inject(SvcClass);` |
| `constructor(private svc: SvcClass) { this.init(); }` | field + constructor with body only |
| `inject` already in import | add to existing named import list |
| `import { Component } from "@angular/core"` | `import { Component, inject } from "@angular/core"` |
172 changes: 172 additions & 0 deletions .github/skills/ng-constructor-to-inject/references/patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Migration Patterns

## Basic: single private dep, empty constructor

```ts
// BEFORE
import { Component } from "@angular/core";
import { MyService } from "./my.service";

@Component({ selector: "my-cmp", template: "" })
export class MyComponent {
constructor(private myService: MyService) {}
}

// AFTER
import { Component, inject } from "@angular/core";
import { MyService } from "./my.service";

@Component({ selector: "my-cmp", template: "" })
export class MyComponent {
private myService = inject(MyService);
}
```

---

## Multiple deps, empty constructor

```ts
// BEFORE
constructor(
private serviceA: ServiceA,
private serviceB: ServiceB,
private serviceC: ServiceC,
) {}

// AFTER (constructor removed entirely)
private serviceA = inject(ServiceA);
private serviceB = inject(ServiceB);
private serviceC = inject(ServiceC);
```

---

## Constructor with body code

```ts
// BEFORE
constructor(private router: Router) {
this.router.navigate(["/home"]);
}

// AFTER (constructor body stays, param removed)
private router = inject(Router);

constructor() {
this.router.navigate(["/home"]);
}
```

---

## `readonly` modifier

```ts
// BEFORE
constructor(private readonly svc: MyService) {}

// AFTER
private readonly svc = inject(MyService);
```

---

## `public` or `protected` modifier (template or subclass access)

```ts
// BEFORE
constructor(public changeDetector: ChangeDetectorRef) {}

// AFTER
public changeDetector = inject(ChangeDetectorRef);
```

---

## Mix of injected and non-injected constructor params

Non-injected params (no access modifier) are **not** migrated — they stay in the constructor signature.

```ts
// BEFORE
constructor(private svc: SvcClass, config: SomeConfig) {
this.value = config.value;
}

// AFTER (only access-modifier params move to inject())
private svc = inject(SvcClass);

constructor(config: SomeConfig) {
this.value = config.value;
}
```

---

## Class with `super()` call

```ts
// BEFORE
constructor(private svc: SvcClass) {
super();
}

// AFTER (constructor kept only for super())
private svc = inject(SvcClass);

constructor() {
super();
}
```

If the super call passes injected deps, those must become field references:

```ts
// BEFORE
constructor(private http: HttpClient) {
super(http);
}

// AFTER
private http = inject(HttpClient);

constructor() {
super(this.http); // <-- update reference
}
```

---

## `inject` already imported

Only add `inject` once. Find the existing `@angular/core` named import and append it:

```ts
// BEFORE
import { Component, OnInit } from "@angular/core";

// AFTER
import { Component, OnInit, inject } from "@angular/core";
```

---

## `inject` already present

No change to the import line needed.

---

## Edge case: `@Inject(TOKEN)` decorator — out of scope

This skill covers **only** TypeScript access-modifier params (`private|public|protected`).
`@Inject(TOKEN)` params require a separate migration (`inject(TOKEN)` with optional flags) and are handled by the `@Inject`-migration skill.

---

## Ordering of injected fields

Place `inject()` fields in the same order the constructor params appeared, directly at the top of the class body (after any class-level decorators but before other fields), to maintain readability.

If the class already has fields, place the new `inject()` fields **before** the first non-inject field, unless a different convention is established in the file.
Loading
Loading