mirror of
https://github.com/supabase/supabase.git
synced 2026-06-28 11:33:52 -04:00
bce47daeea
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Reference documentation generation now only includes feature-enabled SDK pages, so published docs better match what’s available. * Legacy SDK reference pages are now shown selectively based on enabled feature flags. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
444 lines
14 KiB
TypeScript
444 lines
14 KiB
TypeScript
import fs from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
import { isFeatureEnabled, type Feature } from 'common/enabled-features'
|
|
import matter from 'gray-matter'
|
|
import yaml from 'js-yaml'
|
|
import { fromMarkdown } from 'mdast-util-from-markdown'
|
|
import { gfmFromMarkdown, gfmToMarkdown } from 'mdast-util-gfm'
|
|
import { toMarkdown } from 'mdast-util-to-markdown'
|
|
import { gfm } from 'micromark-extension-gfm'
|
|
|
|
import { addBaseUrlPrefix } from './internal-links'
|
|
|
|
const GENERATED = path.join(process.cwd(), 'features/docs/generated')
|
|
const OUT_DIR = path.join(process.cwd(), 'public/markdown/reference')
|
|
const MDX_ROOT = path.join(process.cwd(), 'docs/ref')
|
|
|
|
type Section = {
|
|
id?: string
|
|
title?: string
|
|
slug?: string
|
|
type?: string
|
|
items?: Section[]
|
|
}
|
|
|
|
type Example = { name?: string; code?: string }
|
|
|
|
type LegacyFn = {
|
|
id: string
|
|
$ref?: string
|
|
title?: string
|
|
description?: string
|
|
notes?: string
|
|
examples?: Example[]
|
|
}
|
|
|
|
type TypeSpec = {
|
|
methods: Record<string, { comment?: { shortText?: string; examples?: Example[] } }>
|
|
}
|
|
|
|
type RefBase = {
|
|
title: string
|
|
outFile: string
|
|
/** Folder under docs/ref/ that holds .mdx sections for `type: "markdown"` entries. */
|
|
mdxDir: string
|
|
/**
|
|
* Enabled-features flag gating this reference. When set and the feature is
|
|
* disabled in enabled-features.json, the file is not generated.
|
|
*/
|
|
feature?: Feature
|
|
}
|
|
|
|
type SdkLegacyRef = RefBase & {
|
|
kind: 'sdk-legacy'
|
|
sectionsPath: string
|
|
functionsPath: string
|
|
}
|
|
|
|
type SdkNewRef = RefBase & {
|
|
kind: 'sdk-new'
|
|
contentDir: string
|
|
}
|
|
|
|
type ApiRef = RefBase & {
|
|
kind: 'api'
|
|
sectionsPath: string
|
|
endpointsByIdPath: string
|
|
}
|
|
|
|
type CliRef = RefBase & {
|
|
kind: 'cli'
|
|
sectionsPath: string
|
|
cliSpecPath: string
|
|
}
|
|
|
|
type Ref = SdkLegacyRef | SdkNewRef | ApiRef | CliRef
|
|
|
|
const REFERENCES: Ref[] = [
|
|
{
|
|
kind: 'api',
|
|
title: 'Management API Reference',
|
|
outFile: 'api.md',
|
|
mdxDir: path.join(MDX_ROOT, 'api'),
|
|
sectionsPath: path.join(GENERATED, 'api.latest.sections.json'),
|
|
endpointsByIdPath: path.join(GENERATED, 'api.latest.endpointsById.json'),
|
|
},
|
|
{
|
|
kind: 'cli',
|
|
title: 'Supabase CLI Reference',
|
|
outFile: 'cli.md',
|
|
mdxDir: path.join(MDX_ROOT, 'cli'),
|
|
sectionsPath: path.join(GENERATED, 'cli.latest.sections.json'),
|
|
cliSpecPath: path.join(process.cwd(), 'spec/cli_v1_commands.yaml'),
|
|
},
|
|
{
|
|
kind: 'sdk-new',
|
|
title: 'JavaScript Client Library Reference',
|
|
outFile: 'js.md',
|
|
mdxDir: path.join(MDX_ROOT, 'javascript'),
|
|
contentDir: path.join(process.cwd(), 'content/reference/javascript/v2'),
|
|
},
|
|
{
|
|
kind: 'sdk-legacy',
|
|
title: 'Dart Client Library Reference',
|
|
outFile: 'dart.md',
|
|
mdxDir: path.join(MDX_ROOT, 'dart'),
|
|
sectionsPath: path.join(GENERATED, 'dart.v2.sections.json'),
|
|
functionsPath: path.join(GENERATED, 'dart.v2.functions.json'),
|
|
feature: 'sdk:dart',
|
|
},
|
|
{
|
|
kind: 'sdk-legacy',
|
|
title: 'Kotlin Client Library Reference',
|
|
outFile: 'kotlin.md',
|
|
mdxDir: path.join(MDX_ROOT, 'kotlin'),
|
|
sectionsPath: path.join(GENERATED, 'kotlin.v1.sections.json'),
|
|
functionsPath: path.join(GENERATED, 'kotlin.v1.functions.json'),
|
|
feature: 'sdk:kotlin',
|
|
},
|
|
{
|
|
kind: 'sdk-legacy',
|
|
title: 'Python Client Library Reference',
|
|
outFile: 'python.md',
|
|
mdxDir: path.join(MDX_ROOT, 'python'),
|
|
sectionsPath: path.join(GENERATED, 'python.v2.sections.json'),
|
|
functionsPath: path.join(GENERATED, 'python.v2.functions.json'),
|
|
feature: 'sdk:python',
|
|
},
|
|
{
|
|
kind: 'sdk-legacy',
|
|
title: 'Swift Client Library Reference',
|
|
outFile: 'swift.md',
|
|
mdxDir: path.join(MDX_ROOT, 'swift'),
|
|
sectionsPath: path.join(GENERATED, 'swift.v2.sections.json'),
|
|
functionsPath: path.join(GENERATED, 'swift.v2.functions.json'),
|
|
feature: 'sdk:swift',
|
|
},
|
|
{
|
|
kind: 'sdk-legacy',
|
|
title: 'C# Client Library Reference',
|
|
outFile: 'csharp.md',
|
|
mdxDir: path.join(MDX_ROOT, 'csharp'),
|
|
sectionsPath: path.join(GENERATED, 'csharp.v0.sections.json'),
|
|
functionsPath: path.join(GENERATED, 'csharp.v0.functions.json'),
|
|
feature: 'sdk:csharp',
|
|
},
|
|
]
|
|
|
|
function flatten(sections: Section[]): Section[] {
|
|
return sections.flatMap((s) => (s.items ? [s, ...flatten(s.items)] : [s]))
|
|
}
|
|
|
|
async function readJson<T>(filePath: string): Promise<T> {
|
|
return JSON.parse(await fs.readFile(filePath, 'utf8')) as T
|
|
}
|
|
|
|
function renderExamples(examples: Example[] | undefined): string {
|
|
if (!examples?.length) return ''
|
|
const blocks = examples
|
|
.map((ex) => {
|
|
const header = ex.name ? `#### ${ex.name}\n\n` : ''
|
|
const code = (ex.code ?? '').trim()
|
|
return code ? `${header}${code}` : header.trim()
|
|
})
|
|
.filter(Boolean)
|
|
return blocks.length ? `### Examples\n\n${blocks.join('\n\n')}` : ''
|
|
}
|
|
|
|
function renderFunctionSection(args: {
|
|
title: string
|
|
description?: string
|
|
notes?: string
|
|
examples?: Example[]
|
|
}): string {
|
|
const parts = [`## ${args.title}`]
|
|
if (args.description?.trim()) parts.push(args.description.trim())
|
|
if (args.notes?.trim()) parts.push(args.notes.trim())
|
|
const examples = renderExamples(args.examples)
|
|
if (examples) parts.push(examples)
|
|
return parts.join('\n\n')
|
|
}
|
|
|
|
/** Remove the minimum common leading whitespace from all non-empty lines. */
|
|
function dedentBlock(text: string): string {
|
|
const lines = text.split('\n')
|
|
const nonEmpty = lines.filter((l) => /\S/.test(l))
|
|
if (!nonEmpty.length) return text
|
|
const minIndent = Math.min(...nonEmpty.map((l) => (l.match(/^([ \t]*)/) ?? ['', ''])[1].length))
|
|
if (!minIndent) return text
|
|
return lines.map((l) => l.slice(minIndent)).join('\n')
|
|
}
|
|
|
|
/**
|
|
* Strip MDX comments and JSX component tags while keeping inner prose/code.
|
|
* Outside code fences, leading whitespace from JSX nesting is removed. Inside
|
|
* code fences, the body is dedented so the closing fence sits flush left.
|
|
*/
|
|
function stripMdxJsx(content: string): string {
|
|
const segments = content.split(/(```[\s\S]*?```)/g)
|
|
return segments
|
|
.map((seg, i) => {
|
|
if (i % 2 === 0) {
|
|
let prose = seg
|
|
prose = prose.replace(/\{\/\*[\s\S]*?\*\/\}/g, '')
|
|
prose = prose.replace(/<[$A-Z][\w.]*(?:\s[^>]*)?\s*\/>/gs, '')
|
|
prose = prose.replace(/<[$A-Z][\w.]*(?:\s[^>]*)?\s*>/gs, '')
|
|
prose = prose.replace(/<\/[$A-Z][\w.]*>/g, '')
|
|
prose = prose.replace(/<\/?(?:div|a|span|p|h[1-6])(?:\s[^>]*)?\s*>/g, '')
|
|
return prose.replace(/^[ \t]+/gm, '')
|
|
}
|
|
return seg.replace(
|
|
/^(```[^\n]*\n)([\s\S]*?)(\n[ \t]*```)$/,
|
|
(_, open, body) => open + dedentBlock(body) + '\n```'
|
|
)
|
|
})
|
|
.join('')
|
|
.replace(/^[^\S\n]+$/gm, '')
|
|
.replace(/\n{3,}/g, '\n\n')
|
|
.trim()
|
|
}
|
|
|
|
/**
|
|
* Read a `type: "markdown"` section's .mdx file and convert it to plain
|
|
* markdown. Returns null if the file doesn't exist or has no usable content.
|
|
*/
|
|
async function renderMarkdownSection(mdxDir: string, section: Section): Promise<string | null> {
|
|
if (!section.slug) return null
|
|
let raw: string
|
|
try {
|
|
raw = await fs.readFile(path.join(mdxDir, `${section.slug}.mdx`), 'utf8')
|
|
} catch {
|
|
return null
|
|
}
|
|
const { content, data } = matter(raw)
|
|
const body = stripMdxJsx(content)
|
|
if (!body) return null
|
|
const heading = data.hideTitle
|
|
? null
|
|
: ((typeof data.title === 'string' ? data.title : section.title) ?? null)
|
|
return heading ? `## ${heading}\n\n${body}` : body
|
|
}
|
|
|
|
async function renderSdkLegacy(ref: SdkLegacyRef, sharedTypeSpec: TypeSpec): Promise<string> {
|
|
const [sections, functions] = await Promise.all([
|
|
readJson<Section[]>(ref.sectionsPath),
|
|
readJson<LegacyFn[]>(ref.functionsPath),
|
|
])
|
|
const fnsById = new Map(functions.map((fn) => [fn.id, fn]))
|
|
const methodsByRef = new Map(Object.entries(sharedTypeSpec.methods ?? {}))
|
|
|
|
const parts: string[] = [`# ${ref.title}`]
|
|
for (const section of flatten(sections)) {
|
|
if (section.type === 'category' && section.title) {
|
|
parts.push(`## ${section.title}`)
|
|
continue
|
|
}
|
|
if (section.type === 'markdown') {
|
|
const md = await renderMarkdownSection(ref.mdxDir, section)
|
|
if (md) parts.push(md)
|
|
continue
|
|
}
|
|
if (section.type !== 'function' || !section.id) continue
|
|
const fn = fnsById.get(section.id)
|
|
if (!fn) continue
|
|
const tsm = fn.$ref ? methodsByRef.get(fn.$ref) : undefined
|
|
parts.push(
|
|
renderFunctionSection({
|
|
title: fn.title || section.title || fn.id,
|
|
description: fn.description ?? tsm?.comment?.shortText,
|
|
notes: fn.notes,
|
|
examples: fn.examples ?? tsm?.comment?.examples,
|
|
})
|
|
)
|
|
}
|
|
return parts.join('\n\n') + '\n'
|
|
}
|
|
|
|
async function renderSdkNew(ref: SdkNewRef): Promise<string> {
|
|
const [sections, functions, typeSpec] = await Promise.all([
|
|
readJson<Section[]>(path.join(ref.contentDir, 'sections.json')),
|
|
readJson<LegacyFn[]>(path.join(ref.contentDir, 'functions.json')),
|
|
readJson<TypeSpec>(path.join(ref.contentDir, 'typeSpec.json')),
|
|
])
|
|
const fnsById = new Map(functions.map((fn) => [fn.id, fn]))
|
|
const methods = typeSpec.methods ?? {}
|
|
|
|
const parts: string[] = [`# ${ref.title}`]
|
|
for (const section of flatten(sections)) {
|
|
if (section.type === 'category' && section.title) {
|
|
parts.push(`## ${section.title}`)
|
|
continue
|
|
}
|
|
if (section.type === 'markdown') {
|
|
const md = await renderMarkdownSection(ref.mdxDir, section)
|
|
if (md) parts.push(md)
|
|
continue
|
|
}
|
|
if (section.type !== 'function' || !section.id) continue
|
|
const fn = fnsById.get(section.id)
|
|
if (!fn) continue
|
|
const tsm = fn.$ref ? methods[fn.$ref] : undefined
|
|
parts.push(
|
|
renderFunctionSection({
|
|
title: fn.title || section.title || fn.id,
|
|
description: fn.description ?? tsm?.comment?.shortText,
|
|
notes: fn.notes,
|
|
examples: fn.examples ?? tsm?.comment?.examples,
|
|
})
|
|
)
|
|
}
|
|
return parts.join('\n\n') + '\n'
|
|
}
|
|
|
|
async function renderApi(ref: ApiRef): Promise<string> {
|
|
type Endpoint = {
|
|
summary?: string
|
|
description?: string
|
|
path?: string
|
|
method?: string
|
|
parameters?: Array<{
|
|
name: string
|
|
required?: boolean
|
|
description?: string
|
|
schema?: { type?: string }
|
|
}>
|
|
}
|
|
const [sections, entries] = await Promise.all([
|
|
readJson<Section[]>(ref.sectionsPath),
|
|
readJson<Array<[string, Endpoint]>>(ref.endpointsByIdPath),
|
|
])
|
|
const endpointsById = new Map(entries)
|
|
|
|
const parts: string[] = [`# ${ref.title}`]
|
|
for (const section of flatten(sections)) {
|
|
if (section.type === 'category' && section.title) {
|
|
parts.push(`## ${section.title}`)
|
|
continue
|
|
}
|
|
if (section.type === 'markdown') {
|
|
const md = await renderMarkdownSection(ref.mdxDir, section)
|
|
if (md) parts.push(md)
|
|
continue
|
|
}
|
|
if (section.type !== 'operation' || !section.id) continue
|
|
const ep = endpointsById.get(section.id)
|
|
if (!ep) continue
|
|
const heading = ep.summary || section.title || section.id
|
|
const block = [`## ${heading}`]
|
|
if (ep.path) block.push(`\`${(ep.method ?? 'GET').toUpperCase()} ${ep.path}\``)
|
|
if (ep.description?.trim()) block.push(ep.description.trim())
|
|
if (ep.parameters?.length) {
|
|
const list = ep.parameters
|
|
.map((p) => {
|
|
const required = p.required ? 'required' : 'optional'
|
|
const type = p.schema?.type ?? 'string'
|
|
return `- \`${p.name}\` (${type}, ${required})${p.description ? `: ${p.description}` : ''}`
|
|
})
|
|
.join('\n')
|
|
block.push(`### Parameters\n\n${list}`)
|
|
}
|
|
parts.push(block.join('\n\n'))
|
|
}
|
|
return parts.join('\n\n') + '\n'
|
|
}
|
|
|
|
async function renderCli(ref: CliRef): Promise<string> {
|
|
type Command = {
|
|
id: string
|
|
title?: string
|
|
summary?: string
|
|
description?: string
|
|
usage?: string
|
|
}
|
|
const [sections, spec] = await Promise.all([
|
|
readJson<Section[]>(ref.sectionsPath),
|
|
fs.readFile(ref.cliSpecPath, 'utf8').then((raw) => yaml.load(raw) as { commands: Command[] }),
|
|
])
|
|
const cmdsById = new Map((spec.commands ?? []).map((c) => [c.id, c]))
|
|
|
|
const parts: string[] = [`# ${ref.title}`]
|
|
for (const section of flatten(sections)) {
|
|
if (section.type === 'category' && section.title) {
|
|
parts.push(`## ${section.title}`)
|
|
continue
|
|
}
|
|
if (section.type === 'markdown') {
|
|
const md = await renderMarkdownSection(ref.mdxDir, section)
|
|
if (md) parts.push(md)
|
|
continue
|
|
}
|
|
if (section.type !== 'cli-command' || !section.id) continue
|
|
const cmd = cmdsById.get(section.id)
|
|
if (!cmd) continue
|
|
const block = [`## ${cmd.summary || cmd.title || section.title || section.id}`]
|
|
if (cmd.description?.trim()) block.push(cmd.description.trim())
|
|
if (cmd.usage?.trim()) block.push('```sh\n' + cmd.usage.trim() + '\n```')
|
|
parts.push(block.join('\n\n'))
|
|
}
|
|
return parts.join('\n\n') + '\n'
|
|
}
|
|
|
|
async function generate() {
|
|
await fs.mkdir(OUT_DIR, { recursive: true })
|
|
const sharedTypeSpec = await readJson<TypeSpec>(
|
|
path.join(process.cwd(), 'content/reference/javascript/v2/typeSpec.json')
|
|
)
|
|
|
|
const references = REFERENCES.filter((ref) => !ref.feature || isFeatureEnabled(ref.feature))
|
|
|
|
await Promise.all(
|
|
references.map(async (ref) => {
|
|
let output: string
|
|
switch (ref.kind) {
|
|
case 'sdk-legacy':
|
|
output = await renderSdkLegacy(ref, sharedTypeSpec)
|
|
break
|
|
case 'sdk-new':
|
|
output = await renderSdkNew(ref)
|
|
break
|
|
case 'api':
|
|
output = await renderApi(ref)
|
|
break
|
|
case 'cli':
|
|
output = await renderCli(ref)
|
|
break
|
|
}
|
|
const tree = fromMarkdown(output, {
|
|
extensions: [gfm()],
|
|
mdastExtensions: [gfmFromMarkdown()],
|
|
})
|
|
addBaseUrlPrefix(tree)
|
|
const prefixed = toMarkdown(tree, {
|
|
extensions: [gfmToMarkdown()],
|
|
bullet: '-',
|
|
listItemIndent: 'one',
|
|
})
|
|
await fs.writeFile(path.join(OUT_DIR, ref.outFile), prefixed)
|
|
})
|
|
)
|
|
|
|
console.log(`Generated ${references.length} markdown files under public/markdown/reference/`)
|
|
}
|
|
|
|
generate()
|