mini-effect 0.0.0
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/README.md +190 -0
- package/dist/concurrency.d.mts +9 -0
- package/dist/concurrency.mjs +24 -0
- package/dist/mini-effect.d.mts +37 -0
- package/dist/mini-effect.mjs +75 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# 🎯 mini-effect
|
|
2
|
+
|
|
3
|
+
> _Because sometimes you want Effect, but like... mini._
|
|
4
|
+
|
|
5
|
+
A delightfully tiny Effect system for TypeScript that packs a punch! ✨
|
|
6
|
+
|
|
7
|
+
## 🤔 What's This?
|
|
8
|
+
|
|
9
|
+
Ever looked at [Effect-TS](https://effect.website/) and thought "wow, that's amazing but also... a lot"? Same.
|
|
10
|
+
|
|
11
|
+
**mini-effect** gives you the good stuff:
|
|
12
|
+
|
|
13
|
+
- 🦥 **Lazy evaluation** - Nothing runs until you say so
|
|
14
|
+
- 🔗 **Pipes** - Compose like a functional programming wizard
|
|
15
|
+
- 🎭 **Generators** - Write async code that doesn't look like callback spaghetti
|
|
16
|
+
- 🎣 **Error handling** - Catch 'em all (or just some of 'em)
|
|
17
|
+
- ⚡ **Concurrency** - `all`, `race`, `any`, `allSettled` - the gang's all here
|
|
18
|
+
- 🛑 **AbortSignal support** - Cancel everything! EVERYTHING!
|
|
19
|
+
|
|
20
|
+
All in a package so small it practically fits in a tweet. 🐦
|
|
21
|
+
|
|
22
|
+
## 📦 Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install mini-effect
|
|
26
|
+
# or
|
|
27
|
+
pnpm add mini-effect
|
|
28
|
+
# or
|
|
29
|
+
yarn add mini-effect
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 🚀 Quick Start
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { fn, run, succeed, fail, gen, pipe } from "mini-effect";
|
|
36
|
+
import { all } from "mini-effect/concurrency";
|
|
37
|
+
|
|
38
|
+
// Create an effect (it's lazy - nothing happens yet!)
|
|
39
|
+
const fetchTodo = fn(async (signal) => {
|
|
40
|
+
const res = await fetch("https://jsonplaceholder.typicode.com/todos/1", {
|
|
41
|
+
signal,
|
|
42
|
+
});
|
|
43
|
+
return res.json();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Run it! 🏃♂️
|
|
47
|
+
const todo = await run(fetchTodo);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 🎮 The Fun Parts
|
|
51
|
+
|
|
52
|
+
### Generators (a.k.a. "async/await but cooler")
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
const program = gen(function* (signal) {
|
|
56
|
+
const a = yield* fn(() => Promise.resolve(1));
|
|
57
|
+
const b = yield* fn(() => Promise.resolve(2));
|
|
58
|
+
const c = yield* succeed(3);
|
|
59
|
+
|
|
60
|
+
return a + b + c; // 6!
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await run(program); // 6
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Pipes (a.k.a. "look ma, no nesting!")
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
const result = await run(
|
|
70
|
+
pipe(
|
|
71
|
+
succeed(5),
|
|
72
|
+
(n: number) => succeed(n * 2), // 10
|
|
73
|
+
(n: number) => succeed(n + 1), // 11
|
|
74
|
+
(n: number) => succeed(`Result: ${n}`), // "Result: 11"
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Error Handling (a.k.a. "try/catch grew up")
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { catchClass, catchSome } from "mini-effect";
|
|
83
|
+
|
|
84
|
+
class NotFoundError extends Error {
|
|
85
|
+
constructor(public id: number) {
|
|
86
|
+
super(`Not found: ${id}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const safeProgram = pipe(
|
|
91
|
+
fn(() => {
|
|
92
|
+
throw new NotFoundError(42);
|
|
93
|
+
}),
|
|
94
|
+
catchClass(NotFoundError, (err) => succeed(`Handled: ${err.id}`)),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
await run(safeProgram); // "Handled: 42"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Concurrency (a.k.a. "Promise.all's cooler cousin")
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { all, race, any, allSettled } from "mini-effect/concurrency";
|
|
104
|
+
|
|
105
|
+
// Run 'em all at once!
|
|
106
|
+
const results = await run(
|
|
107
|
+
all([
|
|
108
|
+
fn(() => fetch("/api/users")),
|
|
109
|
+
fn(() => fetch("/api/posts")),
|
|
110
|
+
fn(() => fetch("/api/comments")),
|
|
111
|
+
]),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// First one wins! 🏆
|
|
115
|
+
const fastest = await run(
|
|
116
|
+
race([fn(() => fetchFromServer1()), fn(() => fetchFromServer2())]),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// First SUCCESS wins (failures don't count)
|
|
120
|
+
const firstSuccess = await run(any([fn(() => tryThis()), fn(() => tryThat())]));
|
|
121
|
+
|
|
122
|
+
// Get all results, even the failures 🤷
|
|
123
|
+
const allResults = await run(
|
|
124
|
+
allSettled([succeed(1), fail(new Error("oops")), succeed(3)]),
|
|
125
|
+
);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Cancellation (a.k.a. "STOP THE PRESSES!")
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
const controller = new AbortController();
|
|
132
|
+
|
|
133
|
+
const longRunning = fn(async (signal) => {
|
|
134
|
+
// Check if we should bail
|
|
135
|
+
if (signal.aborted) {
|
|
136
|
+
throw new DOMException("Aborted!", "AbortError");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Or pass it to fetch
|
|
140
|
+
const res = await fetch("/api/slow", { signal });
|
|
141
|
+
return res.json();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Start it
|
|
145
|
+
const promise = run(longRunning, controller.signal);
|
|
146
|
+
|
|
147
|
+
// Actually, never mind! 🙅
|
|
148
|
+
controller.abort();
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## 🗺️ API at a Glance
|
|
152
|
+
|
|
153
|
+
| Function | What it does |
|
|
154
|
+
| ---------------------------- | ----------------------------------- |
|
|
155
|
+
| `fn(thunk)` | Create an effect from a function |
|
|
156
|
+
| `succeed(value)` | Create a successful effect |
|
|
157
|
+
| `fail(error)` | Create a failed effect |
|
|
158
|
+
| `gen(generator)` | Create an effect from a generator |
|
|
159
|
+
| `pipe(effect, ...steps)` | Compose effects left-to-right |
|
|
160
|
+
| `run(effect, signal?)` | Execute an effect (returns Promise) |
|
|
161
|
+
| `catchSome(handler)` | Catch errors selectively |
|
|
162
|
+
| `catchClass(Class, handler)` | Catch errors by type |
|
|
163
|
+
|
|
164
|
+
### Concurrency
|
|
165
|
+
|
|
166
|
+
| Function | What it does |
|
|
167
|
+
| --------------------- | ---------------------------- |
|
|
168
|
+
| `all(effects)` | Run all, fail if any fails |
|
|
169
|
+
| `allSettled(effects)` | Run all, collect all results |
|
|
170
|
+
| `any(effects)` | First success wins |
|
|
171
|
+
| `race(effects)` | First to finish wins |
|
|
172
|
+
|
|
173
|
+
## 🧠 Why Effects?
|
|
174
|
+
|
|
175
|
+
Effects are like promises, but:
|
|
176
|
+
|
|
177
|
+
1. **Lazy** - They don't run until you explicitly `run()` them
|
|
178
|
+
2. **Composable** - Pipe and combine them without execution
|
|
179
|
+
3. **Typed errors** - Your errors are part of the type signature
|
|
180
|
+
4. **Cancellable** - AbortSignal support baked right in
|
|
181
|
+
|
|
182
|
+
Think of an Effect as a _description_ of a computation, not the computation itself. You're building a recipe, not cooking the meal. When you're ready to eat, call `run()`! 🍳
|
|
183
|
+
|
|
184
|
+
## 📄 License
|
|
185
|
+
|
|
186
|
+
MIT © [Jacob Ebey](https://github.com/jacob-ebey)
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
_Built with ☕ and a healthy disregard for bundle size anxiety._
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Effect, inferError, inferReturn } from "./mini-effect.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/concurrency.d.ts
|
|
4
|
+
declare const all: <const E extends Effect<any, any>[]>(values: E) => Effect<{ [K in keyof E]: inferReturn<E[K]> }, inferError<E[number]>>;
|
|
5
|
+
declare const allSettled: <const E extends Effect<any, any>[]>(values: E) => Effect<{ [K in keyof E]: PromiseSettledResult<inferReturn<E[K]>> }, inferError<E[number]>>;
|
|
6
|
+
declare const any: <const E extends Effect<any, any>[]>(values: E) => Effect<inferReturn<E[number]>, inferError<E[number]>>;
|
|
7
|
+
declare const race: <const E extends Effect<any, any>[]>(values: E) => Effect<inferReturn<E[number]>, inferError<E[number]>>;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { all, allSettled, any, race };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { _run as run, fn } from "./mini-effect.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/concurrency.ts
|
|
4
|
+
const localController = (signal) => {
|
|
5
|
+
let controller = new AbortController();
|
|
6
|
+
signal.addEventListener("abort", () => controller.abort(signal.reason));
|
|
7
|
+
return controller;
|
|
8
|
+
};
|
|
9
|
+
const promiseMethod = (method, values) => fn(async (signal) => {
|
|
10
|
+
let controller = localController(signal);
|
|
11
|
+
let localSignal = controller.signal;
|
|
12
|
+
try {
|
|
13
|
+
return await Promise[method](values.map((effect) => run(effect, localSignal)));
|
|
14
|
+
} finally {
|
|
15
|
+
if (!controller.signal.aborted) controller.abort();
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
const all = (values) => promiseMethod("all", values);
|
|
19
|
+
const allSettled = (values) => promiseMethod("allSettled", values);
|
|
20
|
+
const any = (values) => promiseMethod("any", values);
|
|
21
|
+
const race = (values) => promiseMethod("race", values);
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
export { all, allSettled, any, race };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region src/mini-effect.d.ts
|
|
2
|
+
declare const EXCLUDE: unique symbol;
|
|
3
|
+
declare const run: <T, E$1>(effect: Effect<T, E$1>, signal?: AbortSignal) => Promise<T>;
|
|
4
|
+
declare const fn: <R$1, C = never>(thunk: Thunk<R$1>) => Effect<R$1, C>;
|
|
5
|
+
declare const fail: <C>(cause: C) => Effect<never, C>;
|
|
6
|
+
declare const succeed: <R$1>(result: R$1) => Effect<R$1, never>;
|
|
7
|
+
declare const catchSome: <R$1 = any, E$1 = any, EE = never>(thunk: (cause: unknown) => Effect<R$1, E$1> | undefined) => WrapEffect<R$1, E$1, EE>;
|
|
8
|
+
declare const catchClass: <T extends {
|
|
9
|
+
new (...args: any[]): any;
|
|
10
|
+
}, E$1 extends Effect<any, any>>(type: T, thunk: (cause: T) => E$1) => Pipeable<any, inferReturn<E$1>, inferError<E$1>, T>;
|
|
11
|
+
declare const gen: <T extends Effect<any, any>, TReturn, TNext>(thunk: (signal: AbortSignal) => Generator<T, TReturn, TNext>) => Effect<TReturn, inferError<T>>;
|
|
12
|
+
declare const pipe: <F extends Effect<any, any>, P$1 extends [Pipeable<any, any, any>, ...Pipeable<any, any, any>[]]>(first: F, ...pipeable: PipeReturn<[F, ...P$1]> extends never ? never : P$1) => PipeReturn<[F, ...P$1]>;
|
|
13
|
+
type Thunk<out R$1> = (signal: AbortSignal) => R$1 | PromiseLike<R$1>;
|
|
14
|
+
type PipeableThunk<in I = any, out T = any, out E$1 = any> = (next: I) => Effect<T, E$1> | WrapEffect<T, E$1, any>;
|
|
15
|
+
type Pipeable<I = any, T = any, E$1 = any, EE = any> = PipeableThunk<I, T, E$1> | Effect<T, E$1> | WrapEffect<T, E$1, EE>;
|
|
16
|
+
type Effect<out T, out C> = {
|
|
17
|
+
[RUN]?(signal: AbortSignal): PromiseLike<T> | T;
|
|
18
|
+
[WRAP]?(cause: unknown): Effect<T, C> | undefined;
|
|
19
|
+
[Symbol.iterator](): Generator<Effect<T, C>, T, any>;
|
|
20
|
+
pipe<P$1 extends [Pipeable<any, any, any>, ...Pipeable<any, any, any>[]]>(...pipeable: P$1): PipeReturn<[Effect<T, C>, ...P$1]>;
|
|
21
|
+
};
|
|
22
|
+
type WrapEffect<T, C, EE> = {
|
|
23
|
+
[WRAP]?(cause: unknown): Effect<T, C> | undefined;
|
|
24
|
+
[EXCLUDE]?: EE;
|
|
25
|
+
};
|
|
26
|
+
type inferInput<T> = T extends PipeableThunk<infer I, any, any> ? I : never;
|
|
27
|
+
type inferReturn<T> = T extends Pipeable<any, infer R, any> ? R : T extends Effect<infer R, any> ? R : never;
|
|
28
|
+
type inferError<T> = T extends Pipeable<any, any, infer E> ? E : T extends Effect<any, infer E> ? E : never;
|
|
29
|
+
type mergePipeable<A extends Pipeable, B extends Pipeable> = A extends Pipeable<any, infer A2, infer A3> ? B extends Pipeable<any, infer B2, infer B3, infer B4> ? Pipeable<any, A2 | B2, Exclude<A3 | B3, B4>, never> : A : B;
|
|
30
|
+
type asEffect<P$1> = P$1 extends Pipeable<any, infer R, infer E> ? Effect<R, E> : never;
|
|
31
|
+
type PipeReturn<F extends Pipeable[]> = asEffect<mergePipeable<CheckPipe<F, F[0]>, Effect<never, inferError<F[number]>>>>;
|
|
32
|
+
type CheckPipe<F extends Pipeable[], L extends Pipeable> = F extends [Pipeable, WrapEffect<any, any, any>, ...infer R] ? R extends Pipeable[] ? CheckPipe<[mergePipeable<L, mergePipeable<F[0], F[1]>>, ...R], mergePipeable<L, mergePipeable<F[0], F[1]>>> : never : F extends [Pipeable, Pipeable, ...infer P] ? ExtendsStrict<inferReturn<F[0]>, inferInput<F[1]>> extends true ? P extends Pipeable[] ? CheckPipe<[F[1], ...P], F[1]> : never : never : F extends [WrapEffect<any, any, any>] ? mergePipeable<F[0], L> : F extends [Pipeable<any, infer R, infer E>] ? Effect<R, E> : never;
|
|
33
|
+
type IsNever<T> = [T] extends [never] ? true : false;
|
|
34
|
+
type IsAny<T> = 0 extends 1 & NoInfer<T> ? true : false;
|
|
35
|
+
type ExtendsStrict<Left, Right> = IsAny<Left | Right> extends true ? true : IsNever<Left> extends true ? IsNever<Right> : [Left] extends [Right] ? true : false;
|
|
36
|
+
//#endregion
|
|
37
|
+
export { Effect, IsAny, Pipeable, PipeableThunk, Thunk, WrapEffect, run as _run, run, catchClass, catchSome, fail, fn, gen, inferError, inferInput, inferReturn, pipe, succeed };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
//#region src/mini-effect.ts
|
|
2
|
+
const SYMBOL = Symbol;
|
|
3
|
+
const RUN = SYMBOL();
|
|
4
|
+
const WRAP = SYMBOL();
|
|
5
|
+
const DEFAULT_SIGNAL = new AbortController().signal;
|
|
6
|
+
const isFunction = (value) => typeof value === "function";
|
|
7
|
+
const quickYieldResult = (iterator, signal, ret) => {
|
|
8
|
+
ret = iterator.next(ret);
|
|
9
|
+
return ret.done ? ret.value : run(ret.value, signal).then((r) => quickYieldResult(iterator, signal, r));
|
|
10
|
+
};
|
|
11
|
+
const run = (effect, signal) => new Promise((resolve) => resolve(effect[RUN](signal ?? DEFAULT_SIGNAL)));
|
|
12
|
+
const fn = (thunk) => new EffectImpl(thunk);
|
|
13
|
+
const fail = (cause) => fn(() => {
|
|
14
|
+
throw cause;
|
|
15
|
+
});
|
|
16
|
+
const succeed = (result) => fn(() => result);
|
|
17
|
+
const catchSome = (thunk) => new EffectImpl(void 0, thunk);
|
|
18
|
+
const catchClass = (type, thunk) => catchSome((cause) => cause instanceof type ? thunk(cause) : void 0);
|
|
19
|
+
const gen = (thunk) => fn((signal) => quickYieldResult(thunk(signal), signal));
|
|
20
|
+
const wrapEffect = (pipe$1, onErr) => new EffectImpl(pipe$1[RUN], (cause) => pipe$1[WRAP]?.(cause) ?? onErr(cause));
|
|
21
|
+
const pipe = (first, ...pipeable) => {
|
|
22
|
+
let run$1 = [];
|
|
23
|
+
let wrap = [];
|
|
24
|
+
let pipe$1;
|
|
25
|
+
let effect;
|
|
26
|
+
let next;
|
|
27
|
+
let handlePipeError = (cause) => {
|
|
28
|
+
for (pipe$1 of wrap) {
|
|
29
|
+
effect = pipe$1(cause);
|
|
30
|
+
if (effect) return effect;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
let handler, wrappedFirst = wrapEffect(first, handlePipeError);
|
|
34
|
+
for (pipe$1 of pipeable) {
|
|
35
|
+
handler = pipe$1[WRAP];
|
|
36
|
+
if (isFunction(handler)) wrap.push(handler);
|
|
37
|
+
if (isFunction(pipe$1) || isFunction(pipe$1[RUN])) run$1.push(pipe$1);
|
|
38
|
+
}
|
|
39
|
+
return gen(function* () {
|
|
40
|
+
next = yield* wrappedFirst;
|
|
41
|
+
for (pipe$1 of run$1) {
|
|
42
|
+
if (isFunction(pipe$1)) pipe$1 = pipe$1(next);
|
|
43
|
+
if (isFunction(pipe$1[RUN])) next = yield* wrapEffect(pipe$1, handlePipeError);
|
|
44
|
+
}
|
|
45
|
+
return next;
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
var EffectImpl = class {
|
|
49
|
+
[RUN];
|
|
50
|
+
[WRAP];
|
|
51
|
+
constructor(run$1, catchSome$1) {
|
|
52
|
+
this[RUN] = catchSome$1 && run$1 ? (signal) => {
|
|
53
|
+
let res;
|
|
54
|
+
let handleError = (cause) => {
|
|
55
|
+
const recovery = catchSome$1(cause);
|
|
56
|
+
if (recovery) return recovery[RUN]?.(signal);
|
|
57
|
+
throw cause;
|
|
58
|
+
};
|
|
59
|
+
try {
|
|
60
|
+
res = run$1(signal);
|
|
61
|
+
return isFunction(res?.then) ? res.then(void 0, handleError) : res;
|
|
62
|
+
} catch (cause) {
|
|
63
|
+
return handleError(cause);
|
|
64
|
+
}
|
|
65
|
+
} : run$1;
|
|
66
|
+
this[WRAP] = catchSome$1;
|
|
67
|
+
}
|
|
68
|
+
*[SYMBOL.iterator]() {
|
|
69
|
+
return yield this;
|
|
70
|
+
}
|
|
71
|
+
pipe = (...pipeable) => pipe(this, ...pipeable);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
//#endregion
|
|
75
|
+
export { run as _run, run, catchClass, catchSome, fail, fn, gen, pipe, succeed };
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mini-effect",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"description": "A mini-effect library for TypeScript.",
|
|
6
|
+
"author": "Jacob Ebey <jacob.ebey@live.com>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/jacob-ebey/mini-effect#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/jacob-ebey/mini-effect.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/jacob-ebey/mini-effect/issues"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
"./concurrency": "./dist/concurrency.mjs",
|
|
18
|
+
"./mini-effect": "./dist/mini-effect.mjs",
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"main": "./dist/index.mjs",
|
|
22
|
+
"module": "./dist/index.mjs",
|
|
23
|
+
"types": "./dist/index.d.mts",
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsdown",
|
|
29
|
+
"dev": "tsdown --watch",
|
|
30
|
+
"test": "node --test",
|
|
31
|
+
"typecheck": "tsc --noEmit",
|
|
32
|
+
"prepublishOnly": "pnpm run build"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^25.2.3",
|
|
36
|
+
"tsdown": "^0.18.4",
|
|
37
|
+
"typescript": "^5.9.3"
|
|
38
|
+
}
|
|
39
|
+
}
|