mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 13:23:34 +00:00
[PM-17031] Create UI-common (#12831)
Extract core functionality from `libs/angular` to allow teams to depend on `libs/ui-common` instead. Moves the following functionality to `ui-common`. - `I18nPipe`. `libs/angular` still has an old copy but `components` depends on the new variant from `ui-common`. - `safeProvider`, `SafeProvider` and `SafeInjectionToken`. `libs/angular`re-exports these to avoid needing to update all consumers.
This commit is contained in:
15
libs/ui/common/package.json
Normal file
15
libs/ui/common/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@bitwarden/ui-common",
|
||||
"version": "0.0.0",
|
||||
"description": "Low-level utilities for Angular applications",
|
||||
"keywords": [
|
||||
"bitwarden"
|
||||
],
|
||||
"author": "Bitwarden Inc.",
|
||||
"homepage": "https://bitwarden.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/bitwarden/clients"
|
||||
},
|
||||
"license": "GPL-3.0"
|
||||
}
|
||||
2
libs/ui/common/src/di/index.ts
Normal file
2
libs/ui/common/src/di/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./safe-injection-token";
|
||||
export * from "./safe-provider";
|
||||
14
libs/ui/common/src/di/safe-injection-token.ts
Normal file
14
libs/ui/common/src/di/safe-injection-token.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { InjectionToken } from "@angular/core";
|
||||
|
||||
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;
|
||||
}
|
||||
138
libs/ui/common/src/di/safe-provider.ts
Normal file
138
libs/ui/common/src/di/safe-provider.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { Provider } from "@angular/core";
|
||||
import { Constructor, Opaque } from "type-fest";
|
||||
|
||||
import { SafeInjectionToken } from "./safe-injection-token";
|
||||
|
||||
/**
|
||||
* The return type of the {@link safeProvider} helper function.
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Gets the instance type from a constructor, abstract constructor, or SafeInjectionToken
|
||||
*/
|
||||
type ProviderInstanceType<T> =
|
||||
T extends SafeInjectionToken<any>
|
||||
? InstanceType<SafeInjectionTokenType<T>>
|
||||
: T extends Constructor<any> | AbstractConstructor<any>
|
||||
? InstanceType<T>
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Represents a dependency provided with the useClass option.
|
||||
*/
|
||||
type SafeClassProvider<
|
||||
A extends AbstractConstructor<any> | SafeInjectionToken<any>,
|
||||
I extends Constructor<ProviderInstanceType<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.
|
||||
*/
|
||||
type SafeFactoryProvider<
|
||||
A extends AbstractConstructor<any> | SafeInjectionToken<any>,
|
||||
I extends (...args: any) => ProviderInstanceType<A>,
|
||||
D extends MapParametersToDeps<Parameters<I>>,
|
||||
> = {
|
||||
provide: A;
|
||||
useFactory: I;
|
||||
deps: D;
|
||||
multi?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a dependency provided with the useExisting option.
|
||||
*/
|
||||
type SafeExistingProvider<
|
||||
A extends Constructor<any> | AbstractConstructor<any> | SafeInjectionToken<any>,
|
||||
I extends Constructor<ProviderInstanceType<A>> | AbstractConstructor<ProviderInstanceType<A>>,
|
||||
> = {
|
||||
provide: A;
|
||||
useExisting: I;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a dependency where there is no abstract token, the token is the implementation
|
||||
*/
|
||||
type SafeConcreteProvider<
|
||||
I extends Constructor<any>,
|
||||
D extends MapParametersToDeps<ConstructorParameters<I>>,
|
||||
> = {
|
||||
provide: I;
|
||||
deps: D;
|
||||
};
|
||||
|
||||
/**
|
||||
* If useAngularDecorators: true is specified, do not require a deps array.
|
||||
* This is a manual override for where @Injectable decorators are used
|
||||
*/
|
||||
type UseAngularDecorators<T extends { deps: any }> = Omit<T, "deps"> & {
|
||||
useAngularDecorators: true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a type with a deps array that may optionally be overridden with useAngularDecorators
|
||||
*/
|
||||
type AllowAngularDecorators<T extends { deps: any }> = T | UseAngularDecorators<T>;
|
||||
|
||||
/**
|
||||
* A factory function that creates a provider for the ngModule providers array.
|
||||
* This (almost) guarantees type safety for your provider definition. It does nothing at runtime.
|
||||
* Warning: the useAngularDecorators option provides an override where your class uses the Injectable decorator,
|
||||
* however this cannot be enforced by the type system and will not cause an error if the decorator is not used.
|
||||
* @example safeProvider({ provide: MyService, useClass: DefaultMyService, deps: [AnotherService] })
|
||||
* @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> | SafeInjectionToken<any>,
|
||||
IClass extends Constructor<ProviderInstanceType<AClass>>,
|
||||
DClass extends MapParametersToDeps<ConstructorParameters<IClass>>,
|
||||
// types for useValue
|
||||
AValue extends SafeInjectionToken<any>,
|
||||
VValue extends SafeInjectionTokenType<AValue>,
|
||||
// types for useFactory
|
||||
AFactory extends AbstractConstructor<any> | SafeInjectionToken<any>,
|
||||
IFactory extends (...args: any) => ProviderInstanceType<AFactory>,
|
||||
DFactory extends MapParametersToDeps<Parameters<IFactory>>,
|
||||
// types for useExisting
|
||||
AExisting extends Constructor<any> | AbstractConstructor<any> | SafeInjectionToken<any>,
|
||||
IExisting extends
|
||||
| Constructor<ProviderInstanceType<AExisting>>
|
||||
| AbstractConstructor<ProviderInstanceType<AExisting>>,
|
||||
// types for no token
|
||||
IConcrete extends Constructor<any>,
|
||||
DConcrete extends MapParametersToDeps<ConstructorParameters<IConcrete>>,
|
||||
>(
|
||||
provider:
|
||||
| AllowAngularDecorators<SafeClassProvider<AClass, IClass, DClass>>
|
||||
| SafeValueProvider<AValue, VValue>
|
||||
| AllowAngularDecorators<SafeFactoryProvider<AFactory, IFactory, DFactory>>
|
||||
| SafeExistingProvider<AExisting, IExisting>
|
||||
| AllowAngularDecorators<SafeConcreteProvider<IConcrete, DConcrete>>
|
||||
| Constructor<unknown>,
|
||||
): SafeProvider => provider as SafeProvider;
|
||||
111
libs/ui/common/src/di/safe-provider.type.spec.ts
Normal file
111
libs/ui/common/src/di/safe-provider.type.spec.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
// This rule bans @ts-expect-error comments without explanation. In this file, we use it to test our types, and
|
||||
// explanation is provided in header comments before each test.
|
||||
|
||||
import { safeProvider } from "./safe-provider";
|
||||
|
||||
class FooFactory {
|
||||
create() {
|
||||
return "thing";
|
||||
}
|
||||
}
|
||||
|
||||
abstract class FooService {
|
||||
abstract createFoo(str: string): string;
|
||||
}
|
||||
|
||||
class DefaultFooService implements FooService {
|
||||
constructor(private factory: FooFactory) {}
|
||||
|
||||
createFoo(str: string) {
|
||||
return str ?? this.factory.create();
|
||||
}
|
||||
}
|
||||
|
||||
class BarFactory {
|
||||
create() {
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class BarService {
|
||||
abstract createBar(num: number): number;
|
||||
}
|
||||
|
||||
class DefaultBarService implements BarService {
|
||||
constructor(private factory: BarFactory) {}
|
||||
|
||||
createBar(num: number) {
|
||||
return num ?? this.factory.create();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class FooBarService {}
|
||||
|
||||
class DefaultFooBarService {
|
||||
constructor(
|
||||
private fooFactory: FooFactory,
|
||||
private barFactory: BarFactory,
|
||||
) {}
|
||||
}
|
||||
|
||||
// useClass happy path with deps
|
||||
safeProvider({
|
||||
provide: FooService,
|
||||
useClass: DefaultFooService,
|
||||
deps: [FooFactory],
|
||||
});
|
||||
|
||||
// useClass happy path with useAngularDecorators
|
||||
safeProvider({
|
||||
provide: FooService,
|
||||
useClass: DefaultFooService,
|
||||
useAngularDecorators: true,
|
||||
});
|
||||
|
||||
// useClass: expect error if implementation does not match abstraction
|
||||
safeProvider({
|
||||
provide: FooService,
|
||||
// @ts-expect-error
|
||||
useClass: DefaultBarService,
|
||||
deps: [BarFactory],
|
||||
});
|
||||
|
||||
// useClass: expect error if deps type does not match
|
||||
safeProvider({
|
||||
provide: FooService,
|
||||
useClass: DefaultFooService,
|
||||
// @ts-expect-error
|
||||
deps: [BarFactory],
|
||||
});
|
||||
|
||||
// useClass: expect error if not enough deps specified
|
||||
safeProvider({
|
||||
provide: FooService,
|
||||
useClass: DefaultFooService,
|
||||
// @ts-expect-error
|
||||
deps: [],
|
||||
});
|
||||
|
||||
// useClass: expect error if too many deps specified
|
||||
safeProvider({
|
||||
provide: FooService,
|
||||
useClass: DefaultFooService,
|
||||
// @ts-expect-error
|
||||
deps: [FooFactory, BarFactory],
|
||||
});
|
||||
|
||||
// useClass: expect error if deps are in the wrong order
|
||||
safeProvider({
|
||||
provide: FooBarService,
|
||||
useClass: DefaultFooBarService,
|
||||
// @ts-expect-error
|
||||
deps: [BarFactory, FooFactory],
|
||||
});
|
||||
|
||||
// useClass: expect error if no deps specified and not using Angular decorators
|
||||
// @ts-expect-error
|
||||
safeProvider({
|
||||
provide: FooService,
|
||||
useClass: DefaultFooService,
|
||||
});
|
||||
24
libs/ui/common/src/i18n.pipe.ts
Normal file
24
libs/ui/common/src/i18n.pipe.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Pipe, PipeTransform } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
/**
|
||||
* Localizes the specified string.
|
||||
*
|
||||
* @example
|
||||
* {{ 'key' | i18n }}
|
||||
*
|
||||
* @example
|
||||
* {{ 'key' | i18n: 'param1' }}
|
||||
*/
|
||||
@Pipe({
|
||||
name: "i18n",
|
||||
standalone: true,
|
||||
})
|
||||
export class I18nPipe implements PipeTransform {
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
transform(id: string, p1?: string, p2?: string, p3?: string): string {
|
||||
return this.i18nService.t(id, p1, p2, p3);
|
||||
}
|
||||
}
|
||||
2
libs/ui/common/src/index.ts
Normal file
2
libs/ui/common/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./di";
|
||||
export * from "./i18n.pipe";
|
||||
10
libs/ui/common/tsconfig.json
Normal file
10
libs/ui/common/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../shared/tsconfig",
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@bitwarden/common/*": ["../../common/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user