mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-21 16:18:28 +01:00
[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:
parent
8f8385d822
commit
29b5643310
@ -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();
|
||||
|
@ -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: [
|
||||
|
@ -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
|
||||
|
114
libs/angular/src/platform/utils/safe-provider.ts
Normal file
114
libs/angular/src/platform/utils/safe-provider.ts
Normal 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;
|
@ -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
Loading…
Reference in New Issue
Block a user