[AC-2278] [BEEEP] Typesafe Angular DI (#8206)

Use SafeProvider as a factory for all our providers to ensure
that the DI token, implementation, and deps all match.

---------

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
This commit is contained in:
Thomas Rittson 2024-03-13 09:21:57 +10:00 committed by GitHub
parent 8f8385d822
commit 29b5643310
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 928 additions and 784 deletions

View File

@ -1,9 +1,6 @@
import { Injectable } from "@angular/core";
import { ThemingService } from "@bitwarden/angular/platform/services/theming/theming.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
@Injectable()
export class DesktopThemingService extends ThemingService {
protected async getSystemTheme(): Promise<ThemeType> {
return await ipc.platform.getSystemTheme();

View File

@ -1,6 +1,8 @@
import { DOCUMENT } from "@angular/common";
import { APP_INITIALIZER, InjectionToken, NgModule } from "@angular/core";
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
import {
SECURE_STORAGE,
STATE_FACTORY,
@ -11,6 +13,7 @@ import {
OBSERVABLE_MEMORY_STORAGE,
OBSERVABLE_DISK_STORAGE,
WINDOW,
SafeInjectionToken,
} from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
@ -148,10 +151,11 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
provide: FileDownloadService,
useClass: DesktopFileDownloadService,
},
{
safeProvider({
provide: AbstractThemingService,
useClass: DesktopThemingService,
},
deps: [StateServiceAbstraction, WINDOW, DOCUMENT as SafeInjectionToken<Document>],
}),
{
provide: EncryptedMessageHandlerService,
deps: [

View File

@ -1,25 +1,20 @@
import { DOCUMENT } from "@angular/common";
import { Inject, Injectable } from "@angular/core";
import { BehaviorSubject, filter, fromEvent, Observable } from "rxjs";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
import { WINDOW } from "../../../services/injection-tokens";
import { Theme } from "./theme";
import { ThemeBuilder } from "./theme-builder";
import { AbstractThemingService } from "./theming.service.abstraction";
@Injectable()
export class ThemingService implements AbstractThemingService {
private _theme = new BehaviorSubject<ThemeBuilder | null>(null);
theme$: Observable<Theme> = this._theme.pipe(filter((x) => x !== null));
constructor(
private stateService: StateService,
@Inject(WINDOW) private window: Window,
@Inject(DOCUMENT) private document: Document,
private window: Window,
private document: Document,
) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises

View File

@ -0,0 +1,114 @@
import { Provider } from "@angular/core";
import { Constructor, Opaque } from "type-fest";
import { SafeInjectionToken } from "../../services/injection-tokens";
/**
* The return type of our dependency helper functions.
* Used to distinguish a type safe provider definition from a non-type safe provider definition.
*/
export type SafeProvider = Opaque<Provider>;
// TODO: type-fest also provides a type like this when we upgrade >= 3.7.0
type AbstractConstructor<T> = abstract new (...args: any) => T;
type MapParametersToDeps<T> = {
[K in keyof T]: AbstractConstructor<T[K]> | SafeInjectionToken<T[K]>;
};
type SafeInjectionTokenType<T> = T extends SafeInjectionToken<infer J> ? J : never;
/**
* Represents a dependency provided with the useClass option.
*/
type SafeClassProvider<
A extends AbstractConstructor<any>,
I extends Constructor<InstanceType<A>>,
D extends MapParametersToDeps<ConstructorParameters<I>>,
> = {
provide: A;
useClass: I;
deps: D;
};
/**
* Represents a dependency provided with the useValue option.
*/
type SafeValueProvider<A extends SafeInjectionToken<any>, V extends SafeInjectionTokenType<A>> = {
provide: A;
useValue: V;
};
/**
* Represents a dependency provided with the useFactory option where a SafeInjectionToken is used as the token.
*/
type SafeFactoryProviderWithToken<
A extends SafeInjectionToken<any>,
I extends (...args: any) => InstanceType<SafeInjectionTokenType<A>>,
D extends MapParametersToDeps<Parameters<I>>,
> = {
provide: A;
useFactory: I;
deps: D;
};
/**
* Represents a dependency provided with the useFactory option where an abstract class is used as the token.
*/
type SafeFactoryProviderWithClass<
A extends AbstractConstructor<any>,
I extends (...args: any) => InstanceType<A>,
D extends MapParametersToDeps<Parameters<I>>,
> = {
provide: A;
useFactory: I;
deps: D;
};
/**
* Represents a dependency provided with the useExisting option.
*/
type SafeExistingProvider<
A extends Constructor<any> | AbstractConstructor<any>,
I extends Constructor<InstanceType<A>> | AbstractConstructor<InstanceType<A>>,
> = {
provide: A;
useExisting: I;
};
/**
* A factory function that creates a provider for the ngModule providers array.
* This guarantees type safety for your provider definition. It does nothing at runtime.
* @param provider Your provider object in the usual shape (e.g. using useClass, useValue, useFactory, etc.)
* @returns The exact same object without modification (pass-through).
*/
export const safeProvider = <
// types for useClass
AClass extends AbstractConstructor<any>,
IClass extends Constructor<InstanceType<AClass>>,
DClass extends MapParametersToDeps<ConstructorParameters<IClass>>,
// types for useValue
AValue extends SafeInjectionToken<any>,
VValue extends SafeInjectionTokenType<AValue>,
// types for useFactoryWithToken
AFactoryToken extends SafeInjectionToken<any>,
IFactoryToken extends (...args: any) => InstanceType<SafeInjectionTokenType<AFactoryToken>>,
DFactoryToken extends MapParametersToDeps<Parameters<IFactoryToken>>,
// types for useFactoryWithClass
AFactoryClass extends AbstractConstructor<any>,
IFactoryClass extends (...args: any) => InstanceType<AFactoryClass>,
DFactoryClass extends MapParametersToDeps<Parameters<IFactoryClass>>,
// types for useExisting
AExisting extends Constructor<any> | AbstractConstructor<any>,
IExisting extends
| Constructor<InstanceType<AExisting>>
| AbstractConstructor<InstanceType<AExisting>>,
>(
provider:
| SafeClassProvider<AClass, IClass, DClass>
| SafeValueProvider<AValue, VValue>
| SafeFactoryProviderWithToken<AFactoryToken, IFactoryToken, DFactoryToken>
| SafeFactoryProviderWithClass<AFactoryClass, IFactoryClass, DFactoryClass>
| SafeExistingProvider<AExisting, IExisting>
| Constructor<unknown>,
): SafeProvider => provider as SafeProvider;

View File

@ -7,26 +7,39 @@ import {
} from "@bitwarden/common/platform/abstractions/storage.service";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
export const WINDOW = new InjectionToken<Window>("WINDOW");
export const OBSERVABLE_MEMORY_STORAGE = new InjectionToken<
declare const tag: unique symbol;
/**
* A (more) typesafe version of InjectionToken which will more strictly enforce the generic type parameter.
* @remarks The default angular implementation does not use the generic type to define the structure of the object,
* so the structural type system will not complain about a mismatch in the type parameter.
* This is solved by assigning T to an arbitrary private property.
*/
export class SafeInjectionToken<T> extends InjectionToken<T> {
private readonly [tag]: T;
}
export const WINDOW = new SafeInjectionToken<Window>("WINDOW");
export const OBSERVABLE_MEMORY_STORAGE = new SafeInjectionToken<
AbstractMemoryStorageService & ObservableStorageService
>("OBSERVABLE_MEMORY_STORAGE");
export const OBSERVABLE_DISK_STORAGE = new InjectionToken<
export const OBSERVABLE_DISK_STORAGE = new SafeInjectionToken<
AbstractStorageService & ObservableStorageService
>("OBSERVABLE_DISK_STORAGE");
export const OBSERVABLE_DISK_LOCAL_STORAGE = new InjectionToken<
export const OBSERVABLE_DISK_LOCAL_STORAGE = new SafeInjectionToken<
AbstractStorageService & ObservableStorageService
>("OBSERVABLE_DISK_LOCAL_STORAGE");
export const MEMORY_STORAGE = new InjectionToken<AbstractMemoryStorageService>("MEMORY_STORAGE");
export const SECURE_STORAGE = new InjectionToken<AbstractStorageService>("SECURE_STORAGE");
export const STATE_FACTORY = new InjectionToken<StateFactory>("STATE_FACTORY");
export const STATE_SERVICE_USE_CACHE = new InjectionToken<boolean>("STATE_SERVICE_USE_CACHE");
export const LOGOUT_CALLBACK = new InjectionToken<
export const MEMORY_STORAGE = new SafeInjectionToken<AbstractMemoryStorageService>(
"MEMORY_STORAGE",
);
export const SECURE_STORAGE = new SafeInjectionToken<AbstractStorageService>("SECURE_STORAGE");
export const STATE_FACTORY = new SafeInjectionToken<StateFactory>("STATE_FACTORY");
export const STATE_SERVICE_USE_CACHE = new SafeInjectionToken<boolean>("STATE_SERVICE_USE_CACHE");
export const LOGOUT_CALLBACK = new SafeInjectionToken<
(expired: boolean, userId?: string) => Promise<void>
>("LOGOUT_CALLBACK");
export const LOCKED_CALLBACK = new InjectionToken<(userId?: string) => Promise<void>>(
export const LOCKED_CALLBACK = new SafeInjectionToken<(userId?: string) => Promise<void>>(
"LOCKED_CALLBACK",
);
export const LOCALES_DIRECTORY = new InjectionToken<string>("LOCALES_DIRECTORY");
export const SYSTEM_LANGUAGE = new InjectionToken<string>("SYSTEM_LANGUAGE");
export const LOG_MAC_FAILURES = new InjectionToken<string>("LOG_MAC_FAILURES");
export const LOCALES_DIRECTORY = new SafeInjectionToken<string>("LOCALES_DIRECTORY");
export const SYSTEM_LANGUAGE = new SafeInjectionToken<string>("SYSTEM_LANGUAGE");
export const LOG_MAC_FAILURES = new SafeInjectionToken<boolean>("LOG_MAC_FAILURES");

File diff suppressed because it is too large Load Diff