Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5147b18
docs(backend,shared): Generate typedoc output for backend docs
alexisintech Jun 13, 2026
49b7e87
docs(backend): Extract backend API methods into per-method MDX pages
alexisintech Jun 15, 2026
daceb9d
docs(backend,shared): Extract OrganizationAPI methods into per-method…
alexisintech Jun 17, 2026
9f65eee
fix broken organization API links
alexisintech Jun 17, 2026
6273c3f
fix(backend): enumerate OrganizationApi barrel exports to avoid name …
alexisintech Jun 17, 2026
e178b28
docs review - organization
SarahSoutoul Jun 17, 2026
231cddd
document the backend deleted-object
alexisintech Jun 17, 2026
135ca40
fix(backend): enumerate typedoc endpoint entry points explicitly
alexisintech Jun 17, 2026
4da4276
Document Billing, Allowlist Identifier, and Domains APIs
alexisintech Jun 18, 2026
64f24dd
docs review - user
SarahSoutoul Jun 18, 2026
9cb19e5
docs review - domains, allowlist and billing
SarahSoutoul Jun 18, 2026
49556f8
apply Organization docs fixes
alexisintech Jun 18, 2026
c385aa9
custom_google --> oauth_custom_google
alexisintech Jun 18, 2026
7007c43
update CreateAllowlistIdentifierParams
alexisintech Jun 18, 2026
002391a
clean up comment formatting
alexisintech Jun 18, 2026
f89143b
fix deprecation tag missing from params table; add back deprecation t…
alexisintech Jun 18, 2026
9e53ded
Merge branch 'main' into aa/DOCS-10984
alexisintech Jun 18, 2026
88e40b8
remove organization-domain-resource from custom plugin
alexisintech Jun 18, 2026
6127d23
Document Session, Client, Invitation API
alexisintech Jun 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/docs-backend-typedoc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
16 changes: 16 additions & 0 deletions .typedoc/__tests__/file-structure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,22 @@ describe('Typedoc output', () => {

expect(nestedFolders).toMatchInlineSnapshot(`
[
"backend/allowlist-identifier-api",
"backend/allowlist-identifier-api/methods",
"backend/billing-api",
"backend/billing-api/methods",
"backend/client-api",
"backend/client-api/methods",
"backend/domain-api",
"backend/domain-api/methods",
"backend/invitation-api",
"backend/invitation-api/methods",
"backend/organization-api",
"backend/organization-api/methods",
"backend/session-api",
"backend/session-api/methods",
"backend/user-api",
"backend/user-api/methods",
"react/legacy",
"shared/api-key-resource",
"shared/api-key-resource/methods",
Expand Down
81 changes: 80 additions & 1 deletion .typedoc/custom-plugin.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @ts-check - Enable TypeScript checks for safer MDX post-processing and link rewriting
import { Converter } from 'typedoc';
import { Converter, DeclarationReflection, ReflectionKind, ReflectionType, RendererEvent } from 'typedoc';
import { MarkdownPageEvent } from 'typedoc-plugin-markdown';

/**
Expand Down Expand Up @@ -73,6 +73,7 @@ const LINK_REPLACEMENTS = [
['o-auth-consent-info', '/docs/reference/types/oauth-consent-info'],
['o-auth-consent-scope', '/docs/reference/types/oauth-consent-scope'],
['o-auth-strategy', '/docs/reference/types/sso#o-auth-strategy'],
['o-auth-provider', '/docs/reference/types/sso#o-auth-provider'],
['session', '/docs/reference/backend/types/backend-session'],
['session-activity', '/docs/reference/backend/types/backend-session-activity'],
['organization', '/docs/reference/backend/types/backend-organization'],
Expand Down Expand Up @@ -128,6 +129,10 @@ const LINK_REPLACEMENTS = [
['session-task', '/docs/reference/types/session-task'],
['public-user-data', '/docs/reference/types/public-user-data'],
['session-status', '/docs/reference/types/session-status'],
[
'create-organization-invitation-params',
'/docs/reference/backend/organization/create-organization-invitation#create-organization-invitation-params',
],
];

/**
Expand Down Expand Up @@ -289,6 +294,10 @@ function getCatchAllReplacements() {
pattern: /(?<![\[\w`#])`?OAuthStrategy`?(?![\]\w`])/g,
replace: '[OAuthStrategy](/docs/reference/types/sso#o-auth-strategy)',
},
{
pattern: /(?<![\[\w`#])`?OAuthProvider`?(?![\]\w`])/g,
replace: '[OAuthProvider](/docs/reference/types/sso#o-auth-provider)',
},
{
pattern: /(?<![\[\w`#])`?OrganizationResource`?(?![\]\w`])/g,
replace: '[OrganizationResource](/docs/reference/objects/organization)',
Expand Down Expand Up @@ -464,6 +473,33 @@ export function applyCatchAllMdReplacements(contents) {
.join('\n');
}

/**
* Walk a typedoc Type and return a flat list of property declarations to render as a merged table. Used by the `@expandProperties` flattener below to handle three shapes:
* - intersection types: walk each constituent
* - inline object literals (ReflectionType): take its declaration.children
* - named references (ReferenceType): take the target's children plus any properties contributed via type arguments, which captures the `Foo<{ ... }>` instantiation pattern where typedoc otherwise loses the generic parameter at the alias boundary.
*
* @param {import('typedoc').SomeType | undefined} type
* @param {Map<string, import('typedoc').Reflection>} reflectionsByName lookup for cross-package refs whose `.reflection` is not linked
* @returns {import('typedoc').DeclarationReflection[]}
*/
function collectPropertiesFromType(type, reflectionsByName) {
if (!type) return [];
if (type.type === 'reflection') {
return type.declaration?.children ?? [];
}
if (type.type === 'intersection') {
return type.types.flatMap(t => collectPropertiesFromType(t, reflectionsByName));
}
if (type.type === 'reference') {
const target = type.reflection ?? reflectionsByName.get(type.name);
const targetChildren = target?.children ?? [];
const argChildren = (type.typeArguments ?? []).flatMap(t => collectPropertiesFromType(t, reflectionsByName));
return [...targetChildren, ...argChildren];
}
return [];
}

/**
* @param {import('typedoc-plugin-markdown').MarkdownApplication} app
*/
Expand All @@ -479,6 +515,49 @@ export function load(app) {
}
});

/**
* Flatten the `Foo<{...}>` generic-instantiation pattern into a single merged properties table when `Foo` opts in via `@expandProperties`. typedoc-plugin-markdown would otherwise render an empty page for these aliases because the resolved type is a `ReferenceType` with no inline declaration — see `member.declaration.js` in the plugin, which only walks `IntersectionType` sub-types and has no branch for top-level `ReferenceType`.
*
* Runs at `RendererEvent.BEGIN` rather than `EVENT_RESOLVE_END` because the resolve hook fires per package, and cross-package references (e.g. `@clerk/backend` types referencing `ClerkPaginationRequest` from `@clerk/shared`) only link up after typedoc merges packages.
*
* The opt-in tag lives on the wrapper type so we never accidentally flatten unrelated generic aliases (e.g. `SignInErrors = Errors<SignInFields>`).
*/
app.renderer.on(RendererEvent.BEGIN, event => {
const all = Object.values(event.project.reflections);
const reflectionsByName = new Map();
for (const r of all) {
if (r.name && !reflectionsByName.has(r.name)) reflectionsByName.set(r.name, r);
}
const expandable = new Set();
for (const r of all) {
if (r.comment?.modifierTags?.has('@expandProperties')) {
expandable.add(r);
r.comment.modifierTags.delete('@expandProperties');
}
}
for (const reflection of all) {
if (
reflection.kindOf?.(ReflectionKind.TypeAlias) &&
reflection.type?.type === 'reference' &&
Array.isArray(reflection.type.typeArguments) &&
reflection.type.typeArguments.length > 0
) {
const target = reflection.type.reflection ?? reflectionsByName.get(reflection.type.name);
if (!target || !expandable.has(target)) continue;
const merged = collectPropertiesFromType(reflection.type, reflectionsByName);
if (merged.length > 0) {
// typedoc's package-level `sort: 'alphabetical'` is applied during conversion, before
// our synthetic merge runs. Sort here to match the alphabetical ordering used by
// every other table in the docs.
merged.sort((a, b) => a.name.localeCompare(b.name));
const decl = new DeclarationReflection('__type', ReflectionKind.TypeLiteral, reflection);
decl.children = merged;
reflection.type = new ReflectionType(decl);
}
}
}
});

app.renderer.on(MarkdownPageEvent.END, output => {
const fileName = output.url.split('/').pop();

Expand Down
79 changes: 78 additions & 1 deletion .typedoc/custom-theme.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,68 @@ function renderPropertiesFormatTable(args) {
: table(args.headers, args.rows, leftAlignHeadings);
}

/**
* Resolve a property's type to the `@inline` class/interface declaration it ultimately points at, or `undefined` if the type isn't (or doesn't unwrap to) one. Unwraps `OptionalType` and a union arm — covers `T | null` / `T | undefined`. Used to decide whether to expand nested rows for a property whose type is rendered inline as `\{ … \}`.
*
* @param {import('typedoc').Type | undefined} t
*/
function getInlineClassOrInterfaceTarget(t) {
let bare = /** @type {import('typedoc').Type | undefined} */ (unwrapOptional(t));
if (bare && bare.type === 'union') {
const u = /** @type {import('typedoc').UnionType} */ (bare);
bare = u.types.find(arm => {
if (arm.type !== 'reference') return false;
const refl = /** @type {import('typedoc').ReferenceType} */ (arm).reflection;
if (!refl || !isInlineModifierWithoutStandalonePage(refl)) return false;
return (
/** @type {{ kindOf?: (k: number) => boolean }} */ (refl).kindOf?.(ReflectionKind.Class) ||
/** @type {{ kindOf?: (k: number) => boolean }} */ (refl).kindOf?.(ReflectionKind.Interface)
);
});
}
if (!bare || bare.type !== 'reference') return undefined;
const decl = /** @type {import('typedoc').ReferenceType} */ (bare).reflection;
if (!decl) return undefined;
if (!isInlineModifierWithoutStandalonePage(decl)) return undefined;
const d = /** @type {import('typedoc').DeclarationReflection} */ (decl);
if (!d.kindOf(ReflectionKind.Class) && !d.kindOf(ReflectionKind.Interface)) return undefined;
return d;
}

/**
* Append synthesized `parent.child` rows after each property whose type is an `@inline` class or interface (or `null | InlineClass`). Mirrors {@link appendUnionObjectChildPropertyRows} — the synthesized reflection inherits the child's `flags.isOptional` so the renderer appends `?` on its own, and uses `?.` as the separator when the parent is optional.
*
* @template {import('typedoc').DeclarationReflection} T
* @param {T[]} base
* @returns {T[]}
*/
function appendInlineObjectChildPropertyRows(base) {
/** @type {T[]} */
const out = [];
for (const prop of base) {
out.push(prop);
if (prop.name.includes('.')) continue;
const target = getInlineClassOrInterfaceTarget(prop.type);
if (!target) continue;
const children = target.children?.filter(c => c.kindOf(ReflectionKind.Property));
if (!children?.length) continue;
const sep = prop.flags?.isOptional ? '?.' : '.';
for (const child of children) {
out.push(
/** @type {T} */ (
/** @type {unknown} */ ({
...child,
name: `${prop.name}${sep}${child.name}`,
getFullName: () => prop.getFullName(),
getFriendlyFullName: () => prop.getFriendlyFullName(),
})
),
);
}
}
return out;
}

/**
* Same logic as typedoc-plugin-markdown `member.typeDeclarationTable`, but **always** runs `getFlattenedDeclarations` and then {@link appendUnionObjectChildPropertyRows} (union-object arm rows like `telemetry.*`). The default plugin skips flattening in `compact` mode, which hides nested keys like `telemetry.disabled`.
*
Expand Down Expand Up @@ -1105,6 +1167,20 @@ class ClerkMarkdownThemeContext extends MarkdownThemeContext {
if (decl.kindOf(ReflectionKind.TypeAlias) && decl.type) {
return removeLineBreaks(this.partials.someType(decl.type));
}
// Class or interface: there's no RHS to render, but the declaration's children describe the instance shape. Inline as a type-literal `\{ key: type; … \}` so use sites surface the same fields readers would see on the standalone page. Curly braces are escaped because MDX otherwise parses the literal as a JSX expression (and the object-literal bodies we emit aren't valid JS expressions).
if ((decl.kindOf(ReflectionKind.Class) || decl.kindOf(ReflectionKind.Interface)) && decl.children?.length) {
const props = decl.children.filter(c => c.kindOf(ReflectionKind.Property));
if (props.length) {
const fields = props
.map(p => {
const sep = p.flags?.isOptional ? '?:' : ':';
const typeMd = p.type ? this.partials.someType(p.type) : '`unknown`';
return `${p.name}${sep} ${typeMd};`;
})
.join(' ');
return removeLineBreaks(`\\{ ${fields} \\}`);
}
}
return backTicks(decl.name);
}
return superPartials.referenceType.call(this, model);
Expand All @@ -1128,7 +1204,8 @@ class ClerkMarkdownThemeContext extends MarkdownThemeContext {
prop => !isCallableInterfaceProperty(prop, this.helpers) && !prop.comment?.hasModifier('@extractMethods'),
)
: model;
return superPartials.propertiesTable(filtered, options);
const expanded = appendInlineObjectChildPropertyRows(filtered);
return superPartials.propertiesTable(expanded, options);
},
/**
* Parameter tables: same as the stock partial except single-property inline object params are not expanded to nested rows.
Expand Down
Loading
Loading