diff --git a/docs-yml.schema.json b/docs-yml.schema.json index 7cfb2cc33471..f30e9ccfa11b 100644 --- a/docs-yml.schema.json +++ b/docs-yml.schema.json @@ -2051,6 +2051,16 @@ } ] }, + "collapsed": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, "collapsible": { "oneOf": [ { diff --git a/fern-docs.schema.json b/fern-docs.schema.json index ed4261068cac..7327cfaae2dd 100644 --- a/fern-docs.schema.json +++ b/fern-docs.schema.json @@ -348,7 +348,17 @@ } }, "collapsed": { - "type": "boolean" + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "enum": [ + "open-by-default" + ] + } + ] }, "slug": { "type": "string" @@ -1409,4 +1419,4 @@ ] } } -} \ No newline at end of file +} diff --git a/fern/apis/docs-yml/definition/docs.yml b/fern/apis/docs-yml/definition/docs.yml index 72baa8340528..6f4347125568 100644 --- a/fern/apis/docs-yml/definition/docs.yml +++ b/fern/apis/docs-yml/definition/docs.yml @@ -1339,6 +1339,11 @@ types: icon: optional hidden: optional skip-slug: optional + collapsed: + type: optional + docs: | + Deprecated. Use `collapsible` and `collapsed-by-default` instead. + availability: deprecated collapsible: type: optional docs: Whether the section can be expanded/collapsed by the user in the sidebar. diff --git a/generators/csharp/sdk/src/SdkGeneratorCli.ts b/generators/csharp/sdk/src/SdkGeneratorCli.ts index 31682a026779..1494f5c3bab2 100644 --- a/generators/csharp/sdk/src/SdkGeneratorCli.ts +++ b/generators/csharp/sdk/src/SdkGeneratorCli.ts @@ -287,13 +287,23 @@ export class SdkGeneratorCLI extends AbstractCsharpGeneratorCli { endpointSnippets: snippets.endpoints }); } catch (e) { - context.logger.warn("Failed to generate README.md, this is OK."); + const errorMessage = e instanceof Error ? e.message : String(e); + const errorStack = e instanceof Error ? e.stack : undefined; + context.logger.warn(`Failed to generate README.md: ${errorMessage}`); + if (errorStack) { + context.logger.debug(`README.md generation error stack: ${errorStack}`); + } } try { await this.generateReference({ context }); } catch (e) { - context.logger.warn("Failed to generate reference.md, this is OK."); + const errorMessage = e instanceof Error ? e.message : String(e); + const errorStack = e instanceof Error ? e.stack : undefined; + context.logger.warn(`Failed to generate reference.md: ${errorMessage}`); + if (errorStack) { + context.logger.debug(`reference.md generation error stack: ${errorStack}`); + } } } context.logger.debug(`[TIMING] code generation took ${Date.now() - generateStartTime}ms`); diff --git a/generators/csharp/sdk/versions.yml b/generators/csharp/sdk/versions.yml index 90ca9e8509a8..ac8262aff7e1 100644 --- a/generators/csharp/sdk/versions.yml +++ b/generators/csharp/sdk/versions.yml @@ -1,4 +1,14 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 2.24.1 + changelogEntry: + - summary: | + Improve error logging for README.md and reference.md generation failures. + The generator now logs the actual error message and stack trace instead + of a generic "this is OK" message, making it easier to diagnose generation issues. + type: chore + createdAt: "2026-03-09" + irVersion: 65 + - version: 2.24.0 changelogEntry: - summary: | diff --git a/generators/go-v2/sdk/src/SdkGeneratorCli.ts b/generators/go-v2/sdk/src/SdkGeneratorCli.ts index 4f248fbbd776..4cf7b8f9049c 100644 --- a/generators/go-v2/sdk/src/SdkGeneratorCli.ts +++ b/generators/go-v2/sdk/src/SdkGeneratorCli.ts @@ -72,10 +72,11 @@ export class SdkGeneratorCLI extends AbstractGoGeneratorCli>", + "status": "optional", }, "source": { "openapi": "../openapi.yml", }, }, + "ErrorStatus": { + "enum": [ + "error", + ], + "inline": true, + "source": { + "openapi": "../openapi.yml", + }, + }, "FinalTranscript": { "docs": undefined, "extends": [ @@ -579,7 +588,7 @@ Valid values are in the range [0, 1] inclusive. "properties": { "message_type": { "docs": "Describes the type of message.", - "type": "literal<"FinalTranscript">", + "type": "FinalTranscriptMessageType", }, "punctuated": { "docs": "Whether the text has been punctuated and cased.", @@ -594,6 +603,16 @@ Valid values are in the range [0, 1] inclusive. "openapi": "../asyncapi.yml", }, }, + "FinalTranscriptMessageType": { + "docs": "Describes the type of message.", + "enum": [ + "FinalTranscript", + ], + "inline": true, + "source": { + "openapi": "../asyncapi.yml", + }, + }, "LemurActionItemsParameters": "LemurBaseParameters", "LemurActionItemsResponse": { "docs": undefined, @@ -840,13 +859,23 @@ Can be any value between 0.0 and 1.0 inclusive. "properties": { "message_type": { "docs": "Describes the type of message.", - "type": "literal<"PartialTranscript">", + "type": "PartialTranscriptMessageType", }, }, "source": { "openapi": "../asyncapi.yml", }, }, + "PartialTranscriptMessageType": { + "docs": "Describes the type of message.", + "enum": [ + "PartialTranscript", + ], + "inline": true, + "source": { + "openapi": "../asyncapi.yml", + }, + }, "PiiPolicy": { "enum": [ "medical_process", @@ -1128,7 +1157,7 @@ Can be any value between 0.0 and 1.0 inclusive. }, "message_type": { "docs": "Describes the type of the message.", - "type": "literal<"SessionBegins">", + "type": "SessionBeginsMessageType", }, "session_id": { "docs": "Unique identifier for the established session.", @@ -1139,19 +1168,39 @@ Can be any value between 0.0 and 1.0 inclusive. "openapi": "../asyncapi.yml", }, }, + "SessionBeginsMessageType": { + "docs": "Describes the type of the message.", + "enum": [ + "SessionBegins", + ], + "inline": true, + "source": { + "openapi": "../asyncapi.yml", + }, + }, "SessionTerminated": { "docs": undefined, "inline": undefined, "properties": { "message_type": { "docs": "Describes the type of the message.", - "type": "literal<"SessionTerminated">", + "type": "SessionTerminatedMessageType", }, }, "source": { "openapi": "../asyncapi.yml", }, }, + "SessionTerminatedMessageType": { + "docs": "Describes the type of the message.", + "enum": [ + "SessionTerminated", + ], + "inline": true, + "source": { + "openapi": "../asyncapi.yml", + }, + }, "SeverityScoreSummary": { "docs": undefined, "inline": undefined, @@ -3343,12 +3392,18 @@ types: (options: "basic" and "default"). source: openapi: ../openapi.yml + ErrorStatus: + enum: + - error + inline: true + source: + openapi: ../openapi.yml Error: properties: error: type: string docs: Error message - status: optional> + status: optional source: openapi: ../openapi.yml RealtimeBaseMessage: @@ -3394,10 +3449,17 @@ types: - FinalTranscript source: openapi: ../asyncapi.yml + SessionBeginsMessageType: + enum: + - SessionBegins + docs: Describes the type of the message. + inline: true + source: + openapi: ../asyncapi.yml SessionBegins: properties: message_type: - type: literal<"SessionBegins"> + type: SessionBeginsMessageType docs: Describes the type of the message. session_id: type: string @@ -3407,10 +3469,17 @@ types: docs: Timestamp when this session will expire. source: openapi: ../asyncapi.yml + SessionTerminatedMessageType: + enum: + - SessionTerminated + docs: Describes the type of the message. + inline: true + source: + openapi: ../asyncapi.yml SessionTerminated: properties: message_type: - type: literal<"SessionTerminated"> + type: SessionTerminatedMessageType docs: Describes the type of the message. source: openapi: ../asyncapi.yml @@ -3443,19 +3512,33 @@ types: docs: The timestamp for the partial transcript. source: openapi: ../asyncapi.yml + PartialTranscriptMessageType: + enum: + - PartialTranscript + docs: Describes the type of message. + inline: true + source: + openapi: ../asyncapi.yml PartialTranscript: properties: message_type: - type: literal<"PartialTranscript"> + type: PartialTranscriptMessageType docs: Describes the type of message. extends: - RealtimeBaseTranscript source: openapi: ../asyncapi.yml + FinalTranscriptMessageType: + enum: + - FinalTranscript + docs: Describes the type of message. + inline: true + source: + openapi: ../asyncapi.yml FinalTranscript: properties: message_type: - type: literal<"FinalTranscript"> + type: FinalTranscriptMessageType docs: Describes the type of message. punctuated: type: boolean diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/const.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/const.json index 9b925c09da79..7e1546e45387 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/const.json +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/const.json @@ -38,7 +38,7 @@ "properties": { "breed": "optional", "name": "string", - "type": "literal<"pet">", + "type": "CreatePetRequestType", }, }, "content-type": "application/json", @@ -62,6 +62,15 @@ }, }, "types": { + "CreatePetRequestType": { + "enum": [ + "pet", + ], + "inline": true, + "source": { + "openapi": "../openapi.yml", + }, + }, "Pet": { "docs": undefined, "inline": undefined, @@ -69,16 +78,62 @@ "breed": "optional", "id": "integer", "name": "string", - "status": "literal<"active">", - "type": "literal<"pet">", + "status": "PetStatus", + "type": "PetType", + }, + "source": { + "openapi": "../openapi.yml", + }, + }, + "PetStatus": { + "enum": [ + "active", + ], + "inline": true, + "source": { + "openapi": "../openapi.yml", }, + }, + "PetType": { + "enum": [ + "pet", + ], + "inline": true, "source": { "openapi": "../openapi.yml", }, }, }, }, - "rawContents": "service: + "rawContents": "types: + CreatePetRequestType: + enum: + - pet + inline: true + source: + openapi: ../openapi.yml + PetType: + enum: + - pet + inline: true + source: + openapi: ../openapi.yml + PetStatus: + enum: + - active + inline: true + source: + openapi: ../openapi.yml + Pet: + properties: + id: integer + type: PetType + name: string + breed: optional + status: PetStatus + source: + openapi: ../openapi.yml +service: auth: false base-path: '' endpoints: @@ -92,7 +147,7 @@ name: CreatePetRequest body: properties: - type: literal<"pet"> + type: CreatePetRequestType name: string breed: optional content-type: application/json @@ -113,16 +168,6 @@ status: active source: openapi: ../openapi.yml -types: - Pet: - properties: - id: integer - type: literal<"pet"> - name: string - breed: optional - status: literal<"active"> - source: - openapi: ../openapi.yml ", }, }, diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/readonly-ref-shared-schema.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/readonly-ref-shared-schema.json index 6d1be66ed3c3..8f0bf80d7dd1 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/readonly-ref-shared-schema.json +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/readonly-ref-shared-schema.json @@ -70,7 +70,7 @@ }, "content-type": "application/json", "headers": undefined, - "name": "OrgDetails", + "name": "UpdateOrgDetailsRequest", "path-parameters": undefined, "query-parameters": undefined, }, @@ -121,7 +121,6 @@ "pattern": "^org_[A-Za-z0-9]{16}$", }, }, - "UpdateOrgDetailsRequest": "OrgDetailsRead", "UpdateOrgDetailsResponse": "OrgDetailsRead", }, }, @@ -152,7 +151,7 @@ openapi: ../openapi.yml display-name: Update organization details request: - name: OrgDetails + name: UpdateOrgDetailsRequest body: properties: name: @@ -195,7 +194,6 @@ types: source: openapi: ../openapi.yml GetOrgDetailsResponse: OrgDetailsRead - UpdateOrgDetailsRequest: OrgDetailsRead UpdateOrgDetailsResponse: OrgDetailsRead ", }, diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern/src/buildEndpoint.ts b/packages/cli/api-importers/openapi/openapi-ir-to-fern/src/buildEndpoint.ts index ebb30871ff7b..bdb50aea6b9b 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-to-fern/src/buildEndpoint.ts +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern/src/buildEndpoint.ts @@ -522,6 +522,9 @@ function getRequest({ // When respectReadonlySchemas is enabled and the schema has readOnly properties on a write endpoint, // inline the properties so readOnly ones can be filtered out let shouldInlineForReadonly = false; + // When the request schema is a $ref wrapper (e.g., UpdateOrgDetailsRequest -> OrgDetails), + // track the wrapper's name so we can use it instead of the underlying schema's name + let readonlyWrapperSchemaName: string | undefined; if (context.options.respectReadonlySchemas && isWriteMethod(endpoint.method)) { let effectiveSchema = resolvedSchema; while (effectiveSchema?.type === "reference") { @@ -529,6 +532,11 @@ function getRequest({ } if (effectiveSchema?.type === "object" && effectiveSchema.properties.some((p) => p.readonly)) { shouldInlineForReadonly = true; + // If the resolved schema was a $ref wrapper pointing to the object with readonly props, + // preserve the wrapper's name for the request type + if (resolvedSchema?.type === "reference") { + readonlyWrapperSchemaName = resolvedSchema.nameOverride ?? resolvedSchema.generatedName; + } resolvedSchema = effectiveSchema; } } @@ -767,7 +775,11 @@ function getRequest({ } const convertedRequestValue: RawSchemas.HttpRequestSchema = { - name: requestNameOverride ?? resolvedSchema.nameOverride ?? resolvedSchema.generatedName, + name: + requestNameOverride ?? + readonlyWrapperSchemaName ?? + resolvedSchema.nameOverride ?? + resolvedSchema.generatedName, "path-parameters": pathParameters, "query-parameters": queryParameters, headers, @@ -780,7 +792,10 @@ function getRequest({ convertedRequestValue.docs = request.description; } return { - schemaIdsToExclude: maybeSchemaId != null && !shouldInlineForReadonly ? [maybeSchemaId] : [], + schemaIdsToExclude: + maybeSchemaId != null && (!shouldInlineForReadonly || readonlyWrapperSchemaName != null) + ? [maybeSchemaId] + : [], value: convertedRequestValue }; } else if (request.type === "octetStream") { diff --git a/packages/cli/cli-v2/src/__test__/check.test.ts b/packages/cli/cli-v2/src/__test__/check.test.ts index 09cdfdccc5f2..6a6575fd9644 100644 --- a/packages/cli/cli-v2/src/__test__/check.test.ts +++ b/packages/cli/cli-v2/src/__test__/check.test.ts @@ -3,39 +3,10 @@ import { randomUUID } from "crypto"; import { mkdir, rm, writeFile } from "fs/promises"; import { tmpdir } from "os"; import { join } from "path"; -import { Writable } from "stream"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { CheckCommand } from "../commands/check/command.js"; import { CheckCommand as SdkCheckCommand } from "../commands/sdk/check/command.js"; -import { Context } from "../context/Context.js"; - -function createCapturingStream(): { stream: NodeJS.WriteStream; getOutput: () => string } { - const chunks: Buffer[] = []; - const stream = new Writable({ - write(chunk, _encoding, callback) { - chunks.push(Buffer.from(chunk)); - callback(); - } - }) as NodeJS.WriteStream; - return { - stream, - getOutput: () => Buffer.concat(chunks as unknown as Uint8Array[]).toString("utf-8") - }; -} - -function createTestContextWithCapture({ cwd }: { cwd: AbsoluteFilePath }): { - context: Context; - getStdout: () => string; - getStderr: () => string; -} { - const stdout = createCapturingStream(); - const stderr = createCapturingStream(); - return { - context: new Context({ stdout: stdout.stream, stderr: stderr.stream, cwd }), - getStdout: stdout.getOutput, - getStderr: stderr.getOutput - }; -} +import { createTestContextWithCapture } from "./utils/createTestContext.js"; describe("fern check --json", () => { let testDir: AbsoluteFilePath; diff --git a/packages/cli/cli-v2/src/__test__/compile.test.ts b/packages/cli/cli-v2/src/__test__/compile.test.ts index 0c128407007d..030927ffcca3 100644 --- a/packages/cli/cli-v2/src/__test__/compile.test.ts +++ b/packages/cli/cli-v2/src/__test__/compile.test.ts @@ -2,44 +2,15 @@ import { AbsoluteFilePath, doesPathExist } from "@fern-api/fs-utils"; import { randomUUID } from "crypto"; import { readFile, rm } from "fs/promises"; import { join } from "path"; -import { Writable } from "stream"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { CompileCommand } from "../commands/api/compile/command.js"; -import { Context } from "../context/Context.js"; import { CliError } from "../errors/CliError.js"; +import { createTestContextWithCapture } from "./utils/createTestContext.js"; const FIXTURES_DIR = AbsoluteFilePath.of(join(__dirname, "fixtures")); const SIMPLE_API_DIR = AbsoluteFilePath.of(join(FIXTURES_DIR, "simple-api")); const INVALID_API_DIR = AbsoluteFilePath.of(join(FIXTURES_DIR, "invalid-api")); -function createCapturingStream(): { stream: NodeJS.WriteStream; getOutput: () => string } { - const chunks: Buffer[] = []; - const stream = new Writable({ - write(chunk, _encoding, callback) { - chunks.push(Buffer.from(chunk)); - callback(); - } - }) as NodeJS.WriteStream; - return { - stream, - getOutput: () => Buffer.concat(chunks as unknown as Uint8Array[]).toString("utf-8") - }; -} - -function createTestContextWithCapture({ cwd }: { cwd: AbsoluteFilePath }): { - context: Context; - getStdout: () => string; - getStderr: () => string; -} { - const stdout = createCapturingStream(); - const stderr = createCapturingStream(); - return { - context: new Context({ stdout: stdout.stream, stderr: stderr.stream, cwd }), - getStdout: stdout.getOutput, - getStderr: stderr.getOutput - }; -} - function baseArgs(overrides?: Partial): CompileCommand.Args { return { "log-level": "info", diff --git a/packages/cli/cli-v2/src/__test__/utils/createTestContext.ts b/packages/cli/cli-v2/src/__test__/utils/createTestContext.ts index 7deb16229d40..1eed6f4d3c8d 100644 --- a/packages/cli/cli-v2/src/__test__/utils/createTestContext.ts +++ b/packages/cli/cli-v2/src/__test__/utils/createTestContext.ts @@ -16,6 +16,41 @@ export function createTestContext({ cwd }: { cwd: AbsoluteFilePath }): Context { }); } +/** + * Create a Context whose stdout/stderr are captured for assertion. + */ +export function createTestContextWithCapture({ cwd }: { cwd: AbsoluteFilePath }): { + context: Context; + getStdout: () => string; + getStderr: () => string; +} { + const stdout = createCapturingStream(); + const stderr = createCapturingStream(); + return { + context: new Context({ stdout: stdout.stream, stderr: stderr.stream, cwd }), + getStdout: stdout.getOutput, + getStderr: stderr.getOutput + }; +} + +/** + * Create a writable stream that captures all written chunks so they can be + * inspected after the test completes. + */ +function createCapturingStream(): { stream: NodeJS.WriteStream; getOutput: () => string } { + const chunks: Buffer[] = []; + const stream = new Writable({ + write(chunk, _encoding, callback) { + chunks.push(Buffer.from(chunk)); + callback(); + } + }) as NodeJS.WriteStream; + return { + stream, + getOutput: () => Buffer.concat(chunks as unknown as Uint8Array[]).toString("utf-8") + }; +} + /** * A writable stream that discards all output. * Used for testing to suppress console output. diff --git a/packages/cli/cli-v2/src/commands/api/check/command.ts b/packages/cli/cli-v2/src/commands/api/check/command.ts index c6744d085763..76090921ac6d 100644 --- a/packages/cli/cli-v2/src/commands/api/check/command.ts +++ b/packages/cli/cli-v2/src/commands/api/check/command.ts @@ -50,9 +50,8 @@ export class CheckCommand { if (result.violations.length > 0) { for (const v of result.violations) { - process.stderr.write( - `${chalk.red(`${v.displayRelativeFilepath}:${v.line}:${v.column}: ${v.message}`)}\n` - ); + const color = v.severity === "warning" ? chalk.yellow : chalk.red; + process.stderr.write(`${color(`${v.displayRelativeFilepath}:${v.line}:${v.column}: ${v.message}`)}\n`); } } @@ -61,6 +60,7 @@ export class CheckCommand { } if (result.warningCount > 0) { + context.stderr.info(`${Icons.warning} ${chalk.yellow(`Found ${result.warningCount} warnings`)}`); context.stderr.info(chalk.dim(" Run 'fern api check --strict' to treat warnings as errors")); return; } diff --git a/packages/cli/cli-v2/src/commands/check/command.ts b/packages/cli/cli-v2/src/commands/check/command.ts index 5eb45260ad65..e3706fc203a1 100644 --- a/packages/cli/cli-v2/src/commands/check/command.ts +++ b/packages/cli/cli-v2/src/commands/check/command.ts @@ -70,7 +70,7 @@ export class CheckCommand { context, cliVersion: workspace.cliVersion }); - const apiCheckResult = await apiChecker.check({ workspace }); + const apiCheckResult = await apiChecker.check({ workspace, strict: args.strict }); const sdkChecker = new SdkChecker({ context }); const sdkCheckResult = await sdkChecker.check({ workspace }); @@ -90,10 +90,17 @@ export class CheckCommand { } private displayViolations( - violations: Array<{ displayRelativeFilepath: string; line: number; column: number; message: string }> + violations: Array<{ + displayRelativeFilepath: string; + line: number; + column: number; + message: string; + severity: string; + }> ): void { for (const v of violations) { - process.stderr.write(`${chalk.red(`${v.displayRelativeFilepath}:${v.line}:${v.column}: ${v.message}`)}\n`); + const color = v.severity === "warning" ? chalk.yellow : chalk.red; + process.stderr.write(`${color(`${v.displayRelativeFilepath}:${v.line}:${v.column}: ${v.message}`)}\n`); } } diff --git a/packages/cli/cli-v2/src/commands/sdk/check/command.ts b/packages/cli/cli-v2/src/commands/sdk/check/command.ts index 3df69167cb32..583a43b65aac 100644 --- a/packages/cli/cli-v2/src/commands/sdk/check/command.ts +++ b/packages/cli/cli-v2/src/commands/sdk/check/command.ts @@ -37,9 +37,8 @@ export class CheckCommand { if (result.violations.length > 0) { for (const v of result.violations) { - process.stderr.write( - `${chalk.red(`${v.displayRelativeFilepath}:${v.line}:${v.column}: ${v.message}`)}\n` - ); + const color = v.severity === "warning" ? chalk.yellow : chalk.red; + process.stderr.write(`${color(`${v.displayRelativeFilepath}:${v.line}:${v.column}: ${v.message}`)}\n`); } } @@ -48,6 +47,7 @@ export class CheckCommand { } if (result.warningCount > 0) { + context.stderr.info(`${Icons.warning} ${chalk.yellow(`Found ${result.warningCount} warnings`)}`); context.stderr.info(chalk.dim(" Run 'fern sdk check --strict' to treat warnings as errors")); return; } diff --git a/packages/cli/cli-v2/src/commands/sdk/command.ts b/packages/cli/cli-v2/src/commands/sdk/command.ts index 64e3cad9ea04..171fe9245bbf 100644 --- a/packages/cli/cli-v2/src/commands/sdk/command.ts +++ b/packages/cli/cli-v2/src/commands/sdk/command.ts @@ -4,6 +4,7 @@ import { commandGroup } from "../_internal/commandGroup.js"; import { addAddCommand } from "./add/index.js"; import { addCheckCommand } from "./check/index.js"; import { addGenerateCommand } from "./generate/index.js"; +import { addPreviewCommand } from "./preview/index.js"; import { addUpdateCommand } from "./update/index.js"; export function addSdkCommand(cli: Argv): void { @@ -11,6 +12,7 @@ export function addSdkCommand(cli: Argv): void { addAddCommand, addCheckCommand, addGenerateCommand, + addPreviewCommand, addUpdateCommand ]); } diff --git a/packages/cli/cli-v2/src/commands/sdk/generate/command.ts b/packages/cli/cli-v2/src/commands/sdk/generate/command.ts index 0fe8cdbf4c8b..1ede5f13316c 100644 --- a/packages/cli/cli-v2/src/commands/sdk/generate/command.ts +++ b/packages/cli/cli-v2/src/commands/sdk/generate/command.ts @@ -33,32 +33,26 @@ export declare namespace GenerateCommand { /** Path or URL to an API spec file (enables no-config mode) */ api?: string; - /** Organization name (required in no-config mode) */ - org?: string; + /** Filter by audiences */ + audience?: string[]; - /** The SDK target to generate */ - target?: string; + /** Container engine to use for local generation */ + "container-engine"?: ContainerRunner; - /** Override the generator version for the target */ - "target-version"?: string; + /** Force generation without prompts */ + force: boolean; /** Generator group to run (from fern.yml) */ group?: string; - /** Filter by audiences */ - audience?: string[]; + /** Whether to keep containers after completion */ + "keep-container": boolean; /** Whether to run the generator locally in a container */ local: boolean; - /** Container engine to use for local generation */ - "container-engine"?: ContainerRunner; - - /** Whether to keep containers after completion */ - "keep-container": boolean; - - /** Preview mode */ - preview: boolean; + /** Organization name (required in no-config mode) */ + org?: string; /** Output directory or git URL */ output?: string; @@ -66,8 +60,14 @@ export declare namespace GenerateCommand { /** Override the version for generated packages */ "output-version"?: string; - /** Force generation without prompts */ - force: boolean; + /** The SDK target to generate */ + target?: string; + + /** Override the generator version for the target */ + "target-version"?: string; + + /** Preview mode */ + preview: boolean; /** Path to .fernignore file */ fernignore?: string; @@ -604,45 +604,37 @@ export function addGenerateCommand(cli: Argv, parentPath?: string): type: "string", description: "Path or URL to an API spec file (enables no-config mode)" }) - .option("org", { - type: "string", - description: "Organization name (required with --api)" - }) - .option("target", { - type: "string", - description: "The SDK target to generate" - }) - .option("target-version", { - type: "string", - description: "The generator version for the target" - }) - .option("group", { - type: "string", - description: "The SDK group to generate" - }) .option("audience", { type: "array", string: true, description: "Filter the target API(s) with the given audience(s)" }) - .option("local", { - type: "boolean", - default: false, - description: "Run the generator locally in a container" - }) .option("container-engine", { choices: ["docker", "podman"], description: "Choose the container engine to use for local generation" }) + .option("force", { + type: "boolean", + default: false, + description: "Ignore prompts to confirm generation" + }) + .option("group", { + type: "string", + description: "The SDK group to generate" + }) .option("keep-container", { type: "boolean", default: false, description: "Prevent auto-deletion of any containers used for local generation" }) - .option("preview", { + .option("local", { type: "boolean", default: false, - description: "Generate a preview of the generated SDK in a local preview directory" + description: "Run the generator locally in a container" + }) + .option("org", { + type: "string", + description: "Organization name (required with --api)" }) .option("output", { type: "string", @@ -652,10 +644,21 @@ export function addGenerateCommand(cli: Argv, parentPath?: string): type: "string", description: "The version to use for the generated packages (e.g. 1.0.0)" }) - .option("force", { + .option("target", { + type: "string", + description: "The SDK target to generate" + }) + .option("target-version", { + type: "string", + description: "The generator version for the target" + }) + .option("preview", { + // This flag is still accepted for convenience (i.e. users migrating from the original CLI). + // Users should use `fern sdk preview` instead. type: "boolean", default: false, - description: "Ignore prompts to confirm generation" + description: "Generate a preview of the generated SDK in a local preview directory", + hidden: true }) .option("fernignore", { type: "string", diff --git a/packages/cli/cli-v2/src/commands/sdk/preview/command.ts b/packages/cli/cli-v2/src/commands/sdk/preview/command.ts new file mode 100644 index 000000000000..be7a45e690e1 --- /dev/null +++ b/packages/cli/cli-v2/src/commands/sdk/preview/command.ts @@ -0,0 +1,102 @@ +import type { Argv } from "yargs"; +import { GENERATE_COMMAND_TIMEOUT_MS } from "../../../constants.js"; +import type { Context } from "../../../context/Context.js"; +import type { GlobalArgs } from "../../../context/GlobalArgs.js"; +import { CliError } from "../../../errors/CliError.js"; +import { command } from "../../_internal/command.js"; +import { GenerateCommand } from "../generate/command.js"; + +export declare namespace PreviewCommand { + export type Args = Omit; +} + +export class PreviewCommand { + private readonly generateCommand = new GenerateCommand(); + + public async handle(context: Context, args: PreviewCommand.Args): Promise { + await this.generateCommand.handle(context, { + ...args, + preview: true + }); + } +} + +export function addPreviewCommand(cli: Argv, parentPath?: string): void { + const cmd = new PreviewCommand(); + command( + cli, + "preview", + "Generate a preview of an SDK", + async (context, args) => { + const timeout = new Promise((_, reject) => { + setTimeout( + () => reject(new CliError({ message: "Preview generation timed out after 10 minutes." })), + GENERATE_COMMAND_TIMEOUT_MS + ).unref(); + }); + await Promise.race([cmd.handle(context, args as PreviewCommand.Args), timeout]); + }, + (yargs) => + yargs + .option("api", { + type: "string", + description: "Path or URL to an API spec file (enables no-config mode)" + }) + .option("audience", { + type: "array", + string: true, + description: "Filter the target API(s) with the given audience(s)" + }) + .option("container-engine", { + choices: ["docker", "podman"], + description: "Choose the container engine to use for local generation" + }) + .option("force", { + type: "boolean", + default: false, + description: "Ignore prompts to confirm generation" + }) + .option("group", { + type: "string", + description: "The SDK group to generate" + }) + .option("keep-container", { + type: "boolean", + default: false, + description: "Prevent auto-deletion of any containers used for local generation" + }) + .option("local", { + type: "boolean", + default: false, + description: "Run the generator locally in a container" + }) + .option("org", { + type: "string", + description: "Organization name (required with --api)" + }) + + .option("output", { + type: "string", + description: "Output path or git URL (required with --api)" + }) + + .option("output-version", { + type: "string", + description: "The version to use for the generated packages (e.g. 1.0.0)" + }) + .option("target", { + type: "string", + description: "The SDK target to generate" + }) + .option("target-version", { + type: "string", + description: "The generator version for the target" + }) + .option("fernignore", { + type: "string", + description: "Path to .fernignore file", + hidden: true + }), + parentPath + ); +} diff --git a/packages/cli/cli-v2/src/commands/sdk/preview/index.ts b/packages/cli/cli-v2/src/commands/sdk/preview/index.ts new file mode 100644 index 000000000000..63415c508025 --- /dev/null +++ b/packages/cli/cli-v2/src/commands/sdk/preview/index.ts @@ -0,0 +1 @@ +export { addPreviewCommand } from "./command.js"; diff --git a/packages/cli/cli-v2/src/docs/adapter/LegacyDocsWorkspaceAdapter.ts b/packages/cli/cli-v2/src/docs/adapter/LegacyDocsWorkspaceAdapter.ts index d5d47d2c0e73..69d31bb94dea 100644 --- a/packages/cli/cli-v2/src/docs/adapter/LegacyDocsWorkspaceAdapter.ts +++ b/packages/cli/cli-v2/src/docs/adapter/LegacyDocsWorkspaceAdapter.ts @@ -3,12 +3,6 @@ import type { AbsoluteFilePath } from "@fern-api/fs-utils"; import type { DocsWorkspace } from "@fern-api/workspace-loader"; import type { DocsConfig } from "../config/DocsConfig.js"; -// Type alias for indexed access into the legacy configuration type. -type Legacy = docsYml.RawSchemas.DocsConfiguration; - -/** - * Adapts the DocsConfig to the legacy DocsWorkspace interface. - */ export class LegacyDocsWorkspaceAdapter { public adapt({ docsConfig, @@ -22,65 +16,7 @@ export class LegacyDocsWorkspaceAdapter { workspaceName: undefined, absoluteFilePath: absoluteFilePath, absoluteFilepathToDocsConfig: absoluteFilePath, - config: this.toLegacyRawConfig(docsConfig) - }; - } - - /** - * Constructs the legacy DocsConfiguration from the validated raw schema - * retained on DocsConfig._rawSchema. - * - * Each field is explicitly listed so the mapping is transparent. - * - Fields that are structurally identical pass through directly. - * - The Zod-inferred type and the legacy Fern-generated type have minor - * representational differences (enum literal sets, nested shape variants) - * are bridged via the Legacy type alias. - */ - private toLegacyRawConfig(config: DocsConfig): docsYml.RawSchemas.DocsConfiguration { - const raw = config._rawSchema; - - return { - instances: raw.instances.map((inst) => ({ - url: inst.url, - customDomain: inst.customDomain, - audiences: inst.audiences, - editThisPage: inst.editThisPage, - private: inst.private - })), - title: raw.title, - tabs: raw.tabs as Legacy["tabs"], - versions: raw.versions as Legacy["versions"], - products: raw.products as Legacy["products"], - landingPage: raw.landingPage as Legacy["landingPage"], - navigation: raw.navigation as Legacy["navigation"], - navbarLinks: raw.navbarLinks as Legacy["navbarLinks"], - footerLinks: raw.footerLinks as Legacy["footerLinks"], - pageActions: raw.pageActions as Legacy["pageActions"], - logo: raw.logo as Legacy["logo"], - favicon: raw.favicon, - backgroundImage: raw.backgroundImage as Legacy["backgroundImage"], - colors: raw.colors as Legacy["colors"], - typography: raw.typography as Legacy["typography"], - layout: raw.layout as Legacy["layout"], - settings: raw.settings, - theme: raw.theme as Legacy["theme"], - metadata: raw.metadata as Legacy["metadata"], - redirects: raw.redirects as Legacy["redirects"], - analytics: raw.analytics as Legacy["analytics"], - announcement: raw.announcement as Legacy["announcement"], - roles: raw.roles, - libraries: raw.libraries as Legacy["libraries"], - defaultLanguage: raw.defaultLanguage as Legacy["defaultLanguage"], - languages: raw.languages as Legacy["languages"], - css: raw.css, - js: raw.js as Legacy["js"], - aiChat: raw.aiChat as Legacy["aiChat"], - aiSearch: raw.aiSearch as Legacy["aiSearch"], - aiExamples: raw.aiExamples, - integrations: raw.integrations as Legacy["integrations"], - experimental: raw.experimental, - header: raw.header, - footer: raw.footer + config: docsConfig.raw as docsYml.RawSchemas.DocsConfiguration }; } } diff --git a/packages/cli/cli-v2/src/docs/config/DocsConfig.ts b/packages/cli/cli-v2/src/docs/config/DocsConfig.ts index ccf2663c3f99..86a78b3a2ad2 100644 --- a/packages/cli/cli-v2/src/docs/config/DocsConfig.ts +++ b/packages/cli/cli-v2/src/docs/config/DocsConfig.ts @@ -1,67 +1,9 @@ import type { schemas } from "@fern-api/config"; -import type { Navigation, NavigationItem } from "./Navigation.js"; -import type { Product } from "./Product.js"; -import type { SnippetLanguage } from "./SnippetLanguage.js"; -import type { Version } from "./Version.js"; /** - * Rich docs configuration derived from fern.yml. - * - * Pass-through fields use schema types (`schemas.X`) directly from - * @fern-api/config. Custom interfaces are only defined for shapes - * that have post-validation transformations (e.g., discriminated unions). + * Validated docs configuration. For now, this delegates entirely to the + * legacy docs pipeline using the raw schema shape. */ export interface DocsConfig { - instances: schemas.DocsInstanceSchema[]; - title?: string; - - navigation?: Navigation; - landingPage?: NavigationItem.Page; - - tabs?: Record; - versions?: Version[]; - products?: Product[]; - - logo?: schemas.LogoConfigurationSchema; - favicon?: string; - backgroundImage?: schemas.BackgroundImageConfigurationSchema; - colors?: schemas.ColorsConfigurationSchema; - typography?: schemas.TypographyConfigSchema; - - layout?: schemas.LayoutConfigSchema; - settings?: schemas.DocsSettingsConfigSchema; - theme?: schemas.ThemeConfigSchema; - - navbarLinks: schemas.NavbarLinkSchema[]; - footerLinks?: schemas.FooterLinksConfigSchema; - pageActions?: schemas.PageActionsConfigSchema; - - metadata?: schemas.MetadataConfigSchema; - redirects: schemas.RedirectConfigSchema[]; - analytics?: schemas.AnalyticsConfigSchema; - - announcement?: schemas.AnnouncementConfigSchema; - roles?: string[]; - libraries?: Record; - - defaultLanguage?: SnippetLanguage; - languages?: string[]; - - css?: schemas.CssConfigSchema; - js?: schemas.JsConfigSchema; - - aiChat?: schemas.AiChatConfigSchema; - aiSearch?: schemas.AiChatConfigSchema; - aiExamples?: schemas.AiExamplesConfigSchema; - - integrations?: schemas.IntegrationsConfigSchema; - experimental?: Record; - - header?: string; - footer?: string; - - /** - * @internal Validated raw schema retained for the legacy adapter bridge. - */ - _rawSchema: schemas.DocsSchema; + raw: schemas.DocsSchema; } diff --git a/packages/cli/cli-v2/src/docs/config/Navigation.ts b/packages/cli/cli-v2/src/docs/config/Navigation.ts deleted file mode 100644 index ad43fffcb7f2..000000000000 --- a/packages/cli/cli-v2/src/docs/config/Navigation.ts +++ /dev/null @@ -1,129 +0,0 @@ -import type { schemas } from "@fern-api/config"; - -/** - * Docs navigation configuration. Discriminated by `type`. - * - * In the raw YAML, untabbed navigation is a flat array of items and - * tabbed navigation is an array of tabbed items. Here we provide - * a proper discriminated union with explicit `type` tags. - */ -export type Navigation = UntabbedNavigation | TabbedNavigation; - -export interface UntabbedNavigation { - type: "untabbed"; - items: NavigationItem[]; -} - -export interface TabbedNavigation { - type: "tabbed"; - items: TabbedNavigationItem[]; -} - -/** - * Navigation item discriminated union. Unlike the raw YAML which uses - * key-presence discrimination (e.g., `page:` vs `section:`), these - * use an explicit `type` field. - */ -export type NavigationItem = - | NavigationItem.Page - | NavigationItem.Section - | NavigationItem.ApiReference - | NavigationItem.Link - | NavigationItem.Changelog - | NavigationItem.Library - | NavigationItem.Folder; - -export declare namespace NavigationItem { - interface Page { - type: "page"; - /** Relative path to the page file */ - path: string; - title?: string; - slug?: string; - icon?: string; - hidden?: boolean; - noindex?: boolean; - } - - interface Section { - type: "section"; - title: string; - contents: NavigationItem[]; - collapsed?: boolean; - slug?: string; - icon?: string; - hidden?: boolean; - skipSlug?: boolean; - /** Relative path to section overview page */ - overview?: string; - } - - interface ApiReference { - type: "apiReference"; - title?: string; - api?: string; - apiName?: string; - slug?: string; - icon?: string; - hidden?: boolean; - audiences?: string[]; - showErrors?: boolean; - snippets?: schemas.SnippetsConfigurationSchema; - playground?: schemas.PlaygroundSettingsSchema; - collapsed?: boolean; - alphabetized?: boolean; - flattened?: boolean; - paginated?: boolean; - /** Relative path to overview page */ - overview?: string; - } - - interface Link { - type: "link"; - text: string; - href: string; - icon?: string; - } - - interface Changelog { - type: "changelog"; - /** Relative path(s) to changelog directory */ - path: string | string[]; - title?: string; - slug?: string; - icon?: string; - hidden?: boolean; - } - - interface Library { - type: "library"; - name: string; - title?: string; - slug?: string; - } - - interface Folder { - type: "folder"; - title: string; - contents: NavigationItem[]; - collapsed?: boolean; - slug?: string; - icon?: string; - hidden?: boolean; - } -} - -export interface TabbedNavigationItem { - tab: string; - layout?: NavigationItem[]; - variants?: TabVariant[]; -} - -export interface TabVariant { - title: string; - subtitle?: string; - icon?: string; - layout: NavigationItem[]; - slug?: string; - default?: boolean; -} diff --git a/packages/cli/cli-v2/src/docs/config/Product.ts b/packages/cli/cli-v2/src/docs/config/Product.ts deleted file mode 100644 index f17af2b0de2f..000000000000 --- a/packages/cli/cli-v2/src/docs/config/Product.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Navigation } from "./Navigation.js"; -import type { Version } from "./Version.js"; - -export type Product = InternalProduct | ExternalProduct; - -export interface InternalProduct { - type: "internal"; - displayName: string; - icon?: string; - slug?: string; - path?: string; - default?: boolean; - navigation?: Navigation; - versions?: Version[]; -} - -export interface ExternalProduct { - type: "external"; - displayName: string; - href: string; - icon?: string; -} diff --git a/packages/cli/cli-v2/src/docs/config/SnippetLanguage.ts b/packages/cli/cli-v2/src/docs/config/SnippetLanguage.ts deleted file mode 100644 index a69ec7ff3b4c..000000000000 --- a/packages/cli/cli-v2/src/docs/config/SnippetLanguage.ts +++ /dev/null @@ -1 +0,0 @@ -export type SnippetLanguage = "typescript" | "python" | "java" | "go" | "ruby" | "csharp" | "php" | "swift" | "curl"; diff --git a/packages/cli/cli-v2/src/docs/config/Version.ts b/packages/cli/cli-v2/src/docs/config/Version.ts deleted file mode 100644 index 2b619f6f24a4..000000000000 --- a/packages/cli/cli-v2/src/docs/config/Version.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { Navigation } from "./Navigation.js"; - -export interface Version { - displayName: string; - path: string; - slug?: string; - availability?: string; - navigation: Navigation; -} diff --git a/packages/cli/cli-v2/src/docs/config/converter/DocsConfigConverter.ts b/packages/cli/cli-v2/src/docs/config/converter/DocsConfigConverter.ts index fc7b61177597..5ca6666da61c 100644 --- a/packages/cli/cli-v2/src/docs/config/converter/DocsConfigConverter.ts +++ b/packages/cli/cli-v2/src/docs/config/converter/DocsConfigConverter.ts @@ -2,9 +2,6 @@ import type { schemas } from "@fern-api/config"; import { ValidationIssue } from "@fern-api/yaml-loader"; import type { FernYmlSchemaLoader } from "../../../config/fern-yml/FernYmlSchemaLoader.js"; import type { DocsConfig } from "../DocsConfig.js"; -import type { Navigation, NavigationItem, TabbedNavigationItem, TabVariant } from "../Navigation.js"; -import type { Product } from "../Product.js"; -import type { Version } from "../Version.js"; export namespace DocsConfigConverter { export type Result = Success | Failure; @@ -20,92 +17,24 @@ export namespace DocsConfigConverter { } } -/** - * Converts a fern.yml schema to the DocsConfig domain model. - * - * Performs: - * - Domain-specific validation (instances, navigation structure) - * - Navigation key-presence -> discriminated union conversion - * - Version/product structural conversion (contain Navigation) - * - * Pass-through fields (branding, layout, SEO, etc.) are assigned directly - * from the schema without conversion. - * - * The raw schema is retained on the output for the legacy adapter bridge. - */ export class DocsConfigConverter { private readonly issues: ValidationIssue[] = []; - public convert({ fernYml }: { fernYml: FernYmlSchemaLoader.Success }): DocsConfigConverter.Result { const docs = fernYml.data.docs; if (docs == null) { - return { - success: false, - issues: [] - }; + return { success: false, issues: [] }; } this.validateInstances({ docs, fernYml }); if (this.issues.length > 0) { - return { - success: false, - issues: this.issues - }; + return { success: false, issues: this.issues }; } return { success: true, config: { - instances: docs.instances, - title: docs.title, - - navigation: docs.navigation != null ? this.convertNavigation(docs.navigation) : undefined, - landingPage: docs.landingPage != null ? this.convertPage(docs.landingPage) : undefined, - - tabs: docs.tabs, - versions: docs.versions != null ? this.convertVersions(docs.versions) : undefined, - products: docs.products != null ? this.convertProducts(docs.products) : undefined, - - logo: docs.logo, - favicon: docs.favicon, - backgroundImage: docs.backgroundImage, - colors: docs.colors, - typography: docs.typography, - - layout: docs.layout, - settings: docs.settings, - theme: docs.theme, - - navbarLinks: docs.navbarLinks ?? [], - footerLinks: docs.footerLinks, - pageActions: docs.pageActions, - - metadata: docs.metadata, - redirects: docs.redirects ?? [], - analytics: docs.analytics, - - announcement: docs.announcement, - roles: docs.roles, - libraries: docs.libraries, - - defaultLanguage: docs.defaultLanguage, - languages: docs.languages, - - css: docs.css, - js: docs.js, - - aiChat: docs.aiChat, - aiSearch: docs.aiSearch, - aiExamples: docs.aiExamples, - - integrations: docs.integrations, - experimental: docs.experimental, - - header: docs.header, - footer: docs.footer, - - _rawSchema: docs + raw: docs } }; } @@ -126,199 +55,4 @@ export class DocsConfigConverter { ); } } - - private convertNavigation(raw: schemas.NavigationConfigSchema): Navigation { - if (raw.length === 0) { - return { type: "untabbed", items: [] }; - } - const first = raw[0]; - if (first != null && "tab" in first) { - return { - type: "tabbed", - items: (raw as schemas.TabbedNavigationConfigSchema).map((item) => - this.convertTabbedNavigationItem(item) - ) - }; - } - return { - type: "untabbed", - items: (raw as schemas.UntabbedNavigationConfigSchema).map((item) => this.convertNavigationItem(item)) - }; - } - - private convertNavigationItem(raw: schemas.NavigationItem): NavigationItem { - if ("page" in raw) { - return this.convertPage(raw as schemas.PageConfigurationSchema); - } - if ("section" in raw) { - return this.convertSection(raw as schemas.SectionConfigurationSchema); - } - if ("api" in raw) { - return this.convertApiReference(raw as schemas.ApiReferenceConfigurationSchema); - } - if ("link" in raw) { - return this.convertLink(raw as schemas.LinkConfigurationSchema); - } - if ("changelog" in raw) { - return this.convertChangelog(raw as schemas.ChangelogConfigurationSchema); - } - if ("library" in raw) { - return this.convertLibrary(raw as schemas.LibraryReferenceConfigurationSchema); - } - if ("folder" in raw) { - return this.convertFolder(raw as schemas.FolderConfigurationSchema); - } - throw new Error(`Unknown navigation item type: ${JSON.stringify(raw)}`); - } - - private convertPage(raw: schemas.PageConfigurationSchema): NavigationItem.Page { - return { - type: "page", - path: raw.path ?? raw.page, - title: raw.path != null ? raw.page : undefined, - slug: raw.slug, - icon: raw.icon, - hidden: raw.hidden, - noindex: raw.noindex - }; - } - - private convertSection(raw: schemas.SectionConfigurationSchema): NavigationItem.Section { - return { - type: "section", - title: raw.section, - contents: (raw.contents as schemas.NavigationItem[]).map((item) => this.convertNavigationItem(item)), - collapsed: raw.collapsed, - slug: raw.slug, - icon: raw.icon, - hidden: raw.hidden, - skipSlug: raw.skipSlug, - overview: raw.path - }; - } - - private convertApiReference(raw: schemas.ApiReferenceConfigurationSchema): NavigationItem.ApiReference { - return { - type: "apiReference", - title: raw.title, - api: raw.api, - apiName: raw.apiName, - slug: raw.slug, - icon: raw.icon, - hidden: raw.hidden, - audiences: raw.audiences, - showErrors: raw.showErrors, - snippets: raw.snippets, - playground: raw.playground, - alphabetized: raw.alphabetized, - flattened: raw.flattened, - paginated: raw.paginated - }; - } - - private convertLink(raw: schemas.LinkConfigurationSchema): NavigationItem.Link { - return { - type: "link", - text: raw.link, - href: raw.href, - icon: raw.icon - }; - } - - private convertChangelog(raw: schemas.ChangelogConfigurationSchema): NavigationItem.Changelog { - return { - type: "changelog", - path: raw.changelog, - title: raw.title, - slug: raw.slug, - icon: raw.icon, - hidden: raw.hidden - }; - } - - private convertLibrary(raw: schemas.LibraryReferenceConfigurationSchema): NavigationItem.Library { - return { - type: "library", - name: raw.library, - title: raw.title, - slug: raw.slug - }; - } - - private convertFolder(raw: schemas.FolderConfigurationSchema): NavigationItem.Folder { - return { - type: "folder", - title: raw.folder, - contents: [], - collapsed: raw.collapsed, - slug: raw.slug, - icon: raw.icon, - hidden: raw.hidden - }; - } - - private convertTabbedNavigationItem(raw: schemas.TabbedNavigationItemSchema): TabbedNavigationItem { - return { - tab: raw.tab, - layout: - raw.layout != null - ? (raw.layout as schemas.NavigationItem[]).map((item) => this.convertNavigationItem(item)) - : undefined, - variants: raw.variants != null ? raw.variants.map((v) => this.convertTabVariant(v)) : undefined - }; - } - - private convertTabVariant(raw: schemas.TabVariantSchema): TabVariant { - return { - title: raw.title, - subtitle: raw.subtitle, - icon: raw.icon, - layout: (raw.layout as schemas.NavigationItem[]).map((item) => this.convertNavigationItem(item)), - slug: raw.slug, - default: raw.default - }; - } - - // ================================================================ - // Versions & Products - // ================================================================ - - private convertVersions(raw: schemas.VersionConfigSchema[]): Version[] { - return raw.map((version) => this.convertVersion(version)); - } - - private convertVersion(raw: schemas.VersionConfigSchema): Version { - return { - displayName: raw.displayName, - path: raw.path, - slug: raw.slug, - availability: raw.availability, - navigation: this.convertNavigation(raw.navigation) - }; - } - - private convertProducts(raw: schemas.ProductConfigSchema[]): Product[] { - return raw.map((product) => this.convertProduct(product)); - } - - private convertProduct(raw: schemas.ProductConfigSchema): Product { - if ("href" in raw) { - return { - type: "external", - displayName: raw.displayName, - href: raw.href as string, - icon: raw.icon - }; - } - return { - type: "internal", - displayName: raw.displayName, - path: raw.path, - icon: raw.icon, - slug: raw.slug, - default: raw.default, - navigation: raw.navigation != null ? this.convertNavigation(raw.navigation) : undefined, - versions: raw.versions != null ? this.convertVersions(raw.versions) : undefined - }; - } } diff --git a/packages/cli/cli/src/cli.ts b/packages/cli/cli/src/cli.ts index bed8c785a571..f85305b1cc25 100644 --- a/packages/cli/cli/src/cli.ts +++ b/packages/cli/cli/src/cli.ts @@ -55,6 +55,7 @@ import { generateOpenAPIIrForWorkspaces } from "./commands/generate-openapi-ir/g import { compareOpenAPISpecs } from "./commands/generate-overrides/compareOpenAPISpecs.js"; import { writeOverridesForWorkspaces } from "./commands/generate-overrides/writeOverridesForWorkspaces.js"; import { generateJsonschemaForWorkspaces } from "./commands/jsonschema/generateJsonschemaForWorkspace.js"; +import { mergeOpenAPIWithOverrides } from "./commands/merge/mergeOpenAPIWithOverrides.js"; import { mockServer } from "./commands/mock/mockServer.js"; import { registerWorkspacesV1 } from "./commands/register/registerWorkspacesV1.js"; import { registerWorkspacesV2 } from "./commands/register/registerWorkspacesV2.js"; @@ -215,7 +216,7 @@ async function tryRunCli(cliContext: CliContext) { addOverridesCommand(cli, cliContext); addWriteOverridesCommand(cli, cliContext); // Deprecated: use `fern overrides write` instead addTestCommand(cli, cliContext); - addUpdateApiSpecCommand(cli, cliContext); + addApiCommand(cli, cliContext); addSelfUpdateCommand(cli, cliContext); addUpgradeCommand({ cli, @@ -1237,9 +1238,17 @@ function addDowngradeCommand(cli: Argv, cliContext: CliContext ); } +function addApiCommand(cli: Argv, cliContext: CliContext) { + cli.command("api", "Commands for managing your API specs", (yargs) => { + addUpdateApiSpecCommand(yargs, cliContext); + addEnrichCommand(yargs, cliContext); + return yargs; + }); +} + function addUpdateApiSpecCommand(cli: Argv, cliContext: CliContext) { cli.command( - "api update", + "update", `Pulls the latest OpenAPI spec from the specified origin in ${GENERATORS_CONFIGURATION_FILENAME} and updates the local spec.`, (yargs) => yargs @@ -1974,6 +1983,46 @@ function addExportCommand(cli: Argv, cliContext: CliContext) { ); } +function addEnrichCommand(cli: Argv, cliContext: CliContext) { + cli.command( + "enrich ", + false, // Hidden from --help + (yargs) => + yargs + .positional("openapi", { + type: "string", + description: "Path to the OpenAPI spec", + demandOption: true + }) + .option("file", { + type: "string", + alias: "f", + description: "Path to the overrides file (e.g. ai_examples_overrides.yml)", + demandOption: true + }) + .option("output", { + type: "string", + alias: "o", + description: "Path to write the enriched output file", + demandOption: true + }), + async (argv) => { + await cliContext.instrumentPostHogEvent({ + command: "fern api enrich" + }); + const openapiPath = resolve(cwd(), argv.openapi as string); + const overridesPath = resolve(cwd(), argv.file); + const outputPath = resolve(cwd(), argv.output); + await mergeOpenAPIWithOverrides({ + openapiPath: AbsoluteFilePath.of(openapiPath), + overridesPath: AbsoluteFilePath.of(overridesPath), + outputPath: AbsoluteFilePath.of(outputPath), + cliContext + }); + } + ); +} + function addBetaCommand(cli: Argv, cliContext: CliContext) { cli.command( "beta", diff --git a/packages/cli/cli/src/commands/merge/__test__/mergeOpenAPIWithOverrides.test.ts b/packages/cli/cli/src/commands/merge/__test__/mergeOpenAPIWithOverrides.test.ts new file mode 100644 index 000000000000..3292691f1ff7 --- /dev/null +++ b/packages/cli/cli/src/commands/merge/__test__/mergeOpenAPIWithOverrides.test.ts @@ -0,0 +1,421 @@ +import { noop } from "lodash-es"; +import { describe, expect, it } from "vitest"; + +import { findMatchingSpecPath, mergeExamplesIntoSpec } from "../mergeOpenAPIWithOverrides.js"; + +function createMockContext() { + return { + logger: { + disable: noop, + enable: noop, + trace: noop, + debug: noop, + info: noop, + warn: noop, + error: noop, + log: noop + } + }; +} + +describe("findMatchingSpecPath", () => { + it("returns exact match when available", () => { + const specPaths = { "/users/{username}": {}, "/orders": {} }; + expect(findMatchingSpecPath("/users/{username}", specPaths)).toBe("/users/{username}"); + }); + + it("matches bare param name to templated path", () => { + const specPaths = { "/users/{username}": {}, "/orders": {} }; + expect(findMatchingSpecPath("/users/username", specPaths)).toBe("/users/{username}"); + }); + + it("matches multiple param segments", () => { + const specPaths = { "/customers/{customerId}/orders/{orderId}": {} }; + expect(findMatchingSpecPath("/customers/customerId/orders/orderId", specPaths)).toBe( + "/customers/{customerId}/orders/{orderId}" + ); + }); + + it("returns undefined for non-matching paths", () => { + const specPaths = { "/users/{username}": {} }; + expect(findMatchingSpecPath("/products/productId", specPaths)).toBeUndefined(); + }); + + it("returns undefined when segment count differs", () => { + const specPaths = { "/users/{username}/profile": {} }; + expect(findMatchingSpecPath("/users/username", specPaths)).toBeUndefined(); + }); +}); + +describe("mergeExamplesIntoSpec", () => { + const context = createMockContext(); + + it("decomposes path parameter examples into native fields", () => { + const spec = { + openapi: "3.1.0", + paths: { + "/users/{username}": { + get: { + parameters: [{ name: "username", in: "path", required: true, schema: { type: "string" } }], + responses: { "200": { description: "OK" } } + } + } + } + }; + const overrides = { + paths: { + "/users/username": { + get: { + "x-fern-examples": [ + { + "path-parameters": { username: "john_doe" } + } + ] + } + } + } + }; + + const result = mergeExamplesIntoSpec(spec, overrides, context); + expect(result.paths["/users/{username}"].get.parameters[0].example).toBe("john_doe"); + }); + + it("decomposes query parameter examples into native fields", () => { + const spec = { + openapi: "3.1.0", + paths: { + "/users": { + get: { + parameters: [{ name: "limit", in: "query", schema: { type: "integer" } }], + responses: { "200": { description: "OK" } } + } + } + } + }; + const overrides = { + paths: { + "/users": { + get: { + "x-fern-examples": [ + { + "query-parameters": { limit: 10 } + } + ] + } + } + } + }; + + const result = mergeExamplesIntoSpec(spec, overrides, context); + expect(result.paths["/users"].get.parameters[0].example).toBe(10); + }); + + it("decomposes header examples into native fields", () => { + const spec = { + openapi: "3.1.0", + paths: { + "/users": { + get: { + parameters: [{ name: "X-Request-Id", in: "header", schema: { type: "string" } }], + responses: { "200": { description: "OK" } } + } + } + } + }; + const overrides = { + paths: { + "/users": { + get: { + "x-fern-examples": [ + { + headers: { "X-Request-Id": "req-abc-123" } + } + ] + } + } + } + }; + + const result = mergeExamplesIntoSpec(spec, overrides, context); + expect(result.paths["/users"].get.parameters[0].example).toBe("req-abc-123"); + }); + + it("decomposes request body examples into native fields", () => { + const spec = { + openapi: "3.1.0", + paths: { + "/users": { + post: { + requestBody: { + content: { "application/json": { schema: { type: "object" } } } + }, + responses: { "201": { description: "Created" } } + } + } + } + }; + const overrides = { + paths: { + "/users": { + post: { + "x-fern-examples": [ + { + request: { body: { name: "John", email: "john@example.com" } } + } + ] + } + } + } + }; + + const result = mergeExamplesIntoSpec(spec, overrides, context); + expect(result.paths["/users"].post.requestBody.content["application/json"].example).toEqual({ + name: "John", + email: "john@example.com" + }); + }); + + it("decomposes response body examples into native fields", () => { + const spec = { + openapi: "3.1.0", + paths: { + "/users/{username}": { + get: { + parameters: [{ name: "username", in: "path", required: true, schema: { type: "string" } }], + responses: { + "200": { + description: "OK", + content: { + "application/json": { + schema: { type: "object" } + } + } + } + } + } + } + } + }; + const overrides = { + paths: { + "/users/username": { + get: { + "x-fern-examples": [ + { + response: { body: { id: 1, username: "john_doe", email: "john@example.com" } } + } + ] + } + } + } + }; + + const result = mergeExamplesIntoSpec(spec, overrides, context); + expect(result.paths["/users/{username}"].get.responses["200"].content["application/json"].example).toEqual({ + id: 1, + username: "john_doe", + email: "john@example.com" + }); + }); + + it("strips x-fern-examples from output", () => { + const spec = { + openapi: "3.1.0", + paths: { + "/users": { + get: { + parameters: [], + responses: { "200": { description: "OK" } } + } + } + } + }; + const overrides = { + paths: { + "/users": { + get: { + "x-fern-examples": [{ response: { body: { id: 1 } } }] + } + } + } + }; + + const result = mergeExamplesIntoSpec(spec, overrides, context); + expect(result.paths["/users"].get["x-fern-examples"]).toBeUndefined(); + }); + + it("preserves full spec structure after merge", () => { + const spec = { + openapi: "3.1.0", + info: { title: "Test API", version: "1.0.0" }, + servers: [{ url: "https://api.example.com" }], + paths: { + "/health": { + get: { responses: { "200": { description: "OK" } } } + } + }, + components: { + schemas: { + User: { type: "object", properties: { id: { type: "integer" } } } + } + } + }; + const overrides = { paths: {} }; + + const result = mergeExamplesIntoSpec(spec, overrides, context); + expect(result.openapi).toBe("3.1.0"); + expect(result.info).toEqual({ title: "Test API", version: "1.0.0" }); + expect(result.servers).toEqual([{ url: "https://api.example.com" }]); + expect(result.components.schemas.User).toBeDefined(); + }); + + it("handles multiple examples with named entries", () => { + const spec = { + openapi: "3.1.0", + paths: { + "/users/{username}": { + get: { + parameters: [{ name: "username", in: "path", required: true, schema: { type: "string" } }], + responses: { + "200": { + description: "OK", + content: { "application/json": { schema: { type: "object" } } } + } + } + } + } + } + }; + const overrides = { + paths: { + "/users/username": { + get: { + "x-fern-examples": [ + { + name: "admin", + "path-parameters": { username: "admin_user" }, + response: { body: { id: 1, username: "admin_user" } } + }, + { + name: "regular", + "path-parameters": { username: "regular_user" }, + response: { body: { id: 2, username: "regular_user" } } + } + ] + } + } + } + }; + + const result = mergeExamplesIntoSpec(spec, overrides, context); + const param = result.paths["/users/{username}"].get.parameters[0]; + expect(param.examples.admin.value).toBe("admin_user"); + expect(param.examples.regular.value).toBe("regular_user"); + + const responseContent = result.paths["/users/{username}"].get.responses["200"].content["application/json"]; + expect(responseContent.examples.admin.value).toEqual({ id: 1, username: "admin_user" }); + expect(responseContent.examples.regular.value).toEqual({ id: 2, username: "regular_user" }); + }); + + it("resolves $ref parameters before applying examples", () => { + const spec = { + openapi: "3.1.0", + paths: { + "/orders/{orderId}": { + get: { + parameters: [{ $ref: "#/components/parameters/OrderId" }], + responses: { "200": { description: "OK" } } + } + } + }, + components: { + parameters: { + OrderId: { name: "orderId", in: "path", required: true, schema: { type: "string" } } + } + } + }; + const overrides = { + paths: { + "/orders/orderId": { + get: { + "x-fern-examples": [ + { + "path-parameters": { orderId: "ORD-12345" } + } + ] + } + } + } + }; + + const result = mergeExamplesIntoSpec(spec, overrides, context); + expect(result.paths["/orders/{orderId}"].get.parameters[0].example).toBe("ORD-12345"); + expect(result.paths["/orders/{orderId}"].get.parameters[0].name).toBe("orderId"); + }); + + it("handles all example types in a single operation", () => { + const spec = { + openapi: "3.1.0", + paths: { + "/customers/{customerId}": { + put: { + parameters: [ + { name: "customerId", in: "path", required: true, schema: { type: "string" } }, + { name: "include_audit", in: "query", schema: { type: "boolean" } }, + { name: "X-Idempotency-Key", in: "header", schema: { type: "string" } } + ], + requestBody: { + content: { "application/json": { schema: { type: "object" } } } + }, + responses: { + "200": { + description: "OK", + content: { "application/json": { schema: { type: "object" } } } + } + } + } + } + } + }; + const overrides = { + paths: { + "/customers/customerId": { + put: { + "x-fern-examples": [ + { + "path-parameters": { customerId: "cust-999" }, + "query-parameters": { include_audit: true }, + headers: { "X-Idempotency-Key": "key-abc" }, + request: { body: { name: "Acme Corp", email: "acme@example.com" } }, + response: { body: { id: "cust-999", name: "Acme Corp", status: "updated" } } + } + ] + } + } + } + }; + + const result = mergeExamplesIntoSpec(spec, overrides, context); + const op = result.paths["/customers/{customerId}"].put; + + // Path param + expect(op.parameters[0].example).toBe("cust-999"); + // Query param + expect(op.parameters[1].example).toBe(true); + // Header + expect(op.parameters[2].example).toBe("key-abc"); + // Request body + expect(op.requestBody.content["application/json"].example).toEqual({ + name: "Acme Corp", + email: "acme@example.com" + }); + // Response body + expect(op.responses["200"].content["application/json"].example).toEqual({ + id: "cust-999", + name: "Acme Corp", + status: "updated" + }); + // No x-fern-examples + expect(op["x-fern-examples"]).toBeUndefined(); + }); +}); diff --git a/packages/cli/cli/src/commands/merge/mergeOpenAPIWithOverrides.ts b/packages/cli/cli/src/commands/merge/mergeOpenAPIWithOverrides.ts new file mode 100644 index 000000000000..8f75d577b6e9 --- /dev/null +++ b/packages/cli/cli/src/commands/merge/mergeOpenAPIWithOverrides.ts @@ -0,0 +1,450 @@ +import { AbsoluteFilePath, doesPathExist } from "@fern-api/fs-utils"; +import { readFile, writeFile } from "fs/promises"; +import yaml from "js-yaml"; + +import { CliContext } from "../../cli-context/CliContext.js"; + +// biome-ignore lint/suspicious/noExplicitAny: OpenAPI specs can have any shape +type OpenAPISpec = Record; + +const HTTP_METHODS = new Set(["get", "put", "post", "delete", "options", "head", "patch", "trace"]); + +/** + * Merges an AI examples overrides file into an OpenAPI spec. + * The overrides file contains paths with x-fern-examples that are decomposed + * and integrated into native OpenAPI example fields per endpoint. + * + * Mapping: + * - path-parameters -> parameters[].example (where in: path) + * - query-parameters -> parameters[].example (where in: query) + * - headers -> parameters[].example (where in: header) + * - request.body -> requestBody.content.*.example + * - response.body -> responses..content.*.example + */ +export async function mergeOpenAPIWithOverrides({ + openapiPath, + overridesPath, + outputPath, + cliContext +}: { + openapiPath: AbsoluteFilePath; + overridesPath: AbsoluteFilePath; + outputPath: AbsoluteFilePath; + cliContext: CliContext; +}): Promise { + await cliContext.runTask(async (context) => { + // Validate files exist + if (!(await doesPathExist(openapiPath))) { + return context.failAndThrow(`OpenAPI spec file does not exist: ${openapiPath}`); + } + if (!(await doesPathExist(overridesPath))) { + return context.failAndThrow(`Overrides file does not exist: ${overridesPath}`); + } + + context.logger.info(`Merging ${overridesPath} into ${openapiPath}`); + + // Load both files + const openapi = await loadYamlOrJson(openapiPath, context); + const overrides = await loadYamlOrJson(overridesPath, context); + + // Merge the x-fern-examples into native OpenAPI examples + const merged = mergeExamplesIntoSpec(openapi, overrides, context); + + // Determine output format based on output file extension + const isJson = outputPath.endsWith(".json"); + const output = isJson ? JSON.stringify(merged, null, 2) : yaml.dump(merged, { lineWidth: -1, noRefs: true }); + + await writeFile(outputPath, output); + + context.logger.info(`Merged output written to ${outputPath}`); + }); +} + +// biome-ignore lint/suspicious/noExplicitAny: YAML/JSON files can have any shape +async function loadYamlOrJson(filepath: AbsoluteFilePath, context: any): Promise { + const contents = await readFile(filepath, "utf8"); + try { + return JSON.parse(contents); + } catch { + try { + return yaml.load(contents) as OpenAPISpec; + } catch (err) { + return context.failAndThrow(`Failed to parse file as JSON or YAML: ${filepath}`); + } + } +} + +/** + * Iterates over paths/methods in the overrides and integrates x-fern-examples + * into native OpenAPI example fields in the base spec. + */ +// biome-ignore lint/suspicious/noExplicitAny: context logger +export function mergeExamplesIntoSpec(spec: OpenAPISpec, overrides: OpenAPISpec, context: any): OpenAPISpec { + const merged = structuredClone(spec); + + const overridePaths = overrides.paths; + if (overridePaths == null || typeof overridePaths !== "object") { + return merged; + } + + if (merged.paths == null) { + merged.paths = {}; + } + + for (const [overridePath, pathItem] of Object.entries(overridePaths)) { + if (pathItem == null || typeof pathItem !== "object") { + continue; + } + + const specPath = findMatchingSpecPath(overridePath, merged.paths); + if (specPath == null) { + context.logger.warn(`Path ${overridePath} not found in OpenAPI spec, skipping.`); + continue; + } + + for (const [method, operation] of Object.entries(pathItem as Record)) { + if (!HTTP_METHODS.has(method.toLowerCase())) { + continue; + } + + const specOperation = merged.paths[specPath][method]; + if (specOperation == null || typeof specOperation !== "object") { + context.logger.warn( + `Operation ${method.toUpperCase()} ${overridePath} not found in OpenAPI spec, skipping.` + ); + continue; + } + + // biome-ignore lint/suspicious/noExplicitAny: x-fern-examples has dynamic shape + const fernExamples = (operation as any)?.["x-fern-examples"]; + if (!Array.isArray(fernExamples) || fernExamples.length === 0) { + continue; + } + + if (fernExamples.length === 1) { + applyExampleToOperation(specOperation, fernExamples[0], merged); + } else { + applyMultipleExamplesToOperation(specOperation, fernExamples, merged); + } + } + } + + // Strip all x-fern-examples from the merged spec + stripFernExamples(merged); + + return merged; +} + +/** + * Applies a single x-fern-example to an OpenAPI operation using singular `example` fields. + */ +// biome-ignore lint/suspicious/noExplicitAny: OpenAPI operations have dynamic shape +function applyExampleToOperation(operation: any, fernExample: any, spec: OpenAPISpec): void { + applyParameterExamples(operation, fernExample["path-parameters"], "path", spec); + applyParameterExamples(operation, fernExample["query-parameters"], "query", spec); + applyParameterExamples(operation, fernExample.headers, "header", spec); + + const requestBody = fernExample.request?.body; + if (requestBody !== undefined) { + applyRequestBodyExample(operation, requestBody); + } + + const responseBody = fernExample.response?.body; + if (responseBody !== undefined) { + applyResponseBodyExample(operation, responseBody); + } +} + +/** + * Applies multiple x-fern-examples to an OpenAPI operation using plural `examples` fields. + */ +// biome-ignore lint/suspicious/noExplicitAny: OpenAPI operations have dynamic shape +function applyMultipleExamplesToOperation(operation: any, fernExamples: any[], spec: OpenAPISpec): void { + for (let i = 0; i < fernExamples.length; i++) { + const fernExample = fernExamples[i]; + const exampleName = fernExample.name ?? `Example${i + 1}`; + + applyParameterNamedExamples(operation, fernExample["path-parameters"], "path", exampleName, spec); + applyParameterNamedExamples(operation, fernExample["query-parameters"], "query", exampleName, spec); + applyParameterNamedExamples(operation, fernExample.headers, "header", exampleName, spec); + + const requestBody = fernExample.request?.body; + if (requestBody !== undefined) { + applyRequestBodyNamedExample(operation, requestBody, exampleName); + } + + const responseBody = fernExample.response?.body; + if (responseBody !== undefined) { + applyResponseBodyNamedExample(operation, responseBody, exampleName); + } + } +} + +/** + * Sets `example` on matching parameters (by name and location). + */ +function applyParameterExamples( + // biome-ignore lint/suspicious/noExplicitAny: OpenAPI parameter shapes + operation: any, + paramExamples: Record | undefined, + location: string, + spec: OpenAPISpec +): void { + if (paramExamples == null || typeof paramExamples !== "object") { + return; + } + const parameters = resolveParameters(operation, spec); + for (const [paramName, exampleValue] of Object.entries(paramExamples)) { + // biome-ignore lint/suspicious/noExplicitAny: parameter shape + const param = parameters.find((p: any) => p.name === paramName && p.in === location); + if (param != null) { + param.example = exampleValue; + } + } +} + +/** + * Sets named entries in `examples` on matching parameters. + */ +function applyParameterNamedExamples( + // biome-ignore lint/suspicious/noExplicitAny: OpenAPI parameter shapes + operation: any, + paramExamples: Record | undefined, + location: string, + exampleName: string, + spec: OpenAPISpec +): void { + if (paramExamples == null || typeof paramExamples !== "object") { + return; + } + const parameters = resolveParameters(operation, spec); + for (const [paramName, exampleValue] of Object.entries(paramExamples)) { + // biome-ignore lint/suspicious/noExplicitAny: parameter shape + const param = parameters.find((p: any) => p.name === paramName && p.in === location); + if (param != null) { + if (param.examples == null) { + param.examples = {}; + } + param.examples[exampleName] = { value: exampleValue }; + } + } +} + +/** + * Resolves the parameters array for an operation, including any $ref parameters. + */ +// biome-ignore lint/suspicious/noExplicitAny: OpenAPI parameter shapes +function resolveParameters(operation: any, spec: OpenAPISpec): any[] { + if (!Array.isArray(operation.parameters)) { + return []; + } + return operation.parameters.map((param: Record, index: number) => { + if (param.$ref != null && typeof param.$ref === "string") { + const resolved = resolveRef(spec, param.$ref as string); + if (resolved != null) { + operation.parameters[index] = { ...resolved }; + return operation.parameters[index]; + } + } + return param; + }); +} + +/** + * Resolves a JSON $ref pointer within the spec. + */ +// biome-ignore lint/suspicious/noExplicitAny: ref resolution +function resolveRef(spec: OpenAPISpec, ref: string): any | undefined { + if (!ref.startsWith("#/")) { + return undefined; + } + const parts = ref.substring(2).split("/"); + // biome-ignore lint/suspicious/noExplicitAny: traversing spec + let current: any = spec; + for (const part of parts) { + if (current == null || typeof current !== "object") { + return undefined; + } + current = current[part]; + } + return current; +} + +/** + * Sets `example` on the request body's first content type. + */ +// biome-ignore lint/suspicious/noExplicitAny: OpenAPI operation shape +function applyRequestBodyExample(operation: any, bodyExample: unknown): void { + if (operation.requestBody == null) { + operation.requestBody = { content: { "application/json": { schema: {} } } }; + } + const content = operation.requestBody.content; + if (content == null) { + return; + } + const contentType = getFirstContentType(content); + if (contentType != null) { + content[contentType].example = bodyExample; + } +} + +/** + * Sets a named entry in `examples` on the request body's first content type. + */ +// biome-ignore lint/suspicious/noExplicitAny: OpenAPI operation shape +function applyRequestBodyNamedExample(operation: any, bodyExample: unknown, exampleName: string): void { + if (operation.requestBody == null) { + operation.requestBody = { content: { "application/json": { schema: {} } } }; + } + const content = operation.requestBody.content; + if (content == null) { + return; + } + const contentType = getFirstContentType(content); + if (contentType != null) { + if (content[contentType].examples == null) { + content[contentType].examples = {}; + } + content[contentType].examples[exampleName] = { value: bodyExample }; + } +} + +/** + * Sets `example` on the first success response's first content type. + */ +// biome-ignore lint/suspicious/noExplicitAny: OpenAPI operation shape +function applyResponseBodyExample(operation: any, responseExample: unknown): void { + const response = getFirstSuccessResponse(operation); + if (response == null) { + return; + } + const content = response.content; + if (content == null) { + return; + } + const contentType = getFirstContentType(content); + if (contentType != null) { + content[contentType].example = responseExample; + } +} + +/** + * Sets a named entry in `examples` on the first success response's first content type. + */ +// biome-ignore lint/suspicious/noExplicitAny: OpenAPI operation shape +function applyResponseBodyNamedExample(operation: any, responseExample: unknown, exampleName: string): void { + const response = getFirstSuccessResponse(operation); + if (response == null) { + return; + } + const content = response.content; + if (content == null) { + return; + } + const contentType = getFirstContentType(content); + if (contentType != null) { + if (content[contentType].examples == null) { + content[contentType].examples = {}; + } + content[contentType].examples[exampleName] = { value: responseExample }; + } +} + +/** + * Returns the first success (2xx) response object from the operation. + */ +// biome-ignore lint/suspicious/noExplicitAny: OpenAPI responses shape +function getFirstSuccessResponse(operation: any): any | undefined { + const responses = operation.responses; + if (responses == null || typeof responses !== "object") { + return undefined; + } + for (const code of ["200", "201", "202", "203", "204"]) { + if (responses[code] != null) { + return responses[code]; + } + } + for (const code of Object.keys(responses)) { + if (code.startsWith("2")) { + return responses[code]; + } + } + return undefined; +} + +/** + * Returns the first content type key from a content map. + */ +function getFirstContentType(content: Record): string | undefined { + const keys = Object.keys(content); + return keys.length > 0 ? keys[0] : undefined; +} + +/** + * Finds a matching spec path for an override path. + * Supports exact matches and fuzzy matches where override paths use bare + * parameter names (e.g. `/customers/customerId`) that correspond to + * templated spec paths (e.g. `/customers/{customerId}`). + */ +export function findMatchingSpecPath(overridePath: string, specPaths: Record): string | undefined { + // Try exact match first + if (specPaths[overridePath] != null) { + return overridePath; + } + + const overrideSegments = overridePath.split("/"); + + for (const specPath of Object.keys(specPaths)) { + const specSegments = specPath.split("/"); + if (specSegments.length !== overrideSegments.length) { + continue; + } + + let matches = true; + for (let i = 0; i < specSegments.length; i++) { + const specSeg = specSegments[i]; + const overrideSeg = overrideSegments[i]; + if (specSeg === overrideSeg) { + continue; + } + // Match templated segment {param} against bare param name + if (specSeg != null && specSeg.startsWith("{") && specSeg.endsWith("}")) { + const paramName = specSeg.slice(1, -1); + if (paramName === overrideSeg) { + continue; + } + } + matches = false; + break; + } + + if (matches) { + return specPath; + } + } + + return undefined; +} + +/** + * Strips all x-fern-examples from every operation in the spec. + */ +function stripFernExamples(spec: OpenAPISpec): void { + const paths = spec.paths; + if (paths == null || typeof paths !== "object") { + return; + } + for (const pathItem of Object.values(paths)) { + if (pathItem == null || typeof pathItem !== "object") { + continue; + } + for (const [method, operation] of Object.entries(pathItem as Record)) { + if (!HTTP_METHODS.has(method.toLowerCase())) { + continue; + } + if (operation != null && typeof operation === "object" && "x-fern-examples" in operation) { + delete (operation as Record)["x-fern-examples"]; + } + } + } +} diff --git a/packages/cli/cli/versions.yml b/packages/cli/cli/versions.yml index b083466f296c..fde7ed168608 100644 --- a/packages/cli/cli/versions.yml +++ b/packages/cli/cli/versions.yml @@ -1,4 +1,70 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 4.20.3 + changelogEntry: + - summary: | + Add support for the `collapsed` property on API reference section + configurations. Sections inside an API reference `layout` can now use + `collapsed: true` or `collapsed: open-by-default` to control sidebar + collapse behavior, matching the existing support on top-level sections + and folders. + type: fix + createdAt: "2026-03-09" + irVersion: 65 + +- version: 4.20.2 + changelogEntry: + - summary: | + Fix `respect-readonly-schemas` handling for request bodies that reference + a wrapper schema (a `$ref`-only schema pointing to another schema with + `readOnly` properties). The wrapper schema name is now preserved as the + request name and the stale type alias to the `Read` variant is no longer + emitted. + type: fix + createdAt: "2026-03-09" + irVersion: 65 + +- version: 4.20.1 + changelogEntry: + - summary: | + Revert treating JSON Schema `const` values as literal types regardless of + `coerceEnumsToLiterals`. This reverts the behavior introduced in 3.90.8 so + that `const` values are again converted to single-value enums when + `coerceEnumsToLiterals` is not enabled. + type: fix + createdAt: "2026-03-09" + irVersion: 65 + +- version: 4.20.0 + changelogEntry: + - summary: | + Add `fern api enrich` CLI command that decomposes x-fern-examples from an + overrides file into native OpenAPI example fields per endpoint. Usage: + `fern api enrich openapi.yml -f overrides.yml -o output.yml`. + type: feat + createdAt: "2026-03-09" + irVersion: 65 + +- version: 4.19.0 + changelogEntry: + - summary: | + Add support for `open-by-default` value on the `collapsed` property in + docs navigation configuration. Sections configured with + `collapsed: open-by-default` will start expanded but can be collapsed + by the user. + type: feat + createdAt: "2026-03-09" + irVersion: 65 +- version: 4.18.0 + changelogEntry: + - summary: | + Gzip-compress IR before uploading to Fiddle for remote generation. This + reduces upload size by ~85-90% on typical IR payloads, improving upload + speed and reducing bandwidth usage. The Fiddle receiver transparently + detects and decompresses gzip content, maintaining backward compatibility + with older CLI versions. + type: feat + createdAt: "2026-03-09" + irVersion: 65 - version: 4.17.0 changelogEntry: - summary: | @@ -10,7 +76,6 @@ type: feat createdAt: "2026-03-09" irVersion: 65 - - version: 4.16.0 changelogEntry: - summary: | diff --git a/packages/cli/config/package.json b/packages/cli/config/package.json index bb8822bc7a00..d5676c06d5fe 100644 --- a/packages/cli/config/package.json +++ b/packages/cli/config/package.json @@ -33,6 +33,7 @@ }, "devDependencies": { "@fern-api/configs": "workspace:*", + "@fern-api/configuration": "workspace:*", "@types/node": "catalog:", "typescript": "catalog:", "vitest": "catalog:", diff --git a/packages/cli/config/src/schemas/docs/AnnouncementConfigSchema.ts b/packages/cli/config/src/schemas/docs/AnnouncementConfigSchema.ts deleted file mode 100644 index 85beab877c91..000000000000 --- a/packages/cli/config/src/schemas/docs/AnnouncementConfigSchema.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { z } from "zod"; -import { FeatureFlagSchema } from "./FeatureFlagSchema.js"; - -export const AnnouncementConfigSchema = z.object({ - text: z.string().optional(), - message: z.string().optional(), - style: z.enum(["info", "warning", "success", "error"]).optional(), - dismissible: z.boolean().optional(), - featureFlag: FeatureFlagSchema.optional() -}); - -export type AnnouncementConfigSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/AvailabilitySchema.ts b/packages/cli/config/src/schemas/docs/AvailabilitySchema.ts deleted file mode 100644 index b274881d09c8..000000000000 --- a/packages/cli/config/src/schemas/docs/AvailabilitySchema.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { z } from "zod"; - -export const AvailabilitySchema = z.enum(["stable", "ga", "inDevelopment", "preRelease", "deprecated", "beta"]); - -export type AvailabilitySchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/DocsInstanceSchema.ts b/packages/cli/config/src/schemas/docs/DocsInstanceSchema.ts deleted file mode 100644 index 2d05d2adcbc5..000000000000 --- a/packages/cli/config/src/schemas/docs/DocsInstanceSchema.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { z } from "zod"; - -export const EditThisPageConfigSchema = z.object({ - github: z - .object({ - host: z.string().optional(), - owner: z.string(), - repo: z.string(), - branch: z.string().optional(), - path: z.string().optional() - }) - .optional() -}); - -export type EditThisPageConfigSchema = z.infer; - -export const DocsInstanceSchema = z.object({ - url: z.string(), - customDomain: z.union([z.string(), z.array(z.string())]).optional(), - audiences: z.array(z.string()).optional(), - editThisPage: EditThisPageConfigSchema.optional(), - private: z.boolean().optional() -}); - -export type DocsInstanceSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/DocsSchema.ts b/packages/cli/config/src/schemas/docs/DocsSchema.ts index 4ab38a4ca925..6b0daaa5f64f 100644 --- a/packages/cli/config/src/schemas/docs/DocsSchema.ts +++ b/packages/cli/config/src/schemas/docs/DocsSchema.ts @@ -1,70 +1,5 @@ -import { z } from "zod"; -import { AnnouncementConfigSchema } from "./AnnouncementConfigSchema.js"; -import { AiChatConfigSchema } from "./ai/AiChatConfigSchema.js"; -import { AiExamplesConfigSchema } from "./ai/AiExamplesConfigSchema.js"; -import { BackgroundImageConfigurationSchema } from "./branding/BackgroundImageConfigurationSchema.js"; -import { ColorsConfigurationSchema } from "./branding/ColorsConfigurationSchema.js"; -import { LogoConfigurationSchema } from "./branding/LogoConfigurationSchema.js"; -import { TypographyConfigSchema } from "./branding/TypographyConfigSchema.js"; -import { CssConfigSchema } from "./custom/CssConfigSchema.js"; -import { JsConfigSchema } from "./custom/JsConfigSchema.js"; -import { DocsInstanceSchema } from "./DocsInstanceSchema.js"; -import { ExperimentalConfigSchema } from "./ExperimentalConfigSchema.js"; -import { IntegrationsConfigSchema } from "./IntegrationsConfigSchema.js"; -import { LibraryConfigurationSchema } from "./LibraryConfigurationSchema.js"; -import { DocsSettingsConfigSchema } from "./layout/DocsSettingsConfigSchema.js"; -import { LayoutConfigSchema } from "./layout/LayoutConfigSchema.js"; -import { ThemeConfigSchema } from "./layout/ThemeConfigSchema.js"; -import { FooterLinksConfigSchema } from "./links/FooterLinksConfigSchema.js"; -import { NavbarLinkSchema } from "./links/NavbarLinkSchema.js"; -import { PageActionsConfigSchema } from "./links/PageActionsConfigSchema.js"; -import { NavigationConfigSchema } from "./navigation/NavigationConfigSchema.js"; -import { PageConfigurationSchema } from "./navigation/PageConfigurationSchema.js"; -import { ProductConfigSchema } from "./navigation/ProductConfigSchema.js"; -import { TabConfigSchema } from "./navigation/TabConfigSchema.js"; -import { VersionConfigSchema } from "./navigation/VersionConfigSchema.js"; -import { AnalyticsConfigSchema } from "./seo/AnalyticsConfigSchema.js"; -import { MetadataConfigSchema } from "./seo/MetadataConfigSchema.js"; -import { RedirectConfigSchema } from "./seo/RedirectConfigSchema.js"; - -export const DocsSchema = z.object({ - instances: z.array(DocsInstanceSchema), - title: z.string().optional(), - libraries: z.record(z.string(), LibraryConfigurationSchema).optional(), - analytics: AnalyticsConfigSchema.optional(), - announcement: AnnouncementConfigSchema.optional(), - roles: z.array(z.string()).optional(), - tabs: z.record(z.string(), TabConfigSchema).optional(), - versions: z.array(VersionConfigSchema).optional(), - products: z.array(ProductConfigSchema).optional(), - landingPage: PageConfigurationSchema.optional(), - navigation: NavigationConfigSchema.optional(), - navbarLinks: z.array(NavbarLinkSchema).optional(), - footerLinks: FooterLinksConfigSchema.optional(), - pageActions: PageActionsConfigSchema.optional(), - experimental: ExperimentalConfigSchema.optional(), - defaultLanguage: z - .enum(["typescript", "python", "java", "go", "ruby", "csharp", "php", "swift", "curl"]) - .optional(), - languages: z.array(z.string()).optional(), - aiChat: AiChatConfigSchema.optional(), - aiSearch: AiChatConfigSchema.optional(), - aiExamples: AiExamplesConfigSchema.optional(), - metadata: MetadataConfigSchema.optional(), - redirects: z.array(RedirectConfigSchema).optional(), - logo: LogoConfigurationSchema.optional(), - favicon: z.string().optional(), - backgroundImage: BackgroundImageConfigurationSchema.optional(), - colors: ColorsConfigurationSchema.optional(), - typography: TypographyConfigSchema.optional(), - layout: LayoutConfigSchema.optional(), - settings: DocsSettingsConfigSchema.optional(), - theme: ThemeConfigSchema.optional(), - integrations: IntegrationsConfigSchema.optional(), - css: CssConfigSchema.optional(), - js: JsConfigSchema.optional(), - header: z.string().optional(), - footer: z.string().optional() -}); +import { docsYml } from "@fern-api/configuration"; +import type { z } from "zod"; +export const DocsSchema = docsYml.DocsYmlSchemas.DocsConfiguration; export type DocsSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/ExperimentalConfigSchema.ts b/packages/cli/config/src/schemas/docs/ExperimentalConfigSchema.ts deleted file mode 100644 index 28c3ca54dab4..000000000000 --- a/packages/cli/config/src/schemas/docs/ExperimentalConfigSchema.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { z } from "zod"; - -export const ExperimentalConfigSchema = z.record(z.string(), z.unknown()); - -export type ExperimentalConfigSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/FeatureFlagSchema.ts b/packages/cli/config/src/schemas/docs/FeatureFlagSchema.ts deleted file mode 100644 index 1a195ea5d7fa..000000000000 --- a/packages/cli/config/src/schemas/docs/FeatureFlagSchema.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { z } from "zod"; - -export const FeatureFlagConfigurationSchema = z.object({ - flag: z.string(), - fallbackValue: z.unknown().optional(), - match: z.unknown().optional() -}); - -export type FeatureFlagConfigurationSchema = z.infer; - -export const FeatureFlagSchema = z.union([z.string(), FeatureFlagConfigurationSchema]); - -export type FeatureFlagSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/IntegrationsConfigSchema.ts b/packages/cli/config/src/schemas/docs/IntegrationsConfigSchema.ts deleted file mode 100644 index f116cf068a12..000000000000 --- a/packages/cli/config/src/schemas/docs/IntegrationsConfigSchema.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { z } from "zod"; - -export const IntercomConfigSchema = z.object({ - appId: z.string(), - apiBase: z.string().optional() -}); - -export type IntercomConfigSchema = z.infer; - -export const IntegrationsConfigSchema = z.object({ - intercom: IntercomConfigSchema.optional() -}); - -export type IntegrationsConfigSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/LibraryConfigurationSchema.ts b/packages/cli/config/src/schemas/docs/LibraryConfigurationSchema.ts deleted file mode 100644 index e0911c6889d6..000000000000 --- a/packages/cli/config/src/schemas/docs/LibraryConfigurationSchema.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { z } from "zod"; - -export const LibraryInputConfigurationSchema = z.object({ - git: z.string(), - subpath: z.string().optional() -}); - -export type LibraryInputConfigurationSchema = z.infer; - -export const LibraryOutputConfigurationSchema = z.object({ - path: z.string() -}); - -export type LibraryOutputConfigurationSchema = z.infer; - -export const LibraryConfigurationSchema = z.object({ - input: LibraryInputConfigurationSchema, - output: LibraryOutputConfigurationSchema, - lang: z.enum(["python", "typescript", "java", "go", "ruby", "csharp", "php", "swift", "rust"]) -}); - -export type LibraryConfigurationSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/RoleSchema.ts b/packages/cli/config/src/schemas/docs/RoleSchema.ts deleted file mode 100644 index ae7b382ef85e..000000000000 --- a/packages/cli/config/src/schemas/docs/RoleSchema.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { z } from "zod"; - -export const RoleSchema = z.union([z.string(), z.array(z.string())]); - -export type RoleSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/ai/AiChatConfigSchema.ts b/packages/cli/config/src/schemas/docs/ai/AiChatConfigSchema.ts deleted file mode 100644 index 293473cee7da..000000000000 --- a/packages/cli/config/src/schemas/docs/ai/AiChatConfigSchema.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { z } from "zod"; - -export const AiChatDatasourceSchema = z.object({ - type: z.string().optional(), - urls: z.array(z.string()).optional() -}); - -export type AiChatDatasourceSchema = z.infer; - -export const AiChatConfigSchema = z.object({ - model: z.string().optional(), - systemPrompt: z.string().optional(), - location: z.enum(["header", "sidebar", "both"]).optional(), - datasources: z.array(AiChatDatasourceSchema).optional() -}); - -export type AiChatConfigSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/ai/AiExamplesConfigSchema.ts b/packages/cli/config/src/schemas/docs/ai/AiExamplesConfigSchema.ts deleted file mode 100644 index 3d2350be9969..000000000000 --- a/packages/cli/config/src/schemas/docs/ai/AiExamplesConfigSchema.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { z } from "zod"; - -export const AiExamplesConfigSchema = z.object({ - enabled: z.boolean().optional() -}); - -export type AiExamplesConfigSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/ai/index.ts b/packages/cli/config/src/schemas/docs/ai/index.ts deleted file mode 100644 index 8358bcc66e41..000000000000 --- a/packages/cli/config/src/schemas/docs/ai/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { AiChatConfigSchema, AiChatDatasourceSchema } from "./AiChatConfigSchema.js"; -export { AiExamplesConfigSchema } from "./AiExamplesConfigSchema.js"; diff --git a/packages/cli/config/src/schemas/docs/branding/BackgroundImageConfigurationSchema.ts b/packages/cli/config/src/schemas/docs/branding/BackgroundImageConfigurationSchema.ts deleted file mode 100644 index 1be542b2d276..000000000000 --- a/packages/cli/config/src/schemas/docs/branding/BackgroundImageConfigurationSchema.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { z } from "zod"; - -export const BackgroundImageThemedConfigSchema = z.object({ - dark: z.string().optional(), - light: z.string().optional() -}); - -export type BackgroundImageThemedConfigSchema = z.infer; - -export const BackgroundImageConfigurationSchema = z.union([z.string(), BackgroundImageThemedConfigSchema]); - -export type BackgroundImageConfigurationSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/branding/ColorConfigSchema.ts b/packages/cli/config/src/schemas/docs/branding/ColorConfigSchema.ts deleted file mode 100644 index ab930cad25b1..000000000000 --- a/packages/cli/config/src/schemas/docs/branding/ColorConfigSchema.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { z } from "zod"; - -export const ColorThemedConfigSchema = z.object({ - dark: z.string().optional(), - light: z.string().optional() -}); - -export type ColorThemedConfigSchema = z.infer; - -export const ColorConfigSchema = z.union([z.string(), ColorThemedConfigSchema]); - -export type ColorConfigSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/branding/ColorsConfigurationSchema.ts b/packages/cli/config/src/schemas/docs/branding/ColorsConfigurationSchema.ts deleted file mode 100644 index 0fe2104af3fb..000000000000 --- a/packages/cli/config/src/schemas/docs/branding/ColorsConfigurationSchema.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { z } from "zod"; -import { ColorConfigSchema } from "./ColorConfigSchema.js"; - -export const ColorsConfigurationSchema = z.object({ - accentPrimary: ColorConfigSchema.optional(), - background: ColorConfigSchema.optional(), - border: ColorConfigSchema.optional(), - sidebarBackground: ColorConfigSchema.optional(), - headerBackground: ColorConfigSchema.optional(), - cardBackground: ColorConfigSchema.optional() -}); - -export type ColorsConfigurationSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/branding/FontConfigSchema.ts b/packages/cli/config/src/schemas/docs/branding/FontConfigSchema.ts deleted file mode 100644 index b2eeea36744b..000000000000 --- a/packages/cli/config/src/schemas/docs/branding/FontConfigSchema.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { z } from "zod"; - -export const FontConfigVariantSchema = z.object({ - path: z.string(), - weight: z.union([z.string(), z.number()]).optional(), - style: z.enum(["normal", "italic"]).optional() -}); - -export type FontConfigVariantSchema = z.infer; - -export const FontConfigSchema = z.object({ - name: z.string().optional(), - path: z.string().optional(), - paths: z.array(z.union([z.string(), FontConfigVariantSchema])).optional(), - weight: z.union([z.string(), z.number()]).optional(), - style: z.enum(["normal", "italic"]).optional(), - display: z.enum(["auto", "block", "swap", "fallback", "optional"]).optional(), - fallback: z.array(z.string()).optional(), - fontVariationSettings: z.string().optional() -}); - -export type FontConfigSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/branding/LogoConfigurationSchema.ts b/packages/cli/config/src/schemas/docs/branding/LogoConfigurationSchema.ts deleted file mode 100644 index 97796fbe52bd..000000000000 --- a/packages/cli/config/src/schemas/docs/branding/LogoConfigurationSchema.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z } from "zod"; - -export const LogoConfigurationSchema = z.object({ - dark: z.string().optional(), - light: z.string().optional(), - height: z.number().optional(), - href: z.string().optional(), - rightText: z.string().optional() -}); - -export type LogoConfigurationSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/branding/TypographyConfigSchema.ts b/packages/cli/config/src/schemas/docs/branding/TypographyConfigSchema.ts deleted file mode 100644 index 070123c7cb84..000000000000 --- a/packages/cli/config/src/schemas/docs/branding/TypographyConfigSchema.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { z } from "zod"; -import { FontConfigSchema } from "./FontConfigSchema.js"; - -export const TypographyConfigSchema = z.object({ - headingsFont: FontConfigSchema.optional(), - bodyFont: FontConfigSchema.optional(), - codeFont: FontConfigSchema.optional() -}); - -export type TypographyConfigSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/branding/index.ts b/packages/cli/config/src/schemas/docs/branding/index.ts deleted file mode 100644 index 240c59f2ac89..000000000000 --- a/packages/cli/config/src/schemas/docs/branding/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { - BackgroundImageConfigurationSchema, - BackgroundImageThemedConfigSchema -} from "./BackgroundImageConfigurationSchema.js"; -export { ColorConfigSchema, ColorThemedConfigSchema } from "./ColorConfigSchema.js"; -export { ColorsConfigurationSchema } from "./ColorsConfigurationSchema.js"; -export { FontConfigSchema, FontConfigVariantSchema } from "./FontConfigSchema.js"; -export { LogoConfigurationSchema } from "./LogoConfigurationSchema.js"; -export { TypographyConfigSchema } from "./TypographyConfigSchema.js"; diff --git a/packages/cli/config/src/schemas/docs/custom/CssConfigSchema.ts b/packages/cli/config/src/schemas/docs/custom/CssConfigSchema.ts deleted file mode 100644 index e85e6cb90810..000000000000 --- a/packages/cli/config/src/schemas/docs/custom/CssConfigSchema.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { z } from "zod"; - -export const CssConfigSchema = z.union([z.string(), z.array(z.string())]); - -export type CssConfigSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/custom/JsConfigSchema.ts b/packages/cli/config/src/schemas/docs/custom/JsConfigSchema.ts deleted file mode 100644 index 18b43c5dee96..000000000000 --- a/packages/cli/config/src/schemas/docs/custom/JsConfigSchema.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { z } from "zod"; - -export const JsScriptStrategySchema = z.enum(["beforeInteractive", "afterInteractive", "lazyOnload"]); - -export type JsScriptStrategySchema = z.infer; - -export const JsRemoteConfigSchema = z.object({ - url: z.string(), - strategy: JsScriptStrategySchema.optional() -}); - -export type JsRemoteConfigSchema = z.infer; - -export const JsFileConfigSchema = z.object({ - path: z.string(), - strategy: JsScriptStrategySchema.optional() -}); - -export type JsFileConfigSchema = z.infer; - -export const JsConfigItemSchema = z.union([JsRemoteConfigSchema, JsFileConfigSchema]); - -export type JsConfigItemSchema = z.infer; - -export const JsConfigSchema = z.union([JsConfigItemSchema, z.array(JsConfigItemSchema)]); - -export type JsConfigSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/custom/index.ts b/packages/cli/config/src/schemas/docs/custom/index.ts deleted file mode 100644 index 730a4c1f61a7..000000000000 --- a/packages/cli/config/src/schemas/docs/custom/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { CssConfigSchema } from "./CssConfigSchema.js"; -export { - JsConfigItemSchema, - JsConfigSchema, - JsFileConfigSchema, - JsRemoteConfigSchema, - JsScriptStrategySchema -} from "./JsConfigSchema.js"; diff --git a/packages/cli/config/src/schemas/docs/index.ts b/packages/cli/config/src/schemas/docs/index.ts index 7bd0318676ce..a45cd54d03d0 100644 --- a/packages/cli/config/src/schemas/docs/index.ts +++ b/packages/cli/config/src/schemas/docs/index.ts @@ -1,20 +1,159 @@ -export { AnnouncementConfigSchema } from "./AnnouncementConfigSchema.js"; -export { AvailabilitySchema } from "./AvailabilitySchema.js"; -export * from "./ai/index.js"; -export * from "./branding/index.js"; -export * from "./custom/index.js"; -export { DocsInstanceSchema, EditThisPageConfigSchema } from "./DocsInstanceSchema.js"; +import { docsYml } from "@fern-api/configuration"; +import type { z } from "zod"; + +const S = docsYml.DocsYmlSchemas; + +// Main schema export { DocsSchema } from "./DocsSchema.js"; -export { ExperimentalConfigSchema } from "./ExperimentalConfigSchema.js"; -export { FeatureFlagConfigurationSchema, FeatureFlagSchema } from "./FeatureFlagSchema.js"; -export { IntegrationsConfigSchema, IntercomConfigSchema } from "./IntegrationsConfigSchema.js"; -export { - LibraryConfigurationSchema, - LibraryInputConfigurationSchema, - LibraryOutputConfigurationSchema -} from "./LibraryConfigurationSchema.js"; -export * from "./layout/index.js"; -export * from "./links/index.js"; -export * from "./navigation/index.js"; -export { RoleSchema } from "./RoleSchema.js"; -export * from "./seo/index.js"; + +// Re-export all sub-schemas with backward-compatible names +export const DocsInstanceSchema = S.DocsInstance; +export type DocsInstanceSchema = z.infer; + +export const EditThisPageConfigSchema = S.EditThisPageConfig; +export type EditThisPageConfigSchema = z.infer; + +// Navigation +export const NavigationConfigSchema = S.NavigationConfig; +export type NavigationConfigSchema = z.infer; + +export const TabbedNavigationConfigSchema = S.TabbedNavigationConfig; +export type TabbedNavigationConfigSchema = z.infer; + +export const UntabbedNavigationConfigSchema = S.UntabbedNavigationConfig; +export type UntabbedNavigationConfigSchema = z.infer; + +export const NavigationItem = S.NavigationItem; +export type NavigationItem = z.infer; + +export const PageConfigurationSchema = S.PageConfiguration; +export type PageConfigurationSchema = z.infer; + +export const SectionConfigurationSchema = S.SectionConfiguration; +export type SectionConfigurationSchema = z.infer; + +export const ApiReferenceConfigurationSchema = S.ApiReferenceConfiguration; +export type ApiReferenceConfigurationSchema = z.infer; + +export const LinkConfigurationSchema = S.LinkConfiguration; +export type LinkConfigurationSchema = z.infer; + +export const ChangelogConfigurationSchema = S.ChangelogConfiguration; +export type ChangelogConfigurationSchema = z.infer; + +export const LibraryReferenceConfigurationSchema = S.LibraryReferenceConfiguration; +export type LibraryReferenceConfigurationSchema = z.infer; + +export const FolderConfigurationSchema = S.FolderConfiguration; +export type FolderConfigurationSchema = z.infer; + +export const TabbedNavigationItemSchema = S.TabbedNavigationItem; +export type TabbedNavigationItemSchema = z.infer; + +export const TabVariantSchema = S.TabVariant; +export type TabVariantSchema = z.infer; + +export const TabConfigSchema = S.TabConfig; +export type TabConfigSchema = z.infer; + +export const VersionConfigSchema = S.VersionConfig; +export type VersionConfigSchema = z.infer; + +export const ProductConfigSchema = S.ProductConfig; +export type ProductConfigSchema = z.infer; + +export const SnippetsConfigurationSchema = S.SnippetsConfiguration; +export type SnippetsConfigurationSchema = z.infer; + +export const PlaygroundSettingsSchema = S.PlaygroundSettings; +export type PlaygroundSettingsSchema = z.infer; + +// Branding +export const LogoConfigurationSchema = S.LogoConfiguration; +export type LogoConfigurationSchema = z.infer; + +export const BackgroundImageConfigurationSchema = S.BackgroundImageConfiguration; +export type BackgroundImageConfigurationSchema = z.infer; + +export const ColorsConfigurationSchema = S.ColorsConfiguration; +export type ColorsConfigurationSchema = z.infer; + +export const TypographyConfigSchema = S.DocsTypographyConfig; +export type TypographyConfigSchema = z.infer; + +// Layout +export const LayoutConfigSchema = S.LayoutConfig; +export type LayoutConfigSchema = z.infer; + +export const DocsSettingsConfigSchema = S.DocsSettingsConfig; +export type DocsSettingsConfigSchema = z.infer; + +export const ThemeConfigSchema = S.ThemeConfig; +export type ThemeConfigSchema = z.infer; + +// Links +export const NavbarLinkSchema = S.NavbarLink; +export type NavbarLinkSchema = z.infer; + +export const FooterLinksConfigSchema = S.FooterLinksConfig; +export type FooterLinksConfigSchema = z.infer; + +export const PageActionsConfigSchema = S.PageActionsConfig; +export type PageActionsConfigSchema = z.infer; + +// SEO +export const AnalyticsConfigSchema = S.AnalyticsConfig; +export type AnalyticsConfigSchema = z.infer; + +export const MetadataConfigSchema = S.MetadataConfig; +export type MetadataConfigSchema = z.infer; + +export const RedirectConfigSchema = S.RedirectConfig; +export type RedirectConfigSchema = z.infer; + +// AI +export const AiChatConfigSchema = S.AIChatConfig; +export type AiChatConfigSchema = z.infer; + +export const AiExamplesConfigSchema = S.AiExamplesConfig; +export type AiExamplesConfigSchema = z.infer; + +// Misc +export const AnnouncementConfigSchema = S.AnnouncementConfig; +export type AnnouncementConfigSchema = z.infer; + +export const IntegrationsConfigSchema = S.IntegrationsConfig; +export type IntegrationsConfigSchema = z.infer; + +export const IntercomConfigSchema = S.IntercomConfig; +export type IntercomConfigSchema = z.infer; + +export const LibraryConfigurationSchema = S.LibraryConfiguration; +export type LibraryConfigurationSchema = z.infer; + +export const LibraryInputConfigurationSchema = S.LibraryInputConfiguration; +export type LibraryInputConfigurationSchema = z.infer; + +export const LibraryOutputConfigurationSchema = S.LibraryOutputConfiguration; +export type LibraryOutputConfigurationSchema = z.infer; + +export const CssConfigSchema = S.CssConfig; +export type CssConfigSchema = z.infer; + +export const JsConfigSchema = S.JsConfig; +export type JsConfigSchema = z.infer; + +export const ExperimentalConfigSchema = S.ExperimentalConfig; +export type ExperimentalConfigSchema = z.infer; + +export const FeatureFlagSchema = S.FeatureFlag; +export type FeatureFlagSchema = z.infer; + +export const FeatureFlagConfigurationSchema = S.FeatureFlagConfiguration; +export type FeatureFlagConfigurationSchema = z.infer; + +export const AvailabilitySchema = S.Availability; +export type AvailabilitySchema = z.infer; + +export const RoleSchema = S.Role; +export type RoleSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/layout/DocsSettingsConfigSchema.ts b/packages/cli/config/src/schemas/docs/layout/DocsSettingsConfigSchema.ts deleted file mode 100644 index 02a302789ffc..000000000000 --- a/packages/cli/config/src/schemas/docs/layout/DocsSettingsConfigSchema.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { z } from "zod"; - -export const DocsSettingsConfigSchema = z.object({ - substituteEnvVars: z.boolean().optional() -}); - -export type DocsSettingsConfigSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/layout/LayoutConfigSchema.ts b/packages/cli/config/src/schemas/docs/layout/LayoutConfigSchema.ts deleted file mode 100644 index 9f65ca74a0b5..000000000000 --- a/packages/cli/config/src/schemas/docs/layout/LayoutConfigSchema.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { z } from "zod"; - -export const LayoutConfigSchema = z.object({ - pageWidth: z.string().optional(), - contentWidth: z.string().optional(), - sidebarWidth: z.string().optional(), - headerHeight: z.string().optional(), - searchbarPlacement: z.enum(["header", "sidebar", "header-tabs"]).optional(), - tabsPlacement: z.enum(["header", "sidebar"]).optional(), - contentAlignment: z.enum(["center", "left"]).optional(), - headerPosition: z.enum(["fixed", "static", "absolute"]).optional(), - disableHeader: z.boolean().optional() -}); - -export type LayoutConfigSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/layout/ThemeConfigSchema.ts b/packages/cli/config/src/schemas/docs/layout/ThemeConfigSchema.ts deleted file mode 100644 index f839b39aa5a4..000000000000 --- a/packages/cli/config/src/schemas/docs/layout/ThemeConfigSchema.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { z } from "zod"; - -export const SidebarThemeConfigSchema = z.object({ - showNavigationLinks: z.boolean().optional() -}); - -export type SidebarThemeConfigSchema = z.infer; - -export const BodyThemeConfigSchema = z.object({ - showHeader: z.boolean().optional() -}); - -export type BodyThemeConfigSchema = z.infer; - -export const TabsThemeConfigSchema = z.object({ - showNavigationLinks: z.boolean().optional() -}); - -export type TabsThemeConfigSchema = z.infer; - -export const ThemeConfigSchema = z.object({ - sidebar: SidebarThemeConfigSchema.optional(), - body: BodyThemeConfigSchema.optional(), - tabs: TabsThemeConfigSchema.optional() -}); - -export type ThemeConfigSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/layout/index.ts b/packages/cli/config/src/schemas/docs/layout/index.ts deleted file mode 100644 index 9c5fc3ff634c..000000000000 --- a/packages/cli/config/src/schemas/docs/layout/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { DocsSettingsConfigSchema } from "./DocsSettingsConfigSchema.js"; -export { LayoutConfigSchema } from "./LayoutConfigSchema.js"; -export { - BodyThemeConfigSchema, - SidebarThemeConfigSchema, - TabsThemeConfigSchema, - ThemeConfigSchema -} from "./ThemeConfigSchema.js"; diff --git a/packages/cli/config/src/schemas/docs/links/FooterLinksConfigSchema.ts b/packages/cli/config/src/schemas/docs/links/FooterLinksConfigSchema.ts deleted file mode 100644 index 12c03c5b1777..000000000000 --- a/packages/cli/config/src/schemas/docs/links/FooterLinksConfigSchema.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { z } from "zod"; - -export const FooterLinkSchema = z.object({ - href: z.string(), - text: z.string(), - icon: z.string().optional() -}); - -export type FooterLinkSchema = z.infer; - -export const FooterLinkGroupSchema = z.object({ - title: z.string().optional(), - links: z.array(FooterLinkSchema) -}); - -export type FooterLinkGroupSchema = z.infer; - -export const FooterLinksConfigSchema = z.union([ - z.array(FooterLinkGroupSchema), - z.record(z.string(), z.array(FooterLinkSchema)) -]); - -export type FooterLinksConfigSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/links/NavbarLinkSchema.ts b/packages/cli/config/src/schemas/docs/links/NavbarLinkSchema.ts deleted file mode 100644 index 0b989602b551..000000000000 --- a/packages/cli/config/src/schemas/docs/links/NavbarLinkSchema.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { z } from "zod"; - -export const NavbarLinkConfigSchema = z.object({ - href: z.string(), - text: z.string(), - icon: z.string().optional(), - rightIcon: z.string().optional(), - rounded: z.boolean().optional() -}); - -export type NavbarLinkConfigSchema = z.infer; - -export const NavbarDropdownConfigSchema = z.object({ - text: z.string(), - links: z.array( - z.object({ - href: z.string(), - text: z.string(), - icon: z.string().optional() - }) - ) -}); - -export type NavbarDropdownConfigSchema = z.infer; - -export const NavbarGithubConfigSchema = z.union([ - z.string(), - z.object({ - url: z.string(), - text: z.string().optional() - }) -]); - -export type NavbarGithubConfigSchema = z.infer; - -export const NavbarLinkSchema = z.discriminatedUnion("type", [ - z.object({ - type: z.literal("filled"), - ...NavbarLinkConfigSchema.shape - }), - z.object({ - type: z.literal("outlined"), - ...NavbarLinkConfigSchema.shape - }), - z.object({ - type: z.literal("minimal"), - ...NavbarLinkConfigSchema.shape - }), - z.object({ - type: z.literal("github"), - value: NavbarGithubConfigSchema - }), - z.object({ - type: z.literal("dropdown"), - ...NavbarDropdownConfigSchema.shape - }) -]); - -export type NavbarLinkSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/links/PageActionsConfigSchema.ts b/packages/cli/config/src/schemas/docs/links/PageActionsConfigSchema.ts deleted file mode 100644 index 4ffae6bb7008..000000000000 --- a/packages/cli/config/src/schemas/docs/links/PageActionsConfigSchema.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { z } from "zod"; - -export const PageActionOptionSchema = z.enum(["edit", "report", "suggest", "feedback"]); - -export type PageActionOptionSchema = z.infer; - -export const CustomPageActionSchema = z.object({ - icon: z.string().optional(), - text: z.string(), - href: z.string() -}); - -export type CustomPageActionSchema = z.infer; - -export const PageActionsConfigSchema = z.object({ - default: z.array(PageActionOptionSchema).optional(), - options: z.array(z.union([PageActionOptionSchema, CustomPageActionSchema])).optional() -}); - -export type PageActionsConfigSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/links/index.ts b/packages/cli/config/src/schemas/docs/links/index.ts deleted file mode 100644 index 63509bb71509..000000000000 --- a/packages/cli/config/src/schemas/docs/links/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -export { - FooterLinkGroupSchema, - FooterLinkSchema, - FooterLinksConfigSchema -} from "./FooterLinksConfigSchema.js"; -export { - NavbarDropdownConfigSchema, - NavbarGithubConfigSchema, - NavbarLinkConfigSchema, - NavbarLinkSchema -} from "./NavbarLinkSchema.js"; -export { - CustomPageActionSchema, - PageActionOptionSchema, - PageActionsConfigSchema -} from "./PageActionsConfigSchema.js"; diff --git a/packages/cli/config/src/schemas/docs/navigation/ApiReferenceConfigurationSchema.ts b/packages/cli/config/src/schemas/docs/navigation/ApiReferenceConfigurationSchema.ts deleted file mode 100644 index 89b55f1c5b91..000000000000 --- a/packages/cli/config/src/schemas/docs/navigation/ApiReferenceConfigurationSchema.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { z } from "zod"; -import { AvailabilitySchema } from "../AvailabilitySchema.js"; -import { FeatureFlagSchema } from "../FeatureFlagSchema.js"; -import { RoleSchema } from "../RoleSchema.js"; -import { ApiReferenceLayoutItemSchema } from "./ApiReferenceLayoutItemSchema.js"; -import { PlaygroundSettingsSchema } from "./PlaygroundSettingsSchema.js"; -import { SnippetsConfigurationSchema } from "./SnippetsConfigurationSchema.js"; - -export const ApiReferenceConfigurationSchema = z.object({ - api: z.string(), - apiName: z.string().optional(), - audiences: z.array(z.string()).optional(), - displayErrors: z.boolean().optional(), - title: z.string().optional(), - slug: z.string().optional(), - icon: z.string().optional(), - hidden: z.boolean().optional(), - skipSlug: z.boolean().optional(), - flattened: z.boolean().optional(), - alphabetized: z.boolean().optional(), - showErrors: z.boolean().optional(), - snippets: SnippetsConfigurationSchema.optional(), - summary: z.string().optional(), - layout: z.array(ApiReferenceLayoutItemSchema).optional(), - playground: PlaygroundSettingsSchema.optional(), - availability: AvailabilitySchema.optional(), - paginated: z.boolean().optional(), - // WithPermissions - viewers: RoleSchema.optional(), - orphaned: z.boolean().optional(), - // WithFeatureFlags - featureFlag: FeatureFlagSchema.optional() -}); - -export type ApiReferenceConfigurationSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/navigation/ApiReferenceLayoutItemSchema.ts b/packages/cli/config/src/schemas/docs/navigation/ApiReferenceLayoutItemSchema.ts deleted file mode 100644 index 8548440bc5cf..000000000000 --- a/packages/cli/config/src/schemas/docs/navigation/ApiReferenceLayoutItemSchema.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { z } from "zod"; - -export const ApiReferenceEndpointConfigurationSchema = z.object({ - endpoint: z.string(), - title: z.string().optional(), - slug: z.string().optional(), - icon: z.string().optional(), - hidden: z.boolean().optional() -}); - -export type ApiReferenceEndpointConfigurationSchema = z.infer; - -export const ApiReferenceOperationConfigurationSchema = z.object({ - operation: z.string(), - title: z.string().optional(), - slug: z.string().optional(), - icon: z.string().optional(), - hidden: z.boolean().optional() -}); - -export type ApiReferenceOperationConfigurationSchema = z.infer; - -export const ApiReferencePackageConfigurationSchema = z.union([ - z.string(), - z.object({ - package: z.string(), - title: z.string().optional(), - slug: z.string().optional(), - icon: z.string().optional(), - hidden: z.boolean().optional(), - summary: z.string().optional(), - contents: z.array(z.lazy((): z.ZodTypeAny => ApiReferenceLayoutItemSchema)).optional() - }) -]); - -export type ApiReferencePackageConfigurationSchema = z.infer; - -export const ApiReferenceSectionConfigurationSchema = z.object({ - section: z.string(), - slug: z.string().optional(), - icon: z.string().optional(), - collapsible: z.boolean().optional(), - collapsedByDefault: z.boolean().optional(), - contents: z.array(z.lazy((): z.ZodTypeAny => ApiReferenceLayoutItemSchema)) -}); - -export type ApiReferenceSectionConfigurationSchema = z.infer; - -export const ApiReferenceLayoutItemSchema: z.ZodTypeAny = z.union([ - ApiReferenceEndpointConfigurationSchema, - ApiReferenceOperationConfigurationSchema, - ApiReferencePackageConfigurationSchema, - ApiReferenceSectionConfigurationSchema, - z.string() -]); - -export type ApiReferenceLayoutItemSchema = - | ApiReferenceEndpointConfigurationSchema - | ApiReferenceOperationConfigurationSchema - | ApiReferencePackageConfigurationSchema - | ApiReferenceSectionConfigurationSchema - | string; diff --git a/packages/cli/config/src/schemas/docs/navigation/ChangelogConfigurationSchema.ts b/packages/cli/config/src/schemas/docs/navigation/ChangelogConfigurationSchema.ts deleted file mode 100644 index a2e465acbb1d..000000000000 --- a/packages/cli/config/src/schemas/docs/navigation/ChangelogConfigurationSchema.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { z } from "zod"; -import { FeatureFlagSchema } from "../FeatureFlagSchema.js"; -import { RoleSchema } from "../RoleSchema.js"; - -export const ChangelogConfigurationSchema = z.object({ - changelog: z.string(), - title: z.string().optional(), - slug: z.string().optional(), - icon: z.string().optional(), - hidden: z.boolean().optional(), - // WithPermissions - viewers: RoleSchema.optional(), - orphaned: z.boolean().optional(), - // WithFeatureFlags - featureFlag: FeatureFlagSchema.optional() -}); - -export type ChangelogConfigurationSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/navigation/FolderConfigurationSchema.ts b/packages/cli/config/src/schemas/docs/navigation/FolderConfigurationSchema.ts deleted file mode 100644 index 84bd302d6885..000000000000 --- a/packages/cli/config/src/schemas/docs/navigation/FolderConfigurationSchema.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { z } from "zod"; -import { FeatureFlagSchema } from "../FeatureFlagSchema.js"; -import { RoleSchema } from "../RoleSchema.js"; - -/** - * FolderConfigurationSchema uses NavigationItemSchema via z.lazy() - * to handle recursive navigation structures. - * - * The `contents` field is typed as z.ZodTypeAny because z.lazy - * cannot resolve the recursive type at declaration time. - */ -export const FolderConfigurationSchema = z.object({ - folder: z.string(), - title: z.string().optional(), - slug: z.string().optional(), - icon: z.string().optional(), - hidden: z.boolean().optional(), - collapsed: z.boolean().optional(), - collapsible: z.boolean().optional(), - collapsedByDefault: z.boolean().optional(), - path: z.string().optional(), - // WithPermissions - viewers: RoleSchema.optional(), - orphaned: z.boolean().optional(), - // WithFeatureFlags - featureFlag: FeatureFlagSchema.optional() -}); - -export type FolderConfigurationSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/navigation/LibraryReferenceConfigurationSchema.ts b/packages/cli/config/src/schemas/docs/navigation/LibraryReferenceConfigurationSchema.ts deleted file mode 100644 index 775d67f9e0b1..000000000000 --- a/packages/cli/config/src/schemas/docs/navigation/LibraryReferenceConfigurationSchema.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { z } from "zod"; -import { FeatureFlagSchema } from "../FeatureFlagSchema.js"; -import { RoleSchema } from "../RoleSchema.js"; - -export const LibraryReferenceConfigurationSchema = z.object({ - library: z.string(), - title: z.string().optional(), - slug: z.string().optional(), - icon: z.string().optional(), - hidden: z.boolean().optional(), - // WithPermissions - viewers: RoleSchema.optional(), - orphaned: z.boolean().optional(), - // WithFeatureFlags - featureFlag: FeatureFlagSchema.optional() -}); - -export type LibraryReferenceConfigurationSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/navigation/LinkConfigurationSchema.ts b/packages/cli/config/src/schemas/docs/navigation/LinkConfigurationSchema.ts deleted file mode 100644 index dd370b5044cf..000000000000 --- a/packages/cli/config/src/schemas/docs/navigation/LinkConfigurationSchema.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { z } from "zod"; -import { FeatureFlagSchema } from "../FeatureFlagSchema.js"; -import { RoleSchema } from "../RoleSchema.js"; - -export const LinkConfigurationSchema = z.object({ - link: z.string(), - href: z.string(), - icon: z.string().optional(), - target: z.enum(["_blank", "_self"]).optional(), - // WithPermissions - viewers: RoleSchema.optional(), - orphaned: z.boolean().optional(), - // WithFeatureFlags - featureFlag: FeatureFlagSchema.optional() -}); - -export type LinkConfigurationSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/navigation/NavigationConfigSchema.ts b/packages/cli/config/src/schemas/docs/navigation/NavigationConfigSchema.ts deleted file mode 100644 index e9d27df49e1b..000000000000 --- a/packages/cli/config/src/schemas/docs/navigation/NavigationConfigSchema.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { z } from "zod"; -import { NavigationItemSchema } from "./NavigationItemSchema.js"; -import { TabbedNavigationItemSchema } from "./TabbedNavigationItemSchema.js"; - -export const UntabbedNavigationConfigSchema = z.array(NavigationItemSchema); - -export type UntabbedNavigationConfigSchema = z.infer; - -export const TabbedNavigationConfigSchema = z.array(TabbedNavigationItemSchema); - -export type TabbedNavigationConfigSchema = z.infer; - -export const NavigationConfigSchema = z.union([UntabbedNavigationConfigSchema, TabbedNavigationConfigSchema]); - -export type NavigationConfigSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/navigation/NavigationItemSchema.ts b/packages/cli/config/src/schemas/docs/navigation/NavigationItemSchema.ts deleted file mode 100644 index 3843e85575f1..000000000000 --- a/packages/cli/config/src/schemas/docs/navigation/NavigationItemSchema.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { z } from "zod"; -import { ApiReferenceConfigurationSchema } from "./ApiReferenceConfigurationSchema.js"; -import { ChangelogConfigurationSchema } from "./ChangelogConfigurationSchema.js"; -import { FolderConfigurationSchema } from "./FolderConfigurationSchema.js"; -import { LibraryReferenceConfigurationSchema } from "./LibraryReferenceConfigurationSchema.js"; -import { LinkConfigurationSchema } from "./LinkConfigurationSchema.js"; -import { PageConfigurationSchema } from "./PageConfigurationSchema.js"; -import { _injectNavigationItemSchema, SectionConfigurationSchema } from "./SectionConfigurationSchema.js"; - -/** - * NavigationItem is the recursive union type for docs navigation. - * - * Navigation items are discriminated by key presence (not a `type` field): - * - `page` -> PageConfigurationSchema - * - `section` -> SectionConfigurationSchema (recursive) - * - `api` -> ApiReferenceConfigurationSchema - * - `link` -> LinkConfigurationSchema - * - `changelog` -> ChangelogConfigurationSchema - * - `library` -> LibraryReferenceConfigurationSchema - * - `folder` -> FolderConfigurationSchema - * - * We define the TS type explicitly because z.infer cannot resolve z.lazy types. - */ -export type NavigationItem = - | z.infer - | z.infer - | z.infer - | z.infer - | z.infer - | z.infer - | z.infer; - -export const NavigationItemSchema: z.ZodType = z.lazy(() => - z.union([ - PageConfigurationSchema, - SectionConfigurationSchema, - ApiReferenceConfigurationSchema, - LinkConfigurationSchema, - ChangelogConfigurationSchema, - LibraryReferenceConfigurationSchema, - FolderConfigurationSchema - ]) -); - -// Wire up the circular reference: SectionConfigurationSchema.contents uses NavigationItemSchema. -_injectNavigationItemSchema(NavigationItemSchema); diff --git a/packages/cli/config/src/schemas/docs/navigation/PageConfigurationSchema.ts b/packages/cli/config/src/schemas/docs/navigation/PageConfigurationSchema.ts deleted file mode 100644 index b7ee6e0ebed1..000000000000 --- a/packages/cli/config/src/schemas/docs/navigation/PageConfigurationSchema.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { z } from "zod"; -import { AvailabilitySchema } from "../AvailabilitySchema.js"; -import { FeatureFlagSchema } from "../FeatureFlagSchema.js"; -import { RoleSchema } from "../RoleSchema.js"; - -export const PageConfigurationSchema = z.object({ - page: z.string(), - path: z.string().optional(), - slug: z.string().optional(), - icon: z.string().optional(), - hidden: z.boolean().optional(), - noindex: z.boolean().optional(), - availability: AvailabilitySchema.optional(), - // WithPermissions - viewers: RoleSchema.optional(), - orphaned: z.boolean().optional(), - // WithFeatureFlags - featureFlag: FeatureFlagSchema.optional() -}); - -export type PageConfigurationSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/navigation/PlaygroundSettingsSchema.ts b/packages/cli/config/src/schemas/docs/navigation/PlaygroundSettingsSchema.ts deleted file mode 100644 index b541809ba472..000000000000 --- a/packages/cli/config/src/schemas/docs/navigation/PlaygroundSettingsSchema.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { z } from "zod"; - -export const PlaygroundButtonSettingsSchema = z.object({ - href: z.string().optional() -}); - -export type PlaygroundButtonSettingsSchema = z.infer; - -export const PlaygroundSettingsSchema = z.object({ - oauth: z.boolean().optional(), - button: PlaygroundButtonSettingsSchema.optional() -}); - -export type PlaygroundSettingsSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/navigation/ProductConfigSchema.ts b/packages/cli/config/src/schemas/docs/navigation/ProductConfigSchema.ts deleted file mode 100644 index 16476f923aa0..000000000000 --- a/packages/cli/config/src/schemas/docs/navigation/ProductConfigSchema.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { z } from "zod"; -import { FeatureFlagSchema } from "../FeatureFlagSchema.js"; -import { RoleSchema } from "../RoleSchema.js"; -import { NavigationConfigSchema } from "./NavigationConfigSchema.js"; -import { VersionConfigSchema } from "./VersionConfigSchema.js"; - -export const InternalProductConfigSchema = z.object({ - displayName: z.string(), - icon: z.string().optional(), - slug: z.string().optional(), - path: z.string().optional(), - default: z.boolean().optional(), - navigation: NavigationConfigSchema.optional(), - versions: z.array(VersionConfigSchema).optional(), - // WithPermissions - viewers: RoleSchema.optional(), - orphaned: z.boolean().optional(), - // WithFeatureFlags - featureFlag: FeatureFlagSchema.optional() -}); - -export type InternalProductConfigSchema = z.infer; - -export const ExternalProductConfigSchema = z.object({ - displayName: z.string(), - icon: z.string().optional(), - href: z.string(), - // WithPermissions - viewers: RoleSchema.optional(), - orphaned: z.boolean().optional(), - // WithFeatureFlags - featureFlag: FeatureFlagSchema.optional() -}); - -export type ExternalProductConfigSchema = z.infer; - -export const ProductConfigSchema = z.union([InternalProductConfigSchema, ExternalProductConfigSchema]); - -export type ProductConfigSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/navigation/SectionConfigurationSchema.ts b/packages/cli/config/src/schemas/docs/navigation/SectionConfigurationSchema.ts deleted file mode 100644 index 5ac3787ccfcc..000000000000 --- a/packages/cli/config/src/schemas/docs/navigation/SectionConfigurationSchema.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { z } from "zod"; -import { AvailabilitySchema } from "../AvailabilitySchema.js"; -import { FeatureFlagSchema } from "../FeatureFlagSchema.js"; -import { RoleSchema } from "../RoleSchema.js"; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- z.lazy defers access, avoiding TDZ issues with circular ESM imports -let _navigationItemSchema: z.ZodTypeAny; - -/** - * Injects the NavigationItemSchema to break the circular dependency - * between SectionConfigurationSchema and NavigationItemSchema. - * - * Called from NavigationItemSchema.ts after both schemas are defined. - */ -export function _injectNavigationItemSchema(schema: z.ZodTypeAny): void { - _navigationItemSchema = schema; -} - -/** - * SectionConfigurationSchema is a recursive schema - its `contents` field - * contains NavigationItemSchema[], which itself includes SectionConfigurationSchema. - * - * We use z.lazy() to break the circular dependency. - */ -export const SectionConfigurationSchema = z.object({ - section: z.string(), - slug: z.string().optional(), - icon: z.string().optional(), - hidden: z.boolean().optional(), - skipSlug: z.boolean().optional(), - collapsed: z.boolean().optional(), - collapsible: z.boolean().optional(), - collapsedByDefault: z.boolean().optional(), - flattened: z.boolean().optional(), - path: z.string().optional(), - availability: AvailabilitySchema.optional(), - contents: z.lazy(() => z.array(_navigationItemSchema)), - // WithPermissions - viewers: RoleSchema.optional(), - orphaned: z.boolean().optional(), - // WithFeatureFlags - featureFlag: FeatureFlagSchema.optional() -}); - -export type SectionConfigurationSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/navigation/SnippetsConfigurationSchema.ts b/packages/cli/config/src/schemas/docs/navigation/SnippetsConfigurationSchema.ts deleted file mode 100644 index 99f83a67e1d2..000000000000 --- a/packages/cli/config/src/schemas/docs/navigation/SnippetsConfigurationSchema.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { z } from "zod"; - -export const SnippetLanguageConfigurationSchema = z.object({ - package: z.string(), - version: z.string().optional(), - sdk: z.string().optional() -}); - -export type SnippetLanguageConfigurationSchema = z.infer; - -export const VersionedSnippetLanguageConfigurationSchema = z.object({ - package: z.string(), - version: z.string(), - sdk: z.string().optional() -}); - -export type VersionedSnippetLanguageConfigurationSchema = z.infer; - -export const SnippetsConfigurationSchema = z.object({ - python: z.union([z.string(), SnippetLanguageConfigurationSchema]).optional(), - typescript: z.union([z.string(), SnippetLanguageConfigurationSchema]).optional(), - go: z.union([z.string(), SnippetLanguageConfigurationSchema]).optional(), - java: z.union([z.string(), SnippetLanguageConfigurationSchema]).optional(), - ruby: z.union([z.string(), SnippetLanguageConfigurationSchema]).optional(), - csharp: z.union([z.string(), SnippetLanguageConfigurationSchema]).optional(), - swift: z.union([z.string(), SnippetLanguageConfigurationSchema]).optional(), - php: z.union([z.string(), SnippetLanguageConfigurationSchema]).optional(), - rust: z.union([z.string(), SnippetLanguageConfigurationSchema]).optional() -}); - -export type SnippetsConfigurationSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/navigation/TabConfigSchema.ts b/packages/cli/config/src/schemas/docs/navigation/TabConfigSchema.ts deleted file mode 100644 index 1f0e0a9f6db0..000000000000 --- a/packages/cli/config/src/schemas/docs/navigation/TabConfigSchema.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { z } from "zod"; -import { FeatureFlagSchema } from "../FeatureFlagSchema.js"; -import { RoleSchema } from "../RoleSchema.js"; - -export const TabConfigSchema = z.object({ - displayName: z.string(), - icon: z.string().optional(), - slug: z.string().optional(), - skipSlug: z.boolean().optional(), - hidden: z.boolean().optional(), - // WithPermissions - viewers: RoleSchema.optional(), - orphaned: z.boolean().optional(), - // WithFeatureFlags - featureFlag: FeatureFlagSchema.optional() -}); - -export type TabConfigSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/navigation/TabVariantSchema.ts b/packages/cli/config/src/schemas/docs/navigation/TabVariantSchema.ts deleted file mode 100644 index 67450521e3ef..000000000000 --- a/packages/cli/config/src/schemas/docs/navigation/TabVariantSchema.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { z } from "zod"; -import { NavigationItemSchema } from "./NavigationItemSchema.js"; - -export const TabVariantSchema = z.object({ - title: z.string(), - subtitle: z.string().optional(), - icon: z.string().optional(), - layout: z.array(NavigationItemSchema), - slug: z.string().optional(), - default: z.boolean().optional() -}); - -export type TabVariantSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/navigation/TabbedNavigationItemSchema.ts b/packages/cli/config/src/schemas/docs/navigation/TabbedNavigationItemSchema.ts deleted file mode 100644 index 998b5f073e52..000000000000 --- a/packages/cli/config/src/schemas/docs/navigation/TabbedNavigationItemSchema.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z } from "zod"; -import { NavigationItemSchema } from "./NavigationItemSchema.js"; -import { TabVariantSchema } from "./TabVariantSchema.js"; - -export const TabbedNavigationItemSchema = z.object({ - tab: z.string(), - layout: z.array(NavigationItemSchema).optional(), - variants: z.array(TabVariantSchema).optional() -}); - -export type TabbedNavigationItemSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/navigation/VersionConfigSchema.ts b/packages/cli/config/src/schemas/docs/navigation/VersionConfigSchema.ts deleted file mode 100644 index 54cba914f330..000000000000 --- a/packages/cli/config/src/schemas/docs/navigation/VersionConfigSchema.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { z } from "zod"; -import { AvailabilitySchema } from "../AvailabilitySchema.js"; -import { FeatureFlagSchema } from "../FeatureFlagSchema.js"; -import { RoleSchema } from "../RoleSchema.js"; -import { NavigationConfigSchema } from "./NavigationConfigSchema.js"; - -export const VersionConfigSchema = z.object({ - displayName: z.string(), - path: z.string(), - slug: z.string().optional(), - availability: AvailabilitySchema.optional(), - navigation: NavigationConfigSchema, - // WithPermissions - viewers: RoleSchema.optional(), - orphaned: z.boolean().optional(), - // WithFeatureFlags - featureFlag: FeatureFlagSchema.optional() -}); - -export type VersionConfigSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/navigation/index.ts b/packages/cli/config/src/schemas/docs/navigation/index.ts deleted file mode 100644 index d82759b37229..000000000000 --- a/packages/cli/config/src/schemas/docs/navigation/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -export { ApiReferenceConfigurationSchema } from "./ApiReferenceConfigurationSchema.js"; -export { - ApiReferenceEndpointConfigurationSchema, - ApiReferenceLayoutItemSchema, - ApiReferenceOperationConfigurationSchema, - ApiReferencePackageConfigurationSchema, - ApiReferenceSectionConfigurationSchema -} from "./ApiReferenceLayoutItemSchema.js"; -export { ChangelogConfigurationSchema } from "./ChangelogConfigurationSchema.js"; -export { FolderConfigurationSchema } from "./FolderConfigurationSchema.js"; -export { LibraryReferenceConfigurationSchema } from "./LibraryReferenceConfigurationSchema.js"; -export { LinkConfigurationSchema } from "./LinkConfigurationSchema.js"; -export { - NavigationConfigSchema, - TabbedNavigationConfigSchema, - UntabbedNavigationConfigSchema -} from "./NavigationConfigSchema.js"; -export type { NavigationItem } from "./NavigationItemSchema.js"; -export { NavigationItemSchema } from "./NavigationItemSchema.js"; -export { PageConfigurationSchema } from "./PageConfigurationSchema.js"; -export { PlaygroundButtonSettingsSchema, PlaygroundSettingsSchema } from "./PlaygroundSettingsSchema.js"; -export { - ExternalProductConfigSchema, - InternalProductConfigSchema, - ProductConfigSchema -} from "./ProductConfigSchema.js"; -export { SectionConfigurationSchema } from "./SectionConfigurationSchema.js"; -export { - SnippetLanguageConfigurationSchema, - SnippetsConfigurationSchema, - VersionedSnippetLanguageConfigurationSchema -} from "./SnippetsConfigurationSchema.js"; -export { TabbedNavigationItemSchema } from "./TabbedNavigationItemSchema.js"; -export { TabConfigSchema } from "./TabConfigSchema.js"; -export { TabVariantSchema } from "./TabVariantSchema.js"; -export { VersionConfigSchema } from "./VersionConfigSchema.js"; diff --git a/packages/cli/config/src/schemas/docs/seo/AnalyticsConfigSchema.ts b/packages/cli/config/src/schemas/docs/seo/AnalyticsConfigSchema.ts deleted file mode 100644 index 6243d468a432..000000000000 --- a/packages/cli/config/src/schemas/docs/seo/AnalyticsConfigSchema.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { z } from "zod"; - -export const SegmentConfigSchema = z.object({ - writeKey: z.string() -}); - -export type SegmentConfigSchema = z.infer; - -export const FullStoryConfigSchema = z.object({ - orgId: z.string() -}); - -export type FullStoryConfigSchema = z.infer; - -export const PostHogConfigSchema = z.object({ - apiKey: z.string(), - endpoint: z.string().optional() -}); - -export type PostHogConfigSchema = z.infer; - -export const GtmConfigSchema = z.object({ - containerId: z.string() -}); - -export type GtmConfigSchema = z.infer; - -export const GoogleAnalytics4ConfigSchema = z.object({ - measurementId: z.string() -}); - -export type GoogleAnalytics4ConfigSchema = z.infer; - -export const IntercomAnalyticsConfigSchema = z.object({ - appId: z.string(), - apiBase: z.string().optional() -}); - -export type IntercomAnalyticsConfigSchema = z.infer; - -export const AnalyticsConfigSchema = z.object({ - segment: SegmentConfigSchema.optional(), - fullstory: FullStoryConfigSchema.optional(), - intercom: IntercomAnalyticsConfigSchema.optional(), - posthog: PostHogConfigSchema.optional(), - gtm: GtmConfigSchema.optional(), - ga4: GoogleAnalytics4ConfigSchema.optional() -}); - -export type AnalyticsConfigSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/seo/MetadataConfigSchema.ts b/packages/cli/config/src/schemas/docs/seo/MetadataConfigSchema.ts deleted file mode 100644 index 6bdfda90c35e..000000000000 --- a/packages/cli/config/src/schemas/docs/seo/MetadataConfigSchema.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { z } from "zod"; - -export const MetadataConfigSchema = z.object({ - "og:site_name": z.string().optional(), - "og:title": z.string().optional(), - "og:description": z.string().optional(), - "og:url": z.string().optional(), - "og:image": z.string().optional(), - "og:image:width": z.number().optional(), - "og:image:height": z.number().optional(), - "og:locale": z.string().optional(), - "og:logo": z.string().optional(), - "twitter:title": z.string().optional(), - "twitter:description": z.string().optional(), - "twitter:handle": z.string().optional(), - "twitter:image": z.string().optional(), - "twitter:site": z.string().optional(), - "twitter:url": z.string().optional(), - "twitter:card": z.enum(["summary", "summary_large_image", "app", "player"]).optional(), - noindex: z.boolean().optional(), - nofollow: z.boolean().optional() -}); - -export type MetadataConfigSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/seo/RedirectConfigSchema.ts b/packages/cli/config/src/schemas/docs/seo/RedirectConfigSchema.ts deleted file mode 100644 index a4227c988f7f..000000000000 --- a/packages/cli/config/src/schemas/docs/seo/RedirectConfigSchema.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { z } from "zod"; - -export const RedirectConfigSchema = z.object({ - source: z.string(), - destination: z.string(), - permanent: z.boolean().optional() -}); - -export type RedirectConfigSchema = z.infer; diff --git a/packages/cli/config/src/schemas/docs/seo/index.ts b/packages/cli/config/src/schemas/docs/seo/index.ts deleted file mode 100644 index 77d62289fab7..000000000000 --- a/packages/cli/config/src/schemas/docs/seo/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export { - AnalyticsConfigSchema, - FullStoryConfigSchema, - GoogleAnalytics4ConfigSchema, - GtmConfigSchema, - IntercomAnalyticsConfigSchema, - PostHogConfigSchema, - SegmentConfigSchema -} from "./AnalyticsConfigSchema.js"; -export { MetadataConfigSchema } from "./MetadataConfigSchema.js"; -export { RedirectConfigSchema } from "./RedirectConfigSchema.js"; diff --git a/packages/cli/configuration-loader/src/docs-yml/__test__/collapsibleNavigationConfig.test.ts b/packages/cli/configuration-loader/src/docs-yml/__test__/collapsibleNavigationConfig.test.ts index 1ae13372e345..06543ffd87db 100644 --- a/packages/cli/configuration-loader/src/docs-yml/__test__/collapsibleNavigationConfig.test.ts +++ b/packages/cli/configuration-loader/src/docs-yml/__test__/collapsibleNavigationConfig.test.ts @@ -58,6 +58,100 @@ describe("docs.yml navigation collapsible config", () => { ).rejects.toBeInstanceOf(FernCliError); }); + it("should throw if collapsible is used alongside deprecated collapsed: open-by-default", async () => { + const context = createMockTaskContext(); + + const rawDocsConfiguration: docsYml.RawSchemas.DocsConfiguration = { + instances: [], + title: "Test", + navigation: [ + { + section: "Section", + contents: [], + collapsible: true, + collapsed: "open-by-default" + } + ] + }; + + await expect( + parseDocsConfiguration({ + rawDocsConfiguration, + absolutePathToFernFolder: AbsoluteFilePath.of("/tmp"), + absoluteFilepathToDocsConfig: AbsoluteFilePath.of("/tmp/docs.yml"), + context + }) + ).rejects.toBeInstanceOf(FernCliError); + }); + + it("should accept open-by-default as a collapsed value on sections", async () => { + const context = createMockTaskContext(); + + const rawDocsConfiguration: docsYml.RawSchemas.DocsConfiguration = { + instances: [], + title: "Test", + navigation: [ + { + section: "Section", + contents: [], + collapsed: "open-by-default" + } + ] + }; + + const parsed = await parseDocsConfiguration({ + rawDocsConfiguration, + absolutePathToFernFolder: AbsoluteFilePath.of("/tmp"), + absoluteFilepathToDocsConfig: AbsoluteFilePath.of("/tmp/docs.yml"), + context + }); + + expect(parsed.navigation).toMatchObject({ + type: "untabbed", + items: [ + { + type: "section", + title: "Section", + collapsed: "open-by-default" + } + ] + }); + }); + + it("should accept collapsed: true on sections", async () => { + const context = createMockTaskContext(); + + const rawDocsConfiguration: docsYml.RawSchemas.DocsConfiguration = { + instances: [], + title: "Test", + navigation: [ + { + section: "Section", + contents: [], + collapsed: true + } + ] + }; + + const parsed = await parseDocsConfiguration({ + rawDocsConfiguration, + absolutePathToFernFolder: AbsoluteFilePath.of("/tmp"), + absoluteFilepathToDocsConfig: AbsoluteFilePath.of("/tmp/docs.yml"), + context + }); + + expect(parsed.navigation).toMatchObject({ + type: "untabbed", + items: [ + { + type: "section", + title: "Section", + collapsed: true + } + ] + }); + }); + it("should pass through collapsible + collapsedByDefault on sections", async () => { const context = createMockTaskContext(); diff --git a/packages/cli/configuration-loader/src/docs-yml/parseDocsConfiguration.ts b/packages/cli/configuration-loader/src/docs-yml/parseDocsConfiguration.ts index cbae5f9e4ae1..0584c36bafb8 100644 --- a/packages/cli/configuration-loader/src/docs-yml/parseDocsConfiguration.ts +++ b/packages/cli/configuration-loader/src/docs-yml/parseDocsConfiguration.ts @@ -1248,7 +1248,7 @@ function parseApiReferenceLayoutItem( validateCollapsibleConfig({ context, sectionTitle: item.section, - collapsed: undefined, + collapsed: item.collapsed ?? undefined, collapsible: item.collapsible ?? undefined, collapsedByDefault: item.collapsedByDefault ?? undefined }); @@ -1266,6 +1266,7 @@ function parseApiReferenceLayoutItem( slug: item.slug, hidden: item.hidden, skipUrlSlug: item.skipSlug, + collapsed: item.collapsed ?? undefined, collapsible: item.collapsible ?? undefined, collapsedByDefault: item.collapsedByDefault ?? undefined, availability: item.availability, @@ -1684,7 +1685,7 @@ function validateCollapsibleConfig({ }: { context: TaskContext; sectionTitle: string; - collapsed: boolean | undefined; + collapsed: boolean | "open-by-default" | undefined; collapsible: boolean | undefined; collapsedByDefault: boolean | undefined; }): void { diff --git a/packages/cli/configuration/package.json b/packages/cli/configuration/package.json index 05a7a2c0e13f..b1cdc7750bb3 100644 --- a/packages/cli/configuration/package.json +++ b/packages/cli/configuration/package.json @@ -37,13 +37,12 @@ "@fern-api/fern-definition-schema": "workspace:*", "@fern-api/path-utils": "workspace:*", "@fern-fern/fiddle-sdk": "catalog:", - "zod": "catalog:" + "zod": "^4.3.6" }, "devDependencies": { "@fern-api/configs": "workspace:*", "@types/node": "catalog:", "typescript": "catalog:", - "vitest": "catalog:", - "zod-to-json-schema": "^3.25.1" + "vitest": "catalog:" } } diff --git a/packages/cli/configuration/src/docs-yml/DocsYmlSchemas.ts b/packages/cli/configuration/src/docs-yml/DocsYmlSchemas.ts index 1519c2fd51dd..b24068207664 100644 --- a/packages/cli/configuration/src/docs-yml/DocsYmlSchemas.ts +++ b/packages/cli/configuration/src/docs-yml/DocsYmlSchemas.ts @@ -646,7 +646,7 @@ export const FolderConfiguration = WithPermissions.merge(WithFeatureFlags).merge icon: z.string().optional(), hidden: z.boolean().optional(), "skip-slug": z.boolean().optional(), - collapsed: z.boolean().optional(), + collapsed: z.union([z.boolean(), z.literal("open-by-default")]).optional(), collapsible: z.boolean().optional(), "collapsed-by-default": z.boolean().optional(), availability: Availability.optional() @@ -700,6 +700,7 @@ export const ApiReferenceSectionConfiguration = WithPermissions.merge(WithFeatur icon: z.string().optional(), hidden: z.boolean().optional(), "skip-slug": z.boolean().optional(), + collapsed: z.union([z.boolean(), z.literal("open-by-default")]).optional(), collapsible: z.boolean().optional(), "collapsed-by-default": z.boolean().optional(), availability: Availability.optional(), @@ -739,7 +740,7 @@ export const ApiReferenceConfiguration = WithPermissions.merge(WithFeatureFlags) postman: z.string().optional(), summary: z.string().optional(), layout: z.array(ApiReferenceLayoutItem).optional(), - collapsed: z.boolean().optional(), + collapsed: z.union([z.boolean(), z.literal("open-by-default")]).optional(), icon: z.string().optional(), slug: z.string().optional(), hidden: z.boolean().optional(), @@ -772,7 +773,7 @@ export const SectionConfiguration = WithPermissions.merge(WithFeatureFlags).merg section: z.string(), path: z.string().optional(), contents: z.array(NavigationItem), - collapsed: z.boolean().optional(), + collapsed: z.union([z.boolean(), z.literal("open-by-default")]).optional(), collapsible: z.boolean().optional(), "collapsed-by-default": z.boolean().optional(), slug: z.string().optional(), diff --git a/packages/cli/configuration/src/docs-yml/ParsedDocsConfiguration.ts b/packages/cli/configuration/src/docs-yml/ParsedDocsConfiguration.ts index 7d16a3da2a44..a1bbcfbdd3e2 100644 --- a/packages/cli/configuration/src/docs-yml/ParsedDocsConfiguration.ts +++ b/packages/cli/configuration/src/docs-yml/ParsedDocsConfiguration.ts @@ -328,7 +328,7 @@ export declare namespace DocsNavigationItem { title: string; icon: string | AbsoluteFilePath | undefined; contents: DocsNavigationItem[]; - collapsed: boolean | undefined; + collapsed: boolean | "open-by-default" | undefined; collapsible: boolean | undefined; collapsedByDefault: boolean | undefined; slug: string | undefined; @@ -354,7 +354,7 @@ export declare namespace DocsNavigationItem { postman: string | undefined; overviewAbsolutePath: AbsoluteFilePath | undefined; navigation: ParsedApiReferenceLayoutItem[]; - collapsed: boolean | undefined; + collapsed: boolean | "open-by-default" | undefined; hidden: boolean | undefined; slug: string | undefined; skipUrlSlug: boolean | undefined; @@ -425,6 +425,7 @@ export declare namespace ParsedApiReferenceLayoutItem { hidden: boolean | undefined; icon: string | AbsoluteFilePath | undefined; skipUrlSlug: boolean | undefined; + collapsed: boolean | "open-by-default" | undefined; collapsible: boolean | undefined; collapsedByDefault: boolean | undefined; availability: Availability | undefined; diff --git a/packages/cli/configuration/src/docs-yml/__test__/DocsYmlSchemas.test.ts b/packages/cli/configuration/src/docs-yml/__test__/DocsYmlSchemas.test.ts index 4f34cb5fffff..0f6a59890fff 100644 --- a/packages/cli/configuration/src/docs-yml/__test__/DocsYmlSchemas.test.ts +++ b/packages/cli/configuration/src/docs-yml/__test__/DocsYmlSchemas.test.ts @@ -1,41 +1,33 @@ import { describe, expect, it } from "vitest"; -import { zodToJsonSchema } from "zod-to-json-schema"; +import { z } from "zod"; import { DocsConfiguration, ProductFileConfig, VersionFileConfig } from "../DocsYmlSchemas.js"; describe("DocsYmlSchemas", () => { it("should produce JSON Schema for DocsConfiguration", () => { - const jsonSchema = zodToJsonSchema(DocsConfiguration, "DocsConfiguration") as Record; + const jsonSchema = z.toJSONSchema(DocsConfiguration) as Record; expect(jsonSchema).toBeDefined(); - expect(jsonSchema["$ref"]).toBe("#/definitions/DocsConfiguration"); - expect(jsonSchema["definitions"]).toBeDefined(); - const definitions = (jsonSchema["definitions"] ?? {}) as Record; - expect(definitions["DocsConfiguration"]).toBeDefined(); + expect(jsonSchema["type"]).toBe("object"); + expect(jsonSchema["properties"]).toBeDefined(); }); it("should produce JSON Schema for VersionFileConfig", () => { - const jsonSchema = zodToJsonSchema(VersionFileConfig, "VersionFileConfig") as Record; + const jsonSchema = z.toJSONSchema(VersionFileConfig) as Record; expect(jsonSchema).toBeDefined(); - expect(jsonSchema["$ref"]).toBe("#/definitions/VersionFileConfig"); - expect(jsonSchema["definitions"]).toBeDefined(); - const definitions = (jsonSchema["definitions"] ?? {}) as Record; - expect(definitions["VersionFileConfig"]).toBeDefined(); + expect(jsonSchema["type"]).toBe("object"); + expect(jsonSchema["properties"]).toBeDefined(); }); it("should produce JSON Schema for ProductFileConfig", () => { - const jsonSchema = zodToJsonSchema(ProductFileConfig, "ProductFileConfig") as Record; + const jsonSchema = z.toJSONSchema(ProductFileConfig) as Record; expect(jsonSchema).toBeDefined(); - expect(jsonSchema["$ref"]).toBe("#/definitions/ProductFileConfig"); - expect(jsonSchema["definitions"]).toBeDefined(); - const definitions = (jsonSchema["definitions"] ?? {}) as Record; - expect(definitions["ProductFileConfig"]).toBeDefined(); + expect(jsonSchema["type"]).toBe("object"); + expect(jsonSchema["properties"]).toBeDefined(); }); it("DocsConfiguration JSON Schema should contain expected top-level properties", () => { - const jsonSchema = zodToJsonSchema(DocsConfiguration, "DocsConfiguration") as Record; - const definitions = (jsonSchema["definitions"] ?? {}) as Record; - const docsConfigDef = definitions["DocsConfiguration"] as Record; - const properties = docsConfigDef["properties"] as Record; + const jsonSchema = z.toJSONSchema(DocsConfiguration) as Record; + const properties = jsonSchema["properties"] as Record; expect(properties).toBeDefined(); expect(properties["instances"]).toBeDefined(); expect(properties["title"]).toBeDefined(); diff --git a/packages/cli/configuration/src/docs-yml/index.ts b/packages/cli/configuration/src/docs-yml/index.ts index ff7d5a55d623..7e399fdd6167 100644 --- a/packages/cli/configuration/src/docs-yml/index.ts +++ b/packages/cli/configuration/src/docs-yml/index.ts @@ -1,2 +1,3 @@ +export * as DocsYmlSchemas from "./DocsYmlSchemas.js"; export * from "./ParsedDocsConfiguration.js"; export * as RawSchemas from "./schemas/index.js"; diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferenceConfiguration.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferenceConfiguration.ts index 2a1a81e35948..fdf9b1264f73 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferenceConfiguration.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferenceConfiguration.ts @@ -20,7 +20,7 @@ export interface ApiReferenceConfiguration extends FernDocsConfig.WithPermission summary?: string; /** Advanced usage: when specified, this object will be used to customize the order that your API endpoints are displayed in the docs site, including subpackages, and additional markdown pages (to be rendered in between API endpoints). If not specified, the order will be inferred from the OpenAPI Spec or Fern Definition. */ layout?: FernDocsConfig.ApiReferenceLayoutItem[]; - collapsed?: boolean; + collapsed?: boolean | "open-by-default"; icon?: string; slug?: string; hidden?: boolean; diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferenceSectionConfiguration.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferenceSectionConfiguration.ts index 6c3bc0810457..22a08d0f85bd 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferenceSectionConfiguration.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferenceSectionConfiguration.ts @@ -16,6 +16,8 @@ export interface ApiReferenceSectionConfiguration icon?: string; hidden?: boolean; skipSlug?: boolean; + /** Deprecated. Use `collapsible` and `collapsed-by-default` instead. When set to `"open-by-default"`, the section starts expanded but can be collapsed. */ + collapsed?: boolean | "open-by-default"; /** Whether the section can be expanded/collapsed by the user in the sidebar. */ collapsible?: boolean; /** Whether the section starts collapsed. Only meaningful when collapsible is true. */ diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/FolderConfiguration.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/FolderConfiguration.ts index 9fa893eb7441..e8e7fe3a9103 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/FolderConfiguration.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/FolderConfiguration.ts @@ -13,8 +13,8 @@ export interface FolderConfiguration extends FernDocsConfig.WithPermissions, Fer icon?: string; hidden?: boolean; skipSlug?: boolean; - /** Deprecated. Use `collapsible` and `collapsed-by-default` instead. */ - collapsed?: boolean; + /** Deprecated. Use `collapsible` and `collapsed-by-default` instead. When set to `"open-by-default"`, the section starts expanded but can be collapsed. */ + collapsed?: boolean | "open-by-default"; /** Whether the section can be expanded/collapsed by the user. */ collapsible?: boolean; /** Whether the section starts collapsed. Only meaningful when `collapsible` is true. Defaults to false (starts open). */ diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/SectionConfiguration.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/SectionConfiguration.ts index 3dcea7efa2cb..ee8f3c1ede2b 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/SectionConfiguration.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/SectionConfiguration.ts @@ -7,8 +7,8 @@ export interface SectionConfiguration extends FernDocsConfig.WithPermissions, Fe /** The relative path to the markdown file that will be displayed when the section is clicked. */ path?: string; contents: FernDocsConfig.NavigationItem[]; - /** Deprecated. Use `collapsible` and `collapsed-by-default` instead. */ - collapsed?: boolean; + /** Deprecated. Use `collapsible` and `collapsed-by-default` instead. When set to `"open-by-default"`, the section starts expanded but can be collapsed. */ + collapsed?: boolean | "open-by-default"; /** Whether the section can be expanded/collapsed by the user. */ collapsible?: boolean; /** Whether the section starts collapsed. Only meaningful when `collapsible` is true. Defaults to false (starts open). */ diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferenceConfiguration.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferenceConfiguration.ts index eb03e872d684..dea3c22a5d8c 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferenceConfiguration.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferenceConfiguration.ts @@ -28,7 +28,7 @@ export const ApiReferenceConfiguration: core.serialization.ObjectSchema< postman: core.serialization.string().optional(), summary: core.serialization.string().optional(), layout: core.serialization.list(core.serialization.lazy(() => serializers.ApiReferenceLayoutItem)).optional(), - collapsed: core.serialization.boolean().optional(), + collapsed: core.serialization.undiscriminatedUnion([core.serialization.boolean(), core.serialization.stringLiteral("open-by-default")]).optional(), icon: core.serialization.string().optional(), slug: core.serialization.string().optional(), hidden: core.serialization.boolean().optional(), @@ -54,7 +54,7 @@ export declare namespace ApiReferenceConfiguration { postman?: string | null; summary?: string | null; layout?: serializers.ApiReferenceLayoutItem.Raw[] | null; - collapsed?: boolean | null; + collapsed?: boolean | "open-by-default" | null; icon?: string | null; slug?: string | null; hidden?: boolean | null; diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferenceSectionConfiguration.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferenceSectionConfiguration.ts index 05734a1dd6cd..543036f510d5 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferenceSectionConfiguration.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferenceSectionConfiguration.ts @@ -24,6 +24,7 @@ export const ApiReferenceSectionConfiguration: core.serialization.ObjectSchema< icon: core.serialization.string().optional(), hidden: core.serialization.boolean().optional(), skipSlug: core.serialization.property("skip-slug", core.serialization.boolean().optional()), + collapsed: core.serialization.undiscriminatedUnion([core.serialization.boolean(), core.serialization.stringLiteral("open-by-default")]).optional(), collapsible: core.serialization.boolean().optional(), collapsedByDefault: core.serialization.property("collapsed-by-default", core.serialization.boolean().optional()), availability: Availability.optional(), @@ -42,6 +43,7 @@ export declare namespace ApiReferenceSectionConfiguration { icon?: string | null; hidden?: boolean | null; "skip-slug"?: boolean | null; + collapsed?: boolean | "open-by-default" | null; collapsible?: boolean | null; "collapsed-by-default"?: boolean | null; availability?: Availability.Raw | null; diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/FolderConfiguration.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/FolderConfiguration.ts index 49a1e97af175..9b8006a04280 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/FolderConfiguration.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/FolderConfiguration.ts @@ -20,7 +20,7 @@ export const FolderConfiguration: core.serialization.ObjectSchema< icon: core.serialization.string().optional(), hidden: core.serialization.boolean().optional(), skipSlug: core.serialization.property("skip-slug", core.serialization.boolean().optional()), - collapsed: core.serialization.boolean().optional(), + collapsed: core.serialization.undiscriminatedUnion([core.serialization.boolean(), core.serialization.stringLiteral("open-by-default")]).optional(), collapsible: core.serialization.boolean().optional(), collapsedByDefault: core.serialization.property( "collapsed-by-default", @@ -40,7 +40,7 @@ export declare namespace FolderConfiguration { icon?: string | null; hidden?: boolean | null; "skip-slug"?: boolean | null; - collapsed?: boolean | null; + collapsed?: boolean | "open-by-default" | null; collapsible?: boolean | null; "collapsed-by-default"?: boolean | null; availability?: Availability.Raw | null; diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/SectionConfiguration.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/SectionConfiguration.ts index a2dc2af6cdb6..4fccd1c72eda 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/SectionConfiguration.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/SectionConfiguration.ts @@ -15,7 +15,7 @@ export const SectionConfiguration: core.serialization.ObjectSchema< section: core.serialization.string(), path: core.serialization.string().optional(), contents: core.serialization.list(core.serialization.lazy(() => serializers.NavigationItem)), - collapsed: core.serialization.boolean().optional(), + collapsed: core.serialization.undiscriminatedUnion([core.serialization.boolean(), core.serialization.stringLiteral("open-by-default")]).optional(), collapsible: core.serialization.boolean().optional(), collapsedByDefault: core.serialization.property( "collapsed-by-default", @@ -35,7 +35,7 @@ export declare namespace SectionConfiguration { section: string; path?: string | null; contents: serializers.NavigationItem.Raw[]; - collapsed?: boolean | null; + collapsed?: boolean | "open-by-default" | null; collapsible?: boolean | null; "collapsed-by-default"?: boolean | null; slug?: string | null; diff --git a/packages/cli/configuration/src/fern-config-json/schema/ProjectConfigSchema.ts b/packages/cli/configuration/src/fern-config-json/schema/ProjectConfigSchema.ts index 4dd10cd60f5f..1c68207d8629 100644 --- a/packages/cli/configuration/src/fern-config-json/schema/ProjectConfigSchema.ts +++ b/packages/cli/configuration/src/fern-config-json/schema/ProjectConfigSchema.ts @@ -1,8 +1,10 @@ import { z } from "zod"; -export const ProjectConfigSchema = z.strictObject({ - organization: z.string(), - version: z.string() -}); +export const ProjectConfigSchema = z + .object({ + organization: z.string(), + version: z.string() + }) + .strict(); export type ProjectConfigSchema = z.infer; diff --git a/packages/cli/docs-resolver/src/ApiReferenceNodeConverter.ts b/packages/cli/docs-resolver/src/ApiReferenceNodeConverter.ts index aaa3c17572ff..28e80f0ea87d 100644 --- a/packages/cli/docs-resolver/src/ApiReferenceNodeConverter.ts +++ b/packages/cli/docs-resolver/src/ApiReferenceNodeConverter.ts @@ -10,12 +10,10 @@ import urlJoin from "url-join"; // TODO: Remove these when the new fdr-sdk is integrated type ApiReferenceNodeWithCollapsibleConfig = FernNavigation.V1.ApiReferenceNode & { - collapsible?: boolean; - collapsedByDefault?: boolean; + collapsed?: boolean | "open-by-default"; }; type ApiPackageNodeWithCollapsibleConfig = FernNavigation.V1.ApiPackageNode & { - collapsible?: boolean; - collapsedByDefault?: boolean; + collapsed?: boolean | "open-by-default"; }; import { ApiDefinitionHolder } from "./ApiDefinitionHolder.js"; @@ -151,6 +149,7 @@ export class ApiReferenceNodeConverter { title: this.apiSection.title, apiDefinitionId: this.apiDefinitionId, overviewPageId, + collapsed: this.apiSection.collapsed, collapsible: undefined, collapsedByDefault: undefined, paginated: this.apiSection.paginated, @@ -461,6 +460,7 @@ export class ApiReferenceNodeConverter { icon: this.resolveIconFileId(section.icon), hidden: this.hideChildren || section.hidden, overviewPageId, + collapsed: section.collapsed, collapsible: section.collapsible, collapsedByDefault: section.collapsedByDefault, availability: sectionAvailability, diff --git a/packages/cli/docs-resolver/src/DocsDefinitionResolver.ts b/packages/cli/docs-resolver/src/DocsDefinitionResolver.ts index a8339ca59598..1d825defcf60 100644 --- a/packages/cli/docs-resolver/src/DocsDefinitionResolver.ts +++ b/packages/cli/docs-resolver/src/DocsDefinitionResolver.ts @@ -29,8 +29,7 @@ import { camelCase, kebabCase } from "lodash-es"; // TODO: Remove this when the new fdr-sdk is integrated type SectionNodeWithNewCollapsibleConfig = FernNavigation.V1.SectionNode & { - collapsible?: boolean; - collapsedByDefault?: boolean; + collapsed?: boolean | "open-by-default"; }; /** @@ -1189,11 +1188,7 @@ export class DocsDefinitionResolver { return; } - // Temporary coercion to satisfy type checker until new fdr-sdk is integrated - const isCollapsible = - child.type === "section" && - ((child as SectionNodeWithNewCollapsibleConfig).collapsible === true || - ((child as SectionNodeWithNewCollapsibleConfig).collapsible == null && child.collapsed === true)); + const isCollapsible = child.type === "section" && child.collapsed != null; if (child.type === "section" && !isCollapsible) { grouped.push(child); @@ -1916,8 +1911,8 @@ export class DocsDefinitionResolver { : item.title, icon: this.resolveIconFileId(item.icon), collapsed: item.collapsed, - collapsible: item.collapsible ?? (item.collapsed === true ? true : undefined), - collapsedByDefault: item.collapsedByDefault ?? (item.collapsed === true ? true : undefined), + collapsible: item.collapsible, + collapsedByDefault: item.collapsedByDefault, hidden: hiddenSection, viewers: item.viewers, orphaned: item.orphaned, diff --git a/packages/cli/ete-tests/src/tests/write-definition/__snapshots__/writeDefinition.test.ts.snap b/packages/cli/ete-tests/src/tests/write-definition/__snapshots__/writeDefinition.test.ts.snap index bc4bca73a766..fb178e07a880 100644 --- a/packages/cli/ete-tests/src/tests/write-definition/__snapshots__/writeDefinition.test.ts.snap +++ b/packages/cli/ete-tests/src/tests/write-definition/__snapshots__/writeDefinition.test.ts.snap @@ -605,7 +605,13 @@ service: extends: - Default properties: - type: literal<"heartbeat"> + type: HeartbeatType + source: + openapi: asyncapi/sample.yml + HeartbeatType: + enum: + - heartbeat + inline: true source: openapi: asyncapi/sample.yml Market: @@ -635,7 +641,13 @@ service: timestamps: docs: The timestamp in milliseconds for this group of events. type: double - type: literal<"update"> + type: UpdateType + source: + openapi: asyncapi/sample.yml + UpdateType: + enum: + - update + inline: true source: openapi: asyncapi/sample.yml ", diff --git a/packages/cli/ete-tests/src/tests/write-definition/fixtures/namespaced/fern/.definition/stream/__package__.yml b/packages/cli/ete-tests/src/tests/write-definition/fixtures/namespaced/fern/.definition/stream/__package__.yml index 60fd4332074f..2cf09e3cd59a 100644 --- a/packages/cli/ete-tests/src/tests/write-definition/fixtures/namespaced/fern/.definition/stream/__package__.yml +++ b/packages/cli/ete-tests/src/tests/write-definition/fixtures/namespaced/fern/.definition/stream/__package__.yml @@ -80,7 +80,13 @@ types: extends: - Default properties: - type: literal<"heartbeat"> + type: HeartbeatType + source: + openapi: asyncapi/sample.yml + HeartbeatType: + enum: + - heartbeat + inline: true source: openapi: asyncapi/sample.yml Market: @@ -110,6 +116,12 @@ types: timestamps: docs: The timestamp in milliseconds for this group of events. type: double - type: literal<"update"> + type: UpdateType + source: + openapi: asyncapi/sample.yml + UpdateType: + enum: + - update + inline: true source: openapi: asyncapi/sample.yml diff --git a/packages/cli/generation/remote-generation/remote-workspace-runner/src/__test__/gzipCompression.test.ts b/packages/cli/generation/remote-generation/remote-workspace-runner/src/__test__/gzipCompression.test.ts new file mode 100644 index 000000000000..76c0a10e7301 --- /dev/null +++ b/packages/cli/generation/remote-generation/remote-workspace-runner/src/__test__/gzipCompression.test.ts @@ -0,0 +1,152 @@ +import { promisify } from "util"; +import { describe, expect, it } from "vitest"; +import { gunzip, gunzipSync, gzip, gzipSync } from "zlib"; + +const gzipAsync = promisify(gzip); +const gunzipAsync = promisify(gunzip); + +describe("gzip compression for IR upload", () => { + it("should compress JSON string and produce valid gzip output", () => { + const irJson = JSON.stringify({ types: {}, services: {}, apiName: "test-api" }); + const irBytes = new TextEncoder().encode(irJson); + const compressed = gzipSync(irBytes); + + // Verify gzip magic bytes + expect(compressed[0]).toBe(0x1f); + expect(compressed[1]).toBe(0x8b); + + // Verify output is valid gzip (can be decompressed) + const decompressed = gunzipSync(new Uint8Array(compressed)); + expect(decompressed.toString("utf-8")).toBe(irJson); + }); + + it("should roundtrip: compress then decompress produces original JSON", () => { + const irJson = JSON.stringify({ + types: { User: { name: "User", properties: [{ name: "id", type: "string" }] } }, + services: { UserService: { endpoints: [{ path: "/users", method: "GET" }] } }, + apiName: "my-api" + }); + const irBytes = new TextEncoder().encode(irJson); + const compressed = gzipSync(irBytes); + const decompressed = gunzipSync(new Uint8Array(compressed)); + + expect(decompressed.toString("utf-8")).toBe(irJson); + }); + + it("should achieve significant compression on repetitive IR-like JSON", () => { + // Simulate a realistic IR with many repeated keys (like a real Fern IR) + const types: Record = {}; + for (let i = 0; i < 1000; i++) { + types[`type_${i}`] = { + name: { + originalName: `Type${i}`, + camelCase: { unsafeName: `type${i}`, safeName: `type${i}` } + }, + shape: { + type: "object", + properties: [ + { name: "id", type: "string" }, + { name: "name", type: "string" } + ] + } + }; + } + const irJson = JSON.stringify({ types, services: {}, apiName: "large-api" }); + const irBytes = new TextEncoder().encode(irJson); + const compressed = gzipSync(irBytes); + + const reductionPercent = (1 - compressed.length / irBytes.byteLength) * 100; + + // Should achieve at least 70% compression on repetitive JSON + expect(reductionPercent).toBeGreaterThan(70); + }); + + it("should produce a Buffer that can be passed directly to formData.append", () => { + const irJson = JSON.stringify({ apiName: "test" }); + const irBytes = new TextEncoder().encode(irJson); + const compressed = gzipSync(irBytes); + + // gzipSync returns a Buffer (which is a Uint8Array subclass) + expect(Buffer.isBuffer(compressed)).toBe(true); + // .length is available on Buffer + expect(typeof compressed.length).toBe("number"); + expect(compressed.length).toBeGreaterThan(0); + }); + + it("should handle empty JSON object", () => { + const irJson = "{}"; + const irBytes = new TextEncoder().encode(irJson); + const compressed = gzipSync(irBytes); + + // Should still produce valid gzip + expect(compressed[0]).toBe(0x1f); + expect(compressed[1]).toBe(0x8b); + + const decompressed = gunzipSync(new Uint8Array(compressed)); + expect(decompressed.toString("utf-8")).toBe(irJson); + }); + + it("should handle unicode characters in IR JSON", () => { + const irJson = JSON.stringify({ apiName: "test-api", description: "API für Benutzer — 日本語テスト" }); + const irBytes = new TextEncoder().encode(irJson); + const compressed = gzipSync(irBytes); + const decompressed = gunzipSync(new Uint8Array(compressed)); + + expect(decompressed.toString("utf-8")).toBe(irJson); + }); + + it("should correctly report compression stats on realistic payload", () => { + // Use a payload large enough that gzip compression is effective + // (small payloads may be larger after gzip due to header overhead) + const types: Record = {}; + for (let i = 0; i < 100; i++) { + types[`type_${i}`] = { name: `Type${i}`, shape: "object" }; + } + const irJson = JSON.stringify({ types, services: {}, apiName: "stats-test" }); + const irBytes = new TextEncoder().encode(irJson); + const compressed = gzipSync(irBytes); + + const originalSize = irBytes.byteLength; + const compressedSize = compressed.length; + const reductionPercent = ((1 - compressedSize / originalSize) * 100).toFixed(1); + + expect(originalSize).toBeGreaterThan(0); + expect(compressedSize).toBeGreaterThan(0); + expect(compressedSize).toBeLessThan(originalSize); + expect(Number.parseFloat(reductionPercent)).toBeGreaterThan(0); + }); + + it("TextEncoder.encode produces Uint8Array compatible with gzipSync", () => { + const text = "Hello, gzip!"; + const encoded = new TextEncoder().encode(text); + + // TextEncoder produces Uint8Array + expect(encoded).toBeInstanceOf(Uint8Array); + + // gzipSync accepts it without error + const compressed = gzipSync(encoded); + expect(compressed.length).toBeGreaterThan(0); + + // Roundtrip works + const decompressed = gunzipSync(new Uint8Array(compressed)); + expect(decompressed.toString("utf-8")).toBe(text); + }); + + it("async gzip produces identical output to gzipSync", async () => { + const irJson = JSON.stringify({ types: {}, services: {}, apiName: "async-test" }); + const irBytes = new TextEncoder().encode(irJson); + + const syncResult = gzipSync(irBytes); + const asyncResult = await gzipAsync(irBytes); + + // Both should decompress to the same original content + const syncDecompressed = gunzipSync(new Uint8Array(syncResult)); + const asyncDecompressed = await gunzipAsync(new Uint8Array(asyncResult)); + expect(syncDecompressed.toString("utf-8")).toBe(irJson); + expect(asyncDecompressed.toString("utf-8")).toBe(irJson); + + // Both should have gzip magic bytes + expect(asyncResult[0]).toBe(0x1f); + expect(asyncResult[1]).toBe(0x8b); + }); +}); diff --git a/packages/cli/generation/remote-generation/remote-workspace-runner/src/createAndStartJob.ts b/packages/cli/generation/remote-generation/remote-workspace-runner/src/createAndStartJob.ts index 1ba118dfadb1..38d294f8ccf6 100644 --- a/packages/cli/generation/remote-generation/remote-workspace-runner/src/createAndStartJob.ts +++ b/packages/cli/generation/remote-generation/remote-workspace-runner/src/createAndStartJob.ts @@ -15,8 +15,12 @@ import FormData from "form-data"; import { mkdir, readFile, writeFile } from "fs/promises"; import yaml from "js-yaml"; import urlJoin from "url-join"; +import { promisify } from "util"; +import { gzip } from "zlib"; import { retryWithRateLimit, TooManyRequestsError } from "./retryWithRateLimit.js"; +const gzipAsync = promisify(gzip); + export async function createAndStartJob({ projectConfig, workspace, @@ -283,7 +287,13 @@ async function startJob({ context.logger.debug("Wrote IR to disk: " + irFilepath); } }); - formData.append("file", irAsString); + const irBytes = new TextEncoder().encode(irAsString); + const compressed = await gzipAsync(irBytes); + context.logger.debug( + `Compressed IR from ${irBytes.byteLength} bytes to ${compressed.length} bytes ` + + `(${((1 - compressed.length / irBytes.byteLength) * 100).toFixed(1)}% reduction)` + ); + formData.append("file", compressed, { filename: "ir.json", contentType: "application/octet-stream" }); const url = urlJoin(getFiddleOrigin(), `/api/remote-gen/jobs/${job.jobId}/start`); try { diff --git a/packages/cli/workspace/loader/src/docs-yml.schema.json b/packages/cli/workspace/loader/src/docs-yml.schema.json index 7cfb2cc33471..f30e9ccfa11b 100644 --- a/packages/cli/workspace/loader/src/docs-yml.schema.json +++ b/packages/cli/workspace/loader/src/docs-yml.schema.json @@ -2051,6 +2051,16 @@ } ] }, + "collapsed": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, "collapsible": { "oneOf": [ { diff --git a/packages/cli/yaml/docs-validator/src/docsAst/products-yml.schema.json b/packages/cli/yaml/docs-validator/src/docsAst/products-yml.schema.json index f4125f5c1856..b2e76ae338ed 100644 --- a/packages/cli/yaml/docs-validator/src/docsAst/products-yml.schema.json +++ b/packages/cli/yaml/docs-validator/src/docsAst/products-yml.schema.json @@ -951,6 +951,16 @@ } ] }, + "collapsed": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, "collapsible": { "oneOf": [ { diff --git a/packages/cli/yaml/docs-validator/src/docsAst/versions-yml.schema.json b/packages/cli/yaml/docs-validator/src/docsAst/versions-yml.schema.json index f4125f5c1856..b2e76ae338ed 100644 --- a/packages/cli/yaml/docs-validator/src/docsAst/versions-yml.schema.json +++ b/packages/cli/yaml/docs-validator/src/docsAst/versions-yml.schema.json @@ -951,6 +951,16 @@ } ] }, + "collapsed": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, "collapsible": { "oneOf": [ { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92fec8e9ea41..d6508aff5608 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5137,6 +5137,9 @@ importers: '@fern-api/configs': specifier: workspace:* version: link:../../configs + '@fern-api/configuration': + specifier: workspace:* + version: link:../configuration '@types/node': specifier: 'catalog:' version: 18.15.3 @@ -5168,8 +5171,8 @@ importers: specifier: 'catalog:' version: 0.0.775 zod: - specifier: 'catalog:' - version: 3.25.76 + specifier: ^4.3.6 + version: 4.3.6 devDependencies: '@fern-api/configs': specifier: workspace:* @@ -5183,9 +5186,6 @@ importers: vitest: specifier: 'catalog:' version: 4.0.18(@types/node@18.15.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.3.3) - zod-to-json-schema: - specifier: ^3.25.1 - version: 3.25.1(zod@3.25.76) packages/cli/configuration-loader: dependencies: @@ -14663,11 +14663,6 @@ packages: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} - zod-to-json-schema@3.25.1: - resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} - peerDependencies: - zod: ^3.25 || ^4 - zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -22021,10 +22016,6 @@ snapshots: yoctocolors@2.1.2: {} - zod-to-json-schema@3.25.1(zod@3.25.76): - dependencies: - zod: 3.25.76 - zod@3.25.76: {} zod@4.3.6: {} diff --git a/product-yml.schema.json b/product-yml.schema.json index f4125f5c1856..b2e76ae338ed 100644 --- a/product-yml.schema.json +++ b/product-yml.schema.json @@ -951,6 +951,16 @@ } ] }, + "collapsed": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, "collapsible": { "oneOf": [ { diff --git a/seed/java-sdk/audiences/src/main/java/com/snippets/Example0.java b/seed/java-sdk/audiences/src/main/java/com/snippets/Example0.java index e07b2c7f3a69..8c2205cc877e 100644 --- a/seed/java-sdk/audiences/src/main/java/com/snippets/Example0.java +++ b/seed/java-sdk/audiences/src/main/java/com/snippets/Example0.java @@ -6,21 +6,14 @@ public class Example0 { public static void main(String[] args) { - SeedAudiencesClient client = SeedAudiencesClient - .builder() - .url("https://api.fern.com") - .build(); + SeedAudiencesClient client = + SeedAudiencesClient.builder().url("https://api.fern.com").build(); - client.folderA().service().getDirectThread( - GetDirectThreadRequest - .builder() - .ids( - Arrays.asList("ids") - ) - .tags( - Arrays.asList("tags") - ) - .build() - ); + client.folderA() + .service() + .getDirectThread(GetDirectThreadRequest.builder() + .ids(Arrays.asList("ids")) + .tags(Arrays.asList("tags")) + .build()); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/audiences/src/main/java/com/snippets/Example1.java b/seed/java-sdk/audiences/src/main/java/com/snippets/Example1.java index 1438346ca8f9..a4df9b439783 100644 --- a/seed/java-sdk/audiences/src/main/java/com/snippets/Example1.java +++ b/seed/java-sdk/audiences/src/main/java/com/snippets/Example1.java @@ -4,11 +4,9 @@ public class Example1 { public static void main(String[] args) { - SeedAudiencesClient client = SeedAudiencesClient - .builder() - .url("https://api.fern.com") - .build(); + SeedAudiencesClient client = + SeedAudiencesClient.builder().url("https://api.fern.com").build(); client.folderD().service().getDirectThread(); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/audiences/src/main/java/com/snippets/Example2.java b/seed/java-sdk/audiences/src/main/java/com/snippets/Example2.java index 0865004970e3..e5236dd2c357 100644 --- a/seed/java-sdk/audiences/src/main/java/com/snippets/Example2.java +++ b/seed/java-sdk/audiences/src/main/java/com/snippets/Example2.java @@ -5,18 +5,14 @@ public class Example2 { public static void main(String[] args) { - SeedAudiencesClient client = SeedAudiencesClient - .builder() - .url("https://api.fern.com") - .build(); + SeedAudiencesClient client = + SeedAudiencesClient.builder().url("https://api.fern.com").build(); - client.foo().find( - FindRequest - .builder() - .optionalString("optionalString") - .publicProperty("publicProperty") - .privateProperty(1) - .build() - ); + client.foo() + .find(FindRequest.builder() + .optionalString("optionalString") + .publicProperty("publicProperty") + .privateProperty(1) + .build()); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/errors/src/main/java/com/snippets/Example0.java b/seed/java-sdk/errors/src/main/java/com/snippets/Example0.java index 0e257f6e5a2d..8bfe4c6a3db6 100644 --- a/seed/java-sdk/errors/src/main/java/com/snippets/Example0.java +++ b/seed/java-sdk/errors/src/main/java/com/snippets/Example0.java @@ -5,16 +5,9 @@ public class Example0 { public static void main(String[] args) { - SeedErrorsClient client = SeedErrorsClient - .builder() - .url("https://api.fern.com") - .build(); + SeedErrorsClient client = + SeedErrorsClient.builder().url("https://api.fern.com").build(); - client.simple().fooWithoutEndpointError( - FooRequest - .builder() - .bar("bar") - .build() - ); + client.simple().fooWithoutEndpointError(FooRequest.builder().bar("bar").build()); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/errors/src/main/java/com/snippets/Example1.java b/seed/java-sdk/errors/src/main/java/com/snippets/Example1.java index c5668a0e5896..64c80d65cee5 100644 --- a/seed/java-sdk/errors/src/main/java/com/snippets/Example1.java +++ b/seed/java-sdk/errors/src/main/java/com/snippets/Example1.java @@ -5,16 +5,9 @@ public class Example1 { public static void main(String[] args) { - SeedErrorsClient client = SeedErrorsClient - .builder() - .url("https://api.fern.com") - .build(); + SeedErrorsClient client = + SeedErrorsClient.builder().url("https://api.fern.com").build(); - client.simple().fooWithoutEndpointError( - FooRequest - .builder() - .bar("bar") - .build() - ); + client.simple().fooWithoutEndpointError(FooRequest.builder().bar("bar").build()); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/errors/src/main/java/com/snippets/Example10.java b/seed/java-sdk/errors/src/main/java/com/snippets/Example10.java index 11bd47e798ee..999870f0c942 100644 --- a/seed/java-sdk/errors/src/main/java/com/snippets/Example10.java +++ b/seed/java-sdk/errors/src/main/java/com/snippets/Example10.java @@ -5,16 +5,9 @@ public class Example10 { public static void main(String[] args) { - SeedErrorsClient client = SeedErrorsClient - .builder() - .url("https://api.fern.com") - .build(); + SeedErrorsClient client = + SeedErrorsClient.builder().url("https://api.fern.com").build(); - client.simple().fooWithExamples( - FooRequest - .builder() - .bar("hello") - .build() - ); + client.simple().fooWithExamples(FooRequest.builder().bar("hello").build()); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/errors/src/main/java/com/snippets/Example11.java b/seed/java-sdk/errors/src/main/java/com/snippets/Example11.java index a4b2969ff9b3..c9ed67d87cae 100644 --- a/seed/java-sdk/errors/src/main/java/com/snippets/Example11.java +++ b/seed/java-sdk/errors/src/main/java/com/snippets/Example11.java @@ -5,16 +5,9 @@ public class Example11 { public static void main(String[] args) { - SeedErrorsClient client = SeedErrorsClient - .builder() - .url("https://api.fern.com") - .build(); + SeedErrorsClient client = + SeedErrorsClient.builder().url("https://api.fern.com").build(); - client.simple().fooWithExamples( - FooRequest - .builder() - .bar("hello") - .build() - ); + client.simple().fooWithExamples(FooRequest.builder().bar("hello").build()); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/errors/src/main/java/com/snippets/Example12.java b/seed/java-sdk/errors/src/main/java/com/snippets/Example12.java index 2aa937dcb63f..de1875c7dbcb 100644 --- a/seed/java-sdk/errors/src/main/java/com/snippets/Example12.java +++ b/seed/java-sdk/errors/src/main/java/com/snippets/Example12.java @@ -5,16 +5,9 @@ public class Example12 { public static void main(String[] args) { - SeedErrorsClient client = SeedErrorsClient - .builder() - .url("https://api.fern.com") - .build(); + SeedErrorsClient client = + SeedErrorsClient.builder().url("https://api.fern.com").build(); - client.simple().fooWithExamples( - FooRequest - .builder() - .bar("hello") - .build() - ); + client.simple().fooWithExamples(FooRequest.builder().bar("hello").build()); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/errors/src/main/java/com/snippets/Example13.java b/seed/java-sdk/errors/src/main/java/com/snippets/Example13.java index fb3d2190ef14..4024f0654928 100644 --- a/seed/java-sdk/errors/src/main/java/com/snippets/Example13.java +++ b/seed/java-sdk/errors/src/main/java/com/snippets/Example13.java @@ -5,16 +5,9 @@ public class Example13 { public static void main(String[] args) { - SeedErrorsClient client = SeedErrorsClient - .builder() - .url("https://api.fern.com") - .build(); + SeedErrorsClient client = + SeedErrorsClient.builder().url("https://api.fern.com").build(); - client.simple().fooWithExamples( - FooRequest - .builder() - .bar("bar") - .build() - ); + client.simple().fooWithExamples(FooRequest.builder().bar("bar").build()); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/errors/src/main/java/com/snippets/Example14.java b/seed/java-sdk/errors/src/main/java/com/snippets/Example14.java index bb74afceb1a8..3d020f4ce0c8 100644 --- a/seed/java-sdk/errors/src/main/java/com/snippets/Example14.java +++ b/seed/java-sdk/errors/src/main/java/com/snippets/Example14.java @@ -5,16 +5,9 @@ public class Example14 { public static void main(String[] args) { - SeedErrorsClient client = SeedErrorsClient - .builder() - .url("https://api.fern.com") - .build(); + SeedErrorsClient client = + SeedErrorsClient.builder().url("https://api.fern.com").build(); - client.simple().fooWithExamples( - FooRequest - .builder() - .bar("bar") - .build() - ); + client.simple().fooWithExamples(FooRequest.builder().bar("bar").build()); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/errors/src/main/java/com/snippets/Example15.java b/seed/java-sdk/errors/src/main/java/com/snippets/Example15.java index d49059578ba8..ea87a491c38c 100644 --- a/seed/java-sdk/errors/src/main/java/com/snippets/Example15.java +++ b/seed/java-sdk/errors/src/main/java/com/snippets/Example15.java @@ -5,16 +5,9 @@ public class Example15 { public static void main(String[] args) { - SeedErrorsClient client = SeedErrorsClient - .builder() - .url("https://api.fern.com") - .build(); + SeedErrorsClient client = + SeedErrorsClient.builder().url("https://api.fern.com").build(); - client.simple().fooWithExamples( - FooRequest - .builder() - .bar("bar") - .build() - ); + client.simple().fooWithExamples(FooRequest.builder().bar("bar").build()); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/errors/src/main/java/com/snippets/Example16.java b/seed/java-sdk/errors/src/main/java/com/snippets/Example16.java index 26c4b64b6496..0ebfe2c44433 100644 --- a/seed/java-sdk/errors/src/main/java/com/snippets/Example16.java +++ b/seed/java-sdk/errors/src/main/java/com/snippets/Example16.java @@ -5,16 +5,9 @@ public class Example16 { public static void main(String[] args) { - SeedErrorsClient client = SeedErrorsClient - .builder() - .url("https://api.fern.com") - .build(); + SeedErrorsClient client = + SeedErrorsClient.builder().url("https://api.fern.com").build(); - client.simple().fooWithExamples( - FooRequest - .builder() - .bar("bar") - .build() - ); + client.simple().fooWithExamples(FooRequest.builder().bar("bar").build()); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/errors/src/main/java/com/snippets/Example17.java b/seed/java-sdk/errors/src/main/java/com/snippets/Example17.java index 6d6dcbe6a9e1..443bc8dd6447 100644 --- a/seed/java-sdk/errors/src/main/java/com/snippets/Example17.java +++ b/seed/java-sdk/errors/src/main/java/com/snippets/Example17.java @@ -5,16 +5,9 @@ public class Example17 { public static void main(String[] args) { - SeedErrorsClient client = SeedErrorsClient - .builder() - .url("https://api.fern.com") - .build(); + SeedErrorsClient client = + SeedErrorsClient.builder().url("https://api.fern.com").build(); - client.simple().fooWithExamples( - FooRequest - .builder() - .bar("bar") - .build() - ); + client.simple().fooWithExamples(FooRequest.builder().bar("bar").build()); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/errors/src/main/java/com/snippets/Example18.java b/seed/java-sdk/errors/src/main/java/com/snippets/Example18.java index 2b590fff17e0..3f203695e991 100644 --- a/seed/java-sdk/errors/src/main/java/com/snippets/Example18.java +++ b/seed/java-sdk/errors/src/main/java/com/snippets/Example18.java @@ -5,16 +5,9 @@ public class Example18 { public static void main(String[] args) { - SeedErrorsClient client = SeedErrorsClient - .builder() - .url("https://api.fern.com") - .build(); + SeedErrorsClient client = + SeedErrorsClient.builder().url("https://api.fern.com").build(); - client.simple().fooWithExamples( - FooRequest - .builder() - .bar("bar") - .build() - ); + client.simple().fooWithExamples(FooRequest.builder().bar("bar").build()); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/errors/src/main/java/com/snippets/Example2.java b/seed/java-sdk/errors/src/main/java/com/snippets/Example2.java index 4dd9fe463254..d3cec7785773 100644 --- a/seed/java-sdk/errors/src/main/java/com/snippets/Example2.java +++ b/seed/java-sdk/errors/src/main/java/com/snippets/Example2.java @@ -5,16 +5,9 @@ public class Example2 { public static void main(String[] args) { - SeedErrorsClient client = SeedErrorsClient - .builder() - .url("https://api.fern.com") - .build(); + SeedErrorsClient client = + SeedErrorsClient.builder().url("https://api.fern.com").build(); - client.simple().fooWithoutEndpointError( - FooRequest - .builder() - .bar("bar") - .build() - ); + client.simple().fooWithoutEndpointError(FooRequest.builder().bar("bar").build()); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/errors/src/main/java/com/snippets/Example3.java b/seed/java-sdk/errors/src/main/java/com/snippets/Example3.java index 56d293c3f27c..39a7ba0d101b 100644 --- a/seed/java-sdk/errors/src/main/java/com/snippets/Example3.java +++ b/seed/java-sdk/errors/src/main/java/com/snippets/Example3.java @@ -5,16 +5,9 @@ public class Example3 { public static void main(String[] args) { - SeedErrorsClient client = SeedErrorsClient - .builder() - .url("https://api.fern.com") - .build(); + SeedErrorsClient client = + SeedErrorsClient.builder().url("https://api.fern.com").build(); - client.simple().fooWithoutEndpointError( - FooRequest - .builder() - .bar("bar") - .build() - ); + client.simple().fooWithoutEndpointError(FooRequest.builder().bar("bar").build()); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/errors/src/main/java/com/snippets/Example4.java b/seed/java-sdk/errors/src/main/java/com/snippets/Example4.java index bc6a5fea2e7b..7c6b0c9da465 100644 --- a/seed/java-sdk/errors/src/main/java/com/snippets/Example4.java +++ b/seed/java-sdk/errors/src/main/java/com/snippets/Example4.java @@ -5,16 +5,9 @@ public class Example4 { public static void main(String[] args) { - SeedErrorsClient client = SeedErrorsClient - .builder() - .url("https://api.fern.com") - .build(); + SeedErrorsClient client = + SeedErrorsClient.builder().url("https://api.fern.com").build(); - client.simple().foo( - FooRequest - .builder() - .bar("bar") - .build() - ); + client.simple().foo(FooRequest.builder().bar("bar").build()); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/errors/src/main/java/com/snippets/Example5.java b/seed/java-sdk/errors/src/main/java/com/snippets/Example5.java index 7d2e77a304f4..2faab419b623 100644 --- a/seed/java-sdk/errors/src/main/java/com/snippets/Example5.java +++ b/seed/java-sdk/errors/src/main/java/com/snippets/Example5.java @@ -5,16 +5,9 @@ public class Example5 { public static void main(String[] args) { - SeedErrorsClient client = SeedErrorsClient - .builder() - .url("https://api.fern.com") - .build(); + SeedErrorsClient client = + SeedErrorsClient.builder().url("https://api.fern.com").build(); - client.simple().foo( - FooRequest - .builder() - .bar("bar") - .build() - ); + client.simple().foo(FooRequest.builder().bar("bar").build()); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/errors/src/main/java/com/snippets/Example6.java b/seed/java-sdk/errors/src/main/java/com/snippets/Example6.java index 8181a543fa5b..0a7744fd353f 100644 --- a/seed/java-sdk/errors/src/main/java/com/snippets/Example6.java +++ b/seed/java-sdk/errors/src/main/java/com/snippets/Example6.java @@ -5,16 +5,9 @@ public class Example6 { public static void main(String[] args) { - SeedErrorsClient client = SeedErrorsClient - .builder() - .url("https://api.fern.com") - .build(); + SeedErrorsClient client = + SeedErrorsClient.builder().url("https://api.fern.com").build(); - client.simple().foo( - FooRequest - .builder() - .bar("bar") - .build() - ); + client.simple().foo(FooRequest.builder().bar("bar").build()); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/errors/src/main/java/com/snippets/Example7.java b/seed/java-sdk/errors/src/main/java/com/snippets/Example7.java index 23ca898664b7..e1663791a9ac 100644 --- a/seed/java-sdk/errors/src/main/java/com/snippets/Example7.java +++ b/seed/java-sdk/errors/src/main/java/com/snippets/Example7.java @@ -5,16 +5,9 @@ public class Example7 { public static void main(String[] args) { - SeedErrorsClient client = SeedErrorsClient - .builder() - .url("https://api.fern.com") - .build(); + SeedErrorsClient client = + SeedErrorsClient.builder().url("https://api.fern.com").build(); - client.simple().foo( - FooRequest - .builder() - .bar("bar") - .build() - ); + client.simple().foo(FooRequest.builder().bar("bar").build()); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/errors/src/main/java/com/snippets/Example8.java b/seed/java-sdk/errors/src/main/java/com/snippets/Example8.java index c9aa1c996f1e..f3ece9b737da 100644 --- a/seed/java-sdk/errors/src/main/java/com/snippets/Example8.java +++ b/seed/java-sdk/errors/src/main/java/com/snippets/Example8.java @@ -5,16 +5,9 @@ public class Example8 { public static void main(String[] args) { - SeedErrorsClient client = SeedErrorsClient - .builder() - .url("https://api.fern.com") - .build(); + SeedErrorsClient client = + SeedErrorsClient.builder().url("https://api.fern.com").build(); - client.simple().foo( - FooRequest - .builder() - .bar("bar") - .build() - ); + client.simple().foo(FooRequest.builder().bar("bar").build()); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/errors/src/main/java/com/snippets/Example9.java b/seed/java-sdk/errors/src/main/java/com/snippets/Example9.java index 368d1b370b99..0e8b5da736bd 100644 --- a/seed/java-sdk/errors/src/main/java/com/snippets/Example9.java +++ b/seed/java-sdk/errors/src/main/java/com/snippets/Example9.java @@ -5,16 +5,9 @@ public class Example9 { public static void main(String[] args) { - SeedErrorsClient client = SeedErrorsClient - .builder() - .url("https://api.fern.com") - .build(); + SeedErrorsClient client = + SeedErrorsClient.builder().url("https://api.fern.com").build(); - client.simple().foo( - FooRequest - .builder() - .bar("bar") - .build() - ); + client.simple().foo(FooRequest.builder().bar("bar").build()); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/java-custom-package-prefix/java-custom-package-prefix/src/main/java/com/snippets/Example0.java b/seed/java-sdk/java-custom-package-prefix/java-custom-package-prefix/src/main/java/com/snippets/Example0.java index f31cb60c031c..4a72959e9454 100644 --- a/seed/java-sdk/java-custom-package-prefix/java-custom-package-prefix/src/main/java/com/snippets/Example0.java +++ b/seed/java-sdk/java-custom-package-prefix/java-custom-package-prefix/src/main/java/com/snippets/Example0.java @@ -5,18 +5,13 @@ public class Example0 { public static void main(String[] args) { - SeedApiClient client = SeedApiClient - .builder() - .token("") - .url("https://api.fern.com") - .build(); + SeedApiClient client = SeedApiClient.builder() + .token("") + .url("https://api.fern.com") + .build(); - client.imdb().createMovie( - CreateMovieRequest - .builder() - .title("title") - .rating(1.1) - .build() - ); + client.imdb() + .createMovie( + CreateMovieRequest.builder().title("title").rating(1.1).build()); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/java-custom-package-prefix/java-custom-package-prefix/src/main/java/com/snippets/Example1.java b/seed/java-sdk/java-custom-package-prefix/java-custom-package-prefix/src/main/java/com/snippets/Example1.java index ba3720a24a88..b43ec1fcaa46 100644 --- a/seed/java-sdk/java-custom-package-prefix/java-custom-package-prefix/src/main/java/com/snippets/Example1.java +++ b/seed/java-sdk/java-custom-package-prefix/java-custom-package-prefix/src/main/java/com/snippets/Example1.java @@ -4,12 +4,11 @@ public class Example1 { public static void main(String[] args) { - SeedApiClient client = SeedApiClient - .builder() - .token("") - .url("https://api.fern.com") - .build(); + SeedApiClient client = SeedApiClient.builder() + .token("") + .url("https://api.fern.com") + .build(); client.imdb().getMovie("movieId"); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/java-custom-package-prefix/java-custom-package-prefix/src/main/java/com/snippets/Example2.java b/seed/java-sdk/java-custom-package-prefix/java-custom-package-prefix/src/main/java/com/snippets/Example2.java index d9918b3c0645..0da00c861b2b 100644 --- a/seed/java-sdk/java-custom-package-prefix/java-custom-package-prefix/src/main/java/com/snippets/Example2.java +++ b/seed/java-sdk/java-custom-package-prefix/java-custom-package-prefix/src/main/java/com/snippets/Example2.java @@ -4,12 +4,11 @@ public class Example2 { public static void main(String[] args) { - SeedApiClient client = SeedApiClient - .builder() - .token("") - .url("https://api.fern.com") - .build(); + SeedApiClient client = SeedApiClient.builder() + .token("") + .url("https://api.fern.com") + .build(); client.imdb().getMovie("movieId"); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/mixed-case/src/main/java/com/snippets/Example0.java b/seed/java-sdk/mixed-case/src/main/java/com/snippets/Example0.java index 9079aebb81df..c3497b9dbaf9 100644 --- a/seed/java-sdk/mixed-case/src/main/java/com/snippets/Example0.java +++ b/seed/java-sdk/mixed-case/src/main/java/com/snippets/Example0.java @@ -4,11 +4,9 @@ public class Example0 { public static void main(String[] args) { - SeedMixedCaseClient client = SeedMixedCaseClient - .builder() - .url("https://api.fern.com") - .build(); + SeedMixedCaseClient client = + SeedMixedCaseClient.builder().url("https://api.fern.com").build(); client.service().getResource("rsc-xyz"); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/mixed-case/src/main/java/com/snippets/Example1.java b/seed/java-sdk/mixed-case/src/main/java/com/snippets/Example1.java index 182d1e956057..5b749575dee2 100644 --- a/seed/java-sdk/mixed-case/src/main/java/com/snippets/Example1.java +++ b/seed/java-sdk/mixed-case/src/main/java/com/snippets/Example1.java @@ -4,11 +4,9 @@ public class Example1 { public static void main(String[] args) { - SeedMixedCaseClient client = SeedMixedCaseClient - .builder() - .url("https://api.fern.com") - .build(); + SeedMixedCaseClient client = + SeedMixedCaseClient.builder().url("https://api.fern.com").build(); client.service().getResource("ResourceID"); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/mixed-case/src/main/java/com/snippets/Example2.java b/seed/java-sdk/mixed-case/src/main/java/com/snippets/Example2.java index de47e107d6b5..843559df55de 100644 --- a/seed/java-sdk/mixed-case/src/main/java/com/snippets/Example2.java +++ b/seed/java-sdk/mixed-case/src/main/java/com/snippets/Example2.java @@ -5,17 +5,13 @@ public class Example2 { public static void main(String[] args) { - SeedMixedCaseClient client = SeedMixedCaseClient - .builder() - .url("https://api.fern.com") - .build(); + SeedMixedCaseClient client = + SeedMixedCaseClient.builder().url("https://api.fern.com").build(); - client.service().listResources( - ListResourcesRequest - .builder() - .pageLimit(10) - .beforeDate("2023-01-01") - .build() - ); + client.service() + .listResources(ListResourcesRequest.builder() + .pageLimit(10) + .beforeDate("2023-01-01") + .build()); } -} \ No newline at end of file +} diff --git a/seed/java-sdk/mixed-case/src/main/java/com/snippets/Example3.java b/seed/java-sdk/mixed-case/src/main/java/com/snippets/Example3.java index a451e2aeb7d7..ecd844148594 100644 --- a/seed/java-sdk/mixed-case/src/main/java/com/snippets/Example3.java +++ b/seed/java-sdk/mixed-case/src/main/java/com/snippets/Example3.java @@ -5,17 +5,13 @@ public class Example3 { public static void main(String[] args) { - SeedMixedCaseClient client = SeedMixedCaseClient - .builder() - .url("https://api.fern.com") - .build(); + SeedMixedCaseClient client = + SeedMixedCaseClient.builder().url("https://api.fern.com").build(); - client.service().listResources( - ListResourcesRequest - .builder() - .pageLimit(1) - .beforeDate("2023-01-15") - .build() - ); + client.service() + .listResources(ListResourcesRequest.builder() + .pageLimit(1) + .beforeDate("2023-01-15") + .build()); } -} \ No newline at end of file +} diff --git a/version-yml.schema.json b/version-yml.schema.json index f4125f5c1856..b2e76ae338ed 100644 --- a/version-yml.schema.json +++ b/version-yml.schema.json @@ -951,6 +951,16 @@ } ] }, + "collapsed": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, "collapsible": { "oneOf": [ {