diff --git a/.pnp.cjs b/.pnp.cjs index 976729bba64..3bf4bc938e0 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -6484,7 +6484,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "packageDependencies": [\ ["@fern-api/configuration", "workspace:packages/cli/configuration"],\ ["@fern-api/core-utils", "workspace:packages/commons/core-utils"],\ - ["@fern-api/fdr-sdk", "npm:0.97.0-bec92c652"],\ + ["@fern-api/fdr-sdk", "npm:0.98.0-rc1"],\ ["@fern-api/fs-utils", "workspace:packages/commons/fs-utils"],\ ["@fern-api/task-context", "workspace:packages/cli/task-context"],\ ["@fern-fern/fiddle-sdk", "npm:0.0.552"],\ @@ -6513,7 +6513,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "packageLocation": "./packages/core/",\ "packageDependencies": [\ ["@fern-api/core", "workspace:packages/core"],\ - ["@fern-api/fdr-sdk", "npm:0.97.0-bec92c652"],\ + ["@fern-api/fdr-sdk", "npm:0.98.0-rc1"],\ ["@fern-api/venus-api-sdk", "npm:0.0.38"],\ ["@fern-fern/fdr-test-sdk", "npm:0.0.5297"],\ ["@fern-fern/fiddle-sdk", "npm:0.0.552"],\ @@ -6603,7 +6603,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "packageDependencies": [\ ["@fern-api/docs-preview", "workspace:packages/cli/docs-preview"],\ ["@fern-api/docs-resolver", "workspace:packages/cli/docs-resolver"],\ - ["@fern-api/fdr-sdk", "npm:0.97.0-bec92c652"],\ + ["@fern-api/fdr-sdk", "npm:0.98.0-rc1"],\ ["@fern-api/fs-utils", "workspace:packages/commons/fs-utils"],\ ["@fern-api/ir-sdk", "workspace:packages/ir-sdk"],\ ["@fern-api/logger", "workspace:packages/cli/logger"],\ @@ -6645,16 +6645,18 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@fern-api/docs-resolver", "workspace:packages/cli/docs-resolver"],\ ["@fern-api/configuration", "workspace:packages/cli/configuration"],\ ["@fern-api/core-utils", "workspace:packages/commons/core-utils"],\ - ["@fern-api/fdr-sdk", "npm:0.97.0-bec92c652"],\ + ["@fern-api/fdr-sdk", "npm:0.98.0-rc1"],\ ["@fern-api/fs-utils", "workspace:packages/commons/fs-utils"],\ ["@fern-api/ir-generator", "workspace:packages/cli/generation/ir-generator"],\ ["@fern-api/ir-sdk", "workspace:packages/ir-sdk"],\ + ["@fern-api/register", "workspace:packages/cli/register"],\ ["@fern-api/task-context", "workspace:packages/cli/task-context"],\ ["@fern-api/workspace-loader", "workspace:packages/cli/workspace-loader"],\ ["@types/diff", "npm:5.2.1"],\ ["@types/jest", "npm:29.0.3"],\ ["@types/lodash-es", "npm:4.17.12"],\ ["@types/node", "npm:18.7.18"],\ + ["dayjs", "npm:1.11.11"],\ ["depcheck", "npm:1.4.6"],\ ["diff", "npm:5.2.0"],\ ["eslint", "npm:8.56.0"],\ @@ -6731,14 +6733,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["@fern-api/fdr-sdk", [\ - ["npm:0.97.0-bec92c652", {\ - "packageLocation": "./.yarn/cache/@fern-api-fdr-sdk-npm-0.97.0-bec92c652-d87bdbfe14-32a938be2d.zip/node_modules/@fern-api/fdr-sdk/",\ + ["npm:0.98.0-rc1", {\ + "packageLocation": "./.yarn/cache/@fern-api-fdr-sdk-npm-0.98.0-rc1-621f8f4dde-5178c9e245.zip/node_modules/@fern-api/fdr-sdk/",\ "packageDependencies": [\ - ["@fern-api/fdr-sdk", "npm:0.97.0-bec92c652"],\ + ["@fern-api/fdr-sdk", "npm:0.98.0-rc1"],\ ["dayjs", "npm:1.11.11"],\ ["fast-deep-equal", "npm:3.1.3"],\ ["form-data", "npm:4.0.0"],\ + ["formdata-node", "npm:6.0.3"],\ ["js-base64", "npm:3.7.7"],\ + ["node-fetch", "virtual:25958c3cdea01727abe9184cd62ebdcb7f32f5bd5b1d13e8a0e1d5080a9e9f7be886906e1af797d4fcc43965772a072bf87bbcb6b0a29bf8dd97020f3fa1ccf2#npm:2.7.0"],\ ["qs", "npm:6.12.0"],\ ["tinycolor2", "npm:1.6.0"],\ ["title", "npm:3.5.3"],\ @@ -7424,7 +7428,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@fern-api/configuration", "workspace:packages/cli/configuration"],\ ["@fern-api/core", "workspace:packages/core"],\ ["@fern-api/core-utils", "workspace:packages/commons/core-utils"],\ - ["@fern-api/fdr-sdk", "npm:0.97.0-bec92c652"],\ + ["@fern-api/fdr-sdk", "npm:0.98.0-rc1"],\ ["@fern-api/ir-generator", "workspace:packages/cli/generation/ir-generator"],\ ["@fern-api/ir-sdk", "workspace:packages/ir-sdk"],\ ["@fern-api/task-context", "workspace:packages/cli/task-context"],\ @@ -7453,7 +7457,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@fern-api/core", "workspace:packages/core"],\ ["@fern-api/core-utils", "workspace:packages/commons/core-utils"],\ ["@fern-api/docs-resolver", "workspace:packages/cli/docs-resolver"],\ - ["@fern-api/fdr-sdk", "npm:0.97.0-bec92c652"],\ + ["@fern-api/fdr-sdk", "npm:0.98.0-rc1"],\ ["@fern-api/fs-utils", "workspace:packages/commons/fs-utils"],\ ["@fern-api/ir-generator", "workspace:packages/cli/generation/ir-generator"],\ ["@fern-api/ir-migrations", "workspace:packages/cli/generation/ir-migrations"],\ diff --git a/.yarn/cache/@fern-api-fdr-sdk-npm-0.97.0-bec92c652-d87bdbfe14-32a938be2d.zip b/.yarn/cache/@fern-api-fdr-sdk-npm-0.97.0-bec92c652-d87bdbfe14-32a938be2d.zip deleted file mode 100644 index b5a2465f8cb..00000000000 Binary files a/.yarn/cache/@fern-api-fdr-sdk-npm-0.97.0-bec92c652-d87bdbfe14-32a938be2d.zip and /dev/null differ diff --git a/.yarn/cache/@fern-api-fdr-sdk-npm-0.98.0-rc1-621f8f4dde-5178c9e245.zip b/.yarn/cache/@fern-api-fdr-sdk-npm-0.98.0-rc1-621f8f4dde-5178c9e245.zip new file mode 100644 index 00000000000..c1be34f394c Binary files /dev/null and b/.yarn/cache/@fern-api-fdr-sdk-npm-0.98.0-rc1-621f8f4dde-5178c9e245.zip differ diff --git a/packages/cli/configuration/fern/definition/docs.yml b/packages/cli/configuration/fern/definition/docs.yml index ac89315221f..5bcb244fa55 100644 --- a/packages/cli/configuration/fern/definition/docs.yml +++ b/packages/cli/configuration/fern/definition/docs.yml @@ -168,7 +168,7 @@ types: union: - PageConfiguration - SectionConfiguration - - ApiSectionConfiguration + - ApiReferenceConfiguration - LinkConfiguration LogoConfiguration: @@ -391,7 +391,7 @@ types: hidden: optional skip-slug: optional - ApiSectionConfiguration: + ApiReferenceConfiguration: properties: api: string "api-name": @@ -404,75 +404,95 @@ types: snippets: optional summary: type: optional - docs: | - Relative path to the markdown file. This summary is displayed at the top of the API section. + docs: Relative path to the markdown file layout: - type: optional + type: optional> docs: | Advanced usage: when specified, this object will be used to customize the order that your API endpoints are displayed in the docs site, including subpackages, and additional markdown pages (to be rendered in between API endpoints). If not specified, the order will be inferred from the OpenAPI Spec or Fern Definition. icon: optional slug: optional hidden: optional skip-slug: optional + alphabetized: + type: optional + docs: | + If `alphabetized` is set to true, packages and endpoints will be sorted alphabetically, unless explicitly ordered in the `layout` object. flattened: type: optional docs: | If `flattened` is set to true, the title specified in `api` will be hidden, and its endpoints and subpackages won't be grouped under it. This setting is useful if the API reference is short and you want to display all endpoints at the top level. + paginated: + availability: in-development + type: optional + docs: If true, the API reference will be paginated rather than displayed in a single page (long-scrolling). - ApiNavigationItem: + ApiReferenceLayoutItem: docs: | Use the `layout` object to customize the order that your API endpoints are displayed in the docs site. discriminated: false union: - type: string + docs: This should be either an endpoint, websocket, webhook, or subpackage ID + - type: map + docs: Keyed by subpackage name, this object allows you to group endpoints and pages together. + - ApiReferenceSectionConfiguration + - ApiReferenceEndpointConfiguration + - PageConfiguration + - LinkConfiguration + + ApiReferenceSectionConfiguration: + properties: + section: + type: string docs: | - This should be either an endpoint, websocket, webhook, or subpackage ID - - type: map - docs: | - Keyed by subpackage ID, this object allows you to group endpoints and pages together. - - type: PageConfiguration - docs: | - This should be a markdown file that will be displayed in the API section. - - ApiNavigationItems: - type: list - examples: - - name: Ordering Groups - value: - - users # users endpoints will be displayed first - - roles - - permissions - - name: Ordering Endpoints - value: - - roles: - - get - - create - - update - - users: - - get - - create - - update - - permissions: - - get - - create - - update - - name: Ordering Groups and Pages - value: - - users - - roles: - - get - - create - - ./pages/inner-page.mdx - - update - - permissions + The title of the api package that will be displayed in the sidebar. + referenced-packages: + type: optional> + docs: This section will inherit the endpoints from the specified subpackage(s). If multiple packages are specified, they will be merged. + summary: + type: optional + docs: Relative path to the markdown file. + contents: optional> + slug: optional + icon: optional + hidden: optional + skip-slug: optional + + ApiReferencePackageConfiguration: + discriminated: false + union: + - list + - ApiReferencePackageConfigurationWithOptions + + ApiReferencePackageConfigurationWithOptions: + properties: + title: optional + summary: + type: optional + docs: | + Relative path to the markdown file. This summary is displayed at the top of the API section. + contents: optional> + slug: optional + icon: optional + hidden: optional + skip-slug: optional + + ApiReferenceEndpointConfiguration: + properties: + endpoint: string + title: optional + slug: optional + icon: optional + hidden: optional LinkConfiguration: properties: link: string href: string + icon: optional VersionedSnippetLanguageConfiguration: properties: diff --git a/packages/cli/configuration/package.json b/packages/cli/configuration/package.json index 344ceef8f44..0d6d0e81155 100644 --- a/packages/cli/configuration/package.json +++ b/packages/cli/configuration/package.json @@ -29,7 +29,7 @@ }, "dependencies": { "@fern-api/core-utils": "workspace:*", - "@fern-api/fdr-sdk": "0.97.0-bec92c652", + "@fern-api/fdr-sdk": "0.98.0-rc1", "@fern-api/fs-utils": "workspace:*", "@fern-api/task-context": "workspace:*", "@fern-fern/fiddle-sdk": "^0.0.552", diff --git a/packages/cli/configuration/src/docs-yml/ParsedDocsConfiguration.ts b/packages/cli/configuration/src/docs-yml/ParsedDocsConfiguration.ts index df699e6c2ea..f222548da88 100644 --- a/packages/cli/configuration/src/docs-yml/ParsedDocsConfiguration.ts +++ b/packages/cli/configuration/src/docs-yml/ParsedDocsConfiguration.ts @@ -183,17 +183,20 @@ export declare namespace DocsNavigationItem { showErrors: boolean; snippetsConfiguration: SnippetsConfiguration | undefined; summaryAbsolutePath: AbsoluteFilePath | undefined; - navigation: ParsedApiNavigationItem[]; + navigation: ParsedApiReferenceLayoutItem[]; hidden: boolean | undefined; slug: string | undefined; skipUrlSlug: boolean | undefined; - flattened: boolean | undefined; + alphabetized: boolean; + flattened: boolean; + paginated: boolean; } export interface Link { type: "link"; text: string; url: string; + icon: string | undefined; } export interface VersionedSnippetLanguageConfiguration { @@ -209,12 +212,37 @@ export declare namespace DocsNavigationItem { } } -export declare namespace ParsedApiNavigationItem { - export interface Subpackage { - type: "subpackage"; - subpackageId: string; +export declare namespace ParsedApiReferenceLayoutItem { + export interface Section { + type: "section"; + title: string; // title + referencedSubpackages: string[]; // subpackage IDs + summaryAbsolutePath: AbsoluteFilePath | undefined; + contents: ParsedApiReferenceLayoutItem[]; + slug: string | undefined; + hidden: boolean | undefined; + icon: string | undefined; + skipUrlSlug: boolean | undefined; + } + export interface Package { + type: "package"; + title: string | undefined; // defaults to subpackage title + package: string; // subpackage ID summaryAbsolutePath: AbsoluteFilePath | undefined; - items: ParsedApiNavigationItem[]; + contents: ParsedApiReferenceLayoutItem[]; + slug: string | undefined; + hidden: boolean | undefined; + icon: string | undefined; + skipUrlSlug: boolean | undefined; + } + + export interface Endpoint { + type: "endpoint"; + endpoint: string; // endpoint locator + title: string | undefined; + icon: string | undefined; + slug: string | undefined; + hidden: boolean | undefined; } export interface Item { @@ -223,7 +251,10 @@ export declare namespace ParsedApiNavigationItem { } } -export type ParsedApiNavigationItem = - | ParsedApiNavigationItem.Item - | ParsedApiNavigationItem.Subpackage - | DocsNavigationItem.Page; +export type ParsedApiReferenceLayoutItem = + | ParsedApiReferenceLayoutItem.Item + | ParsedApiReferenceLayoutItem.Section + | ParsedApiReferenceLayoutItem.Package + | ParsedApiReferenceLayoutItem.Endpoint + | DocsNavigationItem.Page + | DocsNavigationItem.Link; diff --git a/packages/cli/configuration/src/docs-yml/getAllPages.ts b/packages/cli/configuration/src/docs-yml/getAllPages.ts index db80e803b3c..d462e67402f 100644 --- a/packages/cli/configuration/src/docs-yml/getAllPages.ts +++ b/packages/cli/configuration/src/docs-yml/getAllPages.ts @@ -1,7 +1,11 @@ import { assertNever } from "@fern-api/core-utils"; import { AbsoluteFilePath, RelativeFilePath, relativize } from "@fern-api/fs-utils"; import { readFile } from "fs/promises"; -import { DocsNavigationConfiguration, DocsNavigationItem, ParsedApiNavigationItem } from "./ParsedDocsConfiguration"; +import { + DocsNavigationConfiguration, + DocsNavigationItem, + ParsedApiReferenceLayoutItem +} from "./ParsedDocsConfiguration"; export async function getAllPages({ navigation, @@ -70,7 +74,7 @@ export async function getAllPagesFromNavigationItem({ const toRet = combineMaps( await Promise.all( item.navigation.map((apiNavigation) => - getAllPagesFromApiNavigationItem({ item: apiNavigation, absolutePathToFernFolder }) + getAllPagesFromApiReferenceLayoutItem({ item: apiNavigation, absolutePathToFernFolder }) ) ) ); @@ -106,11 +110,11 @@ function combineMaps(maps: Record[]) { return maps.reduce((acc, record) => ({ ...acc, ...record }), {}); } -async function getAllPagesFromApiNavigationItem({ +async function getAllPagesFromApiReferenceLayoutItem({ item, absolutePathToFernFolder }: { - item: ParsedApiNavigationItem; + item: ParsedApiReferenceLayoutItem; absolutePathToFernFolder: AbsoluteFilePath; }): Promise> { if (item.type === "page") { @@ -119,11 +123,11 @@ async function getAllPagesFromApiNavigationItem({ await readFile(item.absolutePath) ).toString() }; - } else if (item.type === "subpackage") { + } else if (item.type === "package" || item.type === "section") { const toRet = combineMaps( await Promise.all( - item.items.map(async (subItem) => { - return await getAllPagesFromApiNavigationItem({ item: subItem, absolutePathToFernFolder }); + item.contents.map(async (subItem) => { + return await getAllPagesFromApiReferenceLayoutItem({ item: subItem, absolutePathToFernFolder }); }) ) ); diff --git a/packages/cli/configuration/src/docs-yml/parseDocsConfiguration.ts b/packages/cli/configuration/src/docs-yml/parseDocsConfiguration.ts index 896e8693093..533d43bd3b9 100644 --- a/packages/cli/configuration/src/docs-yml/parseDocsConfiguration.ts +++ b/packages/cli/configuration/src/docs-yml/parseDocsConfiguration.ts @@ -14,7 +14,7 @@ import { FilepathOrUrl, FontConfig, JavascriptConfig, - ParsedApiNavigationItem, + ParsedApiReferenceLayoutItem, ParsedDocsConfiguration, ParsedMetadataConfig, TabbedDocsNavigation, @@ -537,28 +537,32 @@ async function convertNavigationItem({ rawConfig.snippets != null ? convertSnippetsConfiguration({ rawConfig: rawConfig.snippets }) : undefined, - navigation: rawConfig.layout?.flatMap((item) => parseApiNavigationItem(item, absolutePathToConfig)) ?? [], + navigation: + rawConfig.layout?.flatMap((item) => parseApiReferenceLayoutItem(item, absolutePathToConfig)) ?? [], summaryAbsolutePath: resolveFilepath(rawConfig.summary, absolutePathToConfig), hidden: rawConfig.hidden ?? undefined, slug: rawConfig.slug, skipUrlSlug: rawConfig.skipSlug ?? false, - flattened: rawConfig.flattened ?? false + flattened: rawConfig.flattened ?? false, + alphabetized: rawConfig.alphabetized ?? false, + paginated: rawConfig.paginated ?? false }; } if (isRawLinkConfig(rawConfig)) { return { type: "link", text: rawConfig.link, - url: rawConfig.href + url: rawConfig.href, + icon: rawConfig.icon }; } assertNever(rawConfig); } -function parseApiNavigationItem( - item: RawDocs.ApiNavigationItem, +function parseApiReferenceLayoutItem( + item: RawDocs.ApiReferenceLayoutItem, absolutePathToConfig: AbsoluteFilePath -): ParsedApiNavigationItem[] { +): ParsedApiReferenceLayoutItem[] { if (typeof item === "string") { return [{ type: "item", value: item }]; } @@ -575,14 +579,67 @@ function parseApiNavigationItem( hidden: item.hidden } ]; + } else if (isRawLinkConfig(item)) { + return [ + { + type: "link", + text: item.link, + url: item.href, + icon: item.icon + } + ]; + } else if (isRawApiRefSectionConfiguration(item)) { + return [ + { + type: "section", + title: item.section, + referencedSubpackages: item.referencedPackages ?? [], + summaryAbsolutePath: resolveFilepath(item.summary, absolutePathToConfig), + contents: + item.contents?.flatMap((value) => parseApiReferenceLayoutItem(value, absolutePathToConfig)) ?? [], + slug: item.slug, + hidden: item.hidden, + skipUrlSlug: item.skipSlug, + icon: item.icon + } + ]; + } else if (isRawApiRefEndpointConfiguration(item)) { + return [ + { + type: "endpoint", + endpoint: item.endpoint, + title: item.title, + icon: item.icon, + slug: item.slug, + hidden: item.hidden + } + ]; } - - return Object.entries(item).map(([key, values]): ParsedApiNavigationItem.Subpackage => { + return Object.entries(item).map(([key, value]): ParsedApiReferenceLayoutItem.Package => { + if (isRawApiRefPackageConfiguration(value)) { + return { + type: "package", + title: value.title, + package: key, + summaryAbsolutePath: resolveFilepath(value.summary, absolutePathToConfig), + contents: + value.contents?.flatMap((value) => parseApiReferenceLayoutItem(value, absolutePathToConfig)) ?? [], + slug: value.slug, + hidden: value.hidden, + skipUrlSlug: value.skipSlug, + icon: value.icon + }; + } return { - type: "subpackage", - subpackageId: key, - summaryAbsolutePath: undefined, // TODO: implement subpackage summary page - items: values.flatMap((value) => parseApiNavigationItem(value, absolutePathToConfig)) + type: "package", + title: undefined, + package: key, + summaryAbsolutePath: undefined, + contents: value.flatMap((value) => parseApiReferenceLayoutItem(value, absolutePathToConfig)), + hidden: false, + slug: undefined, + skipUrlSlug: false, + icon: undefined }; }); } @@ -609,14 +666,29 @@ function isRawSectionConfig(item: RawDocs.NavigationItem): item is RawDocs.Secti return (item as RawDocs.SectionConfiguration).section != null; } -function isRawApiSectionConfig(item: RawDocs.NavigationItem): item is RawDocs.ApiSectionConfiguration { +function isRawApiSectionConfig(item: RawDocs.NavigationItem): item is RawDocs.ApiReferenceConfiguration { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - return (item as RawDocs.ApiSectionConfiguration).api != null; + return (item as RawDocs.ApiReferenceConfiguration).api != null; } -function isRawLinkConfig(item: RawDocs.NavigationItem): item is RawDocs.LinkConfiguration { +function isRawLinkConfig(item: unknown): item is RawDocs.LinkConfiguration { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - return (item as RawDocs.LinkConfiguration).link != null; + RawDocs; + return isPlainObject(item) && typeof item.link === "string" && typeof item.href === "string"; +} + +function isRawApiRefSectionConfiguration(item: unknown): item is RawDocs.ApiReferenceSectionConfiguration { + return isPlainObject(item) && typeof item.section === "string" && Array.isArray(item.contents); +} + +function isRawApiRefEndpointConfiguration(item: unknown): item is RawDocs.ApiReferenceEndpointConfiguration { + return isPlainObject(item) && typeof item.endpoint === "string"; +} + +function isRawApiRefPackageConfiguration( + item: RawDocs.ApiReferencePackageConfiguration +): item is RawDocs.ApiReferencePackageConfigurationWithOptions { + return !Array.isArray(item); } export function resolveFilepath(unresolvedFilepath: string, absolutePath: AbsoluteFilePath): AbsoluteFilePath; diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiNavigationItem.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiNavigationItem.ts deleted file mode 100644 index 6168193cde7..00000000000 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiNavigationItem.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * This file was auto-generated by Fern from our API Definition. - */ - -import * as FernDocsConfig from "../../.."; - -/** - * Use the `layout` object to customize the order that your API endpoints - * are displayed in the docs site. - */ -export type ApiNavigationItem = - /** - * This should be either an endpoint, websocket, webhook, or subpackage ID - * */ - | string - /** - * Keyed by subpackage ID, this object allows you to group endpoints and pages together. - * */ - | Record - /** - * This should be a markdown file that will be displayed in the API section. - * */ - | FernDocsConfig.PageConfiguration; diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiNavigationItems.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiNavigationItems.ts deleted file mode 100644 index 15498b6f200..00000000000 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiNavigationItems.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * This file was auto-generated by Fern from our API Definition. - */ - -import * as FernDocsConfig from "../../.."; - -/** - * @example - * [] - * - * @example - * [] - * - * @example - * [] - */ -export type ApiNavigationItems = FernDocsConfig.ApiNavigationItem[]; diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiSectionConfiguration.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferenceConfiguration.ts similarity index 70% rename from packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiSectionConfiguration.ts rename to packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferenceConfiguration.ts index 4b6cb0a9c6e..d64227ad839 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiSectionConfiguration.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferenceConfiguration.ts @@ -4,7 +4,7 @@ import * as FernDocsConfig from "../../.."; -export interface ApiSectionConfiguration { +export interface ApiReferenceConfiguration { api: string; /** Name of API that we are referencing */ apiName?: string; @@ -12,18 +12,22 @@ export interface ApiSectionConfiguration { /** Defaults to false */ displayErrors?: boolean; snippets?: FernDocsConfig.SnippetsConfiguration; - /** Relative path to the markdown file. This summary is displayed at the top of the API section. */ + /** Relative path to the markdown file */ summary?: string; /** Advanced usage: when specified, this object will be used to customize the order that your API endpoints are displayed in the docs site, including subpackages, and additional markdown pages (to be rendered in between API endpoints). If not specified, the order will be inferred from the OpenAPI Spec or Fern Definition. */ - layout?: FernDocsConfig.ApiNavigationItems; + layout?: FernDocsConfig.ApiReferenceLayoutItem[]; icon?: string; slug?: string; hidden?: boolean; skipSlug?: boolean; + /** If `alphabetized` is set to true, packages and endpoints will be sorted alphabetically, unless explicitly ordered in the `layout` object. */ + alphabetized?: boolean; /** * If `flattened` is set to true, the title specified in `api` will be hidden, and its endpoints and subpackages won't be grouped under it. * * This setting is useful if the API reference is short and you want to display all endpoints at the top level. */ flattened?: boolean; + /** If true, the API reference will be paginated rather than displayed in a single page (long-scrolling). */ + paginated?: boolean; } diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferenceEndpointConfiguration.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferenceEndpointConfiguration.ts new file mode 100644 index 00000000000..49da79e3572 --- /dev/null +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferenceEndpointConfiguration.ts @@ -0,0 +1,11 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export interface ApiReferenceEndpointConfiguration { + endpoint: string; + title?: string; + slug?: string; + icon?: string; + hidden?: boolean; +} diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferenceLayoutItem.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferenceLayoutItem.ts new file mode 100644 index 00000000000..c68744189e1 --- /dev/null +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferenceLayoutItem.ts @@ -0,0 +1,21 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as FernDocsConfig from "../../.."; + +/** + * Use the `layout` object to customize the order that your API endpoints + * are displayed in the docs site. + */ +export type ApiReferenceLayoutItem = + /** + * This should be either an endpoint, websocket, webhook, or subpackage ID */ + | string + /** + * Keyed by subpackage name, this object allows you to group endpoints and pages together. */ + | Record + | FernDocsConfig.ApiReferenceSectionConfiguration + | FernDocsConfig.ApiReferenceEndpointConfiguration + | FernDocsConfig.PageConfiguration + | FernDocsConfig.LinkConfiguration; diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferencePackageConfiguration.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferencePackageConfiguration.ts new file mode 100644 index 00000000000..0bae44b41d6 --- /dev/null +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferencePackageConfiguration.ts @@ -0,0 +1,9 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as FernDocsConfig from "../../.."; + +export type ApiReferencePackageConfiguration = + | FernDocsConfig.ApiReferenceLayoutItem[] + | FernDocsConfig.ApiReferencePackageConfigurationWithOptions; diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferencePackageConfigurationWithOptions.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferencePackageConfigurationWithOptions.ts new file mode 100644 index 00000000000..af0e8014c33 --- /dev/null +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferencePackageConfigurationWithOptions.ts @@ -0,0 +1,16 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as FernDocsConfig from "../../.."; + +export interface ApiReferencePackageConfigurationWithOptions { + title?: string; + /** Relative path to the markdown file. This summary is displayed at the top of the API section. */ + summary?: string; + contents?: FernDocsConfig.ApiReferenceLayoutItem[]; + slug?: string; + icon?: string; + hidden?: boolean; + skipSlug?: boolean; +} diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferenceSectionConfiguration.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferenceSectionConfiguration.ts new file mode 100644 index 00000000000..7a683b7bd5d --- /dev/null +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/ApiReferenceSectionConfiguration.ts @@ -0,0 +1,19 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as FernDocsConfig from "../../.."; + +export interface ApiReferenceSectionConfiguration { + /** The title of the api package that will be displayed in the sidebar. */ + section: string; + /** This section will inherit the endpoints from the specified subpackage(s). If multiple packages are specified, they will be merged. */ + referencedPackages?: string[]; + /** Relative path to the markdown file. */ + summary?: string; + contents?: FernDocsConfig.ApiReferenceLayoutItem[]; + slug?: string; + icon?: string; + hidden?: boolean; + skipSlug?: boolean; +} diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/LinkConfiguration.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/LinkConfiguration.ts index 78d0561c288..75a54469097 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/LinkConfiguration.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/LinkConfiguration.ts @@ -5,4 +5,5 @@ export interface LinkConfiguration { link: string; href: string; + icon?: string; } diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/NavigationItem.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/NavigationItem.ts index f0d627dbb63..d793bfabcc4 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/NavigationItem.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/NavigationItem.ts @@ -7,5 +7,5 @@ import * as FernDocsConfig from "../../.."; export type NavigationItem = | FernDocsConfig.PageConfiguration | FernDocsConfig.SectionConfiguration - | FernDocsConfig.ApiSectionConfiguration + | FernDocsConfig.ApiReferenceConfiguration | FernDocsConfig.LinkConfiguration; diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/index.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/index.ts index 3b61a4ba9d5..c8df654ee2d 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/index.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/api/resources/docs/types/index.ts @@ -29,9 +29,12 @@ export * from "./FontConfigPath"; export * from "./FontConfigVariant"; export * from "./PageConfiguration"; export * from "./SectionConfiguration"; -export * from "./ApiSectionConfiguration"; -export * from "./ApiNavigationItem"; -export * from "./ApiNavigationItems"; +export * from "./ApiReferenceConfiguration"; +export * from "./ApiReferenceLayoutItem"; +export * from "./ApiReferenceSectionConfiguration"; +export * from "./ApiReferencePackageConfiguration"; +export * from "./ApiReferencePackageConfigurationWithOptions"; +export * from "./ApiReferenceEndpointConfiguration"; export * from "./LinkConfiguration"; export * from "./VersionedSnippetLanguageConfiguration"; export * from "./SnippetLanguageConfiguration"; diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiNavigationItem.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiNavigationItem.ts deleted file mode 100644 index b92ee90a459..00000000000 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiNavigationItem.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * This file was auto-generated by Fern from our API Definition. - */ - -import * as serializers from "../../.."; -import * as FernDocsConfig from "../../../../api"; -import * as core from "../../../../core"; - -export const ApiNavigationItem: core.serialization.Schema< - serializers.ApiNavigationItem.Raw, - FernDocsConfig.ApiNavigationItem -> = core.serialization.undiscriminatedUnion([ - core.serialization.string(), - core.serialization.record( - core.serialization.string(), - core.serialization.lazy(async () => (await import("../../..")).ApiNavigationItems) - ), - core.serialization.lazyObject(async () => (await import("../../..")).PageConfiguration), -]); - -export declare namespace ApiNavigationItem { - type Raw = string | Record | serializers.PageConfiguration.Raw; -} diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiNavigationItems.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiNavigationItems.ts deleted file mode 100644 index f9b87a0ee95..00000000000 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiNavigationItems.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * This file was auto-generated by Fern from our API Definition. - */ - -import * as serializers from "../../.."; -import * as FernDocsConfig from "../../../../api"; -import * as core from "../../../../core"; - -export const ApiNavigationItems: core.serialization.Schema< - serializers.ApiNavigationItems.Raw, - FernDocsConfig.ApiNavigationItems -> = core.serialization.list(core.serialization.lazy(async () => (await import("../../..")).ApiNavigationItem)); - -export declare namespace ApiNavigationItems { - type Raw = serializers.ApiNavigationItem.Raw[]; -} diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiSectionConfiguration.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferenceConfiguration.ts similarity index 70% rename from packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiSectionConfiguration.ts rename to packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferenceConfiguration.ts index d91e95873a5..6bf7102dc13 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiSectionConfiguration.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferenceConfiguration.ts @@ -6,9 +6,9 @@ import * as serializers from "../../.."; import * as FernDocsConfig from "../../../../api"; import * as core from "../../../../core"; -export const ApiSectionConfiguration: core.serialization.ObjectSchema< - serializers.ApiSectionConfiguration.Raw, - FernDocsConfig.ApiSectionConfiguration +export const ApiReferenceConfiguration: core.serialization.ObjectSchema< + serializers.ApiReferenceConfiguration.Raw, + FernDocsConfig.ApiReferenceConfiguration > = core.serialization.object({ api: core.serialization.string(), apiName: core.serialization.property("api-name", core.serialization.string().optional()), @@ -16,15 +16,19 @@ export const ApiSectionConfiguration: core.serialization.ObjectSchema< displayErrors: core.serialization.property("display-errors", core.serialization.boolean().optional()), snippets: core.serialization.lazyObject(async () => (await import("../../..")).SnippetsConfiguration).optional(), summary: core.serialization.string().optional(), - layout: core.serialization.lazy(async () => (await import("../../..")).ApiNavigationItems).optional(), + layout: core.serialization + .list(core.serialization.lazy(async () => (await import("../../..")).ApiReferenceLayoutItem)) + .optional(), icon: core.serialization.string().optional(), slug: core.serialization.string().optional(), hidden: core.serialization.boolean().optional(), skipSlug: core.serialization.property("skip-slug", core.serialization.boolean().optional()), + alphabetized: core.serialization.boolean().optional(), flattened: core.serialization.boolean().optional(), + paginated: core.serialization.boolean().optional(), }); -export declare namespace ApiSectionConfiguration { +export declare namespace ApiReferenceConfiguration { interface Raw { api: string; "api-name"?: string | null; @@ -32,11 +36,13 @@ export declare namespace ApiSectionConfiguration { "display-errors"?: boolean | null; snippets?: serializers.SnippetsConfiguration.Raw | null; summary?: string | null; - layout?: serializers.ApiNavigationItems.Raw | null; + layout?: serializers.ApiReferenceLayoutItem.Raw[] | null; icon?: string | null; slug?: string | null; hidden?: boolean | null; "skip-slug"?: boolean | null; + alphabetized?: boolean | null; flattened?: boolean | null; + paginated?: boolean | null; } } diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferenceEndpointConfiguration.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferenceEndpointConfiguration.ts new file mode 100644 index 00000000000..ac377ef02c7 --- /dev/null +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferenceEndpointConfiguration.ts @@ -0,0 +1,28 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../.."; +import * as FernDocsConfig from "../../../../api"; +import * as core from "../../../../core"; + +export const ApiReferenceEndpointConfiguration: core.serialization.ObjectSchema< + serializers.ApiReferenceEndpointConfiguration.Raw, + FernDocsConfig.ApiReferenceEndpointConfiguration +> = core.serialization.object({ + endpoint: core.serialization.string(), + title: core.serialization.string().optional(), + slug: core.serialization.string().optional(), + icon: core.serialization.string().optional(), + hidden: core.serialization.boolean().optional(), +}); + +export declare namespace ApiReferenceEndpointConfiguration { + interface Raw { + endpoint: string; + title?: string | null; + slug?: string | null; + icon?: string | null; + hidden?: boolean | null; + } +} diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferenceLayoutItem.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferenceLayoutItem.ts new file mode 100644 index 00000000000..23b10f41589 --- /dev/null +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferenceLayoutItem.ts @@ -0,0 +1,32 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../.."; +import * as FernDocsConfig from "../../../../api"; +import * as core from "../../../../core"; + +export const ApiReferenceLayoutItem: core.serialization.Schema< + serializers.ApiReferenceLayoutItem.Raw, + FernDocsConfig.ApiReferenceLayoutItem +> = core.serialization.undiscriminatedUnion([ + core.serialization.string(), + core.serialization.record( + core.serialization.string(), + core.serialization.lazy(async () => (await import("../../..")).ApiReferencePackageConfiguration) + ), + core.serialization.lazyObject(async () => (await import("../../..")).ApiReferenceSectionConfiguration), + core.serialization.lazyObject(async () => (await import("../../..")).ApiReferenceEndpointConfiguration), + core.serialization.lazyObject(async () => (await import("../../..")).PageConfiguration), + core.serialization.lazyObject(async () => (await import("../../..")).LinkConfiguration), +]); + +export declare namespace ApiReferenceLayoutItem { + type Raw = + | string + | Record + | serializers.ApiReferenceSectionConfiguration.Raw + | serializers.ApiReferenceEndpointConfiguration.Raw + | serializers.PageConfiguration.Raw + | serializers.LinkConfiguration.Raw; +} diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferencePackageConfiguration.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferencePackageConfiguration.ts new file mode 100644 index 00000000000..e5a41a509b7 --- /dev/null +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferencePackageConfiguration.ts @@ -0,0 +1,19 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../.."; +import * as FernDocsConfig from "../../../../api"; +import * as core from "../../../../core"; + +export const ApiReferencePackageConfiguration: core.serialization.Schema< + serializers.ApiReferencePackageConfiguration.Raw, + FernDocsConfig.ApiReferencePackageConfiguration +> = core.serialization.undiscriminatedUnion([ + core.serialization.list(core.serialization.lazy(async () => (await import("../../..")).ApiReferenceLayoutItem)), + core.serialization.lazyObject(async () => (await import("../../..")).ApiReferencePackageConfigurationWithOptions), +]); + +export declare namespace ApiReferencePackageConfiguration { + type Raw = serializers.ApiReferenceLayoutItem.Raw[] | serializers.ApiReferencePackageConfigurationWithOptions.Raw; +} diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferencePackageConfigurationWithOptions.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferencePackageConfigurationWithOptions.ts new file mode 100644 index 00000000000..505524c92ac --- /dev/null +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferencePackageConfigurationWithOptions.ts @@ -0,0 +1,34 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../.."; +import * as FernDocsConfig from "../../../../api"; +import * as core from "../../../../core"; + +export const ApiReferencePackageConfigurationWithOptions: core.serialization.ObjectSchema< + serializers.ApiReferencePackageConfigurationWithOptions.Raw, + FernDocsConfig.ApiReferencePackageConfigurationWithOptions +> = core.serialization.object({ + title: core.serialization.string().optional(), + summary: core.serialization.string().optional(), + contents: core.serialization + .list(core.serialization.lazy(async () => (await import("../../..")).ApiReferenceLayoutItem)) + .optional(), + slug: core.serialization.string().optional(), + icon: core.serialization.string().optional(), + hidden: core.serialization.boolean().optional(), + skipSlug: core.serialization.property("skip-slug", core.serialization.boolean().optional()), +}); + +export declare namespace ApiReferencePackageConfigurationWithOptions { + interface Raw { + title?: string | null; + summary?: string | null; + contents?: serializers.ApiReferenceLayoutItem.Raw[] | null; + slug?: string | null; + icon?: string | null; + hidden?: boolean | null; + "skip-slug"?: boolean | null; + } +} diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferenceSectionConfiguration.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferenceSectionConfiguration.ts new file mode 100644 index 00000000000..fd7ab37da36 --- /dev/null +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/ApiReferenceSectionConfiguration.ts @@ -0,0 +1,39 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../.."; +import * as FernDocsConfig from "../../../../api"; +import * as core from "../../../../core"; + +export const ApiReferenceSectionConfiguration: core.serialization.ObjectSchema< + serializers.ApiReferenceSectionConfiguration.Raw, + FernDocsConfig.ApiReferenceSectionConfiguration +> = core.serialization.object({ + section: core.serialization.string(), + referencedPackages: core.serialization.property( + "referenced-packages", + core.serialization.list(core.serialization.string()).optional() + ), + summary: core.serialization.string().optional(), + contents: core.serialization + .list(core.serialization.lazy(async () => (await import("../../..")).ApiReferenceLayoutItem)) + .optional(), + slug: core.serialization.string().optional(), + icon: core.serialization.string().optional(), + hidden: core.serialization.boolean().optional(), + skipSlug: core.serialization.property("skip-slug", core.serialization.boolean().optional()), +}); + +export declare namespace ApiReferenceSectionConfiguration { + interface Raw { + section: string; + "referenced-packages"?: string[] | null; + summary?: string | null; + contents?: serializers.ApiReferenceLayoutItem.Raw[] | null; + slug?: string | null; + icon?: string | null; + hidden?: boolean | null; + "skip-slug"?: boolean | null; + } +} diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/LinkConfiguration.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/LinkConfiguration.ts index 0dfc51ecdcf..b096a6b64ec 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/LinkConfiguration.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/LinkConfiguration.ts @@ -12,11 +12,13 @@ export const LinkConfiguration: core.serialization.ObjectSchema< > = core.serialization.object({ link: core.serialization.string(), href: core.serialization.string(), + icon: core.serialization.string().optional(), }); export declare namespace LinkConfiguration { interface Raw { link: string; href: string; + icon?: string | null; } } diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/NavigationItem.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/NavigationItem.ts index 17b87cd4a2d..19d681efa44 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/NavigationItem.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/NavigationItem.ts @@ -10,7 +10,7 @@ export const NavigationItem: core.serialization.Schema (await import("../../..")).PageConfiguration), core.serialization.lazyObject(async () => (await import("../../..")).SectionConfiguration), - core.serialization.lazyObject(async () => (await import("../../..")).ApiSectionConfiguration), + core.serialization.lazyObject(async () => (await import("../../..")).ApiReferenceConfiguration), core.serialization.lazyObject(async () => (await import("../../..")).LinkConfiguration), ]); @@ -18,6 +18,6 @@ export declare namespace NavigationItem { type Raw = | serializers.PageConfiguration.Raw | serializers.SectionConfiguration.Raw - | serializers.ApiSectionConfiguration.Raw + | serializers.ApiReferenceConfiguration.Raw | serializers.LinkConfiguration.Raw; } diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/index.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/index.ts index 3b61a4ba9d5..c8df654ee2d 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/index.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/serialization/resources/docs/types/index.ts @@ -29,9 +29,12 @@ export * from "./FontConfigPath"; export * from "./FontConfigVariant"; export * from "./PageConfiguration"; export * from "./SectionConfiguration"; -export * from "./ApiSectionConfiguration"; -export * from "./ApiNavigationItem"; -export * from "./ApiNavigationItems"; +export * from "./ApiReferenceConfiguration"; +export * from "./ApiReferenceLayoutItem"; +export * from "./ApiReferenceSectionConfiguration"; +export * from "./ApiReferencePackageConfiguration"; +export * from "./ApiReferencePackageConfigurationWithOptions"; +export * from "./ApiReferenceEndpointConfiguration"; export * from "./LinkConfiguration"; export * from "./VersionedSnippetLanguageConfiguration"; export * from "./SnippetLanguageConfiguration"; diff --git a/packages/cli/docs-preview/package.json b/packages/cli/docs-preview/package.json index a7a7a709b4b..9d8f4d6a8d8 100644 --- a/packages/cli/docs-preview/package.json +++ b/packages/cli/docs-preview/package.json @@ -28,7 +28,7 @@ }, "dependencies": { "@fern-api/docs-resolver": "workspace:*", - "@fern-api/fdr-sdk": "0.97.0-bec92c652", + "@fern-api/fdr-sdk": "0.98.0-rc1", "@fern-api/fs-utils": "workspace:*", "@fern-api/ir-sdk": "workspace:*", "@fern-api/logger": "workspace:*", diff --git a/packages/cli/docs-resolver/package.json b/packages/cli/docs-resolver/package.json index 21d5bf26ecc..a92ef557e86 100644 --- a/packages/cli/docs-resolver/package.json +++ b/packages/cli/docs-resolver/package.json @@ -29,12 +29,13 @@ "dependencies": { "@fern-api/configuration": "workspace:*", "@fern-api/core-utils": "workspace:*", - "@fern-api/fdr-sdk": "0.97.0-bec92c652", + "@fern-api/fdr-sdk": "0.98.0-rc1", "@fern-api/fs-utils": "workspace:*", "@fern-api/ir-generator": "workspace:*", "@fern-api/ir-sdk": "workspace:*", + "@fern-api/register": "workspace:*", "@fern-api/task-context": "workspace:*", - "@fern-api/workspace-loader": "workspace:*", + "dayjs": "^1.11.11", "gray-matter": "^4.0.3", "lodash-es": "^4.17.21", "mdast-util-from-markdown": "^2.0.0", @@ -42,6 +43,7 @@ "url-join": "^5.0.0" }, "devDependencies": { + "@fern-api/workspace-loader": "workspace:*", "@types/diff": "^5.2.1", "@types/jest": "^29.0.3", "@types/lodash-es": "^4.17.12", diff --git a/packages/cli/docs-resolver/src/ApiDefinitionHolder.ts b/packages/cli/docs-resolver/src/ApiDefinitionHolder.ts new file mode 100644 index 00000000000..2465cdf0837 --- /dev/null +++ b/packages/cli/docs-resolver/src/ApiDefinitionHolder.ts @@ -0,0 +1,322 @@ +import { APIV1Read, FernNavigation } from "@fern-api/fdr-sdk"; +import { TaskContext } from "@fern-api/task-context"; +import urlJoin from "url-join"; +import { isSubpackage } from "./utils/isSubpackage"; +import { stringifyEndpointPathParts, stringifyEndpointPathParts2 } from "./utils/stringifyEndpointPathParts"; + +// unlike `FernNavigation.EndpointId`, which concatenates the subpackageId and endpointId with a dot, +// SubpackageHolder is intended to help resolve the endpointId from within a subpackage. +export interface SubpackageHolder { + readonly endpoints: ReadonlyMap; + readonly webSockets: ReadonlyMap; + readonly webhooks: ReadonlyMap; +} + +export const ROOT_PACKAGE_ID = "__package__" as const; + +/** + * A holder for an API definition, which provides a way to resolve endpoint, websocket, and webhook IDs. + * This class is intended to be used in the ApiReferenceNodeConverter, which is responsible for resolving + * endpoint references such as: + * + * - `GET /users/{userId}` + * + * into the corresponding endpoint definition, when constructing the API reference tree. + */ +export class ApiDefinitionHolder { + public static create(api: APIV1Read.ApiDefinition, context?: TaskContext): ApiDefinitionHolder { + return new ApiDefinitionHolder(api, context); + } + + #endpoints = new Map(); + #webSockets = new Map(); + #webhooks = new Map(); + #endpointsInverted = new Map(); + #webSocketsInverted = new Map(); + #webhooksInverted = new Map(); + + #subpackages = new Map(); + #endpointsByLocator = new Map(); + #webSocketsByLocator = new Map(); + #webhooksByLocator = new Map(); + #subpackagesByLocator = new Map(); + + public static getSubpackageId(pkg: APIV1Read.ApiDefinitionPackage): string { + return isSubpackage(pkg) ? pkg.subpackageId : ROOT_PACKAGE_ID; + } + + public getSubpackage(subpackageId: string | undefined): APIV1Read.ApiDefinitionPackage | undefined { + if (subpackageId == null) { + return undefined; + } else if (subpackageId === ROOT_PACKAGE_ID) { + return this.api.rootPackage; + } else { + return this.api.subpackages[subpackageId] ?? this.#subpackagesByLocator.get(subpackageId); + } + } + + public resolveSubpackage( + pkg: APIV1Read.ApiDefinitionPackage | undefined + ): APIV1Read.ApiDefinitionPackage | undefined { + if (pkg == null) { + return undefined; + } + while (pkg?.pointsTo != null) { + pkg = this.api.subpackages[pkg.pointsTo]; + } + return pkg; + } + + private constructor(public readonly api: APIV1Read.ApiDefinition, private readonly context?: TaskContext) { + [api.rootPackage, ...Object.values(api.subpackages)].forEach((pkg) => { + const subpackageId = ApiDefinitionHolder.getSubpackageId(pkg); + const subpackageHolder = { + endpoints: new Map(), + webSockets: new Map(), + webhooks: new Map() + }; + this.#subpackages.set(subpackageId, subpackageHolder); + pkg.endpoints.forEach((endpoint) => { + subpackageHolder.endpoints.set(endpoint.id, endpoint); + const endpointId = ApiDefinitionHolder.createEndpointId(endpoint, subpackageId); + this.#endpoints.set(endpointId, endpoint); + const locators: string[] = []; + + const methods: string[] = [endpoint.method]; + + if (endpoint.response?.type.type === "stream") { + methods.push("STREAM"); + } + + methods.forEach((method) => { + locators.push(`${method} ${stringifyEndpointPathParts(endpoint.path.parts)}`); + locators.push(`${method} ${stringifyEndpointPathParts2(endpoint.path.parts)}`); + + endpoint.environments.forEach((environment) => { + locators.push( + `${method} ${urlJoin(environment.baseUrl, stringifyEndpointPathParts(endpoint.path.parts))}` + ); + locators.push( + `${method} ${urlJoin( + environment.baseUrl, + stringifyEndpointPathParts2(endpoint.path.parts) + )}` + ); + const basePath = getBasePath(environment); + if (basePath != null) { + locators.push( + `${method} ${urlJoin(basePath, stringifyEndpointPathParts(endpoint.path.parts))}` + ); + locators.push( + `${method} ${urlJoin(basePath, stringifyEndpointPathParts2(endpoint.path.parts))}` + ); + } + }); + }); + + locators.forEach((locator) => { + this.context?.logger.debug(`Registering endpoint locator: ${locator}`); + this.#endpointsByLocator.set(locator, endpoint); + }); + }); + pkg.websockets.forEach((webSocket) => { + subpackageHolder.webSockets.set(webSocket.id, webSocket); + const webSocketId = ApiDefinitionHolder.createWebSocketId(webSocket, subpackageId); + this.#webSockets.set(webSocketId, webSocket); + + const locators: string[] = []; + const methods: string[] = ["GET", "WSS"]; + + methods.forEach((method) => { + locators.push(`${method} ${stringifyEndpointPathParts(webSocket.path.parts)}`); + locators.push(`${method} ${stringifyEndpointPathParts2(webSocket.path.parts)}`); + + webSocket.environments.forEach((environment) => { + locators.push( + `${method} ${urlJoin( + environment.baseUrl, + stringifyEndpointPathParts(webSocket.path.parts) + )}` + ); + locators.push( + `${method} ${urlJoin( + environment.baseUrl, + stringifyEndpointPathParts2(webSocket.path.parts) + )}` + ); + const basePath = getBasePath(environment); + if (basePath != null) { + locators.push( + `${method} ${urlJoin(basePath, stringifyEndpointPathParts(webSocket.path.parts))}` + ); + locators.push( + `${method} ${urlJoin(basePath, stringifyEndpointPathParts2(webSocket.path.parts))}` + ); + } + }); + }); + + locators.forEach((locator) => { + this.context?.logger.debug(`Registering websocket locator: ${locator}`); + this.#webSocketsByLocator.set(locator, webSocket); + }); + }); + pkg.webhooks.forEach((webhook) => { + subpackageHolder.webhooks.set(webhook.id, webhook); + this.#webhooks.set(ApiDefinitionHolder.createWebhookId(webhook, subpackageId), webhook); + + // webhooks don't have paths, so we just register them by their ID in a later step + }); + }); + + this.#endpoints.forEach((endpoint, endpointId) => { + this.#endpointsInverted.set(endpoint, endpointId); + }); + this.#webSockets.forEach((webSocket, webSocketId) => { + this.#webSocketsInverted.set(webSocket, webSocketId); + }); + this.#webhooks.forEach((webhook, webhookId) => { + this.#webhooksInverted.set(webhook, webhookId); + }); + + this.#constructSubpackageLocators(api.rootPackage, []); + } + + #constructSubpackageLocators(pkg: APIV1Read.ApiDefinitionPackage | undefined, parents: string[]): void { + if (pkg == null) { + return; + } + + const packageList = isSubpackage(pkg) ? [...parents, pkg.name] : parents; + + const path = packageList.length === 0 ? [ROOT_PACKAGE_ID] : packageList; + const locators = [path.join("."), path.join("/"), `${path.join(".")}.yml`]; + locators.forEach((locator) => { + this.context?.logger.debug(`Registering subpackage locator: ${locator}`); + this.#subpackagesByLocator.set(locator, pkg); + }); + + if (pkg.pointsTo != null) { + return this.#constructSubpackageLocators(this.api.subpackages[pkg.pointsTo], packageList); + } + + pkg.endpoints.forEach((endpoint) => { + const path = [...packageList, endpoint.id]; + const locators = [path.join("."), path.join("/")]; + locators.forEach((locator) => { + this.context?.logger.debug(`Registering endpoint locator: ${locator}`); + this.#endpointsByLocator.set(locator, endpoint); + }); + }); + + pkg.websockets.forEach((webSocket) => { + const path = [...packageList, webSocket.id]; + const locators = [path.join("."), path.join("/")]; + locators.forEach((locator) => { + this.context?.logger.debug(`Registering websocket locator: ${locator}`); + this.#webSocketsByLocator.set(locator, webSocket); + }); + }); + + pkg.webhooks.forEach((webhook) => { + const path = [...packageList, webhook.id]; + const locators = [path.join("."), path.join("/")]; + locators.forEach((locator) => { + this.context?.logger.debug(`Registering webhook locator: ${locator}`); + this.#webhooksByLocator.set(locator, webhook); + }); + }); + + pkg.subpackages.forEach((subpackageId) => { + this.#constructSubpackageLocators(this.api.subpackages[subpackageId], packageList); + }); + } + + get endpoints(): ReadonlyMap { + return this.#endpoints; + } + + get webSockets(): ReadonlyMap { + return this.#webSockets; + } + + get webhooks(): ReadonlyMap { + return this.#webhooks; + } + + get endpointsByLocator(): ReadonlyMap { + return this.#endpointsByLocator; + } + + get webSocketsByLocator(): ReadonlyMap { + return this.#webSocketsByLocator; + } + + get webhooksByLocator(): ReadonlyMap { + return this.#webhooksByLocator; + } + + get subpackages(): ReadonlyMap { + return this.#subpackages; + } + + get subpackagesByLocator(): ReadonlyMap { + return this.#subpackagesByLocator; + } + + public getEndpointId(endpoint: APIV1Read.EndpointDefinition): FernNavigation.EndpointId | undefined { + return this.#endpointsInverted.get(endpoint); + } + + public getWebSocketId(webSocket: APIV1Read.WebSocketChannel): FernNavigation.WebSocketId | undefined { + return this.#webSocketsInverted.get(webSocket); + } + + public getWebhookId(webhook: APIV1Read.WebhookDefinition): FernNavigation.WebhookId | undefined { + return this.#webhooksInverted.get(webhook); + } + + // get webhooksByPath(): ReadonlyMap { + // return this.#webhooksByPath; + // } + + public static createEndpointId( + endpoint: APIV1Read.EndpointDefinition, + subpackageId: string + ): FernNavigation.EndpointId { + return FernNavigation.EndpointId(endpoint.originalEndpointId ?? `${subpackageId}.${endpoint.id}`); + } + + public static createWebSocketId( + webSocket: APIV1Read.WebSocketChannel, + subpackageId: string + ): FernNavigation.WebSocketId { + return FernNavigation.WebSocketId(`${subpackageId}.${webSocket.id}`); + } + + public static createWebhookId( + webhook: APIV1Read.WebhookDefinition, + subpackageId: string + ): FernNavigation.WebhookId { + return FernNavigation.WebhookId(`${subpackageId}.${webhook.id}`); + } +} + +function getBasePath(environment: APIV1Read.Environment | undefined): string | undefined { + if (environment == null) { + return undefined; + } + + if (environment.baseUrl.startsWith("/")) { + return environment.baseUrl; + } + + if (environment.baseUrl.startsWith("http") || environment.baseUrl.startsWith("ws")) { + try { + return new URL(environment.baseUrl).pathname; + } catch (e) { + return undefined; + } + } + return undefined; +} diff --git a/packages/cli/docs-resolver/src/ApiReferenceNodeConverter.ts b/packages/cli/docs-resolver/src/ApiReferenceNodeConverter.ts new file mode 100644 index 00000000000..ace957c3781 --- /dev/null +++ b/packages/cli/docs-resolver/src/ApiReferenceNodeConverter.ts @@ -0,0 +1,674 @@ +import { docsYml } from "@fern-api/configuration"; +import { isNonNullish } from "@fern-api/core-utils"; +import { APIV1Read, FernNavigation, titleCase, visitDiscriminatedUnion } from "@fern-api/fdr-sdk"; +import { AbsoluteFilePath, relative, RelativeFilePath } from "@fern-api/fs-utils"; +import { TaskContext } from "@fern-api/task-context"; +import { DocsWorkspace, FernWorkspace } from "@fern-api/workspace-loader"; +import { kebabCase } from "lodash-es"; +import urlJoin from "url-join"; +import { ApiDefinitionHolder, ROOT_PACKAGE_ID } from "./ApiDefinitionHolder"; +import { ChangelogNodeConverter } from "./ChangelogNodeConverter"; +import { NodeIdGenerator } from "./NodeIdGenerator"; +import { isSubpackage } from "./utils/isSubpackage"; +import { stringifyEndpointPathParts } from "./utils/stringifyEndpointPathParts"; + +export class ApiReferenceNodeConverter { + apiDefinitionId: FernNavigation.ApiDefinitionId; + #holder: ApiDefinitionHolder; + #visitedEndpoints = new Set(); + #visitedWebSockets = new Set(); + #visitedWebhooks = new Set(); + #visitedSubpackages = new Set(); + #nodeIdToSubpackageId = new Map(); + #children: FernNavigation.ApiPackageChild[] = []; + #overviewPageId: FernNavigation.PageId | undefined; + #slug: FernNavigation.SlugGenerator; + constructor( + private apiSection: docsYml.DocsNavigationItem.ApiSection, + api: APIV1Read.ApiDefinition, + parentSlug: FernNavigation.SlugGenerator, + private workspace: FernWorkspace, + private docsWorkspace: DocsWorkspace, + private taskContext: TaskContext, + private markdownFilesToFullSlugs: Map + ) { + this.apiDefinitionId = FernNavigation.ApiDefinitionId(api.id); + this.#holder = ApiDefinitionHolder.create(api, taskContext); + + // we are assuming that the apiDefinitionId is unique. + const idgen = NodeIdGenerator.init(this.apiDefinitionId); + + this.#overviewPageId = + this.apiSection.summaryAbsolutePath != null + ? FernNavigation.PageId(this.toRelativeFilepath(this.apiSection.summaryAbsolutePath)) + : 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.summaryAbsolutePath != null + ? this.markdownFilesToFullSlugs.get(this.apiSection.summaryAbsolutePath) + : 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.#holder.api.rootPackage, + this.#slug, + idgen + ); + } + + // Step 2. Fill in the any missing navigation items from the API definition + this.#children = this.#mergeAndFilterChildren( + this.#children.map((child) => this.#convertApiPackageChild(child, this.#slug, idgen)), + this.#convertApiDefinitionPackage(this.#holder.api.rootPackage, this.#slug, idgen) + ); + } + + public get(): FernNavigation.ApiReferenceNode { + const pointsTo = FernNavigation.utils.followRedirects(this.#children); + const idgen = NodeIdGenerator.init(this.apiDefinitionId); + return { + id: idgen.get(), + type: "apiReference", + title: this.apiSection.title, + apiDefinitionId: this.apiDefinitionId, + overviewPageId: this.#overviewPageId, + disableLongScrolling: this.apiSection.paginated, + slug: this.#slug.get(), + icon: this.apiSection.icon, + hidden: this.apiSection.hidden, + hideTitle: this.apiSection.flattened, + showErrors: this.apiSection.showErrors, + changelog: new ChangelogNodeConverter(this.workspace, this.docsWorkspace, idgen).convert( + this.#slug, + `${this.apiSection.title} Changelog` + ), + children: this.#children, + availability: undefined, + pointsTo + }; + } + + // Step 1 + + #convertApiReferenceLayoutItems( + navigation: docsYml.ParsedApiReferenceLayoutItem[], + apiDefinitionPackage: APIV1Read.ApiDefinitionPackage | undefined, + parentSlug: FernNavigation.SlugGenerator, + idgen: NodeIdGenerator + ): FernNavigation.ApiPackageChild[] { + apiDefinitionPackage = this.#holder.resolveSubpackage(apiDefinitionPackage); + const apiDefinitionPackageId = + apiDefinitionPackage != null ? ApiDefinitionHolder.getSubpackageId(apiDefinitionPackage) : undefined; + return navigation + .map((item) => + visitDiscriminatedUnion(item)._visit({ + link: (link) => ({ + id: idgen.append(`link:${link.url}`).get(), + type: "link", + title: link.text, + icon: link.icon, + url: FernNavigation.Url(link.url) + }), + page: (page) => { + const pageId = FernNavigation.PageId(this.toRelativeFilepath(page.absolutePath)); + const pageSlug = parentSlug.apply({ + fullSlug: this.markdownFilesToFullSlugs.get(page.absolutePath)?.split("/"), + urlSlug: page.slug ?? kebabCase(page.title) + }); + return { + id: idgen.append(`page:${pageId}`).get(), + type: "page", + pageId, + title: page.title, + slug: pageSlug.get(), + icon: page.icon, + hidden: page.hidden + }; + }, + package: (pkg) => this.#convertPackage(pkg, parentSlug, idgen), + section: (section) => this.#convertSection(section, parentSlug, idgen), + item: ({ value: unknownIdentifier }): FernNavigation.ApiPackageChild | undefined => + this.#convertUnknownIdentifier(unknownIdentifier, apiDefinitionPackageId, parentSlug, idgen), + endpoint: (endpoint) => this.#convertEndpoint(endpoint, apiDefinitionPackageId, parentSlug, idgen) + }) + ) + .filter(isNonNullish); + } + + #convertPackage( + pkg: docsYml.ParsedApiReferenceLayoutItem.Package, + parentSlug: FernNavigation.SlugGenerator, + idgen: NodeIdGenerator + ): FernNavigation.ApiPackageNode { + const overviewPageId = + pkg.summaryAbsolutePath != null + ? FernNavigation.PageId(this.toRelativeFilepath(pkg.summaryAbsolutePath)) + : undefined; + + const maybeFullSlug = + pkg.summaryAbsolutePath != null ? this.markdownFilesToFullSlugs.get(pkg.summaryAbsolutePath) : undefined; + + const subpackage = this.#holder.getSubpackage(pkg.package); + + if (subpackage != null) { + const subpackageId = ApiDefinitionHolder.getSubpackageId(subpackage); + const subpackageNodeId = idgen.append(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.get(), [subpackageId]); + const urlSlug = + pkg.slug ?? + (isSubpackage(subpackage) + ? subpackage.urlSlug + : this.apiSection.slug ?? kebabCase(this.apiSection.title)); + const slug = parentSlug.apply({ + fullSlug: maybeFullSlug?.split("/"), + skipUrlSlug: pkg.skipUrlSlug, + urlSlug + }); + const convertedItems = this.#convertApiReferenceLayoutItems( + pkg.contents, + subpackage, + slug, + subpackageNodeId + ); + return { + id: subpackageNodeId.get(), + type: "apiPackage", + children: convertedItems, + title: + pkg.title ?? + (isSubpackage(subpackage) + ? subpackage.displayName ?? titleCase(subpackage.name) + : this.apiSection.title), + slug: slug.get(), + icon: pkg.icon, + hidden: pkg.hidden, + overviewPageId, + availability: undefined, + apiDefinitionId: this.apiDefinitionId, + pointsTo: undefined + }; + } 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, undefined, slug, idgen); + return { + id: idgen.append(kebabCase(pkg.package)).get(), + 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 + }; + } + } + + #convertSection( + section: docsYml.ParsedApiReferenceLayoutItem.Section, + parentSlug: FernNavigation.SlugGenerator, + idgen: NodeIdGenerator + ): FernNavigation.ApiPackageNode { + const overviewPageId = + section.summaryAbsolutePath != null + ? FernNavigation.PageId(this.toRelativeFilepath(section.summaryAbsolutePath)) + : undefined; + + const maybeFullSlug = + section.summaryAbsolutePath != null + ? this.markdownFilesToFullSlugs.get(section.summaryAbsolutePath) + : undefined; + + const nodeId = idgen.append(`section:${kebabCase(section.title)}`); + + const subpackages = section.referencedSubpackages.filter((subpackageId) => { + const subpackage = this.#holder.getSubpackage(subpackageId); + if (subpackage == null) { + this.taskContext.logger.error(`Subpackage ${subpackageId} not found in ${this.apiDefinitionId}`); + return false; + } + return true; + }); + + this.#nodeIdToSubpackageId.set(nodeId.get(), subpackages); + subpackages.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, undefined, slug, idgen); + return { + id: nodeId.get(), + type: "apiPackage", + children: convertedItems, + title: section.title, + slug: slug.get(), + icon: section.icon, + hidden: section.hidden, + overviewPageId, + availability: undefined, + apiDefinitionId: this.apiDefinitionId, + pointsTo: undefined + }; + } + + #convertUnknownIdentifier( + unknownIdentifier: string, + apiDefinitionPackageId: string | undefined, + parentSlug: FernNavigation.SlugGenerator, + idgen: NodeIdGenerator + ): FernNavigation.ApiPackageChild | undefined { + unknownIdentifier = unknownIdentifier.trim(); + // unknownIdentifier could either be a package, endpoint, websocket, or webhook. + // We need to determine which one it is. + const subpackage = this.#holder.getSubpackage(unknownIdentifier); + if (subpackage != null) { + const subpackageId = ApiDefinitionHolder.getSubpackageId(subpackage); + const subpackageNodeId = idgen.append(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.get(), [subpackageId]); + const urlSlug = isSubpackage(subpackage) ? subpackage.urlSlug : ""; + const slug = parentSlug.apply({ urlSlug }); + return { + id: subpackageNodeId.get(), + type: "apiPackage", + children: [], + title: isSubpackage(subpackage) + ? subpackage.displayName ?? titleCase(subpackage.name) + : this.apiSection.title, + slug: slug.get(), + icon: undefined, + hidden: undefined, + overviewPageId: undefined, + availability: undefined, + apiDefinitionId: this.apiDefinitionId, + pointsTo: undefined + }; + } + + // 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 + }, + apiDefinitionPackageId, + parentSlug, + idgen + ); + } + + #convertEndpoint( + endpointItem: docsYml.ParsedApiReferenceLayoutItem.Endpoint, + apiDefinitionPackageId: string | undefined, + parentSlug: FernNavigation.SlugGenerator, + idgen: NodeIdGenerator + ): FernNavigation.ApiPackageChild | undefined { + const endpoint = + (apiDefinitionPackageId != null + ? this.#holder.subpackages.get(apiDefinitionPackageId)?.endpoints.get(endpointItem.endpoint) + : undefined) ?? this.#holder.endpointsByLocator.get(endpointItem.endpoint); + + if (endpoint != null) { + const endpointId = this.#holder.getEndpointId(endpoint); + if (endpointId == null) { + throw new Error(`Expected Endpoint ID for ${endpoint.id}. Got undefined.`); + } + if (this.#visitedEndpoints.has(endpointId)) { + this.taskContext.logger.error(`Duplicate endpoint found in the API Reference layout: ${endpointId}`); + } + this.#visitedEndpoints.add(endpointId); + return { + id: idgen.append(endpoint.id).get(), + type: "endpoint", + method: endpoint.method, + endpointId, + apiDefinitionId: this.apiDefinitionId, + availability: FernNavigation.utils.convertAvailability(endpoint.availability), + isResponseStream: endpoint.response?.type.type === "stream", + title: endpointItem.title ?? endpoint.name ?? stringifyEndpointPathParts(endpoint.path.parts), + slug: (endpointItem.slug != null + ? parentSlug.append(endpointItem.slug) + : parentSlug.apply(endpoint) + ).get(), + icon: endpointItem.icon, + hidden: endpointItem.hidden + }; + } + + const webSocket = + (apiDefinitionPackageId != null + ? this.#holder.subpackages.get(apiDefinitionPackageId)?.webSockets.get(endpointItem.endpoint) + : undefined) ?? this.#holder.webSocketsByLocator.get(endpointItem.endpoint); + + if (webSocket != null) { + const webSocketId = this.#holder.getWebSocketId(webSocket); + if (webSocketId == null) { + throw new Error(`Expected WebSocket ID for ${webSocket.id}. Got undefined.`); + } + if (this.#visitedWebSockets.has(webSocketId)) { + this.taskContext.logger.error(`Duplicate web socket found in the API Reference layout: ${webSocketId}`); + } + this.#visitedWebSockets.add(webSocketId); + return { + id: idgen.append(webSocket.id).get(), + type: "webSocket", + webSocketId, + title: endpointItem.title ?? webSocket.name ?? stringifyEndpointPathParts(webSocket.path.parts), + slug: (endpointItem.slug != null + ? parentSlug.append(endpointItem.slug) + : parentSlug.apply(webSocket) + ).get(), + icon: endpointItem.icon, + hidden: endpointItem.hidden, + apiDefinitionId: this.apiDefinitionId, + availability: FernNavigation.utils.convertAvailability(webSocket.availability) + }; + } + + const webhook = + (apiDefinitionPackageId != null + ? this.#holder.subpackages.get(apiDefinitionPackageId)?.webhooks.get(endpointItem.endpoint) + : undefined) ?? this.#holder.webhooks.get(FernNavigation.WebhookId(endpointItem.endpoint)); + + if (webhook != null) { + const webhookId = this.#holder.getWebhookId(webhook); + if (webhookId == null) { + throw new Error(`Expected Webhook ID for ${webhook.id}. Got undefined.`); + } + if (this.#visitedWebhooks.has(webhookId)) { + this.taskContext.logger.error(`Duplicate webhook found in the API Reference layout: ${webhookId}`); + } + this.#visitedWebhooks.add(webhookId); + return { + id: idgen.append(webhook.id).get(), + type: "webhook", + webhookId, + method: webhook.method, + title: endpointItem.title ?? webhook.name ?? urlJoin("/", ...webhook.path), + slug: (endpointItem.slug != null + ? parentSlug.append(endpointItem.slug) + : parentSlug.apply(webhook) + ).get(), + icon: endpointItem.icon, + hidden: endpointItem.hidden, + apiDefinitionId: this.apiDefinitionId, + availability: undefined + }; + } + + this.taskContext.logger.error("Unknown identifier in the API Reference layout: ", endpointItem.endpoint); + + return; + } + + // Step 2 + + #mergeAndFilterChildren( + left: FernNavigation.ApiPackageChild[], + right: FernNavigation.ApiPackageChild[] + ): FernNavigation.ApiPackageChild[] { + return this.mergeEndpointPairs([...left, ...right], NodeIdGenerator.init(this.apiDefinitionId)).filter( + (child) => (child.type === "apiPackage" ? child.children.length > 0 : true) + ); + } + + #convertApiPackageChild( + child: FernNavigation.ApiPackageChild, + parentSlug: FernNavigation.SlugGenerator, + idgen: NodeIdGenerator + ): FernNavigation.ApiPackageChild { + if (child.type === "apiPackage") { + const slug = parentSlug.set(child.slug); + const innerIdgen = idgen.append(kebabCase(child.title)); + const subpackageIds = this.#nodeIdToSubpackageId.get(child.id) ?? []; + const subpackageChildren = subpackageIds.flatMap((subpackageId) => + this.#convertApiDefinitionPackageId(subpackageId, slug, innerIdgen) + ); + + const children = this.#mergeAndFilterChildren(child.children, subpackageChildren); + + return { + ...child, + children, + pointsTo: FernNavigation.utils.followRedirects(children) + }; + } + return child; + } + + #convertApiDefinitionPackage( + pkg: APIV1Read.ApiDefinitionPackage, + parentSlug: FernNavigation.SlugGenerator, + idgen: NodeIdGenerator + ): FernNavigation.ApiPackageChild[] { + // if an endpoint, websocket, webhook, or subpackage is not visited, add it to the additional children list + let additionalChildren: FernNavigation.ApiPackageChild[] = []; + + idgen = idgen.append(isSubpackage(pkg) ? pkg.subpackageId : ROOT_PACKAGE_ID); + + pkg.endpoints.forEach((endpoint) => { + const endpointId = this.#holder.getEndpointId(endpoint); + if (endpointId == null) { + throw new Error(`Expected Endpoint ID for ${endpoint.id}. Got undefined.`); + } + if (this.#visitedEndpoints.has(endpointId)) { + return; + } + additionalChildren.push({ + id: idgen.append(endpoint.id).get(), + type: "endpoint", + method: endpoint.method, + endpointId, + apiDefinitionId: this.apiDefinitionId, + availability: FernNavigation.utils.convertAvailability(endpoint.availability), + isResponseStream: endpoint.response?.type.type === "stream", + title: endpoint.name ?? stringifyEndpointPathParts(endpoint.path.parts), + slug: parentSlug.apply(endpoint).get(), + icon: undefined, + hidden: undefined + }); + }); + + pkg.websockets.forEach((webSocket) => { + const webSocketId = this.#holder.getWebSocketId(webSocket); + if (webSocketId == null) { + throw new Error(`Expected WebSocket ID for ${webSocket.id}. Got undefined.`); + } + if (this.#visitedWebSockets.has(webSocketId)) { + return; + } + additionalChildren.push({ + id: idgen.append(webSocket.id).get(), + type: "webSocket", + webSocketId, + title: webSocket.name ?? stringifyEndpointPathParts(webSocket.path.parts), + slug: parentSlug.apply(webSocket).get(), + icon: undefined, + hidden: undefined, + apiDefinitionId: this.apiDefinitionId, + availability: FernNavigation.utils.convertAvailability(webSocket.availability) + }); + }); + + pkg.webhooks.forEach((webhook) => { + const webhookId = this.#holder.getWebhookId(webhook); + if (webhookId == null) { + throw new Error(`Expected Webhook ID for ${webhook.id}. Got undefined.`); + } + if (this.#visitedWebhooks.has(webhookId)) { + return; + } + additionalChildren.push({ + id: idgen.append(webhook.id).get(), + type: "webhook", + webhookId, + method: webhook.method, + title: webhook.name ?? titleCase(webhook.id), + slug: parentSlug.apply(webhook).get(), + icon: undefined, + hidden: undefined, + apiDefinitionId: this.apiDefinitionId, + availability: undefined + }); + }); + + pkg.subpackages.forEach((subpackageId) => { + if (this.#visitedSubpackages.has(subpackageId)) { + return; + } + + const subpackage = this.#holder.getSubpackage(subpackageId); + if (subpackage == null) { + this.taskContext.logger.error(`Subpackage ${subpackageId} not found in ${this.apiDefinitionId}`); + return; + } + + const subpackageNodeId = idgen.append(subpackageId); + const slug = isSubpackage(subpackage) ? parentSlug.apply(subpackage) : parentSlug; + const subpackageChildren = this.#convertApiDefinitionPackageId(subpackageId, slug, subpackageNodeId); + additionalChildren.push({ + id: subpackageNodeId.get(), + type: "apiPackage", + children: subpackageChildren, + title: isSubpackage(subpackage) + ? subpackage.displayName ?? titleCase(subpackage.name) + : this.apiSection.title, + slug: slug.get(), + icon: undefined, + hidden: undefined, + overviewPageId: undefined, + availability: undefined, + apiDefinitionId: this.apiDefinitionId, + pointsTo: FernNavigation.utils.followRedirects(subpackageChildren) + }); + }); + + 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; + } + + #convertApiDefinitionPackageId( + packageId: string | undefined, + // children: FernNavigation.ApiPackageChild[], + parentSlug: FernNavigation.SlugGenerator, + idgen: NodeIdGenerator + ): FernNavigation.ApiPackageChild[] { + const pkg = + packageId != null ? this.#holder.resolveSubpackage(this.#holder.getSubpackage(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, idgen); + } + + private mergeEndpointPairs( + children: FernNavigation.ApiPackageChild[], + idgen: NodeIdGenerator + ): FernNavigation.ApiPackageChild[] { + const toRet: FernNavigation.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 || toRet.includes(existing) || existing.isResponseStream === child.isResponseStream) { + toRet.push(child); + return; + } + + const idx = toRet.indexOf(existing); + const pairNode: FernNavigation.EndpointPairNode = { + id: idgen.append(`${child.endpointId}:pair`).get(), + type: "endpointPair", + stream: child.isResponseStream ? child : existing, + nonStream: child.isResponseStream ? existing : child + }; + + toRet[idx] = pairNode; + }); + + 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/ChangelogNodeConverter.ts b/packages/cli/docs-resolver/src/ChangelogNodeConverter.ts new file mode 100644 index 00000000000..76e3079ce8d --- /dev/null +++ b/packages/cli/docs-resolver/src/ChangelogNodeConverter.ts @@ -0,0 +1,159 @@ +import { FernNavigation } from "@fern-api/fdr-sdk"; +import { AbsoluteFilePath, relative, RelativeFilePath } from "@fern-api/fs-utils"; +import { DocsWorkspace, FernWorkspace } from "@fern-api/workspace-loader"; +import dayjs from "dayjs"; +import utc from "dayjs/plugin/utc"; +import { last } from "lodash-es"; +import { NodeIdGenerator } from "./NodeIdGenerator"; +import { extractDatetimeFromChangelogTitle } from "./utils/extractDatetimeFromChangelogTitle"; + +dayjs.extend(utc); + +export class ChangelogNodeConverter { + public constructor( + private workspace: FernWorkspace, + private docsWorkspace: DocsWorkspace, + private idgen: NodeIdGenerator + ) {} + + public convert(parentSlug: FernNavigation.SlugGenerator, title?: string): FernNavigation.ChangelogNode | undefined { + if (this.workspace.changelog == null || this.workspace.changelog.files.length === 0) { + return undefined; + } + + this.idgen = this.idgen.append("changelog"); + const unsortedChangelogItems: { date: Date; pageId: RelativeFilePath }[] = []; + for (const file of this.workspace.changelog.files) { + const filename = last(file.absoluteFilepath.split("/")); + if (filename == null) { + continue; + } + const changelogDate = extractDatetimeFromChangelogTitle(filename); + if (changelogDate == null) { + continue; + } + const relativePath = this.toRelativeFilepath(file.absoluteFilepath); + unsortedChangelogItems.push({ date: changelogDate, pageId: relativePath }); + } + // sort changelog items by date, in descending order + const changelogItems = unsortedChangelogItems.map((item): FernNavigation.ChangelogEntryNode => { + const date = dayjs.utc(item.date); + return { + id: this.idgen.append(date.format("YYYY-M-D")).get(), + type: "changelogEntry", + title: date.format("MMMM D, YYYY"), + slug: parentSlug.append(date.format("YYYY/M/D")).get(), + icon: undefined, + hidden: undefined, + date: item.date.toISOString(), + pageId: FernNavigation.PageId(item.pageId) + }; + }); + + const entries = orderBy(changelogItems, (entry) => entry.date, "desc"); + const changelogYears = this.groupByYear(entries, parentSlug); + + return { + id: this.idgen.get(), + type: "changelog", + title: title ?? "Changelog", + slug: parentSlug.get(), + icon: undefined, + hidden: undefined, + children: changelogYears, + overviewPageId: undefined + }; + } + + private groupByYear( + entries: FernNavigation.ChangelogEntryNode[], + parentSlug: FernNavigation.SlugGenerator + ): FernNavigation.ChangelogYearNode[] { + const years = new Map(); + for (const entry of entries) { + const year = dayjs.utc(entry.date).year(); + const yearEntries = years.get(year) ?? []; + yearEntries.push(entry); + years.set(year, yearEntries); + } + return orderBy( + Array.from(years.entries()).map(([year, entries]) => { + const slug = parentSlug.append(year.toString()).get(); + return { + id: this.idgen.append(year.toString()).get(), + type: "changelogYear" as const, + title: year.toString(), + year, + slug, + icon: undefined, + hidden: undefined, + children: this.groupByMonth(entries, parentSlug) + }; + }), + "year", + "desc" + ); + } + + private groupByMonth( + entries: FernNavigation.ChangelogEntryNode[], + parentSlug: FernNavigation.SlugGenerator + ): FernNavigation.ChangelogMonthNode[] { + const months = new Map(); + for (const entry of entries) { + const month = dayjs.utc(entry.date).month() + 1; + const monthEntries = months.get(month) ?? []; + monthEntries.push(entry); + months.set(month, monthEntries); + } + return orderBy( + Array.from(months.entries()).map(([month, entries]) => { + const date = dayjs(new Date(0, month - 1)); + return { + id: this.idgen.append(date.format("YYYY-M")).get(), + type: "changelogMonth" as const, + title: date.format("MMMM YYYY"), + month, + slug: parentSlug.append(month.toString()).get(), + icon: undefined, + hidden: undefined, + children: entries + }; + }), + "month", + "desc" + ); + } + + 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); + } +} + +function orderBy>( + items: T[], + key: K, + order?: "asc" | "desc" +): T[]; +function orderBy(items: T[], key: (item: T) => string | number, order?: "asc" | "desc"): T[]; +function orderBy>( + items: T[], + key: K | ((item: T) => string | number), + order: "asc" | "desc" = "asc" +): T[] { + return items.concat().sort((a, b) => { + const aValue = typeof key === "function" ? key(a) : a[key]; + const bValue = typeof key === "function" ? key(b) : b[key]; + if (aValue < bValue) { + return order === "asc" ? -1 : 1; + } else if (aValue > bValue) { + return order === "asc" ? 1 : -1; + } + return 0; + }); +} diff --git a/packages/cli/docs-resolver/src/DocsDefinitionResolver.ts b/packages/cli/docs-resolver/src/DocsDefinitionResolver.ts index 3775ee0338d..c5ca60f2ddf 100644 --- a/packages/cli/docs-resolver/src/DocsDefinitionResolver.ts +++ b/packages/cli/docs-resolver/src/DocsDefinitionResolver.ts @@ -1,22 +1,26 @@ import { docsYml, WithoutQuestionMarks } from "@fern-api/configuration"; import { assertNever, isNonNullish, visitDiscriminatedUnion } from "@fern-api/core-utils"; -import { APIV1Write, DocsV1Write } from "@fern-api/fdr-sdk"; +import { APIV1Write, DocsV1Write, FernNavigation } from "@fern-api/fdr-sdk"; import { AbsoluteFilePath, relative, RelativeFilePath, resolve } from "@fern-api/fs-utils"; import { generateIntermediateRepresentation } from "@fern-api/ir-generator"; import { IntermediateRepresentation } from "@fern-api/ir-sdk"; import { TaskContext } from "@fern-api/task-context"; import { DocsWorkspace, FernWorkspace } from "@fern-api/workspace-loader"; +import dayjs from "dayjs"; +import utc from "dayjs/plugin/utc"; import matter from "gray-matter"; -import { last, orderBy } from "lodash-es"; +import { kebabCase } from "lodash-es"; import urlJoin from "url-join"; -import { convertDocsSnippetsConfigToFdr } from "./convertDocsSnippetsConfigToFdr"; -import { convertIrToNavigation } from "./convertIrToNavigation"; -import { extractDatetimeFromChangelogTitle } from "./extractDatetimeFromChangelogTitle"; -import { collectFilesFromDocsConfig } from "./getImageFilepathsToUpload"; -import { parseImagePaths, replaceImagePathsAndUrls } from "./parseImagePaths"; -import { replaceReferencedMarkdown } from "./replaceReferencedMarkdown"; +import { ApiReferenceNodeConverter } from "./ApiReferenceNodeConverter"; +import { convertDocsSnippetsConfigToFdr } from "./utils/convertDocsSnippetsConfigToFdr"; +import { convertIrToApiDefinition } from "./utils/convertIrToApiDefinition"; +import { collectFilesFromDocsConfig } from "./utils/getImageFilepathsToUpload"; +import { parseImagePaths, replaceImagePathsAndUrls } from "./utils/parseImagePaths"; +import { replaceReferencedMarkdown } from "./utils/replaceReferencedMarkdown"; import { wrapWithHttps } from "./wrapWithHttps"; +dayjs.extend(utc); + export interface FilePathPair { absoluteFilePath: AbsoluteFilePath; relativeFilePath: RelativeFilePath; @@ -112,7 +116,7 @@ export class DocsDefinitionResolver { }); // postprocess markdown files after uploading all images to replace the image paths in the markdown files with the fileIDs - const basePath = this.getDocsBasePath() ?? "/"; + const basePath = this.getDocsBasePath(); for (const [relativePath, markdown] of Object.entries(this.parsedDocsConfig.pages)) { this.parsedDocsConfig.pages[RelativeFilePath.of(relativePath)] = replaceImagePathsAndUrls( markdown, @@ -154,7 +158,12 @@ export class DocsDefinitionResolver { return resolve(this.docsWorkspace.absoluteFilepath, unresolvedFilepath); } - private toRelativeFilepath(filepath: AbsoluteFilePath): RelativeFilePath { + 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); } @@ -172,7 +181,7 @@ export class DocsDefinitionResolver { return mdxFilePathToSlug; } - private getDocsBasePath(): string | undefined { + private getDocsBasePath(): string { const url = new URL(wrapWithHttps(this.domain)); return url.pathname; } @@ -222,10 +231,11 @@ export class DocsDefinitionResolver { } private async convertNavigationConfig(): Promise { + const slug = FernNavigation.SlugGenerator.init(FernNavigation.utils.slugjoin(this.getDocsBasePath())); switch (this.parsedDocsConfig.navigation.type) { case "untabbed": { const items = await Promise.all( - this.parsedDocsConfig.navigation.items.map((item) => this.convertNavigationItem(item)) + this.parsedDocsConfig.navigation.items.map((item) => this.convertNavigationItem(item, slug)) ); return { items }; } @@ -233,7 +243,8 @@ export class DocsDefinitionResolver { return { tabsV2: await this.convertTabbedNavigation( this.parsedDocsConfig.navigation.items, - this.parsedDocsConfig.tabs + this.parsedDocsConfig.tabs, + slug ) }; } @@ -241,9 +252,11 @@ export class DocsDefinitionResolver { const versions = await Promise.all( this.parsedDocsConfig.navigation.versions.map( async (version): Promise => { + const versionSlug = slug.setVersionSlug(version.slug ?? kebabCase(version.version)); const convertedNavigation = await this.convertUnversionedNavigationConfig({ navigationConfig: version.navigation, - tabs: version.tabs + tabs: version.tabs, + parentSlug: versionSlug }); return { version: version.version, @@ -264,7 +277,10 @@ export class DocsDefinitionResolver { } } - private async convertNavigationItem(item: docsYml.DocsNavigationItem): Promise { + private async convertNavigationItem( + item: docsYml.DocsNavigationItem, + parentSlug: FernNavigation.SlugGenerator + ): Promise { switch (item.type) { case "page": { return { @@ -278,8 +294,13 @@ export class DocsDefinitionResolver { }; } case "section": { + const slug = parentSlug.apply({ + fullSlug: undefined, // TODO: implement fullSlug for sections when summary pages are supported + skipUrlSlug: item.skipUrlSlug, + urlSlug: item.slug ?? kebabCase(item.title) + }); const sectionItems = await Promise.all( - item.contents.map((nestedItem) => this.convertNavigationItem(nestedItem)) + item.contents.map((nestedItem) => this.convertNavigationItem(nestedItem, slug)) ); return { type: "section", @@ -305,55 +326,18 @@ export class DocsDefinitionResolver { readme: undefined }); const apiDefinitionId = await this.registerApi({ ir, snippetsConfig }); - const unsortedChangelogItems: { date: Date; pageId: RelativeFilePath }[] = []; - if (workspace.changelog != null) { - for (const file of workspace.changelog.files) { - const filename = last(file.absoluteFilepath.split("/")); - if (filename == null) { - continue; - } - const changelogDate = extractDatetimeFromChangelogTitle(filename); - if (changelogDate == null) { - continue; - } - const relativePath = this.toRelativeFilepath(file.absoluteFilepath); - unsortedChangelogItems.push({ date: changelogDate, pageId: relativePath }); - } - } - - // sort changelog items by date, in descending order - const changelogItems = orderBy(unsortedChangelogItems, (item) => item.date, "desc").map( - (item): DocsV1Write.ChangelogItem => ({ - date: item.date.toISOString(), - pageId: item.pageId - }) + const api = convertIrToApiDefinition(ir, apiDefinitionId); + const node = new ApiReferenceNodeConverter( + item, + api, + parentSlug, + workspace, + this.docsWorkspace, + this.taskContext, + this.markdownFilesToFullSlugs ); - return { - type: "api", - title: item.title, - icon: item.icon, - api: apiDefinitionId, - urlSlugOverride: item.slug, - skipUrlSlug: item.skipUrlSlug, - showErrors: item.showErrors, - changelog: - changelogItems.length > 0 - ? { - urlSlug: "changelog", - items: changelogItems - } - : undefined, - hidden: item.hidden, - navigation: convertIrToNavigation( - ir, - item.summaryAbsolutePath, - item.navigation, - this.docsWorkspace.absoluteFilepathToDocsConfig, - this.markdownFilesToFullSlugs - ), - flattened: item.flattened - }; + return { type: "apiV2", node: node.get() }; } case "link": { return { @@ -369,15 +353,17 @@ export class DocsDefinitionResolver { private async convertUnversionedNavigationConfig({ navigationConfig, - tabs + tabs, + parentSlug }: { navigationConfig: docsYml.UnversionedNavigationConfiguration; tabs: Record | undefined; + parentSlug: FernNavigation.SlugGenerator; }): Promise { switch (navigationConfig.type) { case "untabbed": { const untabbedItems = await Promise.all( - navigationConfig.items.map((item) => this.convertNavigationItem(item)) + navigationConfig.items.map((item) => this.convertNavigationItem(item, parentSlug)) ); return { items: untabbedItems @@ -385,7 +371,7 @@ export class DocsDefinitionResolver { } case "tabbed": { return { - tabsV2: await this.convertTabbedNavigation(navigationConfig.items, tabs) + tabsV2: await this.convertTabbedNavigation(navigationConfig.items, tabs, parentSlug) }; } default: @@ -395,7 +381,8 @@ export class DocsDefinitionResolver { private async convertTabbedNavigation( items: docsYml.TabbedNavigation[], - tabs: Record | undefined + tabs: Record | undefined, + parentSlug: FernNavigation.SlugGenerator ): Promise { return Promise.all( items.map(async (tabbedItem): Promise> => { @@ -425,8 +412,13 @@ export class DocsDefinitionResolver { ); } + const slug = parentSlug.apply({ + skipUrlSlug: tabConfig.skipSlug, + urlSlug: tabConfig.slug ?? kebabCase(tabConfig.displayName) + }); + const tabbedItems = await Promise.all( - tabbedItem.layout.map((item) => this.convertNavigationItem(item)) + tabbedItem.layout.map((item) => this.convertNavigationItem(item, slug)) ); return { diff --git a/packages/cli/docs-resolver/src/NodeIdGenerator.ts b/packages/cli/docs-resolver/src/NodeIdGenerator.ts new file mode 100644 index 00000000000..41e17bd3158 --- /dev/null +++ b/packages/cli/docs-resolver/src/NodeIdGenerator.ts @@ -0,0 +1,26 @@ +import { FernNavigation } from "@fern-api/fdr-sdk"; + +export class NodeIdGenerator { + public static init(id: string): NodeIdGenerator { + return new NodeIdGenerator(id, new Set([id])); + } + private constructor(private id: string, private ids: Set) {} + + public get(): FernNavigation.NodeId { + return FernNavigation.NodeId(this.id); + } + + public append(part: string): NodeIdGenerator { + let id = `${this.id}.${part}`; + if (this.ids.has(id)) { + let i = 1; + while (this.ids.has(`${id}-${i}`)) { + i++; + } + id = `${id}-${i}`; + } + + this.ids.add(id); + return new NodeIdGenerator(id, this.ids); + } +} diff --git a/packages/cli/docs-resolver/src/__test__/__snapshots__/api-resolver.test.ts.snap b/packages/cli/docs-resolver/src/__test__/__snapshots__/api-resolver.test.ts.snap new file mode 100644 index 00000000000..6cd7f7709cd --- /dev/null +++ b/packages/cli/docs-resolver/src/__test__/__snapshots__/api-resolver.test.ts.snap @@ -0,0 +1,330 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`converts to api reference node 1`] = ` +{ + "apiDefinitionId": "550e8400-e29b-41d4-a716-446655440000", + "availability": undefined, + "changelog": undefined, + "children": [ + { + "apiDefinitionId": "550e8400-e29b-41d4-a716-446655440000", + "availability": undefined, + "children": [ + { + "apiDefinitionId": "550e8400-e29b-41d4-a716-446655440000", + "availability": undefined, + "endpointId": "endpoint_imdb.deleteMovie", + "hidden": undefined, + "icon": undefined, + "id": "550e8400-e29b-41d4-a716-446655440000.deleteMovie", + "isResponseStream": false, + "method": "DELETE", + "slug": "base/path/api-reference/testing/delete-movie", + "title": "Delete Movie", + "type": "endpoint", + }, + { + "apiDefinitionId": "550e8400-e29b-41d4-a716-446655440000", + "availability": undefined, + "endpointId": "endpoint_imdb.getMovie", + "hidden": undefined, + "icon": undefined, + "id": "550e8400-e29b-41d4-a716-446655440000.getMovie", + "isResponseStream": false, + "method": "GET", + "slug": "base/path/api-reference/testing/get-movie", + "title": "Get Movie", + "type": "endpoint", + }, + ], + "hidden": undefined, + "icon": undefined, + "id": "550e8400-e29b-41d4-a716-446655440000.testing", + "overviewPageId": undefined, + "pointsTo": "base/path/api-reference/testing/delete-movie", + "slug": "base/path/api-reference/testing", + "title": "Testing", + "type": "apiPackage", + }, + { + "apiDefinitionId": "550e8400-e29b-41d4-a716-446655440000", + "availability": undefined, + "children": [ + { + "apiDefinitionId": "550e8400-e29b-41d4-a716-446655440000", + "availability": undefined, + "endpointId": "endpoint_imdb.createMovie", + "hidden": undefined, + "icon": undefined, + "id": "550e8400-e29b-41d4-a716-446655440000.__package__.subpackage_imdb.subpackage_imdb.createMovie", + "isResponseStream": false, + "method": "POST", + "slug": "base/path/api-reference/imdb/create-movie", + "title": "Create Movie", + "type": "endpoint", + }, + ], + "hidden": undefined, + "icon": undefined, + "id": "550e8400-e29b-41d4-a716-446655440000.__package__.subpackage_imdb", + "overviewPageId": undefined, + "pointsTo": "base/path/api-reference/imdb/create-movie", + "slug": "base/path/api-reference/imdb", + "title": "Imdb", + "type": "apiPackage", + }, + ], + "disableLongScrolling": false, + "hidden": undefined, + "hideTitle": false, + "icon": undefined, + "id": "550e8400-e29b-41d4-a716-446655440000", + "overviewPageId": undefined, + "pointsTo": "base/path/api-reference/testing/delete-movie", + "showErrors": false, + "slug": "base/path/api-reference", + "title": "API Reference", + "type": "apiReference", +} +`; + +exports[`converts to api reference node: DELETE /movies/{id} 1`] = ` +{ + "authed": false, + "availability": undefined, + "defaultEnvironment": undefined, + "description": "Remove a movie from the database based on the ID", + "environments": [], + "errors": [], + "errorsV2": [], + "examples": [ + { + "codeExamples": { + "goSdk": undefined, + "nodeAxios": "", + "pythonSdk": undefined, + "rubySdk": undefined, + "typescriptSdk": undefined, + }, + "codeSamples": [], + "description": undefined, + "headers": {}, + "name": undefined, + "path": "/movies/:id", + "pathParameters": { + "id": ":id", + }, + "queryParameters": {}, + "requestBody": undefined, + "requestBodyV3": undefined, + "responseBody": undefined, + "responseBodyV3": undefined, + "responseStatusCode": 200, + }, + ], + "headers": [], + "id": "deleteMovie", + "method": "DELETE", + "migratedFromUrlSlugs": undefined, + "name": "Delete Movie", + "originalEndpointId": "endpoint_imdb.deleteMovie", + "path": { + "parts": [ + { + "type": "literal", + "value": "/movies", + }, + { + "type": "literal", + "value": "/", + }, + { + "type": "pathParameter", + "value": "id", + }, + { + "type": "literal", + "value": "", + }, + ], + "pathParameters": [ + { + "description": undefined, + "key": "id", + "type": { + "type": "id", + "value": "type_imdb:MovieId", + }, + }, + ], + }, + "queryParameters": [], + "request": undefined, + "response": undefined, + "snippetTemplates": undefined, + "urlSlug": "delete-movie", +} +`; + +exports[`converts to api reference node: endpointsByLocator keys 1`] = ` +[ + "POST /movies/create-movie", + "GET /movies/{id}", + "GET /movies/:id", + "DELETE /movies/{id}", + "DELETE /movies/:id", + "imdb.createMovie", + "imdb/createMovie", + "imdb.getMovie", + "imdb/getMovie", + "imdb.deleteMovie", + "imdb/deleteMovie", +] +`; + +exports[`converts to api reference node: imdb.getMovie 1`] = ` +{ + "authed": false, + "availability": undefined, + "defaultEnvironment": undefined, + "description": "Retrieve a movie from the database based on the ID", + "environments": [], + "errors": [], + "errorsV2": [ + { + "description": undefined, + "examples": [], + "name": undefined, + "statusCode": 404, + "type": { + "type": "alias", + "value": { + "type": "id", + "value": "type_imdb:MovieId", + }, + }, + }, + ], + "examples": [ + { + "codeExamples": { + "goSdk": undefined, + "nodeAxios": "", + "pythonSdk": undefined, + "rubySdk": undefined, + "typescriptSdk": undefined, + }, + "codeSamples": [], + "description": undefined, + "headers": {}, + "name": undefined, + "path": "/movies/tt0111161", + "pathParameters": { + "id": "tt0111161", + }, + "queryParameters": {}, + "requestBody": undefined, + "requestBodyV3": undefined, + "responseBody": { + "id": "tt0111161", + "rating": 9.3, + "title": "The Shawshank Redemption", + }, + "responseBodyV3": { + "type": "json", + "value": { + "id": "tt0111161", + "rating": 9.3, + "title": "The Shawshank Redemption", + }, + }, + "responseStatusCode": 200, + }, + { + "codeExamples": { + "goSdk": undefined, + "nodeAxios": "", + "pythonSdk": undefined, + "rubySdk": undefined, + "typescriptSdk": undefined, + }, + "codeSamples": [], + "description": undefined, + "headers": {}, + "name": undefined, + "path": "/movies/tt1234", + "pathParameters": { + "id": "tt1234", + }, + "queryParameters": {}, + "requestBody": undefined, + "requestBodyV3": undefined, + "responseBody": "tt1234", + "responseBodyV3": { + "type": "json", + "value": "tt1234", + }, + "responseStatusCode": 404, + }, + ], + "headers": [], + "id": "getMovie", + "method": "GET", + "migratedFromUrlSlugs": undefined, + "name": "Get Movie", + "originalEndpointId": "endpoint_imdb.getMovie", + "path": { + "parts": [ + { + "type": "literal", + "value": "/movies", + }, + { + "type": "literal", + "value": "/", + }, + { + "type": "pathParameter", + "value": "id", + }, + { + "type": "literal", + "value": "", + }, + ], + "pathParameters": [ + { + "description": undefined, + "key": "id", + "type": { + "type": "id", + "value": "type_imdb:MovieId", + }, + }, + ], + }, + "queryParameters": [], + "request": undefined, + "response": { + "statusCode": undefined, + "type": { + "type": "reference", + "value": { + "type": "id", + "value": "type_imdb:Movie", + }, + }, + }, + "snippetTemplates": undefined, + "urlSlug": "get-movie", +} +`; + +exports[`converts to api reference node: subpackagesByLocator keys 1`] = ` +[ + "__package__", + "__package__.yml", + "imdb", + "imdb.yml", +] +`; diff --git a/packages/cli/docs-resolver/src/__test__/api-resolver.test.ts b/packages/cli/docs-resolver/src/__test__/api-resolver.test.ts new file mode 100644 index 00000000000..51d5c0ce0df --- /dev/null +++ b/packages/cli/docs-resolver/src/__test__/api-resolver.test.ts @@ -0,0 +1,94 @@ +import { docsYml } from "@fern-api/configuration"; +import { FernNavigation } from "@fern-api/fdr-sdk"; +import { AbsoluteFilePath, resolve } from "@fern-api/fs-utils"; +import { generateIntermediateRepresentation } from "@fern-api/ir-generator"; +import { createMockTaskContext } from "@fern-api/task-context"; +import { loadAPIWorkspace, loadDocsWorkspace } from "@fern-api/workspace-loader"; +import { ApiDefinitionHolder } from "../ApiDefinitionHolder"; +import { ApiReferenceNodeConverter } from "../ApiReferenceNodeConverter"; +import { convertIrToApiDefinition } from "../utils/convertIrToApiDefinition"; + +const context = createMockTaskContext(); + +const apiDefinitionId = "550e8400-e29b-41d4-a716-446655440000"; + +it("converts to api reference node", async () => { + const docsWorkspace = await loadDocsWorkspace({ + fernDirectory: resolve(AbsoluteFilePath.of(__dirname), "fixtures/api-resolver/fern"), + context + }); + + if (docsWorkspace == null) { + throw new Error("Workspace is null"); + } + + const parsedDocsConfig = await docsYml.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/api-resolver/fern"), + context, + cliVersion: "0.0.0", + workspaceName: undefined, + sdkLanguage: undefined + }); + + if (!result.didSucceed) { + throw new Error("API workspace failed to load"); + } + + const apiWorkspace = result.workspace; + + if (apiWorkspace.type !== "fern") { + throw new Error("Expected fern workspace"); + } + + const slug = FernNavigation.SlugGenerator.init("/base/path"); + + const ir = await generateIntermediateRepresentation({ + workspace: apiWorkspace, + audiences: { type: "all" }, + generationLanguage: undefined, + keywords: undefined, + smartCasing: false, + disableExamples: false, + readme: undefined + }); + + const apiDefinition = convertIrToApiDefinition(ir, apiDefinitionId); + + const node = new ApiReferenceNodeConverter( + apiSection, + apiDefinition, + slug, + apiWorkspace, + docsWorkspace, + context, + new Map() + ).get(); + + expect(node).toMatchSnapshot(); + + const holder = ApiDefinitionHolder.create(apiDefinition); + + expect([...holder.endpointsByLocator.keys()]).toMatchSnapshot("endpointsByLocator keys"); + expect([...holder.subpackagesByLocator.keys()]).toMatchSnapshot("subpackagesByLocator keys"); + + expect(holder.endpointsByLocator.get("DELETE /movies/{id}")).toMatchSnapshot("DELETE /movies/{id}"); + + expect(holder.endpointsByLocator.get("imdb.getMovie")).toMatchSnapshot("imdb.getMovie"); +}); diff --git a/packages/cli/docs-resolver/src/__test__/fixtures/api-resolver/fern/definition/api.yml b/packages/cli/docs-resolver/src/__test__/fixtures/api-resolver/fern/definition/api.yml new file mode 100644 index 00000000000..79c79c049a4 --- /dev/null +++ b/packages/cli/docs-resolver/src/__test__/fixtures/api-resolver/fern/definition/api.yml @@ -0,0 +1,3 @@ +name: api +error-discrimination: + strategy: status-code diff --git a/packages/cli/docs-resolver/src/__test__/fixtures/api-resolver/fern/definition/imdb.yml b/packages/cli/docs-resolver/src/__test__/fixtures/api-resolver/fern/definition/imdb.yml new file mode 100644 index 00000000000..7a39db2dd4d --- /dev/null +++ b/packages/cli/docs-resolver/src/__test__/fixtures/api-resolver/fern/definition/imdb.yml @@ -0,0 +1,67 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json + +service: + auth: false + base-path: /movies + endpoints: + createMovie: + docs: Add a movie to the database + method: POST + path: /create-movie + request: CreateMovieRequest + response: MovieId + + getMovie: + docs: Retrieve a movie from the database based on the ID + method: GET + path: /{id} + path-parameters: + id: MovieId + response: Movie + errors: + - MovieDoesNotExistError + examples: + # Success response + - path-parameters: + id: tt0111161 + response: + body: + id: tt0111161 + title: The Shawshank Redemption + rating: 9.3 + # Error response + - path-parameters: + id: tt1234 + response: + error: MovieDoesNotExistError + body: tt1234 + + deleteMovie: + docs: Remove a movie from the database based on the ID + method: DELETE + path: /{id} + path-parameters: + id: MovieId + +types: + MovieId: + type: string + docs: The unique identifier for a Movie in the database + + Movie: + properties: + id: MovieId + title: string + rating: + type: double + docs: The rating scale out of ten stars + + CreateMovieRequest: + properties: + title: string + rating: double + +errors: + MovieDoesNotExistError: + status-code: 404 + type: MovieId diff --git a/packages/cli/docs-resolver/src/__test__/fixtures/api-resolver/fern/docs.yml b/packages/cli/docs-resolver/src/__test__/fixtures/api-resolver/fern/docs.yml new file mode 100644 index 00000000000..6c4cc08cc7f --- /dev/null +++ b/packages/cli/docs-resolver/src/__test__/fixtures/api-resolver/fern/docs.yml @@ -0,0 +1,9 @@ +instances: [] +navigation: + - api: API Reference + layout: + - testing: + title: Testing + contents: + - DELETE /movies/{id} + - GET /movies/{id} diff --git a/packages/cli/docs-resolver/src/__test__/fixtures/api-resolver/fern/fern.config.json b/packages/cli/docs-resolver/src/__test__/fixtures/api-resolver/fern/fern.config.json new file mode 100644 index 00000000000..9674da195f0 --- /dev/null +++ b/packages/cli/docs-resolver/src/__test__/fixtures/api-resolver/fern/fern.config.json @@ -0,0 +1,4 @@ +{ + "organization": "fern", + "version": "0.30.7" +} \ No newline at end of file diff --git a/packages/cli/docs-resolver/src/__test__/fixtures/api-resolver/fern/generators.yml b/packages/cli/docs-resolver/src/__test__/fixtures/api-resolver/fern/generators.yml new file mode 100644 index 00000000000..c8ae78dfffa --- /dev/null +++ b/packages/cli/docs-resolver/src/__test__/fixtures/api-resolver/fern/generators.yml @@ -0,0 +1,9 @@ +default-group: local +groups: + local: + generators: + - name: fernapi/fern-typescript-node-sdk + version: 0.9.5 + output: + location: local-file-system + path: ../sdks/typescript diff --git a/packages/cli/docs-resolver/src/convertIrToNavigation.ts b/packages/cli/docs-resolver/src/convertIrToNavigation.ts deleted file mode 100644 index c7de370b93d..00000000000 --- a/packages/cli/docs-resolver/src/convertIrToNavigation.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { docsYml } from "@fern-api/configuration"; -import { DocsV1Write } from "@fern-api/fdr-sdk"; -import { AbsoluteFilePath, dirname, relative } from "@fern-api/fs-utils"; -import { FernIr, IntermediateRepresentation } from "@fern-api/ir-sdk"; - -export function convertIrToNavigation( - ir: IntermediateRepresentation, - rootSummaryAbsolutePath: AbsoluteFilePath | undefined, - navigation: docsYml.ParsedApiNavigationItem[] | undefined, - absoluteFilepathToDocsConfig: AbsoluteFilePath, - fullSlugs: Map -): DocsV1Write.ApiNavigationConfigRoot | undefined { - if (navigation == null) { - return undefined; - } - - const defaultRoot = convertIrToDefaultNavigationConfigRoot(ir); - - const items = visitAndSortNavigationSchema( - navigation, - defaultRoot.items, - ir, - absoluteFilepathToDocsConfig, - fullSlugs - ); - - return { - items, - summaryPageId: - rootSummaryAbsolutePath == null - ? undefined - : relative(dirname(absoluteFilepathToDocsConfig), rootSummaryAbsolutePath) - }; -} - -export function convertIrToDefaultNavigationConfigRoot( - ir: IntermediateRepresentation -): DocsV1Write.ApiNavigationConfigRoot { - const items: DocsV1Write.ApiNavigationConfigItem[] = convertPackageToNavigationConfigItems(ir.rootPackage, ir); - return { items }; -} - -function convertPackageToNavigationConfigItems( - package_: FernIr.Package, - ir: IntermediateRepresentation -): DocsV1Write.ApiNavigationConfigItem[] { - if (package_.navigationConfig != null) { - const pointsToPackage = ir.subpackages[package_.navigationConfig.pointsTo]; - if (pointsToPackage != null) { - return convertPackageToNavigationConfigItems(pointsToPackage, ir); - } - } - - const items: DocsV1Write.ApiNavigationConfigItem[] = []; - - if (package_.service != null) { - const httpService = ir.services[package_.service]; - - httpService?.endpoints.forEach((endpoint) => { - items.push({ type: "endpointId", value: endpoint.name.originalName }); - }); - } - - if (package_.websocket != null) { - const channel = ir.websocketChannels?.[package_.websocket]; - if (channel != null) { - items.push({ type: "websocketId", value: channel.name.originalName }); - } - } - - if (package_.webhooks != null) { - const webhooks = ir.webhookGroups[package_.webhooks]; - - webhooks?.forEach((webhook) => { - items.push({ type: "webhookId", value: webhook.name.originalName }); - }); - } - - package_.subpackages.forEach((subpackageId) => { - const subpackage = ir.subpackages[subpackageId]; - - if (subpackage != null) { - items.push({ - type: "subpackage", - subpackageId, - items: convertPackageToNavigationConfigItems(subpackage, ir) - }); - } - }); - - return items; -} - -function createItemMatcher(key: string, ir: IntermediateRepresentation) { - return function (item: DocsV1Write.ApiNavigationConfigItem): boolean { - if (item.type === "subpackage") { - // subpackages are keyed by a generated ID, so we need to look up the original name - return ir.subpackages[item.subpackageId]?.name.originalName === key; - } else if (item.type === "page") { - return false; - } else { - // endpoints, webhooks, and websockets are keyed by their original name - return key === item.value; - } - }; -} - -function visitAndSortNavigationSchema( - navigationItems: docsYml.ParsedApiNavigationItem[], - defaultItems: DocsV1Write.ApiNavigationConfigItem[], - ir: IntermediateRepresentation, - absoluteFilepathToDocsConfig: AbsoluteFilePath, - fullSlugs: Map -): DocsV1Write.ApiNavigationConfigItem[] { - const items: DocsV1Write.ApiNavigationConfigItem[] = []; - for (const navigationItem of navigationItems) { - if (navigationItem.type === "item") { - const foundItem = defaultItems.find(createItemMatcher(navigationItem.value, ir)); - - if (foundItem != null && foundItem.type !== "subpackage") { - items.push(foundItem); - } else if (foundItem != null) { - items.push({ - type: "subpackage", - subpackageId: foundItem.subpackageId, - items: [] - }); - } - } else if (navigationItem.type === "page") { - items.push({ - type: "page", - id: relative(dirname(absoluteFilepathToDocsConfig), navigationItem.absolutePath), - title: navigationItem.title, - icon: undefined, - hidden: undefined, - urlSlugOverride: navigationItem.slug, - fullSlug: fullSlugs.get(navigationItem.absolutePath)?.split("/") - }); - } else { - // item must be a collection of subpackages - const foundItem = defaultItems.find(createItemMatcher(navigationItem.subpackageId, ir)); - - if (foundItem != null && foundItem.type === "subpackage") { - items.push({ - type: "subpackage", - subpackageId: foundItem.subpackageId, - summaryPageId: - navigationItem.summaryAbsolutePath == null - ? undefined - : relative(dirname(absoluteFilepathToDocsConfig), navigationItem.summaryAbsolutePath), - items: visitAndSortNavigationSchema( - navigationItem.items, - foundItem.items, - ir, - absoluteFilepathToDocsConfig, - fullSlugs - ) - }); - } - } - } - - return items; -} diff --git a/packages/cli/docs-resolver/src/__test__/__snapshots__/parseImagePaths.test.ts.snap b/packages/cli/docs-resolver/src/utils/__test__/__snapshots__/parseImagePaths.test.ts.snap similarity index 100% rename from packages/cli/docs-resolver/src/__test__/__snapshots__/parseImagePaths.test.ts.snap rename to packages/cli/docs-resolver/src/utils/__test__/__snapshots__/parseImagePaths.test.ts.snap diff --git a/packages/cli/docs-resolver/src/__test__/extractDatetimeFromChangelogTitle.test.ts b/packages/cli/docs-resolver/src/utils/__test__/extractDatetimeFromChangelogTitle.test.ts similarity index 100% rename from packages/cli/docs-resolver/src/__test__/extractDatetimeFromChangelogTitle.test.ts rename to packages/cli/docs-resolver/src/utils/__test__/extractDatetimeFromChangelogTitle.test.ts diff --git a/packages/cli/docs-resolver/src/__test__/fixtures/bland.mdx b/packages/cli/docs-resolver/src/utils/__test__/fixtures/bland.mdx similarity index 100% rename from packages/cli/docs-resolver/src/__test__/fixtures/bland.mdx rename to packages/cli/docs-resolver/src/utils/__test__/fixtures/bland.mdx diff --git a/packages/cli/docs-resolver/src/__test__/fixtures/hume.mdx b/packages/cli/docs-resolver/src/utils/__test__/fixtures/hume.mdx similarity index 100% rename from packages/cli/docs-resolver/src/__test__/fixtures/hume.mdx rename to packages/cli/docs-resolver/src/utils/__test__/fixtures/hume.mdx diff --git a/packages/cli/docs-resolver/src/__test__/fixtures/multimedia-file.mdx b/packages/cli/docs-resolver/src/utils/__test__/fixtures/multimedia-file.mdx similarity index 100% rename from packages/cli/docs-resolver/src/__test__/fixtures/multimedia-file.mdx rename to packages/cli/docs-resolver/src/utils/__test__/fixtures/multimedia-file.mdx diff --git a/packages/cli/docs-resolver/src/__test__/fixtures/zep.mdx b/packages/cli/docs-resolver/src/utils/__test__/fixtures/zep.mdx similarity index 100% rename from packages/cli/docs-resolver/src/__test__/fixtures/zep.mdx rename to packages/cli/docs-resolver/src/utils/__test__/fixtures/zep.mdx diff --git a/packages/cli/docs-resolver/src/__test__/parseImagePaths.test.ts b/packages/cli/docs-resolver/src/utils/__test__/parseImagePaths.test.ts similarity index 100% rename from packages/cli/docs-resolver/src/__test__/parseImagePaths.test.ts rename to packages/cli/docs-resolver/src/utils/__test__/parseImagePaths.test.ts diff --git a/packages/cli/docs-resolver/src/__test__/replaceReferencedMarkdown.test.ts b/packages/cli/docs-resolver/src/utils/__test__/replaceReferencedMarkdown.test.ts similarity index 100% rename from packages/cli/docs-resolver/src/__test__/replaceReferencedMarkdown.test.ts rename to packages/cli/docs-resolver/src/utils/__test__/replaceReferencedMarkdown.test.ts diff --git a/packages/cli/docs-resolver/src/convertDocsSnippetsConfigToFdr.ts b/packages/cli/docs-resolver/src/utils/convertDocsSnippetsConfigToFdr.ts similarity index 100% rename from packages/cli/docs-resolver/src/convertDocsSnippetsConfigToFdr.ts rename to packages/cli/docs-resolver/src/utils/convertDocsSnippetsConfigToFdr.ts diff --git a/packages/cli/docs-resolver/src/utils/convertIrToApiDefinition.ts b/packages/cli/docs-resolver/src/utils/convertIrToApiDefinition.ts new file mode 100644 index 00000000000..bf76b611c3e --- /dev/null +++ b/packages/cli/docs-resolver/src/utils/convertIrToApiDefinition.ts @@ -0,0 +1,21 @@ +import { APIV1Read, convertAPIDefinitionToDb, convertDbAPIDefinitionToRead, SDKSnippetHolder } from "@fern-api/fdr-sdk"; +import { IntermediateRepresentation } from "@fern-api/ir-sdk"; +import { convertIrToFdrApi } from "@fern-api/register"; + +const EMPTY_SNIPPET_HOLDER = new SDKSnippetHolder({ + snippetsBySdkId: {}, + snippetsConfigWithSdkId: {}, + snippetTemplatesByEndpoint: {}, + snippetsBySdkIdAndEndpointId: {}, + snippetTemplatesByEndpointId: {} +}); + +export function convertIrToApiDefinition( + ir: IntermediateRepresentation, + apiDefinitionId: string +): APIV1Read.ApiDefinition { + // the navigation constructor doesn't need to know about snippets, so we can pass an empty object + return convertDbAPIDefinitionToRead( + convertAPIDefinitionToDb(convertIrToFdrApi({ ir, snippetsConfig: {} }), apiDefinitionId, EMPTY_SNIPPET_HOLDER) + ); +} diff --git a/packages/cli/docs-resolver/src/extractDatetimeFromChangelogTitle.ts b/packages/cli/docs-resolver/src/utils/extractDatetimeFromChangelogTitle.ts similarity index 100% rename from packages/cli/docs-resolver/src/extractDatetimeFromChangelogTitle.ts rename to packages/cli/docs-resolver/src/utils/extractDatetimeFromChangelogTitle.ts diff --git a/packages/cli/docs-resolver/src/getImageFilepathsToUpload.ts b/packages/cli/docs-resolver/src/utils/getImageFilepathsToUpload.ts similarity index 100% rename from packages/cli/docs-resolver/src/getImageFilepathsToUpload.ts rename to packages/cli/docs-resolver/src/utils/getImageFilepathsToUpload.ts diff --git a/packages/cli/docs-resolver/src/utils/isSubpackage.ts b/packages/cli/docs-resolver/src/utils/isSubpackage.ts new file mode 100644 index 00000000000..2e795649cb4 --- /dev/null +++ b/packages/cli/docs-resolver/src/utils/isSubpackage.ts @@ -0,0 +1,5 @@ +import { APIV1Read } from "@fern-api/fdr-sdk"; + +export function isSubpackage(package_: APIV1Read.ApiDefinitionPackage): package_ is APIV1Read.ApiDefinitionSubpackage { + return typeof (package_ as APIV1Read.ApiDefinitionSubpackage).subpackageId === "string"; +} diff --git a/packages/cli/docs-resolver/src/parseImagePaths.ts b/packages/cli/docs-resolver/src/utils/parseImagePaths.ts similarity index 100% rename from packages/cli/docs-resolver/src/parseImagePaths.ts rename to packages/cli/docs-resolver/src/utils/parseImagePaths.ts diff --git a/packages/cli/docs-resolver/src/replaceReferencedMarkdown.ts b/packages/cli/docs-resolver/src/utils/replaceReferencedMarkdown.ts similarity index 100% rename from packages/cli/docs-resolver/src/replaceReferencedMarkdown.ts rename to packages/cli/docs-resolver/src/utils/replaceReferencedMarkdown.ts diff --git a/packages/cli/docs-resolver/src/utils/stringifyEndpointPathParts.ts b/packages/cli/docs-resolver/src/utils/stringifyEndpointPathParts.ts new file mode 100644 index 00000000000..8fa8bc4400b --- /dev/null +++ b/packages/cli/docs-resolver/src/utils/stringifyEndpointPathParts.ts @@ -0,0 +1,16 @@ +import { APIV1Read } from "@fern-api/fdr-sdk"; + +/** + * Converts an array of endpoint path parts into a string. + * @param path The array of endpoint path parts. + * @returns The string representation of the endpoint path parts. + */ +export function stringifyEndpointPathParts(path: APIV1Read.EndpointPathPart[]): string { + return path.map((part) => (part.type === "literal" ? part.value : `{${part.value}}`)).join(""); +} + +// this is a different version of the function that uses colons instead of curly braces +// however, this should not be used except for maintaining backwards compatibility +export function stringifyEndpointPathParts2(path: APIV1Read.EndpointPathPart[]): string { + return path.map((part) => (part.type === "literal" ? part.value : `:${part.value}`)).join(""); +} diff --git a/packages/cli/docs-resolver/tsconfig.json b/packages/cli/docs-resolver/tsconfig.json index b4e5d69d34c..077356fa99e 100644 --- a/packages/cli/docs-resolver/tsconfig.json +++ b/packages/cli/docs-resolver/tsconfig.json @@ -8,6 +8,7 @@ { "path": "../../ir-sdk" }, { "path": "../configuration" }, { "path": "../generation/ir-generator" }, + { "path": "../register" }, { "path": "../task-context" }, { "path": "../workspace-loader" } ] 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 232e79e4254..d21255940a9 100644 --- a/packages/cli/generation/remote-generation/remote-workspace-runner/package.json +++ b/packages/cli/generation/remote-generation/remote-workspace-runner/package.json @@ -32,7 +32,7 @@ "@fern-api/core": "workspace:*", "@fern-api/core-utils": "workspace:*", "@fern-api/docs-resolver": "workspace:*", - "@fern-api/fdr-sdk": "0.97.0-bec92c652", + "@fern-api/fdr-sdk": "0.98.0-rc1", "@fern-api/fs-utils": "workspace:*", "@fern-api/ir-generator": "workspace:*", "@fern-api/ir-migrations": "workspace:*", diff --git a/packages/cli/register/package.json b/packages/cli/register/package.json index 4e3f3fedf31..460cc8c8b6e 100644 --- a/packages/cli/register/package.json +++ b/packages/cli/register/package.json @@ -31,7 +31,7 @@ "@fern-api/configuration": "workspace:*", "@fern-api/core": "workspace:*", "@fern-api/core-utils": "workspace:*", - "@fern-api/fdr-sdk": "0.97.0-bec92c652", + "@fern-api/fdr-sdk": "0.98.0-rc1", "@fern-api/ir-generator": "workspace:*", "@fern-api/ir-sdk": "workspace:*", "@fern-api/task-context": "workspace:*", diff --git a/packages/core/package.json b/packages/core/package.json index d25d230e12f..3657ad30558 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -27,7 +27,7 @@ "depcheck": "depcheck" }, "dependencies": { - "@fern-api/fdr-sdk": "0.97.0-bec92c652", + "@fern-api/fdr-sdk": "0.98.0-rc1", "@fern-api/venus-api-sdk": "0.0.38", "@fern-fern/fdr-test-sdk": "^0.0.5297", "@fern-fern/fiddle-sdk": "^0.0.552" diff --git a/yarn.lock b/yarn.lock index 92a9729307c..5d43702a802 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3450,7 +3450,7 @@ __metadata: resolution: "@fern-api/configuration@workspace:packages/cli/configuration" dependencies: "@fern-api/core-utils": "workspace:*" - "@fern-api/fdr-sdk": 0.97.0-bec92c652 + "@fern-api/fdr-sdk": 0.98.0-rc1 "@fern-api/fs-utils": "workspace:*" "@fern-api/task-context": "workspace:*" "@fern-fern/fiddle-sdk": ^0.0.552 @@ -3497,7 +3497,7 @@ __metadata: version: 0.0.0-use.local resolution: "@fern-api/core@workspace:packages/core" dependencies: - "@fern-api/fdr-sdk": 0.97.0-bec92c652 + "@fern-api/fdr-sdk": 0.98.0-rc1 "@fern-api/venus-api-sdk": 0.0.38 "@fern-fern/fdr-test-sdk": ^0.0.5297 "@fern-fern/fiddle-sdk": ^0.0.552 @@ -3559,7 +3559,7 @@ __metadata: resolution: "@fern-api/docs-preview@workspace:packages/cli/docs-preview" dependencies: "@fern-api/docs-resolver": "workspace:*" - "@fern-api/fdr-sdk": 0.97.0-bec92c652 + "@fern-api/fdr-sdk": 0.98.0-rc1 "@fern-api/fs-utils": "workspace:*" "@fern-api/ir-sdk": "workspace:*" "@fern-api/logger": "workspace:*" @@ -3599,16 +3599,18 @@ __metadata: dependencies: "@fern-api/configuration": "workspace:*" "@fern-api/core-utils": "workspace:*" - "@fern-api/fdr-sdk": 0.97.0-bec92c652 + "@fern-api/fdr-sdk": 0.98.0-rc1 "@fern-api/fs-utils": "workspace:*" "@fern-api/ir-generator": "workspace:*" "@fern-api/ir-sdk": "workspace:*" + "@fern-api/register": "workspace:*" "@fern-api/task-context": "workspace:*" "@fern-api/workspace-loader": "workspace:*" "@types/diff": ^5.2.1 "@types/jest": ^29.0.3 "@types/lodash-es": ^4.17.12 "@types/node": ^18.7.18 + dayjs: ^1.11.11 depcheck: ^1.4.6 diff: ^5.2.0 eslint: ^8.56.0 @@ -3679,19 +3681,21 @@ __metadata: languageName: unknown linkType: soft -"@fern-api/fdr-sdk@npm:0.97.0-bec92c652": - version: 0.97.0-bec92c652 - resolution: "@fern-api/fdr-sdk@npm:0.97.0-bec92c652" +"@fern-api/fdr-sdk@npm:0.98.0-rc1": + version: 0.98.0-rc1 + resolution: "@fern-api/fdr-sdk@npm:0.98.0-rc1" dependencies: dayjs: ^1.11.11 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 url-join: 4.0.1 - checksum: 32a938be2d9fb770dbd9498b06b5598822ce4275ad36d40efec58ac27ad1b15f12fca332169ab18745365d055102c679df484972432f905fac9c1406484202a0 + checksum: 5178c9e2454f19640580e49b22eccb7b95e1333935155e6d0eea4c9ee7c59174a44e7aed71ac262667d8e6ab7989582ba0ea5617665d737bb69a63d156e9a94e languageName: node linkType: hard @@ -4335,7 +4339,7 @@ __metadata: "@fern-api/configuration": "workspace:*" "@fern-api/core": "workspace:*" "@fern-api/core-utils": "workspace:*" - "@fern-api/fdr-sdk": 0.97.0-bec92c652 + "@fern-api/fdr-sdk": 0.98.0-rc1 "@fern-api/ir-generator": "workspace:*" "@fern-api/ir-sdk": "workspace:*" "@fern-api/task-context": "workspace:*" @@ -4362,7 +4366,7 @@ __metadata: "@fern-api/core": "workspace:*" "@fern-api/core-utils": "workspace:*" "@fern-api/docs-resolver": "workspace:*" - "@fern-api/fdr-sdk": 0.97.0-bec92c652 + "@fern-api/fdr-sdk": 0.98.0-rc1 "@fern-api/fs-utils": "workspace:*" "@fern-api/ir-generator": "workspace:*" "@fern-api/ir-migrations": "workspace:*"