Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@kubb/plugin-ts

Package Overview
Dependencies
Maintainers
1
Versions
661
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@kubb/plugin-ts - npm Package Compare versions

Comparing version
5.0.0-beta.30
to
5.0.0-beta.31
+124
-61
dist/index.js

@@ -807,2 +807,50 @@ import { t as __name } from "./chunk--u3MIqq1.js";

//#region ../../internals/shared/src/operation.ts
/**
* Maps a content type to the PascalCase suffix used to name per-content-type variants
* (e.g. `application/json` → `Json`, `application/xml` → `Xml`, `multipart/form-data` → `FormData`).
*/
function getContentTypeSuffix(contentType) {
const baseType = contentType.split(";")[0].trim();
if (baseType === "application/json") return "Json";
if (baseType === "multipart/form-data") return "FormData";
if (baseType === "application/x-www-form-urlencoded") return "FormUrlEncoded";
const parts = (baseType.split("/").pop() ?? baseType).split(/[^a-zA-Z0-9]+/).filter(Boolean);
if (parts.length === 0) return "Unknown";
return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
}
/**
* Appends a content-type suffix to a base name, keeping a trailing `Data` segment last
* (e.g. `AddPetData` + `Json` → `AddPetJsonData`, `AddPetStatus200` + `Xml` → `AddPetStatus200Xml`).
*/
function getPerContentTypeName(baseName, suffix) {
if (baseName.endsWith("Data")) return suffix.endsWith("Data") ? baseName.slice(0, -4) + suffix : `${baseName.slice(0, -4)}${suffix}Data`;
return baseName + suffix;
}
/**
* Resolves per-content-type variant names for a set of content entries, deduplicating suffix
* collisions with a numeric counter. Entries without a schema are skipped. The returned `suffix` is
* the final (possibly counter-augmented) value, so callers can derive parallel names in another
* namespace (e.g. plugin-faker deriving the matching plugin-ts type name).
*/
function resolveContentTypeVariants(entries, baseName) {
const usedNames = /* @__PURE__ */ new Set();
return entries.filter((entry) => entry.schema).map((entry) => {
const baseSuffix = getContentTypeSuffix(entry.contentType);
let suffix = baseSuffix;
let name = getPerContentTypeName(baseName, suffix);
let counter = 2;
while (usedNames.has(name)) {
suffix = `${baseSuffix}${counter++}`;
name = getPerContentTypeName(baseName, suffix);
}
usedNames.add(name);
return {
name,
suffix,
schema: entry.schema,
keysToOmit: entry.keysToOmit,
contentType: entry.contentType
};
});
}
function getOperationParameters(node, options = {}) {

@@ -818,2 +866,35 @@ const params = ast.caseParams(node.parameters, options.paramsCasing);

//#endregion
//#region ../../internals/shared/src/group.ts
/**
* Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
* shared default naming so every plugin groups output consistently:
*
* - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
* - other groups use `${camelCase(group)}${suffix}` (e.g. `petController`).
*
* Returns `null` when grouping is disabled, matching the per-plugin convention.
*
* @param group - The user-supplied group option, or `undefined` to disable grouping.
* @param options.suffix - Appended to non-`path` group names, e.g. `'Controller'` or `'Requests'`.
* @param options.honorName - When `true`, a user-provided `group.name` overrides the default namer.
*
* @example
* ```ts
* createGroupConfig(group, { suffix: 'Controller' }) // plugin-ts, plugin-zod
* createGroupConfig(group, { suffix: 'Controller', honorName: true }) // plugin-faker, plugin-client, …
* createGroupConfig(group, { suffix: 'Requests', honorName: true }) // plugin-cypress, plugin-mcp
* ```
*/
function createGroupConfig(group, options) {
if (!group) return null;
const defaultName = (ctx) => {
if (group.type === "path") return `${ctx.group.split("/")[1]}`;
return `${camelCase(ctx.group)}${options.suffix}`;
};
return {
...group,
name: options.honorName && group.name ? group.name : defaultName
};
}
//#endregion
//#region src/utils.ts

@@ -940,3 +1021,3 @@ /**

function buildResponseUnion(node, { resolver }) {
const responsesWithSchema = node.responses.filter((res) => res.content?.[0]?.schema);
const responsesWithSchema = node.responses.filter((res) => res.content?.some((entry) => entry.schema));
if (responsesWithSchema.length === 0) return null;

@@ -1114,15 +1195,2 @@ return ast.createSchema({

//#region src/generators/typeGenerator.tsx
function getContentTypeSuffix(contentType) {
const baseType = contentType.split(";")[0].trim();
if (baseType === "application/json") return "Json";
if (baseType === "multipart/form-data") return "FormData";
if (baseType === "application/x-www-form-urlencoded") return "FormUrlEncoded";
const parts = (baseType.split("/").pop() ?? baseType).split(/[^a-zA-Z0-9]+/).filter(Boolean);
if (parts.length === 0) return "Unknown";
return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
}
function getPerContentTypeName(dataName, suffix) {
if (dataName.endsWith("Data")) return suffix.endsWith("Data") ? dataName.slice(0, -4) + suffix : `${dataName.slice(0, -4)}${suffix}Data`;
return dataName + suffix;
}
/**

@@ -1287,2 +1355,24 @@ * Built-in generator for `@kubb/plugin-ts`. Emits one TypeScript file per

}
/**
* Emits an individual type per content type plus a union alias under `baseName`.
* Shared by the request body and multi-content-type responses.
*/
function buildContentTypeVariants(entries, baseName, decorate) {
const variants = resolveContentTypeVariants(entries, baseName);
const unionSchema = ast.createSchema({
type: "union",
members: variants.map((variant) => ast.createSchema({
type: "ref",
name: variant.name
}))
});
return /* @__PURE__ */ jsxs(Fragment, { children: [variants.map((variant) => renderSchemaType({
schema: decorate ? decorate(variant.schema) : variant.schema,
name: variant.name,
keysToOmit: variant.keysToOmit
})), renderSchemaType({
schema: unionSchema,
name: baseName
})] });
}
const paramTypes = params.map((param) => renderSchemaType({

@@ -1307,40 +1397,18 @@ schema: param.schema,

}
const dataName = resolver.resolveDataName(node);
const usedNames = /* @__PURE__ */ new Set();
const individualItems = requestBodyContent.filter((entry) => entry.schema).map((entry) => {
const baseSuffix = getContentTypeSuffix(entry.contentType);
let individualName = getPerContentTypeName(dataName, baseSuffix);
let counter = 2;
while (usedNames.has(individualName)) individualName = getPerContentTypeName(dataName, `${baseSuffix}${counter++}`);
usedNames.add(individualName);
return {
name: individualName,
rendered: renderSchemaType({
schema: {
...entry.schema,
description: node.requestBody.description ?? entry.schema.description
},
name: individualName,
keysToOmit: entry.keysToOmit
})
};
});
const unionType = renderSchemaType({
schema: ast.createSchema({
type: "union",
members: individualItems.map((item) => ast.createSchema({
type: "ref",
name: item.name
}))
}),
name: dataName
});
return /* @__PURE__ */ jsxs(Fragment, { children: [individualItems.map((item) => item.rendered), unionType] });
return buildContentTypeVariants(requestBodyContent, resolver.resolveDataName(node), (schema) => ({
...schema,
description: node.requestBody.description ?? schema.description
}));
}
const requestType = buildRequestType();
const responseTypes = node.responses.map((res) => renderSchemaType({
schema: res.content?.[0]?.schema ?? null,
name: resolver.resolveResponseStatusName(node, res.statusCode),
keysToOmit: res.content?.[0]?.keysToOmit
}));
const responseTypes = node.responses.map((res) => {
const variants = (res.content ?? []).filter((entry) => entry.schema);
if (variants.length > 1) return buildContentTypeVariants(variants, resolver.resolveResponseStatusName(node, res.statusCode));
const primary = variants[0] ?? res.content?.[0];
return renderSchemaType({
schema: primary?.schema ?? null,
name: resolver.resolveResponseStatusName(node, res.statusCode),
keysToOmit: primary?.keysToOmit
});
});
const dataType = renderSchemaType({

@@ -1358,9 +1426,10 @@ schema: buildData({

function buildResponseType() {
if (!node.responses.some((res) => res.content?.[0]?.schema)) return null;
const hasSchema = (res) => (res.content ?? []).some((entry) => entry.schema);
if (!node.responses.some(hasSchema)) return null;
const responseName = resolver.resolveResponseName(node);
const responsesWithSchema = node.responses.filter((res) => res.content?.[0]?.schema);
if (new Set(responsesWithSchema.flatMap((res) => res.content?.[0]?.schema ? adapter.getImports(res.content[0].schema, (schemaName) => ({
const responsesWithSchema = node.responses.filter(hasSchema);
if (new Set(responsesWithSchema.flatMap((res) => (res.content ?? []).flatMap((entry) => entry.schema ? adapter.getImports(entry.schema, (schemaName) => ({
name: resolveImportName(schemaName),
path: ""
})).flatMap((imp) => Array.isArray(imp.name) ? imp.name : [imp.name]) : [])).has(responseName)) return null;
})).flatMap((imp) => Array.isArray(imp.name) ? imp.name : [imp.name]) : []))).has(responseName)) return null;
return renderSchemaType({

@@ -1509,9 +1578,3 @@ schema: {

}, group, exclude = [], include, override = [], enumType = "asConst", enumTypeSuffix = "Key", enumKeyCasing = "none", optionalType = "questionToken", arrayType = "array", syntaxType = "type", paramsCasing, printer, resolver: userResolver, transformer: userTransformer, generators: userGenerators = [] } = options;
const groupConfig = group ? {
...group,
name: (ctx) => {
if (group.type === "path") return `${ctx.group.split("/")[1]}`;
return `${camelCase(ctx.group)}Controller`;
}
} : null;
const groupConfig = createGroupConfig(group, { suffix: "Controller" });
return {

@@ -1518,0 +1581,0 @@ name: pluginTsName,

{
"name": "@kubb/plugin-ts",
"version": "5.0.0-beta.30",
"version": "5.0.0-beta.31",
"description": "Generate TypeScript types, interfaces, and enums from your OpenAPI specification. The foundational plugin that powers type safety across the entire Kubb ecosystem.",

@@ -49,5 +49,5 @@ "keywords": [

"dependencies": {
"@kubb/core": "5.0.0-beta.29",
"@kubb/parser-ts": "5.0.0-beta.29",
"@kubb/renderer-jsx": "5.0.0-beta.29",
"@kubb/core": "5.0.0-beta.31",
"@kubb/parser-ts": "5.0.0-beta.31",
"@kubb/renderer-jsx": "5.0.0-beta.31",
"remeda": "^2.34.1",

@@ -61,3 +61,3 @@ "typescript": "^6.0.3"

"peerDependencies": {
"@kubb/renderer-jsx": "5.0.0-beta.29"
"@kubb/renderer-jsx": "5.0.0-beta.31"
},

@@ -64,0 +64,0 @@ "size-limit": [

@@ -0,1 +1,2 @@

import { resolveContentTypeVariants } from '@internals/shared'
import { ast, defineGenerator } from '@kubb/core'

@@ -9,20 +10,2 @@ import { File, jsxRendererSync } from '@kubb/renderer-jsx'

function getContentTypeSuffix(contentType: string): string {
const baseType = contentType.split(';')[0]!.trim()
if (baseType === 'application/json') return 'Json'
if (baseType === 'multipart/form-data') return 'FormData'
if (baseType === 'application/x-www-form-urlencoded') return 'FormUrlEncoded'
const subtype = baseType.split('/').pop() ?? baseType
const parts = subtype.split(/[^a-zA-Z0-9]+/).filter(Boolean)
if (parts.length === 0) return 'Unknown'
return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('')
}
function getPerContentTypeName(dataName: string, suffix: string): string {
if (dataName.endsWith('Data')) {
return suffix.endsWith('Data') ? dataName.slice(0, -4) + suffix : `${dataName.slice(0, -4)}${suffix}Data`
}
return dataName + suffix
}
/**

@@ -172,2 +155,30 @@ * Built-in generator for `@kubb/plugin-ts`. Emits one TypeScript file per

/**
* Emits an individual type per content type plus a union alias under `baseName`.
* Shared by the request body and multi-content-type responses.
*/
function buildContentTypeVariants(
entries: Array<{ contentType: string; schema?: ast.SchemaNode | null; keysToOmit?: Array<string> | null }>,
baseName: string,
decorate?: (schema: ast.SchemaNode) => ast.SchemaNode,
) {
const variants = resolveContentTypeVariants(entries, baseName)
const unionSchema = ast.createSchema({
type: 'union',
members: variants.map((variant) => ast.createSchema({ type: 'ref', name: variant.name })),
})
return (
<>
{variants.map((variant) =>
renderSchemaType({
schema: decorate ? decorate(variant.schema) : variant.schema,
name: variant.name,
keysToOmit: variant.keysToOmit,
}),
)}
{renderSchemaType({ schema: unionSchema, name: baseName })}
</>
)
}
const paramTypes = params.map((param) =>

@@ -197,37 +208,6 @@ renderSchemaType({

// Multiple content types — generate individual types + union alias
const dataName = resolver.resolveDataName(node)
const usedNames = new Set<string>()
const individualItems = requestBodyContent
.filter((entry) => entry.schema)
.map((entry) => {
const baseSuffix = getContentTypeSuffix(entry.contentType)
let individualName = getPerContentTypeName(dataName, baseSuffix)
let counter = 2
while (usedNames.has(individualName)) {
individualName = getPerContentTypeName(dataName, `${baseSuffix}${counter++}`)
}
usedNames.add(individualName)
return {
name: individualName,
rendered: renderSchemaType({
schema: {
...entry.schema!,
description: node.requestBody!.description ?? entry.schema!.description,
},
name: individualName,
keysToOmit: entry.keysToOmit,
}),
}
})
const unionSchema = ast.createSchema({
type: 'union',
members: individualItems.map((item) => ast.createSchema({ type: 'ref', name: item.name })),
})
const unionType = renderSchemaType({ schema: unionSchema, name: dataName })
return (
<>
{individualItems.map((item) => item.rendered)}
{unionType}
</>
)
return buildContentTypeVariants(requestBodyContent, resolver.resolveDataName(node), (schema) => ({
...schema,
description: node.requestBody!.description ?? schema.description,
}))
}

@@ -237,9 +217,15 @@

const responseTypes = node.responses.map((res) =>
renderSchemaType({
schema: res.content?.[0]?.schema ?? null,
const responseTypes = node.responses.map((res) => {
const variants = (res.content ?? []).filter((entry) => entry.schema)
// Multiple content types for a single status code — generate a union of the variants.
if (variants.length > 1) {
return buildContentTypeVariants(variants, resolver.resolveResponseStatusName(node, res.statusCode))
}
const primary = variants[0] ?? res.content?.[0]
return renderSchemaType({
schema: primary?.schema ?? null,
name: resolver.resolveResponseStatusName(node, res.statusCode),
keysToOmit: res.content?.[0]?.keysToOmit,
}),
)
keysToOmit: primary?.keysToOmit,
})
})

@@ -257,3 +243,4 @@ const dataType = renderSchemaType({

function buildResponseType() {
if (!node.responses.some((res) => res.content?.[0]?.schema)) {
const hasSchema = (res: ast.ResponseNode) => (res.content ?? []).some((entry) => entry.schema)
if (!node.responses.some(hasSchema)) {
return null

@@ -264,13 +251,15 @@ }

const responsesWithSchema = node.responses.filter((res) => res.content?.[0]?.schema)
const responsesWithSchema = node.responses.filter(hasSchema)
const importedNames = new Set(
responsesWithSchema.flatMap((res) =>
res.content?.[0]?.schema
? adapter
.getImports(res.content[0].schema, (schemaName) => ({
name: resolveImportName(schemaName),
path: '',
}))
.flatMap((imp) => (Array.isArray(imp.name) ? imp.name : [imp.name]))
: [],
(res.content ?? []).flatMap((entry) =>
entry.schema
? adapter
.getImports(entry.schema, (schemaName) => ({
name: resolveImportName(schemaName),
path: '',
}))
.flatMap((imp) => (Array.isArray(imp.name) ? imp.name : [imp.name]))
: [],
),
),

@@ -277,0 +266,0 @@ )

@@ -1,3 +0,3 @@

import { camelCase } from '@internals/utils'
import { definePlugin, type Group } from '@kubb/core'
import { createGroupConfig } from '@internals/shared'
import { definePlugin } from '@kubb/core'
import { typeGenerator } from './generators/typeGenerator.tsx'

@@ -57,13 +57,3 @@ import { resolverTs } from './resolvers/resolverTs.ts'

const groupConfig = group
? ({
...group,
name: (ctx) => {
if (group.type === 'path') {
return `${ctx.group.split('/')[1]}`
}
return `${camelCase(ctx.group)}Controller`
},
} satisfies Group)
: null
const groupConfig = createGroupConfig(group, { suffix: 'Controller' })

@@ -70,0 +60,0 @@ return {

@@ -130,3 +130,3 @@ import { jsStringEscape, stringify } from '@internals/utils'

export function buildResponseUnion(node: ast.OperationNode, { resolver }: BuildOperationSchemaOptions): ast.SchemaNode | null {
const responsesWithSchema = node.responses.filter((res) => res.content?.[0]?.schema)
const responsesWithSchema = node.responses.filter((res) => res.content?.some((entry) => entry.schema))

@@ -133,0 +133,0 @@ if (responsesWithSchema.length === 0) {

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display