Files
supabase/apps/docs/features/docs/Reference.utils.ts
T
f20cd22dc3 docs: improve SDK automation build step on docs (#46163)
# Second try of making a new better process for SDK automation

Instead of building a new pipeline. We will take the lessons learned
form round 1, plus the good design and improvement on DX quality for
drop-in file as a single step required from SDK team and produce almost
identical set of files as used right now to render using the current
pipeline.



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* New reference-content pipeline producing per-library reference
artifacts and integrating into prebuilds, search ingestion, and
rendering (type-aware examples).

* **Documentation**
* Added comprehensive JavaScript SDK v2 reference content and partials
(Auth MFA, passkeys, admin, TypeScript support, filters, modifiers,
Installing, Initializing, Buckets, etc.).

* **Tests & CI**
* Added regression snapshot test and updated workflows to refresh
reference snapshots and ensure spec downloads.

* **Chores**
* Updated ignore rules, build scripts, Makefile targets, and package
lifecycle hooks.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Katerina Skroumpelou <mandarini@users.noreply.github.com>
Co-authored-by: Katerina Skroumpelou <sk.katherine@gmail.com>
2026-06-03 11:46:02 +03:00

274 lines
8.1 KiB
TypeScript

import { clientSdkIds, REFERENCES, selfHostingServices } from '~/content/navigation.references'
import { getFlattenedSections } from '~/features/docs/Reference.generated.singleton'
import { generateOpenGraphImageMeta } from '~/features/seo/openGraph'
import { BASE_PATH } from '~/lib/constants'
import { getCustomContent } from '~/lib/custom-content/getCustomContent'
import { fromMarkdown } from 'mdast-util-from-markdown'
import { mdxFromMarkdown, mdxToMarkdown } from 'mdast-util-mdx'
import { toMarkdown } from 'mdast-util-to-markdown'
import { mdxjs } from 'micromark-extension-mdxjs'
import type { Metadata, ResolvingMetadata } from 'next'
import { redirect } from 'next/navigation'
import { visit } from 'unist-util-visit'
const { metadataTitle } = getCustomContent(['metadata:title'])
export interface AbbrevApiReferenceSection {
id: string
type: string
title?: string
slug?: string
items?: Array<AbbrevApiReferenceSection>
excludes?: Array<string>
meta?: {
shared?: boolean
}
}
export function parseReferencePath(slug: Array<string>) {
const isClientSdkReference = clientSdkIds.includes(slug[0])
const isCliReference = slug[0] === 'cli'
const isApiReference = slug[0] === 'api'
const isSelfHostingReference = slug[0].startsWith('self-hosting-')
if (isClientSdkReference) {
let sdkId: string
let maybeVersion: string | null
let maybeCrawlers: string | null
let path: string[]
;[sdkId, maybeVersion, maybeCrawlers, ...path] = slug
if (!/v\d+/.test(maybeVersion)) {
maybeVersion = null
path = [maybeCrawlers, ...path]
maybeCrawlers = maybeVersion
}
if (maybeCrawlers !== 'crawlers') {
if (typeof maybeCrawlers === 'string') {
path = [maybeCrawlers, ...path]
}
maybeCrawlers = null
}
return {
__type: 'clientSdk' as const,
sdkId,
maybeVersion,
maybeCrawlers,
path,
}
} else if (isCliReference) {
return {
__type: 'cli' as const,
path: slug.slice(1),
}
} else if (isApiReference) {
return {
__type: 'api' as const,
path: slug.slice(1),
}
} else if (isSelfHostingReference) {
return {
__type: 'self-hosting' as const,
service: slug[0].replace('self-hosting-', ''),
servicePath: slug[0],
path: slug.slice(1),
}
} else {
return {
__type: 'UNIMPLEMENTED' as const,
}
}
}
async function generateStaticParamsForSdkVersion(sdkId: string, version: string) {
const flattenedSections = await getFlattenedSections(sdkId, version)
return (flattenedSections || [])
.filter((section) => section.type !== 'category' && !!section.slug)
.map((section) => ({
slug: [
sdkId,
version === REFERENCES[sdkId].versions[0] ? null : version,
'crawlers',
section.slug,
].filter(Boolean),
}))
}
export async function generateReferenceStaticParams() {
const sdkPages = clientSdkIds
.flatMap((sdkId) =>
REFERENCES[sdkId].versions.map((version) => ({
sdkId,
version,
}))
)
.map(({ sdkId, version }) => ({
slug: [sdkId, version === REFERENCES[sdkId].versions[0] ? null : version].filter(Boolean),
}))
const cliPages = [
{
slug: ['cli'],
},
]
const apiPages = [
{
slug: ['api'],
},
]
const selfHostingPages = selfHostingServices.map((service) => ({
slug: [REFERENCES[service].libPath],
}))
return [...sdkPages, ...cliPages, ...apiPages, ...selfHostingPages]
}
export async function generateReferenceMetadata(
props: { params: Promise<{ slug: Array<string> }> },
resolvingParent: ResolvingMetadata
): Promise<Metadata> {
const { slug } = await props.params
const { alternates: parentAlternates, openGraph: parentOg } = await resolvingParent
const parsedPath = parseReferencePath(slug)
const isClientSdkReference = parsedPath.__type === 'clientSdk'
const isCliReference = parsedPath.__type === 'cli'
const isApiReference = parsedPath.__type === 'api'
const isSelfHostingReference = parsedPath.__type === 'self-hosting'
if (isClientSdkReference) {
const { sdkId, maybeVersion, path } = parsedPath
const version = maybeVersion ?? REFERENCES[sdkId].versions[0]
const flattenedSections = await getFlattenedSections(sdkId, version)
const displayName = REFERENCES[sdkId].name
const sectionTitle =
slug.length > 0
? flattenedSections?.find((section) => section.slug === slug[0])?.title
: undefined
const url = [BASE_PATH, 'reference', sdkId, path[0]].filter(Boolean).join('/')
const images = generateOpenGraphImageMeta({
type: 'API Reference',
title: `${displayName}${sectionTitle ? `: ${sectionTitle}` : ''}`,
})
return {
title: `${displayName} API Reference | ${metadataTitle || 'Supabase'}`,
description: `API reference for the ${displayName} Supabase SDK`,
...(slug.length > 0
? {
alternates: {
canonical: url,
},
}
: {}),
openGraph: {
...parentOg,
url,
images,
},
}
} else if (isCliReference) {
return {
title: 'CLI Reference | Supabase Docs',
description: 'CLI reference for the Supabase CLI',
}
} else if (isApiReference) {
return {
title: 'Management API Reference | Supabase Docs',
description: 'Management API reference for the Supabase API',
}
} else if (isSelfHostingReference) {
return {
title: 'Self-Hosting | Supabase Docs',
}
} else {
return {}
}
}
export async function redirectNonexistentReferenceSection(
sdkId: string,
version: string,
path: Array<string>,
isLatestVersion: boolean
) {
const initialSelectedSection = path[0]
const validSlugs = await generateStaticParamsForSdkVersion(sdkId, version)
if (
initialSelectedSection &&
!validSlugs.some((params) => params.slug[0] === initialSelectedSection)
) {
redirect(`/reference/${sdkId}` + (!isLatestVersion ? '/' + version : ''))
}
}
export function normalizeMarkdown(markdownUnescaped: string): string {
/**
* Need to first escape the braces so that the MDX parser doesn't choke on
* them. Unlike the MDX parser, the regular Markdown parser handles braces
* gracefully, so we use it to find the positions of the code blocks, then
* escape all other braces before the final conversion with the MDX parser.
*/
const markdownTree = fromMarkdown(markdownUnescaped)
const codeBlocks = [] as Array<{
type: string
start: number
end: number
}>
visit(markdownTree, ['code', 'inlineCode'], (node) => {
codeBlocks.push({
type: node.type,
start: node.position?.start?.offset || 0,
end: node.position?.end?.offset || 0,
})
})
// Sort code blocks by start offset in descending order
codeBlocks.sort((a, b) => b.start - a.start)
let markdown = markdownUnescaped
let lastIndex = markdown.length
// Iterate through the sorted code blocks
for (const block of codeBlocks) {
// Escape braces in the text between the current code block and the last processed position
const textBetween = markdown.slice(block.end, lastIndex)
const escapedTextBetween = textBetween.replace(/(?<!\\)([{}])/g, '\\$1')
// Replace the original text with the escaped version
markdown = markdown.slice(0, block.end) + escapedTextBetween + markdown.slice(lastIndex)
// Update the last processed position
lastIndex = block.start
}
// Escape braces in the remaining text before the first code block
if (lastIndex > 0) {
const remainingText = markdown.slice(0, lastIndex)
const escapedRemainingText = remainingText.replace(/(?<!\\)([{}])/g, '\\$1')
markdown = escapedRemainingText + markdown.slice(lastIndex)
}
const mdxTree = fromMarkdown(markdown, {
extensions: [mdxjs()],
mdastExtensions: [mdxFromMarkdown()],
})
visit(mdxTree, 'text', (node) => {
node.value = node.value.replace(/\n/g, ' ')
})
const content = toMarkdown(mdxTree, {
extensions: [mdxToMarkdown()],
})
return content
}
export { SUPPORTS_NEW_REFERENCE_PROCESS } from '~/features/docs/Reference.constants'