From 3cd059c3eca563443d612a136bc632c74225441f Mon Sep 17 00:00:00 2001 From: Danylo Kachur Date: Fri, 13 Mar 2026 17:34:50 -0400 Subject: [PATCH 1/2] feat: support OpenAPI 3.2 itemSchema (SSE/streaming) --- .../test/middleware/schemas/middleware.yaml | 2 +- packages/openapi-typescript/bin/cli.js | 22 +- packages/openapi-typescript/package.json | 4 +- packages/openapi-typescript/src/index.ts | 14 +- packages/openapi-typescript/src/lib/redoc.ts | 25 +- .../openapi-typescript/src/lib/ref-utils.ts | 15 ++ packages/openapi-typescript/src/lib/ts.ts | 2 +- packages/openapi-typescript/src/lib/utils.ts | 8 +- .../src/transform/header-object.ts | 4 +- .../src/transform/media-type-object.ts | 5 +- .../src/transform/schema-object.ts | 2 +- packages/openapi-typescript/src/types.ts | 2 + .../test/discriminators.test.ts | 8 +- .../test/fixtures/_remote-ref-full.yaml | 2 +- .../test/fixtures/anchor-with-ref-test-2.yaml | 2 +- .../test/fixtures/anchor-with-ref-test.yaml | 2 +- .../test/fixtures/generate-params-test.yaml | 2 +- .../test/fixtures/parameters-test.yaml | 2 +- .../test/fixtures/path-object-refs.yaml | 2 +- .../test/fixtures/sse-stream-test.yaml | 99 ++++++++ .../openapi-typescript/test/index.test.ts | 234 +++++++++++++++++- .../openapi-typescript/test/invalid.test.ts | 2 +- .../openapi-typescript/test/node-api.test.ts | 48 ++-- .../openapi-typescript/test/test-helpers.ts | 2 +- .../test/transform/media-type-object.test.ts | 100 ++++++++ .../transform/request-body-object.test.ts | 53 ++++ .../test/transform/response-object.test.ts | 107 ++++++++ pnpm-lock.yaml | 115 ++++----- 28 files changed, 745 insertions(+), 140 deletions(-) create mode 100644 packages/openapi-typescript/src/lib/ref-utils.ts create mode 100644 packages/openapi-typescript/test/fixtures/sse-stream-test.yaml create mode 100644 packages/openapi-typescript/test/transform/media-type-object.test.ts diff --git a/packages/openapi-fetch/test/middleware/schemas/middleware.yaml b/packages/openapi-fetch/test/middleware/schemas/middleware.yaml index 5f91e5dd7..4db3664ec 100644 --- a/packages/openapi-fetch/test/middleware/schemas/middleware.yaml +++ b/packages/openapi-fetch/test/middleware/schemas/middleware.yaml @@ -1,4 +1,4 @@ -openapi: "3.1" +openapi: "3.1.0" info: title: openapi-fetch version: "1.0" diff --git a/packages/openapi-typescript/bin/cli.js b/packages/openapi-typescript/bin/cli.js index 31be6e795..19130b65f 100755 --- a/packages/openapi-typescript/bin/cli.js +++ b/packages/openapi-typescript/bin/cli.js @@ -196,24 +196,32 @@ async function main() { } const redocly = redocConfigPath ? await loadConfig({ configPath: redocConfigPath }) - : await createConfig({}, { extends: ["minimal"] }); + : await createConfig({ + extends: ["minimal"], + rules: { + struct: "warn", + "no-server-trailing-slash": "warn", + }, + }); // handle Redoc APIs - const hasRedoclyApis = Object.keys(redocly?.apis ?? {}).length > 0; + const redoclyApis = redocly?.resolvedConfig?.apis ?? redocly?.apis ?? {}; + const hasRedoclyApis = Object.keys(redoclyApis).length > 0; if (hasRedoclyApis) { if (input) { warn("APIs are specified both in Redocly Config and CLI argument. Only using Redocly config."); } await Promise.all( - Object.entries(redocly.apis).map(async ([name, api]) => { + Object.entries(redoclyApis).map(async ([name, api]) => { let configRoot = CWD; const config = { ...flags, redocly }; - if (redocly.configFile) { + const configFile = redocly.configPath ?? redocly.configFile; + if (configFile) { // note: this will be absolute if --redoc is passed; otherwise, relative - configRoot = path.isAbsolute(redocly.configFile) - ? new URL(`file://${redocly.configFile}`) - : new URL(redocly.configFile, `file://${process.cwd()}/`); + configRoot = path.isAbsolute(configFile) + ? new URL(`file://${configFile}`) + : new URL(configFile, `file://${process.cwd()}/`); } if (!api[REDOC_CONFIG_KEY]?.output) { errorAndExit( diff --git a/packages/openapi-typescript/package.json b/packages/openapi-typescript/package.json index 2b322a039..89b642ae1 100644 --- a/packages/openapi-typescript/package.json +++ b/packages/openapi-typescript/package.json @@ -43,7 +43,7 @@ }, "scripts": { "build": "unbuild", - "dev": "tsc -p tsconfig.build.json --watch", + "dev": "unbuild --stub", "download:schemas": "vite-node ./scripts/download-schemas.ts", "format": "biome format . --write", "lint": "pnpm run lint:js && pnpm run lint:ts", @@ -62,7 +62,7 @@ "typescript": "^5.x" }, "dependencies": { - "@redocly/openapi-core": "^1.34.6", + "@redocly/openapi-core": "^2.21.1", "ansi-colors": "^4.1.3", "change-case": "^5.4.4", "parse-json": "^8.3.0", diff --git a/packages/openapi-typescript/src/index.ts b/packages/openapi-typescript/src/index.ts index d042b81e4..6def9b85b 100644 --- a/packages/openapi-typescript/src/index.ts +++ b/packages/openapi-typescript/src/index.ts @@ -51,14 +51,14 @@ export default async function openapiTS( const redoc = options.redocly ?? - (await createConfig( - { - rules: { - "operation-operationId-unique": { severity: "error" }, // throw error on duplicate operationIDs - }, + (await createConfig({ + extends: ["minimal"], + rules: { + "operation-operationId-unique": { severity: "error" }, // throw error on duplicate operationIDs + struct: "warn", // downgrade struct rule to warning to allow incomplete schemas + "no-server-trailing-slash": "warn", // downgrade to warning to handle real-world specs }, - { extends: ["minimal"] }, - )); + })); const schema = await validateAndBundle(source, { redoc, diff --git a/packages/openapi-typescript/src/lib/redoc.ts b/packages/openapi-typescript/src/lib/redoc.ts index 455695243..119430b9f 100644 --- a/packages/openapi-typescript/src/lib/redoc.ts +++ b/packages/openapi-typescript/src/lib/redoc.ts @@ -5,6 +5,7 @@ import { BaseResolver, bundle, type Document, + isPlainObject, lintDocument, makeDocumentFromString, type NormalizedProblem, @@ -123,19 +124,17 @@ export async function validateAndBundle( debug("Parsed schema", "redoc", performance.now() - redocParseT); // 1. check for OpenAPI 3 or greater - const openapiVersion = Number.parseFloat(document.parsed.openapi); - if ( - document.parsed.swagger || - !document.parsed.openapi || - Number.isNaN(openapiVersion) || - openapiVersion < 3 || - openapiVersion >= 4 - ) { - if (document.parsed.swagger) { + if (!isPlainObject(document.parsed)) { + throw new Error("Unsupported schema format, expected `openapi: 3.x`"); + } + const parsed = document.parsed; + const openapiVersion = Number.parseFloat(String(parsed.openapi ?? "")); + if (parsed.swagger || !parsed.openapi || Number.isNaN(openapiVersion) || openapiVersion < 3 || openapiVersion >= 4) { + if (parsed.swagger) { throw new Error("Unsupported Swagger version: 2.x. Use OpenAPI 3.x instead."); } - if (document.parsed.openapi || openapiVersion < 3 || openapiVersion >= 4) { - throw new Error(`Unsupported OpenAPI version: ${document.parsed.openapi}`); + if (parsed.openapi || openapiVersion < 3 || openapiVersion >= 4) { + throw new Error(`Unsupported OpenAPI version: ${parsed.openapi}`); } throw new Error("Unsupported schema format, expected `openapi: 3.x`"); } @@ -144,7 +143,7 @@ export async function validateAndBundle( const redocLintT = performance.now(); const problems = await lintDocument({ document, - config: options.redoc.styleguide, + config: options.redoc, externalRefResolver: resolver, }); _processProblems(problems, options); @@ -160,5 +159,5 @@ export async function validateAndBundle( _processProblems(bundled.problems, options); debug("Bundled schema", "bundle", performance.now() - redocBundleT); - return bundled.bundle.parsed; + return bundled.bundle.parsed as OpenAPI3; } diff --git a/packages/openapi-typescript/src/lib/ref-utils.ts b/packages/openapi-typescript/src/lib/ref-utils.ts new file mode 100644 index 000000000..55483062e --- /dev/null +++ b/packages/openapi-typescript/src/lib/ref-utils.ts @@ -0,0 +1,15 @@ +import { unescapePointerFragment } from "@redocly/openapi-core"; + +/** Parse a $ref string into its URI and JSON Pointer parts */ +export function parseRef(ref: string): { uri: string | null; pointer: string[] } { + const hashIndex = ref.indexOf("#"); + const uri = hashIndex === -1 ? ref : ref.slice(0, hashIndex); + const fragment = hashIndex === -1 ? "" : ref.slice(hashIndex + 1); + return { + uri: uri || null, + pointer: fragment + .split("/") + .map(unescapePointerFragment) + .filter((s) => s !== ""), + }; +} diff --git a/packages/openapi-typescript/src/lib/ts.ts b/packages/openapi-typescript/src/lib/ts.ts index d1f41eb88..def961ff8 100644 --- a/packages/openapi-typescript/src/lib/ts.ts +++ b/packages/openapi-typescript/src/lib/ts.ts @@ -1,7 +1,7 @@ import type { OasRef, Referenced } from "@redocly/openapi-core"; -import { parseRef } from "@redocly/openapi-core/lib/ref-utils.js"; import ts, { type LiteralTypeNode, type TypeLiteralNode } from "typescript"; import type { ParameterObject } from "../types.js"; +import { parseRef } from "./ref-utils.js"; export const JS_PROPERTY_INDEX_RE = /^[A-Za-z_$][A-Za-z_$0-9]*$/; export const JS_ENUM_INVALID_CHARS_RE = /[^A-Za-z_$0-9]+(.)?/g; diff --git a/packages/openapi-typescript/src/lib/utils.ts b/packages/openapi-typescript/src/lib/utils.ts index 49c192422..7ba10765c 100644 --- a/packages/openapi-typescript/src/lib/utils.ts +++ b/packages/openapi-typescript/src/lib/utils.ts @@ -1,4 +1,4 @@ -import { escapePointer, parseRef } from "@redocly/openapi-core/lib/ref-utils.js"; +import { escapePointerFragment } from "@redocly/openapi-core"; import c from "ansi-colors"; import supportsColor from "supports-color"; import ts from "typescript"; @@ -16,6 +16,8 @@ const DEBUG_GROUPS: Record = { ts: c.blueBright, }; +import { parseRef } from "./ref-utils.js"; + export { c }; /** Given a discriminator object, get the property name */ @@ -55,10 +57,10 @@ export function createRef(parts: (number | string | undefined | null)[]): string const maybeRef = parseRef(String(part)).pointer; if (maybeRef.length) { for (const refPart of maybeRef) { - pointer += `/${escapePointer(refPart)}`; + pointer += `/${escapePointerFragment(refPart)}`; } } else { - pointer += `/${escapePointer(part)}`; + pointer += `/${escapePointerFragment(part)}`; } } return pointer; diff --git a/packages/openapi-typescript/src/transform/header-object.ts b/packages/openapi-typescript/src/transform/header-object.ts index cb3654e5b..53677cb94 100644 --- a/packages/openapi-typescript/src/transform/header-object.ts +++ b/packages/openapi-typescript/src/transform/header-object.ts @@ -1,4 +1,4 @@ -import { escapePointer } from "@redocly/openapi-core/lib/ref-utils.js"; +import { escapePointerFragment } from "@redocly/openapi-core"; import ts from "typescript"; import { addJSDocComment, tsModifiers, tsPropertyIndex, UNKNOWN } from "../lib/ts.js"; import { getEntries } from "../lib/utils.js"; @@ -18,7 +18,7 @@ export default function transformHeaderObject(headerObject: HeaderObject, option if (headerObject.content) { const type: ts.TypeElement[] = []; for (const [contentType, mediaTypeObject] of getEntries(headerObject.content ?? {}, options.ctx)) { - const nextPath = `${options.path ?? "#"}/${escapePointer(contentType)}`; + const nextPath = `${options.path ?? "#"}/${escapePointerFragment(contentType)}`; const mediaType = "$ref" in mediaTypeObject ? transformSchemaObject(mediaTypeObject, { diff --git a/packages/openapi-typescript/src/transform/media-type-object.ts b/packages/openapi-typescript/src/transform/media-type-object.ts index 647febbb0..c1b02ee99 100644 --- a/packages/openapi-typescript/src/transform/media-type-object.ts +++ b/packages/openapi-typescript/src/transform/media-type-object.ts @@ -11,8 +11,9 @@ export default function transformMediaTypeObject( mediaTypeObject: MediaTypeObject, options: TransformNodeOptions, ): ts.TypeNode { - if (!mediaTypeObject.schema) { + const targetSchema = mediaTypeObject.itemSchema ?? mediaTypeObject.schema; + if (!targetSchema) { return UNKNOWN; } - return transformSchemaObject(mediaTypeObject.schema, options); + return transformSchemaObject(targetSchema, options); } diff --git a/packages/openapi-typescript/src/transform/schema-object.ts b/packages/openapi-typescript/src/transform/schema-object.ts index caab5e10f..991d0a4f6 100644 --- a/packages/openapi-typescript/src/transform/schema-object.ts +++ b/packages/openapi-typescript/src/transform/schema-object.ts @@ -1,5 +1,5 @@ -import { parseRef } from "@redocly/openapi-core/lib/ref-utils.js"; import ts from "typescript"; +import { parseRef } from "../lib/ref-utils.js"; import { addJSDocComment, BOOLEAN, diff --git a/packages/openapi-typescript/src/types.ts b/packages/openapi-typescript/src/types.ts index d19185cc6..606d60bb3 100644 --- a/packages/openapi-typescript/src/types.ts +++ b/packages/openapi-typescript/src/types.ts @@ -286,6 +286,8 @@ export interface RequestBodyObject extends Extensable { export interface MediaTypeObject extends Extensable { /** The schema defining the content of the request, response, or parameter. */ schema?: SchemaObject | ReferenceObject; + /** OAS 3.2: The schema defining the content of individual items in a streaming response (e.g. text/event-stream). When present, takes precedence over schema for type generation. */ + itemSchema?: SchemaObject | ReferenceObject; /** Example of the media type. The example object SHOULD be in the correct format as specified by the media type. The example field is mutually exclusive of the examples field. Furthermore, if referencing a schema which contains an example, the example value SHALL override the example provided by the schema. */ example?: any; /** Examples of the media type. Each example object SHOULD match the media type and specified schema if present. The examples field is mutually exclusive of the example field. Furthermore, if referencing a schema which contains an example, the examples value SHALL override the example provided by the schema. */ diff --git a/packages/openapi-typescript/test/discriminators.test.ts b/packages/openapi-typescript/test/discriminators.test.ts index c597b7670..9dbe5ad1a 100644 --- a/packages/openapi-typescript/test/discriminators.test.ts +++ b/packages/openapi-typescript/test/discriminators.test.ts @@ -8,7 +8,7 @@ describe("3.1 discriminators", () => { "allOf > mapping", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "test", version: "1.0" }, components: { schemas: { @@ -120,7 +120,7 @@ export type operations = Record;`, "allOf > no mapping", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "test", version: "1.0" }, components: { schemas: { @@ -189,7 +189,7 @@ export type operations = Record;`, "allOf > inline inheritance", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "test", version: "1.0" }, components: { schemas: { @@ -237,7 +237,7 @@ export type operations = Record;`, "oneOf > implicit mapping", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "test", version: "1.0" }, components: { schemas: { diff --git a/packages/openapi-typescript/test/fixtures/_remote-ref-full.yaml b/packages/openapi-typescript/test/fixtures/_remote-ref-full.yaml index 2397d076c..f6c281627 100644 --- a/packages/openapi-typescript/test/fixtures/_remote-ref-full.yaml +++ b/packages/openapi-typescript/test/fixtures/_remote-ref-full.yaml @@ -1,4 +1,4 @@ -openapi: "3.0" +openapi: "3.0.0" info: title: test version: "1.0" diff --git a/packages/openapi-typescript/test/fixtures/anchor-with-ref-test-2.yaml b/packages/openapi-typescript/test/fixtures/anchor-with-ref-test-2.yaml index 31d2930fb..a65091e47 100644 --- a/packages/openapi-typescript/test/fixtures/anchor-with-ref-test-2.yaml +++ b/packages/openapi-typescript/test/fixtures/anchor-with-ref-test-2.yaml @@ -1,4 +1,4 @@ -openapi: "3.0" +openapi: "3.0.0" info: title: test version: "1.0" diff --git a/packages/openapi-typescript/test/fixtures/anchor-with-ref-test.yaml b/packages/openapi-typescript/test/fixtures/anchor-with-ref-test.yaml index b1eae70f3..56fc0fbb2 100644 --- a/packages/openapi-typescript/test/fixtures/anchor-with-ref-test.yaml +++ b/packages/openapi-typescript/test/fixtures/anchor-with-ref-test.yaml @@ -1,4 +1,4 @@ -openapi: "3.0" +openapi: "3.0.0" info: title: test version: "1.0" diff --git a/packages/openapi-typescript/test/fixtures/generate-params-test.yaml b/packages/openapi-typescript/test/fixtures/generate-params-test.yaml index bd7c2449c..523db5acc 100644 --- a/packages/openapi-typescript/test/fixtures/generate-params-test.yaml +++ b/packages/openapi-typescript/test/fixtures/generate-params-test.yaml @@ -1,4 +1,4 @@ -openapi: "3.0" +openapi: "3.0.0" info: title: Test version: "1.0" diff --git a/packages/openapi-typescript/test/fixtures/parameters-test.yaml b/packages/openapi-typescript/test/fixtures/parameters-test.yaml index c16167f07..6d668ebbc 100644 --- a/packages/openapi-typescript/test/fixtures/parameters-test.yaml +++ b/packages/openapi-typescript/test/fixtures/parameters-test.yaml @@ -1,4 +1,4 @@ -openapi: "3.0" +openapi: "3.0.0" info: title: test version: "1.0" diff --git a/packages/openapi-typescript/test/fixtures/path-object-refs.yaml b/packages/openapi-typescript/test/fixtures/path-object-refs.yaml index bbfa0d8c1..76cd11e51 100644 --- a/packages/openapi-typescript/test/fixtures/path-object-refs.yaml +++ b/packages/openapi-typescript/test/fixtures/path-object-refs.yaml @@ -1,4 +1,4 @@ -openapi: "3.0" +openapi: "3.0.0" info: title: Test version: "1.0" diff --git a/packages/openapi-typescript/test/fixtures/sse-stream-test.yaml b/packages/openapi-typescript/test/fixtures/sse-stream-test.yaml new file mode 100644 index 000000000..39ff8651c --- /dev/null +++ b/packages/openapi-typescript/test/fixtures/sse-stream-test.yaml @@ -0,0 +1,99 @@ +openapi: "3.2.0" +info: + title: SSE Streaming API + version: "1.0" +paths: + /events: + get: + operationId: streamEvents + summary: Stream server-sent events + responses: + "200": + description: SSE event stream + content: + text/event-stream: + itemSchema: + type: object + properties: + event: + type: string + enum: + - message + - heartbeat + - error + data: + type: string + id: + type: integer + timestamp: + type: string + format: date-time + required: + - event + - data + /chat: + post: + operationId: chatStream + summary: Chat with streaming response + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + message: + type: string + model: + type: string + required: + - message + responses: + "200": + description: Streaming chat response + content: + text/event-stream: + itemSchema: + oneOf: + - type: object + properties: + type: + type: string + enum: + - content + text: + type: string + required: + - type + - text + - type: object + properties: + type: + type: string + enum: + - done + usage: + type: object + properties: + input_tokens: + type: integer + output_tokens: + type: integer + required: + - input_tokens + - output_tokens + required: + - type + - usage +components: + schemas: + SSEEvent: + type: object + properties: + event: + type: string + data: + type: string + required: + - event + - data diff --git a/packages/openapi-typescript/test/index.test.ts b/packages/openapi-typescript/test/index.test.ts index ea8f55426..a6005ad10 100644 --- a/packages/openapi-typescript/test/index.test.ts +++ b/packages/openapi-typescript/test/index.test.ts @@ -21,7 +21,7 @@ describe("openapiTS", () => { "$refs > basic", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, components: { schemas: { @@ -77,7 +77,7 @@ export type operations = Record;`, "$refs > arbitrary $refs are respected", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, components: { schemas: { @@ -337,7 +337,7 @@ export type operations = Record;`, "parameters > operations get correct params", { given: { - openapi: "3.0", + openapi: "3.0.0", info: { title: "Test", version: "1.0" }, paths: { "/post/{id}": { @@ -457,7 +457,7 @@ export interface operations { "examples > skipped", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, components: { schemas: { @@ -505,7 +505,7 @@ export type operations = Record;`, "operations > # character is parsed correctly", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, paths: { "/accounts": { @@ -622,7 +622,7 @@ export type operations = Record;`, "TypeScript > WithRequired type helper", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, components: { schemas: { @@ -691,7 +691,7 @@ export type operations = Record;`, "inject option", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, }, want: `type Foo = string; @@ -1111,6 +1111,224 @@ export type $defs = Record; export type operations = Record;`, }, ], + [ + "SSE > itemSchema with $ref to component schema", + { + given: { + openapi: "3.1.0", + info: { title: "SSE Ref Test", version: "1.0" }, + paths: { + "/notifications": { + get: { + responses: { + 200: { + description: "Notification stream", + content: { + "text/event-stream": { + itemSchema: { + $ref: "#/components/schemas/Notification", + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Notification: { + type: "object", + properties: { + id: { type: "string" }, + title: { type: "string" }, + read: { type: "boolean" }, + }, + required: ["id", "title"], + }, + }, + }, + }, + want: `export interface paths { + "/notifications": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Notification stream */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "text/event-stream": components["schemas"]["Notification"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + Notification: { + id: string; + title: string; + read?: boolean; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export type operations = Record;`, + }, + ], + [ + "OpenAPI 3.2 > SSE streaming with itemSchema", + { + given: new URL("./fixtures/sse-stream-test.yaml", import.meta.url), + want: `export interface paths { + "/events": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Stream server-sent events */ + get: operations["streamEvents"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/chat": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Chat with streaming response */ + post: operations["chatStream"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + SSEEvent: { + event: string; + data: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + streamEvents: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description SSE event stream */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "text/event-stream": { + /** @enum {string} */ + event: "message" | "heartbeat" | "error"; + data: string; + id?: number; + /** Format: date-time */ + timestamp?: string; + }; + }; + }; + }; + }; + chatStream: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + message: string; + model?: string; + }; + }; + }; + responses: { + /** @description Streaming chat response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "text/event-stream": { + /** @enum {string} */ + type: "content"; + text: string; + } | { + /** @enum {string} */ + type: "done"; + usage: { + input_tokens: number; + output_tokens: number; + }; + }; + }; + }; + }; + }; +}`, + }, + ], ]; for (const [testName, { given, want, options, ci }] of tests) { @@ -1130,7 +1348,7 @@ export type operations = Record;`, test("does not mutate original reference", async () => { const schema: OpenAPI3 = { - openapi: "3.1", + openapi: "3.1.0", info: { title: "test", version: "1.0" }, components: { schemas: { diff --git a/packages/openapi-typescript/test/invalid.test.ts b/packages/openapi-typescript/test/invalid.test.ts index 2f019a291..2dd956cc3 100644 --- a/packages/openapi-typescript/test/invalid.test.ts +++ b/packages/openapi-typescript/test/invalid.test.ts @@ -25,7 +25,7 @@ describe("Invalid schemas", () => { await expect(() => openapiTS({ - openapi: "3.1", + openapi: "3.1.0", info: { title: "test", version: "1.0" }, components: { schemas: { diff --git a/packages/openapi-typescript/test/node-api.test.ts b/packages/openapi-typescript/test/node-api.test.ts index b22f8ad18..29ca300e6 100644 --- a/packages/openapi-typescript/test/node-api.test.ts +++ b/packages/openapi-typescript/test/node-api.test.ts @@ -14,10 +14,10 @@ describe("Node.js API", () => { [ "input > string > YAML", { - given: `openapi: "3.1" + given: `openapi: "3.1.0" info: title: test - version: 1.0`, + version: "1.0"`, want: `export type paths = Record; export type webhooks = Record; export interface components { @@ -37,7 +37,7 @@ export type operations = Record;`, "input > string > JSON", { given: JSON.stringify({ - openapi: "3.1", + openapi: "3.1.0", info: { title: "test", version: "1.0" }, }), want: `export type paths = Record; @@ -86,7 +86,7 @@ export type operations = Record;`, "input > object", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "test", version: "1.0" }, }, want: `export type paths = Record; @@ -107,7 +107,7 @@ export type operations = Record;`, [ "input > buffer", { - given: Buffer.from(`openapi: "3.1" + given: Buffer.from(`openapi: "3.1.0" info: title: test version: 1.0`), @@ -130,7 +130,7 @@ export type operations = Record;`, "options > exportType > false", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, components: { schemas: { @@ -170,7 +170,7 @@ export type operations = Record;`, "options > exportType > true", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, components: { schemas: { @@ -209,7 +209,7 @@ export type operations = Record;`, "options > pathParamsAsTypes > false", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, paths: { "/user/{user_id}": { @@ -280,7 +280,7 @@ export type operations = Record;`, "options > pathParamsAsTypes > true", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, paths: { "/user/{user_id}": { @@ -351,7 +351,7 @@ export type operations = Record;`, "options > transform", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, components: { schemas: { @@ -392,7 +392,7 @@ export type operations = Record;`, "options > transform with schema object", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, components: { schemas: { @@ -431,7 +431,7 @@ export type operations = Record;`, "options > transform with blob", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, components: { requestBodies: { @@ -479,7 +479,7 @@ export type operations = Record;`, "options > transform with optional blob property", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, components: { requestBodies: { @@ -535,7 +535,7 @@ export type operations = Record;`, "options > postTransform", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, components: { schemas: { @@ -609,7 +609,7 @@ export type operations = Record;`, "options > transformProperty > JSDoc validation annotations", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, components: { schemas: { @@ -717,7 +717,7 @@ export type operations = Record;`, "options > transformProperty > no-op when returning undefined", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, components: { schemas: { @@ -766,7 +766,7 @@ export type operations = Record;`, "options > enum", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, paths: { "/url": { @@ -915,7 +915,7 @@ export type operations = Record;`, "options > enumValues", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, paths: { "/url": { @@ -1009,7 +1009,7 @@ export type operations = Record;`, "options > enumValues with record types", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, components: { schemas: { @@ -1248,7 +1248,7 @@ export const pathsAnalyticsDataGetResponses400ContentApplicationJsonAnyOf2Messag "options > dedupeEnums", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, paths: { "/url": { @@ -1380,7 +1380,7 @@ export type operations = Record;`, "options > enumValues with request body enum", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, paths: { "/test": { @@ -1478,7 +1478,7 @@ export type operations = Record;`, "options > enumValues with nested union types", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, components: { schemas: { @@ -1582,7 +1582,7 @@ export type operations = Record;`, "options > enumValues with same property different inner schemas", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, components: { schemas: { @@ -1681,7 +1681,7 @@ export type operations = Record;`, "options > enumValues with deeply nested unions", { given: { - openapi: "3.1", + openapi: "3.1.0", info: { title: "Test", version: "1.0" }, components: { schemas: { diff --git a/packages/openapi-typescript/test/test-helpers.ts b/packages/openapi-typescript/test/test-helpers.ts index f7008bbff..bc8faaed0 100644 --- a/packages/openapi-typescript/test/test-helpers.ts +++ b/packages/openapi-typescript/test/test-helpers.ts @@ -27,7 +27,7 @@ export const DEFAULT_CTX: GlobalContext = { rootTypes: false, rootTypesNoSchemaPrefix: false, rootTypesKeepCasing: false, - redoc: await createConfig({}, { extends: ["minimal"] }), + redoc: await createConfig({ extends: ["minimal"] }), resolve($ref) { return resolveRef({}, $ref, { silent: false }); }, diff --git a/packages/openapi-typescript/test/transform/media-type-object.test.ts b/packages/openapi-typescript/test/transform/media-type-object.test.ts new file mode 100644 index 000000000..84bd4f1e4 --- /dev/null +++ b/packages/openapi-typescript/test/transform/media-type-object.test.ts @@ -0,0 +1,100 @@ +import { astToString } from "../../src/lib/ts.js"; +import transformMediaTypeObject from "../../src/transform/media-type-object.js"; +import { DEFAULT_CTX, type TestCase } from "../test-helpers.js"; + +const DEFAULT_OPTIONS = { + path: "#/paths/~1get-item/responses/200/content/application~1json", + ctx: { ...DEFAULT_CTX }, +}; + +describe("transformMediaTypeObject", () => { + const tests: TestCase[] = [ + [ + "schema only", + { + given: { + schema: { + type: "object", + properties: { + id: { type: "string" }, + }, + required: ["id"], + }, + }, + want: `{ + id: string; +}`, + }, + ], + [ + "itemSchema only", + { + given: { + itemSchema: { + type: "object", + properties: { + event: { type: "string" }, + data: { type: "number" }, + }, + required: ["event"], + }, + }, + want: `{ + event: string; + data?: number; +}`, + }, + ], + [ + "itemSchema takes precedence over schema", + { + given: { + schema: { type: "string" }, + itemSchema: { + type: "object", + properties: { + id: { type: "integer" }, + message: { type: "string" }, + }, + required: ["id", "message"], + }, + }, + want: `{ + id: number; + message: string; +}`, + }, + ], + [ + "neither schema nor itemSchema returns unknown", + { + given: {}, + want: "unknown", + }, + ], + [ + "schema with no itemSchema still works (backward compat)", + { + given: { + schema: { + type: "object", + properties: { + name: { type: "string" }, + }, + required: ["name"], + }, + }, + want: `{ + name: string; +}`, + }, + ], + ]; + + for (const [testName, { given, want, options = DEFAULT_OPTIONS, ci }] of tests) { + test.skipIf(ci?.skipIf)(testName, async () => { + const result = astToString(transformMediaTypeObject(given, options)); + expect(result).toBe(`${want}\n`); + }); + } +}); diff --git a/packages/openapi-typescript/test/transform/request-body-object.test.ts b/packages/openapi-typescript/test/transform/request-body-object.test.ts index 64fbb676f..3caf10fb7 100644 --- a/packages/openapi-typescript/test/transform/request-body-object.test.ts +++ b/packages/openapi-typescript/test/transform/request-body-object.test.ts @@ -155,6 +155,59 @@ describe("transformRequestBodyObject", () => { }, }, ], + [ + "itemSchema in request body (streaming upload)", + { + given: { + content: { + "application/x-ndjson": { + schema: { type: "string" }, + itemSchema: { + type: "object", + properties: { + action: { type: "string" }, + payload: { type: "object" }, + }, + required: ["action"], + }, + }, + }, + }, + want: `{ + content: { + "application/x-ndjson": { + action: string; + payload?: Record; + }; + }; +}`, + }, + ], + [ + "itemSchema only in request body", + { + given: { + content: { + "text/event-stream": { + itemSchema: { + type: "object", + properties: { + message: { type: "string" }, + }, + required: ["message"], + }, + }, + }, + }, + want: `{ + content: { + "text/event-stream": { + message: string; + }; + }; +}`, + }, + ], ]; for (const [testName, { given, want, options = DEFAULT_OPTIONS, ci }] of tests) { diff --git a/packages/openapi-typescript/test/transform/response-object.test.ts b/packages/openapi-typescript/test/transform/response-object.test.ts index dbb0a3ecb..39d07e34f 100644 --- a/packages/openapi-typescript/test/transform/response-object.test.ts +++ b/packages/openapi-typescript/test/transform/response-object.test.ts @@ -79,6 +79,113 @@ describe("transformResponseObject", () => { [name: string]: unknown; }; content?: never; +}`, + }, + ], + [ + "text/event-stream with itemSchema", + { + given: { + description: "SSE stream", + content: { + "text/event-stream": { + schema: { type: "string" }, + itemSchema: { + type: "object", + properties: { + event: { type: "string" }, + data: { type: "string" }, + }, + required: ["event", "data"], + }, + }, + }, + }, + want: `{ + headers: { + [name: string]: unknown; + }; + content: { + "text/event-stream": { + event: string; + data: string; + }; + }; +}`, + }, + ], + [ + "itemSchema without schema", + { + given: { + description: "SSE stream with only itemSchema", + content: { + "text/event-stream": { + itemSchema: { + type: "object", + properties: { + id: { type: "string" }, + payload: { type: "number" }, + }, + required: ["id"], + }, + }, + }, + }, + want: `{ + headers: { + [name: string]: unknown; + }; + content: { + "text/event-stream": { + id: string; + payload?: number; + }; + }; +}`, + }, + ], + [ + "mixed content types: SSE with itemSchema and JSON with schema", + { + given: { + description: "Mixed response", + content: { + "application/json": { + schema: { + type: "object", + properties: { + results: { type: "array", items: { type: "string" } }, + }, + required: ["results"], + }, + }, + "text/event-stream": { + schema: { type: "string" }, + itemSchema: { + type: "object", + properties: { + event: { type: "string" }, + data: { type: "string" }, + }, + required: ["event", "data"], + }, + }, + }, + }, + want: `{ + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + results: string[]; + }; + "text/event-stream": { + event: string; + data: string; + }; + }; }`, }, ], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e5e48a97..329d00f35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -241,8 +241,8 @@ importers: packages/openapi-typescript: dependencies: '@redocly/openapi-core': - specifier: ^1.34.6 - version: 1.34.6(supports-color@10.2.2) + specifier: ^2.21.1 + version: 2.21.1 ansi-colors: specifier: ^4.1.3 version: 4.1.3 @@ -1411,15 +1411,15 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@redocly/ajv@8.17.3': - resolution: {integrity: sha512-NQsbJbB/GV7JVO88ebFkMndrnuGp/dTm5/2NISeg+JGcLzTfGBJZ01+V5zD8nKBOpi/dLLNFT+Ql6IcUk8ehng==} + '@redocly/ajv@8.18.0': + resolution: {integrity: sha512-F+LMD2IDIXuHxgpLJh3nkLj9+tSaEzoUWd+7fONGq5pe2169FUDjpEkOfEpoGLz1sbZni/69p07OsecNfAOpqA==} - '@redocly/config@0.22.2': - resolution: {integrity: sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==} + '@redocly/config@0.44.1': + resolution: {integrity: sha512-l6/ZE+/RBfNDdhzltau6cbW8+k5PgJbJBMqaBrlQlZQlmGBHMxqGyDaon4dPLj0jdi37gsMQ3yf95JBY/vaDSg==} - '@redocly/openapi-core@1.34.6': - resolution: {integrity: sha512-2+O+riuIUgVSuLl3Lyh5AplWZyVMNuG2F98/o6NrutKJfW4/GTZdPpZlIphS0HGgcOHgmWcCSHj+dWFlZaGSHw==} - engines: {node: '>=18.17.0', npm: '>=9.5.0'} + '@redocly/openapi-core@2.21.1': + resolution: {integrity: sha512-xqO0avM42DOnninr3NqCPGgD61L1EunmDy+hQNZhuCM2/a6X0g19ZYNioQxeGw3/OlKNVeplSO26lEq52R12VQ==} + engines: {node: '>=22.12.0 || >=20.19.0 <21.0.0', npm: '>=10'} '@rolldown/pluginutils@1.0.0-rc.3': resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} @@ -2015,9 +2015,13 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} - agent-base@7.1.4: - resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} - engines: {node: '>= 14'} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true algoliasearch@5.48.0: resolution: {integrity: sha512-aD8EQC6KEman6/S79FtPdQmB7D4af/etcRL/KwiKFKgAE62iU8c5PeEQvpvIcBPurC3O/4Lj78nOl7ZcoazqSw==} @@ -2804,10 +2808,6 @@ packages: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} - https-proxy-agent@7.0.6: - resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} - engines: {node: '>= 14'} - human-id@4.1.3: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true @@ -2941,6 +2941,10 @@ packages: engines: {node: '>=6'} hasBin: true + json-schema-to-ts@2.7.2: + resolution: {integrity: sha512-R1JfqKqbBR4qE8UyBR56Ms30LL62/nlhoz+1UkfI/VE7p54Awu919FZ6ZUPG8zIa3XB65usPJgr1ONVncUGSaQ==} + engines: {node: '>=16'} + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} @@ -3083,10 +3087,6 @@ packages: engines: {node: '>=4.0.0'} hasBin: true - minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} - engines: {node: '>=10'} - minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -4007,6 +4007,9 @@ packages: trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + ts-algebra@1.2.2: + resolution: {integrity: sha512-kloPhf1hq3JbCPOTYoOWDKxebWjNb2o/LKnNfkWhxVVisFFmMJPPdJeGoGmM+iRLyoXAR61e08Pb+vUXINg8aA==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -4595,7 +4598,7 @@ snapshots: '@babel/types': 7.29.0 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3(supports-color@10.2.2) + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -4679,7 +4682,7 @@ snapshots: '@babel/parser': 7.29.0 '@babel/template': 7.28.6 '@babel/types': 7.29.0 - debug: 4.4.3(supports-color@10.2.2) + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -5392,28 +5395,29 @@ snapshots: '@polka/url@1.0.0-next.29': {} - '@redocly/ajv@8.17.3': + '@redocly/ajv@8.18.0': dependencies: fast-deep-equal: 3.1.3 fast-uri: 3.1.0 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - '@redocly/config@0.22.2': {} + '@redocly/config@0.44.1': + dependencies: + json-schema-to-ts: 2.7.2 - '@redocly/openapi-core@1.34.6(supports-color@10.2.2)': + '@redocly/openapi-core@2.21.1': dependencies: - '@redocly/ajv': 8.17.3 - '@redocly/config': 0.22.2 + '@redocly/ajv': 8.18.0 + '@redocly/config': 0.44.1 + ajv: '@redocly/ajv@8.18.0' + ajv-formats: 3.0.1(@redocly/ajv@8.18.0) colorette: 1.4.0 - https-proxy-agent: 7.0.6(supports-color@10.2.2) js-levenshtein: 1.1.6 js-yaml: 4.1.1 - minimatch: 5.1.6 + picomatch: 4.0.3 pluralize: 8.0.0 yaml-ast-parser: 0.0.43 - transitivePeerDependencies: - - supports-color '@rolldown/pluginutils@1.0.0-rc.3': {} @@ -5634,7 +5638,7 @@ snapshots: '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.53.5)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(yaml@2.8.2)))(svelte@5.53.5)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(yaml@2.8.2))': dependencies: '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.53.5)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(yaml@2.8.2)) - debug: 4.4.3(supports-color@10.2.2) + debug: 4.4.3 svelte: 5.53.5 vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(yaml@2.8.2) transitivePeerDependencies: @@ -5643,7 +5647,7 @@ snapshots: '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.53.5)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(yaml@2.8.2))': dependencies: '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.53.5)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(yaml@2.8.2)))(svelte@5.53.5)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(yaml@2.8.2)) - debug: 4.4.3(supports-color@10.2.2) + debug: 4.4.3 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.21 @@ -6006,12 +6010,14 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.3(supports-color@10.2.2) + debug: 4.4.3 transitivePeerDependencies: - supports-color optional: true - agent-base@7.1.4: {} + ajv-formats@3.0.1(@redocly/ajv@8.18.0): + optionalDependencies: + ajv: '@redocly/ajv@8.18.0' algoliasearch@5.48.0: dependencies: @@ -6103,7 +6109,7 @@ snapshots: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.3(supports-color@10.2.2) + debug: 4.4.3 http-errors: 2.0.1 iconv-lite: 0.7.2 on-finished: 2.4.1 @@ -6378,11 +6384,9 @@ snapshots: de-indent@1.0.2: {} - debug@4.4.3(supports-color@10.2.2): + debug@4.4.3: dependencies: ms: 2.1.3 - optionalDependencies: - supports-color: 10.2.2 decimal.js@10.6.0: optional: true @@ -6639,7 +6643,7 @@ snapshots: content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.3(supports-color@10.2.2) + debug: 4.4.3 depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 @@ -6711,7 +6715,7 @@ snapshots: finalhandler@2.1.1: dependencies: - debug: 4.4.3(supports-color@10.2.2) + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -6895,7 +6899,7 @@ snapshots: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.4.3(supports-color@10.2.2) + debug: 4.4.3 transitivePeerDependencies: - supports-color optional: true @@ -6903,18 +6907,11 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.3(supports-color@10.2.2) + debug: 4.4.3 transitivePeerDependencies: - supports-color optional: true - https-proxy-agent@7.0.6(supports-color@10.2.2): - dependencies: - agent-base: 7.1.4 - debug: 4.4.3(supports-color@10.2.2) - transitivePeerDependencies: - - supports-color - human-id@4.1.3: {} human-signals@8.0.1: {} @@ -7036,6 +7033,12 @@ snapshots: jsesc@3.1.0: {} + json-schema-to-ts@2.7.2: + dependencies: + '@babel/runtime': 7.28.6 + '@types/json-schema': 7.0.15 + ts-algebra: 1.2.2 + json-schema-traverse@1.0.0: {} json5@2.2.3: {} @@ -7161,10 +7164,6 @@ snapshots: mime@2.6.0: {} - minimatch@5.1.6: - dependencies: - brace-expansion: 2.0.2 - minimatch@9.0.5: dependencies: brace-expansion: 2.0.2 @@ -7763,7 +7762,7 @@ snapshots: router@2.2.0: dependencies: - debug: 4.4.3(supports-color@10.2.2) + debug: 4.4.3 depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -7802,7 +7801,7 @@ snapshots: send@1.2.1: dependencies: - debug: 4.4.3(supports-color@10.2.2) + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -7993,7 +7992,7 @@ snapshots: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.4.3(supports-color@10.2.2) + debug: 4.4.3 fast-safe-stringify: 2.1.1 form-data: 4.0.5 formidable: 3.5.4 @@ -8124,6 +8123,8 @@ snapshots: trim-lines@3.0.1: {} + ts-algebra@1.2.2: {} + tslib@2.8.1: {} tuple-result@0.0.11: {} From d745f04bb243f47febd353da5dd4884e3fccba15 Mon Sep 17 00:00:00 2001 From: Danylo Kachur Date: Fri, 13 Mar 2026 17:38:14 -0400 Subject: [PATCH 2/2] chore: add docs --- .changeset/add-itemschema-support.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/add-itemschema-support.md diff --git a/.changeset/add-itemschema-support.md b/.changeset/add-itemschema-support.md new file mode 100644 index 000000000..f3c96fcb6 --- /dev/null +++ b/.changeset/add-itemschema-support.md @@ -0,0 +1,5 @@ +--- +"openapi-typescript": minor +--- + +Add OpenAPI 3.2 `itemSchema` support for SSE and streaming responses