@rexeus/typeweaver-clients 0.4.1 → 0.5.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 CHANGED
@@ -31,7 +31,8 @@ npm install @rexeus/typeweaver-core
31
31
  npx typeweaver generate --input ./api/definitions --output ./api/generated --plugins clients
32
32
  ```
33
33
 
34
- More on the CLI in [@rexeus/typeweaver](https://github.com/rexeus/typeweaver/tree/main/packages/cli/README.md#️-cli).
34
+ More on the CLI in
35
+ [@rexeus/typeweaver](https://github.com/rexeus/typeweaver/tree/main/packages/cli/README.md#️-cli).
35
36
 
36
37
  ## 📂 Generated Output
37
38
 
@@ -48,7 +49,8 @@ Resource-specific HTTP clients are generated as `<ResourceName>Client.ts` files,
48
49
 
49
50
  - **Type-safe HTTP methods** - Method overloads for each operation ensuring compile-time type
50
51
  checking
51
- - **axios based** - Supports all axios features and interceptors by using custom axios instances
52
+ - **fetch based** - Zero dependencies, uses the native fetch API. Supports custom fetch functions
53
+ for middleware and testing
52
54
  - **Response type mapping** - Each response is automatically mapped to the associated response class
53
55
  and an instance of the class is returned. This ensures that all responses are in the defined
54
56
  format and it is type-safe.
@@ -66,8 +68,8 @@ Resource-specific HTTP clients are generated as `<ResourceName>Client.ts` files,
66
68
  import { TodoClient } from "path/to/generated/output";
67
69
 
68
70
  const client = new TodoClient({
69
- axiosInstance: customAxios, // Custom axios instance
70
- baseUrl: "https://api.example.com", // Base URL for all requests
71
+ fetchFn: customFetch, // Custom fetch function (optional, defaults to globalThis.fetch)
72
+ baseUrl: "https://api.example.com", // Base URL for all requests (required)
71
73
  unknownResponseHandling: "throw", // "throw" | "passthrough" for unknown responses
72
74
  // -> In "passthrough" mode, the received status code determines if the response is thrown
73
75
  isSuccessStatusCode: code => code < 400, // Custom success status code predicate, determines whether the response is successful or should be thrown
@@ -5,16 +5,18 @@
5
5
  * @generated by @rexeus/typeweaver
6
6
  */
7
7
 
8
- import axios, { AxiosError } from "axios";
9
8
  import type {
10
9
  IHttpHeader,
11
10
  IHttpParam,
12
11
  IHttpQuery,
13
12
  IHttpResponse,
14
13
  } from "@rexeus/typeweaver-core";
14
+ import { NetworkError } from "./NetworkError";
15
+ import { PathParameterError } from "./PathParameterError";
15
16
  import { RequestCommand } from "./RequestCommand";
17
+ import { ResponseParseError } from "./ResponseParseError";
18
+ import type { NetworkErrorCode } from "./NetworkError";
16
19
  import type { ProcessResponseOptions } from "./RequestCommand";
17
- import type { AxiosHeaderValue, AxiosInstance, AxiosResponse } from "axios";
18
20
 
19
21
  /**
20
22
  * Configuration options for handling unknown responses.
@@ -25,14 +27,25 @@ export type UnknownResponseHandling = "throw" | "passthrough";
25
27
  * Configuration options for ApiClient initialization.
26
28
  */
27
29
  export type ApiClientProps = {
28
- /** Custom Axios instance with pre-configured defaults */
29
- axiosInstance?: AxiosInstance;
30
- /** Base URL for API requests. If not provided, must be set in axiosInstance */
31
- baseUrl?: string;
30
+ /** Custom fetch function for HTTP requests. Defaults to globalThis.fetch */
31
+ readonly fetchFn?: typeof globalThis.fetch;
32
+ /** Base URL for API requests */
33
+ readonly baseUrl: string;
32
34
  /** How to handle unknown responses. Defaults to "throw" */
33
- unknownResponseHandling?: UnknownResponseHandling;
35
+ readonly unknownResponseHandling?: UnknownResponseHandling;
34
36
  /** Predicate to determine if a status code represents success. Defaults to 2xx status codes */
35
- isSuccessStatusCode?: (statusCode: number) => boolean;
37
+ readonly isSuccessStatusCode?: (statusCode: number) => boolean;
38
+ /** Request timeout in milliseconds. When set, requests will be aborted after this duration */
39
+ readonly timeoutMs?: number;
40
+ };
41
+
42
+ const NETWORK_ERROR_MESSAGES: Readonly<
43
+ Partial<Record<NetworkErrorCode, string>>
44
+ > = {
45
+ ECONNREFUSED: "Connection refused",
46
+ ECONNRESET: "Connection reset by peer",
47
+ ENOTFOUND: "DNS lookup failed",
48
+ ETIMEDOUT: "Connection timed out",
36
49
  };
37
50
 
38
51
  /**
@@ -41,49 +54,38 @@ export type ApiClientProps = {
41
54
  * This class provides HTTP request execution with:
42
55
  * - Automatic path parameter substitution (`:param` style)
43
56
  * - Query string building with array support
44
- * - Header normalization (converts to Header-Case)
45
57
  * - Network error handling with specific error messages
46
58
  * - Integration with RequestCommand pattern for type safety
47
59
  */
48
60
  export abstract class ApiClient {
49
- /** The Axios instance used for HTTP requests */
50
- public readonly axiosInstance: AxiosInstance;
51
- /** The base URL for all API requests */
61
+ private readonly fetchFn: typeof globalThis.fetch;
52
62
  public readonly baseUrl: string;
53
- /** How to handle unknown responses */
54
63
  public readonly unknownResponseHandling: UnknownResponseHandling;
55
- /** Predicate to determine if a status code represents success */
56
64
  public readonly isSuccessStatusCode: (statusCode: number) => boolean;
65
+ private readonly timeoutMs: number | undefined;
57
66
 
58
- /**
59
- * Creates a new ApiClient instance.
60
- *
61
- * @param props - Configuration options
62
- * @throws {Error} If no base URL is provided in props or axios instance
63
- */
64
67
  protected constructor(props: ApiClientProps) {
65
- this.axiosInstance = props.axiosInstance ?? axios.create();
68
+ this.fetchFn = props.fetchFn ?? globalThis.fetch.bind(globalThis);
69
+ this.baseUrl = props.baseUrl;
66
70
 
67
- this.baseUrl = props.baseUrl ?? this.axiosInstance.defaults.baseURL ?? "";
68
71
  if (!this.baseUrl) {
69
- throw new Error(
70
- "Base URL must be provided either in axios instance or in constructor"
71
- );
72
+ throw new Error("Base URL must be provided");
73
+ }
74
+
75
+ if (
76
+ props.timeoutMs !== undefined &&
77
+ (props.timeoutMs <= 0 || !Number.isFinite(props.timeoutMs))
78
+ ) {
79
+ throw new Error("timeoutMs must be a positive finite number");
72
80
  }
73
- this.axiosInstance.defaults.baseURL = undefined;
74
81
 
75
82
  this.unknownResponseHandling = props.unknownResponseHandling ?? "throw";
76
83
  this.isSuccessStatusCode =
77
84
  props.isSuccessStatusCode ??
78
85
  ((statusCode: number) => statusCode >= 200 && statusCode < 300);
86
+ this.timeoutMs = props.timeoutMs;
79
87
  }
80
88
 
81
- /**
82
- * Gets the process response options for this client instance.
83
- *
84
- * @returns The configuration options for processing responses
85
- * @protected
86
- */
87
89
  protected get processResponseOptions(): ProcessResponseOptions {
88
90
  return {
89
91
  unknownResponseHandling: this.unknownResponseHandling,
@@ -91,79 +93,247 @@ export abstract class ApiClient {
91
93
  };
92
94
  }
93
95
 
94
- /**
95
- * Executes an HTTP request using the provided command.
96
- *
97
- * This method:
98
- * 1. Substitutes path parameters (e.g., `:id` with actual values)
99
- * 2. Builds the complete URL with query parameters
100
- * 3. Sends the HTTP request via Axios
101
- * 4. Normalizes the response format
102
- * 5. Handles network errors with specific error messages
103
- *
104
- * @param request - The request command containing all HTTP parameters
105
- * @returns Promise resolving to the HTTP response
106
- * @throws {Error} Network errors with specific messages (connection refused, timeout, etc.)
107
- * @protected
108
- */
109
96
  protected async execute(request: RequestCommand): Promise<IHttpResponse> {
110
97
  const { method, path, header, query, param, body } = request;
111
98
 
112
- const headers = this.createHeader(header);
113
99
  const pathWithParam = this.createPath(path, param);
114
- const url = this.createUrl(pathWithParam, query);
100
+ const relativeUrl = this.createUrl(pathWithParam, query);
101
+ const fullUrl = this.buildFullUrl(relativeUrl);
115
102
 
116
- try {
117
- const response = await this.axiosInstance.request({
118
- method,
119
- url,
120
- data: body,
121
- headers,
122
- });
123
-
124
- return this.createResponse(response);
125
- } catch (error) {
126
- if (error instanceof AxiosError) {
127
- if (error.response) {
128
- return this.createResponse(error.response);
129
- }
130
-
131
- // TODO: improve network error handling
132
- if (error.code === "ECONNREFUSED") {
133
- throw new Error("Network error: Connection refused");
134
- }
135
- if (error.code === "ECONNRESET") {
136
- throw new Error("Network error: Connection reset by peer");
137
- }
138
- if (error.code === "ENOTFOUND") {
139
- throw new Error("Network error: DNS lookup failed");
140
- }
141
- if (error.code === "ETIMEDOUT") {
142
- throw new Error("Network error: Connection timed out");
143
- }
103
+ const response = await this.performFetch(method, fullUrl, {
104
+ method,
105
+ headers: this.flattenHeaders(header),
106
+ body: this.serializeBody(body),
107
+ signal:
108
+ this.timeoutMs !== undefined
109
+ ? AbortSignal.timeout(this.timeoutMs)
110
+ : undefined,
111
+ });
144
112
 
145
- throw new Error("Network error: Unknown error");
146
- }
113
+ return await this.createResponse(response, method, fullUrl);
114
+ }
147
115
 
148
- throw new Error(
149
- `Network error: ${error instanceof Error ? error.message : String(error)}`
150
- );
116
+ private async performFetch(
117
+ method: string,
118
+ url: string,
119
+ init: RequestInit
120
+ ): Promise<Response> {
121
+ try {
122
+ return await this.fetchFn(url, init);
123
+ } catch (error) {
124
+ throw this.createNetworkError(error, method, url);
151
125
  }
152
126
  }
153
127
 
154
- private createResponse(response: AxiosResponse): IHttpResponse {
128
+ private async createResponse(
129
+ response: Response,
130
+ method: string,
131
+ url: string
132
+ ): Promise<IHttpResponse> {
155
133
  const header: IHttpHeader = {};
156
- Object.entries(response.headers).forEach(([key, value]) => {
157
- this.addMultiValue(header, key, String(value));
134
+ response.headers.forEach((value, key) => {
135
+ header[key] = value;
158
136
  });
159
137
 
138
+ if (typeof response.headers.getSetCookie === "function") {
139
+ const cookies = response.headers.getSetCookie();
140
+ if (cookies.length > 1) {
141
+ header["set-cookie"] = cookies;
142
+ }
143
+ }
144
+
145
+ const body = await this.parseResponseBody(response, method, url);
146
+
160
147
  return {
161
- body: response.data,
148
+ body,
162
149
  header,
163
150
  statusCode: response.status,
164
151
  };
165
152
  }
166
153
 
154
+ private async readBody<T>(
155
+ read: () => Promise<T>,
156
+ response: Response,
157
+ method: string,
158
+ url: string
159
+ ): Promise<T> {
160
+ try {
161
+ return await read();
162
+ } catch (error) {
163
+ throw new ResponseParseError(
164
+ `Failed to read response body (${method} ${url})`,
165
+ response.status,
166
+ "",
167
+ { cause: error instanceof Error ? error : undefined }
168
+ );
169
+ }
170
+ }
171
+
172
+ private async parseResponseBody(
173
+ response: Response,
174
+ method: string,
175
+ url: string
176
+ ): Promise<unknown> {
177
+ if (response.status === 204 || response.status === 304) {
178
+ return undefined;
179
+ }
180
+
181
+ const contentType = response.headers.get("content-type");
182
+
183
+ if (this.isJsonContentType(contentType)) {
184
+ const text = await this.readBody(
185
+ () => response.text(),
186
+ response,
187
+ method,
188
+ url
189
+ );
190
+ if (!text) return undefined;
191
+ try {
192
+ return JSON.parse(text);
193
+ } catch (parseError) {
194
+ throw new ResponseParseError(
195
+ "Failed to parse JSON response",
196
+ response.status,
197
+ text.slice(0, 200),
198
+ {
199
+ cause: parseError instanceof Error ? parseError : undefined,
200
+ }
201
+ );
202
+ }
203
+ }
204
+
205
+ if (this.isTextContentType(contentType) || !contentType) {
206
+ const text = await this.readBody(
207
+ () => response.text(),
208
+ response,
209
+ method,
210
+ url
211
+ );
212
+ if (!text) return undefined;
213
+ return text;
214
+ }
215
+
216
+ return await this.readBody(
217
+ () => response.arrayBuffer(),
218
+ response,
219
+ method,
220
+ url
221
+ );
222
+ }
223
+
224
+ private isTextContentType(contentType: string | null): boolean {
225
+ if (!contentType) return false;
226
+ return contentType.includes("text/");
227
+ }
228
+
229
+ private createNetworkError(
230
+ error: unknown,
231
+ method: string,
232
+ url: string
233
+ ): NetworkError {
234
+ const context = `(${method} ${url})`;
235
+
236
+ if (
237
+ (error instanceof DOMException || error instanceof Error) &&
238
+ error.name === "TimeoutError"
239
+ ) {
240
+ return new NetworkError(
241
+ `Network error: Request timed out ${context}`,
242
+ "TIMEOUT",
243
+ method,
244
+ url,
245
+ { cause: error }
246
+ );
247
+ }
248
+
249
+ if (
250
+ (error instanceof DOMException || error instanceof Error) &&
251
+ error.name === "AbortError"
252
+ ) {
253
+ return new NetworkError(
254
+ `Network error: Request aborted ${context}`,
255
+ "ABORT",
256
+ method,
257
+ url,
258
+ { cause: error }
259
+ );
260
+ }
261
+
262
+ if (error instanceof TypeError) {
263
+ const cause = (error as TypeError & { cause?: { code?: string } }).cause;
264
+ const code = cause?.code;
265
+
266
+ if (code && code in NETWORK_ERROR_MESSAGES) {
267
+ const message = NETWORK_ERROR_MESSAGES[code as NetworkErrorCode];
268
+ return new NetworkError(
269
+ `Network error: ${message} ${context}`,
270
+ code as NetworkErrorCode,
271
+ method,
272
+ url,
273
+ { cause: error }
274
+ );
275
+ }
276
+ }
277
+
278
+ return new NetworkError(
279
+ `Network error: ${error instanceof Error ? error.message : String(error)} ${context}`,
280
+ "UNKNOWN",
281
+ method,
282
+ url,
283
+ { cause: error }
284
+ );
285
+ }
286
+
287
+ private flattenHeaders(
288
+ header: IHttpHeader
289
+ ): Record<string, string> | undefined {
290
+ if (header === undefined) return undefined;
291
+
292
+ const flattened: Record<string, string> = {};
293
+ for (const [key, value] of Object.entries(header)) {
294
+ flattened[key] = Array.isArray(value) ? value.join(", ") : value;
295
+ }
296
+ return flattened;
297
+ }
298
+
299
+ private serializeBody(
300
+ body: unknown
301
+ ): NonNullable<RequestInit["body"]> | undefined {
302
+ if (body === null || body === undefined) return undefined;
303
+ if (typeof body === "string") return body;
304
+ if (this.isNativeBody(body))
305
+ return body as NonNullable<RequestInit["body"]>;
306
+ return JSON.stringify(body);
307
+ }
308
+
309
+ private isNativeBody(body: unknown): boolean {
310
+ return (
311
+ body instanceof Blob ||
312
+ body instanceof ArrayBuffer ||
313
+ body instanceof FormData ||
314
+ body instanceof URLSearchParams ||
315
+ body instanceof ReadableStream ||
316
+ ArrayBuffer.isView(body)
317
+ );
318
+ }
319
+
320
+ private isJsonContentType(contentType: string | null): boolean {
321
+ if (!contentType) return false;
322
+ return (
323
+ contentType.includes("application/json") || contentType.includes("+json")
324
+ );
325
+ }
326
+
327
+ private buildFullUrl(relativePath: string): string {
328
+ const base = this.baseUrl.endsWith("/")
329
+ ? this.baseUrl.slice(0, -1)
330
+ : this.baseUrl;
331
+ const path = relativePath.startsWith("/")
332
+ ? relativePath
333
+ : `/${relativePath}`;
334
+ return `${base}${path}`;
335
+ }
336
+
167
337
  private createPath(path: string, param?: IHttpParam): string {
168
338
  if (!param) {
169
339
  return path;
@@ -173,8 +343,10 @@ export abstract class ApiClient {
173
343
  const result = acc.replace(`:${key}`, encodeURIComponent(value));
174
344
 
175
345
  if (result === acc) {
176
- throw new Error(
177
- `Path parameter '${key}' is not found in path '${path}'`
346
+ throw new PathParameterError(
347
+ `Path parameter '${key}' is not found in path '${path}'`,
348
+ key,
349
+ path
178
350
  );
179
351
  }
180
352
 
@@ -183,12 +355,9 @@ export abstract class ApiClient {
183
355
  }
184
356
 
185
357
  private createUrl(path: string, query?: IHttpQuery): string {
186
- const base = this.baseUrl.replace(/\/+$/, "");
187
358
  const normalizedPath = path.startsWith("/") ? path : `/${path}`;
188
359
  const queryString = this.buildQueryString(query);
189
- return queryString
190
- ? `${base}${normalizedPath}?${queryString}`
191
- : `${base}${normalizedPath}`;
360
+ return queryString ? `${normalizedPath}?${queryString}` : normalizedPath;
192
361
  }
193
362
 
194
363
  private buildQueryString(query?: IHttpQuery): string {
@@ -213,29 +382,4 @@ export abstract class ApiClient {
213
382
  }
214
383
  return params.toString();
215
384
  }
216
-
217
- private createHeader(header: any): any {
218
- return header;
219
- }
220
-
221
- private addMultiValue(
222
- record: Record<string, string | string[]>,
223
- key: string,
224
- value: AxiosHeaderValue
225
- ): void {
226
- const existing = record[key];
227
- const preparedValue = Array.isArray(value)
228
- ? value.map(String)
229
- : [String(value)];
230
- if (existing) {
231
- if (Array.isArray(existing)) {
232
- existing.push(...preparedValue);
233
- } else {
234
- record[key] = [existing, ...preparedValue];
235
- }
236
- } else {
237
- record[key] =
238
- preparedValue.length > 1 ? preparedValue : (preparedValue[0] as string);
239
- }
240
- }
241
385
  }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * This file was automatically generated by typeweaver.
3
+ * DO NOT EDIT. Instead, modify the source definition file and generate again.
4
+ *
5
+ * @generated by @rexeus/typeweaver
6
+ */
7
+
8
+ /**
9
+ * Discriminated error codes for network-level failures.
10
+ */
11
+ export type NetworkErrorCode =
12
+ | "TIMEOUT"
13
+ | "ABORT"
14
+ | "ECONNREFUSED"
15
+ | "ECONNRESET"
16
+ | "ENOTFOUND"
17
+ | "ETIMEDOUT"
18
+ | "UNKNOWN";
19
+
20
+ /**
21
+ * Typed error for network-level failures during HTTP requests.
22
+ *
23
+ * Provides a discriminated `code` property for programmatic error handling
24
+ * instead of message string parsing. Thrown by ApiClient when the underlying
25
+ * fetch call fails before receiving a response.
26
+ */
27
+ export class NetworkError extends Error {
28
+ public override readonly name = "NetworkError";
29
+
30
+ public constructor(
31
+ message: string,
32
+ public readonly code: NetworkErrorCode,
33
+ public readonly method: string,
34
+ public readonly url: string,
35
+ options?: ErrorOptions
36
+ ) {
37
+ super(message, options);
38
+ }
39
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * This file was automatically generated by typeweaver.
3
+ * DO NOT EDIT. Instead, modify the source definition file and generate again.
4
+ *
5
+ * @generated by @rexeus/typeweaver
6
+ */
7
+
8
+ /**
9
+ * Typed error for path parameter substitution failures.
10
+ *
11
+ * Thrown when a path parameter key does not match any placeholder in the
12
+ * URL path pattern. Exposes `paramName` and `path` properties for
13
+ * programmatic error handling.
14
+ */
15
+ export class PathParameterError extends Error {
16
+ public override readonly name = "PathParameterError";
17
+
18
+ public constructor(
19
+ message: string,
20
+ public readonly paramName: string,
21
+ public readonly path: string,
22
+ options?: ErrorOptions
23
+ ) {
24
+ super(message, options);
25
+ }
26
+ }
@@ -47,8 +47,7 @@ export abstract class RequestCommand<
47
47
  Param extends IHttpParam = IHttpParam | undefined,
48
48
  Query extends IHttpQuery = IHttpQuery | undefined,
49
49
  Body extends IHttpBody = IHttpBody | undefined,
50
- > implements IHttpRequest
51
- {
50
+ > implements IHttpRequest {
52
51
  /** The HTTP method for this request */
53
52
  public readonly method!: HttpMethod;
54
53
  /** The URL path pattern with parameter placeholders (e.g., '/users/:id') */
@@ -0,0 +1,26 @@
1
+ /**
2
+ * This file was automatically generated by typeweaver.
3
+ * DO NOT EDIT. Instead, modify the source definition file and generate again.
4
+ *
5
+ * @generated by @rexeus/typeweaver
6
+ */
7
+
8
+ /**
9
+ * Typed error for response body parsing failures.
10
+ *
11
+ * Thrown when a JSON response body cannot be parsed. Provides structured
12
+ * metadata via `statusCode` and `bodyPreview` properties for diagnostics
13
+ * without relying on message string parsing.
14
+ */
15
+ export class ResponseParseError extends Error {
16
+ public override readonly name = "ResponseParseError";
17
+
18
+ public constructor(
19
+ message: string,
20
+ public readonly statusCode: number,
21
+ public readonly bodyPreview: string,
22
+ options?: ErrorOptions
23
+ ) {
24
+ super(message, options);
25
+ }
26
+ }
package/dist/lib/index.ts CHANGED
@@ -6,4 +6,7 @@
6
6
  */
7
7
 
8
8
  export * from "./ApiClient";
9
+ export * from "./NetworkError";
10
+ export * from "./PathParameterError";
9
11
  export * from "./RequestCommand";
12
+ export * from "./ResponseParseError";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rexeus/typeweaver-clients",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Generates HTTP clients directly from your API definitions. Powered by Typeweaver 🧵✨",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -47,22 +47,20 @@
47
47
  },
48
48
  "homepage": "https://github.com/rexeus/typeweaver#readme",
49
49
  "peerDependencies": {
50
- "axios": "^1.13.0",
51
50
  "zod": "^4.3.0",
52
- "@rexeus/typeweaver-core": "^0.4.1",
53
- "@rexeus/typeweaver-gen": "^0.4.1"
51
+ "@rexeus/typeweaver-gen": "^0.5.0",
52
+ "@rexeus/typeweaver-core": "^0.5.0"
54
53
  },
55
54
  "devDependencies": {
56
55
  "@hono/node-server": "^1.19.7",
57
- "axios": "^1.13.2",
58
56
  "test-utils": "file:../test-utils",
59
57
  "zod": "^4.3.6",
60
- "@rexeus/typeweaver-core": "^0.4.1",
61
- "@rexeus/typeweaver-gen": "^0.4.1"
58
+ "@rexeus/typeweaver-core": "^0.5.0",
59
+ "@rexeus/typeweaver-gen": "^0.5.0"
62
60
  },
63
61
  "dependencies": {
64
62
  "case": "^1.6.3",
65
- "@rexeus/typeweaver-zod-to-ts": "^0.4.1"
63
+ "@rexeus/typeweaver-zod-to-ts": "^0.5.0"
66
64
  },
67
65
  "scripts": {
68
66
  "typecheck": "tsc --noEmit",