@pippenly/ts-utils 1.0.1 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/dom/resin.ts +3 -0
- package/src/index.ts +2 -0
- package/src/scope/index.ts +1 -0
- package/src/scope/scope.ts +112 -0
package/package.json
CHANGED
package/src/dom/resin.ts
CHANGED
|
@@ -13,6 +13,7 @@ type BoundResin<T, E extends HTMLElement> = Resin<T>
|
|
|
13
13
|
|
|
14
14
|
type BindingOptions<T, E extends HTMLElement, Mapped = T> = {
|
|
15
15
|
bindTo?: 'innerText' | 'textContent' | 'innerHTML' | 'value';
|
|
16
|
+
stringParse?: 'naive' | 'sanitized'; // try to prevent XSS when using bindTo with innerHTML
|
|
16
17
|
map?: (value: T) => Mapped;
|
|
17
18
|
tap?: (value: Mapped, element: E) => void;
|
|
18
19
|
if?: (value: Mapped) => boolean;
|
|
@@ -128,6 +129,8 @@ export function bind<T, E extends HTMLElement>(resin: Resin<T>, element: E, opti
|
|
|
128
129
|
element.textContent = String(mappedValue);
|
|
129
130
|
break;
|
|
130
131
|
case "innerHTML":
|
|
132
|
+
console.warn("Using bindTo 'innerHTML' can lead to XSS vulnerabilities if the value is not properly sanitized.");
|
|
133
|
+
// TODO: Add sanitizer dependency
|
|
131
134
|
element.innerHTML = String(mappedValue);
|
|
132
135
|
break;
|
|
133
136
|
case "value":
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './scope.js';
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { type Result, Ok, Err } from "@/result/index.js";
|
|
2
|
+
|
|
3
|
+
type Brand<K> = {
|
|
4
|
+
__brand: K;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
type SimpleError = {
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type Ctor<T> = new (...args: any[]) => T;
|
|
12
|
+
|
|
13
|
+
export type InjectorError = SimpleError & Brand<"InjectorError">;
|
|
14
|
+
|
|
15
|
+
export type Scope<TDeps extends readonly Ctor<any>[]> = {
|
|
16
|
+
provide: <C extends TDeps[number]>(ctor: C, instance: InstanceType<C>) => Result<Scope<TDeps>, InjectorError>;
|
|
17
|
+
run: <R>(fn: (get: <C extends TDeps[number]>(ctor: C) => Result<InstanceType<C>, InjectorError>) => R) => R;
|
|
18
|
+
finalize: <R>(fn: (get: <C extends TDeps[number]>(ctor: C) => Result<InstanceType<C>, InjectorError>) => R) => R;
|
|
19
|
+
[Symbol.dispose]: () => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type AsyncScope<TDeps extends readonly Ctor<any>[]> = {
|
|
23
|
+
provide: <C extends TDeps[number]>(ctor: C, instance: InstanceType<C>) => Result<AsyncScope<TDeps>, InjectorError>;
|
|
24
|
+
run: <R>(fn: (get: <C extends TDeps[number]>(ctor: C) => Result<InstanceType<C>, InjectorError>) => Promise<R>) => Promise<R>;
|
|
25
|
+
finalize: <R>(fn: (get: <C extends TDeps[number]>(ctor: C) => Result<InstanceType<C>, InjectorError>) => Promise<R>) => Promise<R>;
|
|
26
|
+
[Symbol.asyncDispose]: () => Promise<void>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function scope<TDeps extends ReadonlyArray<Ctor<any>>>(...deps: TDeps): Scope<TDeps> {
|
|
30
|
+
return createScope(deps, new Map());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function asyncScope<TDeps extends ReadonlyArray<Ctor<any>>>(...deps: TDeps): AsyncScope<TDeps> {
|
|
34
|
+
return createAsyncScope(deps, new Map());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createScope<TDeps extends ReadonlyArray<Ctor<any>>>(
|
|
38
|
+
deps: TDeps,
|
|
39
|
+
instances: Map<Ctor<any>, any>,
|
|
40
|
+
): Scope<TDeps> {
|
|
41
|
+
const find = <C extends TDeps[number]>(ctor: C): Result<InstanceType<C>, InjectorError> => {
|
|
42
|
+
if (!deps.includes(ctor)) {
|
|
43
|
+
return Err(taggedError("InjectorError", `Dependency ${ctor.name} is not registered in this scope.`));
|
|
44
|
+
}
|
|
45
|
+
if (!instances.has(ctor)) {
|
|
46
|
+
return Err(taggedError("InjectorError", `Dependency ${ctor.name} has not been provided.`));
|
|
47
|
+
}
|
|
48
|
+
return Ok(instances.get(ctor)!);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
provide<C extends TDeps[number]>(ctor: C, instance: InstanceType<C>): Result<Scope<TDeps>, InjectorError> {
|
|
53
|
+
if (!deps.includes(ctor)) {
|
|
54
|
+
return Err(taggedError("InjectorError", `Cannot provide instance for ${ctor.name}: not registered in this scope.`));
|
|
55
|
+
}
|
|
56
|
+
const next = new Map(instances);
|
|
57
|
+
next.set(ctor, instance);
|
|
58
|
+
return Ok(createScope(deps, next));
|
|
59
|
+
},
|
|
60
|
+
run<R>(fn: (get: <C extends TDeps[number]>(ctor: C) => Result<InstanceType<C>, InjectorError>) => R): R {
|
|
61
|
+
return fn(find);
|
|
62
|
+
},
|
|
63
|
+
finalize<R>(fn: (get: <C extends TDeps[number]>(ctor: C) => Result<InstanceType<C>, InjectorError>) => R): R {
|
|
64
|
+
return fn(find);
|
|
65
|
+
},
|
|
66
|
+
[Symbol.dispose]() {
|
|
67
|
+
instances.clear();
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function createAsyncScope<TDeps extends ReadonlyArray<Ctor<any>>>(
|
|
73
|
+
deps: TDeps,
|
|
74
|
+
instances: Map<Ctor<any>, any>,
|
|
75
|
+
): AsyncScope<TDeps> {
|
|
76
|
+
const find = <C extends TDeps[number]>(ctor: C): Result<InstanceType<C>, InjectorError> => {
|
|
77
|
+
if (!deps.includes(ctor)) {
|
|
78
|
+
return Err(taggedError("InjectorError", `Dependency ${ctor.name} is not registered in this scope.`));
|
|
79
|
+
}
|
|
80
|
+
if (!instances.has(ctor)) {
|
|
81
|
+
return Err(taggedError("InjectorError", `Dependency ${ctor.name} has not been provided.`));
|
|
82
|
+
}
|
|
83
|
+
return Ok(instances.get(ctor)!);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
provide<C extends TDeps[number]>(ctor: C, instance: InstanceType<C>): Result<AsyncScope<TDeps>, InjectorError> {
|
|
88
|
+
if (!deps.includes(ctor)) {
|
|
89
|
+
return Err(taggedError("InjectorError", `Cannot provide instance for ${ctor.name}: not registered in this scope.`));
|
|
90
|
+
}
|
|
91
|
+
const next = new Map(instances);
|
|
92
|
+
next.set(ctor, instance);
|
|
93
|
+
return Ok(createAsyncScope(deps, next));
|
|
94
|
+
},
|
|
95
|
+
async run<R>(fn: (get: <C extends TDeps[number]>(ctor: C) => Result<InstanceType<C>, InjectorError>) => Promise<R>): Promise<R> {
|
|
96
|
+
return fn(find);
|
|
97
|
+
},
|
|
98
|
+
async finalize<R>(fn: (get: <C extends TDeps[number]>(ctor: C) => Result<InstanceType<C>, InjectorError>) => Promise<R>): Promise<R> {
|
|
99
|
+
return fn(find);
|
|
100
|
+
},
|
|
101
|
+
async [Symbol.asyncDispose]() {
|
|
102
|
+
instances.clear();
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function taggedError<K extends string>(brand: K, message: string): Brand<K> & SimpleError {
|
|
108
|
+
return {
|
|
109
|
+
__brand: brand,
|
|
110
|
+
message,
|
|
111
|
+
};
|
|
112
|
+
}
|