Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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 @@
---
---
10 changes: 10 additions & 0 deletions .typedoc/__tests__/file-structure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ describe('Typedoc output', () => {

expect(nestedFolders).toMatchInlineSnapshot(`
[
"backend/allowlist-identifier-api",
"backend/allowlist-identifier-api/methods",
"backend/billing-api",
"backend/billing-api/methods",
"backend/domain-api",
"backend/domain-api/methods",
"backend/organization-api",
"backend/organization-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