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