@intrig/plugin-react 0.0.1 → 0.0.2-2

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.
Files changed (58) hide show
  1. package/dist/index.cjs +4260 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +4235 -0
  4. package/package.json +6 -3
  5. package/.swcrc +0 -29
  6. package/README.md +0 -7
  7. package/eslint.config.mjs +0 -19
  8. package/project.json +0 -29
  9. package/rollup.config.cjs +0 -54
  10. package/rollup.config.mjs +0 -33
  11. package/src/index.ts +0 -2
  12. package/src/lib/code-generator.ts +0 -79
  13. package/src/lib/get-endpoint-documentation.ts +0 -35
  14. package/src/lib/get-schema-documentation.ts +0 -11
  15. package/src/lib/internal-types.ts +0 -15
  16. package/src/lib/plugin-react.ts +0 -22
  17. package/src/lib/templates/context.template.ts +0 -74
  18. package/src/lib/templates/docs/__snapshots__/async-hook.spec.ts.snap +0 -889
  19. package/src/lib/templates/docs/__snapshots__/download-hook.spec.ts.snap +0 -1445
  20. package/src/lib/templates/docs/__snapshots__/react-hook.spec.ts.snap +0 -1371
  21. package/src/lib/templates/docs/__snapshots__/sse-hook.spec.ts.snap +0 -2008
  22. package/src/lib/templates/docs/async-hook.spec.ts +0 -92
  23. package/src/lib/templates/docs/async-hook.ts +0 -226
  24. package/src/lib/templates/docs/download-hook.spec.ts +0 -182
  25. package/src/lib/templates/docs/download-hook.ts +0 -170
  26. package/src/lib/templates/docs/react-hook.spec.ts +0 -97
  27. package/src/lib/templates/docs/react-hook.ts +0 -323
  28. package/src/lib/templates/docs/schema.ts +0 -105
  29. package/src/lib/templates/docs/sse-hook.spec.ts +0 -207
  30. package/src/lib/templates/docs/sse-hook.ts +0 -221
  31. package/src/lib/templates/extra.template.ts +0 -198
  32. package/src/lib/templates/index.template.ts +0 -14
  33. package/src/lib/templates/intrigMiddleware.template.ts +0 -21
  34. package/src/lib/templates/logger.template.ts +0 -67
  35. package/src/lib/templates/media-type-utils.template.ts +0 -191
  36. package/src/lib/templates/network-state.template.ts +0 -702
  37. package/src/lib/templates/packageJson.template.ts +0 -63
  38. package/src/lib/templates/provider/__tests__/provider-templates.spec.ts +0 -209
  39. package/src/lib/templates/provider/axios-config.template.ts +0 -49
  40. package/src/lib/templates/provider/hooks.template.ts +0 -240
  41. package/src/lib/templates/provider/interfaces.template.ts +0 -72
  42. package/src/lib/templates/provider/intrig-provider-stub.template.ts +0 -73
  43. package/src/lib/templates/provider/intrig-provider.template.ts +0 -185
  44. package/src/lib/templates/provider/main.template.ts +0 -48
  45. package/src/lib/templates/provider/reducer.template.ts +0 -50
  46. package/src/lib/templates/provider/status-trap.template.ts +0 -80
  47. package/src/lib/templates/provider.template.ts +0 -698
  48. package/src/lib/templates/source/controller/method/asyncFunctionHook.template.ts +0 -196
  49. package/src/lib/templates/source/controller/method/clientIndex.template.ts +0 -38
  50. package/src/lib/templates/source/controller/method/download.template.ts +0 -256
  51. package/src/lib/templates/source/controller/method/params.template.ts +0 -31
  52. package/src/lib/templates/source/controller/method/requestHook.template.ts +0 -220
  53. package/src/lib/templates/source/type/typeTemplate.ts +0 -257
  54. package/src/lib/templates/swcrc.template.ts +0 -25
  55. package/src/lib/templates/tsconfig.template.ts +0 -37
  56. package/src/lib/templates/type-utils.template.ts +0 -28
  57. package/tsconfig.json +0 -13
  58. package/tsconfig.lib.json +0 -20
package/dist/index.js ADDED
@@ -0,0 +1,4235 @@
1
+ import { StatsCounter, jsonLiteral, typescript, generatePostfix, camelCase, pascalCase, decodeVariables, mdLiteral } from '@intrig/plugin-sdk';
2
+ import * as path from 'path';
3
+ import path__default from 'path';
4
+ import * as fsx from 'fs-extra';
5
+ import * as mimeType from 'mime-types';
6
+
7
+ class InternalGeneratorContext {
8
+ getCounter(sourceId) {
9
+ this.codeGenerationBreakdown[sourceId] = this.codeGenerationBreakdown[sourceId] || new StatsCounter(sourceId);
10
+ return this.codeGenerationBreakdown[sourceId];
11
+ }
12
+ getCounters() {
13
+ return [
14
+ ...Object.values(this.codeGenerationBreakdown)
15
+ ];
16
+ }
17
+ constructor(potentiallyConflictingDescriptors){
18
+ this.potentiallyConflictingDescriptors = potentiallyConflictingDescriptors;
19
+ this.codeGenerationBreakdown = {};
20
+ }
21
+ }
22
+
23
+ function packageJsonTemplate() {
24
+ const packageJson = fsx.readJsonSync(path.resolve('..', '..', 'package.json'));
25
+ const json = jsonLiteral(path.resolve('package.json'));
26
+ var _packageJson_devDependencies_typescript;
27
+ return json`
28
+ {
29
+ "name": "@intrig/generated",
30
+ "version": "d${Date.now()}",
31
+ "private": true,
32
+ "main": "dist/index.js",
33
+ "types": "dist/index.d.ts",
34
+ "scripts": {
35
+ "build": "swc src -d dist --copy-files --strip-leading-paths && tsc --emitDeclarationOnly"
36
+ },
37
+ "dependencies": {
38
+ "axios": "^1.7.7",
39
+ "date-fns": "^4.1.0",
40
+ "eventsource-parser": "^3.0.2",
41
+ "fast-xml-parser": "^4.5.0",
42
+ "immer": "^10.1.1",
43
+ "loglevel": "1.8.1",
44
+ "module-alias": "^2.2.2",
45
+ "zod": "^3.23.8"
46
+ },
47
+ "devDependencies": {
48
+ "@swc/cli": "^0.7.7",
49
+ "@swc/core": "^1.12.6",
50
+ "@types/node": "^24.0.4",
51
+ "typescript": "${(_packageJson_devDependencies_typescript = packageJson.devDependencies.typescript) != null ? _packageJson_devDependencies_typescript : packageJson.dependencies.typescript}",
52
+ "react": "${packageJson.dependencies.react}",
53
+ "react-dom": "${packageJson.dependencies['react-dom']}"
54
+ },
55
+ "peerDependencies": {
56
+ "react": "^18.0.0 || ^19.0.0",
57
+ "react-dom": "^18.0.0 || ^19.0.0"
58
+ },
59
+ "_moduleAliases": {
60
+ "@intrig/react": "./src"
61
+ },
62
+ "type": "module",
63
+ "exports": {
64
+ ".": {
65
+ "import": "./src/index.js",
66
+ "require": "./src/index.js",
67
+ "types": "./src/index.d.ts"
68
+ },
69
+ "./*": {
70
+ "import": "./src/*.js",
71
+ "require": "./src/*.js",
72
+ "types": "./src/*.d.ts"
73
+ }
74
+ },
75
+ "typesVersions": {
76
+ "*": {
77
+ "*": ["src/*"]
78
+ }
79
+ }
80
+ }
81
+ `;
82
+ }
83
+
84
+ function indexTemplate() {
85
+ const ts = typescript(path.resolve("src", "index.ts"));
86
+ return ts`
87
+ export * from './intrig-provider-main';
88
+ export * from './network-state';
89
+ export * from './extra';
90
+ export * from './media-type-utils';
91
+ `;
92
+ }
93
+
94
+ function networkStateTemplate() {
95
+ const ts = typescript(path.resolve("src", "network-state.tsx"));
96
+ return ts`import { ZodError } from 'zod';
97
+ import {AxiosResponseHeaders, RawAxiosResponseHeaders} from "axios";
98
+
99
+ /**
100
+ * State of an asynchronous call. Network state follows the state diagram given below.
101
+ *
102
+ *
103
+ * <pre>
104
+ * ┌──────┐
105
+ * ┌─────────────► Init ◄────────────┐
106
+ * │ └▲────┬┘ │
107
+ * │ │ │ │
108
+ * │ Reset Execute │
109
+ * Reset │ │ Reset
110
+ * │ ┌──┴────┴──┐ │
111
+ * │ ┌────► Pending ◄────┐ │
112
+ * │ │ └──┬────┬──┘ │ │
113
+ * │ Execute │ │ Execute │
114
+ * │ │ │ │ │ │
115
+ * │ │ OnSuccess OnError │ │
116
+ * │ ┌────┴──┐ │ │ ┌──┴───┐ │
117
+ * └─┤Success◄────┘ └────►Error ├─┘
118
+ * └───────┘ └──────┘
119
+ *
120
+ * </pre>
121
+ */
122
+ export interface NetworkState<T = unknown> {
123
+ state: 'init' | 'pending' | 'success' | 'error';
124
+ }
125
+
126
+ /**
127
+ * Network call is not yet started
128
+ */
129
+ export interface InitState<T> extends NetworkState<T> {
130
+ state: 'init';
131
+ }
132
+
133
+ /**
134
+ * Checks whether the state is init state
135
+ * @param state
136
+ */
137
+ export function isInit<T>(
138
+ state: NetworkState<T>,
139
+ ): state is InitState<T> {
140
+ return state.state === 'init';
141
+ }
142
+
143
+ /**
144
+ * Initializes a new state.
145
+ *
146
+ * @template T The type of the state.
147
+ * @return {InitState<T>} An object representing the initial state.
148
+ */
149
+ export function init<T>(): InitState<T> {
150
+ return {
151
+ state: 'init',
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Network call is not yet completed
157
+ */
158
+ export interface PendingState<T> extends NetworkState<T> {
159
+ state: 'pending';
160
+ progress?: Progress;
161
+ data?: T;
162
+ }
163
+
164
+ /**
165
+ * Interface representing progress information for an upload or download operation.
166
+ *
167
+ * @typedef {object} Progress
168
+ *
169
+ * @property {'upload' | 'download'} type - The type of the operation.
170
+ *
171
+ * @property {number} loaded - The amount of data that has been loaded so far.
172
+ *
173
+ * @property {number} [total] - The total amount of data to be loaded (if known).
174
+ */
175
+ export interface Progress {
176
+ type?: 'upload' | 'download';
177
+ loaded: number;
178
+ total?: number;
179
+ }
180
+
181
+ /**
182
+ * Checks whether the state is pending state
183
+ * @param state
184
+ */
185
+ export function isPending<T>(
186
+ state: NetworkState<T>,
187
+ ): state is PendingState<T> {
188
+ return state.state === 'pending';
189
+ }
190
+
191
+ /**
192
+ * Generates a PendingState object with a state of "pending".
193
+ *
194
+ * @return {PendingState<T>} An object representing the pending state.
195
+ */
196
+ export function pending<T>(
197
+ progress: Progress | undefined = undefined,
198
+ data: T | undefined = undefined,
199
+ ): PendingState<T> {
200
+ return {
201
+ state: 'pending',
202
+ progress,
203
+ data,
204
+ };
205
+ }
206
+
207
+ /**
208
+ * Network call is completed with success state
209
+ */
210
+ export interface SuccessState<T> extends NetworkState<T> {
211
+ state: 'success';
212
+ data: T;
213
+ headers?: Record<string, any | undefined>;
214
+ }
215
+
216
+ /**
217
+ * Checks whether the state is success response
218
+ * @param state
219
+ */
220
+ export function isSuccess<T>(
221
+ state: NetworkState<T>,
222
+ ): state is SuccessState<T> {
223
+ return state.state === 'success';
224
+ }
225
+
226
+ /**
227
+ * Creates a success state object with the provided data.
228
+ *
229
+ * @param {T} data - The data to be included in the success state.
230
+ * @param headers
231
+ * @return {SuccessState<T>} An object representing a success state containing the provided data.
232
+ */
233
+ export function success<T>(
234
+ data: T,
235
+ headers?: Record<string, any | undefined>,
236
+ ): SuccessState<T> {
237
+ return {
238
+ state: 'success',
239
+ data,
240
+ headers,
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Network call is completed with error response
246
+ */
247
+ export interface ErrorState<T> extends NetworkState<T> {
248
+ state: 'error';
249
+ error: IntrigError;
250
+ }
251
+
252
+ /**
253
+ * Checks whether the state is error state
254
+ * @param state
255
+ */
256
+ export function isError<T>(
257
+ state: NetworkState<T>,
258
+ ): state is ErrorState<T> {
259
+ return state.state === 'error';
260
+ }
261
+
262
+ /**
263
+ * Constructs an ErrorState object representing an error.
264
+ *
265
+ * @param {any} error - The error object or message.
266
+ * @param {string} [statusCode] - An optional status code associated with the error.
267
+ * @return {ErrorState<T>} An object representing the error state.
268
+ */
269
+ export function error<T>(
270
+ error: IntrigError,
271
+ ): ErrorState<T> {
272
+ return {
273
+ state: 'error',
274
+ error
275
+ };
276
+ }
277
+
278
+ /**
279
+ * Represents the base structure for error information in the application.
280
+ *
281
+ * This interface is used to define the type of error encountered in various contexts.
282
+ *
283
+ * Properties:
284
+ * - type: Specifies the category of the error which determines its nature and source.
285
+ * - 'http': Indicates the error is related to HTTP operations.
286
+ * - 'network': Indicates the error occurred due to network issues.
287
+ * - 'request-validation': Represents errors that occurred during request validation.
288
+ * - 'response-validation': Represents errors that occurred during response validation.
289
+ * - 'config': Pertains to errors associated with configuration issues.
290
+ */
291
+ export interface IntrigErrorBase {
292
+ type: 'http' | 'network' | 'request-validation' | 'response-validation' | 'config';
293
+ }
294
+
295
+ /**
296
+ * Interface representing an HTTP-related error.
297
+ * Extends the \`IntrigErrorBase\` base interface to provide information specific to HTTP errors.
298
+ *
299
+ * @property type - The type identifier for the error, always set to 'http'.
300
+ * @property status - The HTTP status code associated with the error.
301
+ * @property url - The URL that caused the error.
302
+ * @property method - The HTTP method used in the request that resulted in the error.
303
+ * @property headers - Optional. The HTTP headers relevant to the request and/or response, represented as a record of key-value pairs.
304
+ * @property body - Optional. The parsed body of the server error, if available.
305
+ */
306
+ export interface HttpError extends IntrigErrorBase {
307
+ type: 'http';
308
+ status: number;
309
+ url: string;
310
+ method: string;
311
+ headers?: RawAxiosResponseHeaders | AxiosResponseHeaders;
312
+ body?: unknown; // parsed server error body if any
313
+ }
314
+
315
+ /**
316
+ * Determines if the given error is an HTTP error.
317
+ *
318
+ * @param {IntrigError} error - The error object to check.
319
+ * @return {boolean} Returns true if the error is an instance of HttpError; otherwise, false.
320
+ */
321
+ export function isHttpError(error: IntrigError): error is HttpError {
322
+ return error.type === 'http';
323
+ }
324
+
325
+ /**
326
+ * Creates an object representing an HTTP error.
327
+ *
328
+ * @param {number} status - The HTTP status code of the error.
329
+ * @param {string} url - The URL associated with the HTTP error.
330
+ * @param {string} method - The HTTP method that resulted in the error.
331
+ * @param {Record<string, string | string[] | undefined>} [headers] - Optional headers involved in the HTTP error.
332
+ * @param {unknown} [body] - Optional body data related to the HTTP error.
333
+ * @return {HttpError} An object encapsulating details about the HTTP error.
334
+ */
335
+ export function httpError(
336
+ status: number,
337
+ url: string,
338
+ method: string,
339
+ headers?: RawAxiosResponseHeaders | AxiosResponseHeaders,
340
+ body?: unknown
341
+ ): HttpError {
342
+ return {
343
+ type: 'http',
344
+ status,
345
+ url,
346
+ method,
347
+ headers,
348
+ body
349
+ };
350
+ }
351
+
352
+ /**
353
+ * Represents a network-related error. This error type is used to indicate issues during network operations.
354
+ * Extends the base error functionality from IntrigErrorBase.
355
+ *
356
+ * Properties:
357
+ * - \`type\`: Specifies the type of error as 'network'.
358
+ * - \`reason\`: Indicates the specific reason for the network error. Possible values include:
359
+ * - 'timeout': Occurs when the network request times out.
360
+ * - 'dns': Represents DNS resolution issues.
361
+ * - 'offline': Indicates the device is offline or has no network connectivity.
362
+ * - 'aborted': The network request was aborted.
363
+ * - 'unknown': An unspecified network issue occurred.
364
+ * - \`request\`: Optional property representing the network request that caused the error. Its structure can vary depending on the implementation context.
365
+ */
366
+ export interface NetworkError extends IntrigErrorBase {
367
+ type: 'network';
368
+ reason: 'timeout' | 'dns' | 'offline' | 'aborted' | 'unknown';
369
+ request?: any;
370
+ }
371
+
372
+ /**
373
+ * Determines if the provided error is of type NetworkError.
374
+ *
375
+ * @param {IntrigError} error - The error object to be checked.
376
+ * @return {boolean} Returns true if the error is of type NetworkError, otherwise false.
377
+ */
378
+ export function isNetworkError(error: IntrigError): error is NetworkError {
379
+ return error.type === 'network';
380
+ }
381
+
382
+ /**
383
+ * Creates a network error object with the specified reason and optional request details.
384
+ *
385
+ * @param {'timeout' | 'dns' | 'offline' | 'aborted' | 'unknown'} reason - The reason for the network error.
386
+ * @param {any} [request] - Optional information about the network request that caused the error.
387
+ * @return {NetworkError} An object representing the network error.
388
+ */
389
+ export function networkError(
390
+ reason: 'timeout' | 'dns' | 'offline' | 'aborted' | 'unknown',
391
+ request?: any,
392
+ ): NetworkError {
393
+ return {
394
+ type: 'network',
395
+ reason,
396
+ request,
397
+ };
398
+ }
399
+
400
+ /**
401
+ * Represents an error that occurs during request validation.
402
+ *
403
+ * This interface extends the \`IntrigErrorBase\` to provide
404
+ * additional details about validation errors in the request.
405
+ *
406
+ * \`RequestValidationError\` includes a specific error type
407
+ * identifier, details about the validation error, and information
408
+ * about the fields that failed validation.
409
+ *
410
+ * The \`type\` property indicates the error type as 'request-validation'.
411
+ * The \`error\` property holds the ZodError object with detailed validation
412
+ * errors from the Zod library.
413
+ * The \`fieldErrors\` property is a mapping of field names to an array
414
+ * of validation error messages for that field.
415
+ */
416
+ export interface RequestValidationError extends IntrigErrorBase {
417
+ type: 'request-validation';
418
+ error: ZodError;
419
+ }
420
+
421
+ /**
422
+ * Checks if the given error is of type RequestValidationError.
423
+ *
424
+ * @param {IntrigError} error - The error object to be checked.
425
+ * @return {boolean} Returns true if the error is a RequestValidationError; otherwise, false.
426
+ */
427
+ export function isRequestValidationError(error: IntrigError): error is RequestValidationError {
428
+ return error.type === 'request-validation';
429
+ }
430
+
431
+ /**
432
+ * Constructs a RequestValidationError object, capturing details about validation errors.
433
+ *
434
+ * @param {ZodError} error - The primary Zod validation error object containing detailed error information.
435
+ * @param {Record<string, string[]>} fieldErrors - An object mapping field names to arrays of validation error messages.
436
+ * @return {RequestValidationError} An object representing the request validation error, including the error type, detailed error, and field-specific errors.
437
+ */
438
+ export function requestValidationError(
439
+ error: ZodError
440
+ ): RequestValidationError {
441
+ return {
442
+ type: 'request-validation',
443
+ error
444
+ };
445
+ }
446
+
447
+ /**
448
+ * Describes an error encountered during response validation, typically
449
+ * when the structure or content of a response does not meet the expected schema.
450
+ *
451
+ * This interface extends the \`IntrigErrorBase\` to provide additional
452
+ * details specific to validation issues.
453
+ *
454
+ * The \`type\` property is a discriminative field, always set to 'response-validation',
455
+ * for identifying this specific kind of error.
456
+ *
457
+ * The \`error\` property contains a \`ZodError\` object, which provides structured
458
+ * details about the validation failure, including paths and specific issues.
459
+ *
460
+ * The optional \`raw\` property may hold the unprocessed or unparsed response data,
461
+ * which can be useful for debugging and troubleshooting.
462
+ */
463
+ export interface ResponseValidationError extends IntrigErrorBase {
464
+ type: 'response-validation';
465
+ error: ZodError;
466
+ // optional: raw/unparsed response for troubleshooting
467
+ raw?: unknown;
468
+ }
469
+
470
+ /**
471
+ * Determines if the provided error is of type ResponseValidationError.
472
+ *
473
+ * @param {IntrigError} error - The error object to be evaluated.
474
+ * @return {boolean} Returns true if the error is a ResponseValidationError, otherwise false.
475
+ */
476
+ export function isResponseValidationError(error: IntrigError): error is ResponseValidationError {
477
+ return error.type === 'response-validation';
478
+ }
479
+
480
+ /**
481
+ * Constructs a ResponseValidationError object to represent a response validation failure.
482
+ *
483
+ * @param {ZodError} error - The error object representing the validation issue.
484
+ * @param {unknown} [raw] - Optional raw data related to the validation error.
485
+ * @return {ResponseValidationError} An object containing the type of error, the validation error object, and optional raw data.
486
+ */
487
+ export function responseValidationError(
488
+ error: ZodError,
489
+ raw?: unknown,
490
+ ): ResponseValidationError {
491
+ return {
492
+ type: 'response-validation',
493
+ error,
494
+ raw,
495
+ };
496
+ }
497
+
498
+ /**
499
+ * Represents an error related to configuration issues.
500
+ * ConfigError is an extension of IntrigErrorBase, designed specifically
501
+ * to handle and provide details about errors encountered in application
502
+ * configuration.
503
+ *
504
+ * @interface ConfigError
505
+ * @extends IntrigErrorBase
506
+ *
507
+ * @property type - Identifies the error type as 'config'.
508
+ * @property message - Describes the details of the configuration error encountered.
509
+ */
510
+ export interface ConfigError extends IntrigErrorBase {
511
+ type: 'config';
512
+ message: string;
513
+ }
514
+
515
+ /**
516
+ * Determines if the provided error is a configuration error.
517
+ *
518
+ * @param {IntrigError} error - The error object to be checked.
519
+ * @return {boolean} Returns true if the error is of type 'ConfigError', otherwise false.
520
+ */
521
+ export function isConfigError(error: IntrigError): error is ConfigError {
522
+ return error.type === 'config';
523
+ }
524
+
525
+ /**
526
+ * Generates a configuration error object with a specified error message.
527
+ *
528
+ * @param {string} message - The error message to be associated with the configuration error.
529
+ * @return {ConfigError} The configuration error object containing the error type and message.
530
+ */
531
+ export function configError(message: string): ConfigError {
532
+ return {
533
+ type: 'config',
534
+ message,
535
+ };
536
+ }
537
+
538
+ /**
539
+ * Represents a union type for errors that may occur while handling HTTP requests,
540
+ * network operations, request and response validations, or configuration issues.
541
+ *
542
+ * This type encompasses various error types to provide a unified representation
543
+ * for different error scenarios during the execution of a program.
544
+ *
545
+ * Types:
546
+ * - HttpError: Represents an error occurred in HTTP responses.
547
+ * - NetworkError: Represents an error related to underlying network operations.
548
+ * - RequestValidationError: Represents an error in request validation logic.
549
+ * - ResponseValidationError: Represents an error in response validation logic.
550
+ * - ConfigError: Represents an error related to configuration issues.
551
+ */
552
+ export type IntrigError =
553
+ | HttpError
554
+ | NetworkError
555
+ | RequestValidationError
556
+ | ResponseValidationError
557
+ | ConfigError;
558
+
559
+ /**
560
+ * Represents an error state with additional contextual information.
561
+ *
562
+ * @typedef {Object} ErrorWithContext
563
+ * @template T
564
+ * @extends ErrorState<T>
565
+ *
566
+ * @property {string} source - The origin of the error.
567
+ * @property {string} operation - The operation being performed when the error occurred.
568
+ * @property {string} key - A unique key identifying the specific error instance.
569
+ */
570
+ export interface ErrorWithContext<T = unknown>
571
+ extends ErrorState<T> {
572
+ source: string;
573
+ operation: string;
574
+ key: string;
575
+ }
576
+
577
+ /**
578
+ * Represents an action in the network context.
579
+ *
580
+ * @template T - The type of data associated with the network action
581
+ *
582
+ * @property {NetworkState<any>} state - The current state of the network action
583
+ * @property {string} key - The unique identifier for the network action
584
+ */
585
+ export interface NetworkAction<T> {
586
+ key: string;
587
+ source: string;
588
+ operation: string;
589
+ state: NetworkState<T>;
590
+ handled?: boolean;
591
+ }
592
+
593
+ type HookWithKey = {
594
+ key: string;
595
+ };
596
+
597
+ export type UnitHookOptions =
598
+ | { key?: string; fetchOnMount?: false; clearOnUnmount?: boolean }
599
+ | {
600
+ key?: string;
601
+ fetchOnMount: true;
602
+ params?: Record<string, any>;
603
+ clearOnUnmount?: boolean;
604
+ };
605
+ export type UnitHook = ((
606
+ options: UnitHookOptions,
607
+ ) => [
608
+ NetworkState<never>,
609
+ (params?: Record<string, any>) => DispatchState<any>,
610
+ () => void,
611
+ ]) &
612
+ HookWithKey;
613
+ export type ConstantHook<T> = ((
614
+ options: UnitHookOptions,
615
+ ) => [
616
+ NetworkState<T>,
617
+ (params?: Record<string, any>) => DispatchState<any>,
618
+ () => void,
619
+ ]) &
620
+ HookWithKey;
621
+
622
+ export type UnaryHookOptions<P> =
623
+ | { key?: string; fetchOnMount?: false; clearOnUnmount?: boolean }
624
+ | { key?: string; fetchOnMount: true; params: P; clearOnUnmount?: boolean };
625
+ export type UnaryProduceHook<P> = ((
626
+ options?: UnaryHookOptions<P>,
627
+ ) => [NetworkState<never>, (params: P) => DispatchState<any>, () => void]) &
628
+ HookWithKey;
629
+ export type UnaryFunctionHook<P, T> = ((
630
+ options?: UnaryHookOptions<P>,
631
+ ) => [NetworkState<T>, (params: P) => DispatchState<any>, () => void]) &
632
+ HookWithKey;
633
+
634
+ export type BinaryHookOptions<P, B> =
635
+ | { key?: string; fetchOnMount?: false; clearOnUnmount?: boolean }
636
+ | {
637
+ key?: string;
638
+ fetchOnMount: true;
639
+ params: P;
640
+ body: B;
641
+ clearOnUnmount?: boolean;
642
+ };
643
+ export type BinaryProduceHook<P, B> = ((
644
+ options?: BinaryHookOptions<P, B>,
645
+ ) => [
646
+ NetworkState<never>,
647
+ (body: B, params: P) => DispatchState<any>,
648
+ () => void,
649
+ ]) &
650
+ HookWithKey;
651
+ export type BinaryFunctionHook<P, B, T> = ((
652
+ options?: BinaryHookOptions<P, B>,
653
+ ) => [
654
+ NetworkState<T>,
655
+ (body: B, params: P) => DispatchState<any>,
656
+ () => void,
657
+ ]) &
658
+ HookWithKey;
659
+
660
+ export type IntrigHookOptions<P = undefined, B = undefined> =
661
+ | UnitHookOptions
662
+ | UnaryHookOptions<P>
663
+ | BinaryHookOptions<P, B>;
664
+ export type IntrigHook<P = undefined, B = undefined, T = any> =
665
+ | UnitHook
666
+ | ConstantHook<T>
667
+ | UnaryProduceHook<P>
668
+ | UnaryFunctionHook<P, T>
669
+ | BinaryProduceHook<P, B>
670
+ | BinaryFunctionHook<P, B, T>;
671
+
672
+ export interface AsyncRequestOptions {
673
+ hydrate?: boolean;
674
+ key?: string;
675
+ }
676
+
677
+ // Async hook variants for transient (promise-returning) network requests
678
+
679
+ export type UnaryFunctionAsyncHook<P, T> = (() => [
680
+ (params: P) => Promise<T>,
681
+ () => void,
682
+ ]) & {
683
+ key: string;
684
+ };
685
+
686
+ export type BinaryFunctionAsyncHook<P, B, T> = (() => [
687
+ (body: B, params: P) => Promise<T>,
688
+ () => void,
689
+ ]) & {
690
+ key: string;
691
+ };
692
+
693
+ export type UnaryProduceAsyncHook<P> = (() => [
694
+ (params: P) => Promise<void>,
695
+ () => void,
696
+ ]) & {
697
+ key: string;
698
+ };
699
+
700
+ export type BinaryProduceAsyncHook<P, B> = (() => [
701
+ (body: B, params: P) => Promise<void>,
702
+ () => void,
703
+ ]) & {
704
+ key: string;
705
+ };
706
+
707
+ /**
708
+ * Represents the dispatch state of a process.
709
+ *
710
+ * @template T The type of the state information.
711
+ * @interface
712
+ *
713
+ * @property {string} state The current state of the dispatch process.
714
+ */
715
+ export interface DispatchState<T> {
716
+ state: string;
717
+ }
718
+
719
+ /**
720
+ * Represents a successful dispatch state.
721
+ *
722
+ * @template T - Type of the data associated with the dispatch.
723
+ *
724
+ * @extends DispatchState<T>
725
+ *
726
+ * @property {string} state - The state of the dispatch, always 'success'.
727
+ */
728
+ export interface SuccessfulDispatch<T> extends DispatchState<T> {
729
+ state: 'success';
730
+ }
731
+
732
+ /**
733
+ * Indicates a successful dispatch state.
734
+ *
735
+ * @return {DispatchState<T>} An object representing a successful state.
736
+ */
737
+ export function successfulDispatch<T>(): DispatchState<T> {
738
+ return {
739
+ state: 'success',
740
+ };
741
+ }
742
+
743
+ /**
744
+ * Determines if the provided dispatch state represents a successful dispatch.
745
+ *
746
+ * @param {DispatchState<T>} value - The dispatch state to check.
747
+ * @return {value is SuccessfulDispatch<T>} - True if the dispatch state indicates success, false otherwise.
748
+ */
749
+ export function isSuccessfulDispatch<T>(
750
+ value: DispatchState<T>,
751
+ ): value is SuccessfulDispatch<T> {
752
+ return value.state === 'success';
753
+ }
754
+
755
+ /**
756
+ * ValidationError interface represents a specific type of dispatch state
757
+ * where a validation error has occurred.
758
+ *
759
+ * @typeparam T - The type of the data associated with this dispatch state.
760
+ */
761
+ export interface ValidationError<T> extends DispatchState<T> {
762
+ state: 'validation-error';
763
+ error: any;
764
+ }
765
+
766
+ /**
767
+ * Generates a ValidationError object.
768
+ *
769
+ * @param error The error details that caused the validation to fail.
770
+ * @return The ValidationError object containing the error state and details.
771
+ */
772
+ export function validationError<T>(error: any): ValidationError<T> {
773
+ return {
774
+ state: 'validation-error',
775
+ error,
776
+ };
777
+ }
778
+
779
+ /**
780
+ * Determines if a provided DispatchState object is a ValidationError.
781
+ *
782
+ * @param {DispatchState<T>} value - The DispatchState object to evaluate.
783
+ * @return {boolean} - Returns true if the provided DispatchState object is a ValidationError, otherwise returns false.
784
+ */
785
+ export function isValidationError<T>(
786
+ value: DispatchState<T>,
787
+ ): value is ValidationError<T> {
788
+ return value.state === 'validation-error';
789
+ }
790
+
791
+ `;
792
+ }
793
+
794
+ function contextTemplate(apisToSync) {
795
+ const ts = typescript(path.resolve("src", "intrig-context.ts"));
796
+ const configType = `{
797
+ defaults?: DefaultConfigs,
798
+ ${apisToSync.map((a)=>`${a.id}?: DefaultConfigs`).join(",\n ")}
799
+ }`;
800
+ return ts`
801
+ import { NetworkAction, NetworkState } from '@intrig/react/network-state';
802
+ import { AxiosProgressEvent, AxiosRequestConfig } from 'axios';
803
+ import { ZodSchema, ZodType, ZodTypeDef } from 'zod';
804
+ import { createContext, useContext, Dispatch } from 'react';
805
+ import { DefaultConfigs } from './interfaces';
806
+
807
+ type GlobalState = Record<string, NetworkState>;
808
+
809
+ interface RequestType<T = any> extends AxiosRequestConfig {
810
+ originalData?: T; // Keeps track of the original data type.
811
+ key: string;
812
+ source: string
813
+ }
814
+
815
+ export type SchemaOf<T> = ZodType<T, ZodTypeDef, any>;
816
+
817
+ /**
818
+ * Defines the ContextType interface for managing global state, dispatching actions,
819
+ * and holding a collection of Axios instances.
820
+ *
821
+ * @interface ContextType
822
+ * @property {GlobalState} state - The global state of the application.
823
+ * @property {Dispatch<NetworkAction<unknown>>} dispatch - The dispatch function to send network actions.
824
+ * @property {Record<string, AxiosInstance>} axios - A record of Axios instances for making HTTP requests.
825
+ */
826
+ export interface ContextType {
827
+ state: GlobalState;
828
+ filteredState: GlobalState;
829
+ dispatch: Dispatch<NetworkAction<unknown>>;
830
+ configs: ${configType};
831
+ execute: <T>(request: RequestType, dispatch: (state: NetworkState<T>) => void, schema: SchemaOf<T> | undefined, errorSchema: SchemaOf<T> | undefined) => Promise<void>;
832
+ }
833
+
834
+ /**
835
+ * Context object created using \`createContext\` function. Provides a way to share state, dispatch functions,
836
+ * and axios instance across components without having to pass props down manually at every level.
837
+ *
838
+ * @type {ContextType}
839
+ */
840
+ const Context = createContext<ContextType>({
841
+ state: {},
842
+ filteredState: {},
843
+ dispatch() {
844
+ //intentionally left blank
845
+ },
846
+ configs: {},
847
+ async execute() {
848
+ //intentionally left blank
849
+ },
850
+ });
851
+
852
+ export function useIntrigContext() {
853
+ return useContext(Context);
854
+ }
855
+
856
+ export {
857
+ Context,
858
+ GlobalState,
859
+ RequestType,
860
+ }
861
+ `;
862
+ }
863
+
864
+ function reactLoggerTemplate() {
865
+ const ts = typescript(path.resolve('src', 'logger.ts'));
866
+ return ts`
867
+ // logger.ts
868
+
869
+ import log, { LogLevelDesc } from 'loglevel';
870
+
871
+ // Extend the global interfaces
872
+ declare global {
873
+ interface Window {
874
+ setLogLevel?: (level: LogLevelDesc) => void;
875
+ }
876
+ }
877
+
878
+ // 1) Build-time default via Vite (if available)
879
+ // Cast import.meta to any to avoid TS errors if env isn't typed
880
+ const buildDefault =
881
+ typeof import.meta !== 'undefined'
882
+ ? ((import.meta as any).env?.VITE_LOG_LEVEL as string | undefined)
883
+ : undefined;
884
+
885
+ // 2) Stored default in localStorage
886
+ const storedLevel =
887
+ typeof localStorage !== 'undefined'
888
+ ? (localStorage.getItem('LOG_LEVEL') as string | null)
889
+ : null;
890
+
891
+ // Determine initial log level: build-time → stored → 'error'
892
+ const defaultLevel: LogLevelDesc =
893
+ (buildDefault as LogLevelDesc) ?? (storedLevel as LogLevelDesc) ?? 'error';
894
+
895
+ // Apply initial level
896
+ log.setLevel(defaultLevel);
897
+
898
+ // Expose a console setter to change level at runtime
899
+ if (typeof window !== 'undefined') {
900
+ window.setLogLevel = (level: LogLevelDesc): void => {
901
+ log.setLevel(level);
902
+ try {
903
+ localStorage.setItem('LOG_LEVEL', String(level));
904
+ } catch {
905
+ // ignore if storage is unavailable
906
+ }
907
+ console.log(\`✏️ loglevel set to '\${level}'\`);
908
+ };
909
+ }
910
+
911
+ // Consistent wrapper API
912
+ export const logger = {
913
+ info: (msg: unknown, meta?: unknown): void =>
914
+ meta ? log.info(msg, meta) : log.info(msg),
915
+ warn: (msg: unknown, meta?: unknown): void =>
916
+ meta ? log.warn(msg, meta) : log.warn(msg),
917
+ error: (msg: unknown, meta?: unknown): void =>
918
+ meta ? log.error(msg, meta) : log.error(msg),
919
+ debug: (msg: unknown, meta?: unknown): void =>
920
+ meta ? log.debug(msg, meta) : log.debug(msg),
921
+ };
922
+
923
+ export default logger;
924
+
925
+ `;
926
+ }
927
+
928
+ function reactExtraTemplate() {
929
+ const ts = typescript(path.resolve("src", "extra.ts"));
930
+ return ts`import {
931
+ BinaryFunctionHook,
932
+ BinaryHookOptions,
933
+ BinaryProduceHook,
934
+ ConstantHook,
935
+ error,
936
+ init,
937
+ IntrigHook,
938
+ IntrigHookOptions,
939
+ isSuccess,
940
+ NetworkState,
941
+ pending,
942
+ success,
943
+ UnaryFunctionHook,
944
+ UnaryHookOptions,
945
+ UnaryProduceHook,
946
+ UnitHook,
947
+ UnitHookOptions,
948
+ } from '@intrig/react/network-state';
949
+ import {
950
+ useCallback,
951
+ useEffect,
952
+ useId,
953
+ useMemo,
954
+ useState,
955
+ } from 'react';
956
+ import { useIntrigContext } from '@intrig/react/intrig-context';
957
+
958
+ /**
959
+ * A custom hook that manages and returns the network state of a promise-based function,
960
+ * providing a way to execute the function and clear its state.
961
+ *
962
+ * @param fn The promise-based function whose network state is to be managed. It should be a function that returns a promise.
963
+ * @param key An optional identifier for the network state. Defaults to 'default'.
964
+ * @return A tuple containing the current network state, a function to execute the promise, and a function to clear the state.
965
+ */
966
+ export function useAsNetworkState<T, F extends (...args: any) => Promise<T>>(
967
+ fn: F,
968
+ options: any = {},
969
+ ): [NetworkState<T>, (...params: Parameters<F>) => void, () => void] {
970
+ const id = useId();
971
+
972
+ const context = useIntrigContext();
973
+
974
+ const key = options.key ?? 'default';
975
+
976
+ const networkState = useMemo(() => {
977
+ return context.state?.[${"`promiseState:${id}:${key}}`"}] ?? init();
978
+ }, [context.state?.[${"`promiseState:${id}:${key}}`"}]]);
979
+
980
+ const dispatch = useCallback(
981
+ (state: NetworkState<T>) => {
982
+ context.dispatch({ key, operation: id, source: 'promiseState', state });
983
+ },
984
+ [key, context.dispatch],
985
+ );
986
+
987
+ const execute = useCallback((...args: any[]) => {
988
+ dispatch(pending());
989
+ return fn(...args).then(
990
+ (data) => {
991
+ dispatch(success(data));
992
+ },
993
+ (e) => {
994
+ dispatch(error(e));
995
+ },
996
+ );
997
+ }, []);
998
+
999
+ const clear = useCallback(() => {
1000
+ dispatch(init());
1001
+ }, []);
1002
+
1003
+ return [networkState, execute, clear];
1004
+ }
1005
+
1006
+ /**
1007
+ * A custom hook that resolves the value from the provided hook's state and updates it whenever the state changes.
1008
+ *
1009
+ * @param {IntrigHook<P, B, T>} hook - The hook that provides the state to observe and resolve data from.
1010
+ * @param options
1011
+ * @return {T | undefined} The resolved value from the hook's state or undefined if the state is not successful.
1012
+ */
1013
+ export function useResolvedValue(
1014
+ hook: UnitHook,
1015
+ options: UnitHookOptions,
1016
+ ): undefined;
1017
+
1018
+ export function useResolvedValue<T>(
1019
+ hook: ConstantHook<T>,
1020
+ options: UnitHookOptions,
1021
+ ): T | undefined;
1022
+
1023
+ export function useResolvedValue<P>(
1024
+ hook: UnaryProduceHook<P>,
1025
+ options: UnaryHookOptions<P>,
1026
+ ): undefined;
1027
+
1028
+ export function useResolvedValue<P, T>(
1029
+ hook: UnaryFunctionHook<P, T>,
1030
+ options: UnaryHookOptions<P>,
1031
+ ): T | undefined;
1032
+
1033
+ export function useResolvedValue<P, B>(
1034
+ hook: BinaryProduceHook<P, B>,
1035
+ options: BinaryHookOptions<P, B>,
1036
+ ): undefined;
1037
+
1038
+ export function useResolvedValue<P, B, T>(
1039
+ hook: BinaryFunctionHook<P, B, T>,
1040
+ options: BinaryHookOptions<P, B>,
1041
+ ): T | undefined;
1042
+
1043
+ // **Implementation**
1044
+ export function useResolvedValue<P, B, T>(
1045
+ hook: IntrigHook<P, B, T>,
1046
+ options: IntrigHookOptions<P, B>,
1047
+ ): T | undefined {
1048
+ const [value, setValue] = useState<T | undefined>();
1049
+
1050
+ const [state] = hook(options as any); // Ensure compatibility with different hook types
1051
+
1052
+ useEffect(() => {
1053
+ if (isSuccess(state)) {
1054
+ setValue(state.data);
1055
+ } else {
1056
+ setValue(undefined);
1057
+ }
1058
+ }, [state]);
1059
+
1060
+ return value;
1061
+ }
1062
+
1063
+ /**
1064
+ * A custom hook that resolves and caches the value from a successful state provided by the given hook.
1065
+ * The state is updated only when it is in a successful state.
1066
+ *
1067
+ * @param {IntrigHook<P, B, T>} hook - The hook that provides the state to observe and cache data from.
1068
+ * @param options
1069
+ * @return {T | undefined} The cached value from the hook's state or undefined if the state is not successful.
1070
+ */
1071
+ export function useResolvedCachedValue(
1072
+ hook: UnitHook,
1073
+ options: UnitHookOptions,
1074
+ ): undefined;
1075
+
1076
+ export function useResolvedCachedValue<T>(
1077
+ hook: ConstantHook<T>,
1078
+ options: UnitHookOptions,
1079
+ ): T | undefined;
1080
+
1081
+ export function useResolvedCachedValue<P>(
1082
+ hook: UnaryProduceHook<P>,
1083
+ options: UnaryHookOptions<P>,
1084
+ ): undefined;
1085
+
1086
+ export function useResolvedCachedValue<P, T>(
1087
+ hook: UnaryFunctionHook<P, T>,
1088
+ options: UnaryHookOptions<P>,
1089
+ ): T | undefined;
1090
+
1091
+ export function useResolvedCachedValue<P, B>(
1092
+ hook: BinaryProduceHook<P, B>,
1093
+ options: BinaryHookOptions<P, B>,
1094
+ ): undefined;
1095
+
1096
+ export function useResolvedCachedValue<P, B, T>(
1097
+ hook: BinaryFunctionHook<P, B, T>,
1098
+ options: BinaryHookOptions<P, B>,
1099
+ ): T | undefined;
1100
+
1101
+ // **Implementation**
1102
+ export function useResolvedCachedValue<P, B, T>(
1103
+ hook: IntrigHook<P, B, T>,
1104
+ options: IntrigHookOptions<P, B>,
1105
+ ): T | undefined {
1106
+ const [cachedValue, setCachedValue] = useState<T | undefined>();
1107
+
1108
+ const [state] = hook(options as any); // Ensure compatibility with different hook types
1109
+
1110
+ useEffect(() => {
1111
+ if (isSuccess(state)) {
1112
+ setCachedValue(state.data);
1113
+ }
1114
+ // Do not clear cached value if state is unsuccessful
1115
+ }, [state]);
1116
+
1117
+ return cachedValue;
1118
+ }
1119
+
1120
+ `;
1121
+ }
1122
+
1123
+ function reactMediaTypeUtilsTemplate() {
1124
+ const ts = typescript(path.resolve("src", "media-type-utils.ts"));
1125
+ return ts`
1126
+ import { ZodSchema } from 'zod';
1127
+ import { XMLParser } from 'fast-xml-parser';
1128
+ type EncodersSync = {
1129
+ [k: string]: <T>(request: T,
1130
+ mediaType: string,
1131
+ schema: ZodSchema) => any;
1132
+ };
1133
+
1134
+ const encoders: EncodersSync = {};
1135
+
1136
+ export function encode<T>(request: T, mediaType: string, schema: ZodSchema) {
1137
+ if (encoders[mediaType]) {
1138
+ return encoders[mediaType](request, mediaType, schema);
1139
+ }
1140
+ return request;
1141
+ }
1142
+
1143
+ encoders['application/json'] = (request, mediaType, schema) => {
1144
+ return request;
1145
+ }
1146
+
1147
+ function appendFormData(
1148
+ formData: FormData,
1149
+ data: any,
1150
+ parentKey: string
1151
+ ): void {
1152
+ if (data instanceof Blob || typeof data === 'string') {
1153
+ formData.append(parentKey, data);
1154
+ } else if (data !== null && typeof data === 'object') {
1155
+ if (Array.isArray(data)) {
1156
+ data.forEach((item: any, index: number) => {
1157
+ const key = ${"`${parentKey}`"};
1158
+ appendFormData(formData, item, key);
1159
+ });
1160
+ } else {
1161
+ Object.keys(data).forEach((key: string) => {
1162
+ const newKey = parentKey ? ${"`${parentKey}[${key}]`"} : key;
1163
+ appendFormData(formData, data[key], newKey);
1164
+ });
1165
+ }
1166
+ } else {
1167
+ formData.append(parentKey, data == null ? '' : String(data));
1168
+ }
1169
+ }
1170
+
1171
+ encoders['multipart/form-data'] = (request, mediaType, schema) => {
1172
+ const _request = request as Record<string, any>;
1173
+ const formData = new FormData();
1174
+ Object.keys(_request).forEach((key: string) => {
1175
+ appendFormData(formData, _request[key], key);
1176
+ });
1177
+ return formData;
1178
+ }
1179
+
1180
+ encoders['application/octet-stream'] = (request, mediaType, schema) => {
1181
+ return request;
1182
+ }
1183
+
1184
+ encoders['application/x-www-form-urlencoded'] = (request, mediaType, schema) => {
1185
+ const formData = new FormData();
1186
+ for (const key in request) {
1187
+ const value = request[key];
1188
+ formData.append(key, value instanceof Blob || typeof value === 'string' ? value : String(value));
1189
+ }
1190
+ return formData;
1191
+ }
1192
+
1193
+ type Transformers = {
1194
+ [k: string]: <T>(
1195
+ request: Request,
1196
+ mediaType: string,
1197
+ schema: ZodSchema
1198
+ ) => Promise<T>;
1199
+ };
1200
+
1201
+ const transformers: Transformers = {};
1202
+
1203
+ export function transform<T>(
1204
+ request: Request,
1205
+ mediaType: string,
1206
+ schema: ZodSchema
1207
+ ): Promise<T> {
1208
+ if (transformers[mediaType]) {
1209
+ return transformers[mediaType](request, mediaType, schema);
1210
+ }
1211
+ throw new Error(\`Unsupported media type: \` + mediaType);
1212
+ }
1213
+
1214
+ transformers['application/json'] = async (request, mediaType, schema) => {
1215
+ return schema.parse(await request.json());
1216
+ };
1217
+
1218
+ transformers['multipart/form-data'] = async (request, mediaType, schema) => {
1219
+ const formData = await request.formData();
1220
+ const content: Record<string, any> = {};
1221
+ formData.forEach((value, key) => {
1222
+ if (content[key]) {
1223
+ if (!(content[key] instanceof Array)) {
1224
+ content[key] = [content[key]];
1225
+ }
1226
+ content[key].push(value);
1227
+ } else {
1228
+ content[key] = value
1229
+ }
1230
+ });
1231
+ return schema.parse(content);
1232
+ };
1233
+
1234
+ transformers['application/octet-stream'] = async (
1235
+ request,
1236
+ mediaType,
1237
+ schema
1238
+ ) => {
1239
+ return schema.parse(
1240
+ new Blob([await request.arrayBuffer()], {
1241
+ type: 'application/octet-stream',
1242
+ })
1243
+ );
1244
+ };
1245
+
1246
+ transformers['application/x-www-form-urlencoded'] = async (
1247
+ request,
1248
+ mediaType,
1249
+ schema
1250
+ ) => {
1251
+ const formData = await request.formData();
1252
+ const content: Record<string, any> = {};
1253
+ formData.forEach((value, key) => (content[key] = value));
1254
+ return schema.parse(content);
1255
+ };
1256
+
1257
+ transformers['application/xml'] = async (request, mediaType, schema) => {
1258
+ const xmlParser = new XMLParser();
1259
+ const content = await xmlParser.parse(await request.text());
1260
+ return schema.parse(await content);
1261
+ };
1262
+
1263
+ transformers['text/plain'] = async (request, mediaType, schema) => {
1264
+ return schema.parse(await request.text());
1265
+ };
1266
+
1267
+ transformers['text/html'] = async (request, mediaType, schema) => {
1268
+ return schema.parse(await request.text());
1269
+ };
1270
+
1271
+ transformers['text/css'] = async (request, mediaType, schema) => {
1272
+ return schema.parse(await request.text());
1273
+ };
1274
+
1275
+ transformers['text/javascript'] = async (request, mediaType, schema) => {
1276
+ return schema.parse(await request.text());
1277
+ };
1278
+
1279
+ type ResponseTransformers = {
1280
+ [k: string]: <T>(
1281
+ data: any,
1282
+ mediaType: string,
1283
+ schema: ZodSchema
1284
+ ) => Promise<T>;
1285
+ };
1286
+
1287
+ const responseTransformers: ResponseTransformers = {};
1288
+
1289
+ responseTransformers['application/json'] = async (data, mediaType, schema) => {
1290
+ return schema.parse(data);
1291
+ };
1292
+
1293
+ responseTransformers['application/xml'] = async (data, mediaType, schema) => {
1294
+ const parsed = new XMLParser().parse(data);
1295
+ return schema.parse(parsed);
1296
+ }
1297
+
1298
+ export async function transformResponse<T>(
1299
+ data: any,
1300
+ mediaType: string,
1301
+ schema: ZodSchema
1302
+ ): Promise<T> {
1303
+ if (responseTransformers[mediaType]) {
1304
+ return await responseTransformers[mediaType](data, mediaType, schema);
1305
+ }
1306
+ return data
1307
+ }
1308
+ `;
1309
+ }
1310
+
1311
+ function typeUtilsTemplate() {
1312
+ const ts = typescript(path__default.resolve('src', 'type-utils.ts'));
1313
+ return ts`import { z } from 'zod';
1314
+
1315
+ export type BinaryData = Blob;
1316
+ export const BinaryDataSchema: z.ZodType<BinaryData> = z.instanceof(Blob);
1317
+
1318
+ // Base64 helpers (browser + Node compatible; no Buffer required)
1319
+ export function base64ToUint8Array(b64: string): Uint8Array {
1320
+ if (typeof atob === 'function') {
1321
+ // Browser
1322
+ const bin = atob(b64);
1323
+ const bytes = new Uint8Array(bin.length);
1324
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
1325
+ return bytes;
1326
+ } else {
1327
+ // Node
1328
+ const buf = Buffer.from(b64, 'base64');
1329
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
1330
+ }
1331
+ }
1332
+
1333
+ `;
1334
+ }
1335
+
1336
+ function reactTsConfigTemplate() {
1337
+ const json = jsonLiteral(path.resolve('tsconfig.json'));
1338
+ return json`
1339
+ {
1340
+ "compilerOptions": {
1341
+ "target": "es2020",
1342
+ "module": "ESNext",
1343
+ "declaration": true,
1344
+ "outDir": "./dist",
1345
+ "strict": true,
1346
+ "esModuleInterop": true,
1347
+ "noImplicitAny": false,
1348
+ "moduleResolution": "node",
1349
+ "baseUrl": "./",
1350
+ "paths": {
1351
+ "@intrig/react": [
1352
+ "./src"
1353
+ ],
1354
+ "@intrig/react/*": [
1355
+ "./src/*"
1356
+ ],
1357
+ "intrig-hook": ["src/config/intrig"]
1358
+ },
1359
+ "jsx": "react-jsx",
1360
+ "skipLibCheck": true
1361
+ },
1362
+ "exclude": [
1363
+ "node_modules",
1364
+ "../../node_modules",
1365
+ "**/__tests__/*"
1366
+ ]
1367
+ }
1368
+ `;
1369
+ }
1370
+
1371
+ function reactSwcrcTemplate() {
1372
+ const json = jsonLiteral(path.resolve('.swcrc'));
1373
+ return json`
1374
+ {
1375
+ "jsc": {
1376
+ "parser": {
1377
+ "syntax": "typescript",
1378
+ "decorators": false,
1379
+ "dynamicImport": true
1380
+ },
1381
+ "target": "es2022",
1382
+ "externalHelpers": false
1383
+ },
1384
+ "module": {
1385
+ "type": "es6",
1386
+ "noInterop": false
1387
+ },
1388
+ "sourceMaps": true,
1389
+ "exclude": ["../../node_modules"]
1390
+ }
1391
+ `;
1392
+ }
1393
+
1394
+ function intrigMiddlewareTemplate() {
1395
+ const ts = typescript(path.resolve('src', 'intrig-middleware.ts'));
1396
+ return ts`
1397
+ import axios from 'axios';
1398
+ import { requestInterceptor } from 'intrig-hook';
1399
+
1400
+ export function getAxiosInstance(key: string) {
1401
+ let axiosInstance = axios.create({
1402
+ baseURL: process.env[${"`${key.toUpperCase()}_API_URL`"}],
1403
+ });
1404
+
1405
+ axiosInstance.interceptors.request.use(requestInterceptor);
1406
+
1407
+ return axiosInstance;
1408
+ }
1409
+ `;
1410
+ }
1411
+
1412
+ function providerMainTemplate(apisToSync) {
1413
+ const ts = typescript(path.resolve("src", "intrig-provider-main.tsx"));
1414
+ return ts`// Re-export all provider functionality from modular templates
1415
+ export * from './interfaces';
1416
+ export * from './reducer';
1417
+ export * from './axios-config';
1418
+ export * from './intrig-provider';
1419
+ export * from './intrig-provider-stub';
1420
+ export * from './status-trap';
1421
+ export * from './provider-hooks';
1422
+
1423
+ // Main provider exports for backward compatibility
1424
+ export { IntrigProvider } from './intrig-provider';
1425
+ export { IntrigProviderStub } from './intrig-provider-stub';
1426
+ export { StatusTrap } from './status-trap';
1427
+
1428
+ export {
1429
+ useNetworkState,
1430
+ useTransitionCall,
1431
+ useCentralError,
1432
+ useCentralPendingState,
1433
+ } from './provider-hooks';
1434
+
1435
+ export {
1436
+ requestReducer,
1437
+ inferNetworkReason,
1438
+ debounce,
1439
+ } from './reducer';
1440
+
1441
+ export {
1442
+ createAxiosInstance,
1443
+ createAxiosInstances,
1444
+ } from './axios-config';
1445
+
1446
+ export type {
1447
+ DefaultConfigs,
1448
+ IntrigProviderProps,
1449
+ IntrigProviderStubProps,
1450
+ StatusTrapProps,
1451
+ NetworkStateProps,
1452
+ StubType,
1453
+ WithStubSupport,
1454
+ } from './interfaces';
1455
+ `;
1456
+ }
1457
+
1458
+ function providerHooksTemplate() {
1459
+ const ts = typescript(path.resolve("src", "provider-hooks.ts"));
1460
+ return ts`import React, {
1461
+ useCallback,
1462
+ useContext,
1463
+ useMemo,
1464
+ useState,
1465
+ useRef,
1466
+ } from 'react';
1467
+ import {
1468
+ ErrorState,
1469
+ ErrorWithContext,
1470
+ isSuccess,
1471
+ isError,
1472
+ isPending,
1473
+ NetworkState,
1474
+ pending,
1475
+ Progress,
1476
+ init,
1477
+ } from './network-state';
1478
+ import {
1479
+ AxiosProgressEvent,
1480
+ } from 'axios';
1481
+ import { ZodSchema } from 'zod';
1482
+ import logger from './logger';
1483
+
1484
+ import { Context, RequestType, SchemaOf } from './intrig-context';
1485
+ import { NetworkStateProps } from './interfaces';
1486
+ import { debounce } from './reducer';
1487
+
1488
+ /**
1489
+ * useNetworkState is a custom hook that manages the network state within the specified context.
1490
+ * It handles making network requests, dispatching appropriate states based on the request lifecycle,
1491
+ * and allows aborting ongoing requests.
1492
+ */
1493
+ export function useNetworkState<T>({
1494
+ key,
1495
+ operation,
1496
+ source,
1497
+ schema,
1498
+ errorSchema,
1499
+ debounceDelay: requestDebounceDelay,
1500
+ }: NetworkStateProps<T>): [
1501
+ NetworkState<T>,
1502
+ (request: RequestType) => void,
1503
+ () => void,
1504
+ (state: NetworkState<T>) => void,
1505
+ ] {
1506
+ const context = useContext(Context);
1507
+
1508
+ const [abortController, setAbortController] = useState<AbortController>();
1509
+
1510
+ const networkState = useMemo(() => {
1511
+ logger.info(${"`Updating status ${key} ${operation} ${source}`"});
1512
+ logger.debug("<=", context.state?.[${"`${source}:${operation}:${key}`"}])
1513
+ return (
1514
+ (context.state?.[${"`${source}:${operation}:${key}`"}] as NetworkState<T>) ??
1515
+ init()
1516
+ );
1517
+ }, [JSON.stringify(context.state?.[${"`${source}:${operation}:${key}`"}])]);
1518
+
1519
+ const dispatch = useCallback(
1520
+ (state: NetworkState<T>) => {
1521
+ context.dispatch({ key, operation, source, state });
1522
+ },
1523
+ [key, operation, source, context.dispatch],
1524
+ );
1525
+
1526
+ const debounceDelay = useMemo(() => {
1527
+ return (
1528
+ requestDebounceDelay ?? context.configs?.[source as keyof (typeof context)['configs']]?.debounceDelay ?? 0
1529
+ );
1530
+ }, [context.configs, requestDebounceDelay, source]);
1531
+
1532
+ const execute = useCallback(
1533
+ async (request: RequestType) => {
1534
+ logger.info(${"`Executing request ${key} ${operation} ${source}`"});
1535
+ logger.debug("=>", request)
1536
+
1537
+ const abortController = new AbortController();
1538
+ setAbortController(abortController);
1539
+
1540
+ const requestConfig: RequestType = {
1541
+ ...request,
1542
+ onUploadProgress(event: AxiosProgressEvent) {
1543
+ dispatch(
1544
+ pending({
1545
+ type: 'upload',
1546
+ loaded: event.loaded,
1547
+ total: event.total,
1548
+ }),
1549
+ );
1550
+ request.onUploadProgress?.(event);
1551
+ },
1552
+ onDownloadProgress(event: AxiosProgressEvent) {
1553
+ dispatch(
1554
+ pending({
1555
+ type: 'download',
1556
+ loaded: event.loaded,
1557
+ total: event.total,
1558
+ }),
1559
+ );
1560
+ request.onDownloadProgress?.(event);
1561
+ },
1562
+ signal: abortController.signal,
1563
+ };
1564
+
1565
+ await context.execute(
1566
+ requestConfig,
1567
+ dispatch,
1568
+ schema,
1569
+ errorSchema as any,
1570
+ );
1571
+ },
1572
+ [networkState, context.dispatch],
1573
+ );
1574
+
1575
+ const deboundedExecute = useMemo(
1576
+ () => debounce(execute, debounceDelay ?? 0),
1577
+ [execute],
1578
+ );
1579
+
1580
+ const clear = useCallback(() => {
1581
+ logger.info(${"`Clearing request ${key} ${operation} ${source}`"});
1582
+ dispatch(init());
1583
+ setAbortController((abortController) => {
1584
+ logger.info(${"`Aborting request ${key} ${operation} ${source}`"});
1585
+ abortController?.abort();
1586
+ return undefined;
1587
+ });
1588
+ }, [dispatch, abortController]);
1589
+
1590
+ return [networkState, deboundedExecute, clear, dispatch];
1591
+ }
1592
+
1593
+ /**
1594
+ * A hook for making transient calls that can be aborted and validated against schemas.
1595
+ */
1596
+ export function useTransitionCall<T>({
1597
+ schema,
1598
+ errorSchema,
1599
+ }: {
1600
+ schema?: SchemaOf<T>;
1601
+ errorSchema?: SchemaOf<T>;
1602
+ }): [(request: RequestType) => Promise<T>, () => void] {
1603
+ const ctx = useContext(Context);
1604
+ const controller = useRef<AbortController | undefined>(undefined);
1605
+
1606
+ const call = useCallback(
1607
+ async (request: RequestType) => {
1608
+ controller.current?.abort();
1609
+ const abort = new AbortController();
1610
+ controller.current = abort;
1611
+
1612
+ return new Promise<T>((resolve, reject) => {
1613
+ ctx.execute(
1614
+ { ...request, signal: abort.signal },
1615
+ (state) => {
1616
+ if (isSuccess(state)) {
1617
+ resolve(state.data as T);
1618
+ } else if (isError(state)) {
1619
+ reject(state.error);
1620
+ }
1621
+ },
1622
+ schema,
1623
+ errorSchema,
1624
+ );
1625
+ });
1626
+ },
1627
+ [ctx, schema, errorSchema],
1628
+ );
1629
+
1630
+ const abort = useCallback(() => {
1631
+ controller.current?.abort();
1632
+ }, []);
1633
+
1634
+ return [call, abort];
1635
+ }
1636
+
1637
+ /**
1638
+ * Handles central error extraction from the provided context.
1639
+ * It filters the state to retain error states and maps them to a structured error object with additional context information.
1640
+ * @return {Object[]} An array of objects representing the error states with context information such as source, operation, and key.
1641
+ */
1642
+ export function useCentralError() {
1643
+ const ctx = useContext(Context);
1644
+
1645
+ return useMemo(() => {
1646
+ return Object.entries(ctx.filteredState as Record<string, NetworkState>)
1647
+ .filter(([, state]) => isError(state))
1648
+ .map(([k, state]) => {
1649
+ const [source, operation, key] = k.split(':');
1650
+ return {
1651
+ ...(state as ErrorState<unknown>),
1652
+ source,
1653
+ operation,
1654
+ key,
1655
+ } satisfies ErrorWithContext;
1656
+ });
1657
+ }, [ctx.filteredState]);
1658
+ }
1659
+
1660
+ /**
1661
+ * Uses central pending state handling by aggregating pending states from context.
1662
+ * It calculates the overall progress of pending states if any, or returns an initial state otherwise.
1663
+ *
1664
+ * @return {NetworkState} The aggregated network state based on the pending states and their progress.
1665
+ */
1666
+ export function useCentralPendingState() {
1667
+ const ctx = useContext(Context);
1668
+
1669
+ const result: NetworkState = useMemo(() => {
1670
+ const pendingStates = Object.values(
1671
+ ctx.filteredState as Record<string, NetworkState>,
1672
+ ).filter(isPending);
1673
+ if (!pendingStates.length) {
1674
+ return init();
1675
+ }
1676
+
1677
+ const progress = pendingStates
1678
+ .filter((a) => a.progress)
1679
+ .reduce(
1680
+ (progress, current) => {
1681
+ return {
1682
+ total: progress.total + (current.progress?.total ?? 0),
1683
+ loaded: progress.loaded + (current.progress?.loaded ?? 0),
1684
+ };
1685
+ },
1686
+ { total: 0, loaded: 0 } satisfies Progress,
1687
+ );
1688
+ return pending(progress.total ? progress : undefined);
1689
+ }, [ctx.filteredState]);
1690
+
1691
+ return result;
1692
+ }
1693
+ `;
1694
+ }
1695
+
1696
+ function providerInterfacesTemplate(apisToSync) {
1697
+ const configType = `{
1698
+ defaults?: DefaultConfigs,
1699
+ ${apisToSync.map((a)=>`${a.id}?: DefaultConfigs`).join(",\n ")}
1700
+ }`;
1701
+ const ts = typescript(path.resolve("src", "interfaces.ts"));
1702
+ return ts`import {
1703
+ CreateAxiosDefaults,
1704
+ InternalAxiosRequestConfig,
1705
+ AxiosResponse,
1706
+ } from 'axios';
1707
+ import {
1708
+ IntrigHook,
1709
+ NetworkState,
1710
+ } from './network-state';
1711
+ import { SchemaOf } from './intrig-context';
1712
+
1713
+ export interface DefaultConfigs extends CreateAxiosDefaults {
1714
+ debounceDelay?: number;
1715
+ requestInterceptor?: (
1716
+ config: InternalAxiosRequestConfig,
1717
+ ) => Promise<InternalAxiosRequestConfig>;
1718
+ responseInterceptor?: (
1719
+ config: AxiosResponse<any>,
1720
+ ) => Promise<AxiosResponse<any>>;
1721
+ }
1722
+
1723
+ export interface IntrigProviderProps {
1724
+ configs?: ${configType};
1725
+ children: React.ReactNode;
1726
+ }
1727
+
1728
+ export interface StubType {
1729
+ <P, B, T>(
1730
+ hook: IntrigHook<P, B, T>,
1731
+ fn: (
1732
+ params: P,
1733
+ body: B,
1734
+ dispatch: (state: NetworkState<T>) => void,
1735
+ ) => Promise<void>,
1736
+ ): void;
1737
+ }
1738
+
1739
+ export type WithStubSupport<T> = T & {
1740
+ stubs?: (stub: StubType) => void;
1741
+ };
1742
+
1743
+ export interface IntrigProviderStubProps {
1744
+ configs?: ${configType};
1745
+ stubs?: (stub: StubType) => void;
1746
+ children: React.ReactNode;
1747
+ }
1748
+
1749
+ export interface StatusTrapProps {
1750
+ type: 'pending' | 'error' | 'pending + error';
1751
+ propagate?: boolean;
1752
+ }
1753
+
1754
+ export interface NetworkStateProps<T> {
1755
+ key: string;
1756
+ operation: string;
1757
+ source: string;
1758
+ schema?: SchemaOf<T>;
1759
+ errorSchema?: SchemaOf<any>;
1760
+ debounceDelay?: number;
1761
+ }
1762
+ `;
1763
+ }
1764
+
1765
+ function providerReducerTemplate() {
1766
+ const ts = typescript(path.resolve("src", "reducer.ts"));
1767
+ return ts`import {
1768
+ NetworkAction,
1769
+ } from './network-state';
1770
+ import { GlobalState } from './intrig-context';
1771
+
1772
+ /**
1773
+ * Handles state updates for network requests based on the provided action.
1774
+ *
1775
+ * @param {GlobalState} state - The current state of the application.
1776
+ * @param {NetworkAction<unknown>} action - The action containing source, operation, key, and state.
1777
+ * @return {GlobalState} - The updated state after applying the action.
1778
+ */
1779
+ export function requestReducer(
1780
+ state: GlobalState,
1781
+ action: NetworkAction<unknown>,
1782
+ ): GlobalState {
1783
+ return {
1784
+ ...state,
1785
+ [${"`${action.source}:${action.operation}:${action.key}`"}]: action.state,
1786
+ };
1787
+ }
1788
+
1789
+ function inferNetworkReason(e: any): 'timeout' | 'dns' | 'offline' | 'aborted' | 'unknown' {
1790
+ if (e?.code === 'ECONNABORTED') return 'timeout';
1791
+ if (typeof navigator !== 'undefined' && navigator.onLine === false) return 'offline';
1792
+ if (e?.name === 'AbortError') return 'aborted';
1793
+ return 'unknown';
1794
+ }
1795
+
1796
+ function debounce<T extends (...args: any[]) => void>(func: T, delay: number) {
1797
+ let timeoutId: any;
1798
+
1799
+ return (...args: Parameters<T>) => {
1800
+ if (timeoutId) {
1801
+ clearTimeout(timeoutId);
1802
+ }
1803
+ timeoutId = setTimeout(() => {
1804
+ func(...args);
1805
+ }, delay);
1806
+ };
1807
+ }
1808
+
1809
+ export { inferNetworkReason, debounce };
1810
+ `;
1811
+ }
1812
+
1813
+ function providerAxiosConfigTemplate(apisToSync) {
1814
+ const axiosConfigs = apisToSync.map((a)=>`
1815
+ ${a.id}: createAxiosInstance(configs.defaults, configs['${a.id}']),
1816
+ `).join("\n");
1817
+ const ts = typescript(path.resolve("src", "axios-config.ts"));
1818
+ return ts`import axios, {
1819
+ Axios,
1820
+ AxiosResponse,
1821
+ InternalAxiosRequestConfig,
1822
+ } from 'axios';
1823
+ import { DefaultConfigs } from './interfaces';
1824
+
1825
+ export function createAxiosInstance(
1826
+ defaultConfig?: DefaultConfigs,
1827
+ config?: DefaultConfigs,
1828
+ ) {
1829
+ const axiosInstance = axios.create({
1830
+ ...(defaultConfig ?? {}),
1831
+ ...(config ?? {}),
1832
+ });
1833
+
1834
+ async function requestInterceptor(cfg: InternalAxiosRequestConfig) {
1835
+ const intermediate = (await defaultConfig?.requestInterceptor?.(cfg)) ?? cfg;
1836
+ return config?.requestInterceptor?.(intermediate) ?? intermediate;
1837
+ }
1838
+
1839
+ async function responseInterceptor(cfg: AxiosResponse<any>) {
1840
+ const intermediate = (await defaultConfig?.responseInterceptor?.(cfg)) ?? cfg;
1841
+ return config?.responseInterceptor?.(intermediate) ?? intermediate;
1842
+ }
1843
+
1844
+ axiosInstance.interceptors.request.use(requestInterceptor);
1845
+ axiosInstance.interceptors.response.use(responseInterceptor, (error) => {
1846
+ return Promise.reject(error);
1847
+ });
1848
+ return axiosInstance;
1849
+ }
1850
+
1851
+ export function createAxiosInstances(configs: any): Record<string, Axios> {
1852
+ return {
1853
+ ${axiosConfigs}
1854
+ };
1855
+ }
1856
+ `;
1857
+ }
1858
+
1859
+ function reactIntrigProviderTemplate(_apisToSync) {
1860
+ const ts = typescript(path.resolve('src', 'intrig-provider.tsx'));
1861
+ return ts`import React, { useMemo, useReducer } from 'react';
1862
+ import {
1863
+ error,
1864
+ NetworkState,
1865
+ pending,
1866
+ success,
1867
+ responseValidationError,
1868
+ configError,
1869
+ httpError,
1870
+ networkError,
1871
+ } from './network-state';
1872
+ import { Axios, AxiosResponse, isAxiosError } from 'axios';
1873
+ import { ZodError, ZodSchema } from 'zod';
1874
+ import { flushSync } from 'react-dom';
1875
+ import { createParser } from 'eventsource-parser';
1876
+
1877
+ import { Context, RequestType, GlobalState } from './intrig-context';
1878
+ import { IntrigProviderProps } from './interfaces';
1879
+ import { createAxiosInstances } from './axios-config';
1880
+ import { requestReducer, inferNetworkReason } from './reducer';
1881
+
1882
+ /**
1883
+ * IntrigProvider is a context provider component that sets up global state management
1884
+ * and provides Axios instances for API requests.
1885
+ */
1886
+ export function IntrigProvider({ children, configs = {} }: IntrigProviderProps) {
1887
+ const [state, dispatch] = useReducer(requestReducer, {} as GlobalState);
1888
+
1889
+ const axiosInstances: Record<string, Axios> = useMemo(() => {
1890
+ return createAxiosInstances(configs);
1891
+ }, [configs]);
1892
+
1893
+ const contextValue = useMemo(() => {
1894
+ async function execute<T>(
1895
+ request: RequestType,
1896
+ setState: (s: NetworkState<T>) => void,
1897
+ schema?: ZodSchema<T>, // success payload schema (optional)
1898
+ errorSchema?: ZodSchema<any>, // error-body schema for non-2xx (optional)
1899
+ ) {
1900
+ try {
1901
+ setState(pending());
1902
+
1903
+ const axios = axiosInstances[request.source];
1904
+ if (!axios) {
1905
+ setState(error(configError(${"`Unknown axios source '${request.source}'`"})));
1906
+ return;
1907
+ }
1908
+
1909
+ const response = await axios.request(request);
1910
+ const status = response.status;
1911
+ const method = (response.config?.method || 'GET').toUpperCase();
1912
+ const url = response.config?.url || '';
1913
+ const ctype = String(response.headers?.['content-type'] || '');
1914
+
1915
+ // -------------------- 2xx branch --------------------
1916
+ if (status >= 200 && status < 300) {
1917
+ // SSE stream
1918
+ if (ctype.includes('text/event-stream')) {
1919
+ const reader = response.data.getReader();
1920
+ const decoder = new TextDecoder();
1921
+
1922
+ let lastMessage: any;
1923
+
1924
+ const parser = createParser({
1925
+ onEvent(evt) {
1926
+ let decoded: unknown = evt.data;
1927
+
1928
+ // Try JSON parse; if schema is defined, we require valid JSON for validation
1929
+ try {
1930
+ let parsed: unknown = JSON.parse(String(decoded));
1931
+ if (schema) {
1932
+ const vr = schema.safeParse(parsed);
1933
+ if (!vr.success) {
1934
+ setState(error(responseValidationError(vr.error, parsed)));
1935
+ return;
1936
+ }
1937
+ parsed = vr.data;
1938
+ }
1939
+ decoded = parsed;
1940
+ } catch (ignore) {
1941
+ if (schema) {
1942
+ // schema expects structured data but chunk wasn't JSON
1943
+ setState(error(responseValidationError(new ZodError([]), decoded)));
1944
+ return;
1945
+ }
1946
+ // if no schema, pass raw text
1947
+ }
1948
+
1949
+ lastMessage = decoded;
1950
+ flushSync(() => setState(pending(undefined, decoded as T)));
1951
+ },
1952
+ });
1953
+
1954
+ while (true) {
1955
+ const { done, value } = await reader.read();
1956
+ if (done) {
1957
+ flushSync(() => setState(success(lastMessage as T, response.headers)));
1958
+ break;
1959
+ }
1960
+ parser.feed(decoder.decode(value, { stream: true }));
1961
+ }
1962
+ return;
1963
+ }
1964
+
1965
+ // Non-SSE: validate body if a schema is provided
1966
+ if (schema) {
1967
+ const parsed = schema.safeParse(response.data);
1968
+ if (!parsed.success) {
1969
+ setState(error(responseValidationError(parsed.error, response.data)));
1970
+ return;
1971
+ }
1972
+ setState(success(parsed.data, response.headers));
1973
+ return;
1974
+ }
1975
+
1976
+ // No schema → pass through
1977
+ setState(success(response.data as T, response.headers));
1978
+ return;
1979
+ }
1980
+
1981
+ // -------------------- non-2xx (HTTP error) --------------------
1982
+ let errorBody: unknown = response.data;
1983
+
1984
+ if (errorSchema) {
1985
+ const ev = errorSchema.safeParse(errorBody ?? {});
1986
+ if (!ev.success) {
1987
+ setState(error(responseValidationError(ev.error, errorBody)));
1988
+ return;
1989
+ }
1990
+ errorBody = ev.data;
1991
+ }
1992
+
1993
+ setState(error(httpError(status, url, method, response.headers, errorBody)));
1994
+
1995
+ } catch (e: any) {
1996
+ // -------------------- thrown / transport --------------------
1997
+ if (isAxiosError(e)) {
1998
+ const status = e.response?.status;
1999
+ const method = (e.config?.method || 'GET').toUpperCase();
2000
+ const url = e.config?.url || '';
2001
+
2002
+ if (status != null) {
2003
+ // HTTP error with response
2004
+ let errorBody: unknown = e.response?.data;
2005
+
2006
+ if (errorSchema) {
2007
+ const ev = errorSchema.safeParse(errorBody ?? {});
2008
+ if (!ev.success) {
2009
+ setState(error(responseValidationError(ev.error, errorBody)));
2010
+ return;
2011
+ }
2012
+ errorBody = ev.data;
2013
+ }
2014
+
2015
+ setState(error(httpError(status, url, method, e.response?.headers, errorBody)));
2016
+ return;
2017
+ }
2018
+
2019
+ // No response → network layer
2020
+ setState(error(networkError(inferNetworkReason(e), e.request)));
2021
+ return;
2022
+ }
2023
+
2024
+ // Non-Axios exception → treat as unknown network-ish failure
2025
+ setState(error(networkError('unknown')));
2026
+ }
2027
+ }
2028
+ return {
2029
+ state,
2030
+ dispatch,
2031
+ filteredState: state,
2032
+ configs,
2033
+ execute,
2034
+ };
2035
+ }, [state, axiosInstances]);
2036
+
2037
+ return <Context.Provider value={contextValue}>{children}</Context.Provider>;
2038
+ }
2039
+ `;
2040
+ }
2041
+
2042
+ function reactIntrigProviderStubTemplate(_apisToSync) {
2043
+ const ts = typescript(path.resolve('src', 'intrig-provider-stub.tsx'));
2044
+ return ts`import { useMemo, useReducer } from 'react';
2045
+ import { ZodSchema } from 'zod';
2046
+ import { Context, RequestType, GlobalState } from './intrig-context';
2047
+ import { IntrigProviderStubProps } from './interfaces';
2048
+ import { error, configError, init, NetworkState } from './network-state';
2049
+ import { requestReducer } from './reducer';
2050
+
2051
+ export function IntrigProviderStub({
2052
+ children,
2053
+ configs = {},
2054
+ stubs = () => {
2055
+ //intentionally left blank
2056
+ },
2057
+ }: IntrigProviderStubProps) {
2058
+ const [state, dispatch] = useReducer(requestReducer, {} as GlobalState);
2059
+
2060
+ const collectedStubs = useMemo(() => {
2061
+ const fns: Record<string, (
2062
+ params: any,
2063
+ body: any,
2064
+ dispatch: (state: NetworkState<any>) => void,
2065
+ ) => Promise<void>> = {};
2066
+ function stub<P, B, T>(
2067
+ hook: any,
2068
+ fn: (
2069
+ params: P,
2070
+ body: B,
2071
+ dispatch: (state: NetworkState<T>) => void,
2072
+ ) => Promise<void>,
2073
+ ) {
2074
+ fns[hook.key] = fn;
2075
+ }
2076
+ stubs(stub);
2077
+ return fns;
2078
+ }, [stubs]);
2079
+
2080
+ const contextValue = useMemo(() => {
2081
+ async function execute<T>(
2082
+ request: RequestType,
2083
+ dispatch: (state: NetworkState<T>) => void,
2084
+ schema: ZodSchema<T> | undefined,
2085
+ ) {
2086
+ const stub = collectedStubs[request.key];
2087
+
2088
+ if (stub) {
2089
+ try {
2090
+ await stub(request.params, request.data, dispatch);
2091
+ } catch (e: any) {
2092
+ dispatch(error(configError(e?.message ?? '')));
2093
+ }
2094
+ } else {
2095
+ dispatch(init());
2096
+ }
2097
+ }
2098
+
2099
+ return {
2100
+ state,
2101
+ dispatch,
2102
+ filteredState: state,
2103
+ configs,
2104
+ execute,
2105
+ };
2106
+ }, [state, dispatch, configs, collectedStubs]);
2107
+
2108
+ return <Context.Provider value={contextValue}>{children}</Context.Provider>;
2109
+ }
2110
+ `;
2111
+ }
2112
+
2113
+ function reactStatusTrapTemplate(_apisToSync) {
2114
+ const ts = typescript(path.resolve('src', 'status-trap.tsx'));
2115
+ return ts`import React, { PropsWithChildren, useCallback, useContext, useMemo, useState } from 'react';
2116
+ import { isError, isPending, NetworkState } from './network-state';
2117
+ import { Context } from './intrig-context';
2118
+ import { StatusTrapProps } from './interfaces';
2119
+
2120
+ /**
2121
+ * StatusTrap component is used to track and manage network request states.
2122
+ */
2123
+ export function StatusTrap({
2124
+ children,
2125
+ type,
2126
+ propagate = true,
2127
+ }: PropsWithChildren<StatusTrapProps>) {
2128
+ const ctx = useContext(Context);
2129
+
2130
+ const [requests, setRequests] = useState<string[]>([]);
2131
+
2132
+ const shouldHandleEvent = useCallback(
2133
+ (state: NetworkState) => {
2134
+ switch (type) {
2135
+ case 'error':
2136
+ return isError(state);
2137
+ case 'pending':
2138
+ return isPending(state);
2139
+ case 'pending + error':
2140
+ return isPending(state) || isError(state);
2141
+ default:
2142
+ return false;
2143
+ }
2144
+ },
2145
+ [type],
2146
+ );
2147
+
2148
+ const dispatch = useCallback(
2149
+ (event: any) => {
2150
+ const composite = ${"`${event.source}:${event.operation}:${event.key}`"};
2151
+ if (!event.handled) {
2152
+ if (shouldHandleEvent(event.state)) {
2153
+ setRequests((prev) => [...prev, composite]);
2154
+ if (!propagate) {
2155
+ ctx.dispatch({
2156
+ ...event,
2157
+ handled: true,
2158
+ });
2159
+ return;
2160
+ }
2161
+ } else {
2162
+ setRequests((prev) => prev.filter((k) => k !== composite));
2163
+ }
2164
+ }
2165
+ ctx.dispatch(event);
2166
+ },
2167
+ [ctx, propagate, shouldHandleEvent],
2168
+ );
2169
+
2170
+ const filteredState = useMemo(() => {
2171
+ return Object.fromEntries(
2172
+ Object.entries(ctx.state).filter(([key]) => requests.includes(key)),
2173
+ );
2174
+ }, [ctx.state, requests]);
2175
+
2176
+ return (
2177
+ <Context.Provider
2178
+ value={{
2179
+ ...ctx,
2180
+ dispatch,
2181
+ filteredState,
2182
+ }}
2183
+ >
2184
+ {children}
2185
+ </Context.Provider>
2186
+ );
2187
+ }
2188
+ `;
2189
+ }
2190
+
2191
+ function extractHookShapeAndOptionsShape$1(response, requestBody, imports) {
2192
+ if (response) {
2193
+ if (requestBody) {
2194
+ imports.add(`import { BinaryFunctionHook, BinaryHookOptions } from "@intrig/react"`);
2195
+ return {
2196
+ hookShape: `BinaryFunctionHook<Params, RequestBody, Response>`,
2197
+ optionsShape: `BinaryHookOptions<Params, RequestBody>`
2198
+ };
2199
+ } else {
2200
+ imports.add(`import { UnaryFunctionHook, UnaryHookOptions } from "@intrig/react"`);
2201
+ return {
2202
+ hookShape: `UnaryFunctionHook<Params, Response>`,
2203
+ optionsShape: `UnaryHookOptions<Params>`
2204
+ };
2205
+ }
2206
+ } else {
2207
+ if (requestBody) {
2208
+ imports.add(`import { BinaryProduceHook, BinaryHookOptions } from "@intrig/react"`);
2209
+ return {
2210
+ hookShape: `BinaryProduceHook<Params, RequestBody>`,
2211
+ optionsShape: `BinaryHookOptions<Params, RequestBody>`
2212
+ };
2213
+ } else {
2214
+ imports.add(`import { UnaryProduceHook, UnaryHookOptions } from "@intrig/react"`);
2215
+ return {
2216
+ hookShape: `UnaryProduceHook<Params>`,
2217
+ optionsShape: `UnaryHookOptions<Params>`
2218
+ };
2219
+ }
2220
+ }
2221
+ }
2222
+ function extractParamDeconstruction$2(variables, requestBody) {
2223
+ const isParamMandatory = (variables == null ? void 0 : variables.some((a)=>a.in === 'path')) || false;
2224
+ if (requestBody) {
2225
+ if (isParamMandatory) {
2226
+ return {
2227
+ paramExpression: 'data, p',
2228
+ paramType: 'data: RequestBody, params: Params'
2229
+ };
2230
+ } else {
2231
+ return {
2232
+ paramExpression: 'data, p = {}',
2233
+ paramType: 'data: RequestBody, params?: Params'
2234
+ };
2235
+ }
2236
+ } else {
2237
+ if (isParamMandatory) {
2238
+ return {
2239
+ paramExpression: 'p',
2240
+ paramType: 'params: Params'
2241
+ };
2242
+ } else {
2243
+ return {
2244
+ paramExpression: 'p = {}',
2245
+ paramType: 'params?: Params'
2246
+ };
2247
+ }
2248
+ }
2249
+ }
2250
+ function extractErrorParams$2(errorTypes) {
2251
+ switch(errorTypes.length){
2252
+ case 0:
2253
+ return `
2254
+ export type _ErrorType = any
2255
+ const errorSchema = z.any()`;
2256
+ case 1:
2257
+ return `
2258
+ export type _ErrorType = ${errorTypes[0]}
2259
+ const errorSchema = ${errorTypes[0]}Schema`;
2260
+ default:
2261
+ return `
2262
+ export type _ErrorType = ${errorTypes.join(' | ')}
2263
+ const errorSchema = z.union([${errorTypes.map((a)=>`${a}Schema`).join(', ')}])`;
2264
+ }
2265
+ }
2266
+ async function requestHookTemplate({ source, data: { paths, operationId, response, requestUrl, variables, requestBody, contentType, responseType, errorResponses, method } }, ctx) {
2267
+ var _ctx_getCounter;
2268
+ const postfix = ctx.potentiallyConflictingDescriptors.includes(operationId) ? generatePostfix(contentType, responseType) : '';
2269
+ const ts = typescript(path.resolve('src', source, ...paths, camelCase(operationId), `use${pascalCase(operationId)}${postfix}.ts`));
2270
+ (_ctx_getCounter = ctx.getCounter(source)) == null ? void 0 : _ctx_getCounter.inc("Stateful Hooks");
2271
+ const modifiedRequestUrl = `${requestUrl == null ? void 0 : requestUrl.replace(/\{/g, "${")}`;
2272
+ const imports = new Set();
2273
+ imports.add(`import { z } from 'zod'`);
2274
+ imports.add(`import { useCallback, useEffect } from 'react'`);
2275
+ imports.add(`import {useNetworkState, NetworkState, DispatchState, error, successfulDispatch, validationError, encode, requestValidationError} from "@intrig/react"`);
2276
+ const { hookShape, optionsShape } = extractHookShapeAndOptionsShape$1(response, requestBody, imports);
2277
+ const { paramExpression, paramType } = extractParamDeconstruction$2(variables != null ? variables : [], requestBody);
2278
+ if (requestBody) {
2279
+ imports.add(`import { ${requestBody} as RequestBody, ${requestBody}Schema as requestBodySchema } from "@intrig/react/${source}/components/schemas/${requestBody}"`);
2280
+ }
2281
+ if (response) {
2282
+ imports.add(`import { ${response} as Response, ${response}Schema as schema } from "@intrig/react/${source}/components/schemas/${response}"`);
2283
+ }
2284
+ imports.add(`import {${pascalCase(operationId)}Params as Params} from './${pascalCase(operationId)}.params'`);
2285
+ const errorTypes = [
2286
+ ...new Set(Object.values(errorResponses != null ? errorResponses : {}).map((a)=>a.response))
2287
+ ];
2288
+ errorTypes.forEach((ref)=>imports.add(`import {${ref}, ${ref}Schema } from "@intrig/react/${source}/components/schemas/${ref}"`));
2289
+ var _variables_filter_map;
2290
+ const paramExplode = [
2291
+ ...(_variables_filter_map = variables == null ? void 0 : variables.filter((a)=>a.in === "path").map((a)=>a.name)) != null ? _variables_filter_map : [],
2292
+ "...params"
2293
+ ].join(",");
2294
+ const finalRequestBodyBlock = requestBody ? `,data: encode(data, "${contentType}", requestBodySchema)` : '';
2295
+ function responseTypePart() {
2296
+ switch(responseType){
2297
+ case "application/octet-stream":
2298
+ return `responseType: 'blob', adapter: 'fetch',`;
2299
+ case "text/event-stream":
2300
+ return `responseType: 'stream', adapter: 'fetch',`;
2301
+ }
2302
+ return '';
2303
+ }
2304
+ return ts`
2305
+ ${[
2306
+ ...imports
2307
+ ].join('\n')}
2308
+
2309
+ ${!response ? `
2310
+ type Response = any;
2311
+ const schema = z.any();
2312
+ ` : ''}
2313
+
2314
+ ${extractErrorParams$2(errorTypes.map((a)=>a))}
2315
+
2316
+ const operation = "${method.toUpperCase()} ${requestUrl}| ${contentType} -> ${responseType}"
2317
+ const source = "${source}"
2318
+
2319
+ function use${pascalCase(operationId)}Hook(options: ${optionsShape} = {}): [NetworkState<Response>, (${paramType}) => DispatchState<any>, () => void] {
2320
+ const [state, dispatch, clear, dispatchState] = useNetworkState<Response>({
2321
+ key: options?.key ?? 'default',
2322
+ operation,
2323
+ source,
2324
+ schema,
2325
+ errorSchema
2326
+ });
2327
+
2328
+ const doExecute = useCallback<(${paramType}) => DispatchState<any>>((${paramExpression}) => {
2329
+ const { ${paramExplode}} = p
2330
+
2331
+ ${requestBody ? `
2332
+ const validationResult = requestBodySchema.safeParse(data);
2333
+ if (!validationResult.success) {
2334
+ dispatchState(error(requestValidationError(validationResult.error)));
2335
+ return validationError(validationResult.error.errors);
2336
+ }
2337
+ ` : ``}
2338
+
2339
+ dispatch({
2340
+ method: '${method}',
2341
+ url: \`${modifiedRequestUrl}\`,
2342
+ headers: {
2343
+ ${contentType ? `"Content-Type": "${contentType}",` : ''}
2344
+ },
2345
+ params,
2346
+ key: \`${"${source}: ${operation}"}\`,
2347
+ source: '${source}'
2348
+ ${requestBody ? finalRequestBodyBlock : ''},
2349
+ ${responseTypePart()}
2350
+ })
2351
+ return successfulDispatch();
2352
+ }, [dispatch])
2353
+
2354
+ useEffect(() => {
2355
+ if (options.fetchOnMount) {
2356
+ doExecute(${[
2357
+ requestBody ? `options.body!` : undefined,
2358
+ "options.params!"
2359
+ ].filter((a)=>a).join(",")});
2360
+ }
2361
+
2362
+ return () => {
2363
+ if (options.clearOnUnmount) {
2364
+ clear();
2365
+ }
2366
+ }
2367
+ }, [])
2368
+
2369
+ return [
2370
+ state,
2371
+ doExecute,
2372
+ clear
2373
+ ]
2374
+ }
2375
+
2376
+ use${pascalCase(operationId)}Hook.key = \`${"${source}: ${operation}"}\`
2377
+
2378
+ export const use${pascalCase(operationId)}: ${hookShape} = use${pascalCase(operationId)}Hook;
2379
+ `;
2380
+ }
2381
+
2382
+ async function paramsTemplate({ source, data: { paths, operationId, variables } }, ctx) {
2383
+ const ts = typescript(path.resolve('src', source, ...paths, camelCase(operationId), `${pascalCase(operationId)}.params.ts`));
2384
+ const { variableImports, variableTypes } = decodeVariables(variables != null ? variables : [], source, "@intrig/react");
2385
+ if (variableTypes.length === 0) return ts`
2386
+ export type ${pascalCase(operationId)}Params = Record<string, any>
2387
+ `;
2388
+ return ts`
2389
+ ${variableImports}
2390
+
2391
+ export interface ${pascalCase(operationId)}Params extends Record<string, any> {
2392
+ ${variableTypes}
2393
+ }
2394
+ `;
2395
+ }
2396
+
2397
+ function extractAsyncHookShape(response, requestBody, imports) {
2398
+ if (response) {
2399
+ if (requestBody) {
2400
+ imports.add(`import { BinaryFunctionAsyncHook } from "@intrig/react"`);
2401
+ return `BinaryFunctionAsyncHook<Params, RequestBody, Response>`;
2402
+ } else {
2403
+ imports.add(`import { UnaryFunctionAsyncHook } from "@intrig/react"`);
2404
+ return `UnaryFunctionAsyncHook<Params, Response>`;
2405
+ }
2406
+ } else {
2407
+ if (requestBody) {
2408
+ imports.add(`import { BinaryProduceAsyncHook } from "@intrig/react"`);
2409
+ return `BinaryProduceAsyncHook<Params, RequestBody>`;
2410
+ } else {
2411
+ imports.add(`import { UnaryProduceAsyncHook } from "@intrig/react"`);
2412
+ return `UnaryProduceAsyncHook<Params>`;
2413
+ }
2414
+ }
2415
+ }
2416
+ function extractErrorParams$1(errorTypes) {
2417
+ switch(errorTypes.length){
2418
+ case 0:
2419
+ return `
2420
+ export type _ErrorType = any
2421
+ const errorSchema = z.any()`;
2422
+ case 1:
2423
+ return `
2424
+ export type _ErrorType = ${errorTypes[0]}
2425
+ const errorSchema = ${errorTypes[0]}Schema`;
2426
+ default:
2427
+ return `
2428
+ export type _ErrorType = ${errorTypes.join(' | ')}
2429
+ const errorSchema = z.union([${errorTypes.map((a)=>`${a}Schema`).join(', ')}])`;
2430
+ }
2431
+ }
2432
+ function extractParamDeconstruction$1(variables, requestBody) {
2433
+ const isParamMandatory = (variables == null ? void 0 : variables.some((a)=>a.in === 'path')) || false;
2434
+ if (requestBody) {
2435
+ if (isParamMandatory) {
2436
+ return {
2437
+ paramExpression: 'data, p',
2438
+ paramType: 'data: RequestBody, params: Params'
2439
+ };
2440
+ } else {
2441
+ return {
2442
+ paramExpression: 'data, p = {}',
2443
+ paramType: 'data: RequestBody, params?: Params'
2444
+ };
2445
+ }
2446
+ } else {
2447
+ if (isParamMandatory) {
2448
+ return {
2449
+ paramExpression: 'p',
2450
+ paramType: 'params: Params'
2451
+ };
2452
+ } else {
2453
+ return {
2454
+ paramExpression: 'p = {}',
2455
+ paramType: 'params?: Params'
2456
+ };
2457
+ }
2458
+ }
2459
+ }
2460
+ async function asyncFunctionHookTemplate({ source, data: { paths, operationId, response, requestUrl, variables, requestBody, contentType, responseType, errorResponses, method } }, ctx) {
2461
+ var _ctx_getCounter;
2462
+ const postfix = ctx.potentiallyConflictingDescriptors.includes(operationId) ? generatePostfix(contentType, responseType) : '';
2463
+ const ts = typescript(path.resolve('src', source, ...paths, camelCase(operationId), `use${pascalCase(operationId)}Async${postfix}.ts`));
2464
+ (_ctx_getCounter = ctx.getCounter(source)) == null ? void 0 : _ctx_getCounter.inc("Stateless Hooks");
2465
+ const modifiedRequestUrl = `${requestUrl == null ? void 0 : requestUrl.replace(/\{/g, "${")}`;
2466
+ const imports = new Set();
2467
+ // Basic imports
2468
+ imports.add(`import { z } from 'zod'`);
2469
+ imports.add(`import { useCallback } from 'react'`);
2470
+ imports.add(`import { useTransitionCall, encode, isError, isSuccess } from '@intrig/react'`);
2471
+ // Hook signature type
2472
+ const hookShape = extractAsyncHookShape(response, requestBody, imports);
2473
+ // Add body/response param imports
2474
+ if (requestBody) {
2475
+ imports.add(`import { ${requestBody} as RequestBody, ${requestBody}Schema as requestBodySchema } from "@intrig/react/${source}/components/schemas/${requestBody}"`);
2476
+ }
2477
+ if (response) {
2478
+ imports.add(`import { ${response} as Response, ${response}Schema as schema } from "@intrig/react/${source}/components/schemas/${response}"`);
2479
+ }
2480
+ imports.add(`import { ${pascalCase(operationId)}Params as Params } from './${pascalCase(operationId)}.params'`);
2481
+ // Error types
2482
+ const errorTypes = [
2483
+ ...new Set(Object.values(errorResponses != null ? errorResponses : {}).map((a)=>a.response))
2484
+ ];
2485
+ errorTypes.forEach((ref)=>imports.add(`import { ${ref}, ${ref}Schema } from "@intrig/react/${source}/components/schemas/${ref}"`));
2486
+ // Error schema block
2487
+ const errorSchemaBlock = extractErrorParams$1(errorTypes.map((a)=>a));
2488
+ // Param deconstruction
2489
+ const { paramExpression, paramType } = extractParamDeconstruction$1(variables != null ? variables : [], requestBody);
2490
+ var _variables_filter_map;
2491
+ const paramExplode = [
2492
+ ...(_variables_filter_map = variables == null ? void 0 : variables.filter((a)=>a.in === 'path').map((a)=>a.name)) != null ? _variables_filter_map : [],
2493
+ '...params'
2494
+ ].join(',');
2495
+ const finalRequestBodyBlock = requestBody ? `, data: encode(data, "${contentType}", requestBodySchema)` : '';
2496
+ function responseTypePart() {
2497
+ switch(responseType){
2498
+ case "application/octet-stream":
2499
+ return `responseType: 'blob', adapter: 'fetch',`;
2500
+ case "text/event-stream":
2501
+ return `responseType: 'stream', adapter: 'fetch',`;
2502
+ }
2503
+ return '';
2504
+ }
2505
+ return ts`
2506
+ ${[
2507
+ ...imports
2508
+ ].join('\n')}
2509
+
2510
+ ${!response ? `
2511
+ type Response = any;
2512
+ const schema = z.any();
2513
+ ` : ''}
2514
+
2515
+ ${errorSchemaBlock}
2516
+
2517
+ const operation = "${method.toUpperCase()} ${requestUrl}| ${contentType} -> ${responseType}";
2518
+ const source = "${source}";
2519
+
2520
+ function use${pascalCase(operationId)}AsyncHook(): [(${paramType}) => Promise<Response>, () => void] {
2521
+ const [call, abort] = useTransitionCall<Response>({
2522
+ schema,
2523
+ errorSchema
2524
+ });
2525
+
2526
+ const doExecute = useCallback<(${paramType}) => Promise<Response>>(async (${paramExpression}) => {
2527
+ const { ${paramExplode} } = p;
2528
+
2529
+ ${requestBody ? `
2530
+ const validationResult = requestBodySchema.safeParse(data);
2531
+ if (!validationResult.success) {
2532
+ return Promise.reject(validationResult.error);
2533
+ }
2534
+ ` : ''}
2535
+
2536
+ return await call({
2537
+ method: '${method}',
2538
+ url: \`${modifiedRequestUrl}\`,
2539
+ headers: {
2540
+ ${contentType ? `"Content-Type": "${contentType}",` : ''}
2541
+ },
2542
+ params,
2543
+ key: \`${"${source}: ${operation}"}\`,
2544
+ source: '${source}'
2545
+ ${requestBody ? finalRequestBodyBlock : ''},
2546
+ ${responseTypePart()}
2547
+ });
2548
+ }, [call]);
2549
+
2550
+ return [doExecute, abort];
2551
+ }
2552
+
2553
+ use${pascalCase(operationId)}AsyncHook.key = \`${"${source}: ${operation}"}\`;
2554
+
2555
+ export const use${pascalCase(operationId)}Async: ${hookShape} = use${pascalCase(operationId)}AsyncHook;
2556
+ `;
2557
+ }
2558
+
2559
+ async function clientIndexTemplate(descriptors, ctx) {
2560
+ var _ctx_getCounter;
2561
+ const { source, data: { paths, operationId, responseType, contentType } } = descriptors[0];
2562
+ (_ctx_getCounter = ctx.getCounter(source)) == null ? void 0 : _ctx_getCounter.inc("Endpoints");
2563
+ const ts = typescript(path.resolve('src', source, ...paths, camelCase(operationId), `client.ts`));
2564
+ const postfix = ctx.potentiallyConflictingDescriptors.includes(operationId) ? generatePostfix(contentType, responseType) : '';
2565
+ if (descriptors.length === 1) return ts`
2566
+ export { use${pascalCase(operationId)} } from './use${pascalCase(operationId)}${postfix}'
2567
+ export { use${pascalCase(operationId)}Async } from './use${pascalCase(operationId)}Async${postfix}'
2568
+ `;
2569
+ const exports = descriptors.map(({ data: { contentType, responseType } })=>{
2570
+ const postfix = ctx.potentiallyConflictingDescriptors.includes(operationId) ? generatePostfix(contentType, responseType) : '';
2571
+ return `
2572
+ export { use${pascalCase(operationId)} as use${pascalCase(operationId)}${postfix} } from './use${pascalCase(operationId)}${postfix}'
2573
+ export { use${pascalCase(operationId)}Async as use${pascalCase(operationId)}Async${postfix} } from './use${pascalCase(operationId)}Async${postfix}'
2574
+ `;
2575
+ }).join('\n');
2576
+ return ts`
2577
+ ${exports}
2578
+ `;
2579
+ }
2580
+
2581
+ function extractHookShapeAndOptionsShape(response, requestBody, imports) {
2582
+ if (response) {
2583
+ if (requestBody) {
2584
+ imports.add(`import { BinaryFunctionHook, BinaryHookOptions } from "@intrig/react"`);
2585
+ return {
2586
+ hookShape: `BinaryFunctionHook<Params, RequestBody, Response>`,
2587
+ optionsShape: `BinaryHookOptions<Params, RequestBody>`
2588
+ };
2589
+ } else {
2590
+ imports.add(`import { UnaryFunctionHook, UnaryHookOptions } from "@intrig/react"`);
2591
+ return {
2592
+ hookShape: `UnaryFunctionHook<Params, Response>`,
2593
+ optionsShape: `UnaryHookOptions<Params>`
2594
+ };
2595
+ }
2596
+ } else {
2597
+ if (requestBody) {
2598
+ imports.add(`import { BinaryProduceHook, BinaryHookOptions } from "@intrig/react"`);
2599
+ return {
2600
+ hookShape: `BinaryProduceHook<Params, RequestBody>`,
2601
+ optionsShape: `BinaryHookOptions<Params, RequestBody>`
2602
+ };
2603
+ } else {
2604
+ imports.add(`import { UnaryProduceHook, UnaryHookOptions } from "@intrig/react"`);
2605
+ return {
2606
+ hookShape: `UnaryProduceHook<Params>`,
2607
+ optionsShape: `UnaryHookOptions<Params>`
2608
+ };
2609
+ }
2610
+ }
2611
+ }
2612
+ function extractParamDeconstruction(variables, requestBody) {
2613
+ const isParamMandatory = (variables == null ? void 0 : variables.some((a)=>a.in === 'path')) || false;
2614
+ if (requestBody) {
2615
+ if (isParamMandatory) {
2616
+ return {
2617
+ paramExpression: 'data, p',
2618
+ paramType: 'data: RequestBody, params: Params'
2619
+ };
2620
+ } else {
2621
+ return {
2622
+ paramExpression: 'data, p = {}',
2623
+ paramType: 'data: RequestBody, params?: Params'
2624
+ };
2625
+ }
2626
+ } else {
2627
+ if (isParamMandatory) {
2628
+ return {
2629
+ paramExpression: 'p',
2630
+ paramType: 'params: Params'
2631
+ };
2632
+ } else {
2633
+ return {
2634
+ paramExpression: 'p = {}',
2635
+ paramType: 'params?: Params'
2636
+ };
2637
+ }
2638
+ }
2639
+ }
2640
+ function extractErrorParams(errorTypes) {
2641
+ switch(errorTypes.length){
2642
+ case 0:
2643
+ return `
2644
+ export type _ErrorType = any
2645
+ const errorSchema = z.any()`;
2646
+ case 1:
2647
+ return `
2648
+ export type _ErrorType = ${errorTypes[0]}
2649
+ const errorSchema = ${errorTypes[0]}Schema`;
2650
+ default:
2651
+ return `
2652
+ export type _ErrorType = ${errorTypes.join(' | ')}
2653
+ const errorSchema = z.union([${errorTypes.map((a)=>`${a}Schema`).join(', ')}])`;
2654
+ }
2655
+ }
2656
+ async function downloadHookTemplate({ source, data: { paths, operationId, response, requestUrl, variables, requestBody, contentType, responseType, errorResponses, method } }, ctx) {
2657
+ var _ctx_getCounter;
2658
+ const postfix = ctx.potentiallyConflictingDescriptors.includes(operationId) ? generatePostfix(contentType, responseType) : '';
2659
+ const ts = typescript(path.resolve('src', source, ...paths, camelCase(operationId), `use${pascalCase(operationId)}${postfix}Download.ts`));
2660
+ (_ctx_getCounter = ctx.getCounter(source)) == null ? void 0 : _ctx_getCounter.inc("Download Hooks");
2661
+ const modifiedRequestUrl = `${requestUrl == null ? void 0 : requestUrl.replace(/\{/g, "${")}`;
2662
+ const imports = new Set();
2663
+ imports.add(`import { z } from 'zod'`);
2664
+ imports.add(`import { useCallback, useEffect } from 'react'`);
2665
+ imports.add(`import {useNetworkState, NetworkState, DispatchState, pending, success, error, init, successfulDispatch, validationError, encode, isSuccess, requestValidationError} from "@intrig/react"`);
2666
+ const { hookShape, optionsShape } = extractHookShapeAndOptionsShape(response, requestBody, imports);
2667
+ const { paramExpression, paramType } = extractParamDeconstruction(variables, requestBody);
2668
+ if (requestBody) {
2669
+ imports.add(`import { ${requestBody} as RequestBody, ${requestBody}Schema as requestBodySchema } from "@intrig/react/${source}/components/schemas/${requestBody}"`);
2670
+ }
2671
+ if (response) {
2672
+ imports.add(`import { ${response} as Response, ${response}Schema as schema } from "@intrig/react/${source}/components/schemas/${response}"`);
2673
+ }
2674
+ imports.add(`import {${pascalCase(operationId)}Params as Params} from './${pascalCase(operationId)}.params'`);
2675
+ const errorTypes = [
2676
+ ...new Set(Object.values(errorResponses != null ? errorResponses : {}).map((a)=>a.response))
2677
+ ];
2678
+ errorTypes.forEach((ref)=>imports.add(`import {${ref}, ${ref}Schema } from "@intrig/react/${source}/components/schemas/${ref}"`));
2679
+ var _variables_filter_map;
2680
+ const paramExplode = [
2681
+ ...(_variables_filter_map = variables == null ? void 0 : variables.filter((a)=>a.in === "path").map((a)=>a.name)) != null ? _variables_filter_map : [],
2682
+ "...params"
2683
+ ].join(",");
2684
+ const finalRequestBodyBlock = requestBody ? `,data: encode(data, "${contentType}", requestBodySchema)` : '';
2685
+ function responseTypePart() {
2686
+ switch(responseType){
2687
+ case "application/octet-stream":
2688
+ return `responseType: 'blob', adapter: 'fetch',`;
2689
+ case "text/event-stream":
2690
+ return `responseType: 'stream', adapter: 'fetch',`;
2691
+ }
2692
+ return '';
2693
+ }
2694
+ return ts`
2695
+ ${[
2696
+ ...imports
2697
+ ].join('\n')}
2698
+
2699
+ ${!response ? `
2700
+ type Response = any;
2701
+ const schema = z.any();
2702
+ ` : ''}
2703
+
2704
+ ${extractErrorParams(errorTypes.map((a)=>a))}
2705
+
2706
+ const operation = "${method.toUpperCase()} ${requestUrl}| ${contentType} -> ${responseType}"
2707
+ const source = "${source}"
2708
+
2709
+ function use${pascalCase(operationId)}Hook(options: ${optionsShape} = {}): [NetworkState<Response>, (${paramType}) => DispatchState<any>, () => void] {
2710
+ let [state, dispatch, clear, dispatchState] = useNetworkState<Response>({
2711
+ key: options?.key ?? 'default',
2712
+ operation,
2713
+ source,
2714
+ schema,
2715
+ errorSchema
2716
+ });
2717
+
2718
+ useEffect(() => {
2719
+ if (isSuccess(state)) {
2720
+ let a = document.createElement('a');
2721
+ const ct =
2722
+ state.headers?.['content-type'] ?? 'application/octet-stream';
2723
+ let data: any = state.data;
2724
+ if (ct.startsWith('application/json')) {
2725
+ let data: any[];
2726
+ if (ct.startsWith('application/json')) {
2727
+ data = [JSON.stringify(state.data, null, 2)];
2728
+ } else {
2729
+ data = [state.data];
2730
+ }
2731
+ }
2732
+ a.href = URL.createObjectURL(new Blob(Array.isArray(data) ? data : [data], {type: ct}));
2733
+ const contentDisposition = state.headers?.['content-disposition'];
2734
+ let filename = '${pascalCase(operationId)}.${mimeType.extension(contentType)}';
2735
+ if (contentDisposition) {
2736
+ const rx = /filename\\*=(?:UTF-8'')?([^;\\r\\n]+)|filename="?([^";\\r\\n]+)"?/i;
2737
+ const m = contentDisposition.match(rx);
2738
+ if (m && m[1]) {
2739
+ filename = decodeURIComponent(m[1].replace(/\\+/g, ' '));
2740
+ } else if (m && m[2]) {
2741
+ filename = decodeURIComponent(m[2].replace(/\\+/g, ' '));
2742
+ }
2743
+ }
2744
+ a.download = filename;
2745
+ document.body.appendChild(a);
2746
+ a.click();
2747
+ document.body.removeChild(a);
2748
+ dispatchState(init())
2749
+ }
2750
+ }, [state])
2751
+
2752
+ let doExecute = useCallback<(${paramType}) => DispatchState<any>>((${paramExpression}) => {
2753
+ let { ${paramExplode}} = p
2754
+
2755
+ ${requestBody ? `
2756
+ const validationResult = requestBodySchema.safeParse(data);
2757
+ if (!validationResult.success) {
2758
+ dispatchState(error(requestValidationError(validationResult.error)));
2759
+ return validationError(validationResult.error.errors);
2760
+ }
2761
+ ` : ``}
2762
+
2763
+ dispatch({
2764
+ method: '${method}',
2765
+ url: \`${modifiedRequestUrl}\`,
2766
+ headers: {
2767
+ ${contentType ? `"Content-Type": "${contentType}",` : ''}
2768
+ },
2769
+ params,
2770
+ key: \`${"${source}: ${operation}"}\`,
2771
+ source: '${source}'
2772
+ ${requestBody ? finalRequestBodyBlock : ''},
2773
+ ${responseTypePart()}
2774
+ })
2775
+ return successfulDispatch();
2776
+ }, [dispatch])
2777
+
2778
+ useEffect(() => {
2779
+ if (options.fetchOnMount) {
2780
+ doExecute(${[
2781
+ requestBody ? `options.body!` : undefined,
2782
+ "options.params!"
2783
+ ].filter((a)=>a).join(",")});
2784
+ }
2785
+
2786
+ return () => {
2787
+ if (options.clearOnUnmount) {
2788
+ clear();
2789
+ }
2790
+ }
2791
+ }, [])
2792
+
2793
+ return [
2794
+ state,
2795
+ doExecute,
2796
+ clear
2797
+ ]
2798
+ }
2799
+
2800
+ use${pascalCase(operationId)}Hook.key = \`${"${source}: ${operation}"}\`
2801
+
2802
+ export const use${pascalCase(operationId)}Download: ${hookShape} = use${pascalCase(operationId)}Hook;
2803
+ `;
2804
+ }
2805
+
2806
+ async function typeTemplate(descriptor) {
2807
+ const { data: { schema, name: typeName }, source } = descriptor;
2808
+ const { imports, zodSchema, tsType } = openApiSchemaToZod(schema);
2809
+ const ts = typescript(path.resolve('src', source, 'components', 'schemas', `${typeName}.ts`));
2810
+ const simpleType = (await jsonLiteral('')`${JSON.stringify(schema)}`).content;
2811
+ const transport = schema.type === 'string' && schema.format === 'binary' ? 'binary' : 'json';
2812
+ var _JSON_stringify;
2813
+ return ts`
2814
+ import { z } from 'zod'
2815
+
2816
+ ${[
2817
+ ...imports
2818
+ ].join('\n')}
2819
+
2820
+ //--- Zod Schemas ---//
2821
+
2822
+ export const ${typeName}Schema = ${zodSchema}
2823
+
2824
+ //--- Typescript Type ---//
2825
+
2826
+ export type ${typeName} = ${tsType}
2827
+
2828
+ //--- JSON Schema ---//
2829
+
2830
+ export const ${typeName}_jsonschema = ${(_JSON_stringify = JSON.stringify(schema)) != null ? _JSON_stringify : "{}"}
2831
+
2832
+ //--- Simple Type ---//
2833
+ /*[${simpleType}]*/
2834
+
2835
+ // Transport hint for clients ("binary" => use arraybuffer/blob)
2836
+ export const ${typeName}_transport = '${transport}' as const;
2837
+ `;
2838
+ }
2839
+ function isRef(schema) {
2840
+ return '$ref' in (schema != null ? schema : {});
2841
+ }
2842
+ // Helper function to convert OpenAPI schema types to TypeScript types and Zod schemas
2843
+ function openApiSchemaToZod(schema, imports = new Set()) {
2844
+ if (!schema) {
2845
+ return {
2846
+ tsType: 'any',
2847
+ zodSchema: 'z.any()',
2848
+ imports: new Set()
2849
+ };
2850
+ }
2851
+ if (isRef(schema)) {
2852
+ return handleRefSchema(schema.$ref, imports);
2853
+ }
2854
+ if (!schema.type) {
2855
+ if ('properties' in schema) {
2856
+ schema.type = 'object';
2857
+ } else if ('items' in schema) {
2858
+ schema.type = 'array';
2859
+ }
2860
+ }
2861
+ switch(schema.type){
2862
+ case 'string':
2863
+ return handleStringSchema(schema);
2864
+ case 'number':
2865
+ return handleNumberSchema(schema);
2866
+ case 'integer':
2867
+ return handleIntegerSchema(schema);
2868
+ case 'boolean':
2869
+ return handleBooleanSchema();
2870
+ case 'array':
2871
+ return handleArraySchema(schema, imports);
2872
+ case 'object':
2873
+ return handleObjectSchema(schema, imports);
2874
+ default:
2875
+ return handleComplexSchema(schema, imports);
2876
+ }
2877
+ }
2878
+ function handleRefSchema(ref, imports) {
2879
+ const refParts = ref.split('/');
2880
+ const refName = refParts[refParts.length - 1];
2881
+ imports.add(`import { ${refName}, ${refName}Schema } from './${refName}';`);
2882
+ return {
2883
+ tsType: refName,
2884
+ zodSchema: `z.lazy(() => ${refName}Schema)`,
2885
+ imports
2886
+ };
2887
+ }
2888
+ function handleStringSchema(schema) {
2889
+ const imports = new Set();
2890
+ let binaryish = false;
2891
+ if (schema.enum) {
2892
+ const enumValues = schema.enum.map((value)=>`'${value}'`).join(' | ');
2893
+ const zodEnum = `z.enum([${schema.enum.map((value)=>`'${value}'`).join(', ')}])`;
2894
+ return {
2895
+ tsType: enumValues,
2896
+ zodSchema: zodEnum,
2897
+ imports: new Set()
2898
+ };
2899
+ }
2900
+ let zodSchema = 'z.string()';
2901
+ let tsType = 'string';
2902
+ if (schema.format === 'date' && !schema.pattern) {
2903
+ tsType = 'Date';
2904
+ zodSchema = 'z.coerce.date()';
2905
+ zodSchema += `.transform((val) => {
2906
+ const parsedDate = new Date(val);
2907
+ if (isNaN(parsedDate.getTime())) {
2908
+ throw new Error('Invalid date format');
2909
+ }
2910
+ return parsedDate;
2911
+ })`;
2912
+ } else if (schema.format === 'time') {
2913
+ zodSchema = 'z.string()';
2914
+ if (schema.pattern) {
2915
+ zodSchema += `.regex(new RegExp('${schema.pattern}'))`;
2916
+ }
2917
+ } else if (schema.format === 'date-time' && !schema.pattern) {
2918
+ tsType = 'Date';
2919
+ zodSchema = 'z.coerce.date()';
2920
+ zodSchema += `.transform((val) => {
2921
+ const parsedDateTime = new Date(val);
2922
+ if (isNaN(parsedDateTime.getTime())) {
2923
+ throw new Error('Invalid date-time format');
2924
+ }
2925
+ return parsedDateTime;
2926
+ })`;
2927
+ } else if (schema.format === 'binary') {
2928
+ tsType = 'BinaryData';
2929
+ zodSchema = 'BinaryDataSchema';
2930
+ imports.add(`import { BinaryData, BinaryDataSchema } from '@intrig/react/type-utils'`);
2931
+ binaryish = true;
2932
+ } else if (schema.format === 'byte') {
2933
+ tsType = 'Uint8Array';
2934
+ zodSchema = 'z.string().transform((val) => base64ToUint8Array(val))';
2935
+ imports.add(`import { base64ToUint8Array } from '@intrig/react/type-utils'`);
2936
+ binaryish = true;
2937
+ } else if (schema.format === 'email') {
2938
+ zodSchema = 'z.string().email()';
2939
+ } else if (schema.format === 'uuid') {
2940
+ zodSchema = 'z.string().uuid()';
2941
+ } else if (schema.format === 'uri') {
2942
+ zodSchema = 'z.string().url()';
2943
+ } else if (schema.format === 'hostname') {
2944
+ zodSchema = 'z.string()'; // Zod does not have a direct hostname validator
2945
+ } else if (schema.format === 'ipv4') {
2946
+ zodSchema = 'z.string().regex(/^((25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)$/)';
2947
+ } else if (schema.format === 'ipv6') {
2948
+ zodSchema = 'z.string().regex(/^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6}|:)|::(ffff(:0{1,4}){0,1}:)?((25[0-5]|(2[0-4]|1[0-9]|[0-9])\\.){3}(25[0-5]|(2[0-4]|1[0-9]|[0-9]))))$/)';
2949
+ } else {
2950
+ if (schema.minLength !== undefined) zodSchema += `.min(${schema.minLength})`;
2951
+ if (schema.maxLength !== undefined) zodSchema += `.max(${schema.maxLength})`;
2952
+ if (schema.pattern !== undefined) zodSchema += `.regex(new RegExp('${schema.pattern}'))`;
2953
+ }
2954
+ return {
2955
+ tsType,
2956
+ zodSchema,
2957
+ imports,
2958
+ binaryish
2959
+ };
2960
+ }
2961
+ function handleNumberSchema(schema) {
2962
+ let zodSchema = 'z.number()';
2963
+ if (schema.minimum !== undefined) zodSchema += `.min(${schema.minimum})`;
2964
+ if (schema.maximum !== undefined) zodSchema += `.max(${schema.maximum})`;
2965
+ return {
2966
+ tsType: 'number',
2967
+ zodSchema,
2968
+ imports: new Set()
2969
+ };
2970
+ }
2971
+ function handleIntegerSchema(schema) {
2972
+ let zodSchema = 'z.number().int()';
2973
+ if (schema.minimum !== undefined) zodSchema += `.min(${schema.minimum})`;
2974
+ if (schema.maximum !== undefined) zodSchema += `.max(${schema.maximum})`;
2975
+ return {
2976
+ tsType: 'number',
2977
+ zodSchema,
2978
+ imports: new Set()
2979
+ };
2980
+ }
2981
+ function handleBooleanSchema() {
2982
+ const zodSchema = 'z.boolean()';
2983
+ return {
2984
+ tsType: 'boolean',
2985
+ zodSchema,
2986
+ imports: new Set()
2987
+ };
2988
+ }
2989
+ function handleArraySchema(schema, imports) {
2990
+ if (!schema.items) {
2991
+ throw new Error('Array schema must have an items property');
2992
+ }
2993
+ const { tsType, zodSchema: itemZodSchema, imports: itemImports, binaryish } = openApiSchemaToZod(schema.items, imports);
2994
+ let zodSchema = binaryish ? `z.array(${itemZodSchema})` : `(z.preprocess((raw) => (Array.isArray(raw) ? raw : [raw]), z.array(${itemZodSchema})) as z.ZodType<${tsType}[], z.ZodTypeDef, ${tsType}[]>)`;
2995
+ if (schema.minItems !== undefined) zodSchema += `.min(${schema.minItems})`;
2996
+ if (schema.maxItems !== undefined) zodSchema += `.max(${schema.maxItems})`;
2997
+ return {
2998
+ tsType: `(${tsType})[]`,
2999
+ zodSchema,
3000
+ imports: new Set([
3001
+ ...imports,
3002
+ ...itemImports
3003
+ ])
3004
+ };
3005
+ }
3006
+ function handleObjectSchema(schema, imports) {
3007
+ const updatedRequiredFields = schema.required || [];
3008
+ if (schema.properties) {
3009
+ const propertiesTs = Object.entries(schema.properties).map(([key, value])=>{
3010
+ const { tsType, optional } = openApiSchemaToZod(value);
3011
+ const isRequired = !optional && updatedRequiredFields.includes(key);
3012
+ return `${key}${isRequired ? '' : '?'}: ${tsType} ${isRequired ? '' : ' | null'};`;
3013
+ });
3014
+ const propertiesZod = Object.entries(schema.properties).map(([key, value])=>{
3015
+ const { zodSchema, imports: propImports } = openApiSchemaToZod(value);
3016
+ imports = new Set([
3017
+ ...imports,
3018
+ ...propImports
3019
+ ]);
3020
+ const isRequired = updatedRequiredFields.includes(key);
3021
+ return `${key}: ${isRequired ? zodSchema : zodSchema.includes('.optional().nullable()') ? zodSchema : zodSchema + '.optional().nullable()'}`;
3022
+ });
3023
+ return {
3024
+ tsType: `{ ${propertiesTs.join(' ')} }`,
3025
+ zodSchema: `z.object({ ${propertiesZod.join(', ')} })`,
3026
+ imports
3027
+ };
3028
+ }
3029
+ return {
3030
+ tsType: 'any',
3031
+ zodSchema: 'z.any()',
3032
+ imports: new Set(),
3033
+ optional: true
3034
+ };
3035
+ }
3036
+ function handleComplexSchema(schema, imports) {
3037
+ if (schema.oneOf) {
3038
+ const options = schema.oneOf.map((subSchema)=>openApiSchemaToZod(subSchema));
3039
+ const zodSchemas = options.map((option)=>option.zodSchema);
3040
+ const tsTypes = options.map((option)=>option.tsType);
3041
+ return {
3042
+ tsType: tsTypes.join(' | '),
3043
+ zodSchema: `z.union([${zodSchemas.join(', ')}])`,
3044
+ imports: new Set([
3045
+ ...imports,
3046
+ ...options.flatMap((option)=>Array.from(option.imports))
3047
+ ])
3048
+ };
3049
+ }
3050
+ if (schema.anyOf) {
3051
+ const options = schema.anyOf.map((subSchema)=>openApiSchemaToZod(subSchema));
3052
+ const zodSchemas = options.map((option)=>option.zodSchema);
3053
+ const tsTypes = options.map((option)=>option.tsType);
3054
+ return {
3055
+ tsType: tsTypes.join(' | '),
3056
+ zodSchema: `z.union([${zodSchemas.join(', ')}])`,
3057
+ imports: new Set([
3058
+ ...imports,
3059
+ ...options.flatMap((option)=>Array.from(option.imports))
3060
+ ])
3061
+ };
3062
+ }
3063
+ if (schema.allOf) {
3064
+ const options = schema.allOf.map((subSchema)=>openApiSchemaToZod(subSchema));
3065
+ const zodSchemas = options.map((option)=>option.zodSchema);
3066
+ const tsTypes = options.map((option)=>option.tsType);
3067
+ if (zodSchemas.length === 1) return {
3068
+ tsType: tsTypes.join(' & '),
3069
+ zodSchema: zodSchemas[0],
3070
+ imports: new Set([
3071
+ ...imports,
3072
+ ...options.flatMap((option)=>Array.from(option.imports))
3073
+ ])
3074
+ };
3075
+ return {
3076
+ tsType: tsTypes.join(' & '),
3077
+ zodSchema: `z.intersection(${zodSchemas.join(', ')})`,
3078
+ imports: new Set([
3079
+ ...imports,
3080
+ ...options.flatMap((option)=>Array.from(option.imports))
3081
+ ])
3082
+ };
3083
+ }
3084
+ return {
3085
+ tsType: 'any',
3086
+ zodSchema: 'z.any()',
3087
+ imports
3088
+ };
3089
+ }
3090
+
3091
+ async function generateCode(ctx) {
3092
+ // Root/project files
3093
+ await ctx.dump(packageJsonTemplate());
3094
+ await ctx.dump(reactTsConfigTemplate());
3095
+ await ctx.dump(reactSwcrcTemplate());
3096
+ // Top-level src files
3097
+ await ctx.dump(indexTemplate());
3098
+ await ctx.dump(networkStateTemplate());
3099
+ await ctx.dump(contextTemplate(ctx.sources));
3100
+ await ctx.dump(reactLoggerTemplate());
3101
+ await ctx.dump(reactExtraTemplate());
3102
+ await ctx.dump(reactMediaTypeUtilsTemplate());
3103
+ await ctx.dump(typeUtilsTemplate());
3104
+ await ctx.dump(intrigMiddlewareTemplate());
3105
+ // Provider modular files (placed under src)
3106
+ await ctx.dump(providerMainTemplate(ctx.sources));
3107
+ await ctx.dump(providerHooksTemplate());
3108
+ await ctx.dump(providerInterfacesTemplate(ctx.sources));
3109
+ await ctx.dump(providerReducerTemplate());
3110
+ await ctx.dump(providerAxiosConfigTemplate(ctx.sources));
3111
+ await ctx.dump(reactIntrigProviderTemplate(ctx.sources));
3112
+ await ctx.dump(reactIntrigProviderStubTemplate(ctx.sources));
3113
+ await ctx.dump(reactStatusTrapTemplate(ctx.sources));
3114
+ const potentiallyConflictingDescriptors = ctx.restDescriptors.sort((a, b)=>(a.data.contentType === "application/json" ? -1 : 0) - (b.data.contentType === "application/json" ? -1 : 0)).filter((descriptor, index, array)=>array.findIndex((other)=>other.data.operationId === descriptor.data.operationId) !== index).map((descriptor)=>descriptor.id);
3115
+ const internalGeneratorContext = new InternalGeneratorContext(potentiallyConflictingDescriptors);
3116
+ for (const restDescriptor of ctx.restDescriptors){
3117
+ await ctx.dump(requestHookTemplate(restDescriptor, internalGeneratorContext));
3118
+ await ctx.dump(paramsTemplate(restDescriptor));
3119
+ await ctx.dump(asyncFunctionHookTemplate(restDescriptor, internalGeneratorContext));
3120
+ if (restDescriptor.data.isDownloadable) {
3121
+ await ctx.dump(downloadHookTemplate(restDescriptor, internalGeneratorContext));
3122
+ }
3123
+ await ctx.dump(clientIndexTemplate([
3124
+ restDescriptor
3125
+ ], internalGeneratorContext));
3126
+ }
3127
+ for (const schemaDescriptor of ctx.schemaDescriptors){
3128
+ ctx.dump(typeTemplate(schemaDescriptor));
3129
+ }
3130
+ return internalGeneratorContext.getCounters();
3131
+ }
3132
+
3133
+ /**
3134
+ * Schema documentation tab builders for React binding.
3135
+ * We keep a small, composable API similar to other docs templates.
3136
+ */ async function schemaTypescriptDoc(result) {
3137
+ const md = mdLiteral('schema-typescript.md');
3138
+ const name = result.data.name;
3139
+ const source = result.source;
3140
+ const { tsType } = openApiSchemaToZod(result.data.schema);
3141
+ const ts = typescript(path__default.resolve('src', source, 'temp', name, `${name}.ts`));
3142
+ const importContent = await ts`
3143
+ import type { ${name} } from '@intrig/react/${source}/components/schemas/${name}';
3144
+ `;
3145
+ const codeContent = await ts`
3146
+ export type ${name} = ${tsType};
3147
+ `;
3148
+ return md`
3149
+ # Typescript Type
3150
+ Use this TypeScript type anywhere you need static typing for this object shape in your app code: component props, function params/returns, reducers, and local state in .ts/.tsx files.
3151
+
3152
+ ## Import
3153
+ ${'```ts'}
3154
+ ${importContent}
3155
+ ${'```'}
3156
+
3157
+ ## Definition
3158
+ ${'```ts'}
3159
+ ${codeContent}
3160
+ ${'```'}
3161
+ `;
3162
+ }
3163
+ async function schemaJsonSchemaDoc(result) {
3164
+ const md = mdLiteral('schema-json.md');
3165
+ const name = result.data.name;
3166
+ const source = result.source;
3167
+ const ts = typescript(path__default.resolve('src', source, 'temp', name, `${name}.ts`));
3168
+ const importContent = await ts`
3169
+ import { ${name}_jsonschema } from '@intrig/react/${source}/components/schemas/${name}';
3170
+ `;
3171
+ var _JSON_stringify;
3172
+ const codeContent = await ts`
3173
+ export const ${name}_jsonschema = ${(_JSON_stringify = JSON.stringify(result.data.schema, null, 2)) != null ? _JSON_stringify : "{}"};
3174
+ `;
3175
+ return md`
3176
+ # JSON Schema
3177
+ Use this JSON Schema with tools that consume JSON Schema: UI form builders (e.g. react-jsonschema-form), validators (AJV, validators in backends), and generators.
3178
+
3179
+ ## Import
3180
+ ${'```ts'}
3181
+ ${importContent}
3182
+ ${'```'}
3183
+
3184
+ ## Definition
3185
+ ${'```ts'}
3186
+ ${codeContent}
3187
+ ${'```'}
3188
+ `;
3189
+ }
3190
+ async function schemaZodSchemaDoc(result) {
3191
+ const md = mdLiteral('schema-zod.md');
3192
+ const name = result.data.name;
3193
+ const source = result.source;
3194
+ const { zodSchema } = openApiSchemaToZod(result.data.schema);
3195
+ const ts = typescript(path__default.resolve('src', source, 'temp', name, `${name}.ts`));
3196
+ const importContent = await ts`
3197
+ import { ${name}Schema } from '@intrig/react/${source}/components/schemas/${name}';
3198
+ `;
3199
+ const codeContent = await ts`
3200
+ export const ${name}Schema = ${zodSchema};
3201
+ `;
3202
+ return md`
3203
+ # Zod Schema
3204
+ Use this Zod schema for runtime validation and parsing: form validation, client/server payload guards, and safe transformations before using or storing data.
3205
+
3206
+ ## Import
3207
+ ${'```ts'}
3208
+ ${importContent}
3209
+ ${'```'}
3210
+
3211
+ ## Definition
3212
+ ${'```ts'}
3213
+ ${codeContent}
3214
+ ${'```'}
3215
+ `;
3216
+ }
3217
+
3218
+ async function getSchemaDocumentation(result) {
3219
+ const tabs = [];
3220
+ tabs.push({
3221
+ name: 'Typescript Type',
3222
+ content: (await schemaTypescriptDoc(result)).content
3223
+ });
3224
+ tabs.push({
3225
+ name: 'JSON Schema',
3226
+ content: (await schemaJsonSchemaDoc(result)).content
3227
+ });
3228
+ tabs.push({
3229
+ name: 'Zod Schema',
3230
+ content: (await schemaZodSchemaDoc(result)).content
3231
+ });
3232
+ return tabs;
3233
+ }
3234
+
3235
+ function reactSseHookDocs(descriptor) {
3236
+ const md = mdLiteral("sse-hook.md");
3237
+ var _descriptor_data_variables;
3238
+ // ===== Derived names =====
3239
+ const hasPathParams = ((_descriptor_data_variables = descriptor.data.variables) != null ? _descriptor_data_variables : []).some((v)=>{
3240
+ var _v_in;
3241
+ return ((_v_in = v.in) == null ? void 0 : _v_in.toUpperCase()) === "PATH";
3242
+ });
3243
+ const actionName = camelCase(descriptor.name); // e.g. streamBuildLogs
3244
+ const respVar = `${actionName}Resp`; // e.g. streamBuildLogsResp
3245
+ const clearName = `clear${pascalCase(descriptor.name)}`; // e.g. clearStreamBuildLogs
3246
+ const requestBodyVar = descriptor.data.requestBody ? camelCase(descriptor.data.requestBody) : undefined;
3247
+ const requestBodyType = descriptor.data.requestBody ? pascalCase(descriptor.data.requestBody) : undefined;
3248
+ const paramsVar = hasPathParams ? `${actionName}Params` : undefined; // e.g. streamBuildLogsParams
3249
+ const paramsType = hasPathParams ? `${pascalCase(descriptor.name)}Params` : undefined; // e.g. StreamBuildLogsParams
3250
+ const responseTypeName = `${pascalCase(descriptor.name)}ResponseBody`; // if generated by your build
3251
+ const callArgs = [
3252
+ requestBodyVar,
3253
+ paramsVar != null ? paramsVar : "{}"
3254
+ ].filter(Boolean).join(", ");
3255
+ return md`
3256
+ # Intrig SSE Hooks — Quick Guide
3257
+
3258
+ ## When should I use the SSE hook?
3259
+ - **Your endpoint streams events** (Server-Sent Events) and you want **incremental updates** in the UI → use this **SSE hook**.
3260
+ - **You only need a final result** → use the regular **stateful hook**.
3261
+ - **One-off validate/submit/update** with no shared state → use the **async hook**.
3262
+
3263
+ > Intrig SSE hooks are **stateful hooks** under the hood. **Events arrive while the hook is in \`Pending\`**. When the stream completes, the hook transitions to **\`Success\`** (or **\`Error\`**).
3264
+
3265
+ ---
3266
+
3267
+ ## Copy-paste starter (fast lane)
3268
+
3269
+ ### 1) Hook import
3270
+ \`\`\`ts
3271
+ import { use${pascalCase(descriptor.name)} } from '@intrig/react/${descriptor.path}/client';
3272
+ \`\`\`
3273
+
3274
+ ### 2) Utility guards
3275
+ \`\`\`ts
3276
+ import { isPending, isSuccess, isError } from '@intrig/react';
3277
+ \`\`\`
3278
+
3279
+ ### 3) Hook instance (auto-clear on unmount)
3280
+ \`\`\`ts
3281
+ const [${respVar}, ${actionName}] = use${pascalCase(descriptor.name)}({ clearOnUnmount: true });
3282
+ \`\`\`
3283
+
3284
+ ---
3285
+
3286
+ ## TL;DR (copy–paste)
3287
+
3288
+ \`\`\`tsx
3289
+ import { use${pascalCase(descriptor.name)} } from '@intrig/react/${descriptor.path}/client';
3290
+ import { isPending, isSuccess, isError } from '@intrig/react';
3291
+ import { useEffect, useState } from 'react';
3292
+
3293
+ export default function Example() {
3294
+ const [${respVar}, ${actionName}] = use${pascalCase(descriptor.name)}({ clearOnUnmount: true });
3295
+ const [messages, setMessages] = useState<any[]>([]);
3296
+
3297
+ useEffect(() => {
3298
+ ${actionName}(${callArgs}); // start stream
3299
+ }, [${actionName}]);
3300
+
3301
+ useEffect(() => {
3302
+ // SSE delivers messages while state is Pending
3303
+ if (isPending(${respVar})) {
3304
+ setMessages((prev) => [...prev, ${respVar}.data]);
3305
+ }
3306
+ }, [${respVar}]);
3307
+
3308
+ if (isError(${respVar})) return <>An error occurred</>;
3309
+ if (isPending(${respVar})) return <pre>{JSON.stringify(messages, null, 2)}</pre>;
3310
+ if (isSuccess(${respVar})) return <>Completed</>;
3311
+
3312
+ return null;
3313
+ }
3314
+ \`\`\`
3315
+
3316
+ ${requestBodyType || paramsType ? `### Optional types (if generated by your build)
3317
+ \`\`\`ts
3318
+ ${requestBodyType ? `import type { ${requestBodyType} } from '@intrig/react/${descriptor.source}/components/schemas/${requestBodyType}';\n` : ''}${paramsType ? `import type { ${paramsType} } from '@intrig/react/${descriptor.path}/${pascalCase(descriptor.name)}.params';\n` : ''}import type { ${responseTypeName} } from '@intrig/react/${descriptor.path}/${pascalCase(descriptor.name)}.response';
3319
+ \`\`\`
3320
+ ` : ''}
3321
+
3322
+ ---
3323
+
3324
+ ## Hook API
3325
+
3326
+ \`\`\`ts
3327
+ // Signature (shape shown; concrete generics vary per generated hook)
3328
+ declare function use${pascalCase(descriptor.name)}(options?: {
3329
+ fetchOnMount?: boolean;
3330
+ clearOnUnmount?: boolean; // recommended for streams
3331
+ key?: string; // isolate multiple subscriptions
3332
+ params?: ${paramsType != null ? paramsType : 'unknown'};
3333
+ body?: ${requestBodyType != null ? requestBodyType : 'unknown'};
3334
+ }): [
3335
+ // While streaming: isPending(state) === true and state.data is the latest event
3336
+ NetworkState<${responseTypeName} /* or event payload type */, any>,
3337
+ // Start streaming:
3338
+ (req: { params?: ${paramsType != null ? paramsType : 'unknown'}; body?: ${requestBodyType != null ? requestBodyType : 'unknown'} }) => void,
3339
+ // Clear/close stream:
3340
+ () => void
3341
+ ];
3342
+ \`\`\`
3343
+
3344
+ > **Important:** For SSE, **each incoming event** is surfaced as \`${respVar}.data\` **only while** \`isPending(${respVar})\` is true. On stream completion the hook flips to \`isSuccess\`.
3345
+
3346
+ ---
3347
+
3348
+ ## Usage patterns
3349
+
3350
+ ### 1) Lifecycle-bound stream (start on mount, auto-clear)
3351
+ \`\`\`tsx
3352
+ const [${respVar}, ${actionName}] = use${pascalCase(descriptor.name)}({ clearOnUnmount: true });
3353
+
3354
+ useEffect(() => {
3355
+ ${actionName}(${callArgs});
3356
+ }, [${actionName}]);
3357
+ \`\`\`
3358
+ <details><summary>Description</summary>
3359
+ Starts the stream when the component mounts and closes it when the component unmounts.
3360
+ </details>
3361
+
3362
+ ### 2) Collect messages into an array (simple collector)
3363
+ \`\`\`tsx
3364
+ const [messages, setMessages] = useState<any[]>([]);
3365
+
3366
+ useEffect(() => {
3367
+ if (isPending(${respVar})) setMessages((m) => [...m, ${respVar}.data]);
3368
+ }, [${respVar}]);
3369
+ \`\`\`
3370
+ <details><summary>Description</summary>
3371
+ Appends each event to an in-memory array. Good for logs and chat-like feeds; consider capping length to avoid memory growth.
3372
+ </details>
3373
+
3374
+ ### 3) Keep only the latest event (cheap UI)
3375
+ \`\`\`tsx
3376
+ const latest = isPending(${respVar}) ? ${respVar}.data : undefined;
3377
+ \`\`\`
3378
+ <details><summary>Description</summary>
3379
+ When you only need the most recent message (progress percentage, status line).
3380
+ </details>
3381
+
3382
+ ### 4) Controlled start/stop (user-triggered)
3383
+ \`\`\`tsx
3384
+ const [${respVar}, ${actionName}, ${clearName}] = use${pascalCase(descriptor.name)}();
3385
+
3386
+ const start = () => ${actionName}(${callArgs});
3387
+ const stop = () => ${clearName}();
3388
+ \`\`\`
3389
+ <details><summary>Description</summary>
3390
+ Expose play/pause UI for long streams or admin tools.
3391
+ </details>
3392
+
3393
+ ---
3394
+
3395
+ ## Full example (with flushSync option)
3396
+
3397
+ \`\`\`tsx
3398
+ import { use${pascalCase(descriptor.name)} } from '@intrig/react/${descriptor.path}/client';
3399
+ import { isPending, isSuccess, isError } from '@intrig/react';
3400
+ import { useEffect, useState } from 'react';
3401
+ import { flushSync } from 'react-dom';
3402
+
3403
+ function MyComponent() {
3404
+ const [${respVar}, ${actionName}] = use${pascalCase(descriptor.name)}({ clearOnUnmount: true });
3405
+ const [events, setEvents] = useState<any[]>([]);
3406
+
3407
+ useEffect(() => {
3408
+ ${actionName}(${callArgs});
3409
+ }, [${actionName}]);
3410
+
3411
+ useEffect(() => {
3412
+ if (isPending(${respVar})) {
3413
+ // Use flushSync only if you must render every single event (high-frequency streams).
3414
+ flushSync(() => setEvents((xs) => [...xs, ${respVar}.data]));
3415
+ }
3416
+ }, [${respVar}]);
3417
+
3418
+ if (isError(${respVar})) return <>Stream error</>;
3419
+ return (
3420
+ <>
3421
+ {isPending(${respVar}) && <pre>{JSON.stringify(events, null, 2)}</pre>}
3422
+ {isSuccess(${respVar}) && <>Completed ({events.length} events)</>}
3423
+ </>
3424
+ );
3425
+ }
3426
+ \`\`\`
3427
+
3428
+ ---
3429
+
3430
+ ## Tips, anti-patterns & gotchas
3431
+
3432
+ - **Prefer \`clearOnUnmount: true\`** so the EventSource/web request is closed when the component disappears.
3433
+ - **Don’t store unbounded arrays** for infinite streams—cap the length or batch to IndexedDB.
3434
+ - **Avoid unnecessary \`flushSync\`**; it’s expensive. Use it only when you truly must render every event.
3435
+ - **Multiple streams:** supply a unique \`key\` to isolate independent subscriptions.
3436
+ - **Server requirements:** SSE endpoints should send \`Content-Type: text/event-stream\`, disable buffering, and flush regularly; add relevant CORS headers if needed.
3437
+ - **Completion:** UI can switch from progress view (\`isPending\`) to final view (\`isSuccess\`) automatically.
3438
+
3439
+ ---
3440
+
3441
+ ## Troubleshooting
3442
+
3443
+ - **No intermediate messages:** ensure the server is truly streaming SSE (correct content type + flush) and that proxies/CDNs aren’t buffering responses.
3444
+ - **UI not updating for each event:** remove expensive work from the event effect, consider throttling; only use \`flushSync\` if absolutely necessary.
3445
+ - **Stream never completes:** check server end conditions and that you call \`${clearName}\` when appropriate.
3446
+
3447
+ `;
3448
+ }
3449
+
3450
+ function reactHookDocs(descriptor) {
3451
+ const md = mdLiteral('react-hook.md');
3452
+ var _descriptor_data_variables;
3453
+ // ===== Derived names (preserve these) =====
3454
+ const hasPathParams = ((_descriptor_data_variables = descriptor.data.variables) != null ? _descriptor_data_variables : []).some((v)=>{
3455
+ var _v_in;
3456
+ return ((_v_in = v.in) == null ? void 0 : _v_in.toUpperCase()) === 'PATH';
3457
+ });
3458
+ const actionName = camelCase(descriptor.name) // e.g. getUser
3459
+ ;
3460
+ const respVar = `${actionName}Resp` // e.g. getUserResp
3461
+ ;
3462
+ const dataVar = `${actionName}Data` // e.g. getUserData
3463
+ ;
3464
+ const clearName = `clear${pascalCase(descriptor.name)}` // e.g. clearGetUser
3465
+ ;
3466
+ const requestBodyVar = descriptor.data.requestBody ? camelCase(descriptor.data.requestBody) : undefined;
3467
+ const requestBodyType = descriptor.data.requestBody ? pascalCase(descriptor.data.requestBody) : undefined;
3468
+ const paramsVar = hasPathParams ? `${actionName}Params` : undefined // e.g. getUserParams
3469
+ ;
3470
+ const paramsType = hasPathParams ? `${pascalCase(descriptor.name)}Params` : undefined // e.g. GetUserParams
3471
+ ;
3472
+ const responseTypeName = `${pascalCase(descriptor.name)}ResponseBody`;
3473
+ return md`
3474
+ # Intrig React Hooks — Quick Guide
3475
+
3476
+ ## Copy-paste starter (fast lane)
3477
+
3478
+ ### 1) Hook import
3479
+ ${"```ts"}
3480
+ import { use${pascalCase(descriptor.name)} } from '@intrig/react/${descriptor.path}/client';
3481
+ ${"```"}
3482
+
3483
+ ### 2) Utility guards
3484
+ ${"```ts"}
3485
+ import { isPending, isError, isSuccess } from '@intrig/react';
3486
+ ${"```"}
3487
+
3488
+ ### 3) Hook instance (auto-clear on unmount)
3489
+ ${"```ts"}
3490
+ const [${respVar}, ${actionName}] = use${pascalCase(descriptor.name)}({ clearOnUnmount: true });
3491
+ ${"```"}
3492
+
3493
+ Intrig stateful hooks expose a **NetworkState** plus **actions** to fetch / clear.
3494
+ Use this when you want a cached, reusable result tied to a global store.
3495
+
3496
+ ---
3497
+
3498
+ ## TL;DR (copy–paste)
3499
+ ${"```tsx"}
3500
+ import { use${pascalCase(descriptor.name)} } from '@intrig/react/${descriptor.path}/client';
3501
+ import { isPending, isError, isSuccess } from '@intrig/react';
3502
+ import { useEffect, useMemo } from 'react';
3503
+
3504
+ export default function Example() {
3505
+ const [${respVar}, ${actionName}] = use${pascalCase(descriptor.name)}({ clearOnUnmount: true });
3506
+
3507
+ useEffect(() => {
3508
+ ${actionName}(${[
3509
+ requestBodyVar,
3510
+ paramsVar != null ? paramsVar : '{}'
3511
+ ].filter(Boolean).join(', ')});
3512
+ }, [${[
3513
+ '' + actionName,
3514
+ requestBodyVar,
3515
+ paramsVar
3516
+ ].filter(Boolean).join(', ')}]);
3517
+
3518
+ const ${dataVar} = useMemo(
3519
+ () => (isSuccess(${respVar}) ? ${respVar}.data : undefined),
3520
+ [${respVar}]
3521
+ );
3522
+
3523
+ if (isPending(${respVar})) return <>Loading…</>;
3524
+ if (isError(${respVar})) return <>Error: {String(${respVar}.error)}</>;
3525
+ return <pre>{JSON.stringify(${dataVar}, null, 2)}</pre>;
3526
+ }
3527
+ ${"```"}
3528
+
3529
+ ${requestBodyType || paramsType ? `### Optional types (if generated by your build)
3530
+ ${"```ts"}
3531
+ ${requestBodyType ? `import type { ${requestBodyType} } from '@intrig/react/${descriptor.source}/components/schemas/${requestBodyType}';
3532
+ ` : ''}${paramsType ? `import type { ${paramsType} } from '@intrig/react/${descriptor.path}/${pascalCase(descriptor.name)}.params';
3533
+ ` : ''}// Prefer the concrete response type:
3534
+ import type { ${responseTypeName} } from '@intrig/react/${descriptor.path}/${pascalCase(descriptor.name)}.response';
3535
+ ${"```"}
3536
+ ` : ''}
3537
+
3538
+ ---
3539
+
3540
+ ## Hook API
3541
+ ${"```ts"}
3542
+ // Options are consistent across hooks.
3543
+ type UseHookOptions = {
3544
+ /** Execute once after mount with provided params/body (if required). */
3545
+ fetchOnMount?: boolean;
3546
+ /** Reset the state on unmount (recommended). */
3547
+ clearOnUnmount?: boolean;
3548
+ /** Distinguish multiple instances of the same hook. */
3549
+ key?: string;
3550
+ /** Initial path params for endpoints that require them. */
3551
+ params?: ${paramsType != null ? paramsType : 'unknown'};
3552
+ /** Initial request body (for POST/PUT/etc.). */
3553
+ body?: ${requestBodyType != null ? requestBodyType : 'unknown'};
3554
+ };
3555
+
3556
+ // Prefer concrete types if your build emits them:
3557
+ // import type { ${responseTypeName} } from '@intrig/react/${descriptor.path}/${pascalCase(descriptor.name)}.response';
3558
+
3559
+ type ${pascalCase(descriptor.name)}Data = ${'typeof ' + responseTypeName !== 'undefined' ? responseTypeName : 'unknown'}; // replace with ${responseTypeName} if generated
3560
+ type ${pascalCase(descriptor.name)}Request = { params?: ${paramsType != null ? paramsType : 'unknown'}; body?: ${requestBodyType != null ? requestBodyType : 'unknown'}; };
3561
+
3562
+ // Signature (shape shown; concrete generics vary per generated hook)
3563
+ declare function use${pascalCase(descriptor.name)}(options?: UseHookOptions): [
3564
+ NetworkState<${pascalCase(descriptor.name)}Data>,
3565
+ (req: ${pascalCase(descriptor.name)}Request) => void,
3566
+ () => void
3567
+ ];
3568
+ ${"```"}
3569
+
3570
+ ### NetworkState & guards
3571
+ ${"```ts"}
3572
+ import { isPending, isError, isSuccess } from '@intrig/react';
3573
+ ${"```"}
3574
+
3575
+ ---
3576
+
3577
+ ## Conceptual model (Stateful = single source of truth)
3578
+ - Lives in a shared store keyed by \`key\` + request signature.
3579
+ - Best for **read** endpoints you want to **reuse** or keep **warm** (lists, details, search).
3580
+ - Lifecycle helpers:
3581
+ - \`fetchOnMount\` to kick off automatically.
3582
+ - \`clearOnUnmount\` to clean up (recommended default).
3583
+ - \`key\` to isolate multiple independent instances.
3584
+
3585
+ ---
3586
+
3587
+ ## Usage patterns
3588
+
3589
+ ### 1) Controlled (most explicit)
3590
+ ${"```tsx"}
3591
+ const [${respVar}, ${actionName}, ${clearName}] = use${pascalCase(descriptor.name)}();
3592
+
3593
+ useEffect(() => {
3594
+ ${actionName}(${[
3595
+ requestBodyVar,
3596
+ paramsVar != null ? paramsVar : '{}'
3597
+ ].filter(Boolean).join(', ')});
3598
+ return ${clearName}; // optional cleanup
3599
+ }, [${[
3600
+ '' + actionName,
3601
+ clearName
3602
+ ].join(', ')}]);
3603
+ ${"```"}
3604
+
3605
+ <details><summary>Description</summary>
3606
+ <p><strong>Use when</strong> you need explicit control over when a request fires, what params/body it uses, and when to clean up. Ideal for search forms, pagination, conditional fetches.</p>
3607
+ </details>
3608
+
3609
+ ### 2) Lifecycle-bound (shorthand)
3610
+ ${"```tsx"}
3611
+ const [${respVar}] = use${pascalCase(descriptor.name)}({
3612
+ fetchOnMount: true,
3613
+ clearOnUnmount: true,
3614
+ ${requestBodyType ? `body: ${requestBodyVar},` : ''} ${paramsType ? `params: ${paramsVar != null ? paramsVar : '{}'},` : 'params: {},'}
3615
+ });
3616
+ ${"```"}
3617
+
3618
+ <details><summary>Description</summary>
3619
+ <p><strong>Use when</strong> the data should follow the component lifecycle: fetch once on mount, reset on unmount.</p>
3620
+ </details>
3621
+
3622
+ ### 3) Passive observer (render when data arrives)
3623
+ ${"```tsx"}
3624
+ const [${respVar}] = use${pascalCase(descriptor.name)}();
3625
+ return isSuccess(${respVar}) ? <>{String(${respVar}.data)}</> : null;
3626
+ ${"```"}
3627
+
3628
+ <details><summary>Description</summary>
3629
+ <p><strong>Use when</strong> another part of the app triggers the fetch and this component only reads the state.</p>
3630
+ </details>
3631
+
3632
+ ### 4) Keep previous success while refetching (sticky)
3633
+ ${"```tsx"}
3634
+ const [${dataVar}, set${pascalCase(descriptor.name)}Data] = useState<any>();
3635
+ const [${respVar}, ${actionName}] = use${pascalCase(descriptor.name)}();
3636
+
3637
+ useEffect(() => {
3638
+ if (isSuccess(${respVar})) set${pascalCase(descriptor.name)}Data(${respVar}.data);
3639
+ }, [${respVar}]);
3640
+
3641
+ return (
3642
+ <>
3643
+ {isPending(${respVar}) && ${dataVar} ? <div>Refreshing…</div> : null}
3644
+ <pre>{JSON.stringify(isSuccess(${respVar}) ? ${respVar}.data : ${dataVar}, null, 2)}</pre>
3645
+ </>
3646
+ );
3647
+ ${"```"}
3648
+
3649
+ <details><summary>Description</summary>
3650
+ <p><strong>Use when</strong> you want SWR-like UX without flicker.</p>
3651
+ </details>
3652
+
3653
+ ### 5) Multiple instances of the same hook (isolate with \`key\`)
3654
+ ${"```tsx"}
3655
+ const a = use${pascalCase(descriptor.name)}({ key: 'A', fetchOnMount: true, ${paramsType ? `params: ${paramsVar != null ? paramsVar : '{}'} ` : 'params: {}'} });
3656
+ const b = use${pascalCase(descriptor.name)}({ key: 'B', fetchOnMount: true, ${paramsType ? `params: ${paramsVar != null ? paramsVar : '{}'} ` : 'params: {}'} });
3657
+ ${"```"}
3658
+
3659
+ <details><summary>Description</summary>
3660
+ <p>Use unique keys to prevent state collisions.</p>
3661
+ </details>
3662
+
3663
+ ---
3664
+
3665
+ ## Before / After mini-migrations
3666
+
3667
+ ### If you mistakenly used Stateful for a simple submit → switch to Async
3668
+ ${"```diff"}
3669
+ - const [${respVar}, ${actionName}] = use${pascalCase(descriptor.name)}();
3670
+ - ${actionName}(${[
3671
+ requestBodyVar,
3672
+ paramsVar != null ? paramsVar : '{}'
3673
+ ].filter(Boolean).join(', ')});
3674
+ + const [fn] = use${pascalCase(descriptor.name)}Async();
3675
+ + await fn(${[
3676
+ requestBodyVar,
3677
+ paramsVar
3678
+ ].filter(Boolean).join(', ')});
3679
+ ${"```"}
3680
+
3681
+ ### If you started with Async but need to read later in another component → Stateful
3682
+ ${"```diff"}
3683
+ - const [fn] = use${pascalCase(descriptor.name)}Async();
3684
+ - const data = await fn(${[
3685
+ requestBodyVar,
3686
+ paramsVar
3687
+ ].filter(Boolean).join(', ')});
3688
+ + const [${respVar}, ${actionName}] = use${pascalCase(descriptor.name)}({ fetchOnMount: true, clearOnUnmount: true, ${paramsType ? `params: ${paramsVar != null ? paramsVar : '{}'},` : 'params: {},'} ${requestBodyType ? `body: ${requestBodyVar}` : ''} });
3689
+ + // read from ${respVar} anywhere with use${pascalCase(descriptor.name)}()
3690
+ ${"```"}
3691
+
3692
+ ---
3693
+
3694
+ ## Anti-patterns
3695
+ <details><summary>Don’t use Stateful for field validations or a one-off submit</summary>
3696
+ Use the Async variant instead: \`const [fn] = use${pascalCase(descriptor.name)}Async()\`.
3697
+ </details>
3698
+ <details><summary>Don’t use Async for long-lived lists or detail views</summary>
3699
+ Use Stateful so other components can read the same data and you can avoid refetch churn.
3700
+ </details>
3701
+ <details><summary>Don’t forget required \`params\` when using \`fetchOnMount\`</summary>
3702
+ Provide \`params\` (and \`body\` if applicable) or switch to the controlled pattern.
3703
+ </details>
3704
+ <details><summary>Rendering the same hook twice without a \`key\`</summary>
3705
+ If they should be independent, add a unique \`key\`.
3706
+ </details>
3707
+
3708
+ ---
3709
+
3710
+ ## Error & UX guidance
3711
+ - **Loading:** early return or inline spinner. Prefer **sticky data** to avoid blanking content.
3712
+ - **Errors:** show a banner or inline errors depending on UX; keep previous good state if possible.
3713
+ - **Cleanup:** prefer \`clearOnUnmount: true\` as the default.
3714
+
3715
+ ---
3716
+
3717
+ ## Concurrency patterns
3718
+ - **Refresh:** call the action again; combine with sticky data for smooth UX.
3719
+ - **Dedupe:** isolate instances with \`key\`.
3720
+ - **Parallel:** render two keyed instances; don’t share the same key.
3721
+
3722
+ ---
3723
+
3724
+ ## Full examples
3725
+
3726
+ ### Short format (lifecycle-bound)
3727
+ ${"```tsx"}
3728
+ import { use${pascalCase(descriptor.name)} } from '@intrig/react/${descriptor.path}/client';
3729
+ import { isPending, isError, isSuccess } from '@intrig/react';
3730
+ import { useMemo } from 'react';
3731
+
3732
+ function ShortExample() {
3733
+ const [${respVar}] = use${pascalCase(descriptor.name)}({
3734
+ fetchOnMount: true,
3735
+ clearOnUnmount: true,
3736
+ ${requestBodyType ? `body: ${requestBodyVar},` : ''} ${paramsType ? `params: ${paramsVar != null ? paramsVar : '{}'},` : 'params: {},'}
3737
+ });
3738
+
3739
+ const ${dataVar} = useMemo(() => (isSuccess(${respVar}) ? ${respVar}.data : undefined), [${respVar}]);
3740
+
3741
+ if (isPending(${respVar})) return <>Loading…</>;
3742
+ if (isError(${respVar})) return <>Error: {String(${respVar}.error)}</>;
3743
+ return <pre>{JSON.stringify(${dataVar}, null, 2)}</pre>;
3744
+ }
3745
+ ${"```"}
3746
+
3747
+ <details><summary>Description</summary>
3748
+ <p>Compact lifecycle-bound approach. Great for read-only pages that load once and clean up on unmount.</p>
3749
+ </details>
3750
+
3751
+ ### Controlled format (explicit actions)
3752
+ ${"```tsx"}
3753
+ import { use${pascalCase(descriptor.name)} } from '@intrig/react/${descriptor.path}/client';
3754
+ import { isPending, isError, isSuccess } from '@intrig/react';
3755
+ import { useEffect, useMemo } from 'react';
3756
+
3757
+ function ControlledExample() {
3758
+ const [${respVar}, ${actionName}, ${clearName}] = use${pascalCase(descriptor.name)}();
3759
+
3760
+ useEffect(() => {
3761
+ ${actionName}(${[
3762
+ requestBodyVar,
3763
+ paramsVar != null ? paramsVar : '{}'
3764
+ ].filter(Boolean).join(', ')});
3765
+ return ${clearName};
3766
+ }, [${[
3767
+ '' + actionName,
3768
+ clearName
3769
+ ].join(', ')}]);
3770
+
3771
+ const ${dataVar} = useMemo(() => (isSuccess(${respVar}) ? ${respVar}.data : undefined), [${respVar}]);
3772
+
3773
+ if (isPending(${respVar})) return <>Loading…</>;
3774
+ if (isError(${respVar})) return <>An error occurred: {String(${respVar}.error)}</>;
3775
+ return <pre>{JSON.stringify(${dataVar}, null, 2)}</pre>;
3776
+ }
3777
+ ${"```"}
3778
+
3779
+ ---
3780
+
3781
+ ## Gotchas & Tips
3782
+ - Prefer **\`clearOnUnmount: true\`** in most components.
3783
+ - Use **\`key\`** for multiple independent instances.
3784
+ - Memoize derived values with **\`useMemo\`** to avoid churn.
3785
+ - Inline indicators keep the rest of the page interactive.
3786
+
3787
+ ---
3788
+
3789
+ ## Reference: State helpers
3790
+ ${"```ts"}
3791
+ if (isPending(${respVar})) { /* show spinner */ }
3792
+ if (isError(${respVar})) { /* show error */ }
3793
+ if (isSuccess(${respVar})) { /* read ${respVar}.data */ }
3794
+ ${"```"}
3795
+ `;
3796
+ }
3797
+
3798
+ function reactAsyncFunctionHookDocs(descriptor) {
3799
+ const md = mdLiteral('async-hook.md');
3800
+ var _descriptor_data_variables;
3801
+ // ===== Derived names (preserve these) =====
3802
+ const hasPathParams = ((_descriptor_data_variables = descriptor.data.variables) != null ? _descriptor_data_variables : []).some((v)=>{
3803
+ var _v_in;
3804
+ return ((_v_in = v.in) == null ? void 0 : _v_in.toUpperCase()) === 'PATH';
3805
+ });
3806
+ const actionName = camelCase(descriptor.name) // e.g. getUser
3807
+ ;
3808
+ const abortName = `abort${pascalCase(descriptor.name)}` // e.g. abortGetUser
3809
+ ;
3810
+ const requestBodyVar = descriptor.data.requestBody ? camelCase(descriptor.data.requestBody) // e.g. createUser
3811
+ : undefined;
3812
+ const requestBodyType = descriptor.data.requestBody ? pascalCase(descriptor.data.requestBody) // e.g. CreateUser
3813
+ : undefined;
3814
+ const paramsVar = hasPathParams ? `${actionName}Params` : undefined // e.g. getUserParams
3815
+ ;
3816
+ const paramsType = hasPathParams ? `${pascalCase(descriptor.name)}Params` : undefined // e.g. GetUserParams
3817
+ ;
3818
+ const responseTypeName = `${pascalCase(descriptor.name)}ResponseBody` // e.g. GetUserResponseBody
3819
+ ;
3820
+ const callArgs = [
3821
+ requestBodyVar,
3822
+ paramsVar
3823
+ ].filter(Boolean).join(', ');
3824
+ return md`
3825
+ # Intrig Async Hooks — Quick Guide
3826
+
3827
+ ## Copy-paste starter (fast lane)
3828
+
3829
+ ### 1) Hook import
3830
+ ${"```ts"}
3831
+ import { use${pascalCase(descriptor.name)}Async } from '@intrig/react/${descriptor.path}/client';
3832
+ ${"```"}
3833
+
3834
+ ### 2) Create an instance
3835
+ ${"```ts"}
3836
+ const [${actionName}, ${abortName}] = use${pascalCase(descriptor.name)}Async();
3837
+ ${"```"}
3838
+
3839
+ ### 3) Call it (awaitable)
3840
+ ${"```ts"}
3841
+ // body?, params? — pass what your endpoint needs (order: body, params)
3842
+ await ${actionName}(${callArgs});
3843
+ ${"```"}
3844
+
3845
+ Async hooks are for one-off, low-friction calls (e.g., validations, submissions). They return an **awaitable function** plus an **abort** function. No NetworkState.
3846
+
3847
+ ---
3848
+
3849
+ ## TL;DR (copy–paste)
3850
+ ${"```tsx"}
3851
+ import { use${pascalCase(descriptor.name)}Async } from '@intrig/react/${descriptor.path}/client';
3852
+ import { useCallback, useEffect } from 'react';
3853
+
3854
+ export default function Example() {
3855
+ const [${actionName}, ${abortName}] = use${pascalCase(descriptor.name)}Async();
3856
+
3857
+ const run = useCallback(async () => {
3858
+ try {
3859
+ const result = await ${actionName}(${callArgs});
3860
+ // do something with result
3861
+ console.log(result);
3862
+ } catch (e) {
3863
+ // request failed or was aborted
3864
+ console.error(e);
3865
+ }
3866
+ }, [${actionName}]);
3867
+
3868
+ // Optional: abort on unmount
3869
+ useEffect(() => ${abortName}, [${abortName}]);
3870
+
3871
+ return <button onClick={run}>Call</button>;
3872
+ }
3873
+ ${"```"}
3874
+
3875
+ ${requestBodyType || paramsType ? `### Optional types (if generated by your build)
3876
+ ${"```ts"}
3877
+ ${requestBodyType ? `import type { ${requestBodyType} } from '@intrig/react/${descriptor.source}/components/schemas/${requestBodyType}';
3878
+ ` : ''}${paramsType ? `import type { ${paramsType} } from '@intrig/react/${descriptor.path}/${pascalCase(descriptor.name)}.params';
3879
+ ` : ''}import type { ${responseTypeName} } from '@intrig/react/${descriptor.path}/${pascalCase(descriptor.name)}.response';
3880
+ ${"```"}
3881
+ ` : ''}
3882
+
3883
+ ---
3884
+
3885
+ ## Hook API
3886
+ ${"```ts"}
3887
+ // Prefer concrete types if your build emits them:
3888
+ // import type { ${responseTypeName} } from '@intrig/react/${descriptor.path}/${pascalCase(descriptor.name)}.response';
3889
+ // ${paramsType ? `import type { ${paramsType} } from '@intrig/react/${descriptor.path}/${pascalCase(descriptor.name)}.params';` : ''}
3890
+
3891
+ type ${pascalCase(descriptor.name)}Data = ${'unknown'}; // replace with ${responseTypeName} if generated
3892
+ type ${pascalCase(descriptor.name)}Request = {
3893
+ body?: ${requestBodyType != null ? requestBodyType : 'unknown'};
3894
+ params?: ${paramsType != null ? paramsType : 'unknown'};
3895
+ };
3896
+
3897
+ // Signature (shape shown; return type depends on your endpoint)
3898
+ declare function use${pascalCase(descriptor.name)}Async(): [
3899
+ (body?: ${pascalCase(descriptor.name)}Request['body'], params?: ${pascalCase(descriptor.name)}Request['params']) => Promise<${pascalCase(descriptor.name)}Data>,
3900
+ () => void // abort
3901
+ ];
3902
+ ${"```"}
3903
+
3904
+ ### Why async hooks?
3905
+ - **No state machine:** just \`await\` the result.
3906
+ - **Great for validations & submits:** uniqueness checks, field-level checks, updates.
3907
+ - **Abortable:** cancel in-flight work on demand.
3908
+
3909
+ ---
3910
+
3911
+ ## Usage Patterns
3912
+
3913
+ ### 1) Simple try/catch (recommended)
3914
+ ${"```tsx"}
3915
+ const [${actionName}] = use${pascalCase(descriptor.name)}Async();
3916
+
3917
+ try {
3918
+ const res = await ${actionName}(${callArgs});
3919
+ // use res
3920
+ } catch (e) {
3921
+ // network error or abort
3922
+ }
3923
+ ${"```"}
3924
+
3925
+ <details><summary>Description</summary>
3926
+ <p><strong>Use when</strong> you just need the value or an error. Ideal for validators, uniqueness checks, or quick lookups.</p>
3927
+ </details>
3928
+
3929
+ ### 2) Abort on unmount (safe cleanup)
3930
+ ${"```tsx"}
3931
+ const [${actionName}, ${abortName}] = use${pascalCase(descriptor.name)}Async();
3932
+
3933
+ useEffect(() => ${abortName}, [${abortName}]);
3934
+ ${"```"}
3935
+
3936
+ <details><summary>Description</summary>
3937
+ <p><strong>Use when</strong> the component may unmount while a request is in-flight (route changes, conditional UI).</p>
3938
+ </details>
3939
+
3940
+ ### 3) Debounced validation (e.g., on input change)
3941
+ ${"```tsx"}
3942
+ const [${actionName}, ${abortName}] = use${pascalCase(descriptor.name)}Async();
3943
+
3944
+ const onChange = useMemo(() => {
3945
+ let t: any;
3946
+ return (${requestBodyVar ? `${requestBodyVar}: ${requestBodyType != null ? requestBodyType : 'any'}` : 'value: string'}) => {
3947
+ clearTimeout(t);
3948
+ t = setTimeout(async () => {
3949
+ try {
3950
+ // Optionally abort before firing a new request
3951
+ ${abortName}();
3952
+ await ${actionName}(${[
3953
+ requestBodyVar != null ? requestBodyVar : '/* body from value */',
3954
+ paramsVar != null ? paramsVar : '/* params? */'
3955
+ ].join(', ')});
3956
+ } catch {}
3957
+ }, 250);
3958
+ };
3959
+ }, [${actionName}, ${abortName}]);
3960
+ ${"```"}
3961
+
3962
+ <details><summary>Description</summary>
3963
+ <p><strong>Use when</strong> validating as the user types. Debounce to reduce chatter; consider <code>${abortName}()</code> before firing a new call.</p>
3964
+ </details>
3965
+
3966
+ ### 4) Guard against races (latest-only)
3967
+ ${"```tsx"}
3968
+ const [${actionName}, ${abortName}] = use${pascalCase(descriptor.name)}Async();
3969
+
3970
+ const latestOnly = async () => {
3971
+ ${abortName}();
3972
+ return ${actionName}(${callArgs});
3973
+ };
3974
+ ${"```"}
3975
+
3976
+ <details><summary>Description</summary>
3977
+ <p><strong>Use when</strong> only the most recent call should win (search suggestions, live filters).</p>
3978
+ </details>
3979
+
3980
+ ---
3981
+
3982
+ ## Full example
3983
+ ${"```tsx"}
3984
+ import { use${pascalCase(descriptor.name)}Async } from '@intrig/react/${descriptor.path}/client';
3985
+ import { useCallback } from 'react';
3986
+
3987
+ function MyComponent() {
3988
+ const [${actionName}, ${abortName}] = use${pascalCase(descriptor.name)}Async();
3989
+
3990
+ const run = useCallback(async () => {
3991
+ try {
3992
+ const data = await ${actionName}(${callArgs});
3993
+ alert(JSON.stringify(data));
3994
+ } catch (e) {
3995
+ console.error('Call failed/aborted', e);
3996
+ }
3997
+ }, [${actionName}]);
3998
+
3999
+ return (
4000
+ <>
4001
+ <button onClick={run}>Call remote</button>
4002
+ <button onClick={${abortName}}>Abort</button>
4003
+ </>
4004
+ );
4005
+ }
4006
+ ${"```"}
4007
+
4008
+ ---
4009
+
4010
+ ## Gotchas & Tips
4011
+ - **No \`NetworkState\`:** async hooks return a Promise, not a state machine.
4012
+ - **Abort:** always available; call it to cancel the latest in-flight request.
4013
+ - **Errors:** wrap calls with \`try/catch\` to handle network failures or abort errors.
4014
+ - **Debounce & throttle:** combine with timers to cut down chatter for typeahead/validators.
4015
+ - **Types:** prefer generated \`${responseTypeName}\` and \`${paramsType != null ? paramsType : '...Params'}\` if your build emits them.
4016
+
4017
+ ---
4018
+
4019
+ ## Reference: Minimal cheat sheet
4020
+ ${"```ts"}
4021
+ const [fn, abort] = use${pascalCase(descriptor.name)}Async();
4022
+ await fn(${callArgs});
4023
+ abort(); // optional
4024
+ ${"```"}
4025
+ `;
4026
+ }
4027
+
4028
+ function reactDownloadHookDocs(descriptor) {
4029
+ const md = mdLiteral('download-hook.md');
4030
+ var _descriptor_data_variables;
4031
+ // ===== Derived names (preserve these) =====
4032
+ const hasPathParams = ((_descriptor_data_variables = descriptor.data.variables) != null ? _descriptor_data_variables : []).some((v)=>{
4033
+ var _v_in;
4034
+ return ((_v_in = v.in) == null ? void 0 : _v_in.toUpperCase()) === 'PATH';
4035
+ });
4036
+ const actionName = camelCase(descriptor.name); // e.g. downloadTaskFile
4037
+ const respVar = `${actionName}Resp`; // e.g. downloadTaskFileResp
4038
+ const paramsVar = hasPathParams ? `${actionName}Params` : undefined; // e.g. downloadTaskFileParams
4039
+ const paramsType = hasPathParams ? `${pascalCase(descriptor.name)}Params` : undefined; // e.g. DownloadTaskFileParams
4040
+ const pascal = pascalCase(descriptor.name);
4041
+ const responseTypeName = `${pascal}ResponseBody`; // e.g. DownloadTaskFileResponseBody
4042
+ return md`
4043
+ # Intrig Download Hooks — Quick Guide
4044
+
4045
+ ## Copy-paste starter (fast lane)
4046
+
4047
+ ### Auto-download (most common)
4048
+ ${"```ts"}
4049
+ import { use${pascal}Download } from '@intrig/react/${descriptor.path}/client';
4050
+ ${"```"}
4051
+ ${"```ts"}
4052
+ import { isPending, isError } from '@intrig/react';
4053
+ ${"```"}
4054
+ ${"```tsx"}
4055
+ const [${respVar}, ${actionName}] = use${pascal}Download({ clearOnUnmount: true });
4056
+ // e.g., in a click handler:
4057
+ ${actionName}(${paramsType ? paramsVar != null ? paramsVar : '{}' : '{}'});
4058
+ ${"```"}
4059
+
4060
+ ### Manual/stateful (you handle the blob/UI)
4061
+ ${"```ts"}
4062
+ import { use${pascal} } from '@intrig/react/${descriptor.path}/client';
4063
+ ${"```"}
4064
+ ${"```ts"}
4065
+ import { isSuccess, isPending, isError } from '@intrig/react';
4066
+ ${"```"}
4067
+ ${"```tsx"}
4068
+ const [${respVar}, ${actionName}] = use${pascal}({ clearOnUnmount: true });
4069
+ // later:
4070
+ ${actionName}(${paramsType ? paramsVar != null ? paramsVar : '{}' : '{}'});
4071
+ ${"```"}
4072
+
4073
+ ---
4074
+
4075
+ ## TL;DR (auto-download)
4076
+ ${"```tsx"}
4077
+ import { use${pascal}Download } from '@intrig/react/${descriptor.path}/client';
4078
+ import { isPending, isError } from '@intrig/react';
4079
+
4080
+ export default function Example() {
4081
+ const [${respVar}, ${actionName}] = use${pascal}Download({ clearOnUnmount: true });
4082
+
4083
+ return (
4084
+ <button
4085
+ onClick={() => ${actionName}(${paramsType ? paramsVar != null ? paramsVar : '{}' : '{}'})}
4086
+ disabled={isPending(${respVar})}
4087
+ >
4088
+ {isPending(${respVar}) ? 'Downloading…' : 'Download'}
4089
+ </button>
4090
+ );
4091
+ }
4092
+ ${"```"}
4093
+
4094
+ ${paramsType ? `### Optional types (if generated by your build)
4095
+ ${"```ts"}
4096
+ import type { ${paramsType} } from '@intrig/react/${descriptor.path}/${pascal}.params';
4097
+ import type { ${responseTypeName} } from '@intrig/react/${descriptor.path}/${pascal}.response';
4098
+ ${"```"}
4099
+ ` : ''}
4100
+
4101
+ ---
4102
+
4103
+ ## Hook APIs
4104
+
4105
+ ### \`use${pascal}Download\` (auto-download)
4106
+ - **What it does:** requests the file with \`responseType: 'blob'\` + \`adapter: 'fetch'\`, derives filename from \`Content-Disposition\` if present, creates a temporary object URL, clicks a hidden \`<a>\`, **downloads**, then resets state to \`init\`.
4107
+ - **Signature:** \`[state, download, clear]\`
4108
+ - \`download(params: ${paramsType != null ? paramsType : 'Record<string, unknown>'}) => void\`
4109
+
4110
+ ### \`use${pascal}\` (manual/stateful)
4111
+ - **What it does:** same request but **does not** auto-save. You control preview/saving using \`state.data\` + \`state.headers\`.
4112
+ - **Signature:** \`[state, fetchFile, clear]\`
4113
+ - \`fetchFile(params: ${paramsType != null ? paramsType : 'Record<string, unknown>'}) => void\`
4114
+
4115
+ ---
4116
+
4117
+ ## Usage Patterns
4118
+
4119
+ ### 1) Auto-download on click (recommended)
4120
+ ${"```tsx"}
4121
+ const [${respVar}, ${actionName}] = use${pascal}Download({ clearOnUnmount: true });
4122
+
4123
+ <button
4124
+ onClick={() => ${actionName}(${paramsType ? paramsVar != null ? paramsVar : '{}' : '{}'})}
4125
+ disabled={isPending(${respVar})}
4126
+ >
4127
+ {isPending(${respVar}) ? 'Downloading…' : 'Download file'}
4128
+ </button>
4129
+ {isError(${respVar}) ? <p className="text-red-500">Failed to download.</p> : null}
4130
+ ${"```"}
4131
+
4132
+ <details><summary>Description</summary>
4133
+ <p>Most users just need a button that saves the file. This variant handles object URL creation, filename extraction, click, and state reset.</p>
4134
+ </details>
4135
+
4136
+ ### 2) Auto-download on mount (e.g., “Your file is ready” page)
4137
+ ${"```tsx"}
4138
+ useEffect(() => {
4139
+ ${actionName}(${paramsType ? paramsVar != null ? paramsVar : '{}' : '{}'});
4140
+ }, [${actionName}]);
4141
+ ${"```"}
4142
+
4143
+ <details><summary>Description</summary>
4144
+ <p>Good for post-processing routes that immediately start a download.</p>
4145
+ </details>
4146
+
4147
+ ### 3) Manual handling (preview or custom filename)
4148
+ ${"```tsx"}
4149
+ const [${respVar}, ${actionName}] = use${pascal}({ clearOnUnmount: true });
4150
+
4151
+ useEffect(() => {
4152
+ if (isSuccess(${respVar})) {
4153
+ const ct = ${respVar}.headers?.['content-type'] ?? 'application/octet-stream';
4154
+ const parts = Array.isArray(${respVar}.data) ? ${respVar}.data : [${respVar}.data];
4155
+ const url = URL.createObjectURL(new Blob(parts, { type: ct }));
4156
+ // preview/save with your own UI...
4157
+ return () => URL.revokeObjectURL(url);
4158
+ }
4159
+ }, [${respVar}]);
4160
+ ${"```"}
4161
+
4162
+ <details><summary>Description</summary>
4163
+ <p>Use when you need to inspect headers, show a preview, or control the filename/UI flow.</p>
4164
+ </details>
4165
+
4166
+ ---
4167
+
4168
+ ## Behavior notes (what the auto-download variant does)
4169
+ - Forces \`responseType: 'blob'\` and \`adapter: 'fetch'\`.
4170
+ - If \`content-type\` is JSON, stringifies payload so the saved file is human-readable.
4171
+ - Parses \`Content-Disposition\` to derive a filename; falls back to a default.
4172
+ - Creates and clicks a temporary link, then **resets state to \`init\`**.
4173
+
4174
+ ---
4175
+
4176
+ ## Gotchas & Tips
4177
+ - **Expose headers in CORS:** server should send
4178
+ \`Access-Control-Expose-Headers: Content-Disposition, Content-Type\`
4179
+ - **Disable double clicks:** guard with \`isPending(state)\`.
4180
+ - **Revoke URLs** when you create them manually in the stateful variant.
4181
+ - **iOS Safari:** blob downloads may open a new tab—consider server-side direct-download URLs for a smoother UX.
4182
+
4183
+ ---
4184
+
4185
+ ## Troubleshooting
4186
+ - **No filename shown:** your server didn’t include \`Content-Disposition\`. Add it.
4187
+ - **Got JSON instead of a file:** server returned \`application/json\` (maybe an error). The auto hook saves it as text; surface the error in UI.
4188
+ - **Nothing happens on click:** ensure you’re using the **Download** variant and the request succeeds (check Network tab); verify CORS headers.
4189
+
4190
+ ---
4191
+ `;
4192
+ }
4193
+
4194
+ async function getEndpointDocumentation(result) {
4195
+ const tabs = [];
4196
+ if (result.data.responseType === 'text/event-stream') {
4197
+ tabs.push({
4198
+ name: 'SSE Hook',
4199
+ content: (await reactSseHookDocs(result)).content
4200
+ });
4201
+ } else {
4202
+ tabs.push({
4203
+ name: 'Stateful Hook',
4204
+ content: (await reactHookDocs(result)).content
4205
+ });
4206
+ }
4207
+ tabs.push({
4208
+ name: 'Stateless Hook',
4209
+ content: (await reactAsyncFunctionHookDocs(result)).content
4210
+ });
4211
+ if (result.data.isDownloadable) {
4212
+ tabs.push({
4213
+ name: 'Download Hook',
4214
+ content: (await reactDownloadHookDocs(result)).content
4215
+ });
4216
+ }
4217
+ return tabs;
4218
+ }
4219
+
4220
+ function createPlugin() {
4221
+ return {
4222
+ meta () {
4223
+ return {
4224
+ name: 'intrig-binding',
4225
+ version: '0.0.1',
4226
+ compat: '^0.0.15'
4227
+ };
4228
+ },
4229
+ generate: generateCode,
4230
+ getSchemaDocumentation,
4231
+ getEndpointDocumentation
4232
+ };
4233
+ }
4234
+
4235
+ export { createPlugin, createPlugin as default };