aspi 2.0.1 → 2.2.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 +283 -7
- package/dist/index.cjs +179 -24
- package/dist/index.d.cts +152 -4
- package/dist/index.d.ts +152 -4
- package/dist/index.js +179 -24
- package/package.json +13 -14
package/README.md
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
A tiny, type‑safe wrapper around the native **fetch** API that gives you a clean, monadic interface for HTTP requests.
|
|
4
4
|
It ships with **zero runtime dependencies**, a **tiny bundle size**, and full **TypeScript** support out of the box.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
**Why use Aspi?**
|
|
7
|
+
|
|
8
|
+
- End‑to‑end TypeScript typings (request + response)
|
|
9
|
+
- No extra weight – only a thin wrapper around `fetch`
|
|
10
|
+
- Chain‑of‑responsibility middleware support via `use`
|
|
11
|
+
- Result‑based error handling (values as errors)
|
|
12
|
+
- Built‑in retry, header helpers, query‑string handling, and schema validation (Zod, Arktype, Valibot)
|
|
13
|
+
- Flexible error mapping with `error` and convenience shortcuts
|
|
13
14
|
|
|
14
15
|
---
|
|
15
16
|
|
|
@@ -66,6 +67,67 @@ getTodo(1);
|
|
|
66
67
|
|
|
67
68
|
---
|
|
68
69
|
|
|
70
|
+
## Why Aspi?
|
|
71
|
+
|
|
72
|
+
Most real‑world codebases end up with one or more of these issues:
|
|
73
|
+
|
|
74
|
+
1. **Inconsistent error handling**
|
|
75
|
+
- Some utilities throw raw `Error`/`AxiosError`.
|
|
76
|
+
- Others return `{ ok: false, error }` or `null` or a custom union.
|
|
77
|
+
- Callers don’t know whether to use `try/catch`, check `ok`, or both.
|
|
78
|
+
|
|
79
|
+
2. **Retry logic duplicated everywhere**
|
|
80
|
+
- Each service rolls its own `while (attempt <= retries)` loop.
|
|
81
|
+
- Status codes, backoff strategies, and retry limits slowly diverge over time.
|
|
82
|
+
- There is no single place to see “how do we retry HTTP calls in this app?”.
|
|
83
|
+
|
|
84
|
+
3. **Validation pushed far from the network boundary**
|
|
85
|
+
- Request payloads are sometimes validated, sometimes not.
|
|
86
|
+
- Response validation happens deep in the business logic (if at all).
|
|
87
|
+
- JSON parse errors leak as raw `SyntaxError`, not structured errors.
|
|
88
|
+
|
|
89
|
+
4. **Configuration scattered across factories and interceptors**
|
|
90
|
+
- Base URL helpers, auth decorators, error mappers, retry plugins, and logging interceptors all live in different files.
|
|
91
|
+
- Global state / interceptors can make it hard to tell what a given request will actually do.
|
|
92
|
+
|
|
93
|
+
5. **Type systems are bolted on, not designed in**
|
|
94
|
+
- Generic HTTP clients often expose `any` for responses.
|
|
95
|
+
- Error flows are not encoded in the type system, forcing manual guards and casting.
|
|
96
|
+
|
|
97
|
+
## How Aspi fixes them
|
|
98
|
+
|
|
99
|
+
Aspi’s design centers around three things:
|
|
100
|
+
|
|
101
|
+
1. **Mode‑driven responses**
|
|
102
|
+
|
|
103
|
+
You decide at call‑site how you want to consume responses:
|
|
104
|
+
- `withResult()` → `json/text/blob` return a `Result.Result<Ok, ErrorUnion>`.
|
|
105
|
+
- `throwable()` → `json/text/blob` return `AspiPlainResponse` and throw on failure.
|
|
106
|
+
- Default → `json/text/blob` return `[ok, err]` tuples.
|
|
107
|
+
|
|
108
|
+
All error variants are **tagged** so they can be safely narrowed by `error.tag`.
|
|
109
|
+
|
|
110
|
+
2. **Centralized, configurable retry layer**
|
|
111
|
+
|
|
112
|
+
Retry behavior is described declaratively:
|
|
113
|
+
- `retries`: max attempts.
|
|
114
|
+
- `retryDelay`: number or function `(attempt, maxAttempts, request, response) => delayMs`.
|
|
115
|
+
- `retryOn`: list of HTTP status codes that should trigger a retry.
|
|
116
|
+
- `retryWhile`: predicate `(request, response) => boolean` for custom retry conditions.
|
|
117
|
+
- `onRetry`: hook invoked after each retry attempt.
|
|
118
|
+
|
|
119
|
+
This configuration can be applied globally (`Aspi.setRetry`) and overridden per request (`Request.setRetry`).
|
|
120
|
+
|
|
121
|
+
3. **Validation at the transport boundary**
|
|
122
|
+
|
|
123
|
+
Using a `StandardSchemaV1` interface, Aspi integrates with schema libraries (e.g. Zod, Valibot) to:
|
|
124
|
+
- Validate request bodies with `bodySchema` + `bodyJson` **before** the network call.
|
|
125
|
+
- Validate responses with `schema()` + `json()` **after** JSON parsing.
|
|
126
|
+
|
|
127
|
+
These failures appear as tagged `parseError` values with structured issue lists, not random runtime exceptions.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
69
131
|
## Using the `Result` monad
|
|
70
132
|
|
|
71
133
|
If you prefer a single `Result` value instead of a tuple, call **`.withResult()`** before a body‑parser method.
|
|
@@ -475,6 +537,220 @@ class Request<
|
|
|
475
537
|
|
|
476
538
|
---
|
|
477
539
|
|
|
540
|
+
## Result utilities
|
|
541
|
+
|
|
542
|
+
`Result<T, E>` is a small tagged-union helper used throughout Aspi to represent success or failure without throwing.
|
|
543
|
+
|
|
544
|
+
```ts
|
|
545
|
+
type Ok<T> = { __tag: 'ok'; value: T };
|
|
546
|
+
type Err<E> = { __tag: 'err'; error: E };
|
|
547
|
+
type Result<T, E> = Ok<T> | Err<E>;
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
Creating Results
|
|
551
|
+
|
|
552
|
+
```ts
|
|
553
|
+
import * as Result from './result';
|
|
554
|
+
|
|
555
|
+
const success = Result.ok(42);
|
|
556
|
+
const failure = Result.err('not found');
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
Checking and Extracting
|
|
560
|
+
|
|
561
|
+
```ts
|
|
562
|
+
if (Result.isOk(success)) {
|
|
563
|
+
console.log(success.value); // 42
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (Result.isErr(failure)) {
|
|
567
|
+
console.error(failure.error); // "not found"
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const valueOrNull = Result.getOrNull(success); // 42 | null
|
|
571
|
+
const errorOrNull = Result.getErrorOrNull(failure); // "not found" | null
|
|
572
|
+
|
|
573
|
+
const valueOrFallback = Result.getOrElse(failure, 0); // 0
|
|
574
|
+
|
|
575
|
+
// Throwing
|
|
576
|
+
const mustHaveValue = Result.getOrThrow(success); // 42
|
|
577
|
+
// Result.getOrThrow(failure) throws "not found"
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
Transforming
|
|
581
|
+
|
|
582
|
+
```ts
|
|
583
|
+
// map value
|
|
584
|
+
const doubled = Result.map(success, (n) => n * 2); // ok(84)
|
|
585
|
+
|
|
586
|
+
// map error
|
|
587
|
+
const upperError = Result.mapErr(failure, (e) => e.toUpperCase()); // err("NOT FOUND")
|
|
588
|
+
|
|
589
|
+
// pattern matching
|
|
590
|
+
const message = Result.match(success, {
|
|
591
|
+
onOk: (n) => `Got ${n}`,
|
|
592
|
+
onErr: (e) => `Error: ${e}`,
|
|
593
|
+
});
|
|
594
|
+
// "Got 42"
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
Tagged error helpers
|
|
598
|
+
When your error type is a union with a tag field, you can use helpers to handle specific variants:
|
|
599
|
+
|
|
600
|
+
```ts
|
|
601
|
+
type HttpError =
|
|
602
|
+
| { tag: 'BAD_REQUEST'; details?: string }
|
|
603
|
+
| { tag: 'UNAUTHORIZED' }
|
|
604
|
+
| { tag: 'NOT_FOUND' };
|
|
605
|
+
|
|
606
|
+
const result: Result<number, HttpError> = Result.err({
|
|
607
|
+
tag: 'NOT_FOUND',
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// Handle a specific tag
|
|
611
|
+
Result.catchError(result, 'NOT_FOUND', (e) => {
|
|
612
|
+
console.log('Missing resource');
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
// Handle multiple tags
|
|
616
|
+
Result.catchErrors(result, {
|
|
617
|
+
BAD_REQUEST: (e) => console.log('Invalid input'),
|
|
618
|
+
UNAUTHORIZED: () => console.log('Please log in'),
|
|
619
|
+
});
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
Pipe Utility
|
|
623
|
+
|
|
624
|
+
```ts
|
|
625
|
+
import { pipe } from './result';
|
|
626
|
+
|
|
627
|
+
const label = pipe(
|
|
628
|
+
12345,
|
|
629
|
+
(cents) => cents / 100,
|
|
630
|
+
(amount) => amount.toFixed(2),
|
|
631
|
+
(str) => `$${str}`,
|
|
632
|
+
);
|
|
633
|
+
// "$123.45"
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
---
|
|
637
|
+
|
|
638
|
+
## Experimental capabilities
|
|
639
|
+
|
|
640
|
+
> **Experimental:** This API is still evolving. Names and behavior may change in minor versions.
|
|
641
|
+
|
|
642
|
+
Capabilities are small plugins that wrap the low‑level fetch call for each request. They let you implement cross‑cutting behavior (logging, retries, token refresh, tracing, etc.) without changing Aspi core.
|
|
643
|
+
|
|
644
|
+
```ts
|
|
645
|
+
import type { Capability } from './interceptor';
|
|
646
|
+
import { Aspi } from './aspi';
|
|
647
|
+
|
|
648
|
+
// Capability signature
|
|
649
|
+
const myCapability: Capability = ({ request }) => ({
|
|
650
|
+
async run(runner) {
|
|
651
|
+
// Called before fetch
|
|
652
|
+
console.log('→', request.path);
|
|
653
|
+
|
|
654
|
+
const res = await runner(); // performs fetch(url, requestInit)
|
|
655
|
+
|
|
656
|
+
// Called after fetch
|
|
657
|
+
console.log('←', res.status, res.statusText);
|
|
658
|
+
|
|
659
|
+
return res;
|
|
660
|
+
},
|
|
661
|
+
});
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
Registering capabilities
|
|
665
|
+
Capabilities are attached at the Aspi client level and apply to all requests created from that instance:
|
|
666
|
+
|
|
667
|
+
```ts
|
|
668
|
+
const api = new Aspi({ baseUrl: 'https://api.example.com' }).useCapability(
|
|
669
|
+
myCapability,
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
const user = await api.get('/users/1').throwable().json<User>();
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
You can register multiple capabilities; they execute in the order they were added, each wrapping the next:
|
|
676
|
+
|
|
677
|
+
```ts
|
|
678
|
+
const api = new Aspi({ baseUrl: 'https://api.example.com' })
|
|
679
|
+
.useCapability(loggingCapability)
|
|
680
|
+
.useCapability(tracingCapability)
|
|
681
|
+
.useCapability(refreshTokenCapability);
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
Example: token refresh (simplified)
|
|
685
|
+
|
|
686
|
+
```ts
|
|
687
|
+
import type { Capability } from './interceptor';
|
|
688
|
+
import { Aspi } from './aspi';
|
|
689
|
+
import * as Result from './result';
|
|
690
|
+
|
|
691
|
+
let tokens: { accessToken: string | null; refreshToken: string | null } = {
|
|
692
|
+
accessToken: null,
|
|
693
|
+
refreshToken: null,
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
async function refreshTokenRequest(refreshToken: string) {
|
|
697
|
+
const res = await new Aspi({ baseUrl: 'https://auth.example.com' })
|
|
698
|
+
.post('/refresh')
|
|
699
|
+
.bodyJson({ refreshToken })
|
|
700
|
+
.withResult()
|
|
701
|
+
.json<{ accessToken: string; refreshToken: string }>();
|
|
702
|
+
|
|
703
|
+
await Result.match(res, {
|
|
704
|
+
onOk: ({ data }) => {
|
|
705
|
+
tokens = data;
|
|
706
|
+
},
|
|
707
|
+
onErr: (err) => {
|
|
708
|
+
tokens = { accessToken: null, refreshToken: null };
|
|
709
|
+
throw err;
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
export const refreshTokenCapability: Capability = ({ request }) => {
|
|
715
|
+
let isRefreshing = false;
|
|
716
|
+
|
|
717
|
+
return {
|
|
718
|
+
async run(runner) {
|
|
719
|
+
// First try
|
|
720
|
+
const first = await runner();
|
|
721
|
+
|
|
722
|
+
if (first.status !== 401) {
|
|
723
|
+
return first;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// No refresh token → just propagate 401
|
|
727
|
+
if (!tokens.refreshToken || isRefreshing) {
|
|
728
|
+
return first;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
isRefreshing = true;
|
|
732
|
+
try {
|
|
733
|
+
await refreshTokenRequest(tokens.refreshToken);
|
|
734
|
+
} finally {
|
|
735
|
+
isRefreshing = false;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (!tokens.accessToken) {
|
|
739
|
+
return first;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Inject new Authorization header and retry once
|
|
743
|
+
request.requestInit.headers = {
|
|
744
|
+
...request.requestInit.headers,
|
|
745
|
+
Authorization: `Bearer ${tokens.accessToken}`,
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
return runner();
|
|
749
|
+
},
|
|
750
|
+
};
|
|
751
|
+
};
|
|
752
|
+
```
|
|
753
|
+
|
|
478
754
|
## License
|
|
479
755
|
|
|
480
756
|
MIT © Aspi contributors
|
package/dist/index.cjs
CHANGED
|
@@ -20,7 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
-
Aspi: () =>
|
|
23
|
+
Aspi: () => Aspi,
|
|
24
24
|
AspiError: () => AspiError,
|
|
25
25
|
CustomError: () => CustomError,
|
|
26
26
|
Request: () => Request,
|
|
@@ -315,17 +315,19 @@ var Request = class {
|
|
|
315
315
|
#shouldBeResult = false;
|
|
316
316
|
#bodySchemaIssues = [];
|
|
317
317
|
#throwOnError = false;
|
|
318
|
-
|
|
318
|
+
#capabilities = [];
|
|
319
|
+
constructor(method, path, requestOptions, capabilities = []) {
|
|
319
320
|
this.#path = path;
|
|
320
321
|
this.#middlewares = requestOptions.middlewares || [];
|
|
321
322
|
this.#localRequestInit = {
|
|
322
323
|
...requestOptions.requestConfig,
|
|
323
324
|
method
|
|
324
325
|
};
|
|
325
|
-
this.#retryConfig = requestOptions
|
|
326
|
-
this.#customErrorCbs = requestOptions
|
|
326
|
+
this.#retryConfig = { ...requestOptions?.retryConfig || {} };
|
|
327
|
+
this.#customErrorCbs = { ...requestOptions?.errorCbs || {} };
|
|
327
328
|
this.#throwOnError = requestOptions.throwOnError || false;
|
|
328
329
|
this.#shouldBeResult = requestOptions.shouldBeResult || false;
|
|
330
|
+
this.#capabilities = [...capabilities];
|
|
329
331
|
}
|
|
330
332
|
/**
|
|
331
333
|
* Sets the base URL for the request.
|
|
@@ -680,7 +682,29 @@ var Request = class {
|
|
|
680
682
|
* request.setQueryParams(qp);
|
|
681
683
|
*/
|
|
682
684
|
setQueryParams(params) {
|
|
683
|
-
|
|
685
|
+
let qp;
|
|
686
|
+
if (params instanceof URLSearchParams) {
|
|
687
|
+
qp = new URLSearchParams(params);
|
|
688
|
+
} else if (typeof params === "string") {
|
|
689
|
+
qp = new URLSearchParams(params);
|
|
690
|
+
} else if (Array.isArray(params)) {
|
|
691
|
+
qp = new URLSearchParams();
|
|
692
|
+
for (const entry of params) {
|
|
693
|
+
if (Array.isArray(entry) && entry.length === 2) {
|
|
694
|
+
qp.append(String(entry[0]), String(entry[1]));
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
} else if (typeof params === "object" && params !== null) {
|
|
698
|
+
qp = new URLSearchParams();
|
|
699
|
+
for (const [key, value] of Object.entries(
|
|
700
|
+
params
|
|
701
|
+
)) {
|
|
702
|
+
qp.append(key, String(value));
|
|
703
|
+
}
|
|
704
|
+
} else {
|
|
705
|
+
qp = new URLSearchParams();
|
|
706
|
+
}
|
|
707
|
+
this.#queryParams = qp;
|
|
684
708
|
return this;
|
|
685
709
|
}
|
|
686
710
|
/**
|
|
@@ -921,11 +945,41 @@ var Request = class {
|
|
|
921
945
|
return this.#mapResponse(output);
|
|
922
946
|
}
|
|
923
947
|
#url() {
|
|
948
|
+
if (this.#path.startsWith("http://") || this.#path.startsWith("https://")) {
|
|
949
|
+
const absolute = new URL(this.#path);
|
|
950
|
+
if (this.#queryParams) {
|
|
951
|
+
for (const [k, v] of this.#queryParams.entries()) {
|
|
952
|
+
absolute.searchParams.append(k, v);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return absolute.toString();
|
|
956
|
+
}
|
|
924
957
|
const passedBaseUrl = typeof this.#localRequestInit.baseUrl === "string" ? this.#localRequestInit.baseUrl : this.#localRequestInit.baseUrl.toString();
|
|
925
|
-
const
|
|
926
|
-
const
|
|
927
|
-
const
|
|
928
|
-
|
|
958
|
+
const base = passedBaseUrl.replace(/\/+$/, "");
|
|
959
|
+
const [rawPathAndQuery, fragment] = this.#path.split("#", 2);
|
|
960
|
+
const [rawPath, existingQuery] = rawPathAndQuery.split("?", 2);
|
|
961
|
+
let path = rawPath || "";
|
|
962
|
+
path = path.replace(/^\/+/, "");
|
|
963
|
+
path = path.replace(/\/{2,}/g, "/");
|
|
964
|
+
path = path.replace(/\/+$/, "");
|
|
965
|
+
if (path) {
|
|
966
|
+
path = "/" + path.replace(/^\/+/, "");
|
|
967
|
+
}
|
|
968
|
+
const qs = new URLSearchParams(existingQuery ?? "");
|
|
969
|
+
if (this.#queryParams) {
|
|
970
|
+
for (const [k, v] of this.#queryParams.entries()) {
|
|
971
|
+
qs.append(k, v);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
const queryString = qs.toString();
|
|
975
|
+
let url = base + path;
|
|
976
|
+
if (queryString) {
|
|
977
|
+
url += `?${queryString}`;
|
|
978
|
+
}
|
|
979
|
+
if (fragment) {
|
|
980
|
+
url += `#${fragment}`;
|
|
981
|
+
}
|
|
982
|
+
url = url.replace(/\/+$/, "");
|
|
929
983
|
return url;
|
|
930
984
|
}
|
|
931
985
|
/**
|
|
@@ -1024,7 +1078,15 @@ var Request = class {
|
|
|
1024
1078
|
let responseData = null;
|
|
1025
1079
|
while (attempts <= retries) {
|
|
1026
1080
|
try {
|
|
1027
|
-
|
|
1081
|
+
if (this.#capabilities.length > 0) {
|
|
1082
|
+
for (const capability of this.#capabilities) {
|
|
1083
|
+
response = await capability({ request }).run(
|
|
1084
|
+
() => fetch(url, requestInit)
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
} else {
|
|
1088
|
+
response = await fetch(url, requestInit);
|
|
1089
|
+
}
|
|
1028
1090
|
responseData = await responseParser(response);
|
|
1029
1091
|
if (responseData instanceof Error) {
|
|
1030
1092
|
return err(responseData);
|
|
@@ -1217,8 +1279,9 @@ var Request = class {
|
|
|
1217
1279
|
return {
|
|
1218
1280
|
response,
|
|
1219
1281
|
status: response.status,
|
|
1220
|
-
|
|
1221
|
-
responseData
|
|
1282
|
+
statusLabel: getHttpErrorStatus(response.status),
|
|
1283
|
+
responseData,
|
|
1284
|
+
statusText: response.statusText
|
|
1222
1285
|
};
|
|
1223
1286
|
}
|
|
1224
1287
|
/**
|
|
@@ -1283,16 +1346,62 @@ var Request = class {
|
|
|
1283
1346
|
const cfg = this.#sanitisedRetryConfig();
|
|
1284
1347
|
return { ...cfg };
|
|
1285
1348
|
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Registers a capability for this request.
|
|
1351
|
+
*
|
|
1352
|
+
* A capability is a small wrapper around the underlying `fetch` call that can
|
|
1353
|
+
* intercept, inspect, or modify the request/response lifecycle. Each registered
|
|
1354
|
+
* capability receives the constructed {@link AspiRequest} and can wrap the
|
|
1355
|
+
* execution of the network call via its `run` method.
|
|
1356
|
+
*
|
|
1357
|
+
* Capabilities are applied in the order they are registered. For each HTTP
|
|
1358
|
+
* call, the `Request` will:
|
|
1359
|
+
*
|
|
1360
|
+
* 1. Build an {@link AspiRequest} from the current configuration.
|
|
1361
|
+
* 2. Create a runner: `() => fetch(url, requestInit)`.
|
|
1362
|
+
* 3. Pass that runner through each capability in sequence, allowing them to:
|
|
1363
|
+
* - log or trace requests,
|
|
1364
|
+
* - implement retry/refresh logic,
|
|
1365
|
+
* - short‑circuit with synthetic responses, etc.
|
|
1366
|
+
*
|
|
1367
|
+
* @param capability - The capability factory to register. It will be invoked
|
|
1368
|
+
* for every execution of this request with the current {@link AspiRequest}.
|
|
1369
|
+
*
|
|
1370
|
+
* @returns This {@link Request} instance, enabling fluent chaining.
|
|
1371
|
+
*
|
|
1372
|
+
* @example
|
|
1373
|
+
* ```ts
|
|
1374
|
+
* const api = new Aspi({ baseUrl: 'https://api.example.com' });
|
|
1375
|
+
*
|
|
1376
|
+
* const apiWithLogging = api.useCapability(({ request }) => ({
|
|
1377
|
+
* async run(runner) {
|
|
1378
|
+
* console.log('→', request.path, request.requestInit);
|
|
1379
|
+
* const res = await runner();
|
|
1380
|
+
* console.log('←', res.status, res.statusText);
|
|
1381
|
+
* return res;
|
|
1382
|
+
* },
|
|
1383
|
+
* }));
|
|
1384
|
+
*
|
|
1385
|
+
* const result = await apiWithLogging
|
|
1386
|
+
* .get('/users')
|
|
1387
|
+
* .withResult()
|
|
1388
|
+
* .json();
|
|
1389
|
+
* ```
|
|
1390
|
+
*/
|
|
1391
|
+
useCapability(capability) {
|
|
1392
|
+
return this.#capabilities.push(capability);
|
|
1393
|
+
}
|
|
1286
1394
|
};
|
|
1287
1395
|
|
|
1288
1396
|
// src/aspi.ts
|
|
1289
|
-
var
|
|
1397
|
+
var Aspi = class {
|
|
1290
1398
|
#globalRequestInit;
|
|
1291
1399
|
#middlewares = [];
|
|
1292
1400
|
#retryConfig;
|
|
1293
1401
|
#customErrorCbs = {};
|
|
1294
1402
|
#throwOnError = false;
|
|
1295
1403
|
#shouldBeResult = false;
|
|
1404
|
+
#capabilities = [];
|
|
1296
1405
|
constructor(config) {
|
|
1297
1406
|
const { retryConfig, ...requestInit } = config;
|
|
1298
1407
|
this.#globalRequestInit = requestInit;
|
|
@@ -1335,17 +1444,22 @@ var Aspi2 = class {
|
|
|
1335
1444
|
return this;
|
|
1336
1445
|
}
|
|
1337
1446
|
#createRequest(method, path) {
|
|
1338
|
-
return new Request(
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1447
|
+
return new Request(
|
|
1448
|
+
method,
|
|
1449
|
+
path,
|
|
1450
|
+
{
|
|
1451
|
+
requestConfig: {
|
|
1452
|
+
...this.#globalRequestInit,
|
|
1453
|
+
method
|
|
1454
|
+
},
|
|
1455
|
+
middlewares: this.#middlewares,
|
|
1456
|
+
errorCbs: this.#customErrorCbs,
|
|
1457
|
+
throwOnError: this.#throwOnError,
|
|
1458
|
+
shouldBeResult: this.#shouldBeResult,
|
|
1459
|
+
retryConfig: this.#retryConfig
|
|
1342
1460
|
},
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
throwOnError: this.#throwOnError,
|
|
1346
|
-
shouldBeResult: this.#shouldBeResult,
|
|
1347
|
-
retryConfig: this.#retryConfig
|
|
1348
|
-
});
|
|
1461
|
+
this.#capabilities
|
|
1462
|
+
);
|
|
1349
1463
|
}
|
|
1350
1464
|
/**
|
|
1351
1465
|
* Makes a GET request to the specified path
|
|
@@ -1613,7 +1727,7 @@ var Aspi2 = class {
|
|
|
1613
1727
|
* api.internalServerError((req, res) => ({ message: 'Server error occurred' }));
|
|
1614
1728
|
*/
|
|
1615
1729
|
internalServerError(cb) {
|
|
1616
|
-
return this.error("
|
|
1730
|
+
return this.error("internalServerError", "INTERNAL_SERVER_ERROR", cb);
|
|
1617
1731
|
}
|
|
1618
1732
|
/**
|
|
1619
1733
|
* Sets the aspi to throw an error if the response status is not successful.
|
|
@@ -1640,6 +1754,47 @@ var Aspi2 = class {
|
|
|
1640
1754
|
this.#throwOnError = false;
|
|
1641
1755
|
return this;
|
|
1642
1756
|
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Registers a capability on this {@link Aspi} instance.
|
|
1759
|
+
*
|
|
1760
|
+
* A capability is a small, pluggable unit that can intercept and wrap the
|
|
1761
|
+
* low‑level `fetch` call used by all requests created from this client.
|
|
1762
|
+
* It is invoked for every request with the constructed {@link AspiRequest},
|
|
1763
|
+
* and can:
|
|
1764
|
+
*
|
|
1765
|
+
* - inspect or mutate the outgoing request (e.g. inject auth headers),
|
|
1766
|
+
* - inspect the raw {@link Response},
|
|
1767
|
+
* - implement cross‑cutting concerns such as logging, tracing, retries,
|
|
1768
|
+
* or token refresh, and
|
|
1769
|
+
* - short‑circuit the network call by returning a synthetic {@link Response}.
|
|
1770
|
+
*
|
|
1771
|
+
* Capabilities registered on the {@link Aspi} instance are propagated to every
|
|
1772
|
+
* {@link Request} created via methods like {@link get}, {@link post}, etc.
|
|
1773
|
+
* They are applied in the order they are registered.
|
|
1774
|
+
*
|
|
1775
|
+
* @param capability - The capability factory to install on this client.
|
|
1776
|
+
*
|
|
1777
|
+
* @returns This {@link Aspi} instance for fluent chaining.
|
|
1778
|
+
*
|
|
1779
|
+
* @example
|
|
1780
|
+
* ```ts
|
|
1781
|
+
* const api = new Aspi({ baseUrl: 'https://api.example.com' })
|
|
1782
|
+
* .useCapability(({ request }) => ({
|
|
1783
|
+
* async run(runner) {
|
|
1784
|
+
* console.log('→', request.path);
|
|
1785
|
+
* const res = await runner();
|
|
1786
|
+
* console.log('←', res.status);
|
|
1787
|
+
* return res;
|
|
1788
|
+
* },
|
|
1789
|
+
* }));
|
|
1790
|
+
*
|
|
1791
|
+
* const user = await api.get('/users/1').throwable().json<User>();
|
|
1792
|
+
* ```
|
|
1793
|
+
*/
|
|
1794
|
+
useCapability(capability) {
|
|
1795
|
+
this.#capabilities.push(capability);
|
|
1796
|
+
return this;
|
|
1797
|
+
}
|
|
1643
1798
|
};
|
|
1644
1799
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1645
1800
|
0 && (module.exports = {
|
package/dist/index.d.cts
CHANGED
|
@@ -295,8 +295,9 @@ interface AspiRequest<T extends AspiRequestInit> {
|
|
|
295
295
|
*/
|
|
296
296
|
type AspiResponse<TData = any, IsError extends boolean = false> = Merge<{
|
|
297
297
|
status: HttpErrorCodes;
|
|
298
|
-
|
|
298
|
+
statusLabel: HttpErrorStatus;
|
|
299
299
|
response: Response;
|
|
300
|
+
statusText: string;
|
|
300
301
|
}, IsError extends true ? {
|
|
301
302
|
responseData: TData;
|
|
302
303
|
} : {
|
|
@@ -365,6 +366,72 @@ interface JSONParseError extends CustomError<'jsonParseError', {
|
|
|
365
366
|
declare const isAspiError: <TReq extends AspiRequestInit>(error: unknown) => error is AspiError<TReq>;
|
|
366
367
|
declare const isCustomError: <Tag extends string, A>(error: unknown) => error is CustomError<Tag, A>;
|
|
367
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Arguments passed to a capability factory.
|
|
371
|
+
*
|
|
372
|
+
* @template T - The concrete request-init type used by this Aspi instance.
|
|
373
|
+
*/
|
|
374
|
+
type CapabilityArgs<T extends AspiRequestInit = AspiRequestInit> = {
|
|
375
|
+
/**
|
|
376
|
+
* The fully constructed AspiRequest that will be used to execute `fetch`.
|
|
377
|
+
*
|
|
378
|
+
* You can:
|
|
379
|
+
* - inspect `request.path`, `request.requestInit`, `request.retryConfig`, etc.
|
|
380
|
+
* - mutate `request.requestInit.headers`, `signal`, or other properties
|
|
381
|
+
* before the network call is made.
|
|
382
|
+
*/
|
|
383
|
+
request: AspiRequest<AspiRequestInit>;
|
|
384
|
+
};
|
|
385
|
+
/**
|
|
386
|
+
* A capability is a small wrapper around the low-level `fetch` call that can
|
|
387
|
+
* intercept outgoing requests and incoming responses.
|
|
388
|
+
*
|
|
389
|
+
* It is a factory function that receives the current {@link AspiRequest}
|
|
390
|
+
* and returns an object exposing a single `run` method. The `run` method is
|
|
391
|
+
* responsible for invoking the provided `runner` (which performs the actual
|
|
392
|
+
* `fetch`) and may:
|
|
393
|
+
*
|
|
394
|
+
* - call `runner()` directly and return its result
|
|
395
|
+
* - call `runner()` multiple times (e.g. retry, refresh token then retry)
|
|
396
|
+
* - short‑circuit by *not* calling `runner()` and returning a synthetic
|
|
397
|
+
* {@link Response} instead
|
|
398
|
+
*
|
|
399
|
+
* Capabilities are composed in the order they are registered via
|
|
400
|
+
* `Aspi.useCapability`, with each capability wrapping the next one in the
|
|
401
|
+
* chain.
|
|
402
|
+
*
|
|
403
|
+
* @template T - The concrete request-init type used by this Aspi instance.
|
|
404
|
+
*
|
|
405
|
+
* @example
|
|
406
|
+
* ```ts
|
|
407
|
+
* // Simple logging capability
|
|
408
|
+
* const loggingCapability: Capability = ({ request }) => ({
|
|
409
|
+
* async run(runner) {
|
|
410
|
+
* console.log('→', request.path, request.requestInit);
|
|
411
|
+
* const res = await runner();
|
|
412
|
+
* console.log('←', res.status, res.statusText);
|
|
413
|
+
* return res;
|
|
414
|
+
* },
|
|
415
|
+
* });
|
|
416
|
+
*
|
|
417
|
+
* const api = new Aspi({ baseUrl: 'https://api.example.com' })
|
|
418
|
+
* .useCapability(loggingCapability);
|
|
419
|
+
* ```
|
|
420
|
+
*/
|
|
421
|
+
type Capability<T extends AspiRequestInit = AspiRequestInit> = ({ request, }: CapabilityArgs<T>) => {
|
|
422
|
+
/**
|
|
423
|
+
* Executes the next step in the capability chain.
|
|
424
|
+
*
|
|
425
|
+
* @param runner - A function that, when called, performs the actual
|
|
426
|
+
* network request (or the next capability in the chain) and resolves to
|
|
427
|
+
* a {@link Response}.
|
|
428
|
+
*
|
|
429
|
+
* @returns A promise resolving to the final {@link Response} that should be
|
|
430
|
+
* used by Aspi for this request.
|
|
431
|
+
*/
|
|
432
|
+
run: (runner: () => Promise<Response>) => Promise<Response>;
|
|
433
|
+
};
|
|
434
|
+
|
|
368
435
|
type Ok<T> = {
|
|
369
436
|
__tag: 'ok';
|
|
370
437
|
value: T;
|
|
@@ -767,7 +834,7 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
|
|
|
767
834
|
error: {};
|
|
768
835
|
}> {
|
|
769
836
|
#private;
|
|
770
|
-
constructor(method: HttpMethods, path: string, requestOptions: RequestOptions<TRequest
|
|
837
|
+
constructor(method: HttpMethods, path: string, requestOptions: RequestOptions<TRequest>, capabilities?: Capability[]);
|
|
771
838
|
/**
|
|
772
839
|
* Sets the base URL for the request.
|
|
773
840
|
* @param url The base URL to set
|
|
@@ -1081,7 +1148,7 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
|
|
|
1081
1148
|
* const qp = new URLSearchParams({ page: '1' });
|
|
1082
1149
|
* request.setQueryParams(qp);
|
|
1083
1150
|
*/
|
|
1084
|
-
setQueryParams<T
|
|
1151
|
+
setQueryParams<T = any>(params: T): Request<Method, TRequest, Merge<Omit<Opts, "queryParams">, {
|
|
1085
1152
|
queryParams: T;
|
|
1086
1153
|
}>>;
|
|
1087
1154
|
/**
|
|
@@ -1440,6 +1507,49 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
|
|
|
1440
1507
|
retryWhile: ((request: AspiRequest<TRequest>, response: AspiResponse) => boolean | Promise<boolean>) | undefined;
|
|
1441
1508
|
onRetry: ((request: AspiRequest<TRequest>, response: AspiResponse) => void) | undefined;
|
|
1442
1509
|
};
|
|
1510
|
+
/**
|
|
1511
|
+
* Registers a capability for this request.
|
|
1512
|
+
*
|
|
1513
|
+
* A capability is a small wrapper around the underlying `fetch` call that can
|
|
1514
|
+
* intercept, inspect, or modify the request/response lifecycle. Each registered
|
|
1515
|
+
* capability receives the constructed {@link AspiRequest} and can wrap the
|
|
1516
|
+
* execution of the network call via its `run` method.
|
|
1517
|
+
*
|
|
1518
|
+
* Capabilities are applied in the order they are registered. For each HTTP
|
|
1519
|
+
* call, the `Request` will:
|
|
1520
|
+
*
|
|
1521
|
+
* 1. Build an {@link AspiRequest} from the current configuration.
|
|
1522
|
+
* 2. Create a runner: `() => fetch(url, requestInit)`.
|
|
1523
|
+
* 3. Pass that runner through each capability in sequence, allowing them to:
|
|
1524
|
+
* - log or trace requests,
|
|
1525
|
+
* - implement retry/refresh logic,
|
|
1526
|
+
* - short‑circuit with synthetic responses, etc.
|
|
1527
|
+
*
|
|
1528
|
+
* @param capability - The capability factory to register. It will be invoked
|
|
1529
|
+
* for every execution of this request with the current {@link AspiRequest}.
|
|
1530
|
+
*
|
|
1531
|
+
* @returns This {@link Request} instance, enabling fluent chaining.
|
|
1532
|
+
*
|
|
1533
|
+
* @example
|
|
1534
|
+
* ```ts
|
|
1535
|
+
* const api = new Aspi({ baseUrl: 'https://api.example.com' });
|
|
1536
|
+
*
|
|
1537
|
+
* const apiWithLogging = api.useCapability(({ request }) => ({
|
|
1538
|
+
* async run(runner) {
|
|
1539
|
+
* console.log('→', request.path, request.requestInit);
|
|
1540
|
+
* const res = await runner();
|
|
1541
|
+
* console.log('←', res.status, res.statusText);
|
|
1542
|
+
* return res;
|
|
1543
|
+
* },
|
|
1544
|
+
* }));
|
|
1545
|
+
*
|
|
1546
|
+
* const result = await apiWithLogging
|
|
1547
|
+
* .get('/users')
|
|
1548
|
+
* .withResult()
|
|
1549
|
+
* .json();
|
|
1550
|
+
* ```
|
|
1551
|
+
*/
|
|
1552
|
+
useCapability(capability: Capability<TRequest>): number;
|
|
1443
1553
|
}
|
|
1444
1554
|
|
|
1445
1555
|
/**
|
|
@@ -1718,7 +1828,7 @@ declare class Aspi<TRequest extends AspiRequestInit = AspiRequestInit, Opts exte
|
|
|
1718
1828
|
* api.internalServerError((req, res) => ({ message: 'Server error occurred' }));
|
|
1719
1829
|
*/
|
|
1720
1830
|
internalServerError<A extends {}>(cb: CustomErrorCb<TRequest, A>): Aspi<TRequest, Prettify<Opts & {
|
|
1721
|
-
error: { [K in keyof Opts["error"]
|
|
1831
|
+
error: { [K in "internalServerError" | keyof Opts["error"]]: K extends "internalServerError" ? CustomError<"internalServerError", A> : Opts["error"][K]; };
|
|
1722
1832
|
}>>;
|
|
1723
1833
|
/**
|
|
1724
1834
|
* Sets the aspi to throw an error if the response status is not successful.
|
|
@@ -1743,6 +1853,44 @@ declare class Aspi<TRequest extends AspiRequestInit = AspiRequestInit, Opts exte
|
|
|
1743
1853
|
withResult: true;
|
|
1744
1854
|
throwable: false;
|
|
1745
1855
|
}>>;
|
|
1856
|
+
/**
|
|
1857
|
+
* Registers a capability on this {@link Aspi} instance.
|
|
1858
|
+
*
|
|
1859
|
+
* A capability is a small, pluggable unit that can intercept and wrap the
|
|
1860
|
+
* low‑level `fetch` call used by all requests created from this client.
|
|
1861
|
+
* It is invoked for every request with the constructed {@link AspiRequest},
|
|
1862
|
+
* and can:
|
|
1863
|
+
*
|
|
1864
|
+
* - inspect or mutate the outgoing request (e.g. inject auth headers),
|
|
1865
|
+
* - inspect the raw {@link Response},
|
|
1866
|
+
* - implement cross‑cutting concerns such as logging, tracing, retries,
|
|
1867
|
+
* or token refresh, and
|
|
1868
|
+
* - short‑circuit the network call by returning a synthetic {@link Response}.
|
|
1869
|
+
*
|
|
1870
|
+
* Capabilities registered on the {@link Aspi} instance are propagated to every
|
|
1871
|
+
* {@link Request} created via methods like {@link get}, {@link post}, etc.
|
|
1872
|
+
* They are applied in the order they are registered.
|
|
1873
|
+
*
|
|
1874
|
+
* @param capability - The capability factory to install on this client.
|
|
1875
|
+
*
|
|
1876
|
+
* @returns This {@link Aspi} instance for fluent chaining.
|
|
1877
|
+
*
|
|
1878
|
+
* @example
|
|
1879
|
+
* ```ts
|
|
1880
|
+
* const api = new Aspi({ baseUrl: 'https://api.example.com' })
|
|
1881
|
+
* .useCapability(({ request }) => ({
|
|
1882
|
+
* async run(runner) {
|
|
1883
|
+
* console.log('→', request.path);
|
|
1884
|
+
* const res = await runner();
|
|
1885
|
+
* console.log('←', res.status);
|
|
1886
|
+
* return res;
|
|
1887
|
+
* },
|
|
1888
|
+
* }));
|
|
1889
|
+
*
|
|
1890
|
+
* const user = await api.get('/users/1').throwable().json<User>();
|
|
1891
|
+
* ```
|
|
1892
|
+
*/
|
|
1893
|
+
useCapability(capability: Capability<TRequest>): this;
|
|
1746
1894
|
}
|
|
1747
1895
|
|
|
1748
1896
|
export { Aspi, type AspiConfigBase, AspiError, type AspiPlainResponse, type AspiRequest, type AspiRequestInit, type AspiRequestInitWithoutBodyAndMethod, type AspiResponse, type AspiResultOk, type AspiRetryConfig, type BaseURL, CustomError, type CustomErrorCb, type ErrorCallbacks, type HttpErrorCodes, type HttpErrorStatus, type HttpMethods, type JSONParseError, type Merge, type Prettify, Request, type RequestOptions, type RequestTransformer, result as Result, getHttpErrorStatus, httpErrors, isAspiError, isCustomError };
|
package/dist/index.d.ts
CHANGED
|
@@ -295,8 +295,9 @@ interface AspiRequest<T extends AspiRequestInit> {
|
|
|
295
295
|
*/
|
|
296
296
|
type AspiResponse<TData = any, IsError extends boolean = false> = Merge<{
|
|
297
297
|
status: HttpErrorCodes;
|
|
298
|
-
|
|
298
|
+
statusLabel: HttpErrorStatus;
|
|
299
299
|
response: Response;
|
|
300
|
+
statusText: string;
|
|
300
301
|
}, IsError extends true ? {
|
|
301
302
|
responseData: TData;
|
|
302
303
|
} : {
|
|
@@ -365,6 +366,72 @@ interface JSONParseError extends CustomError<'jsonParseError', {
|
|
|
365
366
|
declare const isAspiError: <TReq extends AspiRequestInit>(error: unknown) => error is AspiError<TReq>;
|
|
366
367
|
declare const isCustomError: <Tag extends string, A>(error: unknown) => error is CustomError<Tag, A>;
|
|
367
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Arguments passed to a capability factory.
|
|
371
|
+
*
|
|
372
|
+
* @template T - The concrete request-init type used by this Aspi instance.
|
|
373
|
+
*/
|
|
374
|
+
type CapabilityArgs<T extends AspiRequestInit = AspiRequestInit> = {
|
|
375
|
+
/**
|
|
376
|
+
* The fully constructed AspiRequest that will be used to execute `fetch`.
|
|
377
|
+
*
|
|
378
|
+
* You can:
|
|
379
|
+
* - inspect `request.path`, `request.requestInit`, `request.retryConfig`, etc.
|
|
380
|
+
* - mutate `request.requestInit.headers`, `signal`, or other properties
|
|
381
|
+
* before the network call is made.
|
|
382
|
+
*/
|
|
383
|
+
request: AspiRequest<AspiRequestInit>;
|
|
384
|
+
};
|
|
385
|
+
/**
|
|
386
|
+
* A capability is a small wrapper around the low-level `fetch` call that can
|
|
387
|
+
* intercept outgoing requests and incoming responses.
|
|
388
|
+
*
|
|
389
|
+
* It is a factory function that receives the current {@link AspiRequest}
|
|
390
|
+
* and returns an object exposing a single `run` method. The `run` method is
|
|
391
|
+
* responsible for invoking the provided `runner` (which performs the actual
|
|
392
|
+
* `fetch`) and may:
|
|
393
|
+
*
|
|
394
|
+
* - call `runner()` directly and return its result
|
|
395
|
+
* - call `runner()` multiple times (e.g. retry, refresh token then retry)
|
|
396
|
+
* - short‑circuit by *not* calling `runner()` and returning a synthetic
|
|
397
|
+
* {@link Response} instead
|
|
398
|
+
*
|
|
399
|
+
* Capabilities are composed in the order they are registered via
|
|
400
|
+
* `Aspi.useCapability`, with each capability wrapping the next one in the
|
|
401
|
+
* chain.
|
|
402
|
+
*
|
|
403
|
+
* @template T - The concrete request-init type used by this Aspi instance.
|
|
404
|
+
*
|
|
405
|
+
* @example
|
|
406
|
+
* ```ts
|
|
407
|
+
* // Simple logging capability
|
|
408
|
+
* const loggingCapability: Capability = ({ request }) => ({
|
|
409
|
+
* async run(runner) {
|
|
410
|
+
* console.log('→', request.path, request.requestInit);
|
|
411
|
+
* const res = await runner();
|
|
412
|
+
* console.log('←', res.status, res.statusText);
|
|
413
|
+
* return res;
|
|
414
|
+
* },
|
|
415
|
+
* });
|
|
416
|
+
*
|
|
417
|
+
* const api = new Aspi({ baseUrl: 'https://api.example.com' })
|
|
418
|
+
* .useCapability(loggingCapability);
|
|
419
|
+
* ```
|
|
420
|
+
*/
|
|
421
|
+
type Capability<T extends AspiRequestInit = AspiRequestInit> = ({ request, }: CapabilityArgs<T>) => {
|
|
422
|
+
/**
|
|
423
|
+
* Executes the next step in the capability chain.
|
|
424
|
+
*
|
|
425
|
+
* @param runner - A function that, when called, performs the actual
|
|
426
|
+
* network request (or the next capability in the chain) and resolves to
|
|
427
|
+
* a {@link Response}.
|
|
428
|
+
*
|
|
429
|
+
* @returns A promise resolving to the final {@link Response} that should be
|
|
430
|
+
* used by Aspi for this request.
|
|
431
|
+
*/
|
|
432
|
+
run: (runner: () => Promise<Response>) => Promise<Response>;
|
|
433
|
+
};
|
|
434
|
+
|
|
368
435
|
type Ok<T> = {
|
|
369
436
|
__tag: 'ok';
|
|
370
437
|
value: T;
|
|
@@ -767,7 +834,7 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
|
|
|
767
834
|
error: {};
|
|
768
835
|
}> {
|
|
769
836
|
#private;
|
|
770
|
-
constructor(method: HttpMethods, path: string, requestOptions: RequestOptions<TRequest
|
|
837
|
+
constructor(method: HttpMethods, path: string, requestOptions: RequestOptions<TRequest>, capabilities?: Capability[]);
|
|
771
838
|
/**
|
|
772
839
|
* Sets the base URL for the request.
|
|
773
840
|
* @param url The base URL to set
|
|
@@ -1081,7 +1148,7 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
|
|
|
1081
1148
|
* const qp = new URLSearchParams({ page: '1' });
|
|
1082
1149
|
* request.setQueryParams(qp);
|
|
1083
1150
|
*/
|
|
1084
|
-
setQueryParams<T
|
|
1151
|
+
setQueryParams<T = any>(params: T): Request<Method, TRequest, Merge<Omit<Opts, "queryParams">, {
|
|
1085
1152
|
queryParams: T;
|
|
1086
1153
|
}>>;
|
|
1087
1154
|
/**
|
|
@@ -1440,6 +1507,49 @@ declare class Request<Method extends HttpMethods, TRequest extends AspiRequestIn
|
|
|
1440
1507
|
retryWhile: ((request: AspiRequest<TRequest>, response: AspiResponse) => boolean | Promise<boolean>) | undefined;
|
|
1441
1508
|
onRetry: ((request: AspiRequest<TRequest>, response: AspiResponse) => void) | undefined;
|
|
1442
1509
|
};
|
|
1510
|
+
/**
|
|
1511
|
+
* Registers a capability for this request.
|
|
1512
|
+
*
|
|
1513
|
+
* A capability is a small wrapper around the underlying `fetch` call that can
|
|
1514
|
+
* intercept, inspect, or modify the request/response lifecycle. Each registered
|
|
1515
|
+
* capability receives the constructed {@link AspiRequest} and can wrap the
|
|
1516
|
+
* execution of the network call via its `run` method.
|
|
1517
|
+
*
|
|
1518
|
+
* Capabilities are applied in the order they are registered. For each HTTP
|
|
1519
|
+
* call, the `Request` will:
|
|
1520
|
+
*
|
|
1521
|
+
* 1. Build an {@link AspiRequest} from the current configuration.
|
|
1522
|
+
* 2. Create a runner: `() => fetch(url, requestInit)`.
|
|
1523
|
+
* 3. Pass that runner through each capability in sequence, allowing them to:
|
|
1524
|
+
* - log or trace requests,
|
|
1525
|
+
* - implement retry/refresh logic,
|
|
1526
|
+
* - short‑circuit with synthetic responses, etc.
|
|
1527
|
+
*
|
|
1528
|
+
* @param capability - The capability factory to register. It will be invoked
|
|
1529
|
+
* for every execution of this request with the current {@link AspiRequest}.
|
|
1530
|
+
*
|
|
1531
|
+
* @returns This {@link Request} instance, enabling fluent chaining.
|
|
1532
|
+
*
|
|
1533
|
+
* @example
|
|
1534
|
+
* ```ts
|
|
1535
|
+
* const api = new Aspi({ baseUrl: 'https://api.example.com' });
|
|
1536
|
+
*
|
|
1537
|
+
* const apiWithLogging = api.useCapability(({ request }) => ({
|
|
1538
|
+
* async run(runner) {
|
|
1539
|
+
* console.log('→', request.path, request.requestInit);
|
|
1540
|
+
* const res = await runner();
|
|
1541
|
+
* console.log('←', res.status, res.statusText);
|
|
1542
|
+
* return res;
|
|
1543
|
+
* },
|
|
1544
|
+
* }));
|
|
1545
|
+
*
|
|
1546
|
+
* const result = await apiWithLogging
|
|
1547
|
+
* .get('/users')
|
|
1548
|
+
* .withResult()
|
|
1549
|
+
* .json();
|
|
1550
|
+
* ```
|
|
1551
|
+
*/
|
|
1552
|
+
useCapability(capability: Capability<TRequest>): number;
|
|
1443
1553
|
}
|
|
1444
1554
|
|
|
1445
1555
|
/**
|
|
@@ -1718,7 +1828,7 @@ declare class Aspi<TRequest extends AspiRequestInit = AspiRequestInit, Opts exte
|
|
|
1718
1828
|
* api.internalServerError((req, res) => ({ message: 'Server error occurred' }));
|
|
1719
1829
|
*/
|
|
1720
1830
|
internalServerError<A extends {}>(cb: CustomErrorCb<TRequest, A>): Aspi<TRequest, Prettify<Opts & {
|
|
1721
|
-
error: { [K in keyof Opts["error"]
|
|
1831
|
+
error: { [K in "internalServerError" | keyof Opts["error"]]: K extends "internalServerError" ? CustomError<"internalServerError", A> : Opts["error"][K]; };
|
|
1722
1832
|
}>>;
|
|
1723
1833
|
/**
|
|
1724
1834
|
* Sets the aspi to throw an error if the response status is not successful.
|
|
@@ -1743,6 +1853,44 @@ declare class Aspi<TRequest extends AspiRequestInit = AspiRequestInit, Opts exte
|
|
|
1743
1853
|
withResult: true;
|
|
1744
1854
|
throwable: false;
|
|
1745
1855
|
}>>;
|
|
1856
|
+
/**
|
|
1857
|
+
* Registers a capability on this {@link Aspi} instance.
|
|
1858
|
+
*
|
|
1859
|
+
* A capability is a small, pluggable unit that can intercept and wrap the
|
|
1860
|
+
* low‑level `fetch` call used by all requests created from this client.
|
|
1861
|
+
* It is invoked for every request with the constructed {@link AspiRequest},
|
|
1862
|
+
* and can:
|
|
1863
|
+
*
|
|
1864
|
+
* - inspect or mutate the outgoing request (e.g. inject auth headers),
|
|
1865
|
+
* - inspect the raw {@link Response},
|
|
1866
|
+
* - implement cross‑cutting concerns such as logging, tracing, retries,
|
|
1867
|
+
* or token refresh, and
|
|
1868
|
+
* - short‑circuit the network call by returning a synthetic {@link Response}.
|
|
1869
|
+
*
|
|
1870
|
+
* Capabilities registered on the {@link Aspi} instance are propagated to every
|
|
1871
|
+
* {@link Request} created via methods like {@link get}, {@link post}, etc.
|
|
1872
|
+
* They are applied in the order they are registered.
|
|
1873
|
+
*
|
|
1874
|
+
* @param capability - The capability factory to install on this client.
|
|
1875
|
+
*
|
|
1876
|
+
* @returns This {@link Aspi} instance for fluent chaining.
|
|
1877
|
+
*
|
|
1878
|
+
* @example
|
|
1879
|
+
* ```ts
|
|
1880
|
+
* const api = new Aspi({ baseUrl: 'https://api.example.com' })
|
|
1881
|
+
* .useCapability(({ request }) => ({
|
|
1882
|
+
* async run(runner) {
|
|
1883
|
+
* console.log('→', request.path);
|
|
1884
|
+
* const res = await runner();
|
|
1885
|
+
* console.log('←', res.status);
|
|
1886
|
+
* return res;
|
|
1887
|
+
* },
|
|
1888
|
+
* }));
|
|
1889
|
+
*
|
|
1890
|
+
* const user = await api.get('/users/1').throwable().json<User>();
|
|
1891
|
+
* ```
|
|
1892
|
+
*/
|
|
1893
|
+
useCapability(capability: Capability<TRequest>): this;
|
|
1746
1894
|
}
|
|
1747
1895
|
|
|
1748
1896
|
export { Aspi, type AspiConfigBase, AspiError, type AspiPlainResponse, type AspiRequest, type AspiRequestInit, type AspiRequestInitWithoutBodyAndMethod, type AspiResponse, type AspiResultOk, type AspiRetryConfig, type BaseURL, CustomError, type CustomErrorCb, type ErrorCallbacks, type HttpErrorCodes, type HttpErrorStatus, type HttpMethods, type JSONParseError, type Merge, type Prettify, Request, type RequestOptions, type RequestTransformer, result as Result, getHttpErrorStatus, httpErrors, isAspiError, isCustomError };
|
package/dist/index.js
CHANGED
|
@@ -287,17 +287,19 @@ var Request = class {
|
|
|
287
287
|
#shouldBeResult = false;
|
|
288
288
|
#bodySchemaIssues = [];
|
|
289
289
|
#throwOnError = false;
|
|
290
|
-
|
|
290
|
+
#capabilities = [];
|
|
291
|
+
constructor(method, path, requestOptions, capabilities = []) {
|
|
291
292
|
this.#path = path;
|
|
292
293
|
this.#middlewares = requestOptions.middlewares || [];
|
|
293
294
|
this.#localRequestInit = {
|
|
294
295
|
...requestOptions.requestConfig,
|
|
295
296
|
method
|
|
296
297
|
};
|
|
297
|
-
this.#retryConfig = requestOptions
|
|
298
|
-
this.#customErrorCbs = requestOptions
|
|
298
|
+
this.#retryConfig = { ...requestOptions?.retryConfig || {} };
|
|
299
|
+
this.#customErrorCbs = { ...requestOptions?.errorCbs || {} };
|
|
299
300
|
this.#throwOnError = requestOptions.throwOnError || false;
|
|
300
301
|
this.#shouldBeResult = requestOptions.shouldBeResult || false;
|
|
302
|
+
this.#capabilities = [...capabilities];
|
|
301
303
|
}
|
|
302
304
|
/**
|
|
303
305
|
* Sets the base URL for the request.
|
|
@@ -652,7 +654,29 @@ var Request = class {
|
|
|
652
654
|
* request.setQueryParams(qp);
|
|
653
655
|
*/
|
|
654
656
|
setQueryParams(params) {
|
|
655
|
-
|
|
657
|
+
let qp;
|
|
658
|
+
if (params instanceof URLSearchParams) {
|
|
659
|
+
qp = new URLSearchParams(params);
|
|
660
|
+
} else if (typeof params === "string") {
|
|
661
|
+
qp = new URLSearchParams(params);
|
|
662
|
+
} else if (Array.isArray(params)) {
|
|
663
|
+
qp = new URLSearchParams();
|
|
664
|
+
for (const entry of params) {
|
|
665
|
+
if (Array.isArray(entry) && entry.length === 2) {
|
|
666
|
+
qp.append(String(entry[0]), String(entry[1]));
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
} else if (typeof params === "object" && params !== null) {
|
|
670
|
+
qp = new URLSearchParams();
|
|
671
|
+
for (const [key, value] of Object.entries(
|
|
672
|
+
params
|
|
673
|
+
)) {
|
|
674
|
+
qp.append(key, String(value));
|
|
675
|
+
}
|
|
676
|
+
} else {
|
|
677
|
+
qp = new URLSearchParams();
|
|
678
|
+
}
|
|
679
|
+
this.#queryParams = qp;
|
|
656
680
|
return this;
|
|
657
681
|
}
|
|
658
682
|
/**
|
|
@@ -893,11 +917,41 @@ var Request = class {
|
|
|
893
917
|
return this.#mapResponse(output);
|
|
894
918
|
}
|
|
895
919
|
#url() {
|
|
920
|
+
if (this.#path.startsWith("http://") || this.#path.startsWith("https://")) {
|
|
921
|
+
const absolute = new URL(this.#path);
|
|
922
|
+
if (this.#queryParams) {
|
|
923
|
+
for (const [k, v] of this.#queryParams.entries()) {
|
|
924
|
+
absolute.searchParams.append(k, v);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return absolute.toString();
|
|
928
|
+
}
|
|
896
929
|
const passedBaseUrl = typeof this.#localRequestInit.baseUrl === "string" ? this.#localRequestInit.baseUrl : this.#localRequestInit.baseUrl.toString();
|
|
897
|
-
const
|
|
898
|
-
const
|
|
899
|
-
const
|
|
900
|
-
|
|
930
|
+
const base = passedBaseUrl.replace(/\/+$/, "");
|
|
931
|
+
const [rawPathAndQuery, fragment] = this.#path.split("#", 2);
|
|
932
|
+
const [rawPath, existingQuery] = rawPathAndQuery.split("?", 2);
|
|
933
|
+
let path = rawPath || "";
|
|
934
|
+
path = path.replace(/^\/+/, "");
|
|
935
|
+
path = path.replace(/\/{2,}/g, "/");
|
|
936
|
+
path = path.replace(/\/+$/, "");
|
|
937
|
+
if (path) {
|
|
938
|
+
path = "/" + path.replace(/^\/+/, "");
|
|
939
|
+
}
|
|
940
|
+
const qs = new URLSearchParams(existingQuery ?? "");
|
|
941
|
+
if (this.#queryParams) {
|
|
942
|
+
for (const [k, v] of this.#queryParams.entries()) {
|
|
943
|
+
qs.append(k, v);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
const queryString = qs.toString();
|
|
947
|
+
let url = base + path;
|
|
948
|
+
if (queryString) {
|
|
949
|
+
url += `?${queryString}`;
|
|
950
|
+
}
|
|
951
|
+
if (fragment) {
|
|
952
|
+
url += `#${fragment}`;
|
|
953
|
+
}
|
|
954
|
+
url = url.replace(/\/+$/, "");
|
|
901
955
|
return url;
|
|
902
956
|
}
|
|
903
957
|
/**
|
|
@@ -996,7 +1050,15 @@ var Request = class {
|
|
|
996
1050
|
let responseData = null;
|
|
997
1051
|
while (attempts <= retries) {
|
|
998
1052
|
try {
|
|
999
|
-
|
|
1053
|
+
if (this.#capabilities.length > 0) {
|
|
1054
|
+
for (const capability of this.#capabilities) {
|
|
1055
|
+
response = await capability({ request }).run(
|
|
1056
|
+
() => fetch(url, requestInit)
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
} else {
|
|
1060
|
+
response = await fetch(url, requestInit);
|
|
1061
|
+
}
|
|
1000
1062
|
responseData = await responseParser(response);
|
|
1001
1063
|
if (responseData instanceof Error) {
|
|
1002
1064
|
return err(responseData);
|
|
@@ -1189,8 +1251,9 @@ var Request = class {
|
|
|
1189
1251
|
return {
|
|
1190
1252
|
response,
|
|
1191
1253
|
status: response.status,
|
|
1192
|
-
|
|
1193
|
-
responseData
|
|
1254
|
+
statusLabel: getHttpErrorStatus(response.status),
|
|
1255
|
+
responseData,
|
|
1256
|
+
statusText: response.statusText
|
|
1194
1257
|
};
|
|
1195
1258
|
}
|
|
1196
1259
|
/**
|
|
@@ -1255,16 +1318,62 @@ var Request = class {
|
|
|
1255
1318
|
const cfg = this.#sanitisedRetryConfig();
|
|
1256
1319
|
return { ...cfg };
|
|
1257
1320
|
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Registers a capability for this request.
|
|
1323
|
+
*
|
|
1324
|
+
* A capability is a small wrapper around the underlying `fetch` call that can
|
|
1325
|
+
* intercept, inspect, or modify the request/response lifecycle. Each registered
|
|
1326
|
+
* capability receives the constructed {@link AspiRequest} and can wrap the
|
|
1327
|
+
* execution of the network call via its `run` method.
|
|
1328
|
+
*
|
|
1329
|
+
* Capabilities are applied in the order they are registered. For each HTTP
|
|
1330
|
+
* call, the `Request` will:
|
|
1331
|
+
*
|
|
1332
|
+
* 1. Build an {@link AspiRequest} from the current configuration.
|
|
1333
|
+
* 2. Create a runner: `() => fetch(url, requestInit)`.
|
|
1334
|
+
* 3. Pass that runner through each capability in sequence, allowing them to:
|
|
1335
|
+
* - log or trace requests,
|
|
1336
|
+
* - implement retry/refresh logic,
|
|
1337
|
+
* - short‑circuit with synthetic responses, etc.
|
|
1338
|
+
*
|
|
1339
|
+
* @param capability - The capability factory to register. It will be invoked
|
|
1340
|
+
* for every execution of this request with the current {@link AspiRequest}.
|
|
1341
|
+
*
|
|
1342
|
+
* @returns This {@link Request} instance, enabling fluent chaining.
|
|
1343
|
+
*
|
|
1344
|
+
* @example
|
|
1345
|
+
* ```ts
|
|
1346
|
+
* const api = new Aspi({ baseUrl: 'https://api.example.com' });
|
|
1347
|
+
*
|
|
1348
|
+
* const apiWithLogging = api.useCapability(({ request }) => ({
|
|
1349
|
+
* async run(runner) {
|
|
1350
|
+
* console.log('→', request.path, request.requestInit);
|
|
1351
|
+
* const res = await runner();
|
|
1352
|
+
* console.log('←', res.status, res.statusText);
|
|
1353
|
+
* return res;
|
|
1354
|
+
* },
|
|
1355
|
+
* }));
|
|
1356
|
+
*
|
|
1357
|
+
* const result = await apiWithLogging
|
|
1358
|
+
* .get('/users')
|
|
1359
|
+
* .withResult()
|
|
1360
|
+
* .json();
|
|
1361
|
+
* ```
|
|
1362
|
+
*/
|
|
1363
|
+
useCapability(capability) {
|
|
1364
|
+
return this.#capabilities.push(capability);
|
|
1365
|
+
}
|
|
1258
1366
|
};
|
|
1259
1367
|
|
|
1260
1368
|
// src/aspi.ts
|
|
1261
|
-
var
|
|
1369
|
+
var Aspi = class {
|
|
1262
1370
|
#globalRequestInit;
|
|
1263
1371
|
#middlewares = [];
|
|
1264
1372
|
#retryConfig;
|
|
1265
1373
|
#customErrorCbs = {};
|
|
1266
1374
|
#throwOnError = false;
|
|
1267
1375
|
#shouldBeResult = false;
|
|
1376
|
+
#capabilities = [];
|
|
1268
1377
|
constructor(config) {
|
|
1269
1378
|
const { retryConfig, ...requestInit } = config;
|
|
1270
1379
|
this.#globalRequestInit = requestInit;
|
|
@@ -1307,17 +1416,22 @@ var Aspi2 = class {
|
|
|
1307
1416
|
return this;
|
|
1308
1417
|
}
|
|
1309
1418
|
#createRequest(method, path) {
|
|
1310
|
-
return new Request(
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1419
|
+
return new Request(
|
|
1420
|
+
method,
|
|
1421
|
+
path,
|
|
1422
|
+
{
|
|
1423
|
+
requestConfig: {
|
|
1424
|
+
...this.#globalRequestInit,
|
|
1425
|
+
method
|
|
1426
|
+
},
|
|
1427
|
+
middlewares: this.#middlewares,
|
|
1428
|
+
errorCbs: this.#customErrorCbs,
|
|
1429
|
+
throwOnError: this.#throwOnError,
|
|
1430
|
+
shouldBeResult: this.#shouldBeResult,
|
|
1431
|
+
retryConfig: this.#retryConfig
|
|
1314
1432
|
},
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
throwOnError: this.#throwOnError,
|
|
1318
|
-
shouldBeResult: this.#shouldBeResult,
|
|
1319
|
-
retryConfig: this.#retryConfig
|
|
1320
|
-
});
|
|
1433
|
+
this.#capabilities
|
|
1434
|
+
);
|
|
1321
1435
|
}
|
|
1322
1436
|
/**
|
|
1323
1437
|
* Makes a GET request to the specified path
|
|
@@ -1585,7 +1699,7 @@ var Aspi2 = class {
|
|
|
1585
1699
|
* api.internalServerError((req, res) => ({ message: 'Server error occurred' }));
|
|
1586
1700
|
*/
|
|
1587
1701
|
internalServerError(cb) {
|
|
1588
|
-
return this.error("
|
|
1702
|
+
return this.error("internalServerError", "INTERNAL_SERVER_ERROR", cb);
|
|
1589
1703
|
}
|
|
1590
1704
|
/**
|
|
1591
1705
|
* Sets the aspi to throw an error if the response status is not successful.
|
|
@@ -1612,9 +1726,50 @@ var Aspi2 = class {
|
|
|
1612
1726
|
this.#throwOnError = false;
|
|
1613
1727
|
return this;
|
|
1614
1728
|
}
|
|
1729
|
+
/**
|
|
1730
|
+
* Registers a capability on this {@link Aspi} instance.
|
|
1731
|
+
*
|
|
1732
|
+
* A capability is a small, pluggable unit that can intercept and wrap the
|
|
1733
|
+
* low‑level `fetch` call used by all requests created from this client.
|
|
1734
|
+
* It is invoked for every request with the constructed {@link AspiRequest},
|
|
1735
|
+
* and can:
|
|
1736
|
+
*
|
|
1737
|
+
* - inspect or mutate the outgoing request (e.g. inject auth headers),
|
|
1738
|
+
* - inspect the raw {@link Response},
|
|
1739
|
+
* - implement cross‑cutting concerns such as logging, tracing, retries,
|
|
1740
|
+
* or token refresh, and
|
|
1741
|
+
* - short‑circuit the network call by returning a synthetic {@link Response}.
|
|
1742
|
+
*
|
|
1743
|
+
* Capabilities registered on the {@link Aspi} instance are propagated to every
|
|
1744
|
+
* {@link Request} created via methods like {@link get}, {@link post}, etc.
|
|
1745
|
+
* They are applied in the order they are registered.
|
|
1746
|
+
*
|
|
1747
|
+
* @param capability - The capability factory to install on this client.
|
|
1748
|
+
*
|
|
1749
|
+
* @returns This {@link Aspi} instance for fluent chaining.
|
|
1750
|
+
*
|
|
1751
|
+
* @example
|
|
1752
|
+
* ```ts
|
|
1753
|
+
* const api = new Aspi({ baseUrl: 'https://api.example.com' })
|
|
1754
|
+
* .useCapability(({ request }) => ({
|
|
1755
|
+
* async run(runner) {
|
|
1756
|
+
* console.log('→', request.path);
|
|
1757
|
+
* const res = await runner();
|
|
1758
|
+
* console.log('←', res.status);
|
|
1759
|
+
* return res;
|
|
1760
|
+
* },
|
|
1761
|
+
* }));
|
|
1762
|
+
*
|
|
1763
|
+
* const user = await api.get('/users/1').throwable().json<User>();
|
|
1764
|
+
* ```
|
|
1765
|
+
*/
|
|
1766
|
+
useCapability(capability) {
|
|
1767
|
+
this.#capabilities.push(capability);
|
|
1768
|
+
return this;
|
|
1769
|
+
}
|
|
1615
1770
|
};
|
|
1616
1771
|
export {
|
|
1617
|
-
|
|
1772
|
+
Aspi,
|
|
1618
1773
|
AspiError,
|
|
1619
1774
|
CustomError,
|
|
1620
1775
|
Request,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aspi",
|
|
3
3
|
"description": "Rest API client for typescript projects with chain of responsibility design pattern.",
|
|
4
|
-
"version": "2.0
|
|
4
|
+
"version": "2.2.0",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"devDependencies": {
|
|
@@ -32,17 +32,6 @@
|
|
|
32
32
|
"url": "git+https://github.com/harshtalks/aspi.git"
|
|
33
33
|
},
|
|
34
34
|
"homepage": "https://github.com/harshtalks/aspi",
|
|
35
|
-
"scripts": {
|
|
36
|
-
"ci": "bun run test:run && bun run build && bun run check-format && bun run lint",
|
|
37
|
-
"format": "prettier --write .",
|
|
38
|
-
"check-format": "prettier --check .",
|
|
39
|
-
"build": "tsup",
|
|
40
|
-
"lint": "tsc",
|
|
41
|
-
"local-release": "changeset version && changeset publish",
|
|
42
|
-
"prepublishOnly": "bun run ci",
|
|
43
|
-
"test": "vitest",
|
|
44
|
-
"test:run": "vitest run"
|
|
45
|
-
},
|
|
46
35
|
"exports": {
|
|
47
36
|
"./package.json": "./package.json",
|
|
48
37
|
".": {
|
|
@@ -52,5 +41,15 @@
|
|
|
52
41
|
},
|
|
53
42
|
"files": [
|
|
54
43
|
"dist"
|
|
55
|
-
]
|
|
56
|
-
|
|
44
|
+
],
|
|
45
|
+
"scripts": {
|
|
46
|
+
"ci": "bun run test:run && bun run build && bun run check-format && bun run lint",
|
|
47
|
+
"format": "prettier --write .",
|
|
48
|
+
"check-format": "prettier --check .",
|
|
49
|
+
"build": "tsup",
|
|
50
|
+
"lint": "tsc",
|
|
51
|
+
"local-release": "changeset version && changeset publish",
|
|
52
|
+
"test": "vitest",
|
|
53
|
+
"test:run": "vitest run"
|
|
54
|
+
}
|
|
55
|
+
}
|