Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4a447b8
Drop functions from property tables; add JSDoc comments
alexisintech Apr 9, 2026
9100976
undo changes about removing functions from property tables
alexisintech Apr 9, 2026
ea3d2eb
Revert "undo changes about removing functions from property tables"
alexisintech Apr 9, 2026
0292f5f
only remove functions from property tables if output page listed in a…
alexisintech Apr 9, 2026
8b9b1d4
begin building extract-methods.mjs
alexisintech Apr 10, 2026
ac8f133
use custom-plugin in extract-methods for link replacements
alexisintech Apr 10, 2026
9139e27
catchall link replacements should never replace text in headings
alexisintech Apr 10, 2026
2978152
protectPipeDelimitedInlineCodeSpans; handle if members live on alias …
alexisintech Apr 10, 2026
adb43df
add client resource to link replacements
alexisintech Apr 10, 2026
a0a9731
refactor extract-methods to use existing plugins to create tables,etc
alexisintech Apr 10, 2026
c9129a4
type intersections should merge all type information into the generat…
alexisintech Apr 10, 2026
2d1ceec
objects get their own folders with <object>-properties file and <obje…
alexisintech Apr 10, 2026
6eedb5b
include typealias functions in the methods generation
alexisintech Apr 11, 2026
ac0f71d
add helpers for documenting union types in property tables
alexisintech Apr 14, 2026
acfba24
cleaning up comments
alexisintech Apr 14, 2026
67bfa0b
fix links
alexisintech Apr 14, 2026
064abe1
fix: handle union objects only for type declaration tables
alexisintech Apr 14, 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
6 changes: 5 additions & 1 deletion .typedoc/__tests__/file-structure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,15 @@ describe('Typedoc output', () => {

it('should only have these nested folders', async () => {
const folders = await scanDirectory('directory');
const nestedFolders = folders.filter(folder => !isTopLevelPath(folder));
const nestedFolders = folders.filter(folder => !isTopLevelPath(folder)).sort((a, b) => a.localeCompare(b));

expect(nestedFolders).toMatchInlineSnapshot(`
[
"react/legacy",
"shared/clerk",
"shared/clerk/clerk-methods",
"shared/client-resource",
"shared/client-resource/client-resource-methods",
]
`);
});
Expand Down
185 changes: 152 additions & 33 deletions .typedoc/custom-plugin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ const LINK_REPLACEMENTS = [
['deleted-object-resource', '/docs/reference/types/deleted-object-resource'],
['checkout-flow-resource', '/docs/reference/hooks/use-checkout#checkout-flow-resource'],
['organization-creation-defaults-resource', '#organization-creation-defaults-resource'],
['billing-namespace', '/docs/reference/objects/billing'],
['client-resource', '/docs/reference/objects/client'],
['redirect-options', '/docs/reference/types/redirect-options'],
['handle-o-auth-callback-params', '/docs/reference/types/handle-o-auth-callback-params'],
];

/**
Expand All @@ -125,97 +129,135 @@ function getRelativeLinkReplacements() {
});
}

/**
* First pass of `MarkdownPageEvent.END`: rewrite `(foo.mdx)` / relative paths to `/docs/...` (see {@link LINK_REPLACEMENTS}).
* Used by `extract-methods.mjs`, which does not go through the renderer hook.
*
* @param {string} contents
*/
export function applyRelativeLinkReplacements(contents) {
if (!contents) {
return contents;
}
let out = contents;
for (const { pattern, replace } of getRelativeLinkReplacements()) {
// @ts-ignore — string | function
out = out.replace(pattern, replace);
}
return out;
}

function getCatchAllReplacements() {
return [
{
pattern: /(?<![\[\w`])`Appearance`\\<`Theme`\\>/g,
pattern: /(?<![\[\w`#])`?APIKeysNamespace`?(?![\]\w`])/g,
replace: '[`APIKeysNamespace`](/docs/reference/objects/api-keys)',
},
{
pattern: /(?<![\[\w`#])`Appearance`\\<`Theme`\\>/g,
replace: '[`Appearance<Theme>`](/docs/guides/customizing-clerk/appearance-prop/overview)',
},
{
pattern: /\(CreateOrganizationParams\)/g,
pattern: /(?<![#])\(CreateOrganizationParams\)/g,
replace: '([CreateOrganizationParams](#create-organization-params))',
},
{
pattern: /`LoadedClerk`/g,
pattern: /(?<![#])`LoadedClerk`/g,
replace: '[Clerk](/docs/reference/objects/clerk)',
},
{
pattern: /(?<![\[\w`])`?LocalizationResource`?(?![\]\w`])/g,
pattern: /(?<![\[\w`#])`?LocalizationResource`?(?![\]\w`])/g,
replace: '[`LocalizationResource`](/docs/guides/customizing-clerk/localization)',
},
{
// SessionResource appears in plain text, with an array next to it, with backticks, etc.
// e.g. `SessionResource[]`
pattern: /(?<![`[\]])\bSessionResource(\[\])?\b(?![\]\)`])/g,
pattern: /(?<![`#[\]])\bSessionResource(\[\])?\b(?![\]\)`])/g,
replace: '[`SessionResource`](/docs/reference/objects/session)$1',
},
{
pattern: /(?<![\[\w`])`?SessionStatusClaim`?(?![\]\w`])/g,
pattern: /(?<![\[\w`#])`?SessionStatusClaim`?(?![\]\w`])/g,
replace: '[`SessionStatusClaim`](/docs/reference/types/session-status)',
},
{
pattern: /(?<![`[\]])\bSetActiveParams\b(?![\]\(])/g,
pattern: /(?<![`#[\]])\bSetActiveParams\b(?![\]\(])/g,
replace: '[SetActiveParams](/docs/reference/types/set-active-params)',
},
{
pattern: /(?<![\[\w`])`?SignInResource`?(?![\]\w`])/g,
pattern: /(?<![\[\w`#])`?SignInResource`?(?![\]\w`])/g,
replace: '[`SignInResource`](/docs/reference/objects/sign-in)',
},
{
pattern: /(?<![\[\w`])`?((?:SignIn|SignUp)Errors)`?(?![\]\w`])/g,
pattern: /(?<![\[\w`#])`?((?:SignIn|SignUp)Errors)`?(?![\]\w`])/g,
replace: (/** @type {string} */ _match, /** @type {string} */ type) =>
`[\`${type}\`](/docs/reference/types/errors)`,
},
{
pattern: /(?<![\[\w`])`?SignInFutureResource`?(?![\]\w`])/g,
pattern: /(?<![\[\w`#])`?SignInFutureResource`?(?![\]\w`])/g,
replace: '[`SignInFutureResource`](/docs/reference/objects/sign-in-future)',
},
{
pattern: /(?<![\[\w`])`?SignedInSessionResource`?(?![\]\w`])/g,
pattern: /(?<![\[\w`#])`?SignedInSessionResource`?(?![\]\w`])/g,
replace: '[`SignedInSessionResource`](/docs/reference/objects/session)',
},
{
pattern: /(?<![\[\w`])`?SignUpResource`?(?![\]\w`])/g,
pattern: /(?<![#])`SignInRedirectOptions`/g,
replace: '[`SignInRedirectOptions`](/docs/reference/types/sign-in-redirect-options)',
},
{
pattern: /(?<![#])`SignUpRedirectOptions`/g,
replace: '[`SignUpRedirectOptions`](/docs/reference/types/sign-up-redirect-options)',
},
{
pattern: /(?<![\[\w`#])`?SignUpResource`?(?![\]\w`])/g,
replace: '[`SignUpResource`](/docs/reference/objects/sign-up)',
},
{
pattern: /(?<![\[\w`])`?SignUpFutureResource`?(?![\]\w`])/g,
pattern: /(?<![#])`SignUpUnsafeMetadata`/g,
replace: '[`SignUpUnsafeMetadata`](/docs/reference/types/metadata#sign-up-unsafe-metadata)',
},
{
pattern: /(?<![\[\w`#])`?SignUpFutureResource`?(?![\]\w`])/g,
replace: '[`SignUpFutureResource`](/docs/reference/objects/sign-up-future)',
},
{
pattern: /(?<![\[\w`])`?OrganizationResource`?(?![\]\w`])/g,
pattern: /(?<![#])`TasksRedirectOptions`/g,
replace: '[`TasksRedirectOptions`](/docs/reference/types/redirect-options)',
},
{
pattern: /(?<![\[\w`#])`?OrganizationResource`?(?![\]\w`])/g,
replace: '[`OrganizationResource`](/docs/reference/objects/organization)',
},
{
pattern: /`OrganizationPrivateMetadata`/g,
pattern: /(?<![#])`OrganizationPrivateMetadata`/g,
replace: '[`OrganizationPrivateMetadata`](/docs/reference/types/metadata#organization-private-metadata)',
},
{
pattern: /OrganizationPublicMetadata/g,
pattern: /(?<![#])\bOrganizationPublicMetadata\b/g,
replace: '[OrganizationPublicMetadata](/docs/reference/types/metadata#organization-public-metadata)',
},
{
pattern: /`OrganizationInvitationPrivateMetadata`/g,
pattern: /(?<![#])`OrganizationInvitationPrivateMetadata`/g,
replace:
'[`OrganizationInvitationPrivateMetadata`](/docs/reference/types/metadata#organization-invitation-private-metadata)',
},
{
pattern: /`OrganizationInvitationPublicMetadata`/g,
pattern: /(?<![#])`OrganizationInvitationPublicMetadata`/g,
replace:
'[`OrganizationInvitationPublicMetadata`](/docs/reference/types/metadata#organization-invitation-public-metadata)',
},
{
pattern: /`OrganizationMembershipPrivateMetadata`/g,
pattern: /(?<![#])`OrganizationMembershipPrivateMetadata`/g,
replace:
'[`OrganizationMembershipPrivateMetadata`](/docs/reference/types/metadata#organization-membership-private-metadata)',
},
{
pattern: /`OrganizationMembershipPublicMetadata`/g,
pattern: /(?<![#])`OrganizationMembershipPublicMetadata`/g,
replace:
'[`OrganizationMembershipPublicMetadata`](/docs/reference/types/metadata#organization-membership-public-metadata)',
},
{
pattern: /(?<![\[\w`])`?UserResource`?(?![\]\w`])/g,
pattern: /(?<![\[\w`#])`?UserResource`?(?![\]\w`])/g,
replace: '[`UserResource`](/docs/reference/objects/user)',
},
{
Expand Down Expand Up @@ -251,27 +293,104 @@ function getCatchAllReplacements() {
];
}

/** CommonMark ATX heading: optional indent, 1–6 `#`, then space or end — entire line is left unchanged. */
const ATX_HEADING_LINE = /^\s{0,3}#{1,6}(?:\s|$)/;

/** Private-use placeholders — must not appear in real MDX and must not match catch-all patterns. */
const PIPE_CODE_PH = /\uE000(\d+)\uE001/g;

/**
* Inline code that contains a pipe (e.g. `` `a \\| b` `` or `` `a | b` ``) cannot receive per-token
* link replacements without breaking MDX. Replace those whole spans with placeholders, run catch-alls,
* then restore.
*
* @param {string} line
* @returns {{ text: string, placeholders: string[] }}
*/
function protectPipeDelimitedInlineCodeSpans(line) {
/** @type {string[]} */
const placeholders = [];
const text = line.replace(/`([^`\n]*)`/g, (full, inner) => {
if (!inner.includes('|')) {
return full;
}
const id = placeholders.length;
placeholders.push(full);
return `\uE000${id}\uE001`;
});
return { text, placeholders };
}

/**
* @param {string} text
* @param {string[]} placeholders
*/
function restoreProtectedInlineCodeSpans(text, placeholders) {
return text.replace(PIPE_CODE_PH, (_, /** @type {string} */ i) => placeholders[Number(i)] ?? '');
}

/**
* Remove the Properties section (heading + table) from reference object pages (e.g. `shared/clerk/clerk.mdx`);
* the table is copied into `shared/<object>/<object>-properties.mdx` by `extract-methods.mjs`.
*
* @param {string} contents
*/
export function stripReferenceObjectPropertiesSection(contents) {
if (!contents) {
return contents;
}
const stripped = contents.replace(/\r\n/g, '\n').replace(/\n## Properties\n+[\s\S]*$/, '');
return stripped.trimEnd() + '\n';
}

/**
* Second pass of `MarkdownPageEvent.END` (after {@link applyRelativeLinkReplacements}).
* Used by `extract-methods.mjs`, which writes MDX outside TypeDoc and never hits that hook.
*
* Skips ATX heading lines (`#` … `######`) so titles like `#### SetActiveParams` are never linkified.
* (A lone `(?<!#)` in regex is not enough: heading text is separated from `###` by spaces.)
*
* Skips inline code spans that contain `|` (union / enum style like `` `v1 \\| v2` ``) so link rules do
* not run inside them — otherwise MDX breaks.
*
* @param {string} contents
*/
export function applyCatchAllMdReplacements(contents) {
if (!contents) {
return contents;
}
return contents
.split('\n')
.map(
/** @param {string} line */ line => {
if (ATX_HEADING_LINE.test(line.replace(/\r$/, ''))) {
return line;
}
const { text: withPh, placeholders } = protectPipeDelimitedInlineCodeSpans(line);
let out = withPh;
for (const { pattern, replace } of getCatchAllReplacements()) {
// @ts-ignore — string | function
out = out.replace(pattern, replace);
}
return restoreProtectedInlineCodeSpans(out, placeholders);
},
)
.join('\n');
}

/**
* @param {import('typedoc-plugin-markdown').MarkdownApplication} app
*/
export function load(app) {
app.renderer.on(MarkdownPageEvent.END, output => {
const fileName = output.url.split('/').pop();
const linkReplacements = getRelativeLinkReplacements();

for (const { pattern, replace } of linkReplacements) {
if (output.contents) {
output.contents = output.contents.replace(pattern, replace);
}
if (output.contents) {
output.contents = applyRelativeLinkReplacements(output.contents);
}

const catchAllReplacements = getCatchAllReplacements();

for (const { pattern, replace } of catchAllReplacements) {
if (output.contents) {
// @ts-ignore - Mixture of string and function replacements
output.contents = output.contents.replace(pattern, replace);
}
if (output.contents) {
output.contents = applyCatchAllMdReplacements(output.contents);
}

if (fileName) {
Expand Down
22 changes: 22 additions & 0 deletions .typedoc/custom-router.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
// @ts-check
import { ReflectionKind } from 'typedoc';
import { MemberRouter } from 'typedoc-plugin-markdown';

import { REFERENCE_OBJECT_PAGE_SYMBOLS } from './reference-objects.mjs';

/** @type {Set<string>} */
const REFERENCE_OBJECT_SYMBOL_NAMES = new Set(Object.values(REFERENCE_OBJECT_PAGE_SYMBOLS));

/**
* From a filepath divided by `/` only keep the first and last part
* @param {string} filePath
Expand Down Expand Up @@ -72,6 +78,22 @@ class ClerkRouter extends MemberRouter {
*/
filePath = flattenDirName(filePath);

/**
* Put each reference object in its own folder alongside `<object>-properties.mdx` and `<object>-methods/` from `extract-methods.mjs`.
* E.g. `shared/clerk.mdx` -> `shared/clerk/clerk.mdx` and `shared/clerk/clerk-properties.mdx` and `shared/clerk/clerk-methods/`.
*/
if (
(reflection.kind === ReflectionKind.Interface || reflection.kind === ReflectionKind.Class) &&
REFERENCE_OBJECT_SYMBOL_NAMES.has(reflection.name)
) {
const kebab = toKebabCase(reflection.name);
const m = filePath.match(/^([^/]+)\/([^/]+)$/);
if (m) {
const [, pkg] = m;
return `${pkg}/${kebab}/${kebab}`;
}
}

return filePath;
}
}
Loading
Loading