mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 10:25:06 +03:00
## What kind of change does this PR introduce? follow-up to #50235 to reduce reference page payloads and cold rendering overhead ## What is the current behavior? reference pages ship a large rsc payload inside the html _ most of it is duplication rather than content along with shiki that writes ~30 character css variable name for every syntax token making the page heavy in some cases ## What is the new behavior? - moves repeated styles into shared css and uses compact, namespaced token classes - renders details icons inside the client trigger - follows shiki’s guidance to [reuse one highlighter](https://shiki.style/guide/best-performance#cache-the-highlighter-instance) and [load languages on demand](https://shiki.style/guide/best-performance#use-shorthands) `page size` page | before | after | change -- | -- | -- | -- javascript | 10.61 mb | 8.28 mb | -21.9% dart | 4.59 mb | 4.13 mb | -10.1% python | 4.38 mb | 3.73 mb | -14.9% swift | 2.87 mb | 2.56 mb | -10.9% server | 1.59 mb | 1.35 mb | -15.2% kotlin | 3.42 mb | 3.17 mb | -7.2% `cold initialization` language | before | after | reduction -- | -- | -- | -- bash | 2,180 ms | 23 ms | 98.95% javascript | 2,245 ms | 38 ms | 98.29% ## Additional context measured on a local production build which uses the checked in generated content _ production has larger sdk data, so absolute sizes there will be higher <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added reusable expand/collapse controls for API reference details, with updated icons, labels, and styling. * Improved code block rendering with class-based syntax highlighting, wrapped-code support, responsive layouts, and lazy language loading. * **Style** * Added theme-aware syntax-token colors, line-number styling, and configurable code-block shadows. * Consolidated expandable reference panel and item styling. * **Tests** * Added coverage for syntax highlighting, code block rendering, language support, token stability, and reference details. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
903 lines
29 KiB
TypeScript
903 lines
29 KiB
TypeScript
import ApiSchema from '~/components/ApiSchema'
|
|
import { MDXRemoteBase } from '~/features/docs/MdxBase'
|
|
import { MDXRemoteRefs } from '~/features/docs/Reference.mdx'
|
|
import type {
|
|
CustomTypePropertyType,
|
|
FunctionParameterType,
|
|
MethodTypes,
|
|
TypeDetails,
|
|
} from '~/features/docs/Reference.typeSpec'
|
|
import { TYPESPEC_NODE_ANONYMOUS } from '~/features/docs/Reference.typeSpec'
|
|
import { DetailsTrigger, ReferenceSectionWrapper } from '~/features/docs/Reference.ui.client'
|
|
import { normalizeMarkdown } from '~/features/docs/Reference.utils'
|
|
import { isEqual } from 'lodash-es'
|
|
import { ChevronRight } from 'lucide-react'
|
|
import { fromMarkdown } from 'mdast-util-from-markdown'
|
|
import type { HTMLAttributes, PropsWithChildren } from 'react'
|
|
import ReactMarkdown from 'react-markdown'
|
|
import { Badge, cn, Collapsible, CollapsibleContent, CollapsibleTrigger } from 'ui'
|
|
|
|
import { getTypeDisplayFromSchema, IApiEndPoint, type ISchema } from './Reference.api.utils'
|
|
import { API_REFERENCE_REQUEST_BODY_SCHEMA_DATA_ATTRIBUTES } from './Reference.ui.shared'
|
|
|
|
interface SectionProps extends PropsWithChildren {
|
|
link: string
|
|
slug?: string
|
|
columns?: 'single' | 'double'
|
|
}
|
|
|
|
function Section({ slug, link, columns = 'single', children }: SectionProps) {
|
|
const singleColumn = columns === 'single'
|
|
|
|
return (
|
|
<ReferenceSectionWrapper
|
|
id={slug || ''}
|
|
link={link}
|
|
className={cn(
|
|
'grid grid-cols-[1fr] gap-x-16 gap-y-8',
|
|
singleColumn ? 'max-w-3xl' : '@4xl/article:grid-cols-[1fr_1fr]'
|
|
)}
|
|
>
|
|
{children}
|
|
</ReferenceSectionWrapper>
|
|
)
|
|
}
|
|
|
|
function Details({ children }: PropsWithChildren) {
|
|
/*
|
|
* `min-w` is necessary because these are used as grid children, which have
|
|
* default `min-w-auto`
|
|
*/
|
|
return <div className="w-full min-w-full">{children}</div>
|
|
}
|
|
|
|
function Examples({ children }: PropsWithChildren) {
|
|
/*
|
|
* `min-w` is necessary because these are used as grid children, which have
|
|
* default `min-w-auto`
|
|
*/
|
|
return <div className="w-full min-w-full sticky top-32 self-start">{children}</div>
|
|
}
|
|
|
|
function EducationSection({ children, slug, ...props }: SectionProps) {
|
|
return (
|
|
<ReferenceSectionWrapper id={slug || ''} className={'prose max-w-none'} {...props}>
|
|
{children}
|
|
</ReferenceSectionWrapper>
|
|
)
|
|
}
|
|
|
|
interface EducationRowProps extends PropsWithChildren {
|
|
className?: string
|
|
}
|
|
|
|
function EducationRow({ className, children }: EducationRowProps) {
|
|
return <div className={cn('grid lg:grid-cols-2 gap-8 lg:gap-16', className)}>{children}</div>
|
|
}
|
|
|
|
export const RefSubLayout = {
|
|
Section,
|
|
EducationSection,
|
|
EducationRow,
|
|
Details,
|
|
Examples,
|
|
}
|
|
|
|
interface StickyHeaderProps {
|
|
title?: React.ReactNode | string
|
|
monoFont?: boolean
|
|
className?: string
|
|
}
|
|
|
|
export function StickyHeader({ title, monoFont = false, className }: StickyHeaderProps) {
|
|
return (
|
|
<h2
|
|
tabIndex={-1} // For programmatic focus on auto-scroll to section
|
|
className={cn(
|
|
'sticky top-0 z-1',
|
|
'w-full',
|
|
// Enough padding to cover the background when stuck to the top,
|
|
// then readjust with negative margin to prevent it looking too
|
|
// spaced-out in regular position
|
|
'pt-[calc(var(--header-height)+1rem)] -mt-[calc(var(--header-height)+1rem-2px)]',
|
|
// Same for bottom
|
|
'pb-8 -mb-3',
|
|
'bg-linear-to-b from-background from-85% to-transparent to-100%',
|
|
'text-2xl font-medium text-foreground',
|
|
'scroll-mt-[calc(var(--header-height)+1rem)]',
|
|
'focus:outline-hidden',
|
|
monoFont && 'font-mono',
|
|
className
|
|
)}
|
|
>
|
|
{title}
|
|
</h2>
|
|
)
|
|
}
|
|
|
|
export function CollapsibleDetails({ title, content }: { title: string; content: string }) {
|
|
const blocks = fromMarkdown(content).children
|
|
const isCodeOnly = blocks.length === 1 && blocks[0].type === 'code'
|
|
|
|
return (
|
|
<Collapsible
|
|
className={cn(
|
|
'overflow-hidden',
|
|
'border border-default rounded-lg bg-surface-100',
|
|
'has-[:focus-visible]:outline-solid has-[:focus-visible]:outline-2',
|
|
'has-[:focus-visible]:outline-offset-[-2px] has-[:focus-visible]:outline-[var(--ring)]'
|
|
)}
|
|
>
|
|
<CollapsibleTrigger
|
|
className={cn(
|
|
'group/trigger',
|
|
'w-full min-h-8',
|
|
'px-2 py-1.5',
|
|
'flex items-center gap-2',
|
|
'text-xs text-foreground-light',
|
|
'cursor-pointer hover:bg-surface-200 hover:text-foreground',
|
|
'focus-visible:outline-none',
|
|
'transition-[background-color,color] duration-150 ease-out'
|
|
)}
|
|
>
|
|
{title}
|
|
<ChevronRight
|
|
size={12}
|
|
strokeWidth={2}
|
|
aria-hidden
|
|
className="ms-auto shrink-0 text-foreground-muted group-data-open/trigger:rotate-90 transition-transform duration-200 ease-out motion-reduce:transition-none"
|
|
/>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent
|
|
className={cn(
|
|
'overflow-hidden',
|
|
'data-open:animate-collapsible-down data-closed:animate-collapsible-up',
|
|
'motion-reduce:animate-none'
|
|
)}
|
|
>
|
|
<div
|
|
className={cn(
|
|
'border-t border-default',
|
|
'prose max-w-none text-sm',
|
|
!isCodeOnly && 'px-4 py-3 [&_:where(p,li)]:text-sm [&_:where(p,li)]:leading-6'
|
|
)}
|
|
>
|
|
<MDXRemoteRefs
|
|
source={content}
|
|
codeBlockProps={isCodeOnly ? { compact: true } : undefined}
|
|
/>
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
)
|
|
}
|
|
|
|
export function FnParameterDetails({
|
|
parameters,
|
|
altParameters,
|
|
className,
|
|
}: {
|
|
parameters: Array<object> | undefined
|
|
altParameters?: Array<Array<FunctionParameterType>>
|
|
className?: string
|
|
}) {
|
|
if (!parameters || parameters.length === 0) return
|
|
|
|
const combinedParameters = altParameters
|
|
? mergeAlternateParameters(parameters, altParameters)
|
|
: parameters
|
|
|
|
return (
|
|
<div className={className ?? ''}>
|
|
<h3 className="mb-3 text-base text-foreground">Parameters</h3>
|
|
<ul>
|
|
{combinedParameters.map((parameter, index) => (
|
|
<li key={index} className="border-t last-of-type:border-b py-5 flex flex-col gap-3">
|
|
<ParamOrTypeDetails paramOrType={parameter} />
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
interface SubContent {
|
|
name: string
|
|
isOptional?: boolean | 'NA' // not applicable
|
|
type?: string
|
|
description?: string
|
|
subContent: Array<SubContent>
|
|
}
|
|
|
|
function ParamOrTypeDetails({ paramOrType }: { paramOrType: object }) {
|
|
if (!('name' in paramOrType)) return
|
|
|
|
const description: string =
|
|
'description' in paramOrType
|
|
? (paramOrType.description as string)
|
|
: isFromTypespec(paramOrType)
|
|
? (paramOrType.comment?.shortText ?? '')
|
|
: ''
|
|
|
|
const subContent =
|
|
'subContent' in paramOrType
|
|
? (paramOrType.subContent as Array<SubContent>)
|
|
: isFromTypespec(paramOrType)
|
|
? getSubDetails(paramOrType)
|
|
: undefined
|
|
|
|
const defaultOpen = isDefaultExpanded(paramOrType)
|
|
|
|
return (
|
|
<>
|
|
<div className="flex flex-wrap items-baseline gap-3">
|
|
<span className="font-mono text-sm font-medium text-foreground">
|
|
{paramOrType.name === TYPESPEC_NODE_ANONYMOUS
|
|
? '[Anonymous]'
|
|
: (paramOrType.name as string)}
|
|
</span>
|
|
{'isOptional' in paramOrType && paramOrType.isOptional === true ? (
|
|
<Badge variant="default">Optional</Badge>
|
|
) : 'isOptional' in paramOrType && paramOrType.isOptional === false ? (
|
|
<Badge variant="warning">Required</Badge>
|
|
) : null}
|
|
{/* @ts-ignore */}
|
|
{paramOrType?.comment?.tags?.some((tag) => tag.tag === 'deprecated') && (
|
|
<span className="text-xs text-warning">Deprecated</span>
|
|
)}
|
|
<span className="text-xs text-foreground-muted">{getTypeName(paramOrType)}</span>
|
|
</div>
|
|
{description && (
|
|
<div className="prose text-sm">
|
|
<MDXRemoteBase source={description} customPreprocess={normalizeMarkdown} />
|
|
</div>
|
|
)}
|
|
{subContent && subContent.length > 0 && (
|
|
<TypeSubDetails details={subContent} defaultOpen={defaultOpen || false} />
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
export function ReturnTypeDetails({ returnType }: { returnType: MethodTypes['ret'] }) {
|
|
// These custom names that aren't defined aren't particularly meaningful, so
|
|
// just don't display them.
|
|
const isNameOnlyType = returnType?.type?.type === 'nameOnly'
|
|
if (isNameOnlyType) return
|
|
|
|
const subContent = getSubDetails(returnType)
|
|
const isDefaultOpen = returnType ? isDefaultExpanded(returnType) : false
|
|
|
|
return (
|
|
<div>
|
|
<h3 className="mb-3 text-base text-foreground">Return Type</h3>
|
|
<div className="border-t border-b py-5 flex flex-col gap-3">
|
|
<div className="text-xs text-foreground-muted">
|
|
{returnType ? getTypeName(returnType) : ''}
|
|
</div>
|
|
{returnType?.comment?.shortText && (
|
|
<div className="prose text-sm">
|
|
<MDXRemoteBase
|
|
source={returnType?.comment?.shortText}
|
|
customPreprocess={normalizeMarkdown}
|
|
/>
|
|
</div>
|
|
)}
|
|
{subContent && subContent.length > 0 && (
|
|
<TypeSubDetails defaultOpen={isDefaultOpen || false} details={subContent} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function TypeSubDetails({
|
|
details,
|
|
className,
|
|
defaultOpen = false,
|
|
}: {
|
|
details: Array<SubContent> | Array<CustomTypePropertyType> | Array<TypeDetails>
|
|
className?: string
|
|
defaultOpen?: boolean
|
|
}) {
|
|
return (
|
|
<Collapsible defaultOpen={defaultOpen}>
|
|
<DetailsTrigger label="Details" className={className} />
|
|
<CollapsibleContent>
|
|
<ul className="reference-details-panel">
|
|
{details.map(
|
|
(detail: SubContent | CustomTypePropertyType | TypeDetails, index: number) => (
|
|
<li key={index} className="reference-details-item">
|
|
<ParamOrTypeDetails paramOrType={detail} />
|
|
</li>
|
|
)
|
|
)}
|
|
</ul>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
)
|
|
}
|
|
|
|
export function ApiSchemaParamDetails({ param }: { param: IApiEndPoint['parameters'][number] }) {
|
|
return (
|
|
<li className="border-t last-of-type:border-b py-5 flex flex-col gap-3">
|
|
<div className="flex flex-wrap items-baseline gap-3">
|
|
<span className="font-mono text-sm font-medium text-foreground break-all">
|
|
{param.name}
|
|
</span>
|
|
{param.required ? (
|
|
<Badge variant="warning">Required</Badge>
|
|
) : (
|
|
<Badge variant="default">Optional</Badge>
|
|
)}
|
|
{param.schema?.deprecated && <span className="text-xs text-warning">Deprecated</span>}
|
|
{param.schema && (
|
|
<span className="text-xs text-foreground-muted">
|
|
{getTypeDisplayFromSchema(param.schema)?.displayName ?? ''}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{param.description && (
|
|
<div className="prose wrap-break-word text-sm">
|
|
<ReactMarkdown>{param.description}</ReactMarkdown>
|
|
</div>
|
|
)}
|
|
{param.schema && <ApiSchemaParamSubdetails schema={param.schema} />}
|
|
</li>
|
|
)
|
|
}
|
|
|
|
export function ApiOperationRequestBodyDetails({
|
|
requestBody,
|
|
}: {
|
|
requestBody: IApiEndPoint['requestBody']
|
|
}) {
|
|
const availableSchemes = Object.keys(requestBody?.content || {}) as Array<
|
|
'application/json' | 'application/x-www-form-urlencoded'
|
|
>
|
|
|
|
return (
|
|
<>
|
|
{availableSchemes.map((scheme, index) => (
|
|
<ApiOperationRequestBodyDetailsInternal
|
|
key={index}
|
|
schema={requestBody?.content?.[scheme]?.schema || ({} as ISchema)}
|
|
hidden={index > 0}
|
|
{...{
|
|
[API_REFERENCE_REQUEST_BODY_SCHEMA_DATA_ATTRIBUTES.KEY]: scheme,
|
|
}}
|
|
/>
|
|
))}
|
|
</>
|
|
)
|
|
}
|
|
|
|
interface ApiOperationRequestBodyDetailsInternalProps extends HTMLAttributes<HTMLUListElement> {
|
|
schema: ISchema
|
|
}
|
|
|
|
// Some specs have allOf/anyOf/oneOf as a single schema object instead of an
|
|
// array. Wrap it so it still renders instead of crashing or disappearing.
|
|
function asSchemaArray(value: unknown): Array<any> {
|
|
if (Array.isArray(value)) return value
|
|
if (value && typeof value === 'object') return [value]
|
|
return []
|
|
}
|
|
|
|
function ApiOperationRequestBodyDetailsInternal({
|
|
schema,
|
|
...props
|
|
}: ApiOperationRequestBodyDetailsInternalProps) {
|
|
if ('allOf' in schema) {
|
|
return (
|
|
<>
|
|
<span className="font-mono text-sm font-medium text-foreground">All of the following:</span>
|
|
{asSchemaArray(schema.allOf).map((option, index) => (
|
|
<ApiSchemaParamSubdetails key={index} schema={option} />
|
|
))}
|
|
</>
|
|
)
|
|
} else if ('anyOf' in schema) {
|
|
return (
|
|
<>
|
|
<span className="font-mono text-sm font-medium text-foreground">Any of the following:</span>
|
|
{asSchemaArray(schema.anyOf).map((option, index) => (
|
|
<ApiSchemaParamSubdetails key={index} schema={option} />
|
|
))}
|
|
</>
|
|
)
|
|
} else if ('oneOf' in schema) {
|
|
return (
|
|
<>
|
|
<span className="font-mono text-sm font-medium text-foreground">One of the following:</span>
|
|
{asSchemaArray(schema.oneOf).map((option, index) => (
|
|
<ApiSchemaParamSubdetails key={index} schema={option} />
|
|
))}
|
|
</>
|
|
)
|
|
} else if ('enum' in schema) {
|
|
return (
|
|
<span className="font-mono text-sm font-medium text-foreground">
|
|
{schema.enum.join(' | ')}
|
|
</span>
|
|
)
|
|
} else if (
|
|
schema.type === 'string' ||
|
|
schema.type === 'boolean' ||
|
|
schema.type === 'number' ||
|
|
schema.type === 'integer'
|
|
) {
|
|
return <span className="font-mono text-sm font-medium text-foreground">{schema.type}</span>
|
|
} else if (schema.type === 'array') {
|
|
const itemTypeDisplay = getTypeDisplayFromSchema(schema.items)
|
|
const displayName = itemTypeDisplay?.displayName ?? 'unknown'
|
|
return (
|
|
<>
|
|
<span className="font-mono text-sm font-medium text-foreground">{`Array of ${displayName}`}</span>
|
|
{schema.items &&
|
|
!(
|
|
'type' in schema.items &&
|
|
['string', 'boolean', 'number', 'integer'].includes(schema.items.type)
|
|
) && <ApiSchemaParamSubdetails className="mt-4" schema={schema.items} />}
|
|
</>
|
|
)
|
|
} else if (schema.type === 'object') {
|
|
return (
|
|
<ul {...props}>
|
|
{Object.keys(schema.properties || {})
|
|
.map((property) => ({
|
|
name: property,
|
|
required: schema.required?.includes(property) || false,
|
|
in: 'body' as const,
|
|
schema: schema.properties?.[property] || ({} as ISchema),
|
|
}))
|
|
.map((property, index) => (
|
|
<ApiSchemaParamDetails key={index} param={property} />
|
|
))}
|
|
</ul>
|
|
)
|
|
}
|
|
}
|
|
|
|
export function ApiSchemaParamSubdetails({
|
|
schema,
|
|
className,
|
|
}: {
|
|
schema: ISchema
|
|
className?: string
|
|
}) {
|
|
if (
|
|
!('enum' in schema) &&
|
|
'type' in schema &&
|
|
(schema.type === 'boolean' ||
|
|
((schema.type === 'number' || schema.type === 'integer') &&
|
|
!('minimum' in schema || 'maximum' in schema)) ||
|
|
(schema.type === 'string' &&
|
|
!('minLength' in schema || 'maxLength' in schema || 'pattern' in schema)) ||
|
|
(schema.type === 'array' &&
|
|
schema.items &&
|
|
'type' in schema.items &&
|
|
['boolean', 'number', 'integer', 'string', 'file'].includes(schema.items.type)))
|
|
) {
|
|
return null
|
|
}
|
|
|
|
const rawSubContent =
|
|
'enum' in schema
|
|
? schema.enum
|
|
: 'anyOf' in schema
|
|
? schema.anyOf
|
|
: 'oneOf' in schema
|
|
? schema.oneOf
|
|
: 'allOf' in schema
|
|
? schema.allOf
|
|
: 'type' in schema && schema.type === 'string'
|
|
? ['minLength', 'maxLength', 'pattern']
|
|
.filter((key) => key in schema)
|
|
.map((key) => ({
|
|
constraint: key,
|
|
value: schema[key],
|
|
}))
|
|
: 'type' in schema && (schema.type === 'number' || schema.type === 'integer')
|
|
? ['minimum', 'maximum']
|
|
.filter((key) => key in schema)
|
|
.map((key) => ({
|
|
constraint: key,
|
|
value: schema[key],
|
|
}))
|
|
: []
|
|
|
|
const subContent = asSchemaArray(rawSubContent)
|
|
|
|
return (
|
|
<Collapsible>
|
|
<DetailsTrigger label={schemaDetailsLabel(schema)} className={className} />
|
|
<CollapsibleContent>
|
|
{'type' in schema && schema.type === 'object' ? (
|
|
<div className="reference-details-panel">
|
|
<div className="p-5 border-b border-default">
|
|
<ApiSchema schema={schema} />
|
|
</div>
|
|
<ApiOperationRequestBodyDetailsInternal schema={schema} className="px-5" />
|
|
</div>
|
|
) : 'type' in schema &&
|
|
schema.type === 'array' &&
|
|
'items' in schema &&
|
|
schema.items &&
|
|
typeof schema.items === 'object' &&
|
|
'type' in schema.items &&
|
|
schema.items.type === 'object' ? (
|
|
<div className="reference-details-panel">
|
|
<div className="p-5 border-b border-default">
|
|
<ApiSchema schema={schema} />
|
|
</div>
|
|
<ApiOperationRequestBodyDetailsInternal schema={schema.items} className="px-5" />
|
|
</div>
|
|
) : (
|
|
<ul className="reference-details-panel">
|
|
{subContent.map((detail: any, index: number) => (
|
|
<li key={index} className="reference-details-item">
|
|
{'enum' in schema ? (
|
|
<span className="font-mono text-sm font-medium text-foreground">
|
|
{String(detail)}
|
|
</span>
|
|
) : 'type' in schema &&
|
|
(schema.type === 'string' ||
|
|
schema.type === 'number' ||
|
|
schema.type === 'integer') ? (
|
|
<span className="text-sm text-foreground flex items-baseline gap-2">
|
|
<span className="font-mono text-sm font-medium text-foreground">
|
|
{detail.constraint}
|
|
</span>
|
|
{detail.value}
|
|
</span>
|
|
) : 'anyOf' in schema || 'allOf' in schema || 'oneOf' in schema ? (
|
|
<ApiSchemaParamDetails
|
|
param={{ name: '', schema: detail, required: !detail.nullable, in: 'body' }}
|
|
/>
|
|
) : null}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
)
|
|
}
|
|
|
|
const schemaDetailsLabel = (schema: ISchema): string => {
|
|
if ('enum' in schema) return 'Accepted values'
|
|
if ('allOf' in schema || 'anyOf' in schema || 'oneOf' in schema) return 'Options'
|
|
if (schema.type === 'array') return 'Items'
|
|
if (schema.type === 'object') return 'Object schema'
|
|
|
|
return 'Details'
|
|
}
|
|
|
|
/**
|
|
* Whether the param comes from overwritten params in the library spec file or
|
|
* directly from the type spec.
|
|
*
|
|
* We're cheating here, this isn't a full validation but rather just checking
|
|
* that it isn't overwritten.
|
|
*/
|
|
function isFromTypespec(parameter: object): parameter is MethodTypes['params'][number] {
|
|
return !('__overwritten' in parameter)
|
|
}
|
|
|
|
function getTypeName(parameter: object): string {
|
|
if (!('type' in parameter)) return ''
|
|
|
|
if (typeof parameter.type === 'string') {
|
|
return parameter.type
|
|
}
|
|
|
|
if (
|
|
typeof parameter.type !== 'object' ||
|
|
parameter.type === null ||
|
|
!('type' in parameter.type)
|
|
) {
|
|
return ''
|
|
}
|
|
|
|
const type = parameter.type
|
|
|
|
switch (type.type) {
|
|
case 'nameOnly':
|
|
return nameOrDefault(type, '')
|
|
case 'intrinsic':
|
|
return nameOrDefault(type, '')
|
|
case 'literal':
|
|
return 'value' in type ? (type.value === null ? 'null' : `"${type.value as string}"`) : ''
|
|
case 'record':
|
|
// Needs an extra level of wrapping to fake the wrapping parameter
|
|
// @ts-ignore
|
|
return `Record<${getTypeName({ type: type.keyType })}, ${getTypeName({ type: type.valueType })}>`
|
|
case 'object':
|
|
return nameOrDefault(type, 'object')
|
|
case 'function':
|
|
return 'function'
|
|
case 'promise':
|
|
// Needs an extra level of wrapping to fake the wrapping parameter
|
|
// @ts-ignore
|
|
return `Promise<${getTypeName({ type: type.awaited })}>`
|
|
case 'union':
|
|
return 'One of the following options'
|
|
case 'index signature':
|
|
// Needs an extra level of wrapping to fake the wrapping parameter
|
|
// @ts-ignore
|
|
return `{ [key: ${getTypeName({ type: type.keyType })}]: ${getTypeName({ type: type.valueType })} }`
|
|
case 'array':
|
|
// Needs an extra level of wrapping to fake the wrapping parameter
|
|
// @ts-ignore
|
|
const innerType = getTypeName({ type: type.elemType })
|
|
return innerType ? `Array<${innerType}>` : 'Array'
|
|
}
|
|
|
|
return ''
|
|
}
|
|
|
|
function nameOrDefault(node: object, fallback: string) {
|
|
return 'name' in node && node.name !== TYPESPEC_NODE_ANONYMOUS ? (node.name as string) : fallback
|
|
}
|
|
|
|
function getSubDetails(parentType: MethodTypes['params'][number] | MethodTypes['ret']) {
|
|
let subDetails: Array<any> = []
|
|
|
|
switch (parentType?.type?.type) {
|
|
case 'object':
|
|
subDetails = parentType?.type?.properties
|
|
break
|
|
case 'function':
|
|
subDetails = [
|
|
...(parentType?.type?.params?.length === 0
|
|
? []
|
|
: [
|
|
{
|
|
name: 'Parameters',
|
|
type: 'callback parameters',
|
|
isOptional: 'NA',
|
|
params: parentType?.type?.params?.map((param) => ({
|
|
...param,
|
|
isOptional: 'NA',
|
|
})),
|
|
},
|
|
]),
|
|
{ name: 'Return', type: parentType?.type?.ret?.type, isOptional: 'NA' },
|
|
]
|
|
break
|
|
// @ts-ignore -- Adding these fake types to take advantage of existing recursion
|
|
case 'callback parameters':
|
|
// @ts-ignore -- Adding these fake types to take advantage of existing recursion
|
|
subDetails = parentType.params
|
|
break
|
|
case 'union':
|
|
subDetails = parentType?.type?.subTypes?.map((subType, index) => ({
|
|
name: `Option ${index + 1}`,
|
|
type: { ...subType },
|
|
isOptional: 'NA',
|
|
}))
|
|
break
|
|
case 'promise':
|
|
if (parentType?.type?.awaited?.type === 'union') {
|
|
subDetails = parentType?.type?.awaited?.subTypes?.map((subType, index) => ({
|
|
name: `Option ${index + 1}`,
|
|
type: { ...subType },
|
|
isOptional: 'NA',
|
|
}))
|
|
} else if (
|
|
parentType?.type?.awaited?.type === 'object' &&
|
|
'properties' in parentType.type.awaited
|
|
) {
|
|
subDetails = (parentType.type.awaited as any).properties?.map((property) => ({
|
|
...property,
|
|
isOptional: 'NA',
|
|
}))
|
|
} else if (parentType?.type?.awaited?.type === 'array') {
|
|
subDetails = [
|
|
{
|
|
name: 'array element',
|
|
type: (parentType?.type?.awaited as any)?.elemType,
|
|
isOptional: 'NA',
|
|
},
|
|
]
|
|
}
|
|
break
|
|
case 'array':
|
|
if (parentType.type.elemType?.type === 'union') {
|
|
subDetails = parentType.type.elemType.subTypes.map((subType, index) => ({
|
|
name: `Option ${index + 1}`,
|
|
type: { ...subType },
|
|
isOptional: 'NA',
|
|
}))
|
|
}
|
|
if (parentType.type.elemType?.type === 'object') {
|
|
subDetails = parentType.type.elemType.properties
|
|
}
|
|
break
|
|
}
|
|
|
|
subDetails?.sort((a, b) => (a.isOptional === true ? 1 : 0) - (b.isOptional === true ? 1 : 0))
|
|
|
|
return subDetails
|
|
}
|
|
|
|
function mergeAlternateParameters(
|
|
parameters: Array<object>,
|
|
altParameters: Array<Array<FunctionParameterType>>
|
|
) {
|
|
const combinedParameters = parameters.map((parameter) => {
|
|
if (!isFromTypespec(parameter)) return parameter
|
|
|
|
const parameterWithoutType = { ...parameter }
|
|
if ('type' in parameterWithoutType) {
|
|
delete parameterWithoutType.type
|
|
}
|
|
|
|
for (const alternate of altParameters) {
|
|
const match = alternate.find((alternateParam) => {
|
|
const alternateWithoutType = { ...alternateParam }
|
|
if ('type' in alternateWithoutType) {
|
|
delete alternateWithoutType.type
|
|
}
|
|
|
|
return isEqual(parameterWithoutType, alternateWithoutType)
|
|
})
|
|
|
|
if (match) {
|
|
// @ts-ignore
|
|
parameter = applyParameterMergeStrategy(parameter, match)
|
|
}
|
|
}
|
|
|
|
return parameter
|
|
})
|
|
|
|
return combinedParameters
|
|
}
|
|
|
|
function applyParameterMergeStrategy(
|
|
parameter: Pick<FunctionParameterType, 'type'>,
|
|
alternateParameter: Pick<FunctionParameterType, 'type'>
|
|
) {
|
|
if (!alternateParameter.type) {
|
|
// Nothing to merge, abort
|
|
return parameter as FunctionParameterType
|
|
}
|
|
|
|
const clonedParameter = JSON.parse(JSON.stringify(parameter)) as FunctionParameterType
|
|
|
|
if (!clonedParameter.type) {
|
|
clonedParameter.type = alternateParameter.type
|
|
return clonedParameter
|
|
}
|
|
|
|
switch (clonedParameter.type.type) {
|
|
case 'nameOnly':
|
|
mergeIntoUnion()
|
|
break
|
|
case 'literal':
|
|
mergeIntoUnion()
|
|
break
|
|
case 'record':
|
|
mergeIntoUnion()
|
|
break
|
|
case 'union':
|
|
if (alternateParameter.type.type === 'union') {
|
|
// Both unions, merge them
|
|
for (const alternateSubType of alternateParameter.type.subTypes) {
|
|
if (
|
|
!clonedParameter.type.subTypes.some((subType) => isEqual(subType, alternateSubType))
|
|
) {
|
|
clonedParameter.type.subTypes.push(alternateSubType)
|
|
}
|
|
}
|
|
} else {
|
|
if (
|
|
!clonedParameter.type.subTypes.some((subType) =>
|
|
isEqual(subType, alternateParameter.type)
|
|
)
|
|
) {
|
|
clonedParameter.type.subTypes.push(alternateParameter.type)
|
|
}
|
|
}
|
|
break
|
|
case 'object':
|
|
if (alternateParameter.type.type === 'object') {
|
|
// Check if the base and alternate parameters have different sets of
|
|
// required properties. If so, they can't be merged without loss of
|
|
// meaning and have to be represented as a union.
|
|
const requiredOriginalProperties = new Set(
|
|
clonedParameter.type.properties.filter(
|
|
// @ts-ignore -- NA introduced as an additional flag for display logic
|
|
(property) => property.isOptional !== true && property.isOptional !== 'NA'
|
|
)
|
|
)
|
|
const requiredAlternateProperties = new Set(
|
|
alternateParameter.type.properties.filter(
|
|
// @ts-ignore -- NA introduced as an additional flag for display logic
|
|
(property) => property.isOptional !== true && property.isOptional !== 'NA'
|
|
)
|
|
)
|
|
if (requiredOriginalProperties.size !== requiredAlternateProperties.size) {
|
|
mergeIntoUnion()
|
|
break
|
|
}
|
|
const union = new Set([...requiredOriginalProperties, ...requiredAlternateProperties])
|
|
if (union.size !== requiredOriginalProperties.size) {
|
|
mergeIntoUnion()
|
|
break
|
|
}
|
|
|
|
const clonedParametersByName = new Map(
|
|
clonedParameter.type.properties.map((property) => [property.name, property])
|
|
)
|
|
const alternateParametersByName = new Map(
|
|
alternateParameter.type.properties.map((property) => [property.name, property])
|
|
)
|
|
|
|
for (const [key, alternateValue] of alternateParametersByName) {
|
|
if (clonedParametersByName.has(key)) {
|
|
clonedParametersByName.set(
|
|
key,
|
|
applyParameterMergeStrategy(clonedParametersByName.get(key)!, alternateValue)
|
|
)
|
|
} else {
|
|
clonedParametersByName.set(key, alternateValue)
|
|
}
|
|
}
|
|
|
|
clonedParameter.type.properties = [...clonedParametersByName.values()]
|
|
} else {
|
|
mergeIntoUnion()
|
|
}
|
|
}
|
|
|
|
return clonedParameter as FunctionParameterType
|
|
|
|
/*********
|
|
* Utils *
|
|
*********/
|
|
|
|
function mergeIntoUnion() {
|
|
if (alternateParameter.type?.type === 'union') {
|
|
const originalType = clonedParameter.type
|
|
|
|
if (
|
|
alternateParameter.type.subTypes.some((subType) => isEqual(subType, clonedParameter.type))
|
|
) {
|
|
clonedParameter.type = alternateParameter.type
|
|
} else {
|
|
clonedParameter.type = {
|
|
type: 'union',
|
|
subTypes: [originalType as TypeDetails, ...(alternateParameter.type?.subTypes || [])],
|
|
}
|
|
}
|
|
} else {
|
|
const originalType = parameter.type
|
|
if (!isEqual(originalType, alternateParameter.type)) {
|
|
clonedParameter.type = {
|
|
type: 'union',
|
|
subTypes: [originalType as TypeDetails, alternateParameter.type!],
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function isDefaultExpanded(meta: object) {
|
|
return (
|
|
'type' in meta &&
|
|
typeof meta.type === 'object' &&
|
|
meta.type &&
|
|
'type' in meta.type &&
|
|
(meta.type.type == 'union' ||
|
|
(meta.type.type === 'promise' &&
|
|
'awaited' in meta.type &&
|
|
typeof meta.type.awaited === 'object' &&
|
|
meta.type.awaited &&
|
|
'type' in meta.type.awaited &&
|
|
meta.type.awaited.type === 'union'))
|
|
)
|
|
}
|