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>
This commit is contained in:
authored and GitHub committed 2026-06-03 11:46:02 +03:00
1 parent 3701302b84
commit f20cd22dc3
58 files changed
+29246 -943190

No files matched your search

+2 -4
View File
@@ -52,11 +52,9 @@ jobs:
echo "Version: ${VERSION}"
make
- name: Generate new typespec snapshot
- name: Refresh reference-content snapshot
working-directory: apps/docs
run: |
echo "Generating new typespec snapshot for review..."
npx vitest run --update --dir features/docs
run: npx vitest run --update scripts/build-reference-content.test.ts
- name: Generate token
id: app-token
+8
View File
@@ -47,6 +47,14 @@ jobs:
- name: Install deps
run: pnpm install --frozen-lockfile
- name: Download JS reference TypeDoc dumps
# The source dumps under apps/docs/spec/reference/<lib>/<ver>/*.json are
# gitignored — `make download.tsdoc.v2` re-fetches them from
# supabase.github.io so the reference-content snapshot test has
# something to walk.
working-directory: apps/docs/spec
run: make download.tsdoc.v2
- name: Run tests
run: |
touch .env
+9
View File
@@ -36,5 +36,14 @@ public/docs.tar.gz
# Copied examples folder
/examples/
# Generated reference content (built by scripts/build-reference-content.ts)
/content/reference/
# Downloaded TypeDoc dumps under spec/reference/<lib>/<ver>/. Regenerated by
# `cd apps/docs/spec && make download.tsdoc.v2`. Hand-authored files in the
# same folders (config.json, partials/) stay tracked.
/spec/reference/*/*/*.json
!/spec/reference/*/*/config.json
# Sentry Config File
.env.sentry-build-plugin
+24 -14
View File
@@ -1,11 +1,3 @@
import { toHtml } from 'hast-util-to-html'
import { fromMarkdown } from 'mdast-util-from-markdown'
import { mdxFromMarkdown } from 'mdast-util-mdx'
import { toHast } from 'mdast-util-to-hast'
import { mdxjs } from 'micromark-extension-mdxjs'
import { notFound } from 'next/navigation'
import { visit } from 'unist-util-visit'
import { REFERENCES } from '~/content/navigation.references'
import {
getFlattenedSections,
@@ -16,6 +8,13 @@ import { getRefMarkdown } from '~/features/docs/Reference.mdx'
import type { MethodTypes, VariableTypes } from '~/features/docs/Reference.typeSpec'
import type { AbbrevApiReferenceSection } from '~/features/docs/Reference.utils'
import { BASE_PATH } from '~/lib/constants'
import { toHtml } from 'hast-util-to-html'
import { fromMarkdown } from 'mdast-util-from-markdown'
import { mdxFromMarkdown } from 'mdast-util-mdx'
import { toHast } from 'mdast-util-to-hast'
import { mdxjs } from 'micromark-extension-mdxjs'
import { notFound } from 'next/navigation'
import { visit } from 'unist-util-visit'
export async function GET(request: Request) {
const url = new URL(request.url)
@@ -138,7 +137,7 @@ async function functionDetails(
let types: MethodTypes | VariableTypes | undefined
if (libraryMeta.typeSpec && '$ref' in fn) {
types = await getTypeSpec(fn['$ref'] as string)
types = await getTypeSpec(lib, version ?? libraryMeta.versions[0], fn['$ref'] as string)
}
const fullDescription = [
@@ -151,7 +150,7 @@ async function functionDetails(
.join('')
const parameters = parametersToHtml(fn, types)
const examples = examplesToHtml(fn)
const examples = examplesToHtml(fn, types)
return fullDescription + parameters + examples
}
@@ -219,13 +218,24 @@ function parametersToHtml(fn: any, types: MethodTypes | VariableTypes | undefine
return result
}
function examplesToHtml(fn: any) {
if (!fn.examples || fn.examples.length === 0) return ''
function examplesToHtml(fn: any, types?: MethodTypes | VariableTypes) {
// Prefer hand-authored YAML/JSON examples on the section entry; fall back to
// TSDoc-extracted `@example` blocks on the method's normalised comment. The
// page renderer in `Reference.sections.tsx` does the same merge, so the
// crawler stays consistent with what a browser sees.
const examples =
Array.isArray(fn.examples) && fn.examples.length > 0
? fn.examples
: (types?.comment?.examples ?? [])
if (examples.length === 0) return ''
let result = '<h2 id="examples">Examples</h2>'
result += fn.examples
.map((example) => `<h3>${example.name ?? ''}</h3>` + mdxToHtml(example.code ?? ''))
result += examples
.map(
(example: { name?: string; code?: string }) =>
`<h3>${example.name ?? ''}</h3>` + mdxToHtml(example.code ?? '')
)
.join('')
return result
+3 -1
View File
@@ -19,8 +19,10 @@ export const REFERENCES = {
icon: 'reference-javascript',
meta: {
v2: {
// JS v2 is driven by the new reference pipeline
// (`scripts/build-reference-content.ts` + `spec/reference/javascript/v2/`).
// It intentionally has no `specFile` — the legacy YAML loader skips it.
libId: 'reference_javascript_v2',
specFile: 'supabase_js_v2',
},
v1: {
libId: 'reference_javascript_v1',
@@ -0,0 +1,12 @@
---
id: 'auth-mfa'
title: Auth MFA
---
This section contains methods commonly used for Multi-Factor Authentication (MFA) and are invoked behind the `supabase.auth.mfa` namespace.
Currently, there is support for time-based one-time password (TOTP) and phone verification code as the 2nd factor. Recovery codes are not supported but users can enroll multiple factors, with an upper limit of 10.
Having a 2nd factor for recovery frees the user of the burden of having to store their recovery codes somewhere. It also reduces the attack surface since multiple recovery codes are usually generated compared to just having 1 backup factor.
Learn more about implementing MFA in your application [in the MFA guide](https://supabase.com/docs/guides/auth/auth-mfa#overview).
@@ -0,0 +1,6 @@
---
id: passkey-admin
title: Passkey admin
---
Contains passkey administration methods. Requires a secret key.
@@ -0,0 +1,19 @@
/**
* Standalone constants for the reference pipeline.
*
* This file intentionally has no runtime dependencies — and in particular,
* it must NOT import anything that transitively pulls in `next/navigation`,
* `next/headers`, or any other client-only module. It is loaded by both UI
* code (e.g. `Reference.generated.singleton.ts` used in app routes) and
* server-only scripts run with `tsx --conditions=react-server`
* (e.g. `scripts/llms.ts`), where the server React build lacks
* `createContext` and Next.js client modules crash on import.
*/
/**
* SDK/version pairs (`${sdkId}-${version}`) that opt into the new reference
* content pipeline produced by `scripts/build-reference-content.ts`. Anything
* not listed here keeps reading from the legacy `features/docs/generated/`
* outputs.
*/
export const SUPPORTS_NEW_REFERENCE_PROCESS = new Set(['javascript-v2'])
@@ -1,12 +1,8 @@
import { isPlainObject, keyBy } from 'lodash-es'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import slugify from 'slugify'
import { parse } from 'yaml'
import { clientSdkIds, REFERENCES } from '~/content/navigation.references'
import { parseTypeSpec } from '~/features/docs/Reference.typeSpec'
import { SUPPORTS_NEW_REFERENCE_PROCESS } from '~/features/docs/Reference.constants'
import type { AbbrevApiReferenceSection } from '~/features/docs/Reference.utils'
import { deepFilterRec } from '~/features/helpers.fn'
import type { Json } from '~/features/helpers.types'
@@ -22,6 +18,10 @@ import selfHostingStorageCommonSections from '~/spec/common-self-hosting-storage
import storageSpec from '~/spec/storage_v0_openapi.json' with { type: 'json' }
import analyticsSpec from '~/spec/transforms/analytics_v0_openapi_deparsed.json' with { type: 'json' }
import openApiSpec from '~/spec/transforms/api_v1_openapi_deparsed.json' with { type: 'json' }
import { isPlainObject, keyBy } from 'lodash-es'
import slugify from 'slugify'
import { parse } from 'yaml'
import { IApiEndPoint } from './Reference.api.utils'
const DOCS_DIRECTORY = join(dirname(fileURLToPath(import.meta.url)), '../..')
@@ -155,21 +155,6 @@ export function flattenCommonClientLibSections(tree: Array<AbbrevApiReferenceSec
}, [] as Array<AbbrevApiReferenceSection>)
}
async function writeTypes() {
const types = await parseTypeSpec()
await writeFile(
join(GENERATED_DIRECTORY, 'typeSpec.json'),
JSON.stringify(types, (key, value) => {
if (key === 'methods' || key === 'variables') {
return Object.fromEntries(value.entries())
} else {
return value
}
})
)
}
async function writeSdkReferenceSections() {
return Promise.all(
clientSdkIds
@@ -180,6 +165,10 @@ async function writeSdkReferenceSections() {
version,
}))
})
// Libs that opted into the new pipeline emit their own outputs via
// `scripts/build-reference-content.ts`. Skip them here so the legacy
// script doesn't need a YAML spec file for them at all.
.filter(({ sdkId, version }) => !SUPPORTS_NEW_REFERENCE_PROCESS.has(`${sdkId}-${version}`))
.flatMap(async ({ sdkId, version }) => {
const spec = await getSpec(REFERENCES[sdkId].meta[version].specFile)
@@ -347,7 +336,6 @@ async function run() {
await mkdir(GENERATED_DIRECTORY, { recursive: true })
await Promise.all([
writeTypes(),
writeSdkReferenceSections(),
writeCliReferenceSections(),
writeApiReferenceSections(),
@@ -1,46 +1,73 @@
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'
import { SUPPORTS_NEW_REFERENCE_PROCESS } from '~/features/docs/Reference.constants'
import type { MethodTypes, VariableTypes } from '~/features/docs/Reference.typeSpec'
import type { AbbrevApiReferenceSection } from '~/features/docs/Reference.utils'
import { parse } from 'yaml'
import type { ModuleTypes } from '~/features/docs/Reference.typeSpec'
import type { AbbrevApiReferenceSection } from '~/features/docs/Reference.utils'
import { type Json } from '../helpers.types'
import { type IApiEndPoint } from './Reference.api.utils'
let typeSpec: Array<ModuleTypes>
async function _typeSpecSingleton() {
if (!typeSpec) {
const rawJson = await readFile(
join(process.cwd(), 'features/docs', './generated/typeSpec.json'),
'utf-8'
)
typeSpec = JSON.parse(rawJson, (key, value) => {
if (key === 'methods' || key === 'variables') {
return new Map(Object.entries(value))
} else {
return value
}
})
/**
* Resolves the on-disk path for a generated SDK reference file. For libraries
* listed in `SUPPORTS_NEW_REFERENCE_PROCESS` we read from the new
* `content/reference/<sdk>/<version>/<name>.json` layout produced by
* `scripts/build-reference-content.ts`; otherwise fall back to the legacy
* `features/docs/generated/<sdk>.<version>.<name>.json` files (still used by
* SDKs whose content hasn't migrated to the new pipeline).
*/
function generatedReferencePath(sdkId: string, version: string, name: string): string {
if (SUPPORTS_NEW_REFERENCE_PROCESS.has(`${sdkId}-${version}`)) {
return join(process.cwd(), 'content/reference', sdkId, version, `${name}.json`)
}
return typeSpec
return join(process.cwd(), 'features/docs/generated', `${sdkId}.${version}.${name}.json`)
}
function normalizeRefPath(path: string) {
return path.replace(/\.index(?=\.|$)/g, '').replace(/\.+/g, '.')
}
export async function getTypeSpec(ref: string) {
const modules = await _typeSpecSingleton()
/**
* Per-lib typeSpec cache. Each entry is the parsed
* `content/reference/<sdk>/<version>/typeSpec.json` — a `{ methods, variables }`
* object keyed by normalised `$ref`. `typeSpec: true` in
* `content/navigation.references.ts` is set at the library level, so it
* applies to every version of that library — but only versions in
* `SUPPORTS_NEW_REFERENCE_PROCESS` actually have a typeSpec file. For
* versions without one (legacy versions like javascript-v1), we return an
* empty spec so the renderer simply omits signature/comment data instead of
* crashing the build.
*/
type TypeSpecFile = {
methods: Record<string, MethodTypes>
variables: Record<string, VariableTypes>
}
const EMPTY_TYPESPEC: TypeSpecFile = { methods: {}, variables: {} }
const typeSpecCache = new Map<string, TypeSpecFile>()
async function loadTypeSpec(sdkId: string, version: string): Promise<TypeSpecFile> {
const key = `${sdkId}.${version}`
const cached = typeSpecCache.get(key)
if (cached) return cached
try {
const rawJson = await readFile(generatedReferencePath(sdkId, version, 'typeSpec'), 'utf-8')
const parsed = JSON.parse(rawJson) as TypeSpecFile
typeSpecCache.set(key, parsed)
return parsed
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
typeSpecCache.set(key, EMPTY_TYPESPEC)
return EMPTY_TYPESPEC
}
throw err
}
}
export async function getTypeSpec(sdkId: string, version: string, ref: string) {
const spec = await loadTypeSpec(sdkId, version)
const normalizedRef = normalizeRefPath(ref)
const delimiter = normalizedRef.indexOf('.')
const refMod = normalizedRef.substring(0, delimiter)
const mod = modules.find((mod) => mod.name === refMod)
// Check methods first, then variables
return mod?.methods.get(normalizedRef) ?? mod?.variables.get(normalizedRef)
return spec.methods[normalizedRef] ?? spec.variables[normalizedRef]
}
let cliSpec: Json
@@ -87,11 +114,7 @@ const functionsList = new Map<string, Array<{ id: unknown }>>()
export async function getFunctionsList(sdkId: string, version: string) {
const key = `${sdkId}.${version}`
if (!functionsList.has(key)) {
const data = await readFile(
join(process.cwd(), 'features/docs', `./generated/${sdkId}.${version}.functions.json`),
'utf-8'
)
const data = await readFile(generatedReferencePath(sdkId, version, 'functions'), 'utf-8')
functionsList.set(key, JSON.parse(data))
}
@@ -103,11 +126,7 @@ const referenceSections = new Map<string, Array<AbbrevApiReferenceSection>>()
export async function getReferenceSections(sdkId: string, version: string) {
const key = `${sdkId}.${version}`
if (!referenceSections.has(key)) {
const data = await readFile(
join(process.cwd(), 'features/docs', `./generated/${sdkId}.${version}.sections.json`),
'utf-8'
)
const data = await readFile(generatedReferencePath(sdkId, version, 'sections'), 'utf-8')
referenceSections.set(key, JSON.parse(data))
}
@@ -120,11 +139,7 @@ const flatSections = new Map<string, Array<AbbrevApiReferenceSection>>()
export async function getFlattenedSections(sdkId: string, version: string) {
const key = `${sdkId}.${version}`
if (!flatSections.has(key)) {
const data = await readFile(
join(process.cwd(), 'features/docs', `./generated/${sdkId}.${version}.flat.json`),
'utf-8'
)
const data = await readFile(generatedReferencePath(sdkId, version, 'flat'), 'utf-8')
flatSections.set(key, JSON.parse(data))
}
@@ -137,12 +152,8 @@ const sectionsBySlug = new Map<string, Map<string, AbbrevApiReferenceSection>>()
export async function getSectionsBySlug(sdkId: string, version: string) {
const key = `${sdkId}.${version}`
if (!sectionsBySlug.has(key)) {
const data = await readFile(
join(process.cwd(), 'features/docs', `./generated/${sdkId}.${version}.bySlug.json`),
'utf-8'
)
const data = await readFile(generatedReferencePath(sdkId, version, 'bySlug'), 'utf-8')
const asObject = JSON.parse(data)
sectionsBySlug.set(key, new Map(Object.entries(asObject)))
}
@@ -353,7 +353,6 @@ function CompoundRefLink({
className={cn('border-l border-control pl-3 ml-1 data-open:mt-2 grid gap-2.5')}
>
<ul className="space-y-2">
<RefLink basePath={basePath} section={section} skipChildren />
{(section.items || []).map((item, idx) => {
return (
<li key={`${section.id}-${idx}`}>
@@ -472,7 +472,7 @@ async function FunctionSection({
let types: MethodTypes | VariableTypes | undefined
if (useTypeSpec && '$ref' in fn) {
types = await getTypeSpec(fn['$ref'] as string)
types = await getTypeSpec(sdkId, version, fn['$ref'] as string)
}
const fullDescription = [
@@ -1,21 +0,0 @@
import { describe, expect, it } from 'vitest'
import { parseTypeSpec } from './Reference.typeSpec'
describe('TS type spec parsing', () => {
it('matches snapshot', async () => {
const parsed = await parseTypeSpec()
const json = JSON.stringify(
parsed,
(key, value) => {
if (key === 'methods') {
return Object.fromEntries(value.entries())
} else {
return value
}
},
2
)
expect(json).toMatchSnapshot()
})
})
+25 -251
View File
@@ -1,20 +1,21 @@
/**
* Types and additional comments for the JavaScript library are found in the
* auto-generated TS typespec.
* Type definitions and TypeDoc normalisation helpers shared between the build
* script (`scripts/build-reference-content.ts`) and the renderer
* (`Reference.sections.tsx`, `Reference.ui.tsx`).
*
* The format of this typespec is difficult to walk, so we re-shape it for easy
* access to a function's type definition, given its name and module.
* The build script walks each per-package TypeDoc dump and calls
* `parseSignature` / `parseType` / `normalizeComment` to produce the
* `MethodTypes` / `VariableTypes` entries the renderer consumes. This module
* is I/O-free so it can be imported from any environment (Node scripts, RSC,
* tests).
*/
import _typeSpec from '~/spec/enrichments/tsdoc_v2/combined.json' with { type: 'json' }
// [Charis] 2024-07-10
// Types are more trouble than they're worth here: manually defining the types
// (correctly) is tedious, and inferring them will throw up a lot of type
// errors. As long as everything is typed on the way out, and we code
// defensively, it should be fine. Keep any unsafe type shenanigans isolated
// to this file.
const typeSpec = _typeSpec as any
export const TYPESPEC_NODE_ANONYMOUS = Symbol('anonymous')
@@ -54,7 +55,7 @@ export interface VariableTypes {
isConst?: boolean
}
interface Comment {
export interface Comment {
shortText?: string
text?: string
tags?: Array<{ tag: string; text: string }>
@@ -177,14 +178,14 @@ export interface CustomTypePropertyType {
// The meaning of kind flags from `typedoc`:
// https://github.com/TypeStrong/typedoc/blob/2953b0148253589448176881a7acb46090f941bd/src/lib/output/themes/default/assets/typedoc/Application.ts#L36
const KIND_MODULE = 2
const KIND_VARIABLE = 32
const KIND_CLASS = 128
const KIND_INTERFACE = 256
const KIND_CONSTRUCTOR = 512
const KIND_PROPERTY = 1024
const KIND_METHOD = 2048
const KIND_TYPE_LITERAL = 65536
export const KIND_MODULE = 2
export const KIND_VARIABLE = 32
export const KIND_CLASS = 128
export const KIND_INTERFACE = 256
export const KIND_CONSTRUCTOR = 512
export const KIND_PROPERTY = 1024
export const KIND_METHOD = 2048
export const KIND_TYPE_LITERAL = 65536
/**
*
@@ -231,7 +232,9 @@ interface CommentBlockTag {
content: CommentKind[]
}
function normalizeComment(original: TypedocComment | Comment | undefined): Comment | undefined {
export function normalizeComment(
original: TypedocComment | Comment | undefined
): Comment | undefined {
if (!original) return
if ('shortText' in original || 'text' in original) {
@@ -345,41 +348,11 @@ function normalizeComment(original: TypedocComment | Comment | undefined): Comme
return comment
}
export function parseTypeSpec() {
const modules = (typeSpec.children ?? []).map(parseMod)
return modules as Array<ModuleTypes>
}
function normalizeRefPath(path: string) {
export function normalizeRefPath(path: string) {
return path.replace(/\.index(?=\.|$)/g, '').replace(/\.+/g, '.')
}
function buildRefPath(segments: Array<string>) {
return normalizeRefPath(segments.filter(Boolean).join('.'))
}
// Reading the type spec happens in several layers. The first layer is the
// module: this corresponds roughly to the JS libraries for each product:
// database, auth, storage, etc.
function parseMod(mod: (typeof typeSpec)['children'][number]) {
const res: ModuleTypes = {
name: mod.name,
methods: new Map(),
variables: new Map(),
}
// Build a map of nodes by their IDs for easy cross-referencing.
const targetMap = new Map<number, any>()
buildMap(mod, targetMap)
const processingRefs = new Set<number>()
parseModInternal(mod, targetMap, [], res, processingRefs)
return res
}
function buildMap(node: any, map: Map<number, any>) {
export function buildMap(node: any, map: Map<number, any>) {
if ('id' in node) {
map.set(node.id, node)
}
@@ -388,127 +361,12 @@ function buildMap(node: any, map: Map<number, any>) {
}
}
// This layer is the top level of a module. Paths are tracked in this layer,
// because they are needed to construct the $ref that will be used to reference
// a specific type definition.
function parseModInternal(
node: any,
map: Map<number, any>,
currentPath: Array<string>,
res: ModuleTypes,
processingRefs: Set<number>
) {
let updatedPath: Array<string>
switch ((node.kindString ?? node.variant)?.toLowerCase()) {
case 'module':
updatedPath = [...currentPath, node.name]
node.children?.forEach((child: any) =>
parseModInternal(child, map, updatedPath, res, processingRefs)
)
return
// Some libraries have undefined where others have Project or declaration // for the same type of top-level node.
case 'project':
case undefined:
updatedPath = [...currentPath, node.name]
node.children?.forEach((child: any) =>
parseModInternal(child, map, updatedPath, res, processingRefs)
)
return
case 'class':
updatedPath = [...currentPath, node.name]
node.children?.forEach((child: any) =>
parseModInternal(child, map, updatedPath, res, processingRefs)
)
return
case 'constructor':
return parseConstructor(node, map, currentPath, res)
case 'method':
return parseMethod(node, map, currentPath, res)
case 'interface':
updatedPath = [...currentPath, node.name]
node.children?.forEach((child: any) =>
parseModInternal(child, map, updatedPath, res, processingRefs)
)
return
case 'declaration':
if (node.kind === KIND_CLASS || node.kind === KIND_MODULE) {
updatedPath = [...currentPath, node.name]
node.children?.forEach((child: any) =>
parseModInternal(child, map, updatedPath, res, processingRefs)
)
} else if (node.kind === KIND_INTERFACE) {
updatedPath = [...currentPath, node.name]
node.children?.forEach((child: any) =>
parseModInternal(child, map, updatedPath, res, processingRefs)
)
} else if (node.kind === KIND_CONSTRUCTOR) {
parseConstructor(node, map, currentPath, res)
} else if (node.kind === KIND_METHOD) {
return parseMethod(node, map, currentPath, res)
} else if (node.kind === KIND_VARIABLE) {
return parseVariable(node, map, currentPath, res)
} else if (node.kind === KIND_PROPERTY) {
parsePropertyReference(node, map, currentPath, res, processingRefs)
}
return
case 'property':
parsePropertyReference(node, map, currentPath, res, processingRefs)
return
case 'reference':
default:
return
}
}
function parsePropertyReference(
node: any,
map: Map<number, any>,
currentPath: Array<string>,
res: ModuleTypes,
processingRefs: Set<number>
) {
const refType = node.type
if (refType?.type !== 'reference') {
return
}
const referent = map.get(refType.target ?? refType.id)
if (!referent) {
return
}
if (processingRefs.has(referent.id)) {
return
}
const isForwardedNamespace =
referent?.variant === 'declaration' &&
(referent.kind === KIND_INTERFACE ||
referent.kind === KIND_CLASS ||
referent.kind === KIND_MODULE)
if (!isForwardedNamespace) {
return
}
const parentPath =
currentPath.length > 0 && currentPath[currentPath.length - 1]?.startsWith('@supabase/')
? currentPath
: currentPath.slice(0, -1)
processingRefs.add(referent.id)
parseModInternal(referent, map, parentPath, res, processingRefs)
processingRefs.delete(referent.id)
}
/**
* Get the name for a node, which may be delegated or missing (placeholders
* are prefaced by two `__` in the type spec). If missing, use the anonymous
* symbol.
*/
function nameOrAnonymous(nodes: any) {
export function nameOrAnonymous(nodes: any) {
if (!Array.isArray(nodes)) {
nodes = [nodes]
}
@@ -522,91 +380,7 @@ function nameOrAnonymous(nodes: any) {
return TYPESPEC_NODE_ANONYMOUS
}
function parseConstructor(
node: any,
map: Map<number, any>,
currentPath: Array<string>,
res: ModuleTypes
) {
const $ref = buildRefPath([...currentPath, 'constructor'])
const signature = node.signatures[0]
if (!signature) return
const { params, ret, comment } = parseSignature(signature, map)
const types: MethodTypes = {
name: $ref,
params,
ret,
comment,
}
res.methods.set($ref, types)
}
function parseMethod(
node: any,
map: Map<number, any>,
currentPath: Array<string>,
res: ModuleTypes
) {
const $ref = buildRefPath([...currentPath, node.name])
const signature = node.signatures[0]
if (!signature) return
let { params, ret, comment } = parseSignature(signature, map)
// When a method has multiple overload signatures, TypeDoc places the shared
// JSDoc on the method node rather than any individual signature. Always merge
// node.comment as the base so that block tags (@remarks, @example, etc.) are
// not lost when overload signatures already carry a minimal summary comment.
if (node.comment) {
const nodeComment = normalizeComment(node.comment)
if (nodeComment) {
comment = { ...nodeComment, ...comment }
}
}
const types: MethodTypes = {
name: $ref,
params,
ret,
comment,
}
if (node.signatures.length > 1) {
types.altSignatures = node.signatures
.slice(1)
.map((signature) => parseSignature(signature, map))
}
res.methods.set($ref, types)
}
function parseVariable(
node: any,
map: Map<number, any>,
currentPath: Array<string>,
res: ModuleTypes
) {
const $ref = buildRefPath([...currentPath, node.name])
const type = parseType(node.type, map)
const comment = node.comment ? normalizeComment(node.comment) : undefined
const types: VariableTypes = {
name: $ref,
type,
comment,
isConst: node.flags?.isConst ?? false,
}
res.variables.set($ref, types)
}
function parseSignature(
export function parseSignature(
signature: any,
map: Map<number, any>
): {
@@ -662,7 +436,7 @@ function parseSignature(
//
// with additional properties depending on the type.
function parseType(type: any, map: Map<number, any>, typeArguments?: any, debug = false) {
export function parseType(type: any, map: Map<number, any>, typeArguments?: any, debug = false) {
switch (type.type) {
case 'literal':
return type
+7 -6
View File
@@ -1,3 +1,8 @@
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'
@@ -6,12 +11,6 @@ import type { Metadata, ResolvingMetadata } from 'next'
import { redirect } from 'next/navigation'
import { visit } from 'unist-util-visit'
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'
const { metadataTitle } = getCustomContent(['metadata:title'])
export interface AbbrevApiReferenceSection {
@@ -270,3 +269,5 @@ export function normalizeMarkdown(markdownUnescaped: string): string {
return content
}
export { SUPPORTS_NEW_REFERENCE_PROCESS } from '~/features/docs/Reference.constants'
File diff suppressed because it is too large. Load diff
+4 -2
View File
@@ -13,6 +13,8 @@
"clean": "rimraf .next .turbo node_modules features/docs/generated examples __generated__",
"codegen:examples": "shx cp -r ../../examples ./examples",
"codegen:graphql": "tsx --conditions=react-server ./scripts/graphqlSchema.ts && graphql-codegen --config codegen.ts",
"codegen:references:ensure": "test -f spec/reference/javascript/v2/supabase.json || (cd spec && make download.tsdoc.v2)",
"codegen:references:new": "pnpm run codegen:references:ensure && tsx scripts/build-reference-content.ts",
"codegen:references": "tsx features/docs/Reference.generated.script.ts",
"codemod:frontmatter": "node ./scripts/codemod/mdx-meta.mjs && prettier --cache --write \"content/**/*.mdx\"",
"dev": "concurrently --kill-others \"next dev --port 3001\" \"pnpm run dev:watch:troubleshooting\"",
@@ -27,8 +29,8 @@
"lint": "eslint .",
"lint:mdx": "supa-mdx-lint content --config ../../supa-mdx-lint.config.toml",
"postbuild": "pnpm run build:sitemap && pnpm run build:llms && ./../../scripts/upload-static-assets.sh",
"prebuild": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples && pnpm run build:guides-markdown && pnpm run build:gz-archive",
"predev": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples",
"prebuild": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:references:new && pnpm run codegen:examples && pnpm run build:guides-markdown && pnpm run build:gz-archive",
"predev": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:references:new && pnpm run codegen:examples",
"preembeddings": "pnpm run codegen:references",
"preinstall": "npx only-allow pnpm",
"presync": "pnpm run codegen:graphql",
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest'
import { collectReferenceContent } from './build-reference-content'
/**
* Regression guard for the new reference-content pipeline. Snapshots the five
* derived artifacts produced for `javascript/v2` so that any change in the
* extraction logic (or in the upstream supabase-js TypeDoc output) shows up
* as a snapshot diff in CI rather than silently shifting what the renderer
* sees. When the supabase-js `make` workflow lands a new release in
* `spec/reference/javascript/v2/`, re-run with `--update` to refresh the
* baseline as part of the same PR.
*/
describe('build-reference-content — javascript/v2', () => {
it('matches snapshot', async () => {
const { bySlug, flat, sections, functionsList, typeSpec } = await collectReferenceContent(
'javascript',
'v2'
)
expect({ bySlug, flat, sections, functionsList, typeSpec }).toMatchSnapshot()
})
})
@@ -0,0 +1,696 @@
/**
* Build reference content files (bySlug.json, etc.) from TypeDoc spec output.
*
* Scans `spec/reference/[library]/[version]/*.json` (TypeDoc output) and writes
* `content/reference/[library]/[version]/{bySlug,flat,sections,functions,typeSpec}.json`.
*
* Library and version names are inferred from the directory layout. An optional
* `config.json` in each version directory may declare `excludeCategories` and
* `categoryOrder` to filter and order the output.
*
* Usage: pnpm tsx scripts/build-reference-content.ts
*/
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
import { dirname, extname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import matter from 'gray-matter'
import {
buildMap,
KIND_VARIABLE,
normalizeComment,
parseSignature,
parseType,
type MethodTypes,
type VariableTypes,
} from '../features/docs/Reference.typeSpec'
const __dirname = dirname(fileURLToPath(import.meta.url))
const DOCS_DIR = join(__dirname, '..')
const SPEC_DIR = join(DOCS_DIR, 'spec/reference')
const OUTPUT_DIR = join(DOCS_DIR, 'content/reference')
const REF_DOCS_DIR = join(DOCS_DIR, 'docs/ref')
type ContentPart = { kind: string; text?: string }
interface BlockTag {
tag: string
name?: string
content: ContentPart[]
}
interface Comment {
summary?: ContentPart[]
blockTags?: BlockTag[]
}
interface Param {
name: string
type: unknown
flags?: { isOptional?: boolean }
}
interface Declaration {
id?: number
name?: string
variant?: string
kind?: number
comment?: Comment
signatures?: Declaration[]
children?: Declaration[]
// signature-only / variable-only fields:
parameters?: Param[]
type?: unknown
flags?: { isOptional?: boolean; isConst?: boolean }
}
interface FunctionEntry {
name: string
category: string
subcategory: string | null
$ref: string
}
interface FunctionsEntry {
id: string
// Either a `$ref` (points at a typeSpec entry) OR rich inline content
// (description, examples, …) shovelled in from a .json partial body.
$ref?: string
[key: string]: unknown
}
/**
* Per-lib typeSpec.json shape. Keys are normalised `$ref`s; values mirror the
* legacy `MethodTypes` / `VariableTypes` so the renderer in
* `Reference.sections.tsx` can consume the new file without changes.
*/
interface TypeSpec {
methods: Record<string, MethodTypes>
variables: Record<string, VariableTypes>
}
interface BySlugFunction {
id: string
title: string
slug: string
product?: string
type: 'function'
isFunc?: false
}
interface BySlugCategory {
type: 'category'
title: string
}
interface BySlugMarkdown {
id: string
title: string
slug: string
type: 'markdown'
}
type BySlugEntry = BySlugFunction | BySlugCategory | BySlugMarkdown
/** A bySlug entry that may additionally carry nested children — used by sections.json. */
type SectionEntry = BySlugEntry & { items?: SectionEntry[] }
interface VersionConfig {
excludeCategories?: string[]
excludeDefinitions?: string[]
categoryOrder?: string[]
partialsOrder?: string[]
/**
* Customizes the navigation slug used as a prefix for a category or
* subcategory (matched on the literal `@category` / `@subcategory` text).
* - `string` → use that value as the prefix (e.g. `"Edge Functions": "functions"`
* turns `edge-functions-invoke` into `functions-invoke`).
* - `false` → drop the prefix entirely for child function slugs (e.g.
* `"Using modifiers": false` turns `using-modifiers-explain` into
* `explain`). The category/subcategory header itself still needs a slug,
* so its entry slug falls back to the slugified title.
* - missing → default to the slugified title.
*/
navigationPrefixes?: Record<string, string | false>
}
interface PartialEntry {
name: string
title: string
ref?: string
/**
* `kind` distinguishes how the partial contributes to the build:
* - `markdown`: an `.md`/`.mdx` partial. Rendered as a `type: 'markdown'`
* section (or `type: 'function'` when a frontmatter `ref` is present).
* - `function`: a `.json` partial whose body (description, examples, …)
* is appended to functions.json so the renderer can display it.
*/
kind: 'markdown' | 'function'
/** Raw parsed JSON body for `kind === 'function'` partials. */
body?: Record<string, unknown>
/** Raw frontmatter + body for `kind === 'markdown'` partials. */
mdxRaw?: string
}
/** Lowercases a string and collapses internal whitespace to hyphens for use as a URL slug. */
function slugifyTag(value: string): string {
return value.toLowerCase().trim().replace(/\s+/g, '-')
}
type NavigationPrefixes = Record<string, string | false> | undefined
/**
* The slug used for a category or subcategory **entry** (the header that
* appears in the navigation). `navigationPrefixes[title] = string` overrides
* the default; `false` and `undefined` both fall back to the slugified title
* because the entry still needs a stable, navigable slug.
*/
function entrySlug(title: string, navigationPrefixes: NavigationPrefixes): string {
const override = navigationPrefixes?.[title]
return typeof override === 'string' ? override : slugifyTag(title)
}
/**
* The prefix segment used in front of a function's name. `false` means "no
* prefix" — the function slug becomes just its lowercased name. `string`
* overrides the default; `undefined` falls back to the slugified title.
*/
function functionPrefix(title: string, navigationPrefixes: NavigationPrefixes): string | null {
const override = navigationPrefixes?.[title]
if (override === false) return null
if (typeof override === 'string') return override
return slugifyTag(title)
}
/**
* Stably reorders `items` so entries whose key appears in `order` come first in
* that order; unranked items keep their original relative order. Returns `items`
* unchanged when `order` is empty or undefined.
*/
function reorder<T>(items: T[], order: string[] | undefined, key: (item: T) => string): T[] {
if (!order?.length) return items
const idx = new Map(order.map((n, i) => [n, i]))
return [...items].sort((a, b) => (idx.get(key(a)) ?? Infinity) - (idx.get(key(b)) ?? Infinity))
}
/**
* Builds a bySlug entry for a partial file:
* - `.json` partials always render as `type: 'function'` (their body feeds
* functions.json).
* - `.md`/`.mdx` partials with a frontmatter `ref` render as `type: 'function'`
* (linked to TypeDoc-derived code via the ref).
* - All other `.md`/`.mdx` partials render as plain `type: 'markdown'`.
*/
const partialEntry = (p: PartialEntry): BySlugMarkdown | BySlugFunction =>
p.kind === 'function' || p.ref
? { id: p.name, title: p.title, slug: p.name, type: 'function' }
: { id: p.name, title: p.title, slug: p.name, type: 'markdown' }
/** Builds a category-type bySlug entry from a category title. */
const categoryEntry = (title: string): BySlugCategory => ({ type: 'category', title })
/** Builds a subcategory bySlug entry — shaped like a function with `isFunc: false`. */
const subcategoryEntry = (slug: string, title: string, product: string): BySlugFunction => ({
id: slug,
isFunc: false,
title,
slug,
product,
type: 'function',
})
/**
* Builds a function bySlug entry. The slug is `${prefix}-${name}` where
* `prefix` comes from the function's nearest container — its `@subcategory`
* if present, otherwise its `@category`. `navigationPrefixes` in `config.json`
* can rename that prefix or drop it entirely (false). `id` always equals
* `slug` so the renderer's `fns.find(f => f.id === section.id)` resolves.
*
* `product` keeps the literal slugified category (independent of any
* navigation prefix) because the renderer uses it for feature filtering
* (e.g. hiding `auth` sections when the SDK Auth flag is disabled).
*/
const functionEntry = (
fn: FunctionEntry,
product: string,
navigationPrefixes: NavigationPrefixes
): BySlugFunction => {
const prefix = functionPrefix(fn.subcategory ?? fn.category, navigationPrefixes)
const nameLower = fn.name.toLowerCase()
const slug = prefix === null ? nameLower : `${prefix}-${nameLower}`
return { id: slug, title: fn.name, slug, product, type: 'function' }
}
/**
* Reads a named TSDoc block tag (e.g. `@category`) from a comment and returns
* its first line trimmed, or `null` if absent.
*/
function readBlockTag(comment: Comment | undefined, tagName: string): string | null {
if (!comment?.blockTags) return null
const tag = comment.blockTags.find((t) => t.tag === tagName)
if (!tag) return null
const text = tag.content
.map((c) => c.text ?? '')
.join('')
.split('\n')[0]
.trim()
return text || null
}
/**
* Reads a block tag from a declaration's comment, falling back to its signature
* comments. TypeDoc emits tags in either place depending on the source style.
*/
function readTagFromDeclOrSignature(decl: Declaration, tagName: string): string | null {
const fromDecl = readBlockTag(decl.comment, tagName)
if (fromDecl) return fromDecl
if (decl.signatures) {
for (const sig of decl.signatures) {
const fromSig = readBlockTag(sig.comment, tagName)
if (fromSig) return fromSig
}
}
return null
}
/** Strips redundant `.index.` segments and collapses consecutive dots in a ref. */
function normalizeRefPath(path: string): string {
return path.replace(/\.index(?=\.|$)/g, '').replace(/\.+/g, '.')
}
/**
* Recursively walks a TypeDoc declaration tree. Builds two outputs in one pass:
* - `functions`: declarations carrying an `@category` tag (with optional
* `@subcategory`) plus a constructed `$ref` of the form
* `<package>.<module…>.<class…>.<name>` (normalized to strip `.index.`).
* - `typeSpec`: separate `methods` and `variables` maps keyed by the same
* `$ref`. Methods carry every signature (first → primary, rest →
* `altSignatures`) with params, return type, and normalised comment
* (shortText, text, tags, examples). Variables (kind 32) carry their
* parsed type and `isConst` flag. Type-tree normalisation and comment
* extraction are delegated to the shared helpers in
* `~/features/docs/Reference.typeSpec` so the renderer can consume the
* output without any shape translation.
*
* Not filtered by category or `excludeDefinitions`, so partials that link
* to "hidden" methods (e.g. a constructor) can still resolve.
*
* Context is threaded down through container nodes:
* - kind 1 (project) sets the package name and resets the path.
* - kinds 2 (module), 4 (namespace), 128 (class), 256 (interface) all
* append their name to the path. Modules are required because some
* packages (storage-js, supabase-js) wrap each source file in a module
* like `packages/StorageFileApi`, and a class named `default` (TypeDoc's
* fallback for default-exported classes) is ambiguous without the module
* segment. Interfaces hold many of the auth admin APIs whose methods
* would otherwise lack a class segment in the ref.
*/
const PATH_CONTAINER_KINDS = new Set([2, 4, 128, 256])
function collectFunctions(
node: Declaration,
out: { functions: FunctionEntry[]; typeSpec: TypeSpec },
idMap: Map<number, any>,
ctx: { pkg: string | null; path: string[] } = { pkg: null, path: [] }
): void {
let nextCtx = ctx
if (node.kind === 1 && node.name) {
nextCtx = { pkg: node.name, path: [] }
} else if (node.kind && PATH_CONTAINER_KINDS.has(node.kind) && node.name) {
nextCtx = { ...ctx, path: [...ctx.path, node.name] }
}
if (ctx.pkg && node.name) {
const ref = normalizeRefPath([ctx.pkg, ...ctx.path, node.name].join('.'))
if (node.signatures?.length) {
const firstSig = node.signatures[0]
const { params, ret, comment: sigComment } = parseSignature(firstSig, idMap)
// Some overloaded methods carry shared JSDoc (e.g. @remarks, @example)
// on the declaration node rather than any individual signature. Merge
// node-level tags as a base so they aren't lost when the first
// signature only has a summary.
let comment = sigComment
if (node.comment) {
const nodeComment = normalizeComment(node.comment as any)
if (nodeComment) {
comment = { ...nodeComment, ...sigComment }
}
}
const methodEntry: MethodTypes = { name: ref, params, ret, comment }
if (node.signatures.length > 1) {
methodEntry.altSignatures = node.signatures.slice(1).map((sig) => {
const { params: altParams, ret: altRet } = parseSignature(sig, idMap)
return { params: altParams, ret: altRet }
}) as MethodTypes['altSignatures']
}
out.typeSpec.methods[ref] = methodEntry
} else if (node.kind === KIND_VARIABLE && node.type) {
const variableEntry: VariableTypes = {
name: ref,
type: parseType(node.type, idMap),
comment: node.comment ? normalizeComment(node.comment as any) : undefined,
isConst: node.flags?.isConst ?? false,
}
out.typeSpec.variables[ref] = variableEntry
}
}
if (ctx.pkg && node.name && node.variant === 'declaration') {
const category = readTagFromDeclOrSignature(node, '@category')
if (category) {
const subcategory = readTagFromDeclOrSignature(node, '@subcategory')
const $ref = normalizeRefPath([ctx.pkg, ...ctx.path, node.name].join('.'))
out.functions.push({ name: node.name, category, subcategory, $ref })
}
}
if (node.children) {
for (const child of node.children) {
collectFunctions(child, out, idMap, nextCtx)
}
}
}
/** Reads `config.json` from a version directory, returning `{}` if the file is missing. */
async function readConfig(versionDir: string): Promise<VersionConfig> {
try {
const raw = await readFile(join(versionDir, 'config.json'), 'utf-8')
return JSON.parse(raw) as VersionConfig
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return {}
throw err
}
}
/**
* Reads every `.mdx` / `.md` / `.json` file from the `partials/` subdirectory.
* Markdown partials (`.md`, `.mdx`) have their frontmatter parsed for `title`
* and `ref`; JSON partials are parsed as objects and their full body kept so
* it can be emitted into functions.json. Returns alphabetically-sorted entries
* (case-insensitive); returns `[]` if `partials/` doesn't exist.
*/
async function readPartials(versionDir: string): Promise<PartialEntry[]> {
const partialsDir = join(versionDir, 'partials')
let files: string[]
try {
files = await readdir(partialsDir)
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return []
throw err
}
const partials: PartialEntry[] = []
for (const file of files) {
const ext = extname(file)
const name = file.slice(0, -ext.length)
const raw = await readFile(join(partialsDir, file), 'utf-8')
if (ext === '.mdx' || ext === '.md') {
// `trimStart()` so frontmatter is parsed even when the file accidentally
// starts with blank lines before `---`.
const trimmed = raw.trimStart()
const { data } = matter(trimmed)
const title = typeof data.title === 'string' ? data.title : name
const ref = typeof data.ref === 'string' ? data.ref : undefined
partials.push({ name, title, ref, kind: 'markdown', mdxRaw: trimmed })
} else if (ext === '.json') {
const body = JSON.parse(raw) as Record<string, unknown>
const title = typeof body.title === 'string' ? body.title : name
partials.push({ name, title, kind: 'function', body })
}
}
// Default to alphabetical so `reorder` can place ranked items first and leave
// the rest in a deterministic order (Array.sort is stable since ES2019).
return partials.sort((a, b) => a.name.localeCompare(b.name))
}
/**
* Builds bySlug (a flat slug→entry map), sections (the same data shaped as a
* nested tree: partials, then each category containing its functions and
* subcategories), and a functions list (id→$ref pairs for functions.json) in
* a single pass. Categories are filtered by `excludeCategories` and ordered by
* `categoryOrder`; individual declarations are filtered by `excludeDefinitions`
* (matched on the source name, case-sensitive). Partials with a `ref` in
* frontmatter are emitted as `type: 'function'` entries and contribute to
* the functions list.
*/
function buildBySlug(
functions: FunctionEntry[],
partials: PartialEntry[],
config: VersionConfig
): {
bySlug: Record<string, BySlugEntry>
sections: SectionEntry[]
functionsList: FunctionsEntry[]
} {
const bySlug: Record<string, BySlugEntry> = {}
const sections: SectionEntry[] = []
const functionsList: FunctionsEntry[] = []
const excludeCats = new Set(config.excludeCategories ?? [])
const excludeDefs = new Set(config.excludeDefinitions ?? [])
const filtered = functions.filter(
({ name, category }) => !excludeCats.has(category) && !excludeDefs.has(name)
)
// Bucket partials by where they belong. A partial filename that matches a
// category title slug (e.g. `database.md`) attaches to that category; one
// that matches a subcategory title slug (e.g. `using-filters.json`) attaches
// to that subcategory; everything else stays at the top level.
const categorySlugs = new Set(filtered.map(({ category }) => slugifyTag(category)))
const subcategorySlugs = new Set(
filtered.filter((f) => f.subcategory).map((f) => slugifyTag(f.subcategory!))
)
const partialsByCategory = new Map<string, PartialEntry>()
const partialsBySubcategory = new Map<string, PartialEntry>()
const topLevelPartials: PartialEntry[] = []
for (const p of partials) {
if (subcategorySlugs.has(p.name)) partialsBySubcategory.set(p.name, p)
else if (categorySlugs.has(p.name)) partialsByCategory.set(p.name, p)
else topLevelPartials.push(p)
}
const writePartial = (p: PartialEntry, items: SectionEntry[]) => {
const entry = partialEntry(p)
bySlug[p.name] = entry
items.push(entry)
if (p.kind === 'function') functionsList.push({ id: p.name, ...p.body })
else if (p.ref) functionsList.push({ id: p.name, $ref: p.ref })
}
for (const p of reorder(topLevelPartials, config.partialsOrder, (x) => x.name)) {
writePartial(p, sections)
}
type CategoryGroup = {
title: string
withoutSub: FunctionEntry[]
bySub: Map<string, FunctionEntry[]>
}
const groups = new Map<string, CategoryGroup>()
for (const fn of filtered) {
let group = groups.get(fn.category)
if (!group) {
group = { title: fn.category, withoutSub: [], bySub: new Map() }
groups.set(fn.category, group)
}
if (fn.subcategory) {
const bucket = group.bySub.get(fn.subcategory) ?? []
bucket.push(fn)
group.bySub.set(fn.subcategory, bucket)
} else {
group.withoutSub.push(fn)
}
}
const orderedCategories = reorder(Array.from(groups.keys()), config.categoryOrder, (c) => c)
const writeFunction = (fn: FunctionEntry, product: string, items: SectionEntry[]) => {
const entry = functionEntry(fn, product, config.navigationPrefixes)
// Spec files can re-declare same-named methods on different classes; the
// slug collides, so only emit each unique slug once (first wins).
if (entry.slug in bySlug) return
bySlug[entry.slug] = entry
items.push(entry)
// functions.json `id` must match the bySlug entry's `id` (not the slug) —
// the renderer in Reference.sections.tsx does `fns.find(f => f.id === section.id)`.
functionsList.push({ id: entry.id, $ref: fn.$ref })
}
// Sort items alphabetically (case-insensitive) within categories and within
// subcategories. Subcategories still appear at the end of each category (the
// order they are pushed below preserves that: withoutSub entries first, then
// each subcategory block).
const byName = (a: { name: string }, b: { name: string }) =>
a.name.toLowerCase().localeCompare(b.name.toLowerCase())
for (const category of orderedCategories) {
const group = groups.get(category)!
// `product` keeps the literal slugified category for feature filtering and
// for partial-filename matching, even when navigationPrefixes renames the
// category's navigation slug.
const product = slugifyTag(category)
const categorySlug = entrySlug(category, config.navigationPrefixes)
const cat = categoryEntry(group.title)
const catItems: SectionEntry[] = []
bySlug[categorySlug] = cat
sections.push({ ...cat, items: catItems })
const categoryPartial = partialsByCategory.get(product)
if (categoryPartial) writePartial(categoryPartial, catItems)
for (const fn of [...group.withoutSub].sort(byName)) writeFunction(fn, product, catItems)
const sortedSubs = [...group.bySub.entries()].sort(([a], [b]) =>
a.toLowerCase().localeCompare(b.toLowerCase())
)
for (const [subcategory, fns] of sortedSubs) {
const subKey = slugifyTag(subcategory)
const subSlug = entrySlug(subcategory, config.navigationPrefixes)
const sub = subcategoryEntry(subSlug, subcategory, product)
const subItems: SectionEntry[] = []
bySlug[subSlug] = sub
catItems.push({ ...sub, items: subItems })
const subcategoryPartial = partialsBySubcategory.get(subKey)
if (subcategoryPartial) writePartial(subcategoryPartial, subItems)
for (const fn of [...fns].sort(byName)) writeFunction(fn, product, subItems)
}
}
return { bySlug, sections, functionsList }
}
/**
* For each markdown-kind partial without a `ref`, writes its raw body
* (frontmatter + content) to `docs/ref/<library>/<name>.mdx` if that file
* does not already exist. The renderer's `getRefMarkdown` loads body text
* from this location, so new partials added under `spec/reference/.../partials/`
* become renderable without manual file shuffling. Existing files are left
* alone to preserve hand-maintained frontmatter (e.g. `hideTitle`).
*/
async function writeNewMarkdownPartials(library: string, partials: PartialEntry[]): Promise<void> {
const markdownPartials = partials.filter((p) => p.kind === 'markdown' && p.mdxRaw && !p.ref)
if (markdownPartials.length === 0) return
const outDir = join(REF_DOCS_DIR, library)
await mkdir(outDir, { recursive: true })
await Promise.all(
markdownPartials.map(async (p) => {
const target = join(outDir, `${p.name}.mdx`)
try {
// `wx` flag fails with EEXIST if the file is already there.
await writeFile(target, p.mdxRaw!, { flag: 'wx' })
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err
}
})
)
}
/**
* In-memory computation for a single `[library]/[version]`: reads spec files,
* partials, and config, walks every TypeDoc declaration, and returns the five
* derived artifacts (`bySlug`, `flat`, `sections`, `functionsList`,
* `typeSpec`) plus the partial list needed for downstream `.mdx` seeding.
*
* Exported so tests can snapshot the output shape without going through the
* filesystem.
*/
export async function collectReferenceContent(library: string, version: string) {
const versionDir = join(SPEC_DIR, library, version)
const files = (await readdir(versionDir)).filter(
(f) => f.endsWith('.json') && f !== 'config.json'
)
const config = await readConfig(versionDir)
const partials = await readPartials(versionDir)
const collected = {
functions: [] as FunctionEntry[],
typeSpec: { methods: {}, variables: {} } as TypeSpec,
}
for (const file of files) {
const raw = await readFile(join(versionDir, file), 'utf-8')
const spec = JSON.parse(raw) as Declaration
// Build a numeric id → node map per package file so `parseType`'s reference
// resolution can walk dereferenced types and aliased declarations.
const idMap = new Map<number, any>()
buildMap(spec, idMap)
collectFunctions(spec, collected, idMap)
}
const { bySlug, sections, functionsList } = buildBySlug(collected.functions, partials, config)
const flat = Object.values(bySlug)
return { bySlug, flat, sections, functionsList, typeSpec: collected.typeSpec, partials }
}
/**
* Processes one `[library]/[version]` directory: reads spec files, partials,
* and config, then writes all five output files (`bySlug.json`, `flat.json`,
* `sections.json`, `functions.json`, `typeSpec.json`) in parallel.
*/
async function processVersion(library: string, version: string): Promise<void> {
const { bySlug, flat, sections, functionsList, typeSpec, partials } =
await collectReferenceContent(library, version)
const counts = { markdown: 0, function: 0, subcategory: 0, category: 0 }
for (const v of flat) {
if (v.type === 'markdown') counts.markdown++
else if (v.type === 'category') counts.category++
else if ('isFunc' in v && v.isFunc === false) counts.subcategory++
else counts.function++
}
const outputDir = join(OUTPUT_DIR, library, version)
await mkdir(outputDir, { recursive: true })
await Promise.all([
writeFile(join(outputDir, 'bySlug.json'), JSON.stringify(bySlug)),
writeFile(join(outputDir, 'flat.json'), JSON.stringify(flat)),
writeFile(join(outputDir, 'sections.json'), JSON.stringify(sections)),
writeFile(join(outputDir, 'functions.json'), JSON.stringify(functionsList)),
writeFile(join(outputDir, 'typeSpec.json'), JSON.stringify(typeSpec)),
])
// The page renderer's `MarkdownSection` loads body text by id from
// `docs/ref/<library>/<id>.mdx`. Seed that file from any markdown partial
// whose runtime counterpart doesn't already exist (we don't overwrite
// hand-maintained legacy partials like introduction.mdx).
await writeNewMarkdownPartials(library, partials)
const typeSpecMethods = Object.keys(typeSpec.methods).length
const typeSpecVariables = Object.keys(typeSpec.variables).length
console.log(
`[${library}/${version}] wrote 5 files — ${counts.markdown} partials, ${counts.function} function slugs, ${counts.subcategory} subcategories, ${counts.category} categories, ${functionsList.length} functions.json entries, ${typeSpecMethods} typeSpec methods, ${typeSpecVariables} typeSpec variables`
)
}
/** Entry point: discovers every `[library]/[version]` pair under `spec/reference` and processes each. */
async function main(): Promise<void> {
const libraries = await readdir(SPEC_DIR, { withFileTypes: true })
for (const lib of libraries) {
if (!lib.isDirectory()) continue
const versions = await readdir(join(SPEC_DIR, lib.name), { withFileTypes: true })
for (const version of versions) {
if (!version.isDirectory()) continue
await processVersion(lib.name, version.name)
}
}
}
// Only run `main()` when invoked as a script (via `tsx`). Importing this
// module from a test should not trigger the side-effecting walk.
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((err) => {
console.error(err)
process.exit(1)
})
}
-259
View File
@@ -1,259 +0,0 @@
/**
* Find methods in typeSpec.json that are NOT documented in supabase_js_v2.yml
*
* Usage: pnpm tsx scripts/find-undocumented.ts
*
* Note: Run `pnpm prebuild` first to generate typeSpec.json
*/
import { existsSync, readFileSync } from 'fs'
import yaml from 'js-yaml'
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const SPEC_DIR = join(__dirname, '../spec')
const GENERATED_DIR = join(__dirname, '../features/docs/generated')
interface YamlFunction {
id: string
$ref?: string
}
interface YamlSpec {
functions: YamlFunction[]
}
interface TypeSpecModule {
name: string
methods: Record<string, unknown>
}
// Same normalization as Reference.typeSpec.ts
function normalizeRefPath(path: string) {
return path.replace(/\.index(?=\.|$)/g, '').replace(/\.+/g, '.')
}
// Categorize a method path
function categorizeMethod(methodPath: string): 'public' | 'constructor' | 'error' | 'internal' {
const parts = methodPath.split('.')
const methodName = parts[parts.length - 1]
const className = parts[parts.length - 2]
// Internal/private methods start with _
if (methodName.startsWith('_')) {
return 'internal'
}
// Error class constructors
if (className?.endsWith('Error') && methodName === 'constructor') {
return 'error'
}
// Other constructors
if (methodName === 'constructor') {
return 'constructor'
}
return 'public'
}
// Dynamically detect re-exports from typeSpec data
// If a class exists in both @supabase/supabase-js and another @supabase/* package,
// prefer the other package (which is the original source)
function buildReexportMap(typeSpecModules: TypeSpecModule[]): Map<string, string> {
const classToPackages = new Map<string, Set<string>>()
for (const mod of typeSpecModules) {
for (const methodPath of Object.keys(mod.methods)) {
const parts = methodPath.split('.')
const pkg = parts[0]
const className = parts[1]
if (pkg?.startsWith('@supabase/') && className) {
if (!classToPackages.has(className)) {
classToPackages.set(className, new Set())
}
classToPackages.get(className)!.add(pkg)
}
}
}
// For classes in multiple packages, map supabase-js to the original package
const reexportMap = new Map<string, string>()
for (const [className, packages] of classToPackages) {
if (packages.has('@supabase/supabase-js') && packages.size > 1) {
// Find the original package (not supabase-js)
for (const pkg of packages) {
if (pkg !== '@supabase/supabase-js') {
reexportMap.set(className, pkg)
break
}
}
}
}
return reexportMap
}
// Get the "canonical" path (prefer original package over supabase-js re-exports)
function getCanonicalPath(methodPath: string, reexportMap: Map<string, string>): string {
if (methodPath.startsWith('@supabase/supabase-js.')) {
const parts = methodPath.split('.')
const className = parts[1]
const originalPkg = reexportMap.get(className)
if (originalPkg) {
return methodPath.replace('@supabase/supabase-js', originalPkg)
}
}
return methodPath
}
// Check if typeSpec.json exists
const typeSpecPath = join(GENERATED_DIR, 'typeSpec.json')
if (!existsSync(typeSpecPath)) {
console.error('ERROR: typeSpec.json not found!')
console.error('Run `pnpm prebuild` first to generate it.')
process.exit(1)
}
// Load typeSpec.json and get all method paths
const typeSpecModules: TypeSpecModule[] = JSON.parse(readFileSync(typeSpecPath, 'utf8'))
const allMethods: string[] = []
for (const mod of typeSpecModules) {
for (const methodPath of Object.keys(mod.methods)) {
allMethods.push(methodPath)
}
}
// Build re-export map dynamically from typeSpec data
const reexportMap = buildReexportMap(typeSpecModules)
// Load YAML and get all documented $ref values (normalized)
const yamlPath = join(SPEC_DIR, 'supabase_js_v2.yml')
const spec = yaml.load(readFileSync(yamlPath, 'utf8')) as YamlSpec
const documentedRefs = new Set<string>()
for (const fn of spec.functions) {
if (fn.$ref) {
// Store both raw and normalized versions
documentedRefs.add(fn.$ref)
documentedRefs.add(normalizeRefPath(fn.$ref))
}
}
// Find undocumented methods and deduplicate
const seenCanonical = new Set<string>()
const undocumented: string[] = []
for (const method of allMethods) {
// Get canonical path first (prefer original packages over supabase-js re-exports)
const canonical = getCanonicalPath(method, reexportMap)
// Skip if already processed this canonical path
if (seenCanonical.has(canonical)) {
continue
}
seenCanonical.add(canonical)
// Check if documented - check both raw path AND canonical path
if (
documentedRefs.has(method) ||
documentedRefs.has(normalizeRefPath(method)) ||
documentedRefs.has(canonical) ||
documentedRefs.has(normalizeRefPath(canonical))
) {
continue
}
// Use canonical path for output
undocumented.push(canonical)
}
// Categorize
const publicMethods: string[] = []
const constructors: string[] = []
const errorConstructors: string[] = []
const internalMethods: string[] = []
for (const method of undocumented) {
switch (categorizeMethod(method)) {
case 'public':
publicMethods.push(method)
break
case 'constructor':
constructors.push(method)
break
case 'error':
errorConstructors.push(method)
break
case 'internal':
internalMethods.push(method)
break
}
}
// Group public methods by package
const publicByPackage = new Map<string, string[]>()
for (const method of publicMethods) {
const pkg = method.split('.')[0]
if (!publicByPackage.has(pkg)) {
publicByPackage.set(pkg, [])
}
publicByPackage.get(pkg)!.push(method)
}
// Output public methods (the interesting ones)
console.log('╔═══════════════════════════════════════════════════════════════╗')
console.log('║ UNDOCUMENTED PUBLIC METHODS ║')
console.log('╚═══════════════════════════════════════════════════════════════╝')
for (const [pkg, methods] of publicByPackage) {
console.log(`\n=== ${pkg} ===`)
methods.sort().forEach((m) => {
const shortName = m.replace(pkg + '.', '')
console.log(` - ${shortName}`)
})
}
// Summary sections for other categories
console.log('\n╔═══════════════════════════════════════════════════════════════╗')
console.log('║ SKIPPED CATEGORIES ║')
console.log('╚═══════════════════════════════════════════════════════════════╝')
console.log(`\n[Constructors] (${constructors.length} items)`)
constructors
.sort()
.slice(0, 5)
.forEach((m) => console.log(` - ${m}`))
if (constructors.length > 5) console.log(` ... and ${constructors.length - 5} more`)
console.log(`\n[Error Constructors] (${errorConstructors.length} items)`)
errorConstructors
.sort()
.slice(0, 5)
.forEach((m) => console.log(` - ${m}`))
if (errorConstructors.length > 5) console.log(` ... and ${errorConstructors.length - 5} more`)
console.log(`\n[Internal/Private Methods] (${internalMethods.length} items)`)
internalMethods
.sort()
.slice(0, 5)
.forEach((m) => console.log(` - ${m}`))
if (internalMethods.length > 5) console.log(` ... and ${internalMethods.length - 5} more`)
// Final summary
const totalUndocumented = undocumented.length
const documentedCount = allMethods.length - totalUndocumented
const percentage = ((documentedCount / allMethods.length) * 100).toFixed(1)
console.log('\n╔═══════════════════════════════════════════════════════════════╗')
console.log('║ SUMMARY ║')
console.log('╚═══════════════════════════════════════════════════════════════╝')
console.log(` Total methods in TypeSpec: ${allMethods.length}`)
console.log(` Documented in YAML: ${documentedCount}`)
console.log(` Undocumented (deduplicated): ${totalUndocumented}`)
console.log(` - Public methods: ${publicMethods.length} ← focus here`)
console.log(` - Constructors: ${constructors.length}`)
console.log(` - Error constructors: ${errorConstructors.length}`)
console.log(` - Internal methods: ${internalMethods.length}`)
console.log(` Coverage: ${percentage}%`)
+14 -11
View File
@@ -2,13 +2,14 @@ import { type GuideModel } from '../../../resources/guide/guideModel.js'
import { GuideModelLoader } from '../../../resources/guide/guideModelLoader.js'
import { LintWarningsGuideLoader, type LintWarningsGuideSource } from './lint-warnings-guide.js'
import { MarkdownLoader, type MarkdownSource } from './markdown.js'
import { IntegrationLoader, type IntegrationSource, fetchPartners } from './partner-integrations.js'
import { fetchPartners, IntegrationLoader, type IntegrationSource } from './partner-integrations.js'
import {
CliReferenceLoader,
type CliReferenceSource,
ClientLibReferenceLoader,
type ClientLibReferenceSource,
CliReferenceLoader,
loadClientLibReferenceFromNewPipeline,
OpenApiReferenceLoader,
type ClientLibReferenceSource,
type CliReferenceSource,
type OpenApiReferenceSource,
} from './reference-doc.js'
import { fetchTroubleshootingSources, type TroubleshootingSource } from './troubleshooting.js'
@@ -39,13 +40,15 @@ export async function fetchOpenApiReferenceSource() {
}
export async function fetchJsLibReferenceSource() {
return new ClientLibReferenceLoader(
'js-lib',
'/reference/javascript',
{ title: 'JavaScript Reference', language: 'JavaScript' },
'spec/supabase_js_v2.yml',
'spec/common-client-libs-sections.json'
).load()
// JS v2 is driven by the new reference pipeline. Ingest search sources from
// the generated `content/reference/javascript/v2/` outputs so embeddings
// never drift from what the renderer shows.
return loadClientLibReferenceFromNewPipeline({
source: 'js-lib',
path: '/reference/javascript',
meta: { title: 'JavaScript Reference', language: 'JavaScript' },
contentDir: 'content/reference/javascript/v2',
})
}
export async function fetchDartLibReferenceSource() {
@@ -2,10 +2,12 @@ import { createHash } from 'crypto'
import { readFile } from 'fs/promises'
import yaml from 'js-yaml'
import type { OpenAPIV3 } from 'openapi-types'
import type {
ICommonItem,
ICommonSection,
IFunctionDefinition,
IFunctionExample,
ISpec,
} from '../../../components/reference/Reference.types.js'
import { getApiEndpointById } from '../../../features/docs/Reference.generated.singleton.js'
@@ -276,6 +278,76 @@ export class ClientLibReferenceLoader extends ReferenceLoader<IFunctionDefinitio
}
}
/**
* Build search sources for a client lib from the new reference pipeline
* (`content/reference/<lib>/<ver>/{sections,functions,typeSpec}.json`) instead
* of the legacy YAML.
*
* Each function entry in `functions.json` either has rich content inline
* (partial-authored entries like subcategory overviews) or just an `id` +
* `$ref` pointing into `typeSpec.json`'s `methods` map for the TSDoc-extracted
* description and examples. We merge those into the `IFunctionDefinition`
* shape `ClientLibReferenceSource` already knows how to render, so no
* downstream changes are needed.
*/
export async function loadClientLibReferenceFromNewPipeline({
source,
path,
meta,
contentDir,
}: {
source: string
path: string
meta: Record<string, unknown>
contentDir: string
}): Promise<BaseSource[]> {
const [sectionsRaw, functionsRaw, typeSpecRaw] = await Promise.all([
readFile(`${contentDir}/sections.json`, 'utf8'),
readFile(`${contentDir}/functions.json`, 'utf8'),
readFile(`${contentDir}/typeSpec.json`, 'utf8'),
])
const refSections = JSON.parse(sectionsRaw) as ICommonItem[]
const functions = JSON.parse(functionsRaw) as Array<{
id: string
$ref?: string
title?: string
description?: string
examples?: IFunctionExample[]
}>
const typeSpec = JSON.parse(typeSpecRaw) as {
methods: Record<string, { comment?: { shortText?: string; examples?: IFunctionExample[] } }>
}
const enriched: IFunctionDefinition[] = functions.map((fn) => {
const typeSpecEntry = fn.$ref ? typeSpec.methods[fn.$ref] : undefined
return {
id: fn.id,
$ref: fn.$ref ?? '',
title: fn.title ?? '',
description: fn.description ?? typeSpecEntry?.comment?.shortText ?? '',
examples: fn.examples ?? typeSpecEntry?.comment?.examples,
}
})
const flattened = flattenSections(refSections)
return flattened
.map((refSection) => {
const specSection = enriched.find((e) => e.id === refSection.id)
if (!specSection) return undefined
const titleForMeta = specSection.title || refSection.title
return new ClientLibReferenceSource(
source,
`${path}/${refSection.slug}`,
refSection,
specSection,
{ ...meta, slug: specSection.id, methodName: titleForMeta }
)
})
.filter((s): s is ClientLibReferenceSource => s !== undefined)
}
export class ClientLibReferenceSource extends ReferenceSource<IFunctionDefinition> {
formatSection(functionDefinition: IFunctionDefinition, refSection: ICommonItem): string {
const { title } = refSection
-103
View File
@@ -1,103 +0,0 @@
/**
* Cross-check IDs between common-client-libs-sections.json and supabase_js_v2.yml
*
* Reports:
* 1. Functions in sections but NOT in YAML
* 2. Groups (isFunc: false) in sections but NOT in YAML
* 3. IDs in YAML but NOT in sections
*
* Usage: pnpm tsx scripts/validate-references.ts
*/
import { readFileSync } from 'fs'
import yaml from 'js-yaml'
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const SPEC_DIR = join(__dirname, '../spec')
interface Section {
id?: string
type: string
isFunc?: boolean
items?: Section[]
}
interface YamlSpec {
functions: Array<{ id: string }>
}
// Flatten sections, extracting all function-type entries
function flattenSections(sections: Section[]): { functions: string[]; groups: string[] } {
const functions: string[] = []
const groups: string[] = []
function recurse(items: Section[]) {
for (const item of items) {
if (item.type === 'function' && item.id) {
if (item.isFunc === false) {
groups.push(item.id)
} else {
functions.push(item.id)
}
}
if (item.items) {
recurse(item.items)
}
}
}
recurse(sections)
return { functions, groups }
}
// Main
const sectionsPath = join(SPEC_DIR, 'common-client-libs-sections.json')
const yamlPath = join(SPEC_DIR, 'supabase_js_v2.yml')
const sections: Section[] = JSON.parse(readFileSync(sectionsPath, 'utf8'))
const spec = yaml.load(readFileSync(yamlPath, 'utf8')) as YamlSpec
const { functions: sectionFunctions, groups: sectionGroups } = flattenSections(sections)
const yamlIds = new Set(spec.functions.map((f) => f.id))
const sectionFunctionSet = new Set(sectionFunctions)
const sectionGroupSet = new Set(sectionGroups)
const allSectionIds = new Set([...sectionFunctions, ...sectionGroups])
// Find mismatches
const functionsNotInYaml = sectionFunctions.filter((id) => !yamlIds.has(id))
const groupsNotInYaml = sectionGroups.filter((id) => !yamlIds.has(id))
const yamlNotInSections = [...yamlIds].filter((id) => !allSectionIds.has(id))
// Output
console.log('=== Functions in sections but NOT in YAML ===')
if (functionsNotInYaml.length === 0) {
console.log('(none)')
} else {
functionsNotInYaml.forEach((id) => console.log(`- ${id}`))
}
console.log('\n=== Groups (isFunc: false) in sections but NOT in YAML ===')
if (groupsNotInYaml.length === 0) {
console.log('(none)')
} else {
groupsNotInYaml.forEach((id) => console.log(`- ${id}`))
}
console.log('\n=== IDs in YAML but NOT in sections ===')
if (yamlNotInSections.length === 0) {
console.log('(none)')
} else {
yamlNotInSections.forEach((id) => console.log(`- ${id}`))
}
console.log(
`\nSummary: ${functionsNotInYaml.length} functions missing, ${groupsNotInYaml.length} groups missing, ${yamlNotInSections.length} orphaned`
)
// Exit with error code if any mismatches
if (functionsNotInYaml.length > 0 || groupsNotInYaml.length > 0 || yamlNotInSections.length > 0) {
process.exit(1)
}
@@ -1,87 +0,0 @@
/**
* Validate that YAML $ref values exist in typeSpec.json
*
* Checks: supabase_js_v2.yml $ref → typeSpec.json (generated from combined.json)
*
* Usage: pnpm tsx scripts/validate-typespec-refs.ts
*
* Note: Run `pnpm prebuild` first to generate typeSpec.json
*/
import { existsSync, readFileSync } from 'fs'
import yaml from 'js-yaml'
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const SPEC_DIR = join(__dirname, '../spec')
const GENERATED_DIR = join(__dirname, '../features/docs/generated')
interface YamlFunction {
id: string
$ref?: string
}
interface YamlSpec {
functions: YamlFunction[]
}
interface TypeSpecModule {
name: string
methods: Record<string, unknown>
}
// Same normalization as Reference.typeSpec.ts
function normalizeRefPath(path: string) {
return path.replace(/\.index(?=\.|$)/g, '').replace(/\.+/g, '.')
}
// Check if typeSpec.json exists
const typeSpecPath = join(GENERATED_DIR, 'typeSpec.json')
if (!existsSync(typeSpecPath)) {
console.error('ERROR: typeSpec.json not found!')
console.error('Run `pnpm prebuild` first to generate it.')
process.exit(1)
}
// Load typeSpec.json and extract all valid method paths
const typeSpecModules: TypeSpecModule[] = JSON.parse(readFileSync(typeSpecPath, 'utf8'))
const validRefs = new Set<string>()
for (const mod of typeSpecModules) {
for (const methodPath of Object.keys(mod.methods)) {
validRefs.add(methodPath)
}
}
// Load YAML and extract $ref values
const yamlPath = join(SPEC_DIR, 'supabase_js_v2.yml')
const spec = yaml.load(readFileSync(yamlPath, 'utf8')) as YamlSpec
const yamlRefs: Array<{ id: string; ref: string }> = []
for (const fn of spec.functions) {
if (fn.$ref) {
yamlRefs.push({ id: fn.id, ref: fn.$ref })
}
}
// Find invalid refs - check both raw and normalized (matches runtime behavior)
const invalidRefs = yamlRefs.filter(
({ ref }) => !validRefs.has(ref) && !validRefs.has(normalizeRefPath(ref))
)
const validCount = yamlRefs.length - invalidRefs.length
// Output
console.log('=== YAML $ref NOT found in TypeSpec ===')
if (invalidRefs.length === 0) {
console.log('(none)')
} else {
invalidRefs.forEach(({ id, ref }) => console.log(`- ${ref} (id: ${id})`))
}
console.log(`\n=== Valid refs: ${validCount} | Invalid refs: ${invalidRefs.length} ===`)
// Exit with error code if any invalid refs
if (invalidRefs.length > 0) {
process.exit(1)
}
+11 -55
View File
@@ -37,12 +37,12 @@ download.storage.v1:
# curl -sS https://supabase.github.io/functions-js/v1/spec.json > $(REPO_DIR)/enrichments/tsdoc_v1/functions.json
download.tsdoc.v2:
curl -sS https://supabase.github.io/supabase-js/supabase-js/v2/spec.json > $(REPO_DIR)/enrichments/tsdoc_v2/supabase.json
curl -sS https://supabase.github.io/supabase-js/auth-js/v2/spec.json > $(REPO_DIR)/enrichments/tsdoc_v2/gotrue.json
curl -sS https://supabase.github.io/supabase-js/postgrest-js/v2/spec.json > $(REPO_DIR)/enrichments/tsdoc_v2/postgrest.json
curl -sS https://supabase.github.io/supabase-js/realtime-js/v2/spec.json > $(REPO_DIR)/enrichments/tsdoc_v2/realtime.json
curl -sS https://supabase.github.io/supabase-js/storage-js/v2/spec.json > $(REPO_DIR)/enrichments/tsdoc_v2/storage.json
curl -sS https://supabase.github.io/supabase-js/functions-js/v2/spec.json > $(REPO_DIR)/enrichments/tsdoc_v2/functions.json
curl -sS https://supabase.github.io/supabase-js/supabase-js/v2/spec.json > $(REPO_DIR)/reference/javascript/v2/supabase.json
curl -sS https://supabase.github.io/supabase-js/auth-js/v2/spec.json > $(REPO_DIR)/reference/javascript/v2/gotrue.json
curl -sS https://supabase.github.io/supabase-js/postgrest-js/v2/spec.json > $(REPO_DIR)/reference/javascript/v2/postgrest.json
curl -sS https://supabase.github.io/supabase-js/realtime-js/v2/spec.json > $(REPO_DIR)/reference/javascript/v2/realtime.json
curl -sS https://supabase.github.io/supabase-js/storage-js/v2/spec.json > $(REPO_DIR)/reference/javascript/v2/storage.json
curl -sS https://supabase.github.io/supabase-js/functions-js/v2/spec.json > $(REPO_DIR)/reference/javascript/v2/functions.json
download.analytics.v0:
curl -sS https://logflare.app/api/openapi > $(REPO_DIR)/analytics_v0_openapi.json
@@ -50,7 +50,11 @@ download.analytics.v0:
###############################################################################
# Transform docs into working files
###############################################################################
transform: dereference.api.v1 dereference.auth.v1 dereference.storage.v0 dereference.tsdoc.v2 combine.tsdoc.v2
# `download.tsdoc.v2` now writes raw TypeDoc JSON directly under
# `reference/javascript/v2/` — the new pipeline (`scripts/build-reference-content.ts`,
# wired into `predev`/`prebuild` via `codegen:references:new`) walks those files
# at build time, so no separate `dereference` / `combine` step is needed.
transform: dereference.api.v1 dereference.auth.v1 dereference.storage.v0
dereference.api.v1:
pnpm exec redocly bundle --dereferenced -o $(REPO_DIR)/transforms/api_v1_openapi_deparsed.json $(REPO_DIR)/api_v1_openapi.json
@@ -64,54 +68,6 @@ dereference.storage.v0:
dereference.analytics.v0:
pnpm exec redocly bundle --dereferenced -o $(REPO_DIR)/transforms/analytics_v0_openapi_deparsed.json $(REPO_DIR)/analytics_v0_openapi.json
# No longer updated
# dereference.tsdoc.v1:
# cd $(GENERATOR_DIR) && npm run tsdoc:dereference:functions:v1
# cd $(GENERATOR_DIR) && npm run tsdoc:dereference:gotrue:v1
# cd $(GENERATOR_DIR) && npm run tsdoc:dereference:postgrest:v1
# cd $(GENERATOR_DIR) && npm run tsdoc:dereference:realtime:v1
# cd $(GENERATOR_DIR) && npm run tsdoc:dereference:storage:v1
# cd $(GENERATOR_DIR) && npm run tsdoc:dereference:supabase:v1
dereference.tsdoc.v2:
cd $(GENERATOR_DIR) && pnpm run tsdoc:dereference:functions:v2
cd $(GENERATOR_DIR) && pnpm run tsdoc:dereference:gotrue:v2
cd $(GENERATOR_DIR) && pnpm run tsdoc:dereference:postgrest:v2
cd $(GENERATOR_DIR) && pnpm run tsdoc:dereference:realtime:v2
cd $(GENERATOR_DIR) && pnpm run tsdoc:dereference:storage:v2
cd $(GENERATOR_DIR) && pnpm run tsdoc:dereference:supabase:v2
# No longer updated
# combine.tsdoc.v1:
# jq -s '{ name: "Combined Specs", children: [.[0], .[1], .[2], .[3], .[4], .[5]] }' \
# $(REPO_DIR)/enrichments/tsdoc_v1/supabase_dereferenced.json \
# $(REPO_DIR)/enrichments/tsdoc_v1/gotrue_dereferenced.json \
# $(REPO_DIR)/enrichments/tsdoc_v1/postgrest_dereferenced.json \
# $(REPO_DIR)/enrichments/tsdoc_v1/realtime_dereferenced.json \
# $(REPO_DIR)/enrichments/tsdoc_v1/storage_dereferenced.json \
# $(REPO_DIR)/enrichments/tsdoc_v1/functions_dereferenced.json \
# > $(REPO_DIR)/enrichments/tsdoc_v1/combined.json
combine.tsdoc.v2:
jq -s '{ name: "Combined Specs", children: [.[0], .[1], .[2], .[3], .[4], .[5]] }' \
$(REPO_DIR)/enrichments/tsdoc_v2/supabase_dereferenced.json \
$(REPO_DIR)/enrichments/tsdoc_v2/gotrue_dereferenced.json \
$(REPO_DIR)/enrichments/tsdoc_v2/postgrest_dereferenced.json \
$(REPO_DIR)/enrichments/tsdoc_v2/realtime_dereferenced.json \
$(REPO_DIR)/enrichments/tsdoc_v2/storage_dereferenced.json \
$(REPO_DIR)/enrichments/tsdoc_v2/functions_dereferenced.json \
> $(REPO_DIR)/enrichments/tsdoc_v2/combined.json
combine-raw.tsdoc.v2:
jq -s '{ name: "Combined Specs", children: [.[0], .[1], .[2], .[3], .[4], .[5]] }' \
$(REPO_DIR)/enrichments/tsdoc_v2/supabase.json \
$(REPO_DIR)/enrichments/tsdoc_v2/gotrue.json \
$(REPO_DIR)/enrichments/tsdoc_v2/postgrest.json \
$(REPO_DIR)/enrichments/tsdoc_v2/realtime.json \
$(REPO_DIR)/enrichments/tsdoc_v2/storage.json \
$(REPO_DIR)/enrichments/tsdoc_v2/functions.json \
> $(REPO_DIR)/enrichments/tsdoc_v2/combined_raw.json
###############################################################################
# Generate sections from OpenAPI 3.0
###############################################################################
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+429
View File
@@ -0,0 +1,429 @@
# Reference content pipeline
This directory feeds the **new** reference-docs pipeline driven by
[`scripts/build-reference-content.ts`](../../scripts/build-reference-content.ts).
The setup lets you drop TypeDoc dumps, layer hand-authored content via
partials, and tweak ordering through `config.json`. Currently it only consumes
[TypeDoc](https://typedoc.org/) JSON output, but other formats can be adapted
to the same shape as a pre-step.
The legacy pipeline (the `spec/supabase_*_v*.yml` files plus
`spec/common-client-libs-sections.json` driven by
`features/docs/Reference.generated.script.ts`) still exists for SDKs that
haven't migrated yet — see "Routing to the new pipeline" below for how a lib
opts in.
## Pipeline at a glance
```
Upstream supabase-js packages publish TypeDoc JSON at
https://supabase.github.io/<pkg>/v2/spec.json
│
│ (1) cd apps/docs/spec && make download.tsdoc.v2
▼
spec/reference/<lib>/<ver>/ ← gitignored *.json
├── *.json ← downloaded TypeDoc dumps (committed: config.json
├── config.json ← hand-authored + partials/)
└── partials/ ← hand-authored
│
│ (2) pnpm codegen:references:new
│ (= ensure dumps + build-reference-content.ts)
▼
content/reference/<lib>/<ver>/ ← gitignored (all 5 files)
├── bySlug.json
├── flat.json
├── sections.json
├── functions.json
└── typeSpec.json
│
│ (3) Next.js render
▼
/docs/reference/<lib>/<ver>/<slug>
```
Both `spec/reference/<lib>/<ver>/*.json` (the source dumps) and the entire
`content/reference/` tree (the build output) are gitignored — only
hand-authored files inside `spec/reference/<lib>/<ver>/` (`config.json` and
`partials/`) are committed. Everything regenerates on `pnpm dev` / `pnpm build`
via `predev` / `prebuild`, so a clean checkout just works.
## Directory layout
```
spec/reference/
└── <library>/ e.g. javascript
└── <version>/ e.g. v2
├── *.json TypeDoc spec files (GITIGNORED — downloaded)
├── config.json Optional. Filters and ordering — see below (committed)
└── partials/ Optional. Per-section content (committed)
├── *.mdx | *.md Markdown partials (with frontmatter)
└── *.json Rich function-type partials (description, examples, …)
```
Library and version names come straight from the folder names. Anything you drop
under `spec/reference/<lib>/<ver>/` is picked up automatically by the build
script — there is no separate manifest to update inside this directory.
The output will be sent to the `content/reference/<lib>/<ver>/` directory
(also gitignored — see "Outputs" below).
## Source: TypeDoc JSON specs
Each top-level `.json` file (except `config.json`) is a TypeDoc dump of one
package. These files are **gitignored** — they're downloaded build artifacts,
not source. Only hand-authored files (`config.json` and `partials/`) are
tracked. To refresh the dumps locally:
```bash
cd apps/docs/spec && make download.tsdoc.v2
```
`pnpm dev` / `pnpm build` runs `codegen:references:ensure` automatically as
part of `predev` / `prebuild`. That script checks for
`spec/reference/javascript/v2/supabase.json` and, if missing, invokes
`make download.tsdoc.v2`. So a clean checkout just works — the dumps land on
first build and are reused on subsequent runs. To force a refresh after
supabase-js ships a release, run the make target by hand (or delete one of
the `.json` files and let the next build re-fetch).
CI runs the same path: `.github/workflows/docs-tests.yml` calls
`make download.tsdoc.v2` before tests, and `docs-js-libs-update.yml` opens a
PR with regenerated snapshots when supabase-js publishes a new version.
The build walks every declaration in those dumps and harvests anything tagged
with `@category` (and optionally `@subcategory`):
```jsonc
// spec/reference/javascript/v2/gotrue.json (excerpt)
{
"name": "@supabase/auth-js", // ← package name, becomes the $ref prefix
"variant": "project",
"kind": 1,
"children": [
{
"kind": 128,
"name": "GoTrueClient",
"children": [
{
"kind": 2048,
"name": "linkIdentity",
"comment": {
"blockTags": [
{ "tag": "@category", "content": [{ "kind": "text", "text": "Auth" }] },
{ "tag": "@subcategory", "content": [{ "kind": "text", "text": "Auth MFA" }] },
],
},
"signatures": [
{
/* params + return type */
},
],
},
],
},
],
}
```
Key tags the walker reads:
- `@category` — required for a declaration to appear in the listing. Becomes
the `product` and groups items in `sections.json`.
- `@subcategory` — optional. Nests the declaration under a sub-grouping inside
the category, with its own header.
Anything without `@category` is collected into `typeSpec.json` (for cross-reference
by `$ref`) but does **not** appear in the navigation/section list.
`$ref` values are constructed as `<package>.<module…>.<class…>.<member>`,
following TypeDoc's module (kind 2), namespace (kind 4), class (kind 128) and
interface (kind 256) nesting. The `index` module segment is stripped via
`normalizeRefPath` (so `@supabase/storage-js.index.StorageClient.foo` becomes
`@supabase/storage-js.StorageClient.foo`).
## Partials
Partials enrich the rendered output with content TypeDoc can't supply
(intro paragraphs, code examples, etc.). The filename (without extension) is
the routing key:
- Matches a **subcategory title slug** (e.g. `using-filters.json` for the
"Using filters" subcategory) → attached to that subcategory.
- Matches a **category title slug** (e.g. `auth.json` for the "Auth" category)
→ attached to that category.
- Anything else → emitted at the **top of the page**, before any category
(this is how `introduction.mdx` / `installing.mdx` work).
The slug check is `title.toLowerCase().replace(/\s+/g, '-')`, so
`"Auth Admin"` → `auth-admin`, `"File Buckets"` → `file-buckets`, etc.
### `.mdx` / `.md` partials (markdown)
Frontmatter is parsed via `gray-matter`. Only `title` and `ref` are read:
```mdx
---
title: Initializing
ref: '@supabase/supabase-js.SupabaseClient.constructor' # optional — see below
---
Body content goes here.
```
- **Without `ref`**: emits a `type: 'markdown'` entry. Its body is also written
to `apps/docs/content/reference/<library>/<version>/<name>.mdx` so the
renderer's routed loader (`getRefMarkdownForLib`) can serve it.
- **With `ref`**: emits a `type: 'function'` entry plus a `functions.json` entry
`{id: <name>, $ref: <ref>}`. The renderer pairs that with `typeSpec.json` to
show the method signature. This is how `initializing.mdx` links to the
`SupabaseClient` constructor.
If the filename matches a category/subcategory, the markdown entry is added as
a **separate** sub-section at the top of that section's items (the subcategory
header itself is untouched).
### `.json` partials (function-type)
JSON partials carry rich content (description, notes, examples) that gets
rendered into the page. The full body is poured into `functions.json`:
````json
// spec/reference/javascript/v2/partials/using-filters.json
{
"id": "using-filters",
"title": "Using Filters",
"description": "Filters allow you to only return rows that match …",
"examples": [{ "id": "applying-filters", "name": "Applying Filters", "code": "```ts\n…\n```" }]
}
````
Routing semantics differ from markdown partials:
- **Matches a category/subcategory** → **enriches** that section's header.
No separate sub-section is emitted. The entry is keyed in `functions.json`
under the matched section's slug (e.g. `auth-admin`, `using-filters` —
whichever slug the header resolves to, after `navigationPrefixes`), so the
renderer's `fns.find(f => f.id === section.id)` resolves to the partial
body and the subcategory header renders with the description/examples in
place.
- **Top-level (no match)** → emitted as its own `type: 'function'` section.
This is why `auth-admin.json` (matching the `auth-admin` subcategory slug)
shows up as the _Auth Admin_ heading with notes and examples, instead of as a
sibling "Overview" block.
## `config.json`
Every option is optional. Default behavior: include everything, in spec order,
alphabetical-within-section.
```jsonc
{
// Categories (matched on the literal @category text, case-sensitive) to
// drop entirely. Functions in these categories are filtered out before
// grouping, so they disappear from bySlug / flat / sections / functions.json
// (but stay in typeSpec.json — partials may still reference them via $ref).
"excludeCategories": ["Initializing"],
// Declaration names (matched on the source identifier, case-sensitive) to
// drop. Useful for hiding noise like `constructor` while still allowing
// partials to link to the constructor's $ref.
"excludeDefinitions": ["constructor"],
// Order categories should appear in `sections.json` / `flat.json`. Anything
// not listed here keeps its discovery order at the end.
"categoryOrder": ["Database", "Auth", "Edge Functions", "Realtime", "Storage"],
// Order top-level partials. Anything not listed here falls back to
// alphabetical order at the end.
"partialsOrder": ["introduction", "installing", "typescript-support"],
// Customize the navigation slug used as a prefix for a category or
// subcategory. Keys are the literal @category / @subcategory text
// (case-sensitive). Values:
// string → use this in place of the default slugified title.
// e.g. "Edge Functions": "functions" turns the function slug
// "edge-functions-invoke" into "functions-invoke".
// false → drop the prefix entirely from child function slugs.
// e.g. "Using modifiers": false turns "using-modifiers-explain"
// into "explain". The category/subcategory header itself still
// needs a navigable slug, so its entry slug falls back to the
// slugified title ("using-modifiers").
// absent → use the slugified title (default behavior).
"navigationPrefixes": {
"Database": false,
"Using filters": false,
"Using modifiers": false,
"Edge Functions": "functions",
},
}
```
### Slug shape
A function's slug is `${prefix}-${name}` where `prefix` is its nearest container
— its `@subcategory` if it has one, otherwise its `@category`. For example a
PostgREST `eq` method tagged `@category Database @subcategory Using filters`:
- without config: `using-filters-eq`
- with `"Using filters": false`: `eq`
- with `"Using filters": "filters"`: `filters-eq`
The category and subcategory header entries (the rows that show up in
navigation) get the resolved prefix too — `"Edge Functions": "functions"`
makes the Edge Functions category render at `/functions`, not `/edge-functions`.
`false` falls back to the title slug for the header (a header needs a stable,
navigable slug).
Within each category, the script always:
1. Emits the category header.
2. Emits direct functions (no `@subcategory`) alphabetically by name.
3. Emits each subcategory (alphabetical), each followed by its functions
(alphabetical).
So `categoryOrder` lets you sort _across_ categories; within a category, order
is fixed.
## Outputs
`pnpm tsx scripts/build-reference-content.ts` writes five files per
`<library>/<version>` to `apps/docs/content/reference/<library>/<version>/`:
| File | Shape | Used for |
| ---------------- | -------------------------------------- | -------------------------------------------------- |
| `bySlug.json` | `Record<slug, Entry>` | Slug → section lookup |
| `flat.json` | `Entry[]` (`Object.values(bySlug)`) | Linear iteration in the renderer |
| `sections.json` | `Entry[]` with nested `items[]` | Sidebar navigation tree |
| `functions.json` | `Array<{id, $ref?, …}>` | Rendered description / examples / signature lookup |
| `typeSpec.json` | `{methods, variables}` keyed by `$ref` | Parameter and return-type display |
The build also writes `<partial-name>.mdx` files for each markdown partial into
the same per-version output directory — the page renderer's routed loader
(`getRefMarkdownForLib`) reads those bodies directly, no manual copy needed.
The whole `apps/docs/content/reference/` tree is gitignored — outputs are
regenerated by `prebuild` (`pnpm codegen:references:new`, which also runs as
part of `predev`).
## Regenerating the snapshot test
CI gates this pipeline through a single snapshot test at
[`scripts/build-reference-content.test.ts`](../../scripts/build-reference-content.test.ts).
It runs `collectReferenceContent('javascript', 'v2')` and snapshots all five
derived artifacts (`bySlug`, `flat`, `sections`, `functionsList`, `typeSpec`)
into `__snapshots__/build-reference-content.test.ts.snap`. Any change in the
output — new methods, slug shape, type signatures, section ordering — surfaces
as a snapshot diff in the PR, which is the **human-reviewable preview** of what
the renderer will see.
The snapshot is the only committable artifact this pipeline produces (the
TypeDoc dumps and `content/reference/` outputs are gitignored), so it doubles as
the change log for upstream and pipeline-logic changes.
Re-run with `--update` whenever you:
- Edit [`scripts/build-reference-content.ts`](../../scripts/build-reference-content.ts)
(extraction or grouping logic).
- Change a lib's `config.json` (`excludeCategories`, `excludeDefinitions`,
`categoryOrder`, `partialsOrder`, `navigationPrefixes`).
- Add, remove, or edit files under `partials/`.
- Pull a refreshed TypeDoc dump (`make download.tsdoc.v2`) — typically because
supabase-js shipped a release. The `docs-js-libs-update.yml` workflow does
this automatically and opens a PR with the refreshed snapshot.
```bash
cd apps/docs && npx vitest run --update scripts/build-reference-content.test.ts
```
Inspect the resulting diff before committing — a clean, additive diff (new
methods, new entries) is the expected shape; large renames or removals are
worth a second look.
> **Local failures in other tests are expected — don't panic.** Vitest picks up
> every `*.test.ts` in `apps/docs`, even when you target one file. Tests that
> hit the Supabase backend (`app/api/graphql/tests/errors*.test.ts`, the
> `errors.collection.test.ts` suite) will fail with `fetch failed` / timeouts
> unless you've run `pnpm supabase start` first, and any `*.smoke.test.ts` file
> will hit live `supabase.com/docs` URLs that depend on the current prod
> deploy. The only result that matters here is the `build-reference-content`
> line — if that's green and the `.snap` file updated, you're done. CI runs
> with the local Supabase stack up and excludes smoke tests, so those failures
> won't follow your PR.
## Routing to the new pipeline
By default, the runtime in
[`features/docs/Reference.generated.singleton.ts`](../../features/docs/Reference.generated.singleton.ts)
keeps reading the legacy `features/docs/generated/<sdk>.<version>.*.json`
files. To route a lib through the new outputs, add its `${sdk}-${version}` key
to the constant in
[`features/docs/Reference.constants.ts`](../../features/docs/Reference.constants.ts):
```ts
export const SUPPORTS_NEW_REFERENCE_PROCESS = new Set([
'javascript-v2',
// 'dart-v2', ← uncomment when ready
])
```
The same set drives every runtime read that depends on the new layout:
- The four lib-version-keyed JSON getters in
[`Reference.generated.singleton.ts`](../../features/docs/Reference.generated.singleton.ts)
(`getFunctionsList`, `getReferenceSections`, `getFlattenedSections`,
`getSectionsBySlug`) pick between
`features/docs/generated/<sdk>.<version>.*.json` and
`content/reference/<sdk>/<version>/*.json`.
- The MDX loader in [`Reference.mdx.tsx`](../../features/docs/Reference.mdx.tsx)
(`getRefMarkdownForLib`) picks between
`docs/ref/<libPath>/[<version>/]<id>.mdx` and
`content/reference/<libPath>/<version>/<id>.mdx`.
- The legacy section-generation script
[`Reference.generated.script.ts`](../../features/docs/Reference.generated.script.ts)
filters out libs in the set, so no `supabase_<lib>_v<ver>.yml` is read for
them. Migrated libs therefore drop their `specFile` field from
[`content/navigation.references.ts`](../../content/navigation.references.ts)
(see the JS v2 entry for an example).
- Search/embeddings ingest in
[`scripts/search/sources/index.ts`](../../scripts/search/sources/index.ts):
`fetchJsLibReferenceSource()` calls `loadClientLibReferenceFromNewPipeline()`
which reads `content/reference/javascript/v2/{sections,functions,typeSpec}.json`
directly. Other libs still go through `ClientLibReferenceLoader` against
their YAML. To migrate another lib, swap the same way once it's in
`SUPPORTS_NEW_REFERENCE_PROCESS`.
No other call sites in the render path need to change.
> ⚠️ Don't move the constant. `Reference.utils.ts` transitively pulls in
> `next/navigation`, which crashes `tsx --conditions=react-server` (used by
> `pnpm build:llms`). The constant lives in its own no-dep file
> (`Reference.constants.ts`) so server-only scripts can import it without
> dragging the Next runtime in.
## Adding a new lib/version (checklist)
1. **Wire up the TypeDoc download** in [`spec/Makefile`](../Makefile). Copy the
`download.tsdoc.v2` target, adapt the URLs and output paths to
`spec/reference/<lib>/<ver>/`. Make sure every public method in the upstream
source has an `@category` tag. One JSON per package is fine — the walker
handles multiple files. The dumps themselves are gitignored; only
`config.json` and `partials/` get tracked.
2. **(Optional) add `config.json`** with `excludeCategories`,
`excludeDefinitions`, `categoryOrder`, `partialsOrder`.
3. **(Optional) add `partials/`** with intro markdown and per-section rich
content (see "Partials" above).
4. **Register the lib/version** in
[`features/docs/Reference.constants.ts`](../../features/docs/Reference.constants.ts)'s
`SUPPORTS_NEW_REFERENCE_PROCESS` so the runtime reads the new outputs.
5. **Run the build**:
```bash
cd apps/docs && pnpm codegen:references:new
```
This auto-downloads missing dumps via `codegen:references:ensure` and then
runs `build-reference-content.ts`. Inspect the five files under
`content/reference/<lib>/<ver>/`. The log line prints declaration /
function / subcategory / category counts.
6. **Verify the rendered page** at `/docs/reference/<lib>/<ver>` in dev. If
subcategory bodies are empty, check that the partial filename matches the
subcategory title slug exactly (`title.toLowerCase().replace(/\s+/g, '-')`).
@@ -0,0 +1,11 @@
{
"excludeCategories": ["Initializing"],
"excludeDefinitions": ["constructor"],
"categoryOrder": ["Database", "Auth", "Edge Functions", "Realtime", "Storage"],
"partialsOrder": ["introduction", "installing", "initializing", "typescript-support"],
"navigationPrefixes": {
"Database": false,
"Realtime": false,
"Edge Functions": "functions"
}
}
@@ -0,0 +1,5 @@
{
"id": "analytics-buckets",
"title": "Analytics Buckets",
"description": "This section contains methods for working with Analytics Buckets."
}
@@ -0,0 +1,13 @@
{
"id": "auth-admin",
"title": "Overview",
"notes": "- Any method under the `supabase.auth.admin` namespace requires a `secret` key.\n- These methods are considered admin methods and should be called on a trusted server. Never expose your `secret` key in the browser.\n",
"examples": [
{
"id": "create-auth-admin-client",
"name": "Create server-side auth client",
"isSpotlight": true,
"code": "```js\nimport { createClient } from '@supabase/supabase-js'\n\nconst supabase = createClient(supabase_url, secret_key, {\n auth: {\n autoRefreshToken: false,\n persistSession: false\n }\n})\n\n// Access auth admin api\nconst adminAuthClient = supabase.auth.admin\n```\n"
}
]
}
@@ -0,0 +1,5 @@
{
"id": "auth-mfa",
"title": "Auth MFA",
"description": "This section contains methods commonly used for Multi-Factor Authentication (MFA) and are invoked behind the `supabase.auth.mfa` namespace.\n\nCurrently, there is support for time-based one-time password (TOTP) and phone verification code as the 2nd factor. Recovery codes are not supported but users can enroll multiple factors, with an upper limit of 10.\n\nHaving a 2nd factor for recovery frees the user of the burden of having to store their recovery codes somewhere. It also reduces the attack surface since multiple recovery codes are usually generated compared to just having 1 backup factor.\n\nLearn more about implementing MFA in your application [in the MFA guide](https://supabase.com/docs/guides/auth/auth-mfa#overview)."
}
@@ -0,0 +1,6 @@
{
"id": "auth-passkey",
"title": "Auth Passkey",
"description": "This section contains methods for WebAuthn passkey registration, authentication, and management. Methods are invoked behind the `supabase.auth.passkey` namespace.\n\nPasskey support is an experimental feature. Enable it when creating the client:",
"code": "```js\nconst supabase = createClient(supabaseUrl, publishableKey, {\n auth: {\n experimental: { passkey: true },\n },\n})\n```"
}
@@ -0,0 +1,19 @@
{
"id": "auth",
"title": "Overview",
"notes": "- The auth methods can be accessed via the `supabase.auth` namespace.\n- By default, the supabase client sets `persistSession` to true and attempts to store the session in local storage. When using the supabase client in an environment that doesn't support local storage, you might notice the following warning message being logged:\n\n > No storage option exists to persist the session, which may result in unexpected behavior when using auth. If you want to set `persistSession` to true, please provide a storage option or you may set `persistSession` to false to disable this warning.\n\n This warning message can be safely ignored if you're not using auth on the server-side. If you are using auth and you want to set `persistSession` to true, you will need to provide a custom storage implementation that follows [this interface](https://github.com/supabase/supabase-js/blob/master/packages/core/auth-js/src/lib/types.ts#L1053).\n- Any email links and one-time passwords (OTPs) sent have a default expiry of 24 hours. We have the following [rate limits](/docs/guides/platform/going-into-prod#auth-rate-limits) in place to guard against brute force attacks.\n- The expiry of an access token can be set in the \"JWT expiry limit\" field in [your project's auth settings](/dashboard/project/_/auth/providers). A refresh token never expires and can only be used once.\n",
"examples": [
{
"id": "create-auth-client",
"name": "Create auth client",
"isSpotlight": true,
"code": "```js\nimport { createClient } from '@supabase/supabase-js'\n\nconst supabase = createClient(supabase_url, publishable_key)\n```\n"
},
{
"id": "create-auth-client-server-side",
"name": "Create auth client (server-side)",
"isSpotlight": false,
"code": "```js\nimport { createClient } from '@supabase/supabase-js'\n\nconst supabase = createClient(supabase_url, publishable_key, {\n auth: {\n autoRefreshToken: false,\n persistSession: false,\n detectSessionInUrl: false\n }\n})\n```\n"
}
]
}
@@ -0,0 +1,5 @@
{
"id": "file-buckets",
"title": "File Buckets",
"description": "This section contains methods for working with File Buckets."
}
@@ -0,0 +1,4 @@
---
title: Initializing
ref: '@supabase/supabase-js.SupabaseClient.constructor'
---
@@ -0,0 +1,119 @@
---
id: installing
title: 'Installing'
slug: installing
---
### Install as package
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
You can install @supabase/supabase-js via the terminal.
</RefSubLayout.Details>
<RefSubLayout.Examples>
<Tabs
size="small"
type="underlined"
defaultActiveId="npm"
queryGroup="platform"
>
<TabPanel id="npm" label="npm">
```sh Terminal
npm install @supabase/supabase-js
```
</TabPanel>
<TabPanel id="yarn" label="Yarn">
```sh Terminal
yarn add @supabase/supabase-js
```
</TabPanel>
<TabPanel id="pnpm" label="pnpm">
```sh Terminal
pnpm add @supabase/supabase-js
```
</TabPanel>
</Tabs>
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
### Install via CDN
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
You can install @supabase/supabase-js via CDN links.
</RefSubLayout.Details>
<RefSubLayout.Examples>
```js
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
//or
<script src="https://unpkg.com/@supabase/supabase-js@2"></script>
```
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
### Use at runtime in Deno
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
You can use supabase-js in the Deno runtime via [JSR](https://jsr.io/@supabase/supabase-js):
</RefSubLayout.Details>
<RefSubLayout.Examples>
```ts
import { createClient } from 'npm:@supabase/supabase-js@2'
```
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
### Enable Data API access
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
supabase-js uses the Data API to query and mutate your Postgres data. You first need to grant Data API roles permissions to access your tables and functions.
In [Data API integrations settings](/dashboard/project/_/integrations/data_api/settings), expose the specific tables and functions you want to access. To automatically grant access for new tables and functions in `public`, enable **Default privileges for new entities**.
Alternatively, use SQL to grant the required permissions:
</RefSubLayout.Details>
<RefSubLayout.Examples>
```sql
-- Before granting access to client roles, make sure RLS is enabled
-- and create the policies required for each role's allowed operations.
alter table public.your_table enable row level security;
-- create policy ... on public.your_table ...;
-- Grant least-privilege access to tables after RLS and policies are in place
grant select on public.your_table to anon;
grant select, insert, update, delete on public.your_table to authenticated;
grant all on public.your_table to service_role;
-- Grant execute on functions after verifying any table access they rely on
grant execute on function public.your_function to authenticated, service_role;
```
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
@@ -0,0 +1,8 @@
---
id: introduction
title: Introduction
---
This reference documents every object and method available in Supabase's isomorphic JavaScript library, `supabase-js`. You can use `supabase-js` to interact with your Postgres database, listen to database changes, invoke Deno Edge Functions, build login and user management functionality, and manage large files.
To convert SQL queries to `supabase-js` calls, use the [SQL to REST API translator](/docs/guides/api/sql-to-rest).
@@ -0,0 +1,5 @@
{
"id": "oauth-admin",
"title": "OAuth Admin",
"description": "The OAuth Admin API allows you to manage OAuth clients programmatically. Only relevant when the OAuth 2.1 server is enabled in Supabase Auth. These functions should only be called on a server. Never expose your `secret` key in the browser."
}
@@ -0,0 +1,5 @@
{
"id": "oauth-server",
"title": "OAuth Server",
"description": "The OAuth Server API allows you to build custom OAuth consent screens for your application. Only relevant when the OAuth 2.1 server is enabled in Supabase Auth."
}
@@ -0,0 +1,5 @@
{
"id": "passkey-admin",
"title": "Passkey admin",
"description": "Contains passkey administration methods. Requires a secret key."
}
@@ -0,0 +1,235 @@
---
id: typescript-support
title: TypeScript support
---
`supabase-js` has TypeScript support for type inference, autocompletion, type-safe queries, and more.
With TypeScript, `supabase-js` detects things like `not null` constraints and [generated columns](https://www.postgresql.org/docs/current/ddl-generated-columns.html). Nullable columns are typed as `T | null` when you select the column. Generated columns will show a type error when you insert to it.
`supabase-js` also detects relationships between tables. A referenced table with one-to-many relationship is typed as `T[]`. Likewise, a referenced table with many-to-one relationship is typed as `T | null`.
## Generating TypeScript Types
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
You can use the Supabase CLI to [generate the types](/docs/reference/cli/supabase-gen-types). You can also generate the types [from the dashboard](https://supabase.com/dashboard/project/_/api?page=tables-intro).
</RefSubLayout.Details>
<RefSubLayout.Examples>
```bash Terminal
supabase gen types typescript --project-id abcdefghijklmnopqrst > database.types.ts
```
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
These types are generated from your database schema. Given a table `public.movies`, the generated types will look like:
</RefSubLayout.Details>
<RefSubLayout.Examples>
```sql
create table public.movies (
id bigint generated always as identity primary key,
name text not null,
data jsonb null
);
```
```ts ./database.types.ts
export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]
export interface Database {
public: {
Tables: {
movies: {
Row: { // the data expected from .select()
id: number
name: string
data: Json | null
}
Insert: { // the data to be passed to .insert()
id?: never // generated columns must not be supplied
name: string // `not null` columns with no default must be supplied
data?: Json | null // nullable columns can be omitted
}
Update: { // the data to be passed to .update()
id?: never
name?: string // `not null` columns are optional on .update()
data?: Json | null
}
}
}
}
}
```
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
## Using TypeScript type definitions
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
You can supply the type definitions to `supabase-js` like so:
</RefSubLayout.Details>
<RefSubLayout.Examples>
```ts ./index.tsx
import { createClient } from '@supabase/supabase-js'
import { Database } from './database.types'
const supabase = createClient<Database>(
process.env.SUPABASE_URL,
process.env.SUPABASE_PUBLISHABLE_KEY
)
```
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
## Helper types for Tables and Joins
You can use the following helper types to make the generated TypeScript types easier to use.
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
Sometimes the generated types are not what you expect. For example, a view's column may show up as nullable when you expect it to be `not null`. Using [type-fest](https://github.com/sindresorhus/type-fest), you can override the types like so:
</RefSubLayout.Details>
<RefSubLayout.Examples>
```ts ./database-generated.types.ts
export type Json = // ...
export interface Database {
// ...
}
```
```ts ./database.types.ts
import { MergeDeep } from 'type-fest'
import { Database as DatabaseGenerated } from './database-generated.types'
export { Json } from './database-generated.types'
// Override the type for a specific column in a view:
export type Database = MergeDeep<
DatabaseGenerated,
{
public: {
Views: {
movies_view: {
Row: {
// id is a primary key in public.movies, so it must be `not null`
id: number
}
}
}
}
}
>
```
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
You can also override the type of an individual successful response if needed:
</RefSubLayout.Details>
<RefSubLayout.Examples>
```ts
// Partial type override allows you to only override some of the properties in your results
const { data } = await supabase.from('countries').select().overrideTypes<Array<{ id: string }>>()
// For a full replacement of the original return type use the `{ merge: false }` property as second argument
const { data } = await supabase
.from('countries')
.select()
.overrideTypes<Array<{ id: string }>, { merge: false }>()
// Use it with `maybeSingle` or `single`
const { data } = await supabase.from('countries').select().single().overrideTypes<{ id: string }>()
```
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
The generated types provide shorthands for accessing tables and enums.
</RefSubLayout.Details>
<RefSubLayout.Examples>
```ts ./index.ts
import { Database, Tables, Enums } from "./database.types.ts";
// Before 😕
let movie: Database['public']['Tables']['movies']['Row'] = // ...
// After 😍
let movie: Tables<'movies'>
```
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
### Response types for complex queries
<RefSubLayout.EducationRow>
<RefSubLayout.Details>
`supabase-js` always returns a `data` object (for success), and an `error` object (for unsuccessful requests).
These helper types provide the result types from any query, including nested types for database joins.
Given the following schema with a relation between cities and countries, we can get the nested `CountriesWithCities` type:
```sql
create table countries (
"id" serial primary key,
"name" text
);
create table cities (
"id" serial primary key,
"name" text,
"country_id" int references "countries"
);
```
</RefSubLayout.Details>
<RefSubLayout.Examples>
```ts
import { QueryResult, QueryData, QueryError } from '@supabase/supabase-js'
const countriesWithCitiesQuery = supabase
.from("countries")
.select(`
id,
name,
cities (
id,
name
)
`);
type CountriesWithCities = QueryData<typeof countriesWithCitiesQuery>;
const { data, error } = await countriesWithCitiesQuery;
if (error) throw error;
const countriesWithCities: CountriesWithCities = data;
```
</RefSubLayout.Examples>
</RefSubLayout.EducationRow>
@@ -0,0 +1,44 @@
{
"id": "using-filters",
"title": "Using Filters",
"description": "Filters allow you to only return rows that match certain conditions.\n\nFilters can be used on `select()`, `update()`, `upsert()`, and `delete()` queries.\n\nIf a Postgres function returns a table response, you can also apply filters.\n",
"examples": [
{
"id": "applying-filters",
"name": "Applying Filters",
"description": "Filters must be applied after any of `select()`, `update()`, `upsert()`,\n`delete()`, and `rpc()` and before\n[modifiers](/docs/reference/javascript/using-modifiers).\n",
"code": "```ts\nconst { data, error } = await supabase\n .from('instruments')\n .select('name, section_id')\n .eq('name', 'violin') // Correct\n\nconst { data, error } = await supabase\n .from('instruments')\n .eq('name', 'violin') // Incorrect\n .select('name, section_id')\n```\n"
},
{
"id": "chaining-filters",
"name": "Chaining",
"description": "Filters can be chained together to produce advanced queries. For example,\nto query cities with population between 1,000 and 10,000:\n\n```ts\nconst { data, error } = await supabase\n .from('cities')\n .select('name, country_id')\n .gte('population', 1000)\n .lt('population', 10000)\n```\n",
"code": "```ts\nconst { data, error } = await supabase\n .from('cities')\n .select('name, country_id')\n .gte('population', 1000)\n .lt('population', 10000)\n```\n"
},
{
"id": "conditional-chaining",
"name": "Conditional Chaining",
"description": "Filters can be built up one step at a time and then executed. For example:\n\n```ts\nconst filterByName = null\nconst filterPopLow = 1000\nconst filterPopHigh = 10000\n\nlet query = supabase\n .from('cities')\n .select('name, country_id')\n\nif (filterByName) { query = query.eq('name', filterByName) }\nif (filterPopLow) { query = query.gte('population', filterPopLow) }\nif (filterPopHigh) { query = query.lt('population', filterPopHigh) }\n\nconst { data, error } = await query\n```\n",
"code": "```ts\nconst filterByName = null\nconst filterPopLow = 1000\nconst filterPopHigh = 10000\n\nlet query = supabase\n .from('cities')\n .select('name, country_id')\n\nif (filterByName) { query = query.eq('name', filterByName) }\nif (filterPopLow) { query = query.gte('population', filterPopLow) }\nif (filterPopHigh) { query = query.lt('population', filterPopHigh) }\n\nconst { data, error } = await query\n```\n"
},
{
"id": "filter-by-value-within-json-column",
"name": "Filter by values within a JSON column",
"code": "```ts\nconst { data, error } = await supabase\n .from('users')\n .select()\n .eq('address->postcode', 90210)\n```\n",
"data": {
"sql": "```sql\ncreate table\n users (\n id int8 primary key,\n name text,\n address jsonb\n );\n\ninsert into\n users (id, name, address)\nvalues\n (1, 'Michael', '{ \"postcode\": 90210 }'),\n (2, 'Jane', null);\n```\n"
},
"response": "```json\n{\n \"data\": [\n {\n \"id\": 1,\n \"name\": \"Michael\",\n \"address\": {\n \"postcode\": 90210\n }\n }\n ],\n \"status\": 200,\n \"statusText\": \"OK\"\n}\n```\n"
},
{
"id": "filter-referenced-tables",
"name": "Filter referenced tables",
"code": "```ts\nconst { data, error } = await supabase\n .from('orchestral_sections')\n .select(`\n name,\n instruments!inner (\n name\n )\n `)\n .eq('instruments.name', 'flute')\n```\n",
"data": {
"sql": "```sql\ncreate table\n orchestral_sections (id int8 primary key, name text);\ncreate table\n instruments (\n id int8 primary key,\n section_id int8 not null references orchestral_sections,\n name text\n );\n\ninsert into\n orchestral_sections (id, name)\nvalues\n (1, 'strings'),\n (2, 'woodwinds');\ninsert into\n instruments (id, section_id, name)\nvalues\n (1, 2, 'flute'),\n (2, 1, 'violin');\n```\n",
"response": "```json\n{\n \"data\": [\n {\n \"name\": \"woodwinds\",\n \"characters\": [\n {\n \"name\": \"flute\"\n }\n ]\n }\n ],\n \"status\": 200,\n \"statusText\": \"OK\"\n}\n```\n"
},
"description": "You can filter on referenced tables in your `select()` query using dot\nnotation.\n"
}
]
}
@@ -0,0 +1,5 @@
{
"id": "using-modifiers",
"title": "Using modifiers",
"description": "Filters work on the row level—they allow you to return rows that only match certain conditions without changing the shape of the rows. Modifiers are everything that don't fit that definition—allowing you to change the format of the response (e.g., returning a CSV string).\n\nModifiers must be specified after filters. Some modifiers only apply for queries that return rows (e.g., `select()` or `rpc()` on a function that returns a table response)."
}
@@ -0,0 +1,5 @@
{
"id": "vector-buckets",
"title": "Vector Buckets",
"description": "This section contains methods for working with Vector Buckets."
}