1
0
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:
Oscar Hinton
2025-01-17 16:42:31 +01:00
committed by GitHub
parent 87171289f0
commit e5f83ff086
52 changed files with 300 additions and 247 deletions

View 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"
}

View File

@@ -0,0 +1,2 @@
export * from "./safe-injection-token";
export * from "./safe-provider";

View 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;
}

View 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;

View 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,
});

View 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);
}
}

View File

@@ -0,0 +1,2 @@
export * from "./di";
export * from "./i18n.pipe";

View File

@@ -0,0 +1,10 @@
{
"extends": "../../shared/tsconfig",
"compilerOptions": {
"paths": {
"@bitwarden/common/*": ["../../common/src/*"]
}
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}