Repository: angular
Issue: Provide a way to obtain the injector outside a class constructor for meta-programming
Labels: area: core,core: di,P3

### Which @angular/* package(s) are relevant/related to the feature request?

core

### Description

Meta-programming for components, directives and services is difficult and limited by the Angular compiler. 

- It is safe to do some transformations, such as adding lifecycle hooks to the prototype.
- It is not safe to transform or intercept the class constructor by extending the base class or using a proxy.
- Angular decorators behave differently in unit tests where they are JIT compiled, causing unexpected behaviours. The workarounds for which are non-trivial.

Intercepting the constructor call is useful for:

- Saving a reference to the class instance
- Obtaining a reference to the node/module injector via `inject`
- Running initialization logic when the class is created

Library authors in the Angular ecosystem are currently limited to using private Angular APIs to work around this, against the advice of Angular team. 

A few examples in the wild:
- [Microwave](https://github.com/jscutlery/devkit/blob/6e29efd2ca34336c21b5a3088dd35df8baeec6c8/packages/microwave/src/lib/shared/decorator.ts#L165)
- [NGXS](https://github.com/ngxs/store/blob/050bc58ab5481cafdeb5cfdc365b4e58515e48ee/packages/store/internals/src/decorator-injector-adapter.ts) (was reverted in later versions due to conflicts)
- [My own project](https://github.com/antischematic/angular-state-library/blob/de2a4ce2f777260076ff61450831fea2eb93eac5/projects/core/src/core.ts#L105)

### Proposed solution

Angular should recognize valid use cases for meta-programming and provide a public API for library authors to use. All solutions here are intended to work within existing AOT compilation constraints.

**Solution 1: add a ngOnCreate lifecycle hook**

```ts
function setup(instance, injector) {
  // run initialization logic
}

@Component()
export class AppComponent {
  ngOnCreate() {
    setup(this, inject(INJECTOR))
  }
}
```

The `ngOnCreate` hook would run directly after invoking the injected class constructor. It runs in the same injection context as the constructor making it possible to use `inject`. Decorators can then attach this hook to the prototype.

**Solution 2: add `useFactory` metadata**

```ts
function setup() {
  const injector = inject(INJECTOR)
  const instance = new AppComponent()
  // run initialization logic
  return instance
}

@Component({
  useFactory: setup
})
export class AppComponent {}
```

This aligns with `Injectable`, allowing us to override the component factory.

**Solution 3: add features metadata**

```ts
function setup(componentDef) {
  const { factory } = componentDef;

  componentDef.factory = (type) => {
    const instance = factory(type);
    const injector = inject(INJECTOR);

    // run initialization logic

    return instance;
  };
}

@Component({
  features: [setup]
})
export class AppComponent {}
```

Make the compiled class definition a public API that can be modified at runtime.

### Alternatives considered

**Alternative 1: Override the class factory**

```ts
function CustomDecorator() {
  return function (target) {
    const factory = target['ɵfac']
    target['ɵfac'] = function (...args) {
      const injector = inject(INJECTOR)
      const instance = factory(...args)
      return instance 
    }
  }
}

@CustomDecorator()
@Component()
export class AppComponent {}
```

The approach is to decorate the `ɵfac` property which is a private Angular API. This needs special handling to account for different behaviours in JIT and AOT compilation modes.

**Alternative 2: Access the constructor with a base class**

```ts
@Component()
export class AppComponent extends BaseComponent {}
```

But this isn't ideal since the base constructor is called before all properties have initialized.

**Alternative 3: Explicitly pass in initialization logic through the constructor**

```ts
function setup(instance, injector) {
  // decorator initialization logic
}

@CustomDecorator()
@Component()
export class AppComponent {
  constructor() {
    // class initialization logic
    setup(this, inject(INJECTOR))
  }
}
```

But this defeats the purpose. It also requires consumers to place the call at the end of the constructor after all other initialization is done. Decorators should be self-contained.

**Alternative 4: Don't use decorators**

🙁