From 90912f353c105a33b8783d84ff058bfa9cc9d3fa Mon Sep 17 00:00:00 2001 From: Rohin Bhargava Date: Wed, 8 Jan 2025 18:04:44 -0500 Subject: [PATCH] feat(cli): docs dev v2 with new OpenApi Parser (#5454) * docs dev * correlate IDs properly, docs dev working * docs dev working for design customer * json schema * remove unused deps * fix issues with newer fdr version * linter * format * added fixture test for when fdr-sdk supports directory imports * address comments * ci/cd --- docs-yml.schema.json | 10 + fern/apis/docs-yml/definition/docs.yml | 4 + packages/cli/cli/package.json | 2 - ...eOpenApiToFdrApiDefinitionForWorkspaces.ts | 54 +- .../generate/generateDocsWorkspace.ts | 6 + .../writeDocsDefinitionForProject.ts | 6 + .../cli/cli/src/utils/filterOssWorkspaces.ts | 16 + .../cli/configuration-loader/package.json | 2 +- packages/cli/configuration/package.json | 2 +- .../docs/types/ExperimentalConfig.ts | 2 + .../docs/types/ExperimentalConfig.ts | 2 + .../cli/docs-importers/commons/package.json | 2 +- .../cli/docs-importers/mintlify/package.json | 2 +- .../mintlify/src/convertMarkdown.ts | 6 +- packages/cli/docs-markdown-utils/package.json | 2 +- packages/cli/docs-preview/package.json | 4 +- packages/cli/docs-preview/src/previewDocs.ts | 63 +- .../cli/docs-preview/src/runPreviewServer.ts | 1 + packages/cli/docs-resolver/package.json | 19 +- .../src/ApiReferenceNodeConverter.ts | 155 +-- .../src/ApiReferenceNodeConverterLatest.ts | 918 ++++++++++++++++++ .../src/DocsDefinitionResolver.ts | 52 +- .../docs-resolver/src/__test__/dry.test.ts | 2 + .../fixtures/openapi-latest/fern/docs.yml | 9 + .../openapi-latest/fern/fern.config.json | 4 + .../openapi-latest/fern/generators.yml | 1 + .../fixtures/openapi-latest/fern/openapi.yml | 332 +++++++ .../src/__test__/openapi-latest.test.ts | 79 ++ packages/cli/docs-resolver/src/index.ts | 1 + .../src/utils/convertIrToApiDefinition.ts | 8 +- .../src/utils/convertPlaygroundSettings.ts | 27 + .../src/utils/enrichApiPackageChild.ts | 48 + .../utils/generateFdrFromOpenApiWorkspace.ts | 53 + .../src/utils/mergeAndFilterChildren.ts | 27 + .../src/utils/mergeEndpointPairs.ts | 58 ++ .../cli/docs-resolver/src/utils/toPageNode.ts | 42 + .../src/utils/toRelativeFilepath.ts | 17 + packages/cli/ete-tests/package.json | 2 +- .../remote-workspace-runner/package.json | 2 +- .../src/publishDocs.ts | 7 +- .../runRemoteGenerationForDocsWorkspace.ts | 4 + packages/cli/register/package.json | 2 +- packages/core/package.json | 2 +- pnpm-lock.yaml | 181 ++-- 44 files changed, 1974 insertions(+), 264 deletions(-) create mode 100644 packages/cli/cli/src/utils/filterOssWorkspaces.ts create mode 100644 packages/cli/docs-resolver/src/ApiReferenceNodeConverterLatest.ts create mode 100644 packages/cli/docs-resolver/src/__test__/fixtures/openapi-latest/fern/docs.yml create mode 100644 packages/cli/docs-resolver/src/__test__/fixtures/openapi-latest/fern/fern.config.json create mode 100644 packages/cli/docs-resolver/src/__test__/fixtures/openapi-latest/fern/generators.yml create mode 100644 packages/cli/docs-resolver/src/__test__/fixtures/openapi-latest/fern/openapi.yml create mode 100644 packages/cli/docs-resolver/src/__test__/openapi-latest.test.ts create mode 100644 packages/cli/docs-resolver/src/utils/convertPlaygroundSettings.ts create mode 100644 packages/cli/docs-resolver/src/utils/enrichApiPackageChild.ts create mode 100644 packages/cli/docs-resolver/src/utils/generateFdrFromOpenApiWorkspace.ts create mode 100644 packages/cli/docs-resolver/src/utils/mergeAndFilterChildren.ts create mode 100644 packages/cli/docs-resolver/src/utils/mergeEndpointPairs.ts create mode 100644 packages/cli/docs-resolver/src/utils/toPageNode.ts create mode 100644 packages/cli/docs-resolver/src/utils/toRelativeFilepath.ts diff --git a/docs-yml.schema.json b/docs-yml.schema.json index d55cdd974ff..7bb37f2dc7b 100644 --- a/docs-yml.schema.json +++ b/docs-yml.schema.json @@ -2258,6 +2258,16 @@ "type": "null" } ] + }, + "openapi-parser-v2": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false diff --git a/fern/apis/docs-yml/definition/docs.yml b/fern/apis/docs-yml/definition/docs.yml index 469041d7b7c..ad03f1b9102 100644 --- a/fern/apis/docs-yml/definition/docs.yml +++ b/fern/apis/docs-yml/definition/docs.yml @@ -974,6 +974,10 @@ types: If `disable-stream-toggle` is set to true, the stream toggle will be disabled. This behavior is unstable and may change in the future. + openapi-parser-v2: + type: optional + docs: | + If `openapi-parser-v2` is set to true, the OpenAPI parser will be used directly, without Fern. PlaygroundSettings: properties: diff --git a/packages/cli/cli/package.json b/packages/cli/cli/package.json index 6449398fdb9..92439618f06 100644 --- a/packages/cli/cli/package.json +++ b/packages/cli/cli/package.json @@ -45,7 +45,6 @@ "@fern-api/configuration-loader": "workspace:*", "@fern-api/core": "workspace:*", "@fern-api/core-utils": "workspace:*", - "@fern-api/docs-parsers": "^0.0.12", "@fern-api/docs-preview": "workspace:*", "@fern-api/docs-resolver": "workspace:*", "@fern-api/docs-validator": "workspace:*", @@ -106,7 +105,6 @@ "js-yaml": "^4.1.0", "latest-version": "^9.0.0", "lodash-es": "^4.17.21", - "openapi-types": "^12.1.3", "ora": "^7.0.1", "@trivago/prettier-plugin-sort-imports": "^5.2.1", "prettier": "^3.4.2", diff --git a/packages/cli/cli/src/commands/generate-openapi-fdr/generateOpenApiToFdrApiDefinitionForWorkspaces.ts b/packages/cli/cli/src/commands/generate-openapi-fdr/generateOpenApiToFdrApiDefinitionForWorkspaces.ts index b7942228696..720a6f3f7a1 100644 --- a/packages/cli/cli/src/commands/generate-openapi-fdr/generateOpenApiToFdrApiDefinitionForWorkspaces.ts +++ b/packages/cli/cli/src/commands/generate-openapi-fdr/generateOpenApiToFdrApiDefinitionForWorkspaces.ts @@ -1,14 +1,8 @@ import { writeFile } from "fs/promises"; -import { merge } from "lodash-es"; -import { OpenAPIV3_1 } from "openapi-types"; import path from "path"; -// TODO: clean up imports -import { OpenApiDocumentConverterNode } from "@fern-api/docs-parsers"; -import { ErrorCollector } from "@fern-api/docs-parsers"; -import { BaseOpenApiV3_1ConverterNodeContext } from "@fern-api/docs-parsers"; +import { generateFdrFromOpenApiWorkspace } from "@fern-api/docs-resolver"; import { AbsoluteFilePath, stringifyLargeObject } from "@fern-api/fs-utils"; -import { LazyFernWorkspace, OSSWorkspace, OpenAPILoader, getAllOpenAPISpecs } from "@fern-api/lazy-fern-workspace"; import { Project } from "@fern-api/project-loader"; import { CliContext } from "../../cli-context/CliContext"; @@ -25,49 +19,11 @@ export async function generateOpenApiToFdrApiDefinitionForWorkspaces({ await Promise.all( project.apiWorkspaces.map(async (workspace) => { await cliContext.runTaskForWorkspace(workspace, async (context) => { - await cliContext.runTaskForWorkspace(workspace, async (context) => { - if (workspace instanceof LazyFernWorkspace) { - context.logger.info("Skipping, API is specified as a Fern Definition."); - return; - } else if (!(workspace instanceof OSSWorkspace)) { - return; - } + const fdrApiDefinition = await generateFdrFromOpenApiWorkspace(workspace, context); - const openApiLoader = new OpenAPILoader(workspace.absoluteFilePath); - const openApiSpecs = await getAllOpenAPISpecs({ context, specs: workspace.specs }); - - const openApiDocuments = await openApiLoader.loadDocuments({ context, specs: openApiSpecs }); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let fdrApiDefinition: any; - for (const openApi of openApiDocuments) { - if (openApi.type !== "openapi") { - continue; - } - - const oasContext: BaseOpenApiV3_1ConverterNodeContext = { - document: openApi.value as OpenAPIV3_1.Document, - logger: context.logger, - errors: new ErrorCollector() - }; - - const openApiFdrJson = new OpenApiDocumentConverterNode({ - input: openApi.value as OpenAPIV3_1.Document, - context: oasContext, - accessPath: [], - pathId: workspace.workspaceName ?? "openapi parser" - }); - - fdrApiDefinition = merge(fdrApiDefinition, openApiFdrJson.convert()); - } - - const resolvedOutputFilePath = path.resolve(outputFilepath); - await writeFile( - resolvedOutputFilePath, - await stringifyLargeObject(fdrApiDefinition, { pretty: true }) - ); - context.logger.info(`Wrote FDR API definition to ${resolvedOutputFilePath}`); - }); + const resolvedOutputFilePath = path.resolve(outputFilepath); + await writeFile(resolvedOutputFilePath, await stringifyLargeObject(fdrApiDefinition, { pretty: true })); + context.logger.info(`Wrote FDR API definition to ${resolvedOutputFilePath}`); }); }) ); diff --git a/packages/cli/cli/src/commands/generate/generateDocsWorkspace.ts b/packages/cli/cli/src/commands/generate/generateDocsWorkspace.ts index 689d242d804..53d6499b36b 100644 --- a/packages/cli/cli/src/commands/generate/generateDocsWorkspace.ts +++ b/packages/cli/cli/src/commands/generate/generateDocsWorkspace.ts @@ -1,9 +1,12 @@ import { createOrganizationIfDoesNotExist } from "@fern-api/auth"; +import { isNonNullish } from "@fern-api/core-utils"; +import { OSSWorkspace } from "@fern-api/lazy-fern-workspace"; import { askToLogin } from "@fern-api/login"; import { Project } from "@fern-api/project-loader"; import { runRemoteGenerationForDocsWorkspace } from "@fern-api/remote-workspace-runner"; import { CliContext } from "../../cli-context/CliContext"; +import { filterOssWorkspaces } from "../../utils/filterOssWorkspaces"; import { validateDocsWorkspaceAndLogIssues } from "../validate/validateDocsWorkspaceAndLogIssues"; export async function generateDocsWorkspace({ @@ -58,9 +61,12 @@ export async function generateDocsWorkspace({ }) ); + const ossWorkspaces = await filterOssWorkspaces(project); + await runRemoteGenerationForDocsWorkspace({ organization: project.config.organization, fernWorkspaces, + ossWorkspaces, docsWorkspace, context, token, diff --git a/packages/cli/cli/src/commands/write-docs-definition/writeDocsDefinitionForProject.ts b/packages/cli/cli/src/commands/write-docs-definition/writeDocsDefinitionForProject.ts index 2730acf882c..dbf7dde153c 100644 --- a/packages/cli/cli/src/commands/write-docs-definition/writeDocsDefinitionForProject.ts +++ b/packages/cli/cli/src/commands/write-docs-definition/writeDocsDefinitionForProject.ts @@ -1,11 +1,14 @@ import chalk from "chalk"; import { writeFile } from "fs/promises"; +import { isNonNullish } from "@fern-api/core-utils"; import { DocsDefinitionResolver } from "@fern-api/docs-resolver"; import { AbsoluteFilePath } from "@fern-api/fs-utils"; +import { OSSWorkspace } from "@fern-api/lazy-fern-workspace"; import { Project } from "@fern-api/project-loader"; import { CliContext } from "../../cli-context/CliContext"; +import { filterOssWorkspaces } from "../../utils/filterOssWorkspaces"; export async function writeDocsDefinitionForProject({ project, @@ -22,6 +25,8 @@ export async function writeDocsDefinitionForProject({ } await cliContext.runTaskForWorkspace(docsWorkspace, async (context) => { + const ossWorkspaces = await filterOssWorkspaces(project); + const fernWorkspaces = await Promise.all( project.apiWorkspaces.map(async (workspace) => { return workspace.toFernWorkspace( @@ -34,6 +39,7 @@ export async function writeDocsDefinitionForProject({ const docsResolver = new DocsDefinitionResolver( docsWorkspace.config.instances[0]?.url ?? "http://localhost:8080", docsWorkspace, + ossWorkspaces, fernWorkspaces, context ); diff --git a/packages/cli/cli/src/utils/filterOssWorkspaces.ts b/packages/cli/cli/src/utils/filterOssWorkspaces.ts new file mode 100644 index 00000000000..259bd895217 --- /dev/null +++ b/packages/cli/cli/src/utils/filterOssWorkspaces.ts @@ -0,0 +1,16 @@ +import { isNonNullish } from "@fern-api/core-utils"; +import { OSSWorkspace } from "@fern-api/lazy-fern-workspace"; +import { Project } from "@fern-api/project-loader"; + +export async function filterOssWorkspaces(project: Project): Promise { + return ( + await Promise.all( + project.apiWorkspaces.map(async (workspace) => { + if (workspace instanceof OSSWorkspace) { + return workspace as OSSWorkspace; + } + return null; + }) + ) + ).filter(isNonNullish); +} diff --git a/packages/cli/configuration-loader/package.json b/packages/cli/configuration-loader/package.json index 2c886a96d3c..3ed3e153162 100644 --- a/packages/cli/configuration-loader/package.json +++ b/packages/cli/configuration-loader/package.json @@ -32,7 +32,7 @@ "@fern-api/fs-utils": "workspace:*", "@fern-api/task-context": "workspace:*", "@fern-api/fern-definition-schema": "workspace:*", - "@fern-fern/fdr-cjs-sdk": "0.126.1-444264056", + "@fern-fern/fdr-cjs-sdk": "0.127.4-331678a74", "@fern-fern/fiddle-sdk": "0.0.584", "@fern-fern/generators-sdk": "0.114.0-5745f9e74", "find-up": "^6.3.0", diff --git a/packages/cli/configuration/package.json b/packages/cli/configuration/package.json index 2d655eb5547..127dd78f896 100644 --- a/packages/cli/configuration/package.json +++ b/packages/cli/configuration/package.json @@ -30,7 +30,7 @@ "@fern-api/core-utils": "workspace:*", "@fern-api/path-utils": "workspace:*", "@fern-api/fern-definition-schema": "workspace:*", - "@fern-fern/fdr-cjs-sdk": "0.126.1-444264056", + "@fern-fern/fdr-cjs-sdk": "0.127.4-331678a74", "@fern-fern/fiddle-sdk": "0.0.584", "zod": "^3.22.3" }, diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ExperimentalConfig.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ExperimentalConfig.ts index 414ee0c3393..bf1dea97ac2 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ExperimentalConfig.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ExperimentalConfig.ts @@ -14,4 +14,6 @@ export interface ExperimentalConfig { * This behavior is unstable and may change in the future. */ disableStreamToggle?: boolean; + /** If `openapi-parser-v2` is set to true, the OpenAPI parser will be used directly, without Fern. */ + openapiParserV2?: boolean; } diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ExperimentalConfig.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ExperimentalConfig.ts index b1be6412c91..67ed0562277 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ExperimentalConfig.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ExperimentalConfig.ts @@ -15,11 +15,13 @@ export const ExperimentalConfig: core.serialization.ObjectSchema< core.serialization.list(core.serialization.string()).optional(), ), disableStreamToggle: core.serialization.property("disable-stream-toggle", core.serialization.boolean().optional()), + openapiParserV2: core.serialization.property("openapi-parser-v2", core.serialization.boolean().optional()), }); export declare namespace ExperimentalConfig { export interface Raw { "mdx-components"?: string[] | null; "disable-stream-toggle"?: boolean | null; + "openapi-parser-v2"?: boolean | null; } } diff --git a/packages/cli/docs-importers/commons/package.json b/packages/cli/docs-importers/commons/package.json index 49c8418e6b1..8189b6a021e 100644 --- a/packages/cli/docs-importers/commons/package.json +++ b/packages/cli/docs-importers/commons/package.json @@ -30,7 +30,7 @@ "@fern-api/configuration": "workspace:*", "@fern-api/fs-utils": "workspace:*", "@fern-api/task-context": "workspace:*", - "@fern-fern/fdr-cjs-sdk": "0.126.1-444264056", + "@fern-fern/fdr-cjs-sdk": "0.127.4-331678a74", "js-yaml": "^4.1.0" }, "devDependencies": { diff --git a/packages/cli/docs-importers/mintlify/package.json b/packages/cli/docs-importers/mintlify/package.json index 9a82cbe0b0c..076dfb3119b 100644 --- a/packages/cli/docs-importers/mintlify/package.json +++ b/packages/cli/docs-importers/mintlify/package.json @@ -33,7 +33,7 @@ "@fern-api/task-context": "workspace:*", "@fern-api/fs-utils": "workspace:*", "@fern-api/logger": "workspace:*", - "@fern-fern/fdr-cjs-sdk": "0.126.1-444264056", + "@fern-fern/fdr-cjs-sdk": "0.127.4-331678a74", "gray-matter": "^4.0.3" }, "devDependencies": { diff --git a/packages/cli/docs-importers/mintlify/src/convertMarkdown.ts b/packages/cli/docs-importers/mintlify/src/convertMarkdown.ts index b6c5ec18610..8e0d39d5989 100644 --- a/packages/cli/docs-importers/mintlify/src/convertMarkdown.ts +++ b/packages/cli/docs-importers/mintlify/src/convertMarkdown.ts @@ -2,7 +2,7 @@ import { readFile } from "fs/promises"; import grayMatter from "gray-matter"; import { FernDocsBuilder } from "@fern-api/docs-importer-commons"; -import { AbsoluteFilePath, RelativeFilePath, dirname, join, relativize } from "@fern-api/fs-utils"; +import { AbsoluteFilePath, RelativeFilePath, dirname, join } from "@fern-api/fs-utils"; import { FernRegistry as CjsFdrSdk, FernRegistry } from "@fern-fern/fdr-cjs-sdk"; @@ -84,7 +84,9 @@ export async function convertMarkdown({ "twitter:card": undefined, noindex: undefined, nofollow: undefined, - "jsonld:breadcrumb": undefined + "jsonld:breadcrumb": undefined, + logo: undefined, + keywords: undefined }, content: transformedContent }; diff --git a/packages/cli/docs-markdown-utils/package.json b/packages/cli/docs-markdown-utils/package.json index 02a6b44caee..c97f8a788e1 100644 --- a/packages/cli/docs-markdown-utils/package.json +++ b/packages/cli/docs-markdown-utils/package.json @@ -29,7 +29,7 @@ "dependencies": { "@fern-api/fs-utils": "workspace:*", "@fern-api/task-context": "workspace:*", - "@fern-fern/fdr-cjs-sdk": "0.126.1-444264056", + "@fern-fern/fdr-cjs-sdk": "0.127.4-331678a74", "gray-matter": "^4.0.3", "mdast-util-from-markdown": "^2.0.1", "mdast-util-mdx": "^3.0.0", diff --git a/packages/cli/docs-preview/package.json b/packages/cli/docs-preview/package.json index fa895155250..60c7ea1cab0 100644 --- a/packages/cli/docs-preview/package.json +++ b/packages/cli/docs-preview/package.json @@ -27,10 +27,12 @@ "depcheck": "depcheck" }, "dependencies": { + "@fern-api/core-utils": "workspace:*", "@fern-api/docs-resolver": "workspace:*", - "@fern-api/fdr-sdk": "0.126.1-444264056", + "@fern-api/fdr-sdk": "0.127.4-331678a74", "@fern-api/fs-utils": "workspace:*", "@fern-api/ir-sdk": "workspace:*", + "@fern-api/lazy-fern-workspace": "workspace:*", "@fern-api/logger": "workspace:*", "@fern-api/project-loader": "workspace:*", "@fern-api/register": "workspace:*", diff --git a/packages/cli/docs-preview/src/previewDocs.ts b/packages/cli/docs-preview/src/previewDocs.ts index c81038597ef..5ddf4d498e0 100644 --- a/packages/cli/docs-preview/src/previewDocs.ts +++ b/packages/cli/docs-preview/src/previewDocs.ts @@ -1,5 +1,6 @@ import { v4 as uuidv4 } from "uuid"; +import { isNonNullish } from "@fern-api/core-utils"; import { DocsDefinitionResolver } from "@fern-api/docs-resolver"; import { APIV1Read, @@ -15,10 +16,13 @@ import { } from "@fern-api/fdr-sdk"; import { convertToFernHostAbsoluteFilePath } from "@fern-api/fs-utils"; import { IntermediateRepresentation } from "@fern-api/ir-sdk"; +import { OSSWorkspace } from "@fern-api/lazy-fern-workspace"; import { Project } from "@fern-api/project-loader"; import { convertIrToFdrApi } from "@fern-api/register"; import { TaskContext } from "@fern-api/task-context"; +import { parseDocsConfiguration } from "../../configuration-loader/src/docs-yml/parseDocsConfiguration"; + export async function getPreviewDocsDefinition({ domain, project, @@ -44,13 +48,33 @@ export async function getPreviewDocsDefinition({ ) ); + const ossWorkspaces = ( + await Promise.all( + apiWorkspaces.map(async (workspace) => { + if (workspace instanceof OSSWorkspace) { + return workspace as OSSWorkspace; + } + return null; + }) + ) + ).filter(isNonNullish); + const apiCollector = new ReferencedAPICollector(context); + const apiCollectorV2 = new ReferencedAPICollectorV2(context); const filesV2: Record = {}; + const parsedDocsConfig = await parseDocsConfiguration({ + rawDocsConfiguration: docsWorkspace.config, + context, + absolutePathToFernFolder: docsWorkspace.absoluteFilePath, + absoluteFilepathToDocsConfig: docsWorkspace.absoluteFilepathToDocsConfig + }); + const resolver = new DocsDefinitionResolver( domain, docsWorkspace, + ossWorkspaces, fernWorkspaces, context, undefined, @@ -67,7 +91,8 @@ export async function getPreviewDocsDefinition({ fileId }; }), - async (opts) => apiCollector.addReferencedAPI(opts) + async (opts) => apiCollector.addReferencedAPI(opts), + async (opts) => apiCollectorV2.addReferencedAPI(opts) ); const writeDocsDefinition = await resolver.resolve(); @@ -80,7 +105,8 @@ export async function getPreviewDocsDefinition({ }); return { - apis: apiCollector.getAPIsForDefinition(), + apis: parsedDocsConfig.experimental?.openapiParserV2 ? {} : apiCollector.getAPIsForDefinition(), + apisV2: parsedDocsConfig.experimental?.openapiParserV2 ? apiCollectorV2.getAPIsForDefinition() : {}, config: readDocsConfig, files: {}, filesV2, @@ -130,7 +156,10 @@ class ReferencedAPICollector { } catch (e) { // Print Error const err = e as Error; - this.context.logger.error(`Failed to read referenced API: ${err?.message} ${err?.stack}`); + this.context.logger.debug(`Failed to read referenced API: ${err?.message} ${err?.stack}`); + this.context.logger.error( + "An error occured while trying to read an API definition. Please reach out to support." + ); if (err.stack != null) { this.context.logger.error(err?.stack); } @@ -142,3 +171,31 @@ class ReferencedAPICollector { return this.apis; } } + +class ReferencedAPICollectorV2 { + private readonly apis: Record = {}; + + constructor(private readonly context: TaskContext) {} + + public addReferencedAPI({ api }: { api: FdrAPI.api.latest.ApiDefinition }): APIDefinitionID { + try { + this.apis[api.id] = api; + return api.id; + } catch (e) { + // Print Error + const err = e as Error; + this.context.logger.debug(`Failed to read referenced API: ${err?.message} ${err?.stack}`); + this.context.logger.error( + "An error occured while trying to read an API definition. Please reach out to support." + ); + if (err.stack != null) { + this.context.logger.error(err?.stack); + } + throw e; + } + } + + public getAPIsForDefinition(): Record { + return this.apis; + } +} diff --git a/packages/cli/docs-preview/src/runPreviewServer.ts b/packages/cli/docs-preview/src/runPreviewServer.ts index cea13e243f7..04abc13c95e 100644 --- a/packages/cli/docs-preview/src/runPreviewServer.ts +++ b/packages/cli/docs-preview/src/runPreviewServer.ts @@ -18,6 +18,7 @@ import { getPreviewDocsDefinition } from "./previewDocs"; const EMPTY_DOCS_DEFINITION: DocsV1Read.DocsDefinition = { pages: {}, apis: {}, + apisV2: {}, files: {}, filesV2: {}, config: { diff --git a/packages/cli/docs-resolver/package.json b/packages/cli/docs-resolver/package.json index f69bb9f4317..d5669729c38 100644 --- a/packages/cli/docs-resolver/package.json +++ b/packages/cli/docs-resolver/package.json @@ -26,12 +26,15 @@ "depcheck": "depcheck" }, "dependencies": { + "@fern-api/api-workspace-commons": "workspace:*", "@fern-api/cli-source-resolver": "workspace:*", "@fern-api/configuration-loader": "workspace:*", "@fern-api/core-utils": "workspace:*", "@fern-api/docs-markdown-utils": "workspace:*", - "@fern-api/fdr-sdk": "0.126.1-444264056", - "@fern-api/ui-core-utils": "0.126.1-444264056", + "@fern-api/docs-parsers": "^0.0.20", + "@fern-api/fdr-sdk": "0.127.4-331678a74", + "@fern-api/lazy-fern-workspace": "workspace:*", + "@fern-api/ui-core-utils": "0.127.4-331678a74", "@fern-api/fs-utils": "workspace:*", "@fern-api/ir-generator": "workspace:*", "@fern-api/ir-sdk": "workspace:*", @@ -45,12 +48,12 @@ "devDependencies": { "@fern-api/workspace-loader": "workspace:*", "@types/lodash-es": "^4.17.12", - "@types/node": "18.15.3", - "depcheck": "^1.4.7", - "eslint": "^9.16.0", - "@trivago/prettier-plugin-sort-imports": "^5.2.1", - "prettier": "^3.4.2", - "typescript": "5.7.2", + "@types/node": "18.7.18", + "depcheck": "^1.4.6", + "eslint": "^8.56.0", + "openapi-types": "^10.0.0", + "prettier": "^2.7.1", + "typescript": "4.6.4", "vitest": "^2.0.5" } } \ No newline at end of file diff --git a/packages/cli/docs-resolver/src/ApiReferenceNodeConverter.ts b/packages/cli/docs-resolver/src/ApiReferenceNodeConverter.ts index c1d2092462b..1702ff82daf 100644 --- a/packages/cli/docs-resolver/src/ApiReferenceNodeConverter.ts +++ b/packages/cli/docs-resolver/src/ApiReferenceNodeConverter.ts @@ -4,7 +4,7 @@ import urlJoin from "url-join"; import { docsYml } from "@fern-api/configuration-loader"; import { isNonNullish } from "@fern-api/core-utils"; import { APIV1Read, FernNavigation } from "@fern-api/fdr-sdk"; -import { AbsoluteFilePath, RelativeFilePath, relative } from "@fern-api/fs-utils"; +import { AbsoluteFilePath } from "@fern-api/fs-utils"; import { TaskContext } from "@fern-api/task-context"; import { titleCase, visitDiscriminatedUnion } from "@fern-api/ui-core-utils"; import { DocsWorkspace, FernWorkspace } from "@fern-api/workspace-loader"; @@ -12,8 +12,14 @@ import { DocsWorkspace, FernWorkspace } from "@fern-api/workspace-loader"; import { ApiDefinitionHolder } from "./ApiDefinitionHolder"; import { ChangelogNodeConverter } from "./ChangelogNodeConverter"; import { NodeIdGenerator } from "./NodeIdGenerator"; +import { convertPlaygroundSettings } from "./utils/convertPlaygroundSettings"; +import { enrichApiPackageChild } from "./utils/enrichApiPackageChild"; import { isSubpackage } from "./utils/isSubpackage"; +import { mergeAndFilterChildren } from "./utils/mergeAndFilterChildren"; +import { mergeEndpointPairs } from "./utils/mergeEndpointPairs"; import { stringifyEndpointPathParts } from "./utils/stringifyEndpointPathParts"; +import { toPageNode } from "./utils/toPageNode"; +import { toRelativeFilepath } from "./utils/toRelativeFilepath"; export class ApiReferenceNodeConverter { apiDefinitionId: FernNavigation.V1.ApiDefinitionId; @@ -47,7 +53,7 @@ export class ApiReferenceNodeConverter { this.#overviewPageId = this.apiSection.overviewAbsolutePath != null - ? FernNavigation.V1.PageId(this.toRelativeFilepath(this.apiSection.overviewAbsolutePath)) + ? FernNavigation.V1.PageId(toRelativeFilepath(this.docsWorkspace, this.apiSection.overviewAbsolutePath)) : undefined; // the overview page markdown could contain a full slug, which would be used as the base slug for the API section. @@ -148,24 +154,13 @@ export class ApiReferenceNodeConverter { page: docsYml.DocsNavigationItem.Page, parentSlug: FernNavigation.V1.SlugGenerator ): FernNavigation.V1.PageNode { - const pageId = FernNavigation.V1.PageId(this.toRelativeFilepath(page.absolutePath)); - const pageSlug = parentSlug.apply({ - fullSlug: this.markdownFilesToFullSlugs.get(page.absolutePath)?.split("/"), - urlSlug: page.slug ?? kebabCase(page.title) + return toPageNode({ + page, + parentSlug, + docsWorkspace: this.docsWorkspace, + markdownFilesToFullSlugs: this.markdownFilesToFullSlugs, + idgen: this.#idgen }); - return { - id: this.#idgen.get(pageId), - type: "page", - pageId, - title: page.title, - slug: pageSlug.get(), - icon: page.icon, - hidden: page.hidden, - noindex: page.noindex, - authed: undefined, - viewers: page.viewers, - orphaned: page.orphaned - }; } #convertPackage( @@ -174,7 +169,7 @@ export class ApiReferenceNodeConverter { ): FernNavigation.V1.ApiPackageNode { const overviewPageId = pkg.overviewAbsolutePath != null - ? FernNavigation.V1.PageId(this.toRelativeFilepath(pkg.overviewAbsolutePath)) + ? FernNavigation.V1.PageId(toRelativeFilepath(this.docsWorkspace, pkg.overviewAbsolutePath)) : undefined; const maybeFullSlug = @@ -265,7 +260,7 @@ export class ApiReferenceNodeConverter { ): FernNavigation.V1.ApiPackageNode { const overviewPageId = section.overviewAbsolutePath != null - ? FernNavigation.V1.PageId(this.toRelativeFilepath(section.overviewAbsolutePath)) + ? FernNavigation.V1.PageId(toRelativeFilepath(this.docsWorkspace, section.overviewAbsolutePath)) : undefined; const maybeFullSlug = @@ -512,33 +507,25 @@ export class ApiReferenceNodeConverter { left: FernNavigation.V1.ApiPackageChild[], right: FernNavigation.V1.ApiPackageChild[] ): FernNavigation.V1.ApiPackageChild[] { - return this.mergeEndpointPairs([...left, ...right]).filter((child) => - child.type === "apiPackage" ? child.children.length > 0 : true - ); + return mergeAndFilterChildren({ + left, + right, + findEndpointById: (endpointId) => this.#holder.endpoints.get(endpointId), + stringifyEndpointPathParts: (endpoint: APIV1Read.EndpointDefinition) => + stringifyEndpointPathParts(endpoint.path.parts), + disableEndpointPairs: this.disableEndpointPairs, + apiDefinitionId: this.apiDefinitionId + }); } #enrichApiPackageChild(child: FernNavigation.V1.ApiPackageChild): FernNavigation.V1.ApiPackageChild { - if (child.type === "apiPackage") { - // expand the subpackage to include children that haven't been visited yet - const slug = FernNavigation.V1.SlugGenerator.init(child.slug); - const subpackageIds = this.#nodeIdToSubpackageId.get(child.id) ?? []; - const subpackageChildren = subpackageIds.flatMap((subpackageId) => - this.#convertApiDefinitionPackageId(subpackageId, slug) - ); - - // recursively apply enrichment to children - const enrichedChildren = child.children.map((innerChild) => this.#enrichApiPackageChild(innerChild)); - - // combine children with subpackage (tacked on at the end to preserve order) - const children = this.#mergeAndFilterChildren(enrichedChildren, subpackageChildren); - - return { - ...child, - children, - pointsTo: undefined - }; - } - return child; + return enrichApiPackageChild({ + child, + nodeIdToSubpackageId: this.#nodeIdToSubpackageId, + convertApiDefinitionPackageId: (subpackageId, slug) => + this.#convertApiDefinitionPackageId(subpackageId, slug), + mergeAndFilterChildren: this.#mergeAndFilterChildren.bind(this) + }); } #convertApiDefinitionPackage( @@ -698,79 +685,17 @@ export class ApiReferenceNodeConverter { #convertPlaygroundSettings( playgroundSettings?: docsYml.RawSchemas.PlaygroundSettings ): FernNavigation.V1.PlaygroundSettings | undefined { - if (playgroundSettings) { - return { - environments: - playgroundSettings.environments != null && playgroundSettings.environments.length > 0 - ? playgroundSettings.environments.map((environmentId) => - FernNavigation.V1.EnvironmentId(environmentId) - ) - : undefined, - button: - playgroundSettings.button != null && playgroundSettings.button.href - ? { href: FernNavigation.V1.Url(playgroundSettings.button.href) } - : undefined, - "limit-websocket-messages-per-connection": - playgroundSettings.limitWebsocketMessagesPerConnection != null - ? playgroundSettings.limitWebsocketMessagesPerConnection - : undefined - }; - } - - return; + return convertPlaygroundSettings(playgroundSettings); } private mergeEndpointPairs(children: FernNavigation.V1.ApiPackageChild[]): FernNavigation.V1.ApiPackageChild[] { - if (this.disableEndpointPairs) { - return children; - } - - const toRet: FernNavigation.V1.ApiPackageChild[] = []; - - const methodAndPathToEndpointNode = new Map(); - children.forEach((child) => { - if (child.type !== "endpoint") { - toRet.push(child); - return; - } - - const endpoint = this.#holder.endpoints.get(child.endpointId); - if (endpoint == null) { - throw new Error(`Endpoint ${child.endpointId} not found`); - } - - const methodAndPath = `${endpoint.method} ${stringifyEndpointPathParts(endpoint.path.parts)}`; - - const existing = methodAndPathToEndpointNode.get(methodAndPath); - methodAndPathToEndpointNode.set(methodAndPath, child); - - if (existing == null || existing.isResponseStream === child.isResponseStream) { - toRet.push(child); - return; - } - - const idx = toRet.indexOf(existing); - const stream = child.isResponseStream ? child : existing; - const nonStream = child.isResponseStream ? existing : child; - const pairNode: FernNavigation.V1.EndpointPairNode = { - id: FernNavigation.V1.NodeId(`${this.apiDefinitionId}:${nonStream.endpointId}+${stream.endpointId}`), - type: "endpointPair", - stream, - nonStream - }; - - toRet[idx] = pairNode; + return mergeEndpointPairs({ + children, + findEndpointById: (endpointId: APIV1Read.EndpointId) => this.#holder.endpoints.get(endpointId), + stringifyEndpointPathParts: (endpoint: APIV1Read.EndpointDefinition) => + stringifyEndpointPathParts(endpoint.path.parts), + disableEndpointPairs: this.disableEndpointPairs, + apiDefinitionId: this.apiDefinitionId }); - - return toRet; - } - - private toRelativeFilepath(filepath: AbsoluteFilePath): RelativeFilePath; - private toRelativeFilepath(filepath: AbsoluteFilePath | undefined): RelativeFilePath | undefined; - private toRelativeFilepath(filepath: AbsoluteFilePath | undefined): RelativeFilePath | undefined { - if (filepath == null) { - return undefined; - } - return relative(this.docsWorkspace.absoluteFilePath, filepath); } } diff --git a/packages/cli/docs-resolver/src/ApiReferenceNodeConverterLatest.ts b/packages/cli/docs-resolver/src/ApiReferenceNodeConverterLatest.ts new file mode 100644 index 00000000000..5a913ddf9d4 --- /dev/null +++ b/packages/cli/docs-resolver/src/ApiReferenceNodeConverterLatest.ts @@ -0,0 +1,918 @@ +import { camelCase, kebabCase } from "lodash-es"; +import urlJoin from "url-join"; + +import { docsYml } from "@fern-api/configuration-loader"; +import { isNonNullish } from "@fern-api/core-utils"; +import { FdrAPI, FernNavigation } from "@fern-api/fdr-sdk"; +import { AbsoluteFilePath } from "@fern-api/fs-utils"; +import { OSSWorkspace } from "@fern-api/lazy-fern-workspace"; +import { TaskContext } from "@fern-api/task-context"; +import { titleCase, visitDiscriminatedUnion } from "@fern-api/ui-core-utils"; +import { DocsWorkspace } from "@fern-api/workspace-loader"; + +import { ChangelogNodeConverter } from "./ChangelogNodeConverter"; +import { NodeIdGenerator } from "./NodeIdGenerator"; +import { convertPlaygroundSettings } from "./utils/convertPlaygroundSettings"; +import { enrichApiPackageChild } from "./utils/enrichApiPackageChild"; +import { mergeAndFilterChildren } from "./utils/mergeAndFilterChildren"; +import { mergeEndpointPairs } from "./utils/mergeEndpointPairs"; +import { stringifyEndpointPathParts } from "./utils/stringifyEndpointPathParts"; +import { toPageNode } from "./utils/toPageNode"; +import { toRelativeFilepath } from "./utils/toRelativeFilepath"; + +// TODO: these functions need an extra piece of information from the fdr latest shape, to see if the operation id takes precedence over slug generation +function getLatestEndpointUrlSlug(endpoint: FdrAPI.api.latest.endpoint.EndpointDefinition) { + const slugParts = endpoint.namespace?.map((subpackageId) => subpackageId.toString()) ?? []; + slugParts.push(kebabCase(endpoint.id.split(".").pop() ?? "")); + return endpoint.operationId != null ? kebabCase(endpoint.operationId) : urlJoin(slugParts); +} + +function getLatestWebSocketUrlSlug(webSocket: FdrAPI.api.latest.websocket.WebSocketChannel) { + const slugParts = webSocket.namespace?.map((subpackageId) => subpackageId.toString()) ?? []; + slugParts.push(kebabCase(webSocket.id.split(".").pop() ?? "")); + return webSocket.operationId != null ? kebabCase(webSocket.operationId) : urlJoin(slugParts); +} + +function getLatestWebhookUrlSlug(webhook: FdrAPI.api.latest.webhook.WebhookDefinition) { + const slugParts = webhook.namespace?.map((subpackageId) => subpackageId.toString()) ?? []; + slugParts.push(kebabCase(webhook.id.split(".").pop() ?? "")); + return webhook.operationId != null ? kebabCase(webhook.operationId) : urlJoin(slugParts); +} +// END TODO + +export class ApiReferenceNodeConverterLatest { + apiDefinitionId: FernNavigation.V1.ApiDefinitionId; + #api: FdrAPI.api.latest.ApiDefinition; + #visitedEndpoints = new Set(); + #visitedWebSockets = new Set(); + #visitedWebhooks = new Set(); + #visitedSubpackages = new Set(); + #nodeIdToSubpackageId = new Map(); + #children: FernNavigation.V1.ApiPackageChild[] = []; + #overviewPageId: FernNavigation.V1.PageId | undefined; + #slug: FernNavigation.V1.SlugGenerator; + #idgen: NodeIdGenerator; + #topLevelSubpackages: Map = new Map(); + private disableEndpointPairs; + constructor( + private apiSection: docsYml.DocsNavigationItem.ApiSection, + api: FdrAPI.api.latest.ApiDefinition, + parentSlug: FernNavigation.V1.SlugGenerator, + private workspace: OSSWorkspace, + private docsWorkspace: DocsWorkspace, + private taskContext: TaskContext, + private markdownFilesToFullSlugs: Map, + idgen: NodeIdGenerator + ) { + this.#api = api; + this.disableEndpointPairs = docsWorkspace.config.experimental?.disableStreamToggle ?? false; + this.apiDefinitionId = FernNavigation.V1.ApiDefinitionId(api.id); + + // we are assuming that the apiDefinitionId is unique. + this.#idgen = idgen; + + this.#overviewPageId = + this.apiSection.overviewAbsolutePath != null + ? FernNavigation.V1.PageId(toRelativeFilepath(this.docsWorkspace, this.apiSection.overviewAbsolutePath)) + : undefined; + + // the overview page markdown could contain a full slug, which would be used as the base slug for the API section. + const maybeFullSlug = + this.apiSection.overviewAbsolutePath != null + ? this.markdownFilesToFullSlugs.get(this.apiSection.overviewAbsolutePath) + : undefined; + + this.#slug = parentSlug.apply({ + fullSlug: maybeFullSlug?.split("/"), + skipUrlSlug: this.apiSection.skipUrlSlug, + urlSlug: this.apiSection.slug ?? kebabCase(this.apiSection.title) + }); + + // Step 1. Convert the navigation items that are manually defined in the API section. + if (this.apiSection.navigation != null) { + this.#children = this.#convertApiReferenceLayoutItems(this.apiSection.navigation, this.#slug); + } + + // Step 2. Fill in the any missing navigation items from the API definition + this.#children = this.#mergeAndFilterChildren( + this.#children.map((child) => this.#enrichApiPackageChild(child)), + this.#convertApiDefinitionPackage(this.#api, this.#slug) + ); + } + + public get(): FernNavigation.V1.ApiReferenceNode { + const pointsTo = FernNavigation.V1.followRedirects(this.#children); + const changelogNodeConverter = new ChangelogNodeConverter( + this.markdownFilesToFullSlugs, + this.workspace.changelog?.files.map((file) => file.absoluteFilepath), + this.docsWorkspace, + this.#idgen + ).orUndefined(); + return { + id: this.#idgen.get(this.apiDefinitionId), + type: "apiReference", + title: this.apiSection.title, + apiDefinitionId: this.apiDefinitionId, + overviewPageId: this.#overviewPageId, + paginated: this.apiSection.paginated, + slug: this.#slug.get(), + icon: this.apiSection.icon, + hidden: this.apiSection.hidden, + hideTitle: this.apiSection.flattened, + showErrors: this.apiSection.showErrors, + changelog: changelogNodeConverter?.toChangelogNode({ + parentSlug: this.#slug, + viewers: undefined + }), + children: this.#children, + availability: undefined, + pointsTo, + noindex: undefined, + playground: this.#convertPlaygroundSettings(this.apiSection.playground), + authed: undefined, + viewers: this.apiSection.viewers, + orphaned: this.apiSection.orphaned + }; + } + + #findSubpackageByLocator(locator: string): FdrAPI.api.latest.SubpackageMetadata | undefined { + return ( + this.#api?.subpackages[FdrAPI.api.v1.SubpackageId(locator)] ?? + this.#api?.subpackages[ + FdrAPI.api.v1.SubpackageId(locator.replace(".yml", "").replace(".yaml", "").split(".").pop() ?? "") + ] ?? + this.#api?.subpackages[FdrAPI.api.v1.SubpackageId(locator.split("/").pop() ?? "")] + ); + } + + #findEndpointByLocator(locator: string): FdrAPI.api.latest.endpoint.EndpointDefinition | undefined { + return ( + this.#api?.endpoints[FdrAPI.EndpointId(locator)] ?? + this.#api?.endpoints[FdrAPI.EndpointId(locator.split(".").pop() ?? "")] ?? + this.#api?.endpoints[FdrAPI.EndpointId(locator.split("/").pop() ?? "")] ?? + Object.values(this.#api?.endpoints ?? {}).find((endpoint) => endpoint.id.includes(locator)) ?? + Object.values(this.#api?.endpoints ?? {}).find( + (endpoint) => `${endpoint.method} ${stringifyEndpointPathParts(endpoint.path)}` === locator + ) ?? + Object.values(this.#api?.endpoints ?? {}).find( + (endpoint) => `STREAM ${stringifyEndpointPathParts(endpoint.path)}` === locator + ) + ); + } + + #findWebSocketByLocator(locator: string): FdrAPI.api.latest.websocket.WebSocketChannel | undefined { + return ( + this.#api?.websockets[FdrAPI.WebSocketId(locator)] ?? + this.#api?.websockets[FdrAPI.WebSocketId(locator.split(".").pop() ?? "")] ?? + this.#api?.websockets[FdrAPI.WebSocketId(locator.split("/").pop() ?? "")] ?? + Object.values(this.#api?.websockets ?? {}).find((endpoint) => endpoint.id.includes(locator)) + ); + } + + #findWebhookByLocator(locator: string): FdrAPI.api.latest.webhook.WebhookDefinition | undefined { + return ( + this.#api?.webhooks[FdrAPI.WebhookId(locator)] ?? + this.#api?.webhooks[FdrAPI.WebhookId(locator.split(".").pop() ?? "")] ?? + this.#api?.webhooks[FdrAPI.WebhookId(locator.split("/").pop() ?? "")] ?? + Object.values(this.#api?.webhooks ?? {}).find((endpoint) => endpoint.id.includes(locator)) + ); + } + + // Step 1 + + #convertApiReferenceLayoutItems( + navigation: docsYml.ParsedApiReferenceLayoutItem[], + parentSlug: FernNavigation.V1.SlugGenerator + ): FernNavigation.V1.ApiPackageChild[] { + return navigation + .map((item) => + visitDiscriminatedUnion(item)._visit({ + link: (link) => ({ + id: this.#idgen.get(link.url), + type: "link", + title: link.text, + icon: link.icon, + url: FernNavigation.Url(link.url) + }), + page: (page) => this.#toPageNode(page, parentSlug), + package: (pkg) => this.#convertPackage(pkg, parentSlug), + section: (section) => this.#convertSection(section, parentSlug), + item: ({ value: unknownIdentifier }): FernNavigation.V1.ApiPackageChild | undefined => + this.#convertUnknownIdentifier(unknownIdentifier, parentSlug), + endpoint: (endpoint) => this.#convertEndpoint(endpoint, parentSlug) + }) + ) + .filter(isNonNullish); + } + + #toPageNode( + page: docsYml.DocsNavigationItem.Page, + parentSlug: FernNavigation.V1.SlugGenerator + ): FernNavigation.V1.PageNode { + return toPageNode({ + docsWorkspace: this.docsWorkspace, + page, + parentSlug, + idgen: this.#idgen, + markdownFilesToFullSlugs: this.markdownFilesToFullSlugs + }); + } + + #convertPackage( + pkg: docsYml.ParsedApiReferenceLayoutItem.Package, + parentSlug: FernNavigation.V1.SlugGenerator + ): FernNavigation.V1.ApiPackageNode { + const overviewPageId = + pkg.overviewAbsolutePath != null + ? FernNavigation.V1.PageId(toRelativeFilepath(this.docsWorkspace, pkg.overviewAbsolutePath)) + : undefined; + + const maybeFullSlug = + pkg.overviewAbsolutePath != null ? this.markdownFilesToFullSlugs.get(pkg.overviewAbsolutePath) : undefined; + + const subpackage = this.#findSubpackageByLocator(pkg.package); + + if (subpackage != null) { + const subpackageNodeId = this.#idgen.get(overviewPageId ?? `${this.apiDefinitionId}:${subpackage.id}`); + + if (this.#visitedSubpackages.has(subpackage.id)) { + this.taskContext.logger.error( + `Duplicate subpackage found in the API Reference layout: ${subpackage.id}` + ); + } + + this.#visitedSubpackages.add(subpackage.id); + this.#nodeIdToSubpackageId.set(subpackageNodeId, [subpackage.id]); + const urlSlug = pkg.slug ?? subpackage.name; + const slug = parentSlug.apply({ + fullSlug: maybeFullSlug?.split("/"), + skipUrlSlug: pkg.skipUrlSlug, + urlSlug + }); + const convertedItems = this.#convertApiReferenceLayoutItems(pkg.contents, slug); + const subpackageNode: FernNavigation.V1.ApiPackageNode = { + id: subpackageNodeId, + type: "apiPackage", + children: convertedItems, + title: pkg.title ?? subpackage.displayName ?? titleCase(subpackage.name), + slug: slug.get(), + icon: pkg.icon, + hidden: pkg.hidden, + overviewPageId, + availability: undefined, + apiDefinitionId: this.apiDefinitionId, + pointsTo: undefined, + noindex: undefined, + playground: this.#convertPlaygroundSettings(pkg.playground), + authed: undefined, + viewers: pkg.viewers, + orphaned: pkg.orphaned + }; + + this.#topLevelSubpackages.set(subpackage.id, subpackageNode); + return subpackageNode; + } else { + this.taskContext.logger.warn( + `Subpackage ${pkg.package} not found in ${this.apiDefinitionId}, treating it as a section` + ); + const urlSlug = pkg.slug ?? kebabCase(pkg.package); + const slug = parentSlug.apply({ + fullSlug: maybeFullSlug?.split("/"), + skipUrlSlug: pkg.skipUrlSlug, + urlSlug + }); + const convertedItems = this.#convertApiReferenceLayoutItems(pkg.contents, slug); + const sectionNode: FernNavigation.V1.ApiPackageNode = { + id: this.#idgen.get(overviewPageId ?? `${this.apiDefinitionId}:${kebabCase(pkg.package)}`), + type: "apiPackage", + children: convertedItems, + title: pkg.title ?? pkg.package, + slug: slug.get(), + icon: pkg.icon, + hidden: pkg.hidden, + overviewPageId, + availability: undefined, + apiDefinitionId: this.apiDefinitionId, + pointsTo: undefined, + noindex: undefined, + playground: this.#convertPlaygroundSettings(pkg.playground), + authed: undefined, + viewers: pkg.viewers, + orphaned: pkg.orphaned + }; + + this.#topLevelSubpackages.set(pkg.package, sectionNode); + return sectionNode; + } + } + + #convertSection( + section: docsYml.ParsedApiReferenceLayoutItem.Section, + parentSlug: FernNavigation.V1.SlugGenerator + ): FernNavigation.V1.ApiPackageNode { + const overviewPageId = + section.overviewAbsolutePath != null + ? FernNavigation.V1.PageId(toRelativeFilepath(this.docsWorkspace, section.overviewAbsolutePath)) + : undefined; + + const maybeFullSlug = + section.overviewAbsolutePath != null + ? this.markdownFilesToFullSlugs.get(section.overviewAbsolutePath) + : undefined; + + const nodeId = this.#idgen.get(overviewPageId ?? maybeFullSlug ?? parentSlug.get()); + + const subpackageIds = section.referencedSubpackages + .map((locator) => { + const subpackage = this.#findSubpackageByLocator(locator); + + return subpackage != null ? subpackage.id : undefined; + }) + .filter((subpackageId) => { + if (subpackageId == null) { + this.taskContext.logger.error(`Subpackage ${subpackageId} not found in ${this.apiDefinitionId}`); + } + return subpackageId != null; + }) + .filter(isNonNullish); + + this.#nodeIdToSubpackageId.set(nodeId, subpackageIds); + subpackageIds.forEach((subpackageId) => { + if (this.#visitedSubpackages.has(subpackageId)) { + this.taskContext.logger.error( + `Duplicate subpackage found in the API Reference layout: ${subpackageId}` + ); + } + this.#visitedSubpackages.add(subpackageId); + }); + + const urlSlug = section.slug ?? kebabCase(section.title); + const slug = parentSlug.apply({ + fullSlug: maybeFullSlug?.split("/"), + skipUrlSlug: section.skipUrlSlug, + urlSlug + }); + const convertedItems = this.#convertApiReferenceLayoutItems(section.contents, slug); + return { + id: nodeId, + type: "apiPackage", + children: convertedItems, + title: section.title, + slug: slug.get(), + icon: section.icon, + hidden: section.hidden, + overviewPageId, + availability: undefined, + apiDefinitionId: this.apiDefinitionId, + pointsTo: undefined, + noindex: undefined, + playground: this.#convertPlaygroundSettings(section.playground), + authed: undefined, + viewers: section.viewers, + orphaned: section.orphaned + }; + } + + #convertUnknownIdentifier( + unknownIdentifier: string, + parentSlug: FernNavigation.V1.SlugGenerator + ): FernNavigation.V1.ApiPackageChild | undefined { + unknownIdentifier = unknownIdentifier.trim(); + // unknownIdentifier could either be a package, endpoint, websocket, or webhook. + // We need to determine which one it is. + + // if the unknownIdentifier is a subpackage, we need to check subpackage metadata, and any locators (strip .yml) + + const subpackage = this.#findSubpackageByLocator(unknownIdentifier); + + if (subpackage != null) { + const subpackageId = subpackage.id; + const subpackageNodeId = this.#idgen.get(`${this.apiDefinitionId}:${subpackageId}`); + + if (this.#visitedSubpackages.has(subpackageId)) { + this.taskContext.logger.error( + `Duplicate subpackage found in the API Reference layout: ${subpackageId}` + ); + } + + this.#visitedSubpackages.add(subpackageId); + this.#nodeIdToSubpackageId.set(subpackageNodeId, [subpackageId]); + const urlSlug = subpackage.name; + const slug = parentSlug.apply({ urlSlug }); + const subpackageNode: FernNavigation.V1.ApiPackageNode = { + id: subpackageNodeId, + type: "apiPackage", + children: [], + title: subpackage.displayName ?? titleCase(subpackage.name), + slug: slug.get(), + icon: undefined, + hidden: undefined, + overviewPageId: undefined, + availability: undefined, + apiDefinitionId: this.apiDefinitionId, + pointsTo: undefined, + noindex: undefined, + playground: undefined, + authed: undefined, + viewers: undefined, + orphaned: undefined + }; + + this.#topLevelSubpackages.set(subpackageId, subpackageNode); + return subpackageNode; + } + + // if the unknownIdentifier is not a subpackage, it could be an http endpoint, websocket, or webhook. + return this.#convertEndpoint( + { + type: "endpoint", + endpoint: unknownIdentifier, + title: undefined, + icon: undefined, + slug: undefined, + hidden: undefined, + playground: undefined, + viewers: undefined, + orphaned: undefined + }, + parentSlug + ); + } + + #convertEndpoint( + endpointItem: docsYml.ParsedApiReferenceLayoutItem.Endpoint, + parentSlug: FernNavigation.V1.SlugGenerator + ): FernNavigation.V1.ApiPackageChild | undefined { + const endpoint = this.#findEndpointByLocator(endpointItem.endpoint); + + if (endpoint != null) { + if (endpoint.id == null) { + throw new Error(`Expected Endpoint ID for ${endpoint.id}. Got undefined.`); + } + if (this.#visitedEndpoints.has(endpoint.id)) { + this.taskContext.logger.error(`Duplicate endpoint found in the API Reference layout: ${endpoint.id}`); + } + this.#visitedEndpoints.add(endpoint.id); + const endpointSlug = + endpointItem.slug != null + ? parentSlug.append(endpointItem.slug) + : parentSlug.apply({ urlSlug: getLatestEndpointUrlSlug(endpoint) }); + return { + id: this.#idgen.get(`${this.apiDefinitionId}:${endpoint.id}`), + type: "endpoint", + method: endpoint.method, + endpointId: endpoint.id, + apiDefinitionId: this.apiDefinitionId, + availability: FernNavigation.V1.convertAvailability(endpoint.availability), + isResponseStream: endpoint.responses?.[0]?.body.type === "stream", + title: endpointItem.title ?? endpoint.displayName ?? stringifyEndpointPathParts(endpoint.path), + slug: endpointSlug.get(), + icon: endpointItem.icon, + hidden: endpointItem.hidden, + playground: this.#convertPlaygroundSettings(endpointItem.playground), + authed: undefined, + viewers: endpointItem.viewers, + orphaned: endpointItem.orphaned + }; + } + + const webSocket = this.#findWebSocketByLocator(endpointItem.endpoint); + + if (webSocket != null) { + if (webSocket.id == null) { + throw new Error(`Expected WebSocket ID for ${webSocket.id}. Got undefined.`); + } + if (this.#visitedWebSockets.has(webSocket.id)) { + this.taskContext.logger.error( + `Duplicate web socket found in the API Reference layout: ${webSocket.id}` + ); + } + this.#visitedWebSockets.add(webSocket.id); + return { + id: this.#idgen.get(`${this.apiDefinitionId}:${webSocket.id}`), + type: "webSocket", + webSocketId: webSocket.id, + title: endpointItem.title ?? webSocket.displayName ?? stringifyEndpointPathParts(webSocket.path), + slug: (endpointItem.slug != null + ? parentSlug.append(endpointItem.slug) + : parentSlug.apply({ urlSlug: getLatestWebSocketUrlSlug(webSocket) }) + ).get(), + icon: endpointItem.icon, + hidden: endpointItem.hidden, + apiDefinitionId: this.apiDefinitionId, + availability: FernNavigation.V1.convertAvailability(webSocket.availability), + playground: this.#convertPlaygroundSettings(endpointItem.playground), + authed: undefined, + viewers: endpointItem.viewers, + orphaned: endpointItem.orphaned + }; + } + + const webhook = this.#findWebhookByLocator(endpointItem.endpoint); + + if (webhook != null) { + if (webhook.id == null) { + throw new Error(`Expected Webhook ID for ${webhook.id}. Got undefined.`); + } + if (this.#visitedWebhooks.has(webhook.id)) { + this.taskContext.logger.error(`Duplicate webhook found in the API Reference layout: ${webhook.id}`); + } + this.#visitedWebhooks.add(webhook.id); + return { + id: this.#idgen.get(`${this.apiDefinitionId}:${webhook.id}`), + type: "webhook", + webhookId: webhook.id, + method: webhook.method, + title: endpointItem.title ?? webhook.displayName ?? urlJoin("/", ...webhook.path), + slug: (endpointItem.slug != null + ? parentSlug.append(endpointItem.slug) + : parentSlug.apply({ urlSlug: getLatestWebhookUrlSlug(webhook) }) + ).get(), + icon: endpointItem.icon, + hidden: endpointItem.hidden, + apiDefinitionId: this.apiDefinitionId, + availability: undefined, + authed: undefined, + viewers: endpointItem.viewers, + orphaned: endpointItem.orphaned + }; + } + + this.taskContext.logger.error("Unknown identifier in the API Reference layout: ", endpointItem.endpoint); + + return; + } + + // Step 2 + + #mergeAndFilterChildren( + left: FernNavigation.V1.ApiPackageChild[], + right: FernNavigation.V1.ApiPackageChild[] + ): FernNavigation.V1.ApiPackageChild[] { + return mergeAndFilterChildren({ + left, + right, + findEndpointById: (endpointId) => this.#findEndpointByLocator(endpointId), + stringifyEndpointPathParts: (endpoint: FdrAPI.api.latest.EndpointDefinition) => + stringifyEndpointPathParts(endpoint.path), + disableEndpointPairs: this.disableEndpointPairs, + apiDefinitionId: this.apiDefinitionId + }); + } + + #enrichApiPackageChild(child: FernNavigation.V1.ApiPackageChild): FernNavigation.V1.ApiPackageChild { + return enrichApiPackageChild({ + child, + nodeIdToSubpackageId: this.#nodeIdToSubpackageId, + convertApiDefinitionPackageId: (subpackageId, slug) => + this.#convertApiDefinitionPackageId(subpackageId, slug), + mergeAndFilterChildren: this.#mergeAndFilterChildren.bind(this) + }); + } + + #convertApiDefinitionPackage( + pkg: FdrAPI.api.latest.ApiDefinition, + parentSlug: FernNavigation.V1.SlugGenerator + ): FernNavigation.V1.ApiPackageChild[] { + // if an endpoint, websocket, webhook, or subpackage is not visited, add it to the additional children list + let additionalChildren: FernNavigation.V1.ApiPackageChild[] = []; + + Object.entries(pkg.subpackages).forEach(([subpackageId, subpackageMetadata]) => { + if (this.#visitedSubpackages.has(subpackageId)) { + return; + } + + const slug = parentSlug.apply({ + urlSlug: subpackageMetadata.name + }); + const subpackageNode: FernNavigation.V1.ApiPackageNode = { + id: FernNavigation.V1.NodeId(`${this.apiDefinitionId}:${subpackageId}`), + type: "apiPackage", + children: [], + title: subpackageMetadata.displayName ?? titleCase(subpackageMetadata.name), + slug: slug.get(), + icon: undefined, + hidden: undefined, + overviewPageId: undefined, + availability: undefined, + apiDefinitionId: this.apiDefinitionId, + pointsTo: undefined, + noindex: undefined, + playground: undefined, + authed: undefined, + viewers: undefined, + orphaned: undefined + }; + this.#topLevelSubpackages.set(subpackageId, subpackageNode); + additionalChildren.push(subpackageNode); + }); + + Object.entries(pkg.endpoints).forEach(([endpointId, endpoint]) => { + if (endpointId == null) { + throw new Error(`Expected Endpoint ID for ${endpoint.id}. Got undefined.`); + } + if (this.#visitedEndpoints.has(FdrAPI.EndpointId(endpointId))) { + return; + } + + const endpointSlug = parentSlug.apply({ + urlSlug: getLatestEndpointUrlSlug(endpoint) + }); + + const endpointNode: FernNavigation.V1.EndpointNode = { + id: FernNavigation.V1.NodeId(`${this.apiDefinitionId}:${endpointId}`), + type: "endpoint", + method: endpoint.method, + endpointId: FdrAPI.EndpointId(endpointId), + apiDefinitionId: this.apiDefinitionId, + availability: FernNavigation.V1.convertAvailability(endpoint.availability), + isResponseStream: endpoint.responses?.[0]?.body.type === "stream", + title: endpoint.displayName ?? stringifyEndpointPathParts(endpoint.path), + slug: endpointSlug.get(), + icon: undefined, + hidden: undefined, + playground: undefined, + authed: undefined, + viewers: undefined, + orphaned: undefined + }; + + if (endpoint.namespace != null && endpoint.namespace.length > 0) { + const firstNamespacePart = camelCase(endpoint.namespace[0]); + if (firstNamespacePart != null) { + let subpackageCursor = this.#topLevelSubpackages.get(firstNamespacePart); + if (subpackageCursor == null) { + this.taskContext.logger.error( + `Subpackage ${firstNamespacePart} not found in ${this.apiDefinitionId}` + ); + return; + } + let slugGenerator = parentSlug.apply({ urlSlug: subpackageCursor.slug }); + + for (const namespacePart of endpoint.namespace.slice(1)) { + let newSubpackageCursor: FdrAPI.navigation.v1.ApiPackageChild | undefined = + subpackageCursor.children.find( + (child) => + child.type === "apiPackage" && child.id === camelCase(namespacePart.toString()) + ); + slugGenerator = slugGenerator.append(kebabCase(namespacePart)); + if (newSubpackageCursor == null) { + newSubpackageCursor = { + id: FernNavigation.V1.NodeId(`${this.apiDefinitionId}:${namespacePart}`), + type: "apiPackage", + children: [], + title: titleCase(namespacePart), + slug: slugGenerator.get(), + icon: undefined, + hidden: undefined, + overviewPageId: undefined, + availability: undefined, + apiDefinitionId: this.apiDefinitionId, + pointsTo: undefined, + noindex: undefined, + playground: undefined, + authed: undefined, + viewers: undefined, + orphaned: undefined + }; + } + if (newSubpackageCursor != null && newSubpackageCursor.type === "apiPackage") { + subpackageCursor = newSubpackageCursor; + } + } + + subpackageCursor.children.push(endpointNode); + } + } else { + additionalChildren.push(endpointNode); + } + }); + + Object.entries(pkg.websockets).forEach(([webSocketId, webSocket]) => { + if (webSocketId == null) { + throw new Error(`Expected WebSocket ID for ${webSocket.id}. Got undefined.`); + } + if (this.#visitedWebSockets.has(FdrAPI.WebSocketId(webSocketId))) { + return; + } + const webSocketNode: FernNavigation.V1.WebSocketNode = { + id: FernNavigation.V1.NodeId(`${this.apiDefinitionId}:${webSocketId}`), + type: "webSocket", + webSocketId: FdrAPI.WebSocketId(webSocketId), + title: webSocket.displayName ?? stringifyEndpointPathParts(webSocket.path), + slug: parentSlug + .apply({ + urlSlug: getLatestWebSocketUrlSlug(webSocket) + }) + .get(), + icon: undefined, + hidden: undefined, + apiDefinitionId: this.apiDefinitionId, + availability: FernNavigation.V1.convertAvailability(webSocket.availability), + playground: undefined, + authed: undefined, + viewers: undefined, + orphaned: undefined + }; + if (webSocket.namespace != null && webSocket.namespace.length > 0) { + const firstNamespacePart = webSocket.namespace[0]; + if (firstNamespacePart != null) { + let subpackageCursor = this.#topLevelSubpackages.get(firstNamespacePart); + if (subpackageCursor == null) { + throw new Error(`Subpackage ${firstNamespacePart} not found in ${this.apiDefinitionId}`); + } + let slugGenerator = parentSlug.apply({ urlSlug: subpackageCursor.slug }); + + for (const namespacePart of webSocket.namespace.slice(1)) { + let newSubpackageCursor: FdrAPI.navigation.v1.ApiPackageChild | undefined = + subpackageCursor.children.find( + (child) => child.type === "apiPackage" && child.id === namespacePart.toString() + ); + slugGenerator = slugGenerator.append(namespacePart); + if (newSubpackageCursor == null) { + newSubpackageCursor = { + id: FernNavigation.V1.NodeId(`${this.apiDefinitionId}:${namespacePart}`), + type: "apiPackage", + children: [], + title: titleCase(namespacePart), + slug: slugGenerator.get(), + icon: undefined, + hidden: undefined, + overviewPageId: undefined, + availability: undefined, + apiDefinitionId: this.apiDefinitionId, + pointsTo: undefined, + noindex: undefined, + playground: undefined, + authed: undefined, + viewers: undefined, + orphaned: undefined + }; + } + if (newSubpackageCursor != null && newSubpackageCursor.type === "apiPackage") { + subpackageCursor = newSubpackageCursor; + } + } + subpackageCursor.children.push(webSocketNode); + } + } else { + additionalChildren.push(webSocketNode); + } + }); + + Object.entries(pkg.webhooks).forEach(([webhookId, webhook]) => { + if (webhookId == null) { + throw new Error(`Expected Webhook ID for ${webhook.id}. Got undefined.`); + } + if (this.#visitedWebhooks.has(FdrAPI.WebhookId(webhookId))) { + return; + } + const webhookNode: FernNavigation.V1.WebhookNode = { + id: FernNavigation.V1.NodeId(`${this.apiDefinitionId}:${webhookId}`), + type: "webhook", + webhookId: FdrAPI.WebhookId(webhookId), + method: webhook.method, + title: webhook.displayName ?? titleCase(webhook.id), + slug: parentSlug + .apply({ + urlSlug: getLatestWebhookUrlSlug(webhook) + }) + .get(), + icon: undefined, + hidden: undefined, + apiDefinitionId: this.apiDefinitionId, + availability: undefined, + authed: undefined, + viewers: undefined, + orphaned: undefined + }; + + if (webhook.namespace != null && webhook.namespace.length > 0) { + const firstNamespacePart = webhook.namespace[0]; + if (firstNamespacePart != null) { + let subpackageCursor = this.#topLevelSubpackages.get(firstNamespacePart); + if (subpackageCursor == null) { + throw new Error(`Subpackage ${firstNamespacePart} not found in ${this.apiDefinitionId}`); + } + let slugGenerator = parentSlug.apply({ urlSlug: subpackageCursor.slug }); + + for (const namespacePart of webhook.namespace.slice(1)) { + let newSubpackageCursor: FdrAPI.navigation.v1.ApiPackageChild | undefined = + subpackageCursor.children.find( + (child) => child.type === "apiPackage" && child.id === namespacePart.toString() + ); + slugGenerator = slugGenerator.append(namespacePart); + if (newSubpackageCursor == null) { + newSubpackageCursor = { + id: FernNavigation.V1.NodeId(`${this.apiDefinitionId}:${namespacePart}`), + type: "apiPackage", + children: [], + title: namespacePart, + slug: slugGenerator.get(), + icon: undefined, + hidden: undefined, + overviewPageId: undefined, + availability: undefined, + apiDefinitionId: this.apiDefinitionId, + pointsTo: undefined, + noindex: undefined, + playground: undefined, + authed: undefined, + viewers: undefined, + orphaned: undefined + }; + } + if (newSubpackageCursor != null && newSubpackageCursor.type === "apiPackage") { + subpackageCursor = newSubpackageCursor; + } + } + subpackageCursor.children.push(webhookNode); + } + } else { + additionalChildren.push(webhookNode); + } + }); + + additionalChildren = this.mergeEndpointPairs(additionalChildren); + + if (this.apiSection.alphabetized) { + additionalChildren = additionalChildren.sort((a, b) => { + const aTitle = a.type === "endpointPair" ? a.nonStream.title : a.title; + const bTitle = b.type === "endpointPair" ? b.nonStream.title : b.title; + return aTitle.localeCompare(bTitle); + }); + } + + return additionalChildren; + } + + // TODO: optimize this with some DP, where we store incrementally found endpoints (constructing an indexed tree of subpackages) + #resolveSubpackage(subpackageId: string): FdrAPI.api.latest.ApiDefinition { + const endpoints = Object.fromEntries( + Object.entries(this.#api?.endpoints ?? {}).filter( + ([_, endpoint]) => + endpoint.namespace != null && + camelCase(endpoint.namespace[endpoint.namespace.length - 1]) === + FdrAPI.api.v1.SubpackageId(subpackageId) + ) + ); + const websockets = Object.fromEntries( + Object.entries(this.#api?.websockets ?? {}).filter( + ([_, webSocket]) => + webSocket.namespace != null && + camelCase(webSocket.namespace[webSocket.namespace.length - 1]) === + FdrAPI.api.v1.SubpackageId(subpackageId) + ) + ); + const webhooks = Object.fromEntries( + Object.entries(this.#api?.webhooks ?? {}).filter( + ([_, webhook]) => + webhook.namespace != null && + camelCase(webhook.namespace[webhook.namespace.length - 1]) === + FdrAPI.api.v1.SubpackageId(subpackageId) + ) + ); + return { + id: FdrAPI.ApiDefinitionId(subpackageId), + endpoints, + websockets, + webhooks, + types: {}, + subpackages: {}, + auths: {}, + globalHeaders: undefined + }; + } + + #convertApiDefinitionPackageId( + packageId: string | undefined, + parentSlug: FernNavigation.V1.SlugGenerator + ): FernNavigation.V1.ApiPackageChild[] { + const pkg = packageId != null ? this.#resolveSubpackage(packageId) : undefined; + + if (pkg == null) { + this.taskContext.logger.error(`Subpackage ${packageId} not found in ${this.apiDefinitionId}`); + return []; + } + + // if an endpoint, websocket, webhook, or subpackage is not visited, add it to the additional children list + return this.#convertApiDefinitionPackage(pkg, parentSlug); + } + + #convertPlaygroundSettings( + playgroundSettings?: docsYml.RawSchemas.PlaygroundSettings + ): FernNavigation.V1.PlaygroundSettings | undefined { + return convertPlaygroundSettings(playgroundSettings); + } + + private mergeEndpointPairs(children: FernNavigation.V1.ApiPackageChild[]): FernNavigation.V1.ApiPackageChild[] { + return mergeEndpointPairs({ + children, + findEndpointById: (endpointId: FdrAPI.EndpointId) => this.#api?.endpoints[endpointId], + stringifyEndpointPathParts: (endpoint: FdrAPI.api.latest.EndpointDefinition) => + stringifyEndpointPathParts(endpoint.path), + disableEndpointPairs: this.disableEndpointPairs, + apiDefinitionId: this.apiDefinitionId + }); + } +} diff --git a/packages/cli/docs-resolver/src/DocsDefinitionResolver.ts b/packages/cli/docs-resolver/src/DocsDefinitionResolver.ts index fe42e25db44..fb8c5c560f1 100644 --- a/packages/cli/docs-resolver/src/DocsDefinitionResolver.ts +++ b/packages/cli/docs-resolver/src/DocsDefinitionResolver.ts @@ -14,18 +14,21 @@ import { replaceReferencedCode, replaceReferencedMarkdown } from "@fern-api/docs-markdown-utils"; -import { APIV1Write, DocsV1Write, FernNavigation } from "@fern-api/fdr-sdk"; -import { AbsoluteFilePath, RelativeFilePath, listFiles, relative, relativize, resolve } from "@fern-api/fs-utils"; +import { APIV1Write, DocsV1Write, FdrAPI, FernNavigation } from "@fern-api/fdr-sdk"; +import { AbsoluteFilePath, RelativeFilePath, listFiles, relative, resolve } from "@fern-api/fs-utils"; import { generateIntermediateRepresentation } from "@fern-api/ir-generator"; import { IntermediateRepresentation } from "@fern-api/ir-sdk"; +import { OSSWorkspace } from "@fern-api/lazy-fern-workspace"; import { TaskContext } from "@fern-api/task-context"; import { DocsWorkspace, FernWorkspace } from "@fern-api/workspace-loader"; import { ApiReferenceNodeConverter } from "./ApiReferenceNodeConverter"; +import { ApiReferenceNodeConverterLatest } from "./ApiReferenceNodeConverterLatest"; import { ChangelogNodeConverter } from "./ChangelogNodeConverter"; import { NodeIdGenerator } from "./NodeIdGenerator"; import { convertDocsSnippetsConfigToFdr } from "./utils/convertDocsSnippetsConfigToFdr"; import { convertIrToApiDefinition } from "./utils/convertIrToApiDefinition"; +import { generateFdrFromOpenApiWorkspace } from "./utils/generateFdrFromOpenApiWorkspace"; import { collectFilesFromDocsConfig } from "./utils/getImageFilepathsToUpload"; import { wrapWithHttps } from "./wrapWithHttps"; @@ -53,6 +56,8 @@ type RegisterApiFn = (opts: { apiName?: string; }) => AsyncOrSync; +type RegisterApiV2Fn = (opts: { api: FdrAPI.api.latest.ApiDefinition; apiName?: string }) => AsyncOrSync; + const defaultUploadFiles: UploadFilesFn = (files) => { return files.map((file) => ({ ...file, fileId: String(file.relativeFilePath) })); }; @@ -63,16 +68,23 @@ const defaultRegisterApi: RegisterApiFn = async ({ ir }) => { return `${ir.apiName.snakeCase.unsafeName}-${apiCounter}`; }; +const defaultRegisterApiV2: RegisterApiV2Fn = async ({ api }) => { + apiCounter++; + return `${api.id}-${apiCounter}`; +}; + export class DocsDefinitionResolver { constructor( private domain: string, private docsWorkspace: DocsWorkspace, + private ossWorkspaces: OSSWorkspace[], private fernWorkspaces: FernWorkspace[], private taskContext: TaskContext, // Optional private editThisPage?: docsYml.RawSchemas.EditThisPageConfig, private uploadFiles: UploadFilesFn = defaultUploadFiles, - private registerApi: RegisterApiFn = defaultRegisterApi + private registerApi: RegisterApiFn = defaultRegisterApi, + private registerApiV2: RegisterApiV2Fn = defaultRegisterApiV2 ) {} #idgen = NodeIdGenerator.init(); @@ -356,6 +368,18 @@ export class DocsDefinitionResolver { throw new Error("Failed to load API Definition referenced in docs"); } + private getOpenApiWorkspaceForApiSection(apiSection: docsYml.DocsNavigationItem.ApiSection): OSSWorkspace { + if (this.ossWorkspaces.length === 1 && this.ossWorkspaces[0] != null) { + return this.ossWorkspaces[0]; + } else if (apiSection.apiName != null) { + const ossWorkspace = this.ossWorkspaces.find((workspace) => workspace.workspaceName === apiSection.apiName); + if (ossWorkspace != null) { + return ossWorkspace; + } + } + throw new Error("Failed to load API Definition referenced in docs"); + } + private async toRootNode(): Promise { const slug = FernNavigation.V1.SlugGenerator.init(FernNavigation.slugjoin(this.getDocsBasePath())); const id = this.#idgen.get("root"); @@ -552,6 +576,28 @@ export class DocsDefinitionResolver { item: docsYml.DocsNavigationItem.ApiSection, parentSlug: FernNavigation.V1.SlugGenerator ): Promise { + if (this.parsedDocsConfig.experimental?.openapiParserV2) { + const workspace = this.getOpenApiWorkspaceForApiSection(item); + const api = await generateFdrFromOpenApiWorkspace(workspace, this.taskContext); + if (api == null) { + throw new Error("Failed to generate API Definition from OpenAPI workspace"); + } + await this.registerApiV2({ + api, + apiName: item.apiName + }); + const node = new ApiReferenceNodeConverterLatest( + item, + api, + parentSlug, + workspace, + this.docsWorkspace, + this.taskContext, + this.markdownFilesToFullSlugs, + this.#idgen + ); + return node.get(); + } const workspace = this.getFernWorkspaceForApiSection(item); const snippetsConfig = convertDocsSnippetsConfigToFdr(item.snippetsConfiguration); const ir = generateIntermediateRepresentation({ diff --git a/packages/cli/docs-resolver/src/__test__/dry.test.ts b/packages/cli/docs-resolver/src/__test__/dry.test.ts index fa11fd86b8b..e6a50ed699c 100644 --- a/packages/cli/docs-resolver/src/__test__/dry.test.ts +++ b/packages/cli/docs-resolver/src/__test__/dry.test.ts @@ -23,9 +23,11 @@ it.skip("converts to api reference node", async () => { "domain", docsWorkspace, [], + [], context, undefined, async (_files) => [], + async (_opts) => "", async (_opts) => "" ); diff --git a/packages/cli/docs-resolver/src/__test__/fixtures/openapi-latest/fern/docs.yml b/packages/cli/docs-resolver/src/__test__/fixtures/openapi-latest/fern/docs.yml new file mode 100644 index 00000000000..2d51970a5ac --- /dev/null +++ b/packages/cli/docs-resolver/src/__test__/fixtures/openapi-latest/fern/docs.yml @@ -0,0 +1,9 @@ +instances: + - url: https://fern-platform-test.docs.dev.buildwithfern.com + - url: https://fern-platform-test.docs.buildwithfern.com +title: SmokeTest | Documentation +navigation: + - api: API Reference +colors: + accentPrimary: '#ffffff' + background: '#000000' diff --git a/packages/cli/docs-resolver/src/__test__/fixtures/openapi-latest/fern/fern.config.json b/packages/cli/docs-resolver/src/__test__/fixtures/openapi-latest/fern/fern.config.json new file mode 100644 index 00000000000..7980537f564 --- /dev/null +++ b/packages/cli/docs-resolver/src/__test__/fixtures/openapi-latest/fern/fern.config.json @@ -0,0 +1,4 @@ +{ + "organization": "fern", + "version": "*" +} \ No newline at end of file diff --git a/packages/cli/docs-resolver/src/__test__/fixtures/openapi-latest/fern/generators.yml b/packages/cli/docs-resolver/src/__test__/fixtures/openapi-latest/fern/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/packages/cli/docs-resolver/src/__test__/fixtures/openapi-latest/fern/generators.yml @@ -0,0 +1 @@ +{} diff --git a/packages/cli/docs-resolver/src/__test__/fixtures/openapi-latest/fern/openapi.yml b/packages/cli/docs-resolver/src/__test__/fixtures/openapi-latest/fern/openapi.yml new file mode 100644 index 00000000000..79d4a073543 --- /dev/null +++ b/packages/cli/docs-resolver/src/__test__/fixtures/openapi-latest/fern/openapi.yml @@ -0,0 +1,332 @@ +openapi: 3.1.0 +info: + title: Swagger Petstore - OpenAPI 3.1 + description: |- + This is a sample Pet Store Server based on the OpenAPI 3.1 specification. + You can find out more about + Swagger at [http://swagger.io](http://swagger.io). + summary: Pet Store 3.1 + version: 1.0.0 +servers: + - url: /api/v31 +tags: + - name: pet + description: Everything about your Pets + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user +paths: + /pet: + put: + tags: + - pet + summary: Update an existing pet + description: Update an existing pet by Id + operationId: updatePet + requestBody: + description: Pet object that needs to be updated in the store + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: A Pet in JSON Format + required: + - id + writeOnly: true + application/xml: + schema: + $ref: "#/components/schemas/Pet" + description: A Pet in XML Format + required: + - id + writeOnly: true + required: true + responses: + "200": + description: Successful operation + content: + application/xml: + schema: + $ref: "#/components/schemas/Pet" + description: A Pet in XML Format + readOnly: true + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: A Pet in JSON Format + readOnly: true + "400": + description: Invalid ID supplied + "404": + description: Pet not found + "405": + description: Validation exception + security: + - petstore_auth: + - "write:pets" + - "read:pets" + post: + tags: + - pet + summary: Add a new pet to the store + description: Add a new pet to the store + operationId: addPet + requestBody: + description: Create a new pet in the store + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: A Pet in JSON Format + required: + - id + writeOnly: true + application/xml: + schema: + $ref: "#/components/schemas/Pet" + description: A Pet in XML Format + required: + - id + writeOnly: true + required: true + responses: + "200": + description: Successful operation + content: + application/xml: + schema: + $ref: "#/components/schemas/Pet" + description: A Pet in XML Format + readOnly: true + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: A Pet in JSON Format + readOnly: true + "405": + description: Invalid input + security: + - petstore_auth: + - "write:pets" + - "read:pets" + "/pet/{petId}": + get: + tags: + - pets + summary: Find pet by ID + description: >- + Returns a pet when 0 < ID <= 10. ID > 10 or nonintegers will simulate + API error conditions + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet that needs to be fetched + required: true + schema: + type: integer + format: int64 + description: param ID of pet that needs to be fetched + exclusiveMaximum: 10 + exclusiveMinimum: 1 + responses: + "400": + description: Invalid ID supplied + "404": + description: Pet not found + default: + description: The pet + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: A Pet in JSON format + application/xml: + schema: + $ref: "#/components/schemas/Pet" + description: A Pet in XML format + security: + - petstore_auth: + - "write:pets" + - "read:pets" + - api_key: [] +components: + schemas: + Order: + x-swagger-router-model: io.swagger.petstore.model.Order + properties: + id: + type: integer + format: int64 + example: 10 + petId: + type: integer + format: int64 + example: 198772 + quantity: + type: integer + format: int32 + example: 7 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + example: approved + complete: + type: boolean + xml: + name: order + type: object + Customer: + properties: + id: + type: integer + format: int64 + example: 100000 + username: + type: string + example: fehguy + address: + type: array + items: + $ref: "#/components/schemas/Address" + xml: + wrapped: true + name: addresses + xml: + name: customer + type: object + Address: + properties: + street: + type: string + example: 437 Lytton + city: + type: string + example: Palo Alto + state: + type: string + example: CA + zip: + type: string + example: 94301 + xml: + name: address + type: object + Category: + x-swagger-router-model: io.swagger.petstore.model.Category + properties: + id: + type: integer + format: int64 + example: 1 + name: + type: string + example: Dogs + xml: + name: category + type: object + User: + x-swagger-router-model: io.swagger.petstore.model.User + properties: + id: + type: integer + format: int64 + example: 10 + username: + type: string + example: theUser + firstName: + type: string + example: John + lastName: + type: string + example: James + email: + type: string + example: john@email.com + password: + type: string + example: 12345 + phone: + type: string + example: 12345 + userStatus: + type: integer + format: int32 + example: 1 + description: User Status + xml: + name: user + type: object + Tag: + x-swagger-router-model: io.swagger.petstore.model.Tag + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: tag + type: object + Pet: + x-swagger-router-model: io.swagger.petstore.model.Pet + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + example: 10 + name: + type: string + example: doggie + category: + $ref: "#/components/schemas/Category" + photoUrls: + type: array + xml: + wrapped: true + items: + type: string + xml: + name: photoUrl + tags: + type: array + xml: + wrapped: true + items: + $ref: "#/components/schemas/Tag" + xml: + name: tag + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: pet + type: object + ApiResponse: + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + xml: + name: "##default" + type: object diff --git a/packages/cli/docs-resolver/src/__test__/openapi-latest.test.ts b/packages/cli/docs-resolver/src/__test__/openapi-latest.test.ts new file mode 100644 index 00000000000..746910f1638 --- /dev/null +++ b/packages/cli/docs-resolver/src/__test__/openapi-latest.test.ts @@ -0,0 +1,79 @@ +import { parseDocsConfiguration } from "@fern-api/configuration-loader"; +import { FernNavigation } from "@fern-api/fdr-sdk"; +import { AbsoluteFilePath, resolve } from "@fern-api/fs-utils"; +import { OSSWorkspace } from "@fern-api/lazy-fern-workspace"; +import { createMockTaskContext } from "@fern-api/task-context"; +import { loadAPIWorkspace, loadDocsWorkspace } from "@fern-api/workspace-loader"; + +import { ApiReferenceNodeConverterLatest } from "../ApiReferenceNodeConverterLatest"; +import { NodeIdGenerator } from "../NodeIdGenerator"; +import { generateFdrFromOpenApiWorkspace } from "../utils/generateFdrFromOpenApiWorkspace"; + +const context = createMockTaskContext(); + +// eslint-disable-next-line jest/no-disabled-tests +it.skip("converts to api reference latest node", async () => { + const docsWorkspace = await loadDocsWorkspace({ + fernDirectory: resolve(AbsoluteFilePath.of(__dirname), "fixtures/openapi-latest/fern"), + context + }); + + if (docsWorkspace == null) { + throw new Error("Workspace is null"); + } + + const parsedDocsConfig = await parseDocsConfiguration({ + rawDocsConfiguration: docsWorkspace.config, + context, + absolutePathToFernFolder: docsWorkspace.absoluteFilePath, + absoluteFilepathToDocsConfig: docsWorkspace.absoluteFilepathToDocsConfig + }); + + if (parsedDocsConfig.navigation.type !== "untabbed") { + throw new Error("Expected untabbed navigation"); + } + + if (parsedDocsConfig.navigation.items[0]?.type !== "apiSection") { + throw new Error("Expected apiSection"); + } + + const apiSection = parsedDocsConfig.navigation.items[0]; + + const result = await loadAPIWorkspace({ + absolutePathToWorkspace: resolve(AbsoluteFilePath.of(__dirname), "fixtures/openapi-latest/fern"), + context, + cliVersion: "0.0.0", + workspaceName: undefined + }); + + if (!result.didSucceed) { + throw new Error("API workspace failed to load"); + } + + const apiWorkspace = result.workspace; + + if (!(apiWorkspace instanceof OSSWorkspace)) { + throw new Error("Expected oss workspace"); + } + + const slug = FernNavigation.V1.SlugGenerator.init("/base/path"); + + const api = await generateFdrFromOpenApiWorkspace(apiWorkspace, context); + + if (api == null) { + throw new Error("API is null"); + } + + const node = new ApiReferenceNodeConverterLatest( + apiSection, + api, + slug, + apiWorkspace, + docsWorkspace, + context, + new Map(), + NodeIdGenerator.init() + ).get(); + + expect(node).toMatchSnapshot(); +}); diff --git a/packages/cli/docs-resolver/src/index.ts b/packages/cli/docs-resolver/src/index.ts index a6692d7a13e..cc615757fb1 100644 --- a/packages/cli/docs-resolver/src/index.ts +++ b/packages/cli/docs-resolver/src/index.ts @@ -1,2 +1,3 @@ export { DocsDefinitionResolver, type UploadedFile } from "./DocsDefinitionResolver"; export { wrapWithHttps } from "./wrapWithHttps"; +export { generateFdrFromOpenApiWorkspace } from "./utils/generateFdrFromOpenApiWorkspace"; diff --git a/packages/cli/docs-resolver/src/utils/convertIrToApiDefinition.ts b/packages/cli/docs-resolver/src/utils/convertIrToApiDefinition.ts index a0e02a633eb..0ca7cec8471 100644 --- a/packages/cli/docs-resolver/src/utils/convertIrToApiDefinition.ts +++ b/packages/cli/docs-resolver/src/utils/convertIrToApiDefinition.ts @@ -1,10 +1,4 @@ -import { - APIV1Read, - DocsV1Read, - SDKSnippetHolder, - convertAPIDefinitionToDb, - convertDbAPIDefinitionToRead -} from "@fern-api/fdr-sdk"; +import { APIV1Read, SDKSnippetHolder, convertAPIDefinitionToDb, convertDbAPIDefinitionToRead } from "@fern-api/fdr-sdk"; import { IntermediateRepresentation } from "@fern-api/ir-sdk"; import { convertIrToFdrApi } from "@fern-api/register"; diff --git a/packages/cli/docs-resolver/src/utils/convertPlaygroundSettings.ts b/packages/cli/docs-resolver/src/utils/convertPlaygroundSettings.ts new file mode 100644 index 00000000000..c5a5acf56e1 --- /dev/null +++ b/packages/cli/docs-resolver/src/utils/convertPlaygroundSettings.ts @@ -0,0 +1,27 @@ +import { docsYml } from "@fern-api/configuration-loader"; +import { FernNavigation } from "@fern-api/fdr-sdk"; + +export function convertPlaygroundSettings( + playgroundSettings?: docsYml.RawSchemas.PlaygroundSettings +): FernNavigation.V1.PlaygroundSettings | undefined { + if (playgroundSettings) { + return { + environments: + playgroundSettings.environments != null && playgroundSettings.environments.length > 0 + ? playgroundSettings.environments.map((environmentId) => + FernNavigation.V1.EnvironmentId(environmentId) + ) + : undefined, + button: + playgroundSettings.button != null && playgroundSettings.button.href + ? { href: FernNavigation.V1.Url(playgroundSettings.button.href) } + : undefined, + "limit-websocket-messages-per-connection": + playgroundSettings.limitWebsocketMessagesPerConnection != null + ? playgroundSettings.limitWebsocketMessagesPerConnection + : undefined + }; + } + + return; +} diff --git a/packages/cli/docs-resolver/src/utils/enrichApiPackageChild.ts b/packages/cli/docs-resolver/src/utils/enrichApiPackageChild.ts new file mode 100644 index 00000000000..9f2ad6cd772 --- /dev/null +++ b/packages/cli/docs-resolver/src/utils/enrichApiPackageChild.ts @@ -0,0 +1,48 @@ +import { FernNavigation } from "@fern-api/fdr-sdk"; + +export function enrichApiPackageChild({ + child, + nodeIdToSubpackageId, + convertApiDefinitionPackageId, + mergeAndFilterChildren +}: { + child: FernNavigation.V1.ApiPackageChild; + nodeIdToSubpackageId: Map; + convertApiDefinitionPackageId: ( + subpackageId: string, + slug: FernNavigation.V1.SlugGenerator + ) => FernNavigation.V1.ApiPackageChild[]; + mergeAndFilterChildren: ( + children: FernNavigation.V1.ApiPackageChild[], + subpackageChildren: FernNavigation.V1.ApiPackageChild[] + ) => FernNavigation.V1.ApiPackageChild[]; +}): FernNavigation.V1.ApiPackageChild { + if (child.type === "apiPackage") { + // expand the subpackage to include children that haven't been visited yet + const slug = FernNavigation.V1.SlugGenerator.init(child.slug); + const subpackageIds = nodeIdToSubpackageId.get(child.id) ?? []; + const subpackageChildren = subpackageIds.flatMap((subpackageId) => + convertApiDefinitionPackageId(subpackageId, slug) + ); + + // recursively apply enrichment to children + const enrichedChildren = child.children.map((innerChild) => + enrichApiPackageChild({ + child: innerChild, + nodeIdToSubpackageId, + convertApiDefinitionPackageId, + mergeAndFilterChildren + }) + ); + + // combine children with subpackage (tacked on at the end to preserve order) + const children = mergeAndFilterChildren(enrichedChildren, subpackageChildren); + + return { + ...child, + children, + pointsTo: undefined + }; + } + return child; +} diff --git a/packages/cli/docs-resolver/src/utils/generateFdrFromOpenApiWorkspace.ts b/packages/cli/docs-resolver/src/utils/generateFdrFromOpenApiWorkspace.ts new file mode 100644 index 00000000000..ba186b30b4a --- /dev/null +++ b/packages/cli/docs-resolver/src/utils/generateFdrFromOpenApiWorkspace.ts @@ -0,0 +1,53 @@ +import { merge } from "lodash-es"; +import { OpenAPIV3_1 } from "openapi-types"; + +import { AbstractAPIWorkspace } from "@fern-api/api-workspace-commons"; +import { + BaseOpenApiV3_1ConverterNodeContext, + ErrorCollector, + OpenApiDocumentConverterNode +} from "@fern-api/docs-parsers"; +import { LazyFernWorkspace, OSSWorkspace, OpenAPILoader, getAllOpenAPISpecs } from "@fern-api/lazy-fern-workspace"; +import { TaskContext } from "@fern-api/task-context"; + +export async function generateFdrFromOpenApiWorkspace( + workspace: AbstractAPIWorkspace, + context: TaskContext +): Promise> { + if (workspace instanceof LazyFernWorkspace) { + context.logger.info("Skipping, API is specified as a Fern Definition."); + return; + } else if (!(workspace instanceof OSSWorkspace)) { + return; + } + + const openApiLoader = new OpenAPILoader(workspace.absoluteFilePath); + const openApiSpecs = await getAllOpenAPISpecs({ context, specs: workspace.specs }); + + const openApiDocuments = await openApiLoader.loadDocuments({ context, specs: openApiSpecs }); + + // // eslint-disable-next-line @typescript-eslint/no-explicit-any + let fdrApiDefinition: ReturnType; + for (const openApi of openApiDocuments) { + if (openApi.type !== "openapi") { + continue; + } + + const oasContext: BaseOpenApiV3_1ConverterNodeContext = { + document: openApi.value as OpenAPIV3_1.Document, + logger: context.logger, + errors: new ErrorCollector() + }; + + const openApiFdrJson = new OpenApiDocumentConverterNode({ + input: openApi.value as OpenAPIV3_1.Document, + context: oasContext, + accessPath: [], + pathId: workspace.workspaceName ?? "openapi parser" + }); + + fdrApiDefinition = merge(fdrApiDefinition, openApiFdrJson.convert()); + } + + return fdrApiDefinition; +} diff --git a/packages/cli/docs-resolver/src/utils/mergeAndFilterChildren.ts b/packages/cli/docs-resolver/src/utils/mergeAndFilterChildren.ts new file mode 100644 index 00000000000..0b018753808 --- /dev/null +++ b/packages/cli/docs-resolver/src/utils/mergeAndFilterChildren.ts @@ -0,0 +1,27 @@ +import { FernNavigation } from "@fern-api/fdr-sdk"; + +import { mergeEndpointPairs } from "./mergeEndpointPairs"; + +export function mergeAndFilterChildren({ + left, + right, + findEndpointById, + stringifyEndpointPathParts, + disableEndpointPairs, + apiDefinitionId +}: { + left: FernNavigation.V1.ApiPackageChild[]; + right: FernNavigation.V1.ApiPackageChild[]; + findEndpointById: (endpointId: FernNavigation.EndpointId) => EndpointType | undefined; + stringifyEndpointPathParts: (endpoint: EndpointType) => string; + disableEndpointPairs: boolean; + apiDefinitionId: FernNavigation.V1.ApiDefinitionId; +}): FernNavigation.V1.ApiPackageChild[] { + return mergeEndpointPairs({ + children: [...left, ...right], + findEndpointById, + stringifyEndpointPathParts, + disableEndpointPairs, + apiDefinitionId + }).filter((child) => (child.type === "apiPackage" ? child.children.length > 0 : true)); +} diff --git a/packages/cli/docs-resolver/src/utils/mergeEndpointPairs.ts b/packages/cli/docs-resolver/src/utils/mergeEndpointPairs.ts new file mode 100644 index 00000000000..f0ba4617a3c --- /dev/null +++ b/packages/cli/docs-resolver/src/utils/mergeEndpointPairs.ts @@ -0,0 +1,58 @@ +import { FernNavigation } from "@fern-api/fdr-sdk"; + +export function mergeEndpointPairs({ + children, + findEndpointById, + stringifyEndpointPathParts, + disableEndpointPairs, + apiDefinitionId +}: { + children: FernNavigation.V1.ApiPackageChild[]; + findEndpointById: (endpointId: FernNavigation.EndpointId) => EndpointType | undefined; + stringifyEndpointPathParts: (endpoint: EndpointType) => string; + disableEndpointPairs: boolean; + apiDefinitionId: FernNavigation.V1.ApiDefinitionId; +}): FernNavigation.V1.ApiPackageChild[] { + if (disableEndpointPairs) { + return children; + } + + const toRet: FernNavigation.V1.ApiPackageChild[] = []; + + const methodAndPathToEndpointNode = new Map(); + children.forEach((child) => { + if (child.type !== "endpoint") { + toRet.push(child); + return; + } + + const endpoint = findEndpointById(child.endpointId); + if (endpoint == null) { + throw new Error(`Endpoint ${child.endpointId} not found`); + } + + const methodAndPath = `${endpoint.method} ${stringifyEndpointPathParts(endpoint)}`; + + const existing = methodAndPathToEndpointNode.get(methodAndPath); + methodAndPathToEndpointNode.set(methodAndPath, child); + + if (existing == null || existing.isResponseStream === child.isResponseStream) { + toRet.push(child); + return; + } + + const idx = toRet.indexOf(existing); + const stream = child.isResponseStream ? child : existing; + const nonStream = child.isResponseStream ? existing : child; + const pairNode: FernNavigation.V1.EndpointPairNode = { + id: FernNavigation.V1.NodeId(`${apiDefinitionId}:${nonStream.endpointId}+${stream.endpointId}`), + type: "endpointPair", + stream, + nonStream + }; + + toRet[idx] = pairNode; + }); + + return toRet; +} diff --git a/packages/cli/docs-resolver/src/utils/toPageNode.ts b/packages/cli/docs-resolver/src/utils/toPageNode.ts new file mode 100644 index 00000000000..612aeced7a7 --- /dev/null +++ b/packages/cli/docs-resolver/src/utils/toPageNode.ts @@ -0,0 +1,42 @@ +import { kebabCase } from "lodash-es"; + +import { docsYml } from "@fern-api/configuration-loader"; +import { FernNavigation } from "@fern-api/fdr-sdk"; +import { AbsoluteFilePath } from "@fern-api/fs-utils"; +import { DocsWorkspace } from "@fern-api/workspace-loader"; + +import { NodeIdGenerator } from "../NodeIdGenerator"; +import { toRelativeFilepath } from "./toRelativeFilepath"; + +export function toPageNode({ + docsWorkspace, + page, + parentSlug, + idgen, + markdownFilesToFullSlugs +}: { + docsWorkspace: DocsWorkspace; + page: docsYml.DocsNavigationItem.Page; + parentSlug: FernNavigation.V1.SlugGenerator; + idgen: NodeIdGenerator; + markdownFilesToFullSlugs: Map; +}): FernNavigation.V1.PageNode { + const pageId = FernNavigation.V1.PageId(toRelativeFilepath(docsWorkspace, page.absolutePath)); + const pageSlug = parentSlug.apply({ + fullSlug: markdownFilesToFullSlugs.get(page.absolutePath)?.split("/"), + urlSlug: page.slug ?? kebabCase(page.title) + }); + return { + id: idgen.get(pageId), + type: "page", + pageId, + title: page.title, + slug: pageSlug.get(), + icon: page.icon, + hidden: page.hidden, + noindex: page.noindex, + authed: undefined, + viewers: page.viewers, + orphaned: page.orphaned + }; +} diff --git a/packages/cli/docs-resolver/src/utils/toRelativeFilepath.ts b/packages/cli/docs-resolver/src/utils/toRelativeFilepath.ts new file mode 100644 index 00000000000..136d21006f1 --- /dev/null +++ b/packages/cli/docs-resolver/src/utils/toRelativeFilepath.ts @@ -0,0 +1,17 @@ +import { AbsoluteFilePath, RelativeFilePath, relative } from "@fern-api/fs-utils"; +import { DocsWorkspace } from "@fern-api/workspace-loader"; + +export function toRelativeFilepath(docsWorkspace: DocsWorkspace, filepath: AbsoluteFilePath): RelativeFilePath; +export function toRelativeFilepath( + docsWorkspace: DocsWorkspace, + filepath: AbsoluteFilePath | undefined +): RelativeFilePath | undefined; +export function toRelativeFilepath( + docsWorkspace: DocsWorkspace, + filepath: AbsoluteFilePath | undefined +): RelativeFilePath | undefined { + if (filepath == null) { + return undefined; + } + return relative(docsWorkspace.absoluteFilePath, filepath); +} diff --git a/packages/cli/ete-tests/package.json b/packages/cli/ete-tests/package.json index cd0eb9b1256..7a433544e9d 100644 --- a/packages/cli/ete-tests/package.json +++ b/packages/cli/ete-tests/package.json @@ -28,7 +28,7 @@ }, "dependencies": { "@fern-api/configuration": "workspace:*", - "@fern-fern/fdr-cjs-sdk": "0.126.1-444264056", + "@fern-fern/fdr-cjs-sdk": "0.127.4-331678a74", "@fern-api/fs-utils": "workspace:*", "@fern-api/logging-execa": "workspace:*", "@fern-typescript/fetcher": "workspace:*", diff --git a/packages/cli/generation/remote-generation/remote-workspace-runner/package.json b/packages/cli/generation/remote-generation/remote-workspace-runner/package.json index 9f4a9dc31f6..d6e64039bc2 100644 --- a/packages/cli/generation/remote-generation/remote-workspace-runner/package.json +++ b/packages/cli/generation/remote-generation/remote-workspace-runner/package.json @@ -34,7 +34,7 @@ "@fern-api/core-utils": "workspace:*", "@fern-api/docs-resolver": "workspace:*", "@fern-api/logging-execa": "workspace:*", - "@fern-fern/fdr-cjs-sdk": "0.126.1-444264056", + "@fern-fern/fdr-cjs-sdk": "0.127.4-331678a74", "@fern-api/fs-utils": "workspace:*", "@fern-api/ir-generator": "workspace:*", "@fern-api/ir-migrations": "workspace:*", diff --git a/packages/cli/generation/remote-generation/remote-workspace-runner/src/publishDocs.ts b/packages/cli/generation/remote-generation/remote-workspace-runner/src/publishDocs.ts index 7fa027ea06f..d0941a96b7b 100644 --- a/packages/cli/generation/remote-generation/remote-workspace-runner/src/publishDocs.ts +++ b/packages/cli/generation/remote-generation/remote-workspace-runner/src/publishDocs.ts @@ -18,6 +18,7 @@ import { DocsWorkspace, FernWorkspace } from "@fern-api/workspace-loader"; import { FernRegistry as CjsFdrSdk } from "@fern-fern/fdr-cjs-sdk"; +import { OSSWorkspace } from "../../../../workspace/lazy-fern-workspace/src"; import { measureImageSizes } from "./measureImageSizes"; const MEASURE_IMAGE_BATCH_SIZE = 10; @@ -36,6 +37,7 @@ export async function publishDocs({ domain, customDomains, fernWorkspaces, + ossWorkspaces, context, preview, editThisPage, @@ -47,6 +49,7 @@ export async function publishDocs({ domain: string; customDomains: string[]; fernWorkspaces: FernWorkspace[]; + ossWorkspaces: OSSWorkspace[]; context: TaskContext; preview: boolean; editThisPage: docsYml.RawSchemas.FernDocsConfig.EditThisPageConfig | undefined; @@ -63,6 +66,7 @@ export async function publishDocs({ const resolver = new DocsDefinitionResolver( domain, docsWorkspace, + ossWorkspaces, fernWorkspaces, context, editThisPage, @@ -161,7 +165,8 @@ export async function publishDocs({ const response = await fdr.api.v1.register.registerApiDefinition({ orgId: CjsFdrSdk.OrgId(organization), apiId: CjsFdrSdk.ApiId(ir.apiName.originalName), - definition: apiDefinition + definition: apiDefinition, + definitionV2: undefined }); if (response.ok) { diff --git a/packages/cli/generation/remote-generation/remote-workspace-runner/src/runRemoteGenerationForDocsWorkspace.ts b/packages/cli/generation/remote-generation/remote-workspace-runner/src/runRemoteGenerationForDocsWorkspace.ts index 373640c4777..1e409d22867 100644 --- a/packages/cli/generation/remote-generation/remote-workspace-runner/src/runRemoteGenerationForDocsWorkspace.ts +++ b/packages/cli/generation/remote-generation/remote-workspace-runner/src/runRemoteGenerationForDocsWorkspace.ts @@ -3,11 +3,13 @@ import { replaceEnvVariables } from "@fern-api/core-utils"; import { TaskContext } from "@fern-api/task-context"; import { DocsWorkspace, FernWorkspace } from "@fern-api/workspace-loader"; +import { OSSWorkspace } from "../../../../workspace/lazy-fern-workspace/src"; import { publishDocs } from "./publishDocs"; export async function runRemoteGenerationForDocsWorkspace({ organization, fernWorkspaces, + ossWorkspaces, docsWorkspace, context, token, @@ -16,6 +18,7 @@ export async function runRemoteGenerationForDocsWorkspace({ }: { organization: string; fernWorkspaces: FernWorkspace[]; + ossWorkspaces: OSSWorkspace[]; docsWorkspace: DocsWorkspace; context: TaskContext; token: FernToken; @@ -77,6 +80,7 @@ export async function runRemoteGenerationForDocsWorkspace({ organization, context, fernWorkspaces, + ossWorkspaces, preview, editThisPage: maybeInstance.editThisPage, isPrivate: maybeInstance.private diff --git a/packages/cli/register/package.json b/packages/cli/register/package.json index c9d55e8ed25..0d8b2a25d7d 100644 --- a/packages/cli/register/package.json +++ b/packages/cli/register/package.json @@ -32,7 +32,7 @@ "@fern-api/configuration": "workspace:*", "@fern-api/core": "workspace:*", "@fern-api/core-utils": "workspace:*", - "@fern-fern/fdr-cjs-sdk": "0.126.1-444264056", + "@fern-fern/fdr-cjs-sdk": "0.127.4-331678a74", "@fern-api/fs-utils": "workspace:*", "@fern-api/ir-generator": "workspace:*", "@fern-api/ir-sdk": "workspace:*", diff --git a/packages/core/package.json b/packages/core/package.json index 31fcb7a6963..fa34aa3846f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -27,7 +27,7 @@ "depcheck": "depcheck" }, "dependencies": { - "@fern-fern/fdr-cjs-sdk": "0.126.1-444264056", + "@fern-fern/fdr-cjs-sdk": "0.127.4-331678a74", "@fern-fern/generators-sdk": "0.114.0-5745f9e74", "@fern-api/venus-api-sdk": "0.10.2", "@fern-fern/fdr-test-sdk": "^0.0.5297", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6dda0823d75..4f21a924071 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3944,9 +3944,6 @@ importers: '@fern-api/core-utils': specifier: workspace:* version: link:../../commons/core-utils - '@fern-api/docs-parsers': - specifier: ^0.0.12 - version: 0.0.12(@types/node@18.15.3)(jsdom@20.0.3)(sass@1.72.0)(terser@5.31.5)(typescript@5.7.2) '@fern-api/docs-preview': specifier: workspace:* version: link:../docs-preview @@ -4130,9 +4127,6 @@ importers: lodash-es: specifier: ^4.17.21 version: 4.17.21 - openapi-types: - specifier: ^12.1.3 - version: 12.1.3 ora: specifier: ^7.0.1 version: 7.0.1 @@ -4329,8 +4323,8 @@ importers: specifier: workspace:* version: link:../../commons/path-utils '@fern-fern/fdr-cjs-sdk': - specifier: 0.126.1-444264056 - version: 0.126.1-444264056 + specifier: 0.127.4-331678a74 + version: 0.127.4-331678a74 '@fern-fern/fiddle-sdk': specifier: 0.0.584 version: 0.0.584 @@ -4378,8 +4372,8 @@ importers: specifier: workspace:* version: link:../task-context '@fern-fern/fdr-cjs-sdk': - specifier: 0.126.1-444264056 - version: 0.126.1-444264056 + specifier: 0.127.4-331678a74 + version: 0.127.4-331678a74 '@fern-fern/fiddle-sdk': specifier: 0.0.584 version: 0.0.584 @@ -4457,8 +4451,8 @@ importers: specifier: workspace:* version: link:../../task-context '@fern-fern/fdr-cjs-sdk': - specifier: 0.126.1-444264056 - version: 0.126.1-444264056 + specifier: 0.127.4-331678a74 + version: 0.127.4-331678a74 js-yaml: specifier: ^4.1.0 version: 4.1.0 @@ -4509,8 +4503,8 @@ importers: specifier: workspace:* version: link:../../task-context '@fern-fern/fdr-cjs-sdk': - specifier: 0.126.1-444264056 - version: 0.126.1-444264056 + specifier: 0.127.4-331678a74 + version: 0.127.4-331678a74 gray-matter: specifier: ^4.0.3 version: 4.0.3 @@ -4546,8 +4540,8 @@ importers: specifier: workspace:* version: link:../task-context '@fern-fern/fdr-cjs-sdk': - specifier: 0.126.1-444264056 - version: 0.126.1-444264056 + specifier: 0.127.4-331678a74 + version: 0.127.4-331678a74 gray-matter: specifier: ^4.0.3 version: 4.0.3 @@ -4597,18 +4591,24 @@ importers: packages/cli/docs-preview: dependencies: + '@fern-api/core-utils': + specifier: workspace:* + version: link:../../commons/core-utils '@fern-api/docs-resolver': specifier: workspace:* version: link:../docs-resolver '@fern-api/fdr-sdk': - specifier: 0.126.1-444264056 - version: 0.126.1-444264056(typescript@5.7.2) + specifier: 0.127.4-331678a74 + version: 0.127.4-331678a74(typescript@5.7.2) '@fern-api/fs-utils': specifier: workspace:* version: link:../../commons/fs-utils '@fern-api/ir-sdk': specifier: workspace:* version: link:../../ir-sdk + '@fern-api/lazy-fern-workspace': + specifier: workspace:* + version: link:../workspace/lazy-fern-workspace '@fern-api/logger': specifier: workspace:* version: link:../logger @@ -4700,6 +4700,9 @@ importers: packages/cli/docs-resolver: dependencies: + '@fern-api/api-workspace-commons': + specifier: workspace:* + version: link:../workspace/commons '@fern-api/cli-source-resolver': specifier: workspace:* version: link:../cli-source-resolver @@ -4712,9 +4715,12 @@ importers: '@fern-api/docs-markdown-utils': specifier: workspace:* version: link:../docs-markdown-utils + '@fern-api/docs-parsers': + specifier: ^0.0.20 + version: 0.0.20(@types/node@18.7.18)(jsdom@20.0.3)(sass@1.72.0)(terser@5.31.5)(typescript@4.6.4) '@fern-api/fdr-sdk': - specifier: 0.126.1-444264056 - version: 0.126.1-444264056(typescript@5.7.2) + specifier: 0.127.4-331678a74 + version: 0.127.4-331678a74(typescript@4.6.4) '@fern-api/fs-utils': specifier: workspace:* version: link:../../commons/fs-utils @@ -4724,6 +4730,9 @@ importers: '@fern-api/ir-sdk': specifier: workspace:* version: link:../../ir-sdk + '@fern-api/lazy-fern-workspace': + specifier: workspace:* + version: link:../workspace/lazy-fern-workspace '@fern-api/register': specifier: workspace:* version: link:../register @@ -4731,8 +4740,8 @@ importers: specifier: workspace:* version: link:../task-context '@fern-api/ui-core-utils': - specifier: 0.126.1-444264056 - version: 0.126.1-444264056 + specifier: 0.127.4-331678a74 + version: 0.127.4-331678a74 dayjs: specifier: ^1.11.11 version: 1.11.11 @@ -4749,30 +4758,30 @@ importers: '@fern-api/workspace-loader': specifier: workspace:* version: link:../workspace/loader - '@trivago/prettier-plugin-sort-imports': - specifier: ^5.2.1 - version: 5.2.1(@vue/compiler-sfc@3.5.13)(prettier@3.4.2) '@types/lodash-es': specifier: ^4.17.12 version: 4.17.12 '@types/node': - specifier: 18.15.3 - version: 18.15.3 + specifier: 18.7.18 + version: 18.7.18 depcheck: - specifier: ^1.4.7 + specifier: ^1.4.6 version: 1.4.7 eslint: - specifier: ^9.16.0 - version: 9.16.0(jiti@1.18.2) + specifier: ^8.56.0 + version: 8.56.0 + openapi-types: + specifier: ^10.0.0 + version: 10.0.0 prettier: - specifier: ^3.4.2 - version: 3.4.2 + specifier: ^2.7.1 + version: 2.8.8 typescript: - specifier: 5.7.2 - version: 5.7.2 + specifier: 4.6.4 + version: 4.6.4 vitest: specifier: ^2.0.5 - version: 2.0.5(@types/node@18.15.3)(jsdom@20.0.3)(sass@1.72.0)(terser@5.31.5) + version: 2.0.5(@types/node@18.7.18)(jsdom@20.0.3)(sass@1.72.0)(terser@5.31.5) packages/cli/ete-tests: dependencies: @@ -4786,8 +4795,8 @@ importers: specifier: workspace:* version: link:../../commons/logging-execa '@fern-fern/fdr-cjs-sdk': - specifier: 0.126.1-444264056 - version: 0.126.1-444264056 + specifier: 0.127.4-331678a74 + version: 0.127.4-331678a74 '@fern-typescript/fetcher': specifier: workspace:* version: link:../../../generators/typescript/utils/core-utilities/fetcher @@ -5599,8 +5608,8 @@ importers: specifier: workspace:* version: link:../../../workspace/loader '@fern-fern/fdr-cjs-sdk': - specifier: 0.126.1-444264056 - version: 0.126.1-444264056 + specifier: 0.127.4-331678a74 + version: 0.127.4-331678a74 '@fern-fern/fiddle-sdk': specifier: 0.0.584 version: 0.0.584 @@ -6069,8 +6078,8 @@ importers: specifier: workspace:* version: link:../task-context '@fern-fern/fdr-cjs-sdk': - specifier: 0.126.1-444264056 - version: 0.126.1-444264056 + specifier: 0.127.4-331678a74 + version: 0.127.4-331678a74 lodash-es: specifier: ^4.17.21 version: 4.17.21 @@ -6899,8 +6908,8 @@ importers: specifier: 0.10.2 version: 0.10.2 '@fern-fern/fdr-cjs-sdk': - specifier: 0.126.1-444264056 - version: 0.126.1-444264056 + specifier: 0.127.4-331678a74 + version: 0.127.4-331678a74 '@fern-fern/fdr-test-sdk': specifier: ^0.0.5297 version: 0.0.5297 @@ -8295,8 +8304,8 @@ packages: '@fern-api/core-utils@0.4.24-rc1': resolution: {integrity: sha512-aYu4lQK2qZIKzTF9TeFrICTPJ/zGEZUEWQmZt6pJeHu+R6afrcCBNkUleWU1OpHlDbe+xXUUBOktRg0PM9Hywg==} - '@fern-api/docs-parsers@0.0.12': - resolution: {integrity: sha512-bTnhztE8xZlnyzm3lvnvv+DGGup81qD/A8k2f9OP4I9kxzGsR7p/U/OnEDD+ztXWvlemdH7QxKLjzVmZBLLeJg==} + '@fern-api/docs-parsers@0.0.20': + resolution: {integrity: sha512-N4Xvc1GA5EkZiYkFLnbeOH2fwe4omYC3L/2fArYDvP0KZgpMigSGQOjwbH8MDboZ45BcXzbEs5NsOeCeNMHkgA==} '@fern-api/dynamic-ir-sdk@53.24.0': resolution: {integrity: sha512-4XvzvSsh7lNZz5oYN0LH+FRpFGxg9TjfdipSJMpgHvPShe85m/tcfbOzbS0DHBpQGlKb/gHQtQRPAoBR1wdDAw==} @@ -8304,8 +8313,8 @@ packages: '@fern-api/dynamic-ir-sdk@54.0.0': resolution: {integrity: sha512-5ewX7042AGuw697jA6ubulCIQbfTM2HC8UbHxTakE0px871HJTRwHNT2Y9o2kjMbsRXTzyYdb5lOVr+85sc/KQ==} - '@fern-api/fdr-sdk@0.126.1-444264056': - resolution: {integrity: sha512-Xl1Ctteav1GOulX40756FLEoJAI7VCn6zgkdDb6RRSZhm9Z8fjaBRv/cdMo1saupqHNd62Hm5b8FdmGjwWFAPw==} + '@fern-api/fdr-sdk@0.127.4-331678a74': + resolution: {integrity: sha512-uat4R+gWFoe8UT67q0Yhc6dDwHUn84Jb6do7r5tDk4fUWcy4oPaCMQxutvyclNj6M3AUtFY8uEy/8dCNgk3ADA==} '@fern-api/logger@0.4.24-rc1': resolution: {integrity: sha512-yh0E2F3K3IPnJZcE4dv+u8I51iKgTgv/reinKo4K5YmYEG1iLtw5vBEYMOPkQmsYFPAKIh++OMB/6TrsahMWew==} @@ -8319,8 +8328,8 @@ packages: '@fern-api/ui-core-utils@0.0.0': resolution: {integrity: sha512-8T3YLd+n8z5Vs+WNRIwH6PUW31ZC4/lkRD5G2+qyBcdePfOVYV3CHp3eiUrSSArOr0SJmzN/mQwPm3iAaey7nw==} - '@fern-api/ui-core-utils@0.126.1-444264056': - resolution: {integrity: sha512-9L3Tgl83nGaw9Jug17ZXSkPL7ZTeOP3SukG/Fz2Y2Aa/GqEToCrexuGkTO090nwuv2zO9gi7CYDHvQOEl5IIMQ==} + '@fern-api/ui-core-utils@0.127.4-331678a74': + resolution: {integrity: sha512-hBUEOTKVbGqfKQ+cA8MR1jvn4XYVqTBkW3I0G1LF0NSgzRr8dvpL+BYGmhLQkXweOjTfYsVNIvqzX80oUU2Urw==} '@fern-api/venus-api-sdk@0.10.2': resolution: {integrity: sha512-BD18ZNJeWYvztRXSUWY2lfZ8jFUbNAfHWctvIsWyXgI7C8EL3N0+CWUGD5cZ1QqRnq42nm6XJdWRKhny3yrz4g==} @@ -8328,8 +8337,8 @@ packages: '@fern-fern/docs-config@0.0.80': resolution: {integrity: sha512-wAZCNxwM4qIPn3idoIihPP65KWPjNuoTSfdP4+5f8K2jJLgs5cdkY0pm4cQlGWvi5K6b+lfbUWDaslnXscJ2gQ==} - '@fern-fern/fdr-cjs-sdk@0.126.1-444264056': - resolution: {integrity: sha512-APStUB4pA/H620WMm/K6uyCtk7hxXB4ZuroY+VIfLexpieC39ZSHeN6DlpbuG/vPkF6LUtnpYlfWxGpEVCmhrw==} + '@fern-fern/fdr-cjs-sdk@0.127.4-331678a74': + resolution: {integrity: sha512-ZWWiN71iD/xK9zZPBac5dL2UU3iJPOmK9SazVwPkTxcAMRgVX+Pu/hNKz07NEvQD79l4otCmK5PB8IYMyqlncw==} '@fern-fern/fdr-test-sdk@0.0.5297': resolution: {integrity: sha512-jrZUZ6oIA64LHtrv77xEq0X7qJhT9xRMGWhKcLjIUArMsD7h6KMWUHVdoB/1MP0Mz/uL/E2xmM31SOgJicpIcA==} @@ -10179,6 +10188,14 @@ packages: resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} engines: {node: '>= 0.4'} + date-fns-tz@3.2.0: + resolution: {integrity: sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==} + peerDependencies: + date-fns: ^3.0.0 || ^4.0.0 + + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + dayjs@1.11.11: resolution: {integrity: sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==} @@ -10494,8 +10511,8 @@ packages: resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} engines: {node: '>= 0.4'} - es-toolkit@1.26.0: - resolution: {integrity: sha512-gg6ZaI4eTwzkQGkdEG+t4b4VH2mTxodBQ2KvyTKAQFIag3Vvxoq3XZ1ie4ijIQmVxiipL2+dZLDMkh1VIvAFyg==} + es-toolkit@1.30.1: + resolution: {integrity: sha512-ZXflqanzH8BpHkDhFa10bBf6ONDCe84EPUm7SSICGzuuROSluT2ynTPtwn9PcRelMtorCRozSknI/U0MNYp0Uw==} es6-promise@3.3.1: resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} @@ -12456,6 +12473,9 @@ packages: resolution: {integrity: sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==} engines: {node: '>=12'} + openapi-types@10.0.0: + resolution: {integrity: sha512-Y8xOCT2eiKGYDzMW9R4x5cmfc3vGaaI4EL2pwhDmodWw1HlK18YcZ4uJxc7Rdp7/gGzAygzH9SXr6GKYIXbRcQ==} + openapi-types@12.1.3: resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} @@ -15584,16 +15604,15 @@ snapshots: lodash-es: 4.17.21 strip-ansi: 7.1.0 - '@fern-api/docs-parsers@0.0.12(@types/node@18.15.3)(jsdom@20.0.3)(sass@1.72.0)(terser@5.31.5)(typescript@5.7.2)': + '@fern-api/docs-parsers@0.0.20(@types/node@18.7.18)(jsdom@20.0.3)(sass@1.72.0)(terser@5.31.5)(typescript@4.6.4)': dependencies: '@fern-api/logger': 0.4.24-rc1 '@fern-api/ui-core-utils': 0.0.0 - ajv: 8.17.1 - es-toolkit: 1.26.0 + es-toolkit: 1.30.1 openapi-types: 12.1.3 - ts-essentials: 10.0.1(typescript@5.7.2) + ts-essentials: 10.0.1(typescript@4.6.4) uuid: 9.0.1 - vitest: 2.1.8(@types/node@18.15.3)(jsdom@20.0.3)(sass@1.72.0)(terser@5.31.5) + vitest: 2.1.8(@types/node@18.7.18)(jsdom@20.0.3)(sass@1.72.0)(terser@5.31.5) whatwg-mimetype: 4.0.0 transitivePeerDependencies: - '@edge-runtime/vm' @@ -15617,12 +15636,32 @@ snapshots: '@fern-api/dynamic-ir-sdk@54.0.0': {} - '@fern-api/fdr-sdk@0.126.1-444264056(typescript@5.7.2)': + '@fern-api/fdr-sdk@0.127.4-331678a74(typescript@4.6.4)': + dependencies: + '@fern-api/ui-core-utils': 0.127.4-331678a74 + '@ungap/structured-clone': 1.2.0 + dayjs: 1.11.11 + es-toolkit: 1.30.1 + fast-deep-equal: 3.1.3 + form-data: 4.0.0 + formdata-node: 6.0.3 + js-base64: 3.7.7 + node-fetch: 2.7.0 + qs: 6.12.0 + tinycolor2: 1.6.0 + title: 3.5.3 + ts-essentials: 10.0.1(typescript@4.6.4) + url-join: 5.0.0 + transitivePeerDependencies: + - encoding + - typescript + + '@fern-api/fdr-sdk@0.127.4-331678a74(typescript@5.7.2)': dependencies: - '@fern-api/ui-core-utils': 0.126.1-444264056 + '@fern-api/ui-core-utils': 0.127.4-331678a74 '@ungap/structured-clone': 1.2.0 dayjs: 1.11.11 - es-toolkit: 1.26.0 + es-toolkit: 1.30.1 fast-deep-equal: 3.1.3 form-data: 4.0.0 formdata-node: 6.0.3 @@ -15664,8 +15703,10 @@ snapshots: title: 3.5.3 ua-parser-js: 1.0.37 - '@fern-api/ui-core-utils@0.126.1-444264056': + '@fern-api/ui-core-utils@0.127.4-331678a74': dependencies: + date-fns: 4.1.0 + date-fns-tz: 3.2.0(date-fns@4.1.0) strip-ansi: 7.1.0 title: 3.5.3 ua-parser-js: 1.0.37 @@ -15684,7 +15725,7 @@ snapshots: '@fern-fern/docs-config@0.0.80': {} - '@fern-fern/fdr-cjs-sdk@0.126.1-444264056': + '@fern-fern/fdr-cjs-sdk@0.127.4-331678a74': dependencies: form-data: 4.0.0 formdata-node: 6.0.3 @@ -17891,6 +17932,12 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.1 + date-fns-tz@3.2.0(date-fns@4.1.0): + dependencies: + date-fns: 4.1.0 + + date-fns@4.1.0: {} + dayjs@1.11.11: {} debug@2.6.9: @@ -18278,7 +18325,7 @@ snapshots: is-date-object: 1.0.5 is-symbol: 1.0.4 - es-toolkit@1.26.0: {} + es-toolkit@1.30.1: {} es6-promise@3.3.1: {} @@ -20995,6 +21042,8 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + openapi-types@10.0.0: {} + openapi-types@12.1.3: {} optionator@0.9.3: @@ -22447,6 +22496,10 @@ snapshots: dependencies: typescript: 5.7.2 + ts-essentials@10.0.1(typescript@4.6.4): + optionalDependencies: + typescript: 4.6.4 + ts-essentials@10.0.1(typescript@5.7.2): optionalDependencies: typescript: 5.7.2