@kubb/plugin-fetch 5.0.0-beta.76 → 5.0.0-beta.79
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 +242 -87
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +242 -87
- 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 +311 -170
- 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,55 @@ 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
|
+
}
|
|
367
433
|
function buildOperationComments(node, options = {}) {
|
|
368
434
|
const { link = "pathTemplate", linkPosition = "afterDeprecated", splitLines = false } = options;
|
|
369
435
|
const linkComment = getOperationLink(node, link);
|
|
@@ -384,10 +450,10 @@ function buildOperationComments(node, options = {}) {
|
|
|
384
450
|
function getOperationParameters(node, options = {}) {
|
|
385
451
|
const params = caseParams(node.parameters, options.paramsCasing === "original" ? void 0 : "camelcase");
|
|
386
452
|
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")
|
|
453
|
+
path: dedupeByCasedName(params.filter((param) => param.in === "path")),
|
|
454
|
+
query: dedupeByCasedName(params.filter((param) => param.in === "query")),
|
|
455
|
+
header: dedupeByCasedName(params.filter((param) => param.in === "header")),
|
|
456
|
+
cookie: dedupeByCasedName(params.filter((param) => param.in === "cookie"))
|
|
391
457
|
};
|
|
392
458
|
}
|
|
393
459
|
function getStatusCodeNumber(statusCode) {
|
|
@@ -398,6 +464,15 @@ function isSuccessStatusCode(statusCode) {
|
|
|
398
464
|
const code = getStatusCodeNumber(statusCode);
|
|
399
465
|
return code !== null && code >= 200 && code < 300;
|
|
400
466
|
}
|
|
467
|
+
function getSuccessResponses(responses) {
|
|
468
|
+
return responses.filter((response) => isSuccessStatusCode(response.statusCode));
|
|
469
|
+
}
|
|
470
|
+
function getOperationSuccessResponses(node) {
|
|
471
|
+
return getSuccessResponses(node.responses);
|
|
472
|
+
}
|
|
473
|
+
function getPrimarySuccessResponse(node) {
|
|
474
|
+
return getOperationSuccessResponses(node)[0] ?? null;
|
|
475
|
+
}
|
|
401
476
|
//#endregion
|
|
402
477
|
//#region ../../internals/shared/src/group.ts
|
|
403
478
|
/**
|
|
@@ -430,39 +505,39 @@ function createGroupConfig(group) {
|
|
|
430
505
|
};
|
|
431
506
|
}
|
|
432
507
|
//#endregion
|
|
433
|
-
//#region ../../internals/client/src/builders/
|
|
508
|
+
//#region ../../internals/client/src/builders/validatorOptions.ts
|
|
434
509
|
/**
|
|
435
|
-
* Returns `true` when any direction of the
|
|
510
|
+
* Returns `true` when any direction of the validator uses zod (used for dependency checks).
|
|
436
511
|
*/
|
|
437
|
-
function
|
|
438
|
-
if (!
|
|
439
|
-
if (
|
|
440
|
-
return Boolean(
|
|
512
|
+
function isValidatorEnabled(validator) {
|
|
513
|
+
if (!validator) return false;
|
|
514
|
+
if (validator === "zod") return true;
|
|
515
|
+
return Boolean(validator.request || validator.response);
|
|
441
516
|
}
|
|
442
517
|
/**
|
|
443
518
|
* Returns `'zod'` when request body parsing is enabled, `null` otherwise. The string shorthand
|
|
444
519
|
* `'zod'` validates the response only, so it does not enable request parsing.
|
|
445
520
|
*/
|
|
446
|
-
function
|
|
447
|
-
if (!
|
|
448
|
-
return
|
|
521
|
+
function resolveRequestValidator(validator) {
|
|
522
|
+
if (!validator || validator === "zod") return null;
|
|
523
|
+
return validator.request ?? null;
|
|
449
524
|
}
|
|
450
525
|
/**
|
|
451
526
|
* Returns `'zod'` when query-parameters parsing is enabled, `null` otherwise. Only the object form
|
|
452
527
|
* `{ request: 'zod' }` enables it.
|
|
453
528
|
*/
|
|
454
|
-
function
|
|
455
|
-
if (!
|
|
456
|
-
return
|
|
529
|
+
function resolveQueryParamsValidator(validator) {
|
|
530
|
+
if (!validator || validator === "zod") return null;
|
|
531
|
+
return validator.request ?? null;
|
|
457
532
|
}
|
|
458
533
|
/**
|
|
459
534
|
* Returns `'zod'` when response parsing is enabled, `null` otherwise. The string shorthand `'zod'`
|
|
460
535
|
* maps to response parsing.
|
|
461
536
|
*/
|
|
462
|
-
function
|
|
463
|
-
if (!
|
|
464
|
-
if (
|
|
465
|
-
return
|
|
537
|
+
function resolveResponseValidator(validator) {
|
|
538
|
+
if (!validator) return null;
|
|
539
|
+
if (validator === "zod") return "zod";
|
|
540
|
+
return validator.response ?? null;
|
|
466
541
|
}
|
|
467
542
|
/**
|
|
468
543
|
* Resolves the zod expression a generated client validates a success response with. Only success
|
|
@@ -616,23 +691,74 @@ function buildGroupedOptionsSignature({ node, tsResolver }) {
|
|
|
616
691
|
};
|
|
617
692
|
}
|
|
618
693
|
//#endregion
|
|
694
|
+
//#region ../../internals/client/src/builders/styles.ts
|
|
695
|
+
/**
|
|
696
|
+
* Renders a parameter name as an object-literal key, quoting it when the camelCased name is not a
|
|
697
|
+
* bare identifier (for example a name that starts with a digit) so the emitted literal stays valid.
|
|
698
|
+
*/
|
|
699
|
+
function toKey(name) {
|
|
700
|
+
const cased = camelCase(name);
|
|
701
|
+
return isValidVarName(cased) ? cased : JSON.stringify(cased);
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Serializes one parameter's metadata into a `{ style, explode }` literal, or `null` when the
|
|
705
|
+
* parameter carries neither. Path and query carry the serialization `style`; header and cookie use a
|
|
706
|
+
* fixed style (`simple` and `form`), so only `explode` is emitted for them.
|
|
707
|
+
*/
|
|
708
|
+
function serializeParameter(parameter) {
|
|
709
|
+
const parts = [];
|
|
710
|
+
if ((parameter.in === "path" || parameter.in === "query") && parameter.style) parts.push(`style: '${parameter.style}'`);
|
|
711
|
+
if (parameter.explode !== void 0) parts.push(`explode: ${parameter.explode}`);
|
|
712
|
+
return parts.length > 0 ? `{ ${parts.join(", ")} }` : null;
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Builds the per-operation `styles` literal from the operation's parameters, grouped by location and
|
|
716
|
+
* keyed by the camelCased parameter name to match the generated `path` / `query` / `headers` keys.
|
|
717
|
+
* Only parameters whose source defines `style` or `explode` are emitted, so calls without
|
|
718
|
+
* serialization metadata keep the runtime defaults and existing output is unchanged. Returns `null`
|
|
719
|
+
* when no parameter carries metadata.
|
|
720
|
+
*
|
|
721
|
+
* @example
|
|
722
|
+
* ```ts
|
|
723
|
+
* // a path param with { style: 'matrix', explode: true } and a query param with { explode: false }
|
|
724
|
+
* buildStyles({ node }) // "{ path: { id: { style: 'matrix', explode: true } }, query: { tags: { explode: false } } }"
|
|
725
|
+
* ```
|
|
726
|
+
*/
|
|
727
|
+
function buildStyles({ node }) {
|
|
728
|
+
if (!ast.isHttpOperationNode(node)) return null;
|
|
729
|
+
const groups = {
|
|
730
|
+
path: [],
|
|
731
|
+
query: [],
|
|
732
|
+
header: [],
|
|
733
|
+
cookie: []
|
|
734
|
+
};
|
|
735
|
+
for (const parameter of node.parameters) {
|
|
736
|
+
const literal = serializeParameter(parameter);
|
|
737
|
+
if (!literal) continue;
|
|
738
|
+
groups[parameter.in].push(`${toKey(parameter.name)}: ${literal}`);
|
|
739
|
+
}
|
|
740
|
+
const locations = Object.keys(groups).filter((location) => groups[location].length > 0);
|
|
741
|
+
if (locations.length === 0) return null;
|
|
742
|
+
return `{ ${locations.map((location) => `${location}: { ${groups[location].join(", ")} }`).join(", ")} }`;
|
|
743
|
+
}
|
|
744
|
+
//#endregion
|
|
619
745
|
//#region ../../internals/client/src/builders/validator.ts
|
|
620
746
|
/**
|
|
621
|
-
* Builds the
|
|
622
|
-
* response
|
|
623
|
-
*
|
|
747
|
+
* Builds the validator-hook references for one operation. Request validation runs before the send;
|
|
748
|
+
* response validation runs on the success body only. Returns `null` references when the matching
|
|
749
|
+
* direction is disabled or the schema is absent.
|
|
624
750
|
*/
|
|
625
|
-
function
|
|
751
|
+
function buildValidatorHooks({ node, validator, zodResolver }) {
|
|
626
752
|
const importedZodNames = [];
|
|
627
753
|
const hasRequestBody = Boolean(node.requestBody?.content?.[0]?.schema);
|
|
628
|
-
const zodRequestName = zodResolver &&
|
|
629
|
-
const request = zodRequestName
|
|
754
|
+
const zodRequestName = zodResolver && resolveRequestValidator(validator) === "zod" && hasRequestBody ? zodResolver.resolveDataName?.(node) : null;
|
|
755
|
+
const request = zodRequestName ?? null;
|
|
630
756
|
if (zodRequestName) importedZodNames.push(zodRequestName);
|
|
631
|
-
const responseParse = zodResolver &&
|
|
632
|
-
const response = responseParse ?
|
|
757
|
+
const responseParse = zodResolver && resolveResponseValidator(validator) === "zod" ? buildZodResponseParse(node, zodResolver) : null;
|
|
758
|
+
const response = responseParse ? responseParse.expression : null;
|
|
633
759
|
if (responseParse) importedZodNames.push(...responseParse.importNames);
|
|
634
|
-
const errorParse = zodResolver &&
|
|
635
|
-
const error = errorParse ?
|
|
760
|
+
const errorParse = zodResolver && resolveResponseValidator(validator) === "zod" ? buildZodErrorParse(node, zodResolver) : null;
|
|
761
|
+
const error = errorParse ? errorParse.expression : null;
|
|
636
762
|
if (errorParse) importedZodNames.push(...errorParse.importNames);
|
|
637
763
|
return {
|
|
638
764
|
request,
|
|
@@ -648,34 +774,49 @@ function buildParserHooks({ node, parser, zodResolver }) {
|
|
|
648
774
|
* single `options` object to the resolved client and returns the `RequestResult`. The type, signature,
|
|
649
775
|
* and call config are built with the AST factory, and only the jsx-renderer emits the source.
|
|
650
776
|
*/
|
|
651
|
-
function Operation({ name, node, tsResolver, zodResolver,
|
|
777
|
+
function Operation({ name, node, tsResolver, zodResolver, validator, security, isExportable = true, isIndexable = true }) {
|
|
652
778
|
if (!ast.isHttpOperationNode(node)) return null;
|
|
653
779
|
const signature = buildGroupedOptionsSignature({
|
|
654
780
|
node,
|
|
655
781
|
tsResolver
|
|
656
782
|
});
|
|
657
|
-
const
|
|
783
|
+
const validators = buildValidatorHooks({
|
|
658
784
|
node,
|
|
659
|
-
|
|
785
|
+
validator,
|
|
660
786
|
zodResolver
|
|
661
787
|
});
|
|
662
788
|
const securityLiteral = buildSecurityMetadata({ security });
|
|
789
|
+
const stylesLiteral = buildStyles({ node });
|
|
663
790
|
const { defaultContentType } = getContentTypeInfo(node);
|
|
664
|
-
const
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
791
|
+
const bakedRequestContentType = Boolean(node.requestBody?.content?.[0]?.schema) && defaultContentType !== "application/json" ? defaultContentType : null;
|
|
792
|
+
const mergeContentType = Boolean(bakedRequestContentType) && getResponseContentTypeInfo(node).isMultipleContentTypes;
|
|
793
|
+
const contentTypeLiteral = !bakedRequestContentType ? null : mergeContentType ? `contentType: { request: '${bakedRequestContentType}', ...(typeof contentType === 'string' ? { request: contentType } : contentType) }` : `contentType: { request: '${bakedRequestContentType}' }`;
|
|
794
|
+
const eventStream = isEventStream(node);
|
|
795
|
+
const responseType = getResponseType(node);
|
|
796
|
+
const responseTypeLiteral = responseType ? `responseType: '${responseType}'` : null;
|
|
797
|
+
const validatorEntries = [
|
|
798
|
+
validators.request ? `request: ${validators.request}` : null,
|
|
799
|
+
validators.response ? `response: ${validators.response}` : null,
|
|
800
|
+
validators.error ? `error: ${validators.error}` : null
|
|
669
801
|
].filter(Boolean);
|
|
670
|
-
const
|
|
802
|
+
const validatorLiteral = validatorEntries.length ? `validator: { ${validatorEntries.join(", ")} }` : null;
|
|
671
803
|
const callConfig = `{ ${[
|
|
672
804
|
`method: '${node.method.toUpperCase()}'`,
|
|
673
805
|
`url: '${Url.toCasedTemplate(node.path, { casing: "camelcase" })}'`,
|
|
674
806
|
securityLiteral ? `security: ${securityLiteral}` : null,
|
|
675
|
-
|
|
807
|
+
stylesLiteral ? `styles: ${stylesLiteral}` : null,
|
|
808
|
+
validatorLiteral,
|
|
676
809
|
contentTypeLiteral,
|
|
810
|
+
responseTypeLiteral,
|
|
677
811
|
"...config"
|
|
678
812
|
].filter(Boolean).join(", ")} }`;
|
|
813
|
+
const eventType = `SuccessOf<${tsResolver.resolveResponsesName(node)}>`;
|
|
814
|
+
const returnType = eventStream ? `Promise<EventStreamResult<${eventType}>>` : signature.returnType;
|
|
815
|
+
const returnStatement = eventStream ? `return toEventStream<${eventType}>(request(${callConfig}))` : buildReturnStatement({
|
|
816
|
+
node,
|
|
817
|
+
tsResolver,
|
|
818
|
+
callConfig
|
|
819
|
+
});
|
|
679
820
|
return /* @__PURE__ */ jsx(File.Source, {
|
|
680
821
|
name,
|
|
681
822
|
isExportable,
|
|
@@ -685,20 +826,16 @@ function Operation({ name, node, tsResolver, zodResolver, parser, security, isEx
|
|
|
685
826
|
export: isExportable,
|
|
686
827
|
generics: signature.generics,
|
|
687
828
|
params: signature.paramsSignature,
|
|
688
|
-
returnType
|
|
829
|
+
returnType,
|
|
689
830
|
JSDoc: { comments: buildOperationComments(node, {
|
|
690
831
|
link: "urlPath",
|
|
691
832
|
linkPosition: "beforeDeprecated",
|
|
692
833
|
splitLines: true
|
|
693
834
|
}) },
|
|
694
835
|
children: [
|
|
695
|
-
"const { client: request = client, ...config } = options",
|
|
836
|
+
mergeContentType ? "const { client: request = client, contentType, ...config } = options" : "const { client: request = client, ...config } = options",
|
|
696
837
|
/* @__PURE__ */ jsx("br", {}),
|
|
697
|
-
|
|
698
|
-
node,
|
|
699
|
-
tsResolver,
|
|
700
|
-
callConfig
|
|
701
|
-
})
|
|
838
|
+
returnStatement
|
|
702
839
|
]
|
|
703
840
|
})
|
|
704
841
|
});
|
|
@@ -707,23 +844,23 @@ function Operation({ name, node, tsResolver, zodResolver, parser, security, isEx
|
|
|
707
844
|
//#region ../../internals/client/src/builders/sdkMethod.ts
|
|
708
845
|
/**
|
|
709
846
|
* Builds the call config literal forwarded to the contract client, mirroring the shared `Operation`
|
|
710
|
-
* component: `{ method, url, security?,
|
|
847
|
+
* component: `{ method, url, security?, validator?, ...config }`. The `...config` spread carries every
|
|
711
848
|
* per-call field (including `throwOnError`), so the method stays a thin wrapper over the contract.
|
|
712
849
|
*/
|
|
713
|
-
function buildCallConfig({ node,
|
|
714
|
-
const
|
|
850
|
+
function buildCallConfig({ node, validator, zodResolver, security }) {
|
|
851
|
+
const validators = buildValidatorHooks({
|
|
715
852
|
node,
|
|
716
|
-
|
|
853
|
+
validator,
|
|
717
854
|
zodResolver
|
|
718
855
|
});
|
|
719
|
-
const
|
|
720
|
-
const
|
|
856
|
+
const validatorEntries = [validators.request ? `request: ${validators.request}` : null, validators.response ? `response: ${validators.response}` : null].filter(Boolean);
|
|
857
|
+
const validatorLiteral = validatorEntries.length ? `validator: { ${validatorEntries.join(", ")} }` : null;
|
|
721
858
|
const securityLiteral = buildSecurityMetadata({ security });
|
|
722
859
|
return `{ ${[
|
|
723
860
|
`method: '${node.method.toUpperCase()}'`,
|
|
724
861
|
`url: '${Url.toCasedTemplate(node.path, { casing: "camelcase" })}'`,
|
|
725
862
|
securityLiteral ? `security: ${securityLiteral}` : null,
|
|
726
|
-
|
|
863
|
+
validatorLiteral,
|
|
727
864
|
"...config"
|
|
728
865
|
].filter(Boolean).join(", ")} }`;
|
|
729
866
|
}
|
|
@@ -733,7 +870,7 @@ function buildCallConfig({ node, parser, zodResolver, security }) {
|
|
|
733
870
|
* returns the `RequestResult`. A per-call `options.client` still overrides the instance client, so
|
|
734
871
|
* one operation can be routed to a different environment without a new instance.
|
|
735
872
|
*/
|
|
736
|
-
function buildSdkMethod({ node, name, tsResolver, zodResolver,
|
|
873
|
+
function buildSdkMethod({ node, name, tsResolver, zodResolver, validator, security }) {
|
|
737
874
|
if (!ast.isHttpOperationNode(node)) return "";
|
|
738
875
|
const signature = buildGroupedOptionsSignature({
|
|
739
876
|
node,
|
|
@@ -744,7 +881,7 @@ function buildSdkMethod({ node, name, tsResolver, zodResolver, parser, security
|
|
|
744
881
|
tsResolver,
|
|
745
882
|
callConfig: buildCallConfig({
|
|
746
883
|
node,
|
|
747
|
-
|
|
884
|
+
validator,
|
|
748
885
|
zodResolver,
|
|
749
886
|
security
|
|
750
887
|
})
|
|
@@ -770,13 +907,13 @@ function buildSdkMethod({ node, name, tsResolver, zodResolver, parser, security
|
|
|
770
907
|
* instance: `const api = new PetClient({ baseURL }); api.getPetById(...)`. A per-call `client` option
|
|
771
908
|
* still overrides the instance client for a one-off call.
|
|
772
909
|
*/
|
|
773
|
-
function SdkClient({ name, isExportable = true, isIndexable = true, operations,
|
|
910
|
+
function SdkClient({ name, isExportable = true, isIndexable = true, operations, validator, children }) {
|
|
774
911
|
const methods = operations.map(({ node, name: methodName, tsResolver, zodResolver, security }) => buildSdkMethod({
|
|
775
912
|
node,
|
|
776
913
|
name: methodName,
|
|
777
914
|
tsResolver,
|
|
778
915
|
zodResolver,
|
|
779
|
-
|
|
916
|
+
validator,
|
|
780
917
|
security
|
|
781
918
|
}));
|
|
782
919
|
const classCode = `export class ${name} {\n${[
|
|
@@ -822,13 +959,13 @@ function SdkFacade({ name, isExportable = true, isIndexable = true, members, chi
|
|
|
822
959
|
function resolveTypeImportNames(node, tsResolver) {
|
|
823
960
|
return [tsResolver.resolveRequestConfigName(node), tsResolver.resolveResponsesName(node)];
|
|
824
961
|
}
|
|
825
|
-
function resolveZodImportNames(node, zodResolver,
|
|
962
|
+
function resolveZodImportNames(node, zodResolver, validator) {
|
|
826
963
|
const { query: queryParams } = getOperationParameters(node, { paramsCasing: "original" });
|
|
827
964
|
return [
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
965
|
+
resolveResponseValidator(validator) === "zod" ? zodResolver.resolveResponseName?.(node) : null,
|
|
966
|
+
resolveResponseValidator(validator) === "zod" ? buildZodErrorParse(node, zodResolver)?.expression ?? null : null,
|
|
967
|
+
resolveRequestValidator(validator) === "zod" && node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName?.(node) : null,
|
|
968
|
+
resolveQueryParamsValidator(validator) === "zod" && queryParams.length > 0 ? zodResolver.resolveQueryParamsName?.(node, queryParams[0]) : null
|
|
832
969
|
].filter((n) => Boolean(n));
|
|
833
970
|
}
|
|
834
971
|
/**
|
|
@@ -837,11 +974,11 @@ function resolveZodImportNames(node, zodResolver, parser) {
|
|
|
837
974
|
*/
|
|
838
975
|
function buildControllers(nodes, ctx) {
|
|
839
976
|
const { driver, resolver, root } = ctx;
|
|
840
|
-
const { output, group,
|
|
977
|
+
const { output, group, validator } = ctx.options;
|
|
841
978
|
const pluginTs = driver.getPlugin(pluginTsName);
|
|
842
979
|
const tsResolver = driver.getResolver(pluginTsName);
|
|
843
980
|
const tsPluginOptions = pluginTs.options;
|
|
844
|
-
const pluginZod =
|
|
981
|
+
const pluginZod = isValidatorEnabled(validator) ? driver.getPlugin(pluginZodName) : null;
|
|
845
982
|
const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null;
|
|
846
983
|
const document = ctx.adapter.document;
|
|
847
984
|
function buildOperationData(node) {
|
|
@@ -928,7 +1065,7 @@ function createSdkGenerator() {
|
|
|
928
1065
|
renderer: jsxRenderer,
|
|
929
1066
|
operations(nodes, ctx) {
|
|
930
1067
|
const { config, resolver, root } = ctx;
|
|
931
|
-
const { output, group,
|
|
1068
|
+
const { output, group, validator, sdk } = ctx.options;
|
|
932
1069
|
if (!ctx.driver.getPlugin(pluginTsName) || !sdk) return null;
|
|
933
1070
|
const controllers = buildControllers(nodes, ctx);
|
|
934
1071
|
const clientPath = path.resolve(root, ".kubb/client.ts");
|
|
@@ -953,9 +1090,9 @@ function createSdkGenerator() {
|
|
|
953
1090
|
file: op.typeFile,
|
|
954
1091
|
names: resolveTypeImportNames(op.node, op.tsResolver)
|
|
955
1092
|
}));
|
|
956
|
-
const { namesByPath: zodNamesByPath, filesByPath: zodFilesByPath } =
|
|
1093
|
+
const { namesByPath: zodNamesByPath, filesByPath: zodFilesByPath } = isValidatorEnabled(validator) ? collectImportsByFile(ops, (op) => ({
|
|
957
1094
|
file: op.zodFile,
|
|
958
|
-
names: op.zodResolver ? resolveZodImportNames(op.node, op.zodResolver,
|
|
1095
|
+
names: op.zodResolver ? resolveZodImportNames(op.node, op.zodResolver, validator) : []
|
|
959
1096
|
})) : {
|
|
960
1097
|
namesByPath: /* @__PURE__ */ new Map(),
|
|
961
1098
|
filesByPath: /* @__PURE__ */ new Map()
|
|
@@ -983,7 +1120,7 @@ function createSdkGenerator() {
|
|
|
983
1120
|
path: clientPath,
|
|
984
1121
|
isTypeOnly: true
|
|
985
1122
|
}),
|
|
986
|
-
|
|
1123
|
+
validator === "zod" && ops.some((op) => op.node.requestBody?.content?.[0]?.schema != null) && /* @__PURE__ */ jsx(File.Import, {
|
|
987
1124
|
name: ["z"],
|
|
988
1125
|
path: "zod",
|
|
989
1126
|
isTypeOnly: true
|
|
@@ -994,7 +1131,7 @@ function createSdkGenerator() {
|
|
|
994
1131
|
path: typeFilesByPath.get(filePath).path,
|
|
995
1132
|
isTypeOnly: true
|
|
996
1133
|
}, filePath)),
|
|
997
|
-
|
|
1134
|
+
isValidatorEnabled(validator) && Array.from(zodNamesByPath.entries()).map(([filePath, set]) => /* @__PURE__ */ jsx(File.Import, {
|
|
998
1135
|
name: Array.from(set),
|
|
999
1136
|
root: file.path,
|
|
1000
1137
|
path: zodFilesByPath.get(filePath).path
|
|
@@ -1002,7 +1139,7 @@ function createSdkGenerator() {
|
|
|
1002
1139
|
/* @__PURE__ */ jsx(SdkClient, {
|
|
1003
1140
|
name: className,
|
|
1004
1141
|
operations: ops,
|
|
1005
|
-
|
|
1142
|
+
validator
|
|
1006
1143
|
})
|
|
1007
1144
|
]
|
|
1008
1145
|
}, file.path);
|
|
@@ -1113,18 +1250,18 @@ const clientGenerator = defineGenerator({
|
|
|
1113
1250
|
operation(node, ctx) {
|
|
1114
1251
|
if (!ast.isHttpOperationNode(node)) return null;
|
|
1115
1252
|
const { config, driver, resolver, root } = ctx;
|
|
1116
|
-
const { output,
|
|
1253
|
+
const { output, validator, group } = ctx.options;
|
|
1117
1254
|
const pluginTs = driver.getPlugin(pluginTsName);
|
|
1118
1255
|
if (!pluginTs) return null;
|
|
1119
1256
|
const tsResolver = driver.getResolver(pluginTsName);
|
|
1120
|
-
const pluginZod =
|
|
1257
|
+
const pluginZod = resolveResponseValidator(validator) === "zod" || resolveRequestValidator(validator) === "zod" ? driver.getPlugin(pluginZodName) : null;
|
|
1121
1258
|
const zodResolver = pluginZod ? driver.getResolver(pluginZodName) : null;
|
|
1122
1259
|
const hasRequestBody = Boolean(node.requestBody?.content?.[0]?.schema);
|
|
1123
1260
|
const importedTypeNames = [tsResolver.resolveRequestConfigName(node), tsResolver.resolveResponsesName(node)];
|
|
1124
1261
|
const importedZodNames = zodResolver ? [
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1262
|
+
resolveResponseValidator(validator) === "zod" ? zodResolver.resolveResponseName?.(node) : null,
|
|
1263
|
+
resolveResponseValidator(validator) === "zod" ? buildZodErrorParse(node, zodResolver)?.expression ?? null : null,
|
|
1264
|
+
resolveRequestValidator(validator) === "zod" && hasRequestBody ? zodResolver.resolveDataName?.(node) : null
|
|
1128
1265
|
].filter((name) => Boolean(name)) : [];
|
|
1129
1266
|
const meta = {
|
|
1130
1267
|
name: resolver.resolveName(node.operationId),
|
|
@@ -1150,6 +1287,7 @@ const clientGenerator = defineGenerator({
|
|
|
1150
1287
|
path: node.path
|
|
1151
1288
|
});
|
|
1152
1289
|
const clientPath = path.resolve(root, ".kubb/client.ts");
|
|
1290
|
+
const eventStream = isEventStream(node);
|
|
1153
1291
|
return /* @__PURE__ */ jsxs(File, {
|
|
1154
1292
|
baseName: meta.file.baseName,
|
|
1155
1293
|
path: meta.file.path,
|
|
@@ -1172,12 +1310,16 @@ const clientGenerator = defineGenerator({
|
|
|
1172
1310
|
}),
|
|
1173
1311
|
children: [
|
|
1174
1312
|
/* @__PURE__ */ jsx(File.Import, {
|
|
1175
|
-
name: ["client"],
|
|
1313
|
+
name: eventStream ? ["client", "toEventStream"] : ["client"],
|
|
1176
1314
|
root: meta.file.path,
|
|
1177
1315
|
path: clientPath
|
|
1178
1316
|
}),
|
|
1179
1317
|
/* @__PURE__ */ jsx(File.Import, {
|
|
1180
|
-
name: [
|
|
1318
|
+
name: eventStream ? [
|
|
1319
|
+
"Options",
|
|
1320
|
+
"EventStreamResult",
|
|
1321
|
+
"SuccessOf"
|
|
1322
|
+
] : ["Options", "RequestResult"],
|
|
1181
1323
|
root: meta.file.path,
|
|
1182
1324
|
path: clientPath,
|
|
1183
1325
|
isTypeOnly: true
|
|
@@ -1198,7 +1340,7 @@ const clientGenerator = defineGenerator({
|
|
|
1198
1340
|
node,
|
|
1199
1341
|
tsResolver,
|
|
1200
1342
|
zodResolver,
|
|
1201
|
-
|
|
1343
|
+
validator,
|
|
1202
1344
|
security
|
|
1203
1345
|
})
|
|
1204
1346
|
]
|
|
@@ -1207,12 +1349,15 @@ const clientGenerator = defineGenerator({
|
|
|
1207
1349
|
});
|
|
1208
1350
|
//#endregion
|
|
1209
1351
|
//#region src/templates.ts
|
|
1352
|
+
/** Absolute path to the fetch client template, copied into `.kubb/client.ts`. */
|
|
1353
|
+
const fetchClientTemplatePath = fileURLToPath(new URL("../templates/fetch.ts", import.meta.url));
|
|
1354
|
+
/** Absolute path to the fetch serializers template, copied into `.kubb/serializers.ts`. */
|
|
1355
|
+
const fetchSerializersTemplatePath = fileURLToPath(new URL("../templates/serializers.ts", import.meta.url));
|
|
1210
1356
|
/**
|
|
1211
|
-
* Absolute path to the
|
|
1212
|
-
*
|
|
1213
|
-
* field to emit the runtime into the generated `.kubb/client.ts` verbatim.
|
|
1357
|
+
* Absolute path to the Standard Schema runtime template. Pass it to a file node's `copy` field to
|
|
1358
|
+
* emit the helper into the generated `.kubb/standardSchema.ts` verbatim.
|
|
1214
1359
|
*/
|
|
1215
|
-
const
|
|
1360
|
+
const standardSchemaTemplatePath = fileURLToPath(new URL("../templates/standardSchema.ts", import.meta.url));
|
|
1216
1361
|
//#endregion
|
|
1217
1362
|
//#region src/plugin.ts
|
|
1218
1363
|
/**
|
|
@@ -1246,7 +1391,7 @@ const pluginFetch = definePlugin((options) => {
|
|
|
1246
1391
|
const { output = {
|
|
1247
1392
|
path: "clients",
|
|
1248
1393
|
barrel: { type: "named" }
|
|
1249
|
-
}, exclude = [], include, override = [], baseURL,
|
|
1394
|
+
}, exclude = [], include, override = [], baseURL, validator = false, group, sdk, resolver: userResolver } = options;
|
|
1250
1395
|
const resolved = {
|
|
1251
1396
|
output,
|
|
1252
1397
|
exclude,
|
|
@@ -1254,7 +1399,7 @@ const pluginFetch = definePlugin((options) => {
|
|
|
1254
1399
|
override,
|
|
1255
1400
|
group: createGroupConfig(group),
|
|
1256
1401
|
baseURL,
|
|
1257
|
-
|
|
1402
|
+
validator,
|
|
1258
1403
|
sdk: sdk ? {
|
|
1259
1404
|
mode: sdk.mode ?? "tag",
|
|
1260
1405
|
name: sdk.name
|
|
@@ -1268,19 +1413,29 @@ const pluginFetch = definePlugin((options) => {
|
|
|
1268
1413
|
return {
|
|
1269
1414
|
name: pluginFetchName,
|
|
1270
1415
|
options,
|
|
1271
|
-
dependencies: [pluginTsName,
|
|
1416
|
+
dependencies: [pluginTsName, isValidatorEnabled(resolved.validator) ? pluginZodName : null].filter((dependency) => Boolean(dependency)),
|
|
1272
1417
|
hooks: { "kubb:plugin:setup"(ctx) {
|
|
1273
1418
|
ctx.setOptions(resolved);
|
|
1274
1419
|
ctx.setResolver(resolved.resolver);
|
|
1275
1420
|
ctx.setMacros([...defaultMacros, ...options.macros ?? []]);
|
|
1276
1421
|
ctx.addGenerator(...selectedGenerators);
|
|
1277
1422
|
const root = path.resolve(ctx.config.root, ctx.config.output.path);
|
|
1423
|
+
ctx.injectFile({
|
|
1424
|
+
baseName: "serializers.ts",
|
|
1425
|
+
path: path.resolve(root, ".kubb/serializers.ts"),
|
|
1426
|
+
copy: fetchSerializersTemplatePath
|
|
1427
|
+
});
|
|
1278
1428
|
ctx.injectFile({
|
|
1279
1429
|
baseName: "client.ts",
|
|
1280
1430
|
path: path.resolve(root, ".kubb/client.ts"),
|
|
1281
1431
|
copy: fetchClientTemplatePath,
|
|
1282
1432
|
footer: baseURL ? `client.setConfig({ baseURL: ${JSON.stringify(baseURL)} })` : void 0
|
|
1283
1433
|
});
|
|
1434
|
+
ctx.injectFile({
|
|
1435
|
+
baseName: "standardSchema.ts",
|
|
1436
|
+
path: path.resolve(root, ".kubb/standardSchema.ts"),
|
|
1437
|
+
copy: standardSchemaTemplatePath
|
|
1438
|
+
});
|
|
1284
1439
|
} }
|
|
1285
1440
|
};
|
|
1286
1441
|
});
|