mirror of
https://github.com/supabase/supabase.git
synced 2026-10-17 07:09:07 +03:00
Scripts currently use CJS, which is causing a bit of a mess when trying to use shared utilities from the app. Converting everything to ESM so there are fewer conflicts when adding new scripts going forward.
897 lines
28 KiB
TypeScript
897 lines
28 KiB
TypeScript
import { isEqual } from 'lodash-es'
|
|
import { ChevronRight, XCircle } from 'lucide-react'
|
|
import type { HTMLAttributes, PropsWithChildren } from 'react'
|
|
import ReactMarkdown from 'react-markdown'
|
|
|
|
import { cn, Collapsible_Shadcn_, CollapsibleContent_Shadcn_, CollapsibleTrigger_Shadcn_ } from 'ui'
|
|
|
|
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 { ReferenceSectionWrapper } from '~/features/docs/Reference.ui.client'
|
|
import { normalizeMarkdown } from '~/features/docs/Reference.utils'
|
|
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-gradient-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-none',
|
|
monoFont && 'font-mono',
|
|
className
|
|
)}
|
|
>
|
|
{title}
|
|
</h2>
|
|
)
|
|
}
|
|
|
|
export function CollapsibleDetails({ title, content }: { title: string; content: string }) {
|
|
return (
|
|
<Collapsible_Shadcn_>
|
|
<CollapsibleTrigger_Shadcn_
|
|
className={cn(
|
|
'group',
|
|
'w-full h-8',
|
|
'border bg-surface-100 rounded',
|
|
'px-5',
|
|
'flex items-center gap-3',
|
|
'text-xs text-foreground-light',
|
|
'data-[state=open]:bg-surface-200',
|
|
'data-[state=open]:rounded-b-none data-[state=open]:border-b-0',
|
|
'transition-safe-all ease-out'
|
|
)}
|
|
>
|
|
<ChevronRight
|
|
size={12}
|
|
className="group-data-[state=open]:rotate-90 transition-transform"
|
|
/>
|
|
{title}
|
|
</CollapsibleTrigger_Shadcn_>
|
|
<CollapsibleContent_Shadcn_
|
|
className={cn(
|
|
'border border-default bg-surface-100 rounded-b',
|
|
'px-5 py-2',
|
|
'prose max-w-none text-sm'
|
|
)}
|
|
>
|
|
<MDXRemoteRefs source={content} />
|
|
</CollapsibleContent_Shadcn_>
|
|
</Collapsible_Shadcn_>
|
|
)
|
|
}
|
|
|
|
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>
|
|
<RequiredBadge
|
|
isOptional={
|
|
'isOptional' in paramOrType ? (paramOrType.isOptional as boolean | 'NA') : false
|
|
}
|
|
/>
|
|
{/* @ts-ignore */}
|
|
{paramOrType?.comment?.tags?.some((tag) => tag.tag === 'deprecated') && (
|
|
<span className="text-xs text-warning-600">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} />
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
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 = isDefaultExpanded(returnType)
|
|
|
|
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">{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} details={subContent} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function TypeSubDetails({
|
|
details,
|
|
className,
|
|
defaultOpen = false,
|
|
}: {
|
|
details: Array<SubContent> | Array<CustomTypePropertyType> | Array<TypeDetails>
|
|
className?: string
|
|
defaultOpen?: boolean
|
|
}) {
|
|
return (
|
|
<Collapsible_Shadcn_ defaultOpen={defaultOpen}>
|
|
<CollapsibleTrigger_Shadcn_
|
|
className={cn(
|
|
'group',
|
|
'w-fit rounded-full',
|
|
'px-5 py-1',
|
|
'border border-default',
|
|
'flex items-center gap-2',
|
|
'text-left text-sm text-foreground-light',
|
|
'hover:bg-surface-100',
|
|
'data-[state=open]:w-full',
|
|
'data-[state=open]:rounded-b-none data-[state=open]:rounded-tl-lg data-[state=open]:rounded-tr-lg',
|
|
'transition [transition-property:width,background-color]',
|
|
className
|
|
)}
|
|
>
|
|
<XCircle
|
|
size={14}
|
|
className={cn(
|
|
'text-foreground-muted',
|
|
'group-data-[state=closed]:rotate-45',
|
|
'transition-transform'
|
|
)}
|
|
/>
|
|
Details
|
|
</CollapsibleTrigger_Shadcn_>
|
|
<CollapsibleContent_Shadcn_>
|
|
<ul className={cn('border-b border-x border-default', 'rounded-b-lg')}>
|
|
{details.map(
|
|
(detail: SubContent | CustomTypePropertyType | TypeDetails, index: number) => (
|
|
<li
|
|
key={index}
|
|
className={cn(
|
|
'px-5 py-3',
|
|
'border-t border-default first:border-t-0',
|
|
'flex flex-col gap-3'
|
|
)}
|
|
>
|
|
<ParamOrTypeDetails paramOrType={detail} />
|
|
</li>
|
|
)
|
|
)}
|
|
</ul>
|
|
</CollapsibleContent_Shadcn_>
|
|
</Collapsible_Shadcn_>
|
|
)
|
|
}
|
|
|
|
export function RequiredBadge({ isOptional }: { isOptional: boolean | 'NA' }) {
|
|
return isOptional === true ? (
|
|
<span className="font-mono text-[10px] text-foreground-lighter tracking-wide">Optional</span>
|
|
) : isOptional === false ? (
|
|
<span
|
|
className={cn(
|
|
'inline-block',
|
|
'px-2 py-0.25 rounded-full',
|
|
'-translate-y-[0.125rem]', // retranslate to undo visual misalignment from the y-padding
|
|
'border border-amber-700 bg-amber-300',
|
|
'font-mono text-[10px] text-amber-900 uppercase tracking-wide'
|
|
)}
|
|
>
|
|
Required
|
|
</span>
|
|
) : undefined
|
|
}
|
|
|
|
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>
|
|
<RequiredBadge isOptional={!param.required} />
|
|
{param.schema?.deprecated && <span className="text-xs text-warning-600">Deprecated</span>}
|
|
{param.schema && (
|
|
<span className="text-xs text-foreground-muted">
|
|
{getTypeDisplayFromSchema(param.schema)?.displayName ?? ''}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{param.description && (
|
|
<ReactMarkdown className="prose break-words text-sm">{param.description}</ReactMarkdown>
|
|
)}
|
|
{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}
|
|
hidden={index > 0}
|
|
{...{
|
|
[API_REFERENCE_REQUEST_BODY_SCHEMA_DATA_ATTRIBUTES.KEY]: scheme,
|
|
}}
|
|
/>
|
|
))}
|
|
</>
|
|
)
|
|
}
|
|
|
|
interface ApiOperationRequestBodyDetailsInternalProps extends HTMLAttributes<HTMLUListElement> {
|
|
schema: ISchema
|
|
}
|
|
|
|
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>
|
|
{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>
|
|
{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>
|
|
{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') {
|
|
return (
|
|
<>
|
|
<span className="font-mono text-sm font-medium text-foreground">{`Array of ${getTypeDisplayFromSchema(schema.items).displayName}`}</span>
|
|
{!(
|
|
'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),
|
|
in: 'body' as const,
|
|
schema: schema.properties[property],
|
|
}))
|
|
.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 &&
|
|
(['boolean', 'number', 'integer'].includes(schema.type) ||
|
|
(schema.type === 'string' &&
|
|
!('minLength' in schema || 'maxLength' in schema || 'pattern' in schema)) ||
|
|
(schema.type === 'array' &&
|
|
'type' in schema.items &&
|
|
['boolean', 'number', 'integer', 'string', 'file'].includes(schema.items.type)))
|
|
) {
|
|
return null
|
|
}
|
|
|
|
const subContent =
|
|
'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],
|
|
}))
|
|
: []
|
|
|
|
return (
|
|
<Collapsible_Shadcn_>
|
|
<CollapsibleTrigger_Shadcn_
|
|
className={cn(
|
|
'group',
|
|
'w-fit rounded-full',
|
|
'px-5 py-1',
|
|
'border border-default',
|
|
'flex items-center gap-2',
|
|
'text-left text-sm text-foreground-light',
|
|
'hover:bg-surface-100',
|
|
'data-[state=open]:w-full',
|
|
'data-[state=open]:rounded-b-none data-[state=open]:rounded-tl-lg data-[state=open]:rounded-tr-lg',
|
|
'transition [transition-property:width,background-color]',
|
|
className
|
|
)}
|
|
>
|
|
<XCircle
|
|
size={14}
|
|
className={cn(
|
|
'text-foreground-muted',
|
|
'group-data-[state=closed]:rotate-45',
|
|
'transition-transform'
|
|
)}
|
|
/>
|
|
{'enum' in schema
|
|
? 'Accepted values'
|
|
: 'allOf' in schema || 'anyOf' in schema || 'oneOf' in schema
|
|
? 'Options'
|
|
: schema.type === 'array'
|
|
? 'Items'
|
|
: schema.type === 'object'
|
|
? 'Object schema'
|
|
: 'Details'}
|
|
</CollapsibleTrigger_Shadcn_>
|
|
<CollapsibleContent_Shadcn_>
|
|
{'type' in schema && schema.type === 'object' ? (
|
|
<div className={cn('border-b border-x border-fault', 'rounded-b-lg', 'p-5')}>
|
|
<ApiSchema schema={schema} />
|
|
</div>
|
|
) : (
|
|
<ul className={cn('border-b border-x border-default', 'rounded-b-lg')}>
|
|
{subContent.map((detail: any, index: number) => (
|
|
<li
|
|
key={index}
|
|
className={cn(
|
|
'px-5 py-3',
|
|
'border-t border-default first:border-t-0',
|
|
'flex flex-col gap-3'
|
|
)}
|
|
>
|
|
{'enum' in schema ? (
|
|
<span className="font-mono text-sm font-medium text-foreground">
|
|
{String(detail)}
|
|
</span>
|
|
) : 'type' in schema && schema.type === 'string' ? (
|
|
<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_Shadcn_>
|
|
</Collapsible_Shadcn_>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* 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' || !('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') {
|
|
subDetails = parentType.type.awaited.properties.map((property) => ({
|
|
...property,
|
|
isOptional: 'NA',
|
|
}))
|
|
} else if (parentType.type.awaited.type === 'array') {
|
|
subDetails = [
|
|
{ name: 'array element', type: parentType.type.awaited.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, ...alternateParameter.type.subTypes],
|
|
}
|
|
}
|
|
} else {
|
|
const originalType = parameter.type
|
|
if (!isEqual(originalType, alternateParameter.type)) {
|
|
clonedParameter.type = {
|
|
type: 'union',
|
|
subTypes: [originalType, alternateParameter.type],
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function isDefaultExpanded(meta: object) {
|
|
return (
|
|
'type' in meta &&
|
|
typeof meta.type === 'object' &&
|
|
'type' in meta.type &&
|
|
(meta.type.type == 'union' ||
|
|
(meta.type.type === 'promise' &&
|
|
'awaited' in meta.type &&
|
|
typeof meta.type.awaited === 'object' &&
|
|
'type' in meta.type.awaited &&
|
|
meta.type.awaited.type === 'union'))
|
|
)
|
|
}
|