@kubb/plugin-fetch 5.0.0-beta.77 → 5.0.0-beta.80
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 +276 -88
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +276 -88
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/generators/clientGenerator.tsx +24 -11
- package/src/plugin.ts +19 -5
- package/src/templates.ts +9 -4
- package/templates/fetch.ts +300 -174
- package/templates/serializers.ts +424 -0
- package/templates/standardSchema.ts +54 -0
package/dist/index.js
CHANGED
|
@@ -77,7 +77,7 @@ function toFilePath(name, caseLast = camelCase) {
|
|
|
77
77
|
* JavaScript and Java reserved words.
|
|
78
78
|
* @link https://github.com/jonschlinkert/reserved/blob/master/index.js
|
|
79
79
|
*/
|
|
80
|
-
const reservedWords = new Set([
|
|
80
|
+
const reservedWords = /* @__PURE__ */ new Set([
|
|
81
81
|
"abstract",
|
|
82
82
|
"arguments",
|
|
83
83
|
"boolean",
|
|
@@ -326,6 +326,23 @@ function caseParams(params, casing) {
|
|
|
326
326
|
caseParamsCache.set(params, result);
|
|
327
327
|
return result;
|
|
328
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* Drops parameters that collapse to the same property identity once camelCased, keeping the first.
|
|
331
|
+
*
|
|
332
|
+
* Some specs declare the same parameter twice under different casings (for example AWS S3 lists both
|
|
333
|
+
* `max-uploads` and `MaxUploads`). Both resolve to one output property, so emitting both would yield
|
|
334
|
+
* an object type with a duplicate member, which TypeScript rejects. De-duplicate by the camelCased
|
|
335
|
+
* identity so the resulting group is collision-free regardless of the names each caller carries.
|
|
336
|
+
*/
|
|
337
|
+
function dedupeByCasedName(params) {
|
|
338
|
+
const seen = /* @__PURE__ */ new Set();
|
|
339
|
+
return params.filter((param) => {
|
|
340
|
+
const key = camelCase(param.name);
|
|
341
|
+
if (seen.has(key)) return false;
|
|
342
|
+
seen.add(key);
|
|
343
|
+
return true;
|
|
344
|
+
});
|
|
345
|
+
}
|
|
329
346
|
//#endregion
|
|
330
347
|
//#region ../../internals/shared/src/operation.ts
|
|
331
348
|
/**
|
|
@@ -364,6 +381,86 @@ function getContentTypeInfo(node) {
|
|
|
364
381
|
hasFormData: contentTypes.some((ct) => ct === "multipart/form-data")
|
|
365
382
|
};
|
|
366
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* The request-body counterpart for the primary success response: the content types it documents and
|
|
386
|
+
* whether several are present, so the client can let a caller pick which one to accept.
|
|
387
|
+
*/
|
|
388
|
+
function getResponseContentTypeInfo(node) {
|
|
389
|
+
const contentTypes = getPrimarySuccessResponse(node)?.content?.map((e) => e.contentType) ?? [];
|
|
390
|
+
const isMultipleContentTypes = contentTypes.length > 1;
|
|
391
|
+
return {
|
|
392
|
+
contentTypes,
|
|
393
|
+
isMultipleContentTypes,
|
|
394
|
+
contentTypeUnion: isMultipleContentTypes ? contentTypes.map((ct) => JSON.stringify(ct)).join(" | ") : "",
|
|
395
|
+
defaultContentType: contentTypes[0] ?? "application/json",
|
|
396
|
+
hasFormData: contentTypes.some((ct) => ct === "multipart/form-data")
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Reads the single base content type of an operation's primary success response, lowercased and
|
|
401
|
+
* stripped of any `; charset=...` suffix. Returns `undefined` when the response declares zero or
|
|
402
|
+
* more than one content type, since neither case has a single type to act on.
|
|
403
|
+
*/
|
|
404
|
+
function getPrimarySuccessContentType(node) {
|
|
405
|
+
const contentTypes = getPrimarySuccessResponse(node)?.content?.map((entry) => entry.contentType) ?? [];
|
|
406
|
+
if (contentTypes.length !== 1) return void 0;
|
|
407
|
+
return contentTypes[0].split(";")[0].trim().toLowerCase();
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Whether an operation streams its primary success response as Server-Sent Events
|
|
411
|
+
* (`text/event-stream`). The client generator uses this to return a typed event stream instead of a
|
|
412
|
+
* one-shot `RequestResult`.
|
|
413
|
+
*/
|
|
414
|
+
function isEventStream(node) {
|
|
415
|
+
return getPrimarySuccessContentType(node) === "text/event-stream";
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Derives the default `responseType` for an operation from its primary success response.
|
|
419
|
+
*
|
|
420
|
+
* Returns a value only when that response declares a single non-JSON content type. `text/event-stream`
|
|
421
|
+
* and other binary types (`application/octet-stream`, `application/pdf`, `image/*`, `audio/*`,
|
|
422
|
+
* `video/*`) map to a stream or `'blob'`, and other `text/*` maps to `'text'`. Otherwise `undefined`,
|
|
423
|
+
* leaving the runtime client's `Content-Type` auto-detection in charge.
|
|
424
|
+
*/
|
|
425
|
+
function getResponseType(node) {
|
|
426
|
+
const baseType = getPrimarySuccessContentType(node);
|
|
427
|
+
if (!baseType) return void 0;
|
|
428
|
+
if (baseType === "application/json" || baseType.endsWith("+json") || baseType === "text/json") return void 0;
|
|
429
|
+
if (baseType === "text/event-stream") return "stream";
|
|
430
|
+
if (baseType.startsWith("text/")) return "text";
|
|
431
|
+
if (baseType === "application/octet-stream" || baseType === "application/pdf" || /^(image|audio|video)\//.test(baseType)) return "blob";
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Which of the grouped request options an operation carries.
|
|
435
|
+
*/
|
|
436
|
+
function getRequestGroups(node) {
|
|
437
|
+
const { path, query, header } = getOperationParameters(node);
|
|
438
|
+
return {
|
|
439
|
+
path: path.length > 0,
|
|
440
|
+
query: query.length > 0,
|
|
441
|
+
body: Boolean(node.requestBody?.content?.[0]?.schema),
|
|
442
|
+
headers: header.length > 0
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Resolves which grouped request options an operation carries together with whether each group
|
|
447
|
+
* holds a required member. The grouped parameter stays optional only when nothing inside it is
|
|
448
|
+
* required, matching the generated `RequestConfig` type.
|
|
449
|
+
*/
|
|
450
|
+
function getRequestGroupOptionality(node) {
|
|
451
|
+
const groups = getRequestGroups(node);
|
|
452
|
+
const { path, query, header } = getOperationParameters(node);
|
|
453
|
+
const hasRequiredPath = path.some((param) => param.required);
|
|
454
|
+
const hasRequiredQuery = query.some((param) => param.required);
|
|
455
|
+
const hasRequiredHeader = header.some((param) => param.required);
|
|
456
|
+
return {
|
|
457
|
+
groups,
|
|
458
|
+
hasRequiredPath,
|
|
459
|
+
hasRequiredQuery,
|
|
460
|
+
hasRequiredHeader,
|
|
461
|
+
isOptional: !hasRequiredPath && !hasRequiredQuery && !hasRequiredHeader && !groups.body
|
|
462
|
+
};
|
|
463
|
+
}
|
|
367
464
|
function buildOperationComments(node, options = {}) {
|
|
368
465
|
const { link = "pathTemplate", linkPosition = "afterDeprecated", splitLines = false } = options;
|
|
369
466
|
const linkComment = getOperationLink(node, link);
|
|
@@ -384,10 +481,10 @@ function buildOperationComments(node, options = {}) {
|
|
|
384
481
|
function getOperationParameters(node, options = {}) {
|
|
385
482
|
const params = caseParams(node.parameters, options.paramsCasing === "original" ? void 0 : "camelcase");
|
|
386
483
|
return {
|
|
387
|
-
path: params.filter((param) => param.in === "path"),
|
|
388
|
-
query: params.filter((param) => param.in === "query"),
|
|
389
|
-
header: params.filter((param) => param.in === "header"),
|
|
390
|
-
cookie: params.filter((param) => param.in === "cookie")
|
|
484
|
+
path: dedupeByCasedName(params.filter((param) => param.in === "path")),
|
|
485
|
+
query: dedupeByCasedName(params.filter((param) => param.in === "query")),
|
|
486
|
+
header: dedupeByCasedName(params.filter((param) => param.in === "header")),
|
|
487
|
+
cookie: dedupeByCasedName(params.filter((param) => param.in === "cookie"))
|
|
391
488
|
};
|
|
392
489
|
}
|
|
393
490
|
function getStatusCodeNumber(statusCode) {
|
|
@@ -398,6 +495,15 @@ function isSuccessStatusCode(statusCode) {
|
|
|
398
495
|
const code = getStatusCodeNumber(statusCode);
|
|
399
496
|
return code !== null && code >= 200 && code < 300;
|
|
400
497
|
}
|
|
498
|
+
function getSuccessResponses(responses) {
|
|
499
|
+
return responses.filter((response) => isSuccessStatusCode(response.statusCode));
|
|
500
|
+
}
|
|
501
|
+
function getOperationSuccessResponses(node) {
|
|
502
|
+
return getSuccessResponses(node.responses);
|
|
503
|
+
}
|
|
504
|
+
function getPrimarySuccessResponse(node) {
|
|
505
|
+
return getOperationSuccessResponses(node)[0] ?? null;
|
|
506
|
+
}
|
|
401
507
|
//#endregion
|
|
402
508
|
//#region ../../internals/shared/src/group.ts
|
|
403
509
|
/**
|
|
@@ -430,39 +536,39 @@ function createGroupConfig(group) {
|
|
|
430
536
|
};
|
|
431
537
|
}
|
|
432
538
|
//#endregion
|
|
433
|
-
//#region ../../internals/client/src/builders/
|
|
539
|
+
//#region ../../internals/client/src/builders/validatorOptions.ts
|
|
434
540
|
/**
|
|
435
|
-
* Returns `true` when any direction of the
|
|
541
|
+
* Returns `true` when any direction of the validator uses zod (used for dependency checks).
|
|
436
542
|
*/
|
|
437
|
-
function
|
|
438
|
-
if (!
|
|
439
|
-
if (
|
|
440
|
-
return Boolean(
|
|
543
|
+
function isValidatorEnabled(validator) {
|
|
544
|
+
if (!validator) return false;
|
|
545
|
+
if (validator === "zod") return true;
|
|
546
|
+
return Boolean(validator.request || validator.response);
|
|
441
547
|
}
|
|
442
548
|
/**
|
|
443
549
|
* Returns `'zod'` when request body parsing is enabled, `null` otherwise. The string shorthand
|
|
444
550
|
* `'zod'` validates the response only, so it does not enable request parsing.
|
|
445
551
|
*/
|
|
446
|
-
function
|
|
447
|
-
if (!
|
|
448
|
-
return
|
|
552
|
+
function resolveRequestValidator(validator) {
|
|
553
|
+
if (!validator || validator === "zod") return null;
|
|
554
|
+
return validator.request ?? null;
|
|
449
555
|
}
|
|
450
556
|
/**
|
|
451
557
|
* Returns `'zod'` when query-parameters parsing is enabled, `null` otherwise. Only the object form
|
|
452
558
|
* `{ request: 'zod' }` enables it.
|
|
453
559
|
*/
|
|
454
|
-
function
|
|
455
|
-
if (!
|
|
456
|
-
return
|
|
560
|
+
function resolveQueryParamsValidator(validator) {
|
|
561
|
+
if (!validator || validator === "zod") return null;
|
|
562
|
+
return validator.request ?? null;
|
|
457
563
|
}
|
|
458
564
|
/**
|
|
459
565
|
* Returns `'zod'` when response parsing is enabled, `null` otherwise. The string shorthand `'zod'`
|
|
460
566
|
* maps to response parsing.
|
|
461
567
|
*/
|
|
462
|
-
function
|
|
463
|
-
if (!
|
|
464
|
-
if (
|
|
465
|
-
return
|
|
568
|
+
function resolveResponseValidator(validator) {
|
|
569
|
+
if (!validator) return null;
|
|
570
|
+
if (validator === "zod") return "zod";
|
|
571
|
+
return validator.response ?? null;
|
|
466
572
|
}
|
|
467
573
|
/**
|
|
468
574
|
* Resolves the zod expression a generated client validates a success response with. Only success
|
|
@@ -604,11 +710,13 @@ function buildGroupedOptionsSignature({ node, tsResolver }) {
|
|
|
604
710
|
node,
|
|
605
711
|
tsResolver
|
|
606
712
|
});
|
|
713
|
+
const { isOptional } = getRequestGroupOptionality(node);
|
|
607
714
|
return {
|
|
608
715
|
dataTypeName: requestConfigName,
|
|
609
716
|
paramsSignature: declarationPrinter.print(createFunctionParameters({ params: [createFunctionParameter({
|
|
610
717
|
name: "options",
|
|
611
|
-
type: `Options<${requestConfigName}, ThrowOnError
|
|
718
|
+
type: `Options<${requestConfigName}, ThrowOnError>`,
|
|
719
|
+
...isOptional ? { default: "{}" } : {}
|
|
612
720
|
})] })) ?? "",
|
|
613
721
|
returnType: `Promise<RequestResult<${resultGenerics}>>`,
|
|
614
722
|
generics: ["ThrowOnError extends boolean = true"],
|
|
@@ -616,23 +724,74 @@ function buildGroupedOptionsSignature({ node, tsResolver }) {
|
|
|
616
724
|
};
|
|
617
725
|
}
|
|
618
726
|
//#endregion
|
|
727
|
+
//#region ../../internals/client/src/builders/styles.ts
|
|
728
|
+
/**
|
|
729
|
+
* Renders a parameter name as an object-literal key, quoting it when the camelCased name is not a
|
|
730
|
+
* bare identifier (for example a name that starts with a digit) so the emitted literal stays valid.
|
|
731
|
+
*/
|
|
732
|
+
function toKey(name) {
|
|
733
|
+
const cased = camelCase(name);
|
|
734
|
+
return isValidVarName(cased) ? cased : JSON.stringify(cased);
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Serializes one parameter's metadata into a `{ style, explode }` literal, or `null` when the
|
|
738
|
+
* parameter carries neither. Path and query carry the serialization `style`; header and cookie use a
|
|
739
|
+
* fixed style (`simple` and `form`), so only `explode` is emitted for them.
|
|
740
|
+
*/
|
|
741
|
+
function serializeParameter(parameter) {
|
|
742
|
+
const parts = [];
|
|
743
|
+
if ((parameter.in === "path" || parameter.in === "query") && parameter.style) parts.push(`style: '${parameter.style}'`);
|
|
744
|
+
if (parameter.explode !== void 0) parts.push(`explode: ${parameter.explode}`);
|
|
745
|
+
return parts.length > 0 ? `{ ${parts.join(", ")} }` : null;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Builds the per-operation `styles` literal from the operation's parameters, grouped by location and
|
|
749
|
+
* keyed by the camelCased parameter name to match the generated `path` / `query` / `headers` keys.
|
|
750
|
+
* Only parameters whose source defines `style` or `explode` are emitted, so calls without
|
|
751
|
+
* serialization metadata keep the runtime defaults and existing output is unchanged. Returns `null`
|
|
752
|
+
* when no parameter carries metadata.
|
|
753
|
+
*
|
|
754
|
+
* @example
|
|
755
|
+
* ```ts
|
|
756
|
+
* // a path param with { style: 'matrix', explode: true } and a query param with { explode: false }
|
|
757
|
+
* buildStyles({ node }) // "{ path: { id: { style: 'matrix', explode: true } }, query: { tags: { explode: false } } }"
|
|
758
|
+
* ```
|
|
759
|
+
*/
|
|
760
|
+
function buildStyles({ node }) {
|
|
761
|
+
if (!ast.isHttpOperationNode(node)) return null;
|
|
762
|
+
const groups = {
|
|
763
|
+
path: [],
|
|
764
|
+
query: [],
|
|
765
|
+
header: [],
|
|
766
|
+
cookie: []
|
|
767
|
+
};
|
|
768
|
+
for (const parameter of node.parameters) {
|
|
769
|
+
const literal = serializeParameter(parameter);
|
|
770
|
+
if (!literal) continue;
|
|
771
|
+
groups[parameter.in].push(`${toKey(parameter.name)}: ${literal}`);
|
|
772
|
+
}
|
|
773
|
+
const locations = Object.keys(groups).filter((location) => groups[location].length > 0);
|
|
774
|
+
if (locations.length === 0) return null;
|
|
775
|
+
return `{ ${locations.map((location) => `${location}: { ${groups[location].join(", ")} }`).join(", ")} }`;
|
|
776
|
+
}
|
|
777
|
+
//#endregion
|
|
619
778
|
//#region ../../internals/client/src/builders/validator.ts
|
|
620
779
|
/**
|
|
621
|
-
* Builds the
|
|
622
|
-
* response
|
|
623
|
-
*
|
|
780
|
+
* Builds the validator-hook references for one operation. Request validation runs before the send;
|
|
781
|
+
* response validation runs on the success body only. Returns `null` references when the matching
|
|
782
|
+
* direction is disabled or the schema is absent.
|
|
624
783
|
*/
|
|
625
|
-
function
|
|
784
|
+
function buildValidatorHooks({ node, validator, zodResolver }) {
|
|
626
785
|
const importedZodNames = [];
|
|
627
786
|
const hasRequestBody = Boolean(node.requestBody?.content?.[0]?.schema);
|
|
628
|
-
const zodRequestName = zodResolver &&
|
|
629
|
-
const request = zodRequestName
|
|
787
|
+
const zodRequestName = zodResolver && resolveRequestValidator(validator) === "zod" && hasRequestBody ? zodResolver.resolveDataName?.(node) : null;
|
|
788
|
+
const request = zodRequestName ?? null;
|
|
630
789
|
if (zodRequestName) importedZodNames.push(zodRequestName);
|
|
631
|
-
const responseParse = zodResolver &&
|
|
632
|
-
const response = responseParse ?
|
|
790
|
+
const responseParse = zodResolver && resolveResponseValidator(validator) === "zod" ? buildZodResponseParse(node, zodResolver) : null;
|
|
791
|
+
const response = responseParse ? responseParse.expression : null;
|
|
633
792
|
if (responseParse) importedZodNames.push(...responseParse.importNames);
|
|
634
|
-
const errorParse = zodResolver &&
|
|
635
|
-
const error = errorParse ?
|
|
793
|
+
const errorParse = zodResolver && resolveResponseValidator(validator) === "zod" ? buildZodErrorParse(node, zodResolver) : null;
|
|
794
|
+
const error = errorParse ? errorParse.expression : null;
|
|
636
795
|
if (errorParse) importedZodNames.push(...errorParse.importNames);
|
|
637
796
|
return {
|
|
638
797
|
request,
|
|
@@ -648,34 +807,49 @@ function buildParserHooks({ node, parser, zodResolver }) {
|
|
|
648
807
|
* single `options` object to the resolved client and returns the `RequestResult`. The type, signature,
|
|
649
808
|
* and call config are built with the AST factory, and only the jsx-renderer emits the source.
|
|
650
809
|
*/
|
|
651
|
-
function Operation({ name, node, tsResolver, zodResolver,
|
|
810
|
+
function Operation({ name, node, tsResolver, zodResolver, validator, security, isExportable = true, isIndexable = true }) {
|
|
652
811
|
if (!ast.isHttpOperationNode(node)) return null;
|
|
653
812
|
const signature = buildGroupedOptionsSignature({
|
|
654
813
|
node,
|
|
655
814
|
tsResolver
|
|
656
815
|
});
|
|
657
|
-
const
|
|
816
|
+
const validators = buildValidatorHooks({
|
|
658
817
|
node,
|
|
659
|
-
|
|
818
|
+
validator,
|
|
660
819
|
zodResolver
|
|
661
820
|
});
|
|
662
821
|
const securityLiteral = buildSecurityMetadata({ security });
|
|
822
|
+
const stylesLiteral = buildStyles({ node });
|
|
663
823
|
const { defaultContentType } = getContentTypeInfo(node);
|
|
664
|
-
const
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
824
|
+
const bakedRequestContentType = Boolean(node.requestBody?.content?.[0]?.schema) && defaultContentType !== "application/json" ? defaultContentType : null;
|
|
825
|
+
const mergeContentType = Boolean(bakedRequestContentType) && getResponseContentTypeInfo(node).isMultipleContentTypes;
|
|
826
|
+
const contentTypeLiteral = !bakedRequestContentType ? null : mergeContentType ? `contentType: { request: '${bakedRequestContentType}', ...(typeof contentType === 'string' ? { request: contentType } : contentType) }` : `contentType: { request: '${bakedRequestContentType}' }`;
|
|
827
|
+
const eventStream = isEventStream(node);
|
|
828
|
+
const responseType = getResponseType(node);
|
|
829
|
+
const responseTypeLiteral = responseType ? `responseType: '${responseType}'` : null;
|
|
830
|
+
const validatorEntries = [
|
|
831
|
+
validators.request ? `request: ${validators.request}` : null,
|
|
832
|
+
validators.response ? `response: ${validators.response}` : null,
|
|
833
|
+
validators.error ? `error: ${validators.error}` : null
|
|
669
834
|
].filter(Boolean);
|
|
670
|
-
const
|
|
835
|
+
const validatorLiteral = validatorEntries.length ? `validator: { ${validatorEntries.join(", ")} }` : null;
|
|
671
836
|
const callConfig = `{ ${[
|
|
672
837
|
`method: '${node.method.toUpperCase()}'`,
|
|
673
838
|
`url: '${Url.toCasedTemplate(node.path, { casing: "camelcase" })}'`,
|
|
674
839
|
securityLiteral ? `security: ${securityLiteral}` : null,
|
|
675
|
-
|
|
840
|
+
stylesLiteral ? `styles: ${stylesLiteral}` : null,
|
|
841
|
+
validatorLiteral,
|
|
676
842
|
contentTypeLiteral,
|
|
843
|
+
responseTypeLiteral,
|
|
677
844
|
"...config"
|
|
678
845
|
].filter(Boolean).join(", ")} }`;
|
|
846
|
+
const eventType = `SuccessOf<${tsResolver.resolveResponsesName(node)}>`;
|
|
847
|
+
const returnType = eventStream ? `Promise<EventStreamResult<${eventType}>>` : signature.returnType;
|
|
848
|
+
const returnStatement = eventStream ? `return toEventStream<${eventType}>(request(${callConfig}))` : buildReturnStatement({
|
|
849
|
+
node,
|
|
850
|
+
tsResolver,
|
|
851
|
+
callConfig
|
|
852
|
+
});
|
|
679
853
|
return /* @__PURE__ */ jsx(File.Source, {
|
|
680
854
|
name,
|
|
681
855
|
isExportable,
|
|
@@ -685,20 +859,16 @@ function Operation({ name, node, tsResolver, zodResolver, parser, security, isEx
|
|
|
685
859
|
export: isExportable,
|
|
686
860
|
generics: signature.generics,
|
|
687
861
|
params: signature.paramsSignature,
|
|
688
|
-
returnType
|
|
862
|
+
returnType,
|
|
689
863
|
JSDoc: { comments: buildOperationComments(node, {
|
|
690
864
|
link: "urlPath",
|
|
691
865
|
linkPosition: "beforeDeprecated",
|
|
692
866
|
splitLines: true
|
|
693
867
|
}) },
|
|
694
868
|
children: [
|
|
695
|
-
"const { client: request = client, ...config } = options",
|
|
869
|
+
mergeContentType ? "const { client: request = client, contentType, ...config } = options" : "const { client: request = client, ...config } = options",
|
|
696
870
|
/* @__PURE__ */ jsx("br", {}),
|
|
697
|
-
|
|
698
|
-
node,
|
|
699
|
-
tsResolver,
|
|
700
|
-
callConfig
|
|
701
|
-
})
|
|
871
|
+
returnStatement
|
|
702
872
|
]
|
|
703
873
|
})
|
|
704
874
|
});
|
|
@@ -707,23 +877,23 @@ function Operation({ name, node, tsResolver, zodResolver, parser, security, isEx
|
|
|
707
877
|
//#region ../../internals/client/src/builders/sdkMethod.ts
|
|
708
878
|
/**
|
|
709
879
|
* Builds the call config literal forwarded to the contract client, mirroring the shared `Operation`
|
|
710
|
-
* component: `{ method, url, security?,
|
|
880
|
+
* component: `{ method, url, security?, validator?, ...config }`. The `...config` spread carries every
|
|
711
881
|
* per-call field (including `throwOnError`), so the method stays a thin wrapper over the contract.
|
|
712
882
|
*/
|
|
713
|
-
function buildCallConfig({ node,
|
|
714
|
-
const
|
|
883
|
+
function buildCallConfig({ node, validator, zodResolver, security }) {
|
|
884
|
+
const validators = buildValidatorHooks({
|
|
715
885
|
node,
|
|
716
|
-
|
|
886
|
+
validator,
|
|
717
887
|
zodResolver
|
|
718
888
|
});
|
|
719
|
-
const
|
|
720
|
-
const
|
|
889
|
+
const validatorEntries = [validators.request ? `request: ${validators.request}` : null, validators.response ? `response: ${validators.response}` : null].filter(Boolean);
|
|
890
|
+
const validatorLiteral = validatorEntries.length ? `validator: { ${validatorEntries.join(", ")} }` : null;
|
|
721
891
|
const securityLiteral = buildSecurityMetadata({ security });
|
|
722
892
|
return `{ ${[
|
|
723
893
|
`method: '${node.method.toUpperCase()}'`,
|
|
724
894
|
`url: '${Url.toCasedTemplate(node.path, { casing: "camelcase" })}'`,
|
|
725
895
|
securityLiteral ? `security: ${securityLiteral}` : null,
|
|
726
|
-
|
|
896
|
+
validatorLiteral,
|
|
727
897
|
"...config"
|
|
728
898
|
].filter(Boolean).join(", ")} }`;
|
|
729
899
|
}
|
|
@@ -733,7 +903,7 @@ function buildCallConfig({ node, parser, zodResolver, security }) {
|
|
|
733
903
|
* returns the `RequestResult`. A per-call `options.client` still overrides the instance client, so
|
|
734
904
|
* one operation can be routed to a different environment without a new instance.
|
|
735
905
|
*/
|
|
736
|
-
function buildSdkMethod({ node, name, tsResolver, zodResolver,
|
|
906
|
+
function buildSdkMethod({ node, name, tsResolver, zodResolver, validator, security }) {
|
|
737
907
|
if (!ast.isHttpOperationNode(node)) return "";
|
|
738
908
|
const signature = buildGroupedOptionsSignature({
|
|
739
909
|
node,
|
|
@@ -744,7 +914,7 @@ function buildSdkMethod({ node, name, tsResolver, zodResolver, parser, security
|
|
|
744
914
|
tsResolver,
|
|
745
915
|
callConfig: buildCallConfig({
|
|
746
916
|
node,
|
|
747
|
-
|
|
917
|
+
validator,
|
|
748
918
|
zodResolver,
|
|
749
919
|
security
|
|
750
920
|
})
|
|
@@ -770,13 +940,13 @@ function buildSdkMethod({ node, name, tsResolver, zodResolver, parser, security
|
|
|
770
940
|
* instance: `const api = new PetClient({ baseURL }); api.getPetById(...)`. A per-call `client` option
|
|
771
941
|
* still overrides the instance client for a one-off call.
|
|
772
942
|
*/
|
|
773
|
-
function SdkClient({ name, isExportable = true, isIndexable = true, operations,
|
|
943
|
+
function SdkClient({ name, isExportable = true, isIndexable = true, operations, validator, children }) {
|
|
774
944
|
const methods = operations.map(({ node, name: methodName, tsResolver, zodResolver, security }) => buildSdkMethod({
|
|
775
945
|
node,
|
|
776
946
|
name: methodName,
|
|
777
947
|
tsResolver,
|
|
778
948
|
zodResolver,
|
|
779
|
-
|
|
949
|
+
validator,
|
|
780
950
|
security
|
|
781
951
|
}));
|
|
782
952
|
const classCode = `export class ${name} {\n${[
|
|
@@ -822,13 +992,13 @@ function SdkFacade({ name, isExportable = true, isIndexable = true, members, chi
|
|
|
822
992
|
function resolveTypeImportNames(node, tsResolver) {
|
|
823
993
|
return [tsResolver.resolveRequestConfigName(node), tsResolver.resolveResponsesName(node)];
|
|
824
994
|
}
|
|
825
|
-
function resolveZodImportNames(node, zodResolver,
|
|
995
|
+
function resolveZodImportNames(node, zodResolver, validator) {
|
|
826
996
|
const { query: queryParams } = getOperationParameters(node, { paramsCasing: "original" });
|
|
827
997
|
return [
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
998
|
+
resolveResponseValidator(validator) === "zod" ? zodResolver.resolveResponseName?.(node) : null,
|
|
999
|
+
resolveResponseValidator(validator) === "zod" ? buildZodErrorParse(node, zodResolver)?.expression ?? null : null,
|
|
1000
|
+
resolveRequestValidator(validator) === "zod" && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null,
|
|
1001
|
+
resolveQueryParamsValidator(validator) === "zod" && queryParams.length > 0 ? zodResolver.resolveQueryParamsName?.(node, queryParams[0]) : null
|
|
832
1002
|
].filter((n) => Boolean(n));
|
|
833
1003
|
}
|
|
834
1004
|
/**
|
|
@@ -837,11 +1007,11 @@ function resolveZodImportNames(node, zodResolver, parser) {
|
|
|
837
1007
|
*/
|
|
838
1008
|
function buildControllers(nodes, ctx) {
|
|
839
1009
|
const { driver, resolver, root } = ctx;
|
|
840
|
-
const { output, group,
|
|
1010
|
+
const { output, group, validator } = ctx.options;
|
|
841
1011
|
const pluginTs = driver.getPlugin(pluginTsName);
|
|
842
1012
|
const tsResolver = driver.getResolver(pluginTsName);
|
|
843
1013
|
const tsPluginOptions = pluginTs.options;
|
|
844
|
-
const pluginZod =
|
|
1014
|
+
const pluginZod = isValidatorEnabled(validator) ? driver.getPlugin(pluginZodName) : null;
|
|
845
1015
|
const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null;
|
|
846
1016
|
const document = ctx.adapter.document;
|
|
847
1017
|
function buildOperationData(node) {
|
|
@@ -928,7 +1098,7 @@ function createSdkGenerator() {
|
|
|
928
1098
|
renderer: jsxRenderer,
|
|
929
1099
|
operations(nodes, ctx) {
|
|
930
1100
|
const { config, resolver, root } = ctx;
|
|
931
|
-
const { output, group,
|
|
1101
|
+
const { output, group, validator, sdk } = ctx.options;
|
|
932
1102
|
if (!ctx.driver.getPlugin(pluginTsName) || !sdk) return null;
|
|
933
1103
|
const controllers = buildControllers(nodes, ctx);
|
|
934
1104
|
const clientPath = path.resolve(root, ".kubb/client.ts");
|
|
@@ -953,9 +1123,9 @@ function createSdkGenerator() {
|
|
|
953
1123
|
file: op.typeFile,
|
|
954
1124
|
names: resolveTypeImportNames(op.node, op.tsResolver)
|
|
955
1125
|
}));
|
|
956
|
-
const { namesByPath: zodNamesByPath, filesByPath: zodFilesByPath } =
|
|
1126
|
+
const { namesByPath: zodNamesByPath, filesByPath: zodFilesByPath } = isValidatorEnabled(validator) ? collectImportsByFile(ops, (op) => ({
|
|
957
1127
|
file: op.zodFile,
|
|
958
|
-
names: op.zodResolver ? resolveZodImportNames(op.node, op.zodResolver,
|
|
1128
|
+
names: op.zodResolver ? resolveZodImportNames(op.node, op.zodResolver, validator) : []
|
|
959
1129
|
})) : {
|
|
960
1130
|
namesByPath: /* @__PURE__ */ new Map(),
|
|
961
1131
|
filesByPath: /* @__PURE__ */ new Map()
|
|
@@ -983,7 +1153,7 @@ function createSdkGenerator() {
|
|
|
983
1153
|
path: clientPath,
|
|
984
1154
|
isTypeOnly: true
|
|
985
1155
|
}),
|
|
986
|
-
|
|
1156
|
+
validator === "zod" && ops.some((op) => op.node.requestBody?.content?.[0]?.schema != null) && /* @__PURE__ */ jsx(File.Import, {
|
|
987
1157
|
name: ["z"],
|
|
988
1158
|
path: "zod",
|
|
989
1159
|
isTypeOnly: true
|
|
@@ -994,7 +1164,7 @@ function createSdkGenerator() {
|
|
|
994
1164
|
path: typeFilesByPath.get(filePath).path,
|
|
995
1165
|
isTypeOnly: true
|
|
996
1166
|
}, filePath)),
|
|
997
|
-
|
|
1167
|
+
isValidatorEnabled(validator) && Array.from(zodNamesByPath.entries()).map(([filePath, set]) => /* @__PURE__ */ jsx(File.Import, {
|
|
998
1168
|
name: Array.from(set),
|
|
999
1169
|
root: file.path,
|
|
1000
1170
|
path: zodFilesByPath.get(filePath).path
|
|
@@ -1002,7 +1172,7 @@ function createSdkGenerator() {
|
|
|
1002
1172
|
/* @__PURE__ */ jsx(SdkClient, {
|
|
1003
1173
|
name: className,
|
|
1004
1174
|
operations: ops,
|
|
1005
|
-
|
|
1175
|
+
validator
|
|
1006
1176
|
})
|
|
1007
1177
|
]
|
|
1008
1178
|
}, file.path);
|
|
@@ -1113,18 +1283,18 @@ const clientGenerator = defineGenerator({
|
|
|
1113
1283
|
operation(node, ctx) {
|
|
1114
1284
|
if (!ast.isHttpOperationNode(node)) return null;
|
|
1115
1285
|
const { config, driver, resolver, root } = ctx;
|
|
1116
|
-
const { output,
|
|
1286
|
+
const { output, validator, group } = ctx.options;
|
|
1117
1287
|
const pluginTs = driver.getPlugin(pluginTsName);
|
|
1118
1288
|
if (!pluginTs) return null;
|
|
1119
1289
|
const tsResolver = driver.getResolver(pluginTsName);
|
|
1120
|
-
const pluginZod =
|
|
1290
|
+
const pluginZod = resolveResponseValidator(validator) === "zod" || resolveRequestValidator(validator) === "zod" ? driver.getPlugin(pluginZodName) : null;
|
|
1121
1291
|
const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null;
|
|
1122
1292
|
const hasRequestBody = Boolean(node.requestBody?.content?.[0]?.schema);
|
|
1123
1293
|
const importedTypeNames = [tsResolver.resolveRequestConfigName(node), tsResolver.resolveResponsesName(node)];
|
|
1124
1294
|
const importedZodNames = zodResolver ? [
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1295
|
+
resolveResponseValidator(validator) === "zod" ? zodResolver.resolveResponseName?.(node) : null,
|
|
1296
|
+
resolveResponseValidator(validator) === "zod" ? buildZodErrorParse(node, zodResolver)?.expression ?? null : null,
|
|
1297
|
+
resolveRequestValidator(validator) === "zod" && hasRequestBody ? zodResolver.resolveDataName?.(node) : null
|
|
1128
1298
|
].filter((name) => Boolean(name)) : [];
|
|
1129
1299
|
const meta = {
|
|
1130
1300
|
name: resolver.resolveName(node.operationId),
|
|
@@ -1150,6 +1320,7 @@ const clientGenerator = defineGenerator({
|
|
|
1150
1320
|
path: node.path
|
|
1151
1321
|
});
|
|
1152
1322
|
const clientPath = path.resolve(root, ".kubb/client.ts");
|
|
1323
|
+
const eventStream = isEventStream(node);
|
|
1153
1324
|
return /* @__PURE__ */ jsxs(File, {
|
|
1154
1325
|
baseName: meta.file.baseName,
|
|
1155
1326
|
path: meta.file.path,
|
|
@@ -1172,12 +1343,16 @@ const clientGenerator = defineGenerator({
|
|
|
1172
1343
|
}),
|
|
1173
1344
|
children: [
|
|
1174
1345
|
/* @__PURE__ */ jsx(File.Import, {
|
|
1175
|
-
name: ["client"],
|
|
1346
|
+
name: eventStream ? ["client", "toEventStream"] : ["client"],
|
|
1176
1347
|
root: meta.file.path,
|
|
1177
1348
|
path: clientPath
|
|
1178
1349
|
}),
|
|
1179
1350
|
/* @__PURE__ */ jsx(File.Import, {
|
|
1180
|
-
name: [
|
|
1351
|
+
name: eventStream ? [
|
|
1352
|
+
"Options",
|
|
1353
|
+
"EventStreamResult",
|
|
1354
|
+
"SuccessOf"
|
|
1355
|
+
] : ["Options", "RequestResult"],
|
|
1181
1356
|
root: meta.file.path,
|
|
1182
1357
|
path: clientPath,
|
|
1183
1358
|
isTypeOnly: true
|
|
@@ -1198,7 +1373,7 @@ const clientGenerator = defineGenerator({
|
|
|
1198
1373
|
node,
|
|
1199
1374
|
tsResolver,
|
|
1200
1375
|
zodResolver,
|
|
1201
|
-
|
|
1376
|
+
validator,
|
|
1202
1377
|
security
|
|
1203
1378
|
})
|
|
1204
1379
|
]
|
|
@@ -1207,12 +1382,15 @@ const clientGenerator = defineGenerator({
|
|
|
1207
1382
|
});
|
|
1208
1383
|
//#endregion
|
|
1209
1384
|
//#region src/templates.ts
|
|
1385
|
+
/** Absolute path to the fetch client template, copied into `.kubb/client.ts`. */
|
|
1386
|
+
const fetchClientTemplatePath = fileURLToPath(new URL("../templates/fetch.ts", import.meta.url));
|
|
1387
|
+
/** Absolute path to the fetch serializers template, copied into `.kubb/serializers.ts`. */
|
|
1388
|
+
const fetchSerializersTemplatePath = fileURLToPath(new URL("../templates/serializers.ts", import.meta.url));
|
|
1210
1389
|
/**
|
|
1211
|
-
* Absolute path to the
|
|
1212
|
-
*
|
|
1213
|
-
* field to emit the runtime into the generated `.kubb/client.ts` verbatim.
|
|
1390
|
+
* Absolute path to the Standard Schema runtime template. Pass it to a file node's `copy` field to
|
|
1391
|
+
* emit the helper into the generated `.kubb/standardSchema.ts` verbatim.
|
|
1214
1392
|
*/
|
|
1215
|
-
const
|
|
1393
|
+
const standardSchemaTemplatePath = fileURLToPath(new URL("../templates/standardSchema.ts", import.meta.url));
|
|
1216
1394
|
//#endregion
|
|
1217
1395
|
//#region src/plugin.ts
|
|
1218
1396
|
/**
|
|
@@ -1246,7 +1424,7 @@ const pluginFetch = definePlugin((options) => {
|
|
|
1246
1424
|
const { output = {
|
|
1247
1425
|
path: "clients",
|
|
1248
1426
|
barrel: { type: "named" }
|
|
1249
|
-
}, exclude = [], include, override = [], baseURL,
|
|
1427
|
+
}, exclude = [], include, override = [], baseURL, validator = false, group, sdk, resolver: userResolver } = options;
|
|
1250
1428
|
const resolved = {
|
|
1251
1429
|
output,
|
|
1252
1430
|
exclude,
|
|
@@ -1254,7 +1432,7 @@ const pluginFetch = definePlugin((options) => {
|
|
|
1254
1432
|
override,
|
|
1255
1433
|
group: createGroupConfig(group),
|
|
1256
1434
|
baseURL,
|
|
1257
|
-
|
|
1435
|
+
validator,
|
|
1258
1436
|
sdk: sdk ? {
|
|
1259
1437
|
mode: sdk.mode ?? "tag",
|
|
1260
1438
|
name: sdk.name
|
|
@@ -1268,19 +1446,29 @@ const pluginFetch = definePlugin((options) => {
|
|
|
1268
1446
|
return {
|
|
1269
1447
|
name: pluginFetchName,
|
|
1270
1448
|
options,
|
|
1271
|
-
dependencies: [pluginTsName,
|
|
1449
|
+
dependencies: [pluginTsName, isValidatorEnabled(resolved.validator) ? pluginZodName : null].filter((dependency) => Boolean(dependency)),
|
|
1272
1450
|
hooks: { "kubb:plugin:setup"(ctx) {
|
|
1273
1451
|
ctx.setOptions(resolved);
|
|
1274
1452
|
ctx.setResolver(resolved.resolver);
|
|
1275
1453
|
ctx.setMacros([...defaultMacros, ...options.macros ?? []]);
|
|
1276
1454
|
ctx.addGenerator(...selectedGenerators);
|
|
1277
1455
|
const root = path.resolve(ctx.config.root, ctx.config.output.path);
|
|
1456
|
+
ctx.injectFile({
|
|
1457
|
+
baseName: "serializers.ts",
|
|
1458
|
+
path: path.resolve(root, ".kubb/serializers.ts"),
|
|
1459
|
+
copy: fetchSerializersTemplatePath
|
|
1460
|
+
});
|
|
1278
1461
|
ctx.injectFile({
|
|
1279
1462
|
baseName: "client.ts",
|
|
1280
1463
|
path: path.resolve(root, ".kubb/client.ts"),
|
|
1281
1464
|
copy: fetchClientTemplatePath,
|
|
1282
1465
|
footer: baseURL ? `client.setConfig({ baseURL: ${JSON.stringify(baseURL)} })` : void 0
|
|
1283
1466
|
});
|
|
1467
|
+
ctx.injectFile({
|
|
1468
|
+
baseName: "standardSchema.ts",
|
|
1469
|
+
path: path.resolve(root, ".kubb/standardSchema.ts"),
|
|
1470
|
+
copy: standardSchemaTemplatePath
|
|
1471
|
+
});
|
|
1284
1472
|
} }
|
|
1285
1473
|
};
|
|
1286
1474
|
});
|