@fragno-dev/core 0.1.1 → 0.1.3
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/.turbo/turbo-build.log +41 -32
- package/CHANGELOG.md +15 -0
- package/LICENSE.md +16 -0
- package/dist/api/api.d.ts +1 -1
- package/dist/api/fragment-builder.d.ts +2 -2
- package/dist/api/fragment-instantiation.d.ts +2 -2
- package/dist/api/fragment-instantiation.js +3 -2
- package/dist/{api-Dcr4_-3g.d.ts → api-BX90b4-D.d.ts} +92 -6
- package/dist/api-BX90b4-D.d.ts.map +1 -0
- package/dist/client/client.d.ts +2 -2
- package/dist/client/client.js +4 -3
- package/dist/client/client.svelte.d.ts +2 -2
- package/dist/client/client.svelte.js +4 -3
- package/dist/client/client.svelte.js.map +1 -1
- package/dist/client/react.d.ts +2 -2
- package/dist/client/react.d.ts.map +1 -1
- package/dist/client/react.js +4 -3
- package/dist/client/react.js.map +1 -1
- package/dist/client/solid.d.ts +2 -2
- package/dist/client/solid.js +4 -3
- package/dist/client/solid.js.map +1 -1
- package/dist/client/vanilla.d.ts +2 -2
- package/dist/client/vanilla.js +4 -3
- package/dist/client/vanilla.js.map +1 -1
- package/dist/client/vue.d.ts +2 -2
- package/dist/client/vue.js +4 -3
- package/dist/client/vue.js.map +1 -1
- package/dist/{client-D5ORmjBP.js → client-C6LChM0Y.js} +4 -3
- package/dist/client-C6LChM0Y.js.map +1 -0
- package/dist/{fragment-builder-D6-oLYnH.d.ts → fragment-builder-BZr2JkuW.d.ts} +51 -38
- package/dist/fragment-builder-BZr2JkuW.d.ts.map +1 -0
- package/dist/fragment-builder-DOnCVBqc.js.map +1 -1
- package/dist/{fragment-instantiation-f4AhwQss.js → fragment-instantiation-DMw8OKMC.js} +137 -11
- package/dist/fragment-instantiation-DMw8OKMC.js.map +1 -0
- package/dist/integrations/react-ssr.js +1 -1
- package/dist/mod.d.ts +2 -2
- package/dist/mod.js +3 -2
- package/dist/route-CTxjMtGZ.js +10 -0
- package/dist/route-CTxjMtGZ.js.map +1 -0
- package/dist/{route-B4RbOWjd.js → route-D1MZR6JL.js} +22 -22
- package/dist/route-D1MZR6JL.js.map +1 -0
- package/dist/{ssr-CamRrMc0.js → ssr-BByDVfFD.js} +1 -1
- package/dist/{ssr-CamRrMc0.js.map → ssr-BByDVfFD.js.map} +1 -1
- package/dist/test/test.d.ts +112 -0
- package/dist/test/test.d.ts.map +1 -0
- package/dist/test/test.js +155 -0
- package/dist/test/test.js.map +1 -0
- package/package.json +18 -24
- package/src/api/fragment-builder.ts +0 -1
- package/src/api/fragment-instantiation.ts +16 -3
- package/src/api/mutable-request-state.ts +107 -0
- package/src/api/request-input-context.test.ts +51 -0
- package/src/api/request-input-context.ts +20 -13
- package/src/api/request-middleware.test.ts +88 -2
- package/src/api/request-middleware.ts +28 -6
- package/src/api/request-output-context.test.ts +6 -2
- package/src/api/request-output-context.ts +15 -9
- package/src/client/component.test.svelte +2 -0
- package/src/client/internal/ndjson-streaming.ts +6 -2
- package/src/client/react.ts +3 -1
- package/src/test/test.test.ts +449 -0
- package/src/test/test.ts +379 -0
- package/src/util/async.test.ts +6 -2
- package/tsdown.config.ts +1 -0
- package/.turbo/turbo-test.log +0 -297
- package/.turbo/turbo-types$colon$check.log +0 -1
- package/dist/api-Dcr4_-3g.d.ts.map +0 -1
- package/dist/client-D5ORmjBP.js.map +0 -1
- package/dist/fragment-builder-D6-oLYnH.d.ts.map +0 -1
- package/dist/fragment-instantiation-f4AhwQss.js.map +0 -1
- package/dist/route-B4RbOWjd.js.map +0 -1
package/src/test/test.ts
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
import type { FragmentDefinition } from "../api/fragment-builder";
|
|
3
|
+
import type { FragnoRouteConfig, HTTPMethod } from "../api/api";
|
|
4
|
+
import type { ExtractPathParams } from "../api/internal/path";
|
|
5
|
+
import { RequestInputContext } from "../api/request-input-context";
|
|
6
|
+
import { RequestOutputContext } from "../api/request-output-context";
|
|
7
|
+
import type { AnyRouteOrFactory, FlattenRouteFactories } from "../api/route";
|
|
8
|
+
import { resolveRouteFactories } from "../api/route";
|
|
9
|
+
import type { FragnoPublicConfig } from "../api/fragment-instantiation";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Discriminated union representing all possible test response types
|
|
13
|
+
*/
|
|
14
|
+
export type TestResponse<T> =
|
|
15
|
+
| {
|
|
16
|
+
type: "empty";
|
|
17
|
+
status: number;
|
|
18
|
+
headers: Headers;
|
|
19
|
+
}
|
|
20
|
+
| {
|
|
21
|
+
type: "error";
|
|
22
|
+
status: number;
|
|
23
|
+
headers: Headers;
|
|
24
|
+
error: { message: string; code: string };
|
|
25
|
+
}
|
|
26
|
+
| {
|
|
27
|
+
type: "json";
|
|
28
|
+
status: number;
|
|
29
|
+
headers: Headers;
|
|
30
|
+
data: T;
|
|
31
|
+
}
|
|
32
|
+
| {
|
|
33
|
+
type: "jsonStream";
|
|
34
|
+
status: number;
|
|
35
|
+
headers: Headers;
|
|
36
|
+
stream: AsyncGenerator<T>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parse a Response object into a TestResponse discriminated union
|
|
41
|
+
*/
|
|
42
|
+
async function parseResponse<T>(response: Response): Promise<TestResponse<T>> {
|
|
43
|
+
const status = response.status;
|
|
44
|
+
const headers = response.headers;
|
|
45
|
+
const contentType = headers.get("content-type") || "";
|
|
46
|
+
|
|
47
|
+
// Check for streaming response
|
|
48
|
+
if (contentType.includes("application/x-ndjson")) {
|
|
49
|
+
return {
|
|
50
|
+
type: "jsonStream",
|
|
51
|
+
status,
|
|
52
|
+
headers,
|
|
53
|
+
stream: parseNDJSONStream<T>(response),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Parse JSON body
|
|
58
|
+
const text = await response.text();
|
|
59
|
+
|
|
60
|
+
// Empty response
|
|
61
|
+
if (!text || text === "null") {
|
|
62
|
+
return {
|
|
63
|
+
type: "empty",
|
|
64
|
+
status,
|
|
65
|
+
headers,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const data = JSON.parse(text);
|
|
70
|
+
|
|
71
|
+
// Error response (has message and code)
|
|
72
|
+
if (data && typeof data === "object" && "message" in data && "code" in data) {
|
|
73
|
+
return {
|
|
74
|
+
type: "error",
|
|
75
|
+
status,
|
|
76
|
+
headers,
|
|
77
|
+
error: { message: data.message, code: data.code },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// JSON response
|
|
82
|
+
return {
|
|
83
|
+
type: "json",
|
|
84
|
+
status,
|
|
85
|
+
headers,
|
|
86
|
+
data: data as T,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Parse an NDJSON stream into an async generator
|
|
92
|
+
*/
|
|
93
|
+
async function* parseNDJSONStream<T>(response: Response): AsyncGenerator<T> {
|
|
94
|
+
if (!response.body) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const reader = response.body.getReader();
|
|
99
|
+
const decoder = new TextDecoder();
|
|
100
|
+
let buffer = "";
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
while (true) {
|
|
104
|
+
const { done, value } = await reader.read();
|
|
105
|
+
|
|
106
|
+
if (done) {
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
buffer += decoder.decode(value, { stream: true });
|
|
111
|
+
const lines = buffer.split("\n");
|
|
112
|
+
|
|
113
|
+
// Keep the last incomplete line in the buffer
|
|
114
|
+
buffer = lines.pop() || "";
|
|
115
|
+
|
|
116
|
+
for (const line of lines) {
|
|
117
|
+
if (line.trim()) {
|
|
118
|
+
yield JSON.parse(line) as T;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Process any remaining data in the buffer
|
|
124
|
+
if (buffer.trim()) {
|
|
125
|
+
yield JSON.parse(buffer) as T;
|
|
126
|
+
}
|
|
127
|
+
} finally {
|
|
128
|
+
reader.releaseLock();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Options for creating a test fragment
|
|
134
|
+
*/
|
|
135
|
+
export interface CreateFragmentForTestOptions<
|
|
136
|
+
TConfig,
|
|
137
|
+
TDeps,
|
|
138
|
+
TServices,
|
|
139
|
+
TAdditionalContext extends Record<string, unknown>,
|
|
140
|
+
TOptions extends FragnoPublicConfig,
|
|
141
|
+
> {
|
|
142
|
+
config: TConfig;
|
|
143
|
+
options?: Partial<TOptions>;
|
|
144
|
+
deps?: Partial<TDeps>;
|
|
145
|
+
services?: Partial<TServices>;
|
|
146
|
+
additionalContext?: Partial<TAdditionalContext>;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Options for calling a route handler
|
|
151
|
+
*/
|
|
152
|
+
export interface RouteHandlerInputOptions<
|
|
153
|
+
TPath extends string,
|
|
154
|
+
TInputSchema extends StandardSchemaV1 | undefined,
|
|
155
|
+
> {
|
|
156
|
+
pathParams?: ExtractPathParams<TPath>;
|
|
157
|
+
body?: TInputSchema extends StandardSchemaV1 ? StandardSchemaV1.InferInput<TInputSchema> : never;
|
|
158
|
+
query?: URLSearchParams | Record<string, string>;
|
|
159
|
+
headers?: Headers | Record<string, string>;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Options for overriding config/deps/services when initializing routes
|
|
164
|
+
*/
|
|
165
|
+
export interface InitRoutesOverrides<TConfig, TDeps, TServices> {
|
|
166
|
+
config?: Partial<TConfig>;
|
|
167
|
+
deps?: Partial<TDeps>;
|
|
168
|
+
services?: Partial<TServices>;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Fragment test instance with type-safe handler method
|
|
173
|
+
*/
|
|
174
|
+
export interface FragmentForTest<
|
|
175
|
+
TConfig,
|
|
176
|
+
TDeps,
|
|
177
|
+
TServices,
|
|
178
|
+
TAdditionalContext extends Record<string, unknown>,
|
|
179
|
+
TOptions extends FragnoPublicConfig,
|
|
180
|
+
> {
|
|
181
|
+
config: TConfig;
|
|
182
|
+
deps: TDeps;
|
|
183
|
+
services: TServices;
|
|
184
|
+
additionalContext: TAdditionalContext & TOptions;
|
|
185
|
+
handler: <
|
|
186
|
+
TMethod extends HTTPMethod,
|
|
187
|
+
TPath extends string,
|
|
188
|
+
TInputSchema extends StandardSchemaV1 | undefined,
|
|
189
|
+
TOutputSchema extends StandardSchemaV1 | undefined,
|
|
190
|
+
TErrorCode extends string,
|
|
191
|
+
TQueryParameters extends string,
|
|
192
|
+
>(
|
|
193
|
+
route: FragnoRouteConfig<
|
|
194
|
+
TMethod,
|
|
195
|
+
TPath,
|
|
196
|
+
TInputSchema,
|
|
197
|
+
TOutputSchema,
|
|
198
|
+
TErrorCode,
|
|
199
|
+
TQueryParameters
|
|
200
|
+
>,
|
|
201
|
+
inputOptions?: RouteHandlerInputOptions<TPath, TInputSchema>,
|
|
202
|
+
) => Promise<
|
|
203
|
+
TestResponse<
|
|
204
|
+
TOutputSchema extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<TOutputSchema> : unknown
|
|
205
|
+
>
|
|
206
|
+
>;
|
|
207
|
+
initRoutes: <const TRoutesOrFactories extends readonly AnyRouteOrFactory[]>(
|
|
208
|
+
routesOrFactories: TRoutesOrFactories,
|
|
209
|
+
overrides?: InitRoutesOverrides<TConfig, TDeps, TServices>,
|
|
210
|
+
) => FlattenRouteFactories<TRoutesOrFactories>;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Create a fragment instance for testing with optional dependency and service overrides
|
|
215
|
+
*
|
|
216
|
+
* @param fragmentBuilder - The fragment builder with definition and required options
|
|
217
|
+
* @param options - Configuration and optional overrides for deps/services
|
|
218
|
+
* @returns A fragment test instance with a type-safe handler method
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* ```typescript
|
|
222
|
+
* const fragment = createFragmentForTest(chatnoDefinition, {
|
|
223
|
+
* config: { openaiApiKey: "test-key" },
|
|
224
|
+
* options: { mountRoute: "/api/chatno" },
|
|
225
|
+
* services: {
|
|
226
|
+
* generateStreamMessages: mockGenerator
|
|
227
|
+
* }
|
|
228
|
+
* });
|
|
229
|
+
*
|
|
230
|
+
* // Initialize routes with fragment's config/deps/services
|
|
231
|
+
* const [route] = fragment.initRoutes(routes);
|
|
232
|
+
*
|
|
233
|
+
* // Or override specific config/deps/services for certain routes
|
|
234
|
+
* const [route] = fragment.initRoutes(routes, {
|
|
235
|
+
* services: { mockService: mockImplementation }
|
|
236
|
+
* });
|
|
237
|
+
*
|
|
238
|
+
* const response = await fragment.handler(route, {
|
|
239
|
+
* pathParams: { id: "123" },
|
|
240
|
+
* body: { message: "Hello" }
|
|
241
|
+
* });
|
|
242
|
+
*
|
|
243
|
+
* if (response.type === 'json') {
|
|
244
|
+
* expect(response.data).toEqual({...});
|
|
245
|
+
* }
|
|
246
|
+
* ```
|
|
247
|
+
*/
|
|
248
|
+
export function createFragmentForTest<
|
|
249
|
+
TConfig,
|
|
250
|
+
TDeps,
|
|
251
|
+
TServices extends Record<string, unknown>,
|
|
252
|
+
TAdditionalContext extends Record<string, unknown>,
|
|
253
|
+
TOptions extends FragnoPublicConfig,
|
|
254
|
+
>(
|
|
255
|
+
fragmentBuilder: {
|
|
256
|
+
definition: FragmentDefinition<TConfig, TDeps, TServices, TAdditionalContext>;
|
|
257
|
+
$requiredOptions: TOptions;
|
|
258
|
+
},
|
|
259
|
+
options: CreateFragmentForTestOptions<TConfig, TDeps, TServices, TAdditionalContext, TOptions>,
|
|
260
|
+
): FragmentForTest<TConfig, TDeps, TServices, TAdditionalContext, TOptions> {
|
|
261
|
+
const {
|
|
262
|
+
config,
|
|
263
|
+
options: fragmentOptions = {} as TOptions,
|
|
264
|
+
deps: depsOverride,
|
|
265
|
+
services: servicesOverride,
|
|
266
|
+
additionalContext: additionalContextOverride,
|
|
267
|
+
} = options;
|
|
268
|
+
|
|
269
|
+
// Create deps from definition or use empty object
|
|
270
|
+
const definition = fragmentBuilder.definition;
|
|
271
|
+
const baseDeps = definition.dependencies
|
|
272
|
+
? definition.dependencies(config, fragmentOptions)
|
|
273
|
+
: ({} as TDeps);
|
|
274
|
+
|
|
275
|
+
// Merge deps with overrides
|
|
276
|
+
const deps = { ...baseDeps, ...depsOverride } as TDeps;
|
|
277
|
+
|
|
278
|
+
// Create services from definition or use empty object
|
|
279
|
+
const baseServices = definition.services
|
|
280
|
+
? definition.services(config, fragmentOptions, deps)
|
|
281
|
+
: ({} as TServices);
|
|
282
|
+
|
|
283
|
+
// Merge services with overrides
|
|
284
|
+
const services = { ...baseServices, ...servicesOverride } as TServices;
|
|
285
|
+
|
|
286
|
+
// Merge additional context with options
|
|
287
|
+
const additionalContext = {
|
|
288
|
+
...definition.additionalContext,
|
|
289
|
+
...fragmentOptions,
|
|
290
|
+
...additionalContextOverride,
|
|
291
|
+
} as TAdditionalContext & TOptions;
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
config,
|
|
295
|
+
deps,
|
|
296
|
+
services,
|
|
297
|
+
additionalContext,
|
|
298
|
+
initRoutes: <const TRoutesOrFactories extends readonly AnyRouteOrFactory[]>(
|
|
299
|
+
routesOrFactories: TRoutesOrFactories,
|
|
300
|
+
overrides?: InitRoutesOverrides<TConfig, TDeps, TServices>,
|
|
301
|
+
): FlattenRouteFactories<TRoutesOrFactories> => {
|
|
302
|
+
// Merge overrides with base config/deps/services
|
|
303
|
+
const routeContext = {
|
|
304
|
+
config: { ...config, ...overrides?.config } as TConfig,
|
|
305
|
+
deps: { ...deps, ...overrides?.deps } as TDeps,
|
|
306
|
+
services: { ...services, ...overrides?.services } as TServices,
|
|
307
|
+
};
|
|
308
|
+
return resolveRouteFactories(routeContext, routesOrFactories);
|
|
309
|
+
},
|
|
310
|
+
handler: async <
|
|
311
|
+
TMethod extends HTTPMethod,
|
|
312
|
+
TPath extends string,
|
|
313
|
+
TInputSchema extends StandardSchemaV1 | undefined,
|
|
314
|
+
TOutputSchema extends StandardSchemaV1 | undefined,
|
|
315
|
+
TErrorCode extends string,
|
|
316
|
+
TQueryParameters extends string,
|
|
317
|
+
>(
|
|
318
|
+
route: FragnoRouteConfig<
|
|
319
|
+
TMethod,
|
|
320
|
+
TPath,
|
|
321
|
+
TInputSchema,
|
|
322
|
+
TOutputSchema,
|
|
323
|
+
TErrorCode,
|
|
324
|
+
TQueryParameters
|
|
325
|
+
>,
|
|
326
|
+
inputOptions?: RouteHandlerInputOptions<TPath, TInputSchema>,
|
|
327
|
+
): Promise<
|
|
328
|
+
TestResponse<
|
|
329
|
+
TOutputSchema extends StandardSchemaV1
|
|
330
|
+
? StandardSchemaV1.InferOutput<TOutputSchema>
|
|
331
|
+
: unknown
|
|
332
|
+
>
|
|
333
|
+
> => {
|
|
334
|
+
const {
|
|
335
|
+
pathParams = {} as ExtractPathParams<TPath>,
|
|
336
|
+
body,
|
|
337
|
+
query,
|
|
338
|
+
headers,
|
|
339
|
+
} = inputOptions || {};
|
|
340
|
+
|
|
341
|
+
// Convert query to URLSearchParams if needed
|
|
342
|
+
const searchParams =
|
|
343
|
+
query instanceof URLSearchParams
|
|
344
|
+
? query
|
|
345
|
+
: query
|
|
346
|
+
? new URLSearchParams(query)
|
|
347
|
+
: new URLSearchParams();
|
|
348
|
+
|
|
349
|
+
// Convert headers to Headers if needed
|
|
350
|
+
const requestHeaders =
|
|
351
|
+
headers instanceof Headers ? headers : headers ? new Headers(headers) : new Headers();
|
|
352
|
+
|
|
353
|
+
// Construct RequestInputContext
|
|
354
|
+
const inputContext = new RequestInputContext<TPath, TInputSchema>({
|
|
355
|
+
path: route.path,
|
|
356
|
+
method: route.method,
|
|
357
|
+
pathParams,
|
|
358
|
+
searchParams,
|
|
359
|
+
headers: requestHeaders,
|
|
360
|
+
body,
|
|
361
|
+
inputSchema: route.inputSchema,
|
|
362
|
+
shouldValidateInput: false, // Skip validation in tests
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Construct RequestOutputContext
|
|
366
|
+
const outputContext = new RequestOutputContext(route.outputSchema);
|
|
367
|
+
|
|
368
|
+
// Call the route handler
|
|
369
|
+
const response = await route.handler(inputContext, outputContext);
|
|
370
|
+
|
|
371
|
+
// Parse and return the response
|
|
372
|
+
return parseResponse<
|
|
373
|
+
TOutputSchema extends StandardSchemaV1
|
|
374
|
+
? StandardSchemaV1.InferOutput<TOutputSchema>
|
|
375
|
+
: unknown
|
|
376
|
+
>(response);
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
}
|
package/src/util/async.test.ts
CHANGED
|
@@ -20,7 +20,9 @@ describe("createAsyncIteratorFromCallback", () => {
|
|
|
20
20
|
const values: string[] = [];
|
|
21
21
|
for await (const value of iterator) {
|
|
22
22
|
values.push(value);
|
|
23
|
-
if (values.length === 3)
|
|
23
|
+
if (values.length === 3) {
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
24
26
|
}
|
|
25
27
|
return values;
|
|
26
28
|
})();
|
|
@@ -53,7 +55,9 @@ describe("createAsyncIteratorFromCallback", () => {
|
|
|
53
55
|
const values: string[] = [];
|
|
54
56
|
for await (const value of iterator) {
|
|
55
57
|
values.push(value);
|
|
56
|
-
if (values.length === 2)
|
|
58
|
+
if (values.length === 2) {
|
|
59
|
+
break;
|
|
60
|
+
} // Break after 2 values
|
|
57
61
|
}
|
|
58
62
|
return values;
|
|
59
63
|
})();
|
package/tsdown.config.ts
CHANGED
|
@@ -16,6 +16,7 @@ export default defineConfig({
|
|
|
16
16
|
"./src/integrations/next-js.ts",
|
|
17
17
|
"./src/integrations/react-ssr.ts",
|
|
18
18
|
"./src/integrations/svelte-kit.ts",
|
|
19
|
+
"./src/test/test.ts",
|
|
19
20
|
],
|
|
20
21
|
dts: true,
|
|
21
22
|
// TODO: This should be true, but we need some additional type exports in chatno/src/index.ts
|