@blimu/codegen 0.1.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 ADDED
@@ -0,0 +1,142 @@
1
+ # @blimu/codegen
2
+
3
+ A powerful TypeScript library and CLI tool for generating type-safe SDKs from OpenAPI specifications. Built with NestJS Commander and following NestJS conventions.
4
+
5
+ ## Features
6
+
7
+ - 🚀 **Multiple Language Support**: TypeScript, Go, Python (with more planned)
8
+ - 📝 **OpenAPI 3.x Support**: Full support for modern OpenAPI specifications
9
+ - 🎯 **Tag Filtering**: Include/exclude specific API endpoints by tags
10
+ - 🔧 **Highly Configurable**: Flexible configuration via MJS config files with TypeScript hints
11
+ - 📦 **Library & CLI**: Use as a TypeScript library or standalone CLI tool
12
+ - 🎨 **Beautiful Generated Code**: Clean, idiomatic code with excellent TypeScript types
13
+ - ⚡ **Function-Based Transforms**: Use JavaScript functions for operationId transformation with full type safety
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ yarn add @blimu/codegen
19
+ # or
20
+ npm install @blimu/codegen
21
+ ```
22
+
23
+ ## CLI Usage
24
+
25
+ ### Using Command Line Arguments
26
+
27
+ ```bash
28
+ # Generate TypeScript SDK from OpenAPI spec
29
+ codegen generate \
30
+ --input https://petstore3.swagger.io/api/v3/openapi.json \
31
+ --type typescript \
32
+ --out ./petstore-sdk \
33
+ --package-name petstore-client \
34
+ --client-name PetStoreClient
35
+ ```
36
+
37
+ ### Using Configuration File
38
+
39
+ The CLI automatically looks for `chunkflow-codegen.config.mjs` in the current directory and parent directories. You can also specify a custom path:
40
+
41
+ ```bash
42
+ # Auto-discover chunkflow-codegen.config.mjs
43
+ codegen generate
44
+
45
+ # Use explicit config file path
46
+ codegen generate --config ./chunkflow-codegen.config.mjs
47
+
48
+ # Generate only a specific client from config
49
+ codegen generate --client MyClient
50
+ ```
51
+
52
+ ### Configuration File Example
53
+
54
+ Create `chunkflow-codegen.config.mjs` in your project root:
55
+
56
+ ```javascript
57
+ // @ts-check
58
+ import { defineConfig } from "@blimu/codegen";
59
+
60
+ export default defineConfig({
61
+ spec: "http://localhost:3020/docs/backend-api/json",
62
+ clients: [
63
+ {
64
+ type: "typescript",
65
+ outDir: "./my-sdk",
66
+ packageName: "my-sdk",
67
+ name: "MyClient",
68
+ operationIdParser: (operationId, method, path) => {
69
+ // Custom transform logic
70
+ return operationId.replace(/Controller/g, "");
71
+ },
72
+ },
73
+ ],
74
+ });
75
+ ```
76
+
77
+ The `// @ts-check` directive enables TypeScript type checking and autocomplete in your config file!
78
+
79
+ See `examples/chunkflow-codegen.config.mjs.example` for a complete example with all available options.
80
+
81
+ ## Programmatic API
82
+
83
+ Use the codegen library programmatically in your TypeScript code:
84
+
85
+ ```typescript
86
+ import { generate, loadConfig, defineConfig } from "@blimu/codegen";
87
+
88
+ // Generate from config object
89
+ await generate({
90
+ spec: "http://localhost:3020/docs/backend-api/json",
91
+ clients: [
92
+ {
93
+ type: "typescript",
94
+ outDir: "./my-sdk",
95
+ packageName: "my-sdk",
96
+ name: "MyClient",
97
+ operationIdParser: (operationId, method, path) => {
98
+ return operationId.replace(/Controller/g, "");
99
+ },
100
+ },
101
+ ],
102
+ });
103
+
104
+ // Generate from config file path
105
+ await generate("./chunkflow-codegen.config.mjs");
106
+
107
+ // Generate only a specific client
108
+ await generate("./chunkflow-codegen.config.mjs", { client: "MyClient" });
109
+
110
+ // Load config programmatically
111
+ const config = await loadConfig("./chunkflow-codegen.config.mjs");
112
+ ```
113
+
114
+ ## Configuration Options
115
+
116
+ ### Top-Level Config
117
+
118
+ - `spec` (required): OpenAPI spec file path or URL
119
+ - `name` (optional): Name for this configuration
120
+ - `clients` (required): Array of client configurations
121
+
122
+ ### Client Configuration
123
+
124
+ - `type` (required): Generator type (e.g., `'typescript'`)
125
+ - `outDir` (required): Output directory for generated SDK
126
+ - `packageName` (required): Package name for the generated SDK
127
+ - `name` (required): Client class name
128
+ - `moduleName` (optional): Module name for type augmentation generators
129
+ - `includeTags` (optional): Array of regex patterns for tags to include
130
+ - `excludeTags` (optional): Array of regex patterns for tags to exclude
131
+ - `includeQueryKeys` (optional): Generate query key helper methods
132
+ - `operationIdParser` (optional): Function to transform operationId to method name
133
+ - Signature: `(operationId: string, method: string, path: string) => string | Promise<string>`
134
+ - `preCommand` (optional): Commands to run before SDK generation
135
+ - `postCommand` (optional): Commands to run after SDK generation
136
+ - `defaultBaseURL` (optional): Default base URL for the client
137
+ - `exclude` (optional): Array of file paths to exclude from generation
138
+ - `typeAugmentation` (optional): Options for type augmentation generators
139
+
140
+ ## License
141
+
142
+ MIT
@@ -0,0 +1,123 @@
1
+ # {{Client.name}} TypeScript SDK
2
+
3
+ This is an auto-generated TypeScript/JavaScript SDK for the {{Client.name}} API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install {{Client.packageName}}
9
+ # or
10
+ yarn add {{Client.packageName}}
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```typescript
16
+ import { {{pascal Client.name}}Client } from '{{Client.packageName}}';
17
+
18
+ // Create a new client
19
+ const client = new {{pascal Client.name}}Client({
20
+ baseURL: '{{Client.defaultBaseURL}}',
21
+ timeoutMs: 10000,
22
+ retry: { retries: 2, backoffMs: 300, retryOn: [429, 500, 502, 503, 504] },
23
+ // Environment-based baseURL (optional)
24
+ env: 'sandbox',
25
+ envBaseURLs: { sandbox: 'https://api-sandbox.example.com', production: 'https://api.example.com' },
26
+ // Auth (generic API Key or Bearer header)
27
+ accessToken: process.env.API_TOKEN,
28
+ headerName: 'access_token', // or 'Authorization' (defaults to Authorization: Bearer <token>)
29
+ });
30
+
31
+ {{#each IR.services}}
32
+ {{#if (gt (len this.operations) 0)}}
33
+ {{#with (index this.operations 0) as |firstOp|}}
34
+ // Example: {{firstOp.summary}}
35
+ try {
36
+ const result = await client.{{serviceProp ../tag}}.{{methodName firstOp}}(
37
+ {{#each (pathParamsInOrder firstOp)}}{{#if @index}}, {{/if}}'{{this.name}}'{{/each}}
38
+ {{#if (gt (len firstOp.queryParams) 0)}}{{#if (gt (len (pathParamsInOrder firstOp)) 0)}}, {{/if}}{
39
+ {{#each firstOp.queryParams}}
40
+ {{#if this.required}}
41
+ {{this.name}}: {{#if (eq this.schema.kind "string")}}'example'{{else if (eq this.schema.kind "integer")}}123{{else if (eq this.schema.kind "boolean")}}true{{else}}undefined{{/if}},
42
+ {{/if}}
43
+ {{/each}}
44
+ }{{/if}}
45
+ {{#if firstOp.requestBody}}{{#if (or (gt (len (pathParamsInOrder firstOp)) 0) (gt (len firstOp.queryParams) 0))}}, {{/if}}{
46
+ // Request body data
47
+ }{{/if}}
48
+ );
49
+ console.log('Result:', result);
50
+ } catch (error) {
51
+ // FetchError with structured data
52
+ console.error(error);
53
+ }
54
+ {{/with}}
55
+ {{/if}}
56
+ {{/each}}
57
+ ```
58
+
59
+ ## TypeScript Support
60
+
61
+ This SDK is written in TypeScript and provides full type safety:
62
+
63
+ ```typescript
64
+ import { {{pascal Client.name}}Client, Schema } from '{{Client.packageName}}';
65
+
66
+ const client = new {{pascal Client.name}}Client({ /* config */ });
67
+
68
+ // All methods are fully typed
69
+ // Schema types are available
70
+ {{#if IR.modelDefs}}
71
+ {{#with (index IR.modelDefs 0) as |firstModel|}}
72
+ const data: Schema.{{firstModel.name}} = {
73
+ // Fully typed object
74
+ };
75
+ {{/with}}
76
+ {{/if}}
77
+ ```
78
+
79
+ ## Node.js Usage
80
+
81
+ For Node.js environments, you may need to provide a fetch implementation:
82
+
83
+ ```bash
84
+ npm install undici
85
+ ```
86
+
87
+ ```typescript
88
+ import { fetch } from 'undici';
89
+ import { {{pascal Client.name}}Client } from '{{Client.packageName}}';
90
+
91
+ const client = new {{pascal Client.name}}Client({
92
+ baseURL: '{{Client.defaultBaseURL}}',
93
+ fetch,
94
+ });
95
+ ```
96
+
97
+ {{#if IR.modelDefs}}
98
+ ## Models and Types
99
+
100
+ The SDK includes the following TypeScript interfaces:
101
+
102
+ {{#each IR.modelDefs}}
103
+ - **{{this.name}}**{{#if this.annotations.description}}: {{this.annotations.description}}{{/if}}
104
+ {{/each}}
105
+
106
+ All types are available under the `Schema` namespace:
107
+
108
+ ```typescript
109
+ import { Schema } from '{{Client.packageName}}';
110
+
111
+ // Use any model type
112
+ const user: Schema.User = { /* ... */ };
113
+ ```
114
+ {{/if}}
115
+
116
+ ## Contributing
117
+
118
+ This SDK is auto-generated. Please do not edit the generated files directly.
119
+ If you find issues, please report them in the main project repository.
120
+
121
+ ## License
122
+
123
+ This SDK is generated from the {{Client.name}} API specification.
@@ -0,0 +1,380 @@
1
+ {{~#if IR.securitySchemes~}}
2
+ export type ClientOption = {
3
+ baseURL?: string;
4
+ headers?: Record<string, string>;
5
+ timeoutMs?: number;
6
+ retry?: { retries: number; backoffMs: number; retryOn?: number[] };
7
+ onRequest?: (ctx: { url: string; init: RequestInit & { path: string; method: string; query?: Record<string, any>; headers: Headers }; attempt: number }) => void | Promise<void>;
8
+ onResponse?: (ctx: { url: string; init: RequestInit & { path: string; method: string; query?: Record<string, any>; headers: Headers }; attempt: number; response: Response }) => void | Promise<void>;
9
+ onError?: (err: unknown, ctx: { url: string; init: RequestInit & { path: string; method: string; query?: Record<string, any> }; attempt: number }) => void | Promise<void>;
10
+ // Environment & Auth
11
+ env?: 'sandbox' | 'production';
12
+ envBaseURLs?: { sandbox: string; production: string };
13
+ accessToken?: string | undefined | (() => string | undefined | Promise<string | undefined>);
14
+ headerName?: string;
15
+ {{~#each IR.securitySchemes~}}
16
+ {{~#if (eq this.type "http")~}}
17
+ {{~#if (eq this.scheme "bearer")~}}
18
+ {{camel this.key}}?: string;
19
+ {{~else if (eq this.scheme "basic")~}}
20
+ {{camel this.key}}?: { username: string; password: string };
21
+ {{~/if~}}
22
+ {{~else if (eq this.type "apiKey")~}}
23
+ {{camel this.key}}?: string;
24
+ {{~/if~}}
25
+ {{~/each~}}
26
+ fetch?: typeof fetch;
27
+ credentials?: RequestCredentials;
28
+ };
29
+
30
+ export class FetchError<T = unknown> extends Error {
31
+ constructor(
32
+ message: string,
33
+ readonly status: number,
34
+ readonly data?: T,
35
+ readonly headers?: Headers,
36
+ ) {
37
+ super(message);
38
+ this.name = "FetchError";
39
+ }
40
+ }
41
+
42
+ export class CoreClient {
43
+ constructor(private cfg: ClientOption = {}) {
44
+ // Set default base URL if not provided
45
+ if (!this.cfg.baseURL) {
46
+ if (this.cfg.env && this.cfg.envBaseURLs) {
47
+ this.cfg.baseURL = this.cfg.env === 'production' ? this.cfg.envBaseURLs.production : this.cfg.envBaseURLs.sandbox;
48
+ } else {
49
+ this.cfg.baseURL = "{{Client.defaultBaseURL}}";
50
+ }
51
+ }
52
+ }
53
+ setAccessToken(token: string | undefined | (() => string | undefined | Promise<string | undefined>)) {
54
+ this.cfg.accessToken = token;
55
+ }
56
+ async request(
57
+ init: RequestInit & {
58
+ path: string;
59
+ method: string;
60
+ query?: Record<string, any>;
61
+ }
62
+ ) {
63
+ let normalizedPath = init.path || "";
64
+ if (normalizedPath.length > 1 && normalizedPath.endsWith('/')) {
65
+ normalizedPath = normalizedPath.slice(0, -1);
66
+ }
67
+ const url = new URL((this.cfg.baseURL || "") + normalizedPath);
68
+ if (init.query) {
69
+ Object.entries(init.query).forEach(([k, v]) => {
70
+ if (v === undefined || v === null) return;
71
+ if (Array.isArray(v))
72
+ v.forEach((vv) => url.searchParams.append(k, String(vv)));
73
+ else url.searchParams.set(k, String(v));
74
+ });
75
+ }
76
+ {{~#each IR.securitySchemes~}}
77
+ {{~#if (and (eq this.type "apiKey") (eq this.in "query"))~}}
78
+ if (this.cfg.{{camel this.key}}) {
79
+ url.searchParams.set("{{this.name}}", String(this.cfg.{{camel this.key}}));
80
+ }
81
+ {{~/if~}}
82
+ {{~/each~}}
83
+ const headers = new Headers({
84
+ ...(this.cfg.headers || {}),
85
+ ...(init.headers as any),
86
+ });
87
+ // Generic access token support (optional)
88
+ if (this.cfg.accessToken) {
89
+ const token = typeof this.cfg.accessToken === 'function' ? await this.cfg.accessToken() : this.cfg.accessToken;
90
+ // Only set header if token is not nullish
91
+ if (token != null) {
92
+ const name = this.cfg.headerName || 'Authorization';
93
+ if (name.toLowerCase() === 'authorization') headers.set(name, `Bearer ${String(token)}`);
94
+ else headers.set(name, String(token));
95
+ }
96
+ }
97
+ {{~#each IR.securitySchemes~}}
98
+ {{~#if (eq this.type "http")~}}
99
+ {{~#if (eq this.scheme "bearer")~}}
100
+ const {{camel this.key}}Key = "{{camel this.key}}";
101
+ if (this.cfg[{{camel this.key}}Key])
102
+ headers.set("Authorization", `Bearer ${this.cfg[{{camel this.key}}Key]}`);
103
+ {{~else if (eq this.scheme "basic")~}}
104
+ const {{camel this.key}}Key = "{{camel this.key}}";
105
+ if (this.cfg[{{camel this.key}}Key]) {
106
+ const u = this.cfg[{{camel this.key}}Key].username;
107
+ const p = this.cfg[{{camel this.key}}Key].password;
108
+ const encoded = typeof btoa !== 'undefined' ? btoa(`${u}:${p}`) : (typeof Buffer !== 'undefined' ? Buffer.from(`${u}:${p}`).toString('base64') : '' );
109
+ if (encoded) headers.set("Authorization", `Basic ${encoded}`);
110
+ }
111
+ {{~/if~}}
112
+ {{~else if (eq this.type "apiKey")~}}
113
+ {{~#if (eq this.in "header")~}}
114
+ if (this.cfg?.{{camel this.key}})
115
+ headers.set("{{this.name}}", String(this.cfg?.{{camel this.key}}));
116
+ {{~else if (eq this.in "cookie")~}}
117
+ if (this.cfg?.{{camel this.key}})
118
+ headers.set("Cookie", `${"{{this.name}}"}=${String(this.cfg?.{{camel this.key}})}`);
119
+ {{~/if~}}
120
+ {{~/if~}}
121
+ {{~/each~}}
122
+
123
+ const doFetch = async (attempt: number) => {
124
+ // Clone init to prevent mutations from affecting concurrent requests
125
+ // Create a new Headers object for each request to avoid sharing references
126
+ const requestHeaders = new Headers(headers);
127
+ const fetchInit: RequestInit & {
128
+ path: string;
129
+ method: string;
130
+ query?: Record<string, any>;
131
+ headers: Headers;
132
+ } = {
133
+ ...init,
134
+ headers: requestHeaders,
135
+ };
136
+ // Set credentials from config if provided (can be overridden by onRequest)
137
+ if (this.cfg.credentials !== undefined) {
138
+ fetchInit.credentials = this.cfg.credentials;
139
+ }
140
+ if (this.cfg.onRequest) await this.cfg.onRequest({ url: url.toString(), init: fetchInit, attempt });
141
+ let controller: AbortController | undefined;
142
+ let timeoutId: any;
143
+ const existingSignal = fetchInit.signal;
144
+
145
+ if (this.cfg.timeoutMs && typeof AbortController !== 'undefined') {
146
+ controller = new AbortController();
147
+
148
+ // If there's an existing signal, combine it with the timeout signal
149
+ // The combined controller will abort when either signal aborts
150
+ if (existingSignal) {
151
+ // If existing signal is already aborted, abort the new controller immediately
152
+ if (existingSignal.aborted) {
153
+ controller.abort();
154
+ } else {
155
+ // Listen to the existing signal and abort the combined controller when it aborts
156
+ existingSignal.addEventListener('abort', () => {
157
+ controller?.abort();
158
+ });
159
+ }
160
+ }
161
+
162
+ fetchInit.signal = controller.signal;
163
+ timeoutId = setTimeout(() => controller?.abort(), this.cfg.timeoutMs);
164
+ }
165
+ try {
166
+ const res = await (this.cfg.fetch || fetch)(url.toString(), fetchInit);
167
+ if (this.cfg.onResponse) await this.cfg.onResponse({ url: url.toString(), init: fetchInit, attempt, response: res });
168
+ const ct = res.headers.get("content-type") || "";
169
+ let parsed: any;
170
+ if (ct.includes("application/json")) {
171
+ parsed = await res.json();
172
+ } else if (ct.startsWith("text/")) {
173
+ parsed = await res.text();
174
+ } else {
175
+ // binary or unknown -> ArrayBuffer
176
+ parsed = await res.arrayBuffer();
177
+ }
178
+ if (!res.ok) {
179
+ throw new FetchError(
180
+ parsed?.message || `HTTP ${res.status}`,
181
+ res.status,
182
+ parsed,
183
+ res.headers,
184
+ );
185
+ }
186
+ return parsed as any;
187
+ } catch (err) {
188
+ if (this.cfg.onError) await this.cfg.onError(err, { url: url.toString(), init, attempt });
189
+ throw err;
190
+ } finally {
191
+ if (timeoutId) clearTimeout(timeoutId);
192
+ }
193
+ };
194
+
195
+ const retries = this.cfg.retry?.retries ?? 0;
196
+ const baseBackoff = this.cfg.retry?.backoffMs ?? 300;
197
+ const retryOn = this.cfg.retry?.retryOn ?? [429, 500, 502, 503, 504];
198
+
199
+ let lastError: unknown;
200
+ for (let attempt = 0; attempt <= retries; attempt++) {
201
+ try {
202
+ return await doFetch(attempt);
203
+ } catch (err: any) {
204
+ // Retry on network errors or configured status errors
205
+ const status = err?.status as number | undefined;
206
+ const shouldRetry = status ? retryOn.includes(status) : true;
207
+ if (attempt < retries && shouldRetry) {
208
+ const delay = baseBackoff * Math.pow(2, attempt);
209
+ await new Promise((r) => setTimeout(r, delay));
210
+ lastError = err;
211
+ continue;
212
+ }
213
+ if (err instanceof DOMException) throw err;
214
+ if (err instanceof FetchError) throw err;
215
+ if (typeof err === 'string') throw new FetchError(err, status ?? 0);
216
+ throw new FetchError((err as Error)?.message || 'Network error', status ?? 0);
217
+ }
218
+ }
219
+ throw lastError as any;
220
+ }
221
+
222
+ async *requestStream<T = any>(
223
+ init: RequestInit & {
224
+ path: string;
225
+ method: string;
226
+ query?: Record<string, any>;
227
+ contentType: string;
228
+ streamingFormat?: "sse" | "ndjson" | "chunked";
229
+ }
230
+ ): AsyncGenerator<T, void, unknown> {
231
+ let normalizedPath = init.path || "";
232
+ if (normalizedPath.length > 1 && normalizedPath.endsWith('/')) {
233
+ normalizedPath = normalizedPath.slice(0, -1);
234
+ }
235
+ const url = new URL((this.cfg.baseURL || "") + normalizedPath);
236
+ if (init.query) {
237
+ Object.entries(init.query).forEach(([k, v]) => {
238
+ if (v === undefined || v === null) return;
239
+ if (Array.isArray(v))
240
+ v.forEach((vv) => url.searchParams.append(k, String(vv)));
241
+ else url.searchParams.set(k, String(v));
242
+ });
243
+ }
244
+ {{~#each IR.securitySchemes~}}
245
+ {{~#if (and (eq this.type "apiKey") (eq this.in "query"))~}}
246
+ if (this.cfg.{{camel this.key}}) {
247
+ url.searchParams.set("{{this.name}}", String(this.cfg.{{camel this.key}}));
248
+ }
249
+ {{~/if~}}
250
+ {{~/each~}}
251
+ const headers = new Headers({
252
+ ...(this.cfg.headers || {}),
253
+ ...(init.headers as any),
254
+ });
255
+ // Generic access token support (optional)
256
+ if (this.cfg.accessToken) {
257
+ const token = typeof this.cfg.accessToken === 'function' ? await this.cfg.accessToken() : this.cfg.accessToken;
258
+ // Only set header if token is not nullish
259
+ if (token != null) {
260
+ const name = this.cfg.headerName || 'Authorization';
261
+ if (name.toLowerCase() === 'authorization') headers.set(name, `Bearer ${String(token)}`);
262
+ else headers.set(name, String(token));
263
+ }
264
+ }
265
+ {{~#each IR.securitySchemes~}}
266
+ {{~#if (eq this.type "http")~}}
267
+ {{~#if (eq this.scheme "bearer")~}}
268
+ const {{camel this.key}}Key = "{{camel this.key}}";
269
+ if (this.cfg[{{camel this.key}}Key])
270
+ headers.set("Authorization", `Bearer ${this.cfg[{{camel this.key}}Key]}`);
271
+ {{~else if (eq this.scheme "basic")~}}
272
+ const {{camel this.key}}Key = "{{camel this.key}}";
273
+ if (this.cfg[{{camel this.key}}Key]) {
274
+ const u = this.cfg[{{camel this.key}}Key].username;
275
+ const p = this.cfg[{{camel this.key}}Key].password;
276
+ const encoded = typeof btoa !== 'undefined' ? btoa(`${u}:${p}`) : (typeof Buffer !== 'undefined' ? Buffer.from(`${u}:${p}`).toString('base64') : '' );
277
+ if (encoded) headers.set("Authorization", `Basic ${encoded}`);
278
+ }
279
+ {{~/if~}}
280
+ {{~else if (eq this.type "apiKey")~}}
281
+ {{~#if (eq this.in "header")~}}
282
+ if (this.cfg?.{{camel this.key}})
283
+ headers.set("{{this.name}}", String(this.cfg?.{{camel this.key}}));
284
+ {{~else if (eq this.in "cookie")~}}
285
+ if (this.cfg?.{{camel this.key}})
286
+ headers.set("Cookie", `${"{{this.name}}"}=${String(this.cfg?.{{camel this.key}})}`);
287
+ {{~/if~}}
288
+ {{~/if~}}
289
+ {{~/each~}}
290
+
291
+ const fetchInit: RequestInit & {
292
+ path: string;
293
+ method: string;
294
+ query?: Record<string, any>;
295
+ headers: Headers;
296
+ } = {
297
+ ...init,
298
+ headers,
299
+ };
300
+ // Set credentials from config if provided
301
+ if (this.cfg.credentials !== undefined) {
302
+ fetchInit.credentials = this.cfg.credentials;
303
+ }
304
+
305
+ if (this.cfg.onRequest) await this.cfg.onRequest({ url: url.toString(), init: fetchInit, attempt: 0 });
306
+
307
+ let controller: AbortController | undefined;
308
+ let timeoutId: any;
309
+ const existingSignal = fetchInit.signal;
310
+
311
+ if (this.cfg.timeoutMs && typeof AbortController !== 'undefined') {
312
+ controller = new AbortController();
313
+ if (existingSignal) {
314
+ if (existingSignal.aborted) {
315
+ controller.abort();
316
+ } else {
317
+ existingSignal.addEventListener('abort', () => {
318
+ controller?.abort();
319
+ });
320
+ }
321
+ }
322
+ fetchInit.signal = controller.signal;
323
+ timeoutId = setTimeout(() => controller?.abort(), this.cfg.timeoutMs);
324
+ }
325
+
326
+ try {
327
+ const res = await (this.cfg.fetch || fetch)(url.toString(), fetchInit);
328
+ if (this.cfg.onResponse) await this.cfg.onResponse({ url: url.toString(), init: fetchInit, attempt: 0, response: res });
329
+
330
+ if (!res.ok) {
331
+ const ct = res.headers.get("content-type") || "";
332
+ let parsed: any;
333
+ if (ct.includes("application/json")) {
334
+ parsed = await res.json();
335
+ } else if (ct.startsWith("text/")) {
336
+ parsed = await res.text();
337
+ } else {
338
+ parsed = await res.arrayBuffer();
339
+ }
340
+ throw new FetchError(
341
+ parsed?.message || `HTTP ${res.status}`,
342
+ res.status,
343
+ parsed,
344
+ res.headers,
345
+ );
346
+ }
347
+
348
+ // Import streaming parsers
349
+ const { parseSSEStream, parseNDJSONStream } = await import("./utils");
350
+
351
+ // Route to appropriate parser based on streaming format
352
+ if (init.streamingFormat === "sse") {
353
+ yield* parseSSEStream(res) as AsyncGenerator<T, void, unknown>;
354
+ } else if (init.streamingFormat === "ndjson") {
355
+ yield* parseNDJSONStream<T>(res);
356
+ } else {
357
+ // Generic chunked streaming - yield raw chunks as strings
358
+ if (!res.body) return;
359
+ const reader = res.body.getReader();
360
+ const decoder = new TextDecoder();
361
+ try {
362
+ while (true) {
363
+ const { done, value } = await reader.read();
364
+ if (done) break;
365
+ const chunk = decoder.decode(value, { stream: true });
366
+ yield chunk as T;
367
+ }
368
+ } finally {
369
+ reader.releaseLock();
370
+ }
371
+ }
372
+ } catch (err) {
373
+ if (this.cfg.onError) await this.cfg.onError(err, { url: url.toString(), init, attempt: 0 });
374
+ throw err;
375
+ } finally {
376
+ if (timeoutId) clearTimeout(timeoutId);
377
+ }
378
+ }
379
+ }
380
+ {{~/if~}}