diff --git a/.typedoc/__tests__/file-structure.test.ts b/.typedoc/__tests__/file-structure.test.ts index 983a0972d5b..650ff374e25 100644 --- a/.typedoc/__tests__/file-structure.test.ts +++ b/.typedoc/__tests__/file-structure.test.ts @@ -52,6 +52,10 @@ describe('Typedoc output', () => { expect(nestedFolders).toMatchInlineSnapshot(` [ "react/legacy", + "shared/clerk", + "shared/clerk/clerk-methods", + "shared/client-resource", + "shared/client-resource/client-resource-methods", ] `); }); diff --git a/.typedoc/custom-plugin.mjs b/.typedoc/custom-plugin.mjs index d2f31f4270e..c6a8dde0f3a 100644 --- a/.typedoc/custom-plugin.mjs +++ b/.typedoc/custom-plugin.mjs @@ -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'], ]; /** @@ -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: /(?/g, + pattern: /(?/g, replace: '[`Appearance`](/docs/guides/customizing-clerk/appearance-prop/overview)', }, { - pattern: /\(CreateOrganizationParams\)/g, + pattern: /(? `[\`${type}\`](/docs/reference/types/errors)`, }, { - pattern: /(? { + 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//-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 `(? { + 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) { diff --git a/.typedoc/custom-router.mjs b/.typedoc/custom-router.mjs index 97cf8acef8d..b273745b2cb 100644 --- a/.typedoc/custom-router.mjs +++ b/.typedoc/custom-router.mjs @@ -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} */ +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 @@ -72,6 +78,22 @@ class ClerkRouter extends MemberRouter { */ filePath = flattenDirName(filePath); + /** + * Put each reference object in its own folder alongside `-properties.mdx` and `-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; } } diff --git a/.typedoc/custom-theme.mjs b/.typedoc/custom-theme.mjs index ec1c32fa2dd..a1b29dc10c1 100644 --- a/.typedoc/custom-theme.mjs +++ b/.typedoc/custom-theme.mjs @@ -1,7 +1,242 @@ // @ts-check -import { ReflectionKind, ReflectionType, UnionType } from 'typedoc'; +import { IntersectionType, ReferenceType, ReflectionKind, ReflectionType, UnionType } from 'typedoc'; import { MarkdownTheme, MarkdownThemeContext } from 'typedoc-plugin-markdown'; +import { REFERENCE_OBJECTS_LIST } from './reference-objects.mjs'; + +export { REFERENCE_OBJECTS_LIST }; + +/** + * Unwrap optional TypeDoc types so referenced object shapes are still found. + * + * @param {import('typedoc').Type} t + * @returns {import('typedoc').Type} + */ +/** + * Prefer structural checks over `instanceof` so we still match when multiple TypeDoc copies are loaded + * (otherwise `instanceof IntersectionType` is false at render time). + * + * @param {import('typedoc').Type | undefined} t + * @returns {t is import('typedoc').IntersectionType} + */ +function isIntersectionTypeDoc(t) { + const o = /** @type {{ type?: string; types?: import('typedoc').Type[] } | null} */ (t); + return Boolean(o && typeof o === 'object' && o.type === 'intersection' && Array.isArray(o.types)); +} + +/** + * @param {import('typedoc').Type | undefined} t + * @returns {t is import('typedoc').ReferenceType} + */ +function isReferenceTypeDoc(/** @type {import('typedoc').Type | undefined} */ t) { + return Boolean(t && typeof t === 'object' && /** @type {{ type?: string }} */ (t).type === 'reference'); +} + +/** + * @param {import('typedoc').Type | undefined} t + * @returns {t is import('typedoc').ReflectionType} + */ +function isReflectionTypeDoc(/** @type {import('typedoc').Type | undefined} */ t) { + return Boolean(t && typeof t === 'object' && /** @type {{ type?: string }} */ (t).type === 'reflection'); +} + +/** + * @param {import('typedoc').Type | undefined} t + */ +function unwrapOptionalType(t) { + if ( + t && + typeof t === 'object' && + 'type' in t && + /** @type {{ type: string }} */ (t).type === 'optional' && + 'elementType' in t + ) { + return /** @type {{ elementType: import('typedoc').Type }} */ (t).elementType; + } + return t; +} + +/** + * When `ReferenceType.reflection` is unset (common for imported aliases), resolve by name in the converted project. + * + * @param {import('typedoc').ProjectReflection | undefined} project + * @param {string} name + * @returns {import('typedoc').DeclarationReflection | undefined} + */ +function findNamedTypeDeclaration(project, name) { + if (!project?.reflections) { + return undefined; + } + for (const r of Object.values(project.reflections)) { + if (r.name !== name) { + continue; + } + if (r.kind === ReflectionKind.TypeAlias || r.kind === ReflectionKind.Interface) { + return /** @type {import('typedoc').DeclarationReflection} */ (r); + } + } + return undefined; +} + +/** + * Collect documented property reflections from one intersection arm (object literal, type alias, interface, nested `&`). + * + * @param {import('typedoc').Type} t + * @param {Set} visitedReflectionIds + * @param {import('typedoc').ProjectReflection | undefined} project + * @returns {import('typedoc').DeclarationReflection[]} + */ +function collectPropertyReflectionsFromIntersectionArm(t, visitedReflectionIds, project) { + const unwrapped = unwrapOptionalType(t); + if (!unwrapped) { + return []; + } + + if (isReflectionTypeDoc(unwrapped)) { + const decl = unwrapped.declaration; + if (!decl) { + return []; + } + if (decl.signatures?.length && !decl.children?.length) { + return []; + } + return (decl.children ?? []).filter(c => c.kind === ReflectionKind.Property); + } + + if (isReferenceTypeDoc(unwrapped)) { + let ref = unwrapped.reflection; + if (!ref && unwrapped.name && project) { + ref = findNamedTypeDeclaration(project, unwrapped.name); + } + if (!ref) { + return []; + } + const declRef = /** @type {import('typedoc').DeclarationReflection | undefined} */ ( + 'kind' in ref ? ref : undefined + ); + if (!declRef) { + return []; + } + const id = declRef.id; + if (id != null) { + if (visitedReflectionIds.has(id)) { + return []; + } + visitedReflectionIds.add(id); + } + try { + if (declRef.kind === ReflectionKind.TypeAlias) { + if (declRef.children?.length) { + return declRef.children.filter( + /** @param {import('typedoc').DeclarationReflection} c */ + c => c.kind === ReflectionKind.Property, + ); + } + if (declRef.type) { + return collectPropertyReflectionsFromIntersectionArm(declRef.type, visitedReflectionIds, project); + } + return []; + } + if ( + (declRef.kind === ReflectionKind.Interface || declRef.kind === ReflectionKind.Class) && + declRef.children?.length + ) { + return declRef.children.filter( + /** @param {import('typedoc').DeclarationReflection} c */ + c => c.kind === ReflectionKind.Property, + ); + } + } finally { + if (id != null) { + visitedReflectionIds.delete(id); + } + } + return []; + } + + if (isIntersectionTypeDoc(unwrapped)) { + /** @type {import('typedoc').DeclarationReflection[]} */ + const out = []; + for (const arm of unwrapped.types) { + out.push(...collectPropertyReflectionsFromIntersectionArm(arm, visitedReflectionIds, project)); + } + return out; + } + + return []; +} + +/** + * Merge intersection arms into one property list (later duplicate names override earlier ones, then sort by name). + * + * @param {import('typedoc').IntersectionType} intersection + * @param {import('typedoc').ProjectReflection | undefined} project + * @returns {import('typedoc').DeclarationReflection[]} + */ +function mergeIntersectionPropertyReflections(intersection, project) { + /** @type {Map} */ + const byName = new Map(); + const visited = new Set(); + for (const arm of intersection.types) { + for (const p of collectPropertyReflectionsFromIntersectionArm(arm, visited, project)) { + byName.set(p.name, p); + } + } + return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)); +} + +/** + * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx + * @param {import('typedoc').DeclarationReflection} model + * @param {{ nested?: boolean; headingLevel?: number }} opts + * @param {import('typedoc').DeclarationReflection[]} mergedChildren + * @param {import('typedoc-plugin-markdown').MarkdownThemeContext['partials']} superPartials + */ +function renderMergedIntersectionDeclaration(ctx, model, opts, mergedChildren, superPartials) { + /** @type {string[]} */ + const md = []; + const headingLevel = opts.headingLevel ?? 2; + const nested = opts.nested ?? false; + + if (!nested && model.sources && !ctx.options.getValue('disableSources')) { + md.push(superPartials.sources(model)); + } + if (model?.documents) { + md.push(superPartials.documents(model, { headingLevel })); + } + if (model.comment) { + md.push( + superPartials.comment(model.comment, { + headingLevel, + showSummary: true, + showTags: false, + }), + ); + } + + const synthetic = /** @type {import('typedoc').DeclarationReflection} */ ( + /** @type {unknown} */ ({ + children: mergedChildren, + parent: model, + kind: ReflectionKind.TypeLiteral, + }) + ); + md.push(superPartials.typeDeclaration(synthetic, { headingLevel })); + + if (model.comment) { + md.push( + superPartials.comment(model.comment, { + headingLevel, + showSummary: false, + showTags: true, + showReturns: true, + }), + ); + } + md.push(superPartials.inheritance(model, { headingLevel: opts.headingLevel ?? headingLevel })); + return md.filter(Boolean).join('\n\n'); +} + /** * @param {import('typedoc-plugin-markdown').MarkdownApplication} app */ @@ -29,6 +264,23 @@ class ClerkMarkdownTheme extends MarkdownTheme { */ const unionCommentMap = new Map(); +/** + * Only for the specified pages do we remove function-valued members from property tables in the "Properties" section. + * + * @param {string | undefined} pageUrl - The URL of the page to check. + * @param {readonly string[]} allowlist - The list of pages to check. + */ +function pageMatchesPropertyTableFunctionFilterAllowlist(pageUrl, allowlist) { + if (!pageUrl) { + return false; + } + const normalized = pageUrl.replace(/\\/g, '/').replace(/^\.\//, ''); + return allowlist.some(entry => { + const e = entry.replace(/\\/g, '/').replace(/^\/+/, ''); + return normalized === e || normalized.endsWith(`/${e}`) || normalized.endsWith(e); + }); +} + /** * Our custom Clerk theme * @extends MarkdownThemeContext @@ -48,6 +300,21 @@ class ClerkMarkdownThemeContext extends MarkdownThemeContext { this.partials = { ...superPartials, + /** + * On allowlisted output pages only (see `PROPERTY_TABLE_EXCLUDE_FUNCTIONS_ALLOWLIST`): drop function-valued + * interface/class properties from property tables (property syntax with function types). Other pages unchanged. + * + * @param {import('typedoc').DeclarationReflection[]} model + * @param {Parameters[1]} [options] + */ + propertiesTable: (model, options) => { + if (!Array.isArray(model)) { + return superPartials.propertiesTable(/** @type {any} */ (model), options); + } + const allowlisted = pageMatchesPropertyTableFunctionFilterAllowlist(this.page?.url, REFERENCE_OBJECTS_LIST); + const filtered = allowlisted ? model.filter(prop => !isCallableInterfaceProperty(prop, this.helpers)) : model; + return superPartials.propertiesTable(filtered, options); + }, /** * This hides the "Type parameters" section and the signature title from the output (by default). Shows the signature title if the `@displayFunctionSignature` tag is present. * @param {import('typedoc').SignatureReflection} model @@ -319,6 +586,21 @@ class ClerkMarkdownThemeContext extends MarkdownThemeContext { * @param {{ headingLevel: number, nested?: boolean }} options */ declaration: (model, options = { headingLevel: 2, nested: false }) => { + const opts = { nested: false, ...options }; + const customizedModel = model; + customizedModel.typeParameters = undefined; + + if (!opts.nested && model.type && isIntersectionTypeDoc(model.type)) { + const merged = mergeIntersectionPropertyReflections( + /** @type {import('typedoc').IntersectionType} */ (model.type), + model.project, + ); + if (merged.length > 0) { + const output = renderMergedIntersectionDeclaration(this, customizedModel, opts, merged, superPartials); + return output.replace(/^## Type declaration$/gm, ''); + } + } + // Create a local override const localPartials = { ...this.partials, @@ -327,9 +609,6 @@ class ClerkMarkdownThemeContext extends MarkdownThemeContext { // Store original so that we can restore it later const originalPartials = this.partials; - const customizedModel = model; - customizedModel.typeParameters = undefined; - this.partials = localPartials; const output = superPartials.declaration(customizedModel, options); this.partials = originalPartials; @@ -637,3 +916,107 @@ function swap(arr, i, j) { arr[j] = t; return arr; } + +/** + * @param {import('typedoc').DeclarationReflection} prop + * @param {import('typedoc-plugin-markdown').MarkdownThemeContext['helpers']} helpers + */ +function isCallableInterfaceProperty(prop, helpers) { + /** + * Use the declared value type for properties. `getDeclarationType` mirrors accessor/parameter behavior and can + * return the wrong node when TypeDoc attaches signatures to the property (same class of bug as TypeAlias + `decl.type`). + */ + const t = + (prop.kind === ReflectionKind.Property || prop.kind === ReflectionKind.Variable) && prop.type + ? prop.type + : helpers.getDeclarationType(prop); + return isCallablePropertyValueType(t, helpers, new Set()); +} + +/** + * True when the property's value type is callable (function type, union/intersection of callables, or reference to a + * type alias of a function type). Object types with properties (e.g. namespaces) stay false. + * + * @param {import('typedoc').Type | undefined} t + * @param {import('typedoc-plugin-markdown').MarkdownThemeContext['helpers']} helpers + * @param {Set} seenReflectionIds + * @returns {boolean} + */ +function isCallablePropertyValueType(t, helpers, seenReflectionIds) { + if (!t) { + return false; + } + if (t.type === 'optional' && 'elementType' in t) { + return isCallablePropertyValueType( + /** @type {{ elementType: import('typedoc').Type }} */ (t).elementType, + helpers, + seenReflectionIds, + ); + } + if (t instanceof UnionType) { + const nonNullish = t.types.filter( + u => !(u.type === 'intrinsic' && ['undefined', 'null'].includes(/** @type {{ name: string }} */ (u).name)), + ); + if (nonNullish.length === 0) { + return false; + } + return nonNullish.every(u => isCallablePropertyValueType(u, helpers, seenReflectionIds)); + } + if (t instanceof IntersectionType) { + return t.types.some(u => isCallablePropertyValueType(u, helpers, seenReflectionIds)); + } + if (t instanceof ReflectionType) { + const decl = t.declaration; + const callSigs = decl.signatures?.length ?? 0; + const hasProps = (decl.children?.length ?? 0) > 0; + const hasIndex = (decl.indexSignatures?.length ?? 0) > 0; + return callSigs > 0 && !hasProps && !hasIndex; + } + if (t instanceof ReferenceType) { + /** + * Unresolved reference (`reflection` missing): TypeDoc did not link the symbol (not in entry graph, external, + * filtered, etc.). We cannot tell a function alias from an interface, so we only treat a few **name** patterns as + * callable (`*Function`, `*Listener`). For anything else, ensure the type is part of the documented program so + * `reflection` resolves and the structural checks above apply — do not add one-off type names here. + * E.g. `CustomNavigation`, `RouterFn`, etc. + */ + if (!t.reflection && typeof t.name === 'string' && /(?:Function|Listener)$/.test(t.name)) { + return true; + } + const ref = t.reflection; + if (!ref) { + return false; + } + const refId = ref.id; + if (refId != null && seenReflectionIds.has(refId)) { + return false; + } + if (refId != null) { + seenReflectionIds.add(refId); + } + try { + const decl = /** @type {import('typedoc').DeclarationReflection} */ (ref); + /** + * For `type Fn = (a: T) => U`, TypeDoc may attach call signatures to the TypeAlias reflection. + * `getDeclarationType` then returns `signatures[0].type` (here `U`), not the full function type, so we + * mis-classify properties typed as that alias (e.g. `navigate: CustomNavigation`) as non-callable. + * Prefer `decl.type` (the full RHS) for type aliases. + */ + const typeToCheck = + decl.kind === ReflectionKind.TypeAlias && decl.type + ? decl.type + : (helpers.getDeclarationType(decl) ?? decl.type); + if (typeToCheck) { + return isCallablePropertyValueType(typeToCheck, helpers, seenReflectionIds); + } + } finally { + if (refId != null) { + seenReflectionIds.delete(refId); + } + } + return false; + } + return false; +} + +export { isCallableInterfaceProperty }; diff --git a/.typedoc/extract-methods.mjs b/.typedoc/extract-methods.mjs new file mode 100644 index 00000000000..c17ebd5ed1c --- /dev/null +++ b/.typedoc/extract-methods.mjs @@ -0,0 +1,881 @@ +// @ts-check +/** + * For each entry in REFERENCE_OBJECTS_LIST, reads the TypeDoc output (e.g. `shared/clerk/clerk.mdx`), strips **Properties** from the main generated file and copies it into `-properties.mdx`, and writes one .mdx per method under `-methods/`. + * + * Run after `typedoc` (same cwd as repo root). Uses a second TypeDoc convert pass to read reflections. + * + * Like `extract-returns-and-params.mjs`, parameter tables are not hand-built: they use the same + * `MarkdownThemeContext.partials` as TypeDoc markdown output (`parametersTable` / `propertiesTable`, which call + * `someType` and therefore pick up `custom-theme.mjs` union/`<code>` behavior). Router + theme are prepared + * via `prepare-markdown-renderer.mjs` (same idea as `typedoc-plugin-markdown` `render()`). + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { Application, Comment, PageKind, ReflectionKind } from 'typedoc'; +import { MarkdownPageEvent, MarkdownTheme } from 'typedoc-plugin-markdown'; + +import typedocConfig from '../typedoc.config.mjs'; +import { isCallableInterfaceProperty } from './custom-theme.mjs'; +import { + applyCatchAllMdReplacements, + applyRelativeLinkReplacements, + stripReferenceObjectPropertiesSection, +} from './custom-plugin.mjs'; +import { prepareMarkdownRenderer } from './prepare-markdown-renderer.mjs'; +import { REFERENCE_OBJECTS_LIST, REFERENCE_OBJECT_PAGE_SYMBOLS } from './reference-objects.mjs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +/** + * @param {number} level + * @param {string} text + */ +function markdownHeading(level, text) { + const l = Math.min(Math.max(level, 1), 6); + return `${'#'.repeat(l)} ${text}`; +} + +/** + * Same as typedoc-plugin-markdown `removeLineBreaks` for table cells. + * + * @param {string | undefined} str + */ +function removeLineBreaksForTableCell(str) { + return str?.replace(/\r?\n/g, ' ').replace(/ {2,}/g, ' '); +} + +/** + * Append data rows to a markdown table string (header + separator + rows). + * + * @param {string} tableMd + * @param {string[]} rowLines Lines like `| a | b | c |` + */ +function appendMarkdownTableRows(tableMd, rowLines) { + if (!rowLines.length) { + return tableMd; + } + return `${tableMd.trimEnd()}\n${rowLines.join('\n')}\n`; +} + +/** + * Post-process the theme’s parameters markdown table. TypeDoc flattens object params as `parent.child` and may + * interleave those rows with other parameters. Here we (1) move each `parent.*` block directly under `parent`, + * and (2) rewrite dotted paths in the name column to optional-chaining (`parent?.child`, `a?.b?.c`). Top-level + * names are unchanged (`foo?`, `exa`). + * + * @param {string} tableMd + */ +function formatMethodParametersTable(tableMd) { + const leadingNewlines = (tableMd.match(/^\n+/) ?? [''])[0]; + const nonEmpty = tableMd.split('\n').filter(l => l.trim().length); + if (nonEmpty.length < 3) { + return tableMd; + } + const header = nonEmpty[0]; + const sep = nonEmpty[1]; + const dataLines = nonEmpty.slice(2).filter(l => l.trim().startsWith('|')); + if (dataLines.length <= 1) { + return tableMd; + } + + /** @param {string} line */ + const firstName = line => { + const m = line.match(/^\|\s*(?:<\/a>\s*)?`([^`]+)`/); + return m ? m[1] : ''; + }; + /** `parent.child` / `parent?.child` → grouping key `parent` (matches top-level `parent` or `parent?` via fallback below). */ + /** @param {string} raw */ + const parentOfNested = raw => { + const j = raw.indexOf('?.'); + if (j !== -1) { + return raw.slice(0, j); + } + const i = raw.indexOf('.'); + return i === -1 ? '' : raw.slice(0, i); + }; + /** `a.b.c` → `a?.b?.c`; leave `foo?` and names without `.` alone. */ + /** @param {string} raw */ + const nameForDisplay = raw => (!raw.includes('.') || raw.includes('?.') ? raw : raw.split('.').join('?.')); + /** @param {string} line @param {string} name */ + const replaceFirstName = (line, name) => + line.replace(/^(\|\s*(?:<\/a>\s*)?)`[^`]+`/, `$1\`${name}\``); + + const topLevelOrder = []; + const seenTop = new Set(); + /** @type {Map} */ + const childrenOf = new Map(); + + for (const line of dataLines) { + const raw = firstName(line); + if (!raw) { + continue; + } + if (!raw.includes('.')) { + if (!seenTop.has(raw)) { + seenTop.add(raw); + topLevelOrder.push(raw); + } + continue; + } + const p = parentOfNested(raw); + if (!p) { + continue; + } + let bucket = childrenOf.get(p); + if (!bucket) { + bucket = []; + childrenOf.set(p, bucket); + } + bucket.push(line); + } + + for (const lines of childrenOf.values()) { + lines.sort((a, b) => firstName(a).localeCompare(firstName(b))); + } + + /** @param {string} top */ + const rowsForParent = top => + childrenOf.get(top) ?? (top.endsWith('?') ? childrenOf.get(top.slice(0, -1)) : undefined); + + const body = []; + const emitted = new Set(); + + for (const top of topLevelOrder) { + const topLine = dataLines.find(l => firstName(l) === top); + if (topLine) { + const r = firstName(topLine); + body.push(replaceFirstName(topLine, nameForDisplay(r))); + emitted.add(topLine); + } + const kids = rowsForParent(top); + if (kids) { + for (const line of kids) { + body.push(replaceFirstName(line, nameForDisplay(firstName(line)))); + emitted.add(line); + } + } + } + + for (const line of dataLines) { + if (!emitted.has(line)) { + const r = firstName(line); + body.push(r ? replaceFirstName(line, nameForDisplay(r)) : line); + } + } + + return `${leadingNewlines}${[header, sep, ...body].join('\n')}\n`; +} + +/** + * @param {import('typedoc').Application} app + * @param {import('typedoc').ProjectReflection} project + * @param {string} pageUrl e.g. `shared/clerk/index.mdx` + * @param {import('typedoc').DeclarationReflection} interfaceDecl + */ +function createThemeContextForReferencePage(app, project, pageUrl, interfaceDecl) { + const page = new MarkdownPageEvent(interfaceDecl); + page.url = pageUrl; + page.filename = path.join(app.options.getValue('out') ?? '', pageUrl); + page.pageKind = PageKind.Reflection; + page.project = project; + const theme = /** @type {InstanceType | undefined} */ (app.renderer.theme); + if (!theme || typeof theme.getRenderContext !== 'function') { + throw new Error('[extract-methods] Renderer theme is not ready; call prepareMarkdownRenderer(app) after convert'); + } + return /** @type {import('typedoc-plugin-markdown').MarkdownThemeContext} */ (theme.getRenderContext(page)); +} + +/** + * TypeDoc `code` display parts often already include backticks (same as {@link Comment.combineDisplayParts}). + * Wrapping again would produce `` `Client` `` in MDX. + * + * @param {string} text + */ +function codeDisplayPartToMarkdown(text) { + const trimmed = text.trim(); + if (trimmed.length >= 2 && trimmed.startsWith('`') && trimmed.endsWith('`')) { + return trimmed; + } + return `\`${text}\``; +} + +/** + * @param {import('typedoc').CommentDisplayPart[] | undefined} parts + */ +function displayPartsToString(parts) { + if (!parts?.length) { + return ''; + } + return parts + .map(p => { + if (p.kind === 'text') { + return p.text; + } + if (p.kind === 'code') { + return codeDisplayPartToMarkdown(p.text); + } + if (p.kind === 'inline-tag') { + return p.text; + } + if (p.kind === 'relative-link') { + return p.text; + } + return ''; + }) + .join(''); +} + +/** + * @param {import('typedoc').ProjectReflection} project + * @param {string} name + * @param {string} [sourcePathHint] e.g. `types/clerk` + */ +function findInterfaceOrClass(project, name, sourcePathHint) { + /** @type {import('typedoc').DeclarationReflection[]} */ + const candidates = []; + for (const r of Object.values(project.reflections)) { + if (r.name !== name) { + continue; + } + if (!r.kindOf(ReflectionKind.Interface) && !r.kindOf(ReflectionKind.Class)) { + continue; + } + candidates.push(/** @type {import('typedoc').DeclarationReflection} */ (r)); + } + if (candidates.length === 0) { + return undefined; + } + if (candidates.length === 1) { + return candidates[0]; + } + if (sourcePathHint) { + const hit = candidates.find(c => c.sources?.some(s => s.fileName.replace(/\\/g, '/').includes(sourcePathHint))); + if (hit) { + return hit; + } + } + return candidates[0]; +} + +/** + * @param {import('typedoc').DeclarationReflection} decl + * @returns {import('typedoc').SignatureReflection | undefined} + */ +function getPrimaryCallSignature(decl) { + if (decl.signatures?.length) { + return decl.signatures[0]; + } + const t = decl.type; + if (t && 'declaration' in t && t.declaration?.signatures?.length) { + return t.declaration.signatures[0]; + } + /** + * e.g. `navigate: CustomNavigation` — for `type Fn = () => void`, signatures often live on the inner `declaration` + * of `alias.type` (ReflectionType), not on `alias.signatures` (see `custom-theme.mjs` `isCallablePropertyValueType`). + */ + if (t && typeof t === 'object' && 'type' in t && /** @type {{ type?: string }} */ (t).type === 'reference') { + const ref = /** @type {import('typedoc').ReferenceType} */ (t); + const target = ref.reflection; + const sigs = + target && 'signatures' in target + ? /** @type {{ signatures?: import('typedoc').SignatureReflection[] }} */ (target).signatures + : undefined; + if (sigs?.length) { + return sigs[0]; + } + const aliasTarget = /** @type {import('typedoc').DeclarationReflection | undefined} */ ( + target && 'kind' in target ? target : undefined + ); + if (aliasTarget?.kind === ReflectionKind.TypeAlias && aliasTarget.type && 'declaration' in aliasTarget.type) { + const inner = /** @type {import('typedoc').ReflectionType} */ (aliasTarget.type).declaration; + if (inner?.signatures?.length) { + return inner.signatures[0]; + } + } + } + return undefined; +} + +/** + * Must stay aligned with allowlisted `propertiesTable` filtering in `custom-theme.mjs` (callable members are + * extracted here, not listed as properties). + * + * @param {import('typedoc').DeclarationReflection} decl + * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx + */ +function shouldExtractCallableMember(decl, ctx) { + if (decl.kind === ReflectionKind.Method) { + return true; + } + if ( + decl.kind === ReflectionKind.Property || + decl.kind === ReflectionKind.Accessor || + decl.kind === ReflectionKind.Variable + ) { + return isCallableInterfaceProperty(decl, ctx.helpers); + } + return false; +} + +/** + * @param {string} markdown + * @returns {string | undefined} Body under `## Properties` (no heading), or undefined + */ +function extractPropertiesSectionBody(markdown) { + const normalized = markdown.replace(/\r\n/g, '\n'); + const m = normalized.match(/(^|\n)## Properties\n+/); + if (!m || m.index === undefined) { + return undefined; + } + const start = m.index + m[0].length; + const rest = normalized.slice(start); + const nextH2 = rest.search(/\n## /); + const section = nextH2 === -1 ? rest : rest.slice(0, nextH2); + const trimmed = section.trim(); + return trimmed.length ? trimmed : undefined; +} + +/** + * @param {string} pageUrl e.g. `shared/clerk/clerk.mdx` + */ +function extractPropertiesAndTrimSourcePage(pageUrl) { + const sourcePath = path.join(__dirname, 'temp-docs', pageUrl); + if (!fs.existsSync(sourcePath)) { + console.warn(`[extract-methods] Expected TypeDoc output missing: ${sourcePath}`); + return; + } + const raw = fs.readFileSync(sourcePath, 'utf-8'); + const body = extractPropertiesSectionBody(raw); + const pageDir = path.dirname(pageUrl); + const slug = path.basename(pageUrl, '.mdx'); + const objectDir = path.join(__dirname, 'temp-docs', pageDir); + fs.mkdirSync(objectDir, { recursive: true }); + + if (body) { + const propertiesDoc = [`## Properties`, '', body.trimEnd(), ''].join('\n'); + const propertiesPath = path.join(objectDir, `${slug}-properties.mdx`); + fs.writeFileSync( + propertiesPath, + applyCatchAllMdReplacements(applyRelativeLinkReplacements(propertiesDoc)), + 'utf-8', + ); + console.log(`[extract-methods] Wrote ${path.relative(path.join(__dirname, '..'), propertiesPath)}`); + } + + const stripped = stripReferenceObjectPropertiesSection(raw); + if (stripped !== raw) { + fs.writeFileSync(sourcePath, stripped, 'utf-8'); + console.log(`[extract-methods] Stripped Properties from ${path.relative(path.join(__dirname, '..'), sourcePath)}`); + } +} + +/** + * @param {string} name + */ +function toKebabCase(name) { + return name + .replace(/([a-z\d])([A-Z])/g, '$1-$2') + .replace(/[\s_]+/g, '-') + .toLowerCase(); +} + +/** + * @param {import('typedoc').SignatureReflection} sig + * @param {string} memberName + */ +function formatTypeScriptSignature(sig, memberName) { + const typeParams = sig.typeParameters?.map(tp => tp.name).join(', ') ?? ''; + const typeParamStr = typeParams ? `<${typeParams}>` : ''; + const params = + sig.parameters?.map(p => { + const opt = p.flags.isOptional ? '?' : ''; + const rest = p.flags.isRest ? '...' : ''; + const typeStr = p.type ? p.type.toString() : 'unknown'; + return `${rest}${p.name}${opt}: ${typeStr}`; + }) ?? []; + const ret = sig.type ? sig.type.toString() : 'void'; + return `function ${memberName}${typeParamStr}(${params.join(', ')}): ${ret}`; +} + +/** + * `@returns - foo` is often stored with a leading dash, which renders as a bullet. Normalize to prose for + * "Returns …" lines. + * @param {string} body + */ +function normalizeReturnsBody(body) { + return body.replace(/^\s*[-*]\s+/, '').trim(); +} + +/** + * Lowercase the first character so the line reads "Returns an …" not "Returns An …". + * @param {string} body + */ +function lowercaseFirstCharacter(body) { + if (!body) { + return body; + } + return body.charAt(0).toLowerCase() + body.slice(1); +} + +/** + * @param {import('typedoc').CommentTag} tag + */ +function formatReturnsLineFromTag(tag) { + const raw = Comment.combineDisplayParts(tag.content).trim(); + if (!raw) { + return ''; + } + const body = lowercaseFirstCharacter(normalizeReturnsBody(raw)); + return `Returns ${body}`; +} + +/** + * @param {import('typedoc').Comment | undefined} comment + */ +/** + * `typedoc-plugin-markdown` table partials include `@example` in Description cells. For extract-methods, we want to exclude examples from the generated output. + * + * Uses the same `getFlattenedDeclarations` list as `propertiesTable` so nested property rows omit examples too. + * + * @template T + * @param {import('typedoc').Reflection[]} roots + * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx + * @param {() => T} render + * @returns {T} + */ +function renderMemberTableOmittingExampleBlocks(roots, ctx, render) { + const flatten = + typeof ctx.helpers?.getFlattenedDeclarations === 'function' + ? ctx.helpers.getFlattenedDeclarations( + /** @type {import('typedoc').DeclarationReflection[]} */ (/** @type {unknown} */ (roots)), + ) + : roots; + /** @type {Set} */ + const processedComments = new Set(); + /** @type {{ ref: import('typedoc').Reflection; orig: import('typedoc').Comment }[]} */ + const restore = []; + for (const r of flatten) { + const c = 'comment' in r ? r.comment : undefined; + if (!c?.getTag('@example') || processedComments.has(c)) { + continue; + } + processedComments.add(c); + const next = c.clone(); + next.removeTags('@example'); + for (const ref of flatten) { + if (ref.comment === c) { + ref.comment = next; + restore.push({ ref, orig: c }); + } + } + } + try { + return render(); + } finally { + for (const { ref, orig } of restore) { + ref.comment = orig; + } + } +} + +/** + * @param {import('typedoc').Comment | undefined} comment + */ +function commentSummaryAndBody(comment) { + if (!comment) { + return ''; + } + const summary = displayPartsToString(comment.summary).trim(); + const block = comment.blockTags + ?.filter(t => !['@param', '@typeParam', '@returns'].includes(t.tag)) + .map(t => displayPartsToString(t.content).trim()) + .filter(Boolean) + .join('\n\n'); + const returnsLines = + comment.blockTags + ?.filter(t => t.tag === '@returns') + .map(t => formatReturnsLineFromTag(t)) + .filter(Boolean) ?? []; + return [summary, block, ...returnsLines].filter(Boolean).join('\n\n'); +} + +/** + * When `@returns` exists only on the call signature (not on the declaration), append it to the prose. + * @param {import('typedoc').Comment | undefined} declComment + * @param {import('typedoc').Comment | undefined} sigComment + */ +function appendSignatureOnlyReturns(declComment, sigComment) { + if (declComment?.getTag('@returns')?.content?.length) { + return ''; + } + const tag = sigComment?.getTag('@returns'); + if (!tag?.content?.length) { + return ''; + } + return formatReturnsLineFromTag(tag); +} + +/** + * Object / type-literal declaration for a parameter type (reference, inlined reflection, intersection). + * TypeDoc applies `@param parent.prop` descriptions onto property reflections under this declaration. + * + * @param {import('typedoc').SomeType | undefined} t + * @returns {import('typedoc').DeclarationReflection | undefined} + */ +function resolveDeclarationWithObjectMembers(t) { + if (!t) { + return undefined; + } + if (t.type === 'reflection') { + const d = t.declaration; + if (d.children?.length) { + return d; + } + } + if (t.type === 'reference') { + const ref = /** @type {import('typedoc').ReferenceType} */ (t); + const r = ref.reflection; + if (r && 'type' in r) { + const decl = /** @type {import('typedoc').DeclarationReflection} */ (r); + if (decl.children?.length) { + return decl; + } + if (decl.type) { + return resolveDeclarationWithObjectMembers(decl.type); + } + } + } + if (t.type === 'intersection') { + for (const inner of /** @type {import('typedoc').IntersectionType} */ (t).types) { + const res = resolveDeclarationWithObjectMembers(inner); + if (res) { + return res; + } + } + } + if (t.type === 'optional') { + return resolveDeclarationWithObjectMembers(/** @type {import('typedoc').OptionalType} */ (t).elementType); + } + return undefined; +} + +/** + * @param {string} baseName + * @param {string[]} pathSegments + */ +function formatNestedParamNameColumn(baseName, pathSegments) { + const pathChain = pathSegments.join('?.'); + return `\`${baseName}?.${pathChain}\``; +} + +/** + * Rows for object properties that have documentation (including from `@param parent.prop` on the method), + * which TypeDoc stores on property reflections rather than leaving `@param` block tags on the signature. + * + * @param {import('typedoc').ParameterReflection} param + * @returns {string[]} + */ +/** + * @param {import('typedoc').ParameterReflection} param + * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx + */ +function nestedParameterRowsFromDocumentedProperties(param, ctx) { + // `parametersTable` already flattens inline `{ ... }` params (see typedoc-plugin-markdown `parseParams`). + // Adding rows here would duplicate those (e.g. `options.skipInitialEmit` twice on `addListener`). + if (param.type?.type === 'reflection') { + const d = /** @type {import('typedoc').DeclarationReflection} */ (param.type.declaration); + if (d?.kind === ReflectionKind.TypeLiteral && d.children?.length) { + return []; + } + } + + const holder = resolveDeclarationWithObjectMembers(param.type); + if (!holder?.children?.length) { + return []; + } + const props = holder.children.filter(c => c.kindOf(ReflectionKind.Property) && c.comment?.summary?.length); + props.sort((a, b) => a.name.localeCompare(b.name)); + /** @type {string[]} */ + const rows = []; + for (const child of props) { + const summary = child.comment?.summary; + if (!summary?.length) { + continue; + } + const typeCell = child.type ? removeLineBreaksForTableCell(ctx.partials.someType(child.type)) : '`unknown`'; + const nestedNameCol = formatNestedParamNameColumn(param.name, [child.name]); + const nestedDesc = displayPartsToString(summary).trim() || '—'; + rows.push(`| ${nestedNameCol} | ${typeCell} | ${nestedDesc} |`); + } + return rows; +} + +/** + * Merged / external references sometimes leave {@link ReferenceType.reflection} unset; resolve by name. + * + * @param {import('typedoc').ProjectReflection} project + * @param {string} name + * @returns {import('typedoc').DeclarationReflection | undefined} + */ +function lookupInterfaceOrTypeAliasByName(project, name) { + /** @type {import('typedoc').DeclarationReflection[]} */ + const cands = []; + for (const r of Object.values(project.reflections)) { + if (r.name !== name) { + continue; + } + if (!r.kindOf(ReflectionKind.Interface) && !r.kindOf(ReflectionKind.TypeAlias)) { + continue; + } + cands.push(/** @type {import('typedoc').DeclarationReflection} */ (r)); + } + if (cands.length === 0) { + return undefined; + } + if (cands.length === 1) { + return cands[0]; + } + const withChildren = cands.find(c => c.children?.length); + return withChildren ?? cands[0]; +} + +/** + * Unwrap optional wrappers. When the parameter is a single named interface or type alias for an object + * shape, returns that name and the declaration holding object properties. + * + * @param {import('typedoc').SomeType | undefined} t + * @param {import('typedoc').ProjectReflection} project + * @returns {{ sectionTitle: string, holder: import('typedoc').DeclarationReflection, typeDecl: import('typedoc').DeclarationReflection } | undefined} + */ +function resolveNominalObjectTypeForSingleParam(t, project) { + if (!t) { + return undefined; + } + if (t.type === 'optional') { + return resolveNominalObjectTypeForSingleParam( + /** @type {import('typedoc').OptionalType} */ (t).elementType, + project, + ); + } + if (t.type === 'reference') { + const ref = /** @type {import('typedoc').ReferenceType} */ (t); + let typeDecl = + ref.reflection && 'kind' in ref.reflection + ? /** @type {import('typedoc').DeclarationReflection} */ (ref.reflection) + : lookupInterfaceOrTypeAliasByName(project, ref.name); + if (!typeDecl) { + return undefined; + } + if (typeDecl.kindOf(ReflectionKind.Interface)) { + if (!typeDecl.children?.length) { + return undefined; + } + return { sectionTitle: typeDecl.name, holder: typeDecl, typeDecl }; + } + if (typeDecl.kindOf(ReflectionKind.TypeAlias)) { + // Same as `resolveDeclarationWithObjectMembers` for a reference: members may live on the alias + // (`typeDecl.children`) with no `typeDecl.type` (e.g. `SignOutOptions`, `JoinWaitlistParams`). + const holder = typeDecl.children?.length + ? typeDecl + : typeDecl.type + ? resolveDeclarationWithObjectMembers(typeDecl.type) + : undefined; + if (!holder?.children?.length) { + return undefined; + } + return { sectionTitle: typeDecl.name, holder, typeDecl }; + } + } + return undefined; +} + +/** + * @param {import('typedoc').DeclarationReflection} typeDecl + * @param {import('typedoc').DeclarationReflection[]} props + */ +function isNominalParamTypeDocumented(typeDecl, props) { + if (typeDecl.comment?.summary?.length) { + return true; + } + return props.some(p => p.comment?.summary?.length); +} + +/** + * Single parameter that is a named object type (interface / type alias): one section titled after the type, + * table lists every property (not the outer `params` row). Uses the same `propertiesTable` partial as TypeDoc. + * + * @param {import('typedoc').SignatureReflection} sig + * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx + * @returns {string | undefined} + */ +function trySingleNominalParameterTypeSection(sig, ctx) { + const params = sig.parameters ?? []; + if (params.length !== 1) { + return undefined; + } + const p = params[0]; + const nominal = resolveNominalObjectTypeForSingleParam(p.type, sig.project); + if (!nominal) { + return undefined; + } + const props = (nominal.holder.children ?? []).filter(c => c.kindOf(ReflectionKind.Property)); + if (props.length === 0) { + return undefined; + } + if (!isNominalParamTypeDocumented(nominal.typeDecl, props)) { + return undefined; + } + const tableMd = renderMemberTableOmittingExampleBlocks(props, ctx, () => + ctx.partials.propertiesTable(props, { + kind: nominal.typeDecl.kind, + isEventProps: false, + }), + ); + if (!tableMd?.trim()) { + return undefined; + } + return [markdownHeading(4, nominal.sectionTitle), '', tableMd, ''].join('\n'); +} + +/** + * @param {import('typedoc').SignatureReflection} sig + * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx + */ +function parametersMarkdownTable(sig, ctx) { + const params = sig.parameters ?? []; + if (params.length === 0) { + return ''; + } + + const singleNominal = trySingleNominalParameterTypeSection(sig, ctx); + if (singleNominal) { + return singleNominal; + } + + let tableMd = renderMemberTableOmittingExampleBlocks(params, ctx, () => ctx.partials.parametersTable(params)); + /** @type {string[]} */ + const nested = []; + for (const p of params) { + nested.push(...nestedParameterRowsFromDocumentedProperties(p, ctx)); + } + if (nested.length) { + tableMd = appendMarkdownTableRows(tableMd, nested); + } + + tableMd = formatMethodParametersTable(tableMd); + + return [markdownHeading(4, ReflectionKind.pluralString(ReflectionKind.Parameter)), '', tableMd, ''].join('\n'); +} + +/** + * @param {import('typedoc').DeclarationReflection} decl + * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx + */ +function buildMethodMdx(decl, ctx) { + const name = decl.name; + const sig = getPrimaryCallSignature(decl); + if (!sig) { + return ''; + } + const title = `### \`${name}()\``; + /** Prefer the declaration comment (property-style methods document `addListener` on the property, not the signature). */ + const comment = decl.comment ?? sig.comment; + let description = commentSummaryAndBody(comment); + const sigReturns = appendSignatureOnlyReturns(decl.comment, sig.comment); + if (sigReturns) { + description = [description, sigReturns].filter(Boolean).join('\n\n'); + } + const ts = ['```typescript', formatTypeScriptSignature(sig, name), '```'].join('\n'); + const paramsMd = parametersMarkdownTable(sig, ctx); + + // Same post-process as `custom-plugin.mjs` `MarkdownPageEvent.END`: relative `.mdx` links, then catch-alls. + // Skip the ```typescript``` fence so signatures stay plain code. + const head = applyCatchAllMdReplacements(applyRelativeLinkReplacements([title, '', description].join('\n'))); + const paramsProcessed = paramsMd ? applyCatchAllMdReplacements(applyRelativeLinkReplacements(paramsMd)) : ''; + const chunks = [head, ts]; + if (paramsProcessed) { + chunks.push(paramsProcessed); + } + return chunks.join('\n\n').trim() + '\n'; +} + +/** + * @param {string} pageUrl + * @param {import('typedoc').ProjectReflection} project + * @param {import('typedoc').Application} app + */ +function extractMethodsForPage(pageUrl, project, app) { + const symbol = /** @type {Record} */ (/** @type {unknown} */ (REFERENCE_OBJECT_PAGE_SYMBOLS))[ + pageUrl + ]; + if (!symbol) { + console.warn(`[extract-methods] No symbol mapping for ${pageUrl}, skipping`); + return 0; + } + + const hint = symbol === 'Clerk' ? 'types/clerk' : symbol === 'ClientResource' ? 'types/client' : undefined; + const decl = findInterfaceOrClass(project, symbol, hint); + if (!decl?.children) { + console.warn(`[extract-methods] Could not find interface/class "${symbol}"`); + return 0; + } + + extractPropertiesAndTrimSourcePage(pageUrl); + + const ctx = createThemeContextForReferencePage(app, project, pageUrl, decl); + + const pageDir = path.dirname(pageUrl); + const slug = path.basename(pageUrl, '.mdx'); + const objectDir = path.join(__dirname, 'temp-docs', pageDir); + const outDir = path.join(objectDir, `${slug}-methods`); + fs.mkdirSync(outDir, { recursive: true }); + + let count = 0; + for (const child of decl.children) { + if (child.name.startsWith('__')) { + continue; + } + if (!shouldExtractCallableMember(/** @type {import('typedoc').DeclarationReflection} */ (child), ctx)) { + continue; + } + const mdx = buildMethodMdx(/** @type {import('typedoc').DeclarationReflection} */ (child), ctx); + if (!mdx) { + continue; + } + const fileName = `${toKebabCase(child.name)}.mdx`; + const filePath = path.join(outDir, fileName); + fs.writeFileSync(filePath, mdx, 'utf-8'); + console.log(`[extract-methods] Wrote ${path.relative(path.join(__dirname, '..'), filePath)}`); + count++; + } + return count; +} + +async function main() { + const app = await Application.bootstrapWithPlugins({ + ...typedocConfig, + // Avoid writing markdown twice; we only need reflections. + out: path.join(__dirname, 'temp-docs-unused'), + }); + + const project = await app.convert(); + if (!project) { + console.error('[extract-methods] TypeDoc conversion failed'); + process.exit(1); + } + + prepareMarkdownRenderer(app, project); + + let total = 0; + for (const pageUrl of REFERENCE_OBJECTS_LIST) { + total += extractMethodsForPage(pageUrl, project, app); + } + console.log(`[extract-methods] Wrote ${total} method files total`); +} + +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/.typedoc/prepare-markdown-renderer.mjs b/.typedoc/prepare-markdown-renderer.mjs new file mode 100644 index 00000000000..bbf373c63f7 --- /dev/null +++ b/.typedoc/prepare-markdown-renderer.mjs @@ -0,0 +1,118 @@ +// @ts-check +/** + * Mirrors `prepareRouter` + `prepareTheme` from `typedoc-plugin-markdown` `render()` so code outside the + * markdown render pass can build a `MarkdownThemeContext` (same `partials` as generated pages). + * + * Only `member`, `module`, and plugin-registered routers (e.g. `clerk-router`) are supported — matching this repo's + * TypeDoc config. + * + * @see https://github.com/typedoc2md/typedoc-plugin-markdown/blob/main/packages/typedoc-plugin-markdown/src/renderer/render.ts + */ +import { MarkdownTheme, MemberRouter, ModuleRouter } from 'typedoc-plugin-markdown'; + +/** + * @param {import('typedoc').Renderer} renderer + * @returns {string} + */ +function getRouterName(renderer) { + const routerOption = renderer.application.options.getValue('router'); + if (!renderer.application.options.isSet('router')) { + if (renderer.application.options.isSet('outputFileStrategy')) { + const outputFileStrategy = renderer.application.options.getValue('outputFileStrategy'); + return outputFileStrategy === 'modules' ? 'module' : 'member'; + } + return 'member'; + } + return routerOption; +} + +/** + * TypeDoc types `Renderer['routers']` as private; at runtime plugins register routers on this map (e.g. `clerk-router`). + * + * @param {import('typedoc').Renderer} renderer + * @param {string} routerName + * @returns {typeof MemberRouter | typeof ModuleRouter | (new (application: import('typedoc').Application) => import('typedoc').Router) | undefined} + */ +function getRouterConstructor(renderer, routerName) { + if (routerName === 'member') { + return MemberRouter; + } + if (routerName === 'module') { + return ModuleRouter; + } + const routers = + /** @type {{ routers: Map import('typedoc').Router> }} */ ( + /** @type {unknown} */ (renderer) + ).routers; + return routers.get(routerName); +} + +/** + * Same situation as {@link getRouterConstructor}: `themes` is public at runtime but typed private. + * + * @param {import('typedoc').Renderer} renderer + * @returns {Map import('typedoc').Theme>} + */ +function getThemeRegistry(renderer) { + return /** @type {{ themes: Map import('typedoc').Theme> }} */ ( + /** @type {unknown} */ (renderer) + ).themes; +} + +/** + * @param {import('typedoc').Renderer} renderer + */ +function prepareRouter(renderer) { + const routerName = getRouterName(renderer); + const RouterCtor = getRouterConstructor(renderer, routerName); + if (!RouterCtor) { + throw new Error( + `[prepare-markdown-renderer] Router "${routerName}" is not registered (expected member, module, or a custom router from a plugin)`, + ); + } + renderer.router = new RouterCtor(renderer.application); +} + +/** + * @param {import('typedoc').Renderer} renderer + */ +function getThemeName(renderer) { + const themeOption = renderer.application.options.getValue('theme'); + return themeOption === 'default' ? 'markdown' : themeOption; +} + +/** + * @param {import('typedoc').Renderer} renderer + */ +function prepareTheme(renderer) { + const themes = getThemeRegistry(renderer); + const themeName = getThemeName(renderer); + const ThemeCtor = themes.get(themeName); + if (!ThemeCtor) { + throw new Error(`[prepare-markdown-renderer] Theme "${themeName}" is not registered`); + } + const theme = new ThemeCtor(renderer); + if (!(theme instanceof MarkdownTheme)) { + renderer.application.logger.warn( + `[prepare-markdown-renderer] Theme "${themeName}" is not MarkdownTheme; falling back to built-in markdown theme`, + ); + renderer.theme = new /** @type {typeof MarkdownTheme} */ (themes.get('markdown'))(renderer); + return; + } + renderer.theme = theme; +} + +/** + * @param {import('typedoc').Application} app + * @param {import('typedoc').ProjectReflection} project + */ +export function prepareMarkdownRenderer(app, project) { + prepareRouter(app.renderer); + prepareTheme(app.renderer); + // Required so `referenceType` / links can resolve (`getFullUrl`); same as `render()` before each page. + const router = app.renderer.router; + if (!router) { + throw new Error('[prepare-markdown-renderer] Router was not set after prepareRouter'); + } + router.buildPages(project); +} diff --git a/.typedoc/reference-objects.mjs b/.typedoc/reference-objects.mjs new file mode 100644 index 00000000000..aa4e76bbdf4 --- /dev/null +++ b/.typedoc/reference-objects.mjs @@ -0,0 +1,19 @@ +// @ts-check +/** + * Shared between the markdown theme and extract-methods.mjs. + * `page.url` values are relative to TypeDoc `out` (e.g. `.typedoc/temp-docs`). + */ + +/** + * TypeDoc output paths for the main reference pages (`shared//.mdx`, see `ClerkRouter`). + * `extract-methods.mjs` reads each file, writes `-properties.mdx` with the same Properties table as TypeDoc, strips Properties from `.mdx`, and writes methods under `-methods/`. + */ +export const REFERENCE_OBJECTS_LIST = ['shared/clerk/clerk.mdx', 'shared/client-resource/client-resource.mdx']; + +/** + * Primary interface/class documented on each reference object page (used to resolve TypeDoc reflections). + */ +export const REFERENCE_OBJECT_PAGE_SYMBOLS = { + 'shared/clerk/clerk.mdx': 'Clerk', + 'shared/client-resource/client-resource.mdx': 'ClientResource', +}; diff --git a/package.json b/package.json index 69259d945cb..01bca99d149 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "test:typedoc": "pnpm typedoc:generate && cd ./.typedoc && vitest run", "turbo:clean": "turbo daemon clean", "typedoc:generate": "pnpm build && pnpm typedoc:generate:skip-build", - "typedoc:generate:skip-build": "typedoc --tsconfig tsconfig.typedoc.json && node .typedoc/extract-returns-and-params.mjs && rimraf .typedoc/docs && cpy '.typedoc/temp-docs/**' '.typedoc/docs' && rimraf .typedoc/temp-docs", + "typedoc:generate:skip-build": "typedoc --tsconfig tsconfig.typedoc.json && node .typedoc/extract-returns-and-params.mjs && node .typedoc/extract-methods.mjs && rimraf .typedoc/docs && cpy '.typedoc/temp-docs/**' '.typedoc/docs' && rimraf .typedoc/temp-docs", "version-packages": "changeset version && pnpm install --lockfile-only --engine-strict=false", "version-packages:canary": "./scripts/canary.mjs", "version-packages:canary-core3": "./scripts/canary-core3.mjs", diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index e199021fa03..6ab532be38c 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -423,6 +423,14 @@ export class Clerk implements ClerkInterface { return !!this.session; } + /** + * Create an instance of the `Clerk` class with dedicated options. + * + * This method is only available when importing `Clerk` from `@clerk/clerk-js`, rather than using a Window script. + * + * @param key - Your Clerk [Publishable Key](!publishable-key). + * @param options - The satellite domain or reverse proxy URL used to connect to Clerk. + */ public constructor(key: string, options?: DomainOrProxyUrl) { key = (key || '').trim(); diff --git a/packages/shared/src/types/apiKeys.ts b/packages/shared/src/types/apiKeys.ts index 09679504ecf..0cd1f6e8bc0 100644 --- a/packages/shared/src/types/apiKeys.ts +++ b/packages/shared/src/types/apiKeys.ts @@ -3,21 +3,69 @@ import type { ClerkPaginatedResponse } from './pagination'; import type { ClerkResource } from './resource'; export interface APIKeyResource extends ClerkResource { + /** + * A unique identifier for the API key. + */ id: string; + /** + * The type of the API key. + */ type: string; + /** + * The name of the API key. + */ name: string; + /** + * The user or organization ID that the API key is associated with. + */ subject: string; + /** + * An array of scopes that define what the API key can access. + */ scopes: string[]; + /** + * Custom claims associated with the API key, or `null` if none. + */ claims: Record | null; + /** + * Indicates whether the API key has been revoked. + */ revoked: boolean; + /** + * The reason the API key was revoked, or `null` if not revoked. + */ revocationReason: string | null; + /** + * Indicates whether the API key has expired. + */ expired: boolean; + /** + * The expiration date and time for the API key, or `null` if the key never expires. + */ expiration: Date | null; + /** + * The ID of the user that created the API key. + */ createdBy: string | null; + /** + * An optional description for the API key. + */ description: string | null; + /** + * The API key secret. **This property is only present in the response from [`create()`](https://clerk.com/docs/reference/objects/api-keys#create) and cannot be retrieved later.** + */ secret?: string; + /** + * The date and time when the API key was last used to authenticate a request, or `null` if it has never been used. + */ lastUsedAt: Date | null; + /** + * The date and time when the API key was created. + */ createdAt: Date; + /** + * The date and time when the API key was last updated. + */ updatedAt: Date; } diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts index 1a6921a6717..35a1c0b51e2 100644 --- a/packages/shared/src/types/clerk.ts +++ b/packages/shared/src/types/clerk.ts @@ -134,7 +134,16 @@ export type SDKMetadata = { environment?: string; }; +/** + * A callback function that is called when Clerk resources change. + * @inline + */ export type ListenerCallback = (emission: Resources) => void; +/** + * Optional configuration for the `addListener()` method. + * @param skipInitialEmit - If `true`, the callback will not be called immediately after registration. Defaults to `false`. + * @inline + */ export type ListenerOptions = { skipInitialEmit?: boolean }; export type UnsubscribeCallback = () => void; @@ -173,16 +182,21 @@ export type SetActiveNavigate = (params: { decorateUrl: DecorateUrl; }) => void | Promise; +/** + * A callback that runs after sign out completes. + * @inline */ export type SignOutCallback = () => void | Promise; +/** + * Configuration options. + */ export type SignOutOptions = { /** - * Specify a specific session to sign out. Useful for - * multi-session applications. + * Specify a specific session to sign out. Useful for multi-session applications. */ sessionId?: string; /** - * Specify a redirect URL to navigate to after sign out is complete. + * Specify a redirect URL to navigate to after sign-out is complete. */ redirectUrl?: string; }; @@ -214,27 +228,27 @@ export type ClerkStatus = 'degraded' | 'error' | 'loading' | 'ready'; */ export interface Clerk { /** - * Clerk SDK version number. + * The Clerk SDK version number. */ version: string | undefined; /** * If present, contains information about the SDK that the host application is using. - * For example, if Clerk is loaded through `@clerk/nextjs`, this would be `{ name: '@clerk/nextjs', version: '1.0.0' }` + * For example, if Clerk is loaded through `@clerk/nextjs`, this would be `{ name: '@clerk/nextjs', version: '1.0.0' }`. You don't need to set this value yourself unless you're [developing an SDK](https://clerk.com/docs/guides/development/sdk-development/overview). */ sdkMetadata: SDKMetadata | undefined; /** - * If true the bootstrapping of Clerk.load() has completed successfully. + * Indicates if the `Clerk` object is ready for use. Set to `false` when the `status` is `"loading"`. Set to `true` when the `status` is `"ready"` or `"degraded"`. */ loaded: boolean; /** - * Describes the state the clerk singleton operates in: - * - `"error"`: Clerk failed to initialize. - * - `"loading"`: Clerk is still attempting to load. - * - `"ready"`: Clerk singleton is fully operational. - * - `"degraded"`: Clerk singleton is partially operational. + * The status of the `Clerk` instance. Possible values are: + * - `"error"`: Set when hotloading `clerk-js` or `Clerk.load()` failed. + * - `"loading"`: Set during initialization. + * - `"ready"`: Set when Clerk is fully operational. + * - `"degraded"`: Set when Clerk is partially operational. */ status: ClerkStatus; @@ -245,39 +259,42 @@ export interface Clerk { frontendApi: string; - /** Clerk Publishable Key string. */ + /** Your Clerk [Publishable Key](!publishable-key). */ publishableKey: string; - /** Clerk Proxy url string. */ + /** Your Clerk app's proxy URL. Required for applications that run behind a reverse proxy. Can be either a relative path (`/__clerk`) or a full URL (`https:///__clerk`). */ proxyUrl: string | undefined; - /** Clerk Satellite Frontend API string. */ + /** The current Clerk app's domain. Prefixed with `clerk.` on production if not already prefixed. Returns `""` when ran on the server. */ domain: string; - /** Clerk Flag for satellite apps. */ + /** Indicates if the instance is a satellite app. */ isSatellite: boolean; - /** Clerk Instance type is defined from the Publishable key */ + /** Indicates if the Clerk instance is running in a production or development environment. */ instanceType: InstanceType | undefined; - /** Clerk flag for loading Clerk in a standard browser setup */ + /** + * Indicates if the instance is being loaded in a standard browser environment. Set to `false` on native platforms where cookies cannot be set. When `undefined`, Clerk assumes a standard browser. + * @inline + */ isStandardBrowser: boolean | undefined; /** - * Indicates whether the current user has a valid signed-in client session + * Indicates whether the current user has a valid signed-in client session. */ isSignedIn: boolean; - /** Client handling most Clerk operations. */ + /** The `Client` object for the current window. */ client: ClientResource | undefined; - /** Current Session. */ + /** The currently active `Session`, which is guaranteed to be one of the sessions in `Client.sessions`. If there is no active session, this field will be `null`. If the session is loading, this field will be `undefined`. */ session: SignedInSessionResource | null | undefined; - /** Active Organization */ + /** A shortcut to the last active `Session.user.organizationMemberships` which holds an instance of a `Organization` object. If the session is `null` or `undefined`, the user field will match. */ organization: OrganizationResource | null | undefined; - /** Current User. */ + /** A shortcut to `Session.user` which holds the currently active `User` object. If the session is `null` or `undefined`, the user field will match. */ user: UserResource | null | undefined; /** @@ -291,25 +308,30 @@ export interface Clerk { * Entrypoint for Clerk's Signal API containing resource signals along with accessible versions of `computed()` and * `effect()` that can be used to subscribe to changes from Signals. * + * @internal * @experimental This experimental API is subject to change. */ __internal_state: State; /** + * The `Billing` object used for managing billing. + * * @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change. It is advised to [pin](https://clerk.com/docs/pinning) the SDK version and the clerk-js version to avoid breaking changes. */ billing: BillingNamespace; + /** + * [Telemetry](https://clerk.com/docs/guides/how-clerk-works/security/clerk-telemetry) configuration. + */ telemetry: TelemetryCollector | undefined; + /** + * @internal + */ __internal_country?: string | null; /** - * Signs out the current user on single-session instances, or all users on multi-session instances - * - * @param signOutCallback - Optional A callback that runs after sign out completes. - * @param options - Optional Configuration options, see {@link SignOutOptions} - * @returns A promise that resolves when the sign out process completes. + * Signs out the current user on single-session instances, or all users on multi-session instances. */ signOut: SignOut; @@ -329,11 +351,13 @@ export interface Clerk { * Opens the Clerk Checkout component in a drawer. * * @param props - Optional checkout configuration parameters. + * @internal */ __internal_openCheckout: (props?: __internal_CheckoutProps) => void; /** * Closes the Clerk Checkout drawer. + * @internal */ __internal_closeCheckout: () => void; @@ -341,11 +365,13 @@ export interface Clerk { * Opens the Clerk PlanDetails drawer component in a drawer. * * @param props - `plan` or `planId` parameters are required. + * @internal */ __internal_openPlanDetails: (props: __internal_PlanDetailsProps) => void; /** * Closes the Clerk PlanDetails drawer. + * @internal */ __internal_closePlanDetails: () => void; @@ -353,11 +379,13 @@ export interface Clerk { * Opens the Clerk SubscriptionDetails drawer component in a drawer. * * @param props - Optional configuration parameters. + * @internal */ __internal_openSubscriptionDetails: (props?: __internal_SubscriptionDetailsProps) => void; /** * Closes the Clerk SubscriptionDetails drawer. + * @internal */ __internal_closeSubscriptionDetails: () => void; @@ -365,16 +393,19 @@ export interface Clerk { * Opens the Clerk UserVerification component in a modal. * * @param props - Optional user verification configuration parameters. + * @internal */ __internal_openReverification: (props?: __internal_UserVerificationModalProps) => void; /** * Closes the Clerk user verification modal. + * @internal */ __internal_closeReverification: () => void; /** * Attempts to enable a environment setting from a development instance, prompting if disabled. + * @internal */ __internal_attemptToEnableEnvironmentSetting: ( options: __internal_AttemptToEnableEnvironmentSettingParams, @@ -382,11 +413,13 @@ export interface Clerk { /** * Opens the Clerk Enable Organizations prompt for development instance + * @internal */ __internal_openEnableOrganizationsPrompt: (props: __internal_EnableOrganizationsPromptProps) => void; /** * Closes the Clerk Enable Organizations modal. + * @internal */ __internal_closeEnableOrganizationsPrompt: () => void; @@ -664,6 +697,7 @@ export interface Clerk { * * @param targetNode - Target node to mount the OAuth consent component. * @param oauthConsentProps - OAuth consent configuration parameters. + * @internal */ __internal_mountOAuthConsent: (targetNode: HTMLDivElement, oauthConsentProps?: __internal_OAuthConsentProps) => void; @@ -671,6 +705,7 @@ export interface Clerk { * Unmounts a OAuth consent component from the target element. * * @param targetNode - Target node to unmount the OAuth consent component from. + * @internal */ __internal_unmountOAuthConsent: (targetNode: HTMLDivElement) => void; @@ -730,27 +765,29 @@ export interface Clerk { __internal_loadStripeJs: () => Promise; /** - * Register a listener that triggers a callback each time important Clerk resources are changed. - * Allows to hook up at different steps in the sign up, sign in processes. + * Register a listener that triggers a callback whenever a change in the [`Client`](https://clerk.com/docs/reference/objects/client), [`Session`](https://clerk.com/docs/reference/objects/session), [`User`](https://clerk.com/docs/reference/objects/user), or [`Organization`](https://clerk.com/docs/reference/objects/organization) resources occurs. This method is primarily used to build frontend SDKs like [`@clerk/react`](https://www.npmjs.com/package/@clerk/react). + * + * Allows hooking up at different steps in the sign up, sign in processes. * * Some important checkpoints: - * When there is an active session, user === session.user. - * When there is no active session, user and session will both be null. - * When a session is loading, user and session will be undefined. + * - When there is an active session, `user === session.user`. + * - When there is no active session, user and session will both be `null`. + * - When a session is loading, user and session will be `undefined`. * - * @param callback - Callback function receiving the most updated Clerk resources after a change. - * @param options.skipInitialEmit - If true, the callback will not be called immediately after registration. - * @returns - Unsubscribe callback + * @param callback - The function to call when Clerk resources change. + * @param options - Optional configuration. + * @param options.skipInitialEmit - If `true`, the callback will not be called immediately after registration. Defaults to `false`. + * @returns - An `UnsubscribeCallback` function that can be used to remove the listener from the `Clerk` object. */ addListener: (callback: ListenerCallback, options?: ListenerOptions) => UnsubscribeCallback; /** * Registers an event handler for a specific Clerk event. * - * @param event - The event name to subscribe to - * @param handler - The callback function to execute when the event is dispatched - * @param opt - Optional configuration object - * @param opt.notify - If true and the event was previously dispatched, handler will be called immediately with the latest payload + * @param event - The event name to subscribe to. + * @param handler - The callback function to execute when the event is triggered. + * @param opt - An optional object to control the behavior of the event handler. If true, and the event was previously dispatched, handler will be called immediately with the latest payload. + * @param opt.notify - If `true` and the event was previously dispatched, the handler will be called immediately with the latest payload. */ on: OnEventListener; @@ -758,7 +795,7 @@ export interface Clerk { * Removes an event handler for a specific Clerk event. * * @param event - The event name to unsubscribe from - * @param handler - The callback function to remove + * @param handler - The callback function to remove. */ off: OffEventListener; @@ -771,7 +808,7 @@ export interface Clerk { __internal_addNavigationListener: (callback: () => void) => UnsubscribeCallback; /** - * Set the active session and Organization explicitly. + * A method used to set the current session and/or Organization for the client. Accepts a [`SetActiveParams`](https://clerk.com/docs/reference/types/set-active-params) object. * * If the session param is `null`, the active session is deleted. * In a similar fashion, if the organization param is `null`, the current organization is removed as active. @@ -779,148 +816,172 @@ export interface Clerk { setActive: SetActive; /** - * Function used to commit a navigation after certain steps in the Clerk processes. + * Helper method which will use the custom push navigation function of your application to navigate to the provided URL or relative path. + * + * Returns a promise that can be `await`ed in order to listen for the navigation to finish. The inner value should not be relied on, as it can change based on the framework it's used within. */ navigate: CustomNavigation; /** - * Decorates the provided url with the auth token for development instances. + * Decorates the provided URL with the auth token for development instances. * - * @param to + * @param to - The route to create a URL towards. */ buildUrlWithAuth(to: string): string; /** - * Returns the configured url where `` is mounted or a custom sign-in page is rendered. + * Returns the configured URL where [``](https://clerk.com/docs/reference/components/authentication/sign-in) is mounted or a custom sign-in page is rendered. * - * @param opts - A {@link RedirectOptions} object + * @param opts - Options used to control the redirect in the constructed URL. */ buildSignInUrl(opts?: RedirectOptions): string; /** - * Returns the configured url where `` is mounted or a custom sign-up page is rendered. + * Returns the configured URL where [``](https://clerk.com/docs/reference/components/authentication/sign-up) is mounted or a custom sign-up page is rendered. * - * @param opts - A {@link RedirectOptions} object + * @param opts - Options used to control the redirect in the constructed URL. */ buildSignUpUrl(opts?: RedirectOptions): string; /** - * Returns the url where `` is mounted or a custom user-profile page is rendered. + * Returns the configured URL where [``](https://clerk.com/docs/reference/components/authentication/user-profile) is mounted or a custom user-profile page is rendered. */ buildUserProfileUrl(): string; /** - * Returns the configured url where `` is mounted or a custom create-organization page is rendered. + * Returns the configured URL where [``](https://clerk.com/docs/reference/components/organization/create-organization) is mounted or a custom create-organization page is rendered. */ buildCreateOrganizationUrl(): string; /** - * Returns the configured url where `` is mounted or a custom organization-profile page is rendered. + * Returns the configured URL where [``](https://clerk.com/docs/reference/components/organization/organization-profile) is mounted or a custom organization-profile page is rendered. */ buildOrganizationProfileUrl(): string; /** - * Returns the configured url where tasks are mounted. + * Returns the configured URL where [session tasks](https://clerk.com/docs/guides/configure/session-tasks) are mounted. */ buildTasksUrl(): string; /** - * Returns the configured afterSignInUrl of the instance. + * Returns the configured `afterSignInUrl` of the instance. + * @param params - Optional configuration. + * @param params.params - Optional query parameters to append to the URL. */ buildAfterSignInUrl({ params }?: { params?: URLSearchParams }): string; /** - * Returns the configured afterSignInUrl of the instance. + * Returns the configured `afterSignInUrl` of the instance. + * @param params - Optional configuration. + * @param params.params - Optional query parameters to append to the URL. */ buildAfterSignUpUrl({ params }?: { params?: URLSearchParams }): string; /** - * Returns the configured afterSignOutUrl of the instance. + * Returns the configured `afterSignOutUrl` of the instance. */ buildAfterSignOutUrl(): string; /** - * Returns the configured newSubscriptionRedirectUrl of the instance. + * Returns the configured `newSubscriptionRedirectUrl` of the instance. */ buildNewSubscriptionRedirectUrl(): string; /** - * Returns the configured afterMultiSessionSingleSignOutUrl of the instance. + * Returns the configured `afterMultiSessionSingleSignOutUrl` of the instance. */ buildAfterMultiSessionSingleSignOutUrl(): string; /** - * Returns the configured url where `` is mounted or a custom waitlist page is rendered. + * Returns the configured URL where [``](https://clerk.com/docs/reference/components/authentication/waitlist) is mounted or a custom waitlist page is rendered. + * + * @param opts - Options to control the waitlist URL. + * @param opts.initialValues - Initial values to prefill the waitlist form. */ buildWaitlistUrl(opts?: { initialValues?: Record }): string; /** * - * Redirects to the provided url after decorating it with the auth token for development instances. + * Redirects to the provided URL after appending authentication credentials. For development instances, this method decorates the URL with an auth token to maintain authentication state. For production instances, the standard session cookie is used. + * + * Returns a promise that can be `await`ed in order to listen for the navigation to finish. The inner value should not be relied on, as it can change based on the framework it's used within. * - * @param to + * @param to - The URL to redirect to. */ redirectWithAuth(to: string): Promise; /** - * Redirects to the configured URL where `` is mounted. + * Redirects to the sign-in URL, as configured in your application's instance settings. This method uses the [`navigate()`](https://clerk.com/docs/reference/objects/clerk#navigate) method under the hood. * - * @param opts - A {@link RedirectOptions} object + * Returns a promise that can be `await`ed in order to listen for the navigation to finish. The inner value should not be relied on, as it can change based on the framework it's used within. + * + * @param opts - Options to control the redirect. */ redirectToSignIn(opts?: SignInRedirectOptions): Promise; /** - * Redirects to the configured URL where `` is mounted. + * Redirects to the sign-up URL, as configured in your application's instance settings. This method uses the [`navigate()`](https://clerk.com/docs/reference/objects/clerk#navigate) method under the hood. + * + * Returns a promise that can be `await`ed in order to listen for the navigation to finish. The inner value should not be relied on, as it can change based on the framework it's used within. * - * @param opts - A {@link RedirectOptions} object + * @param opts - Options to control the redirect. */ redirectToSignUp(opts?: SignUpRedirectOptions): Promise; /** - * Redirects to the configured URL where `` is mounted. + * Redirects to the configured URL where [``](https://clerk.com/docs/reference/components/user/user-profile) is mounted. + * + * Returns a promise that can be `await`ed in order to listen for the navigation to finish. The inner value should not be relied on, as it can change based on the framework it's used within. */ redirectToUserProfile: () => Promise; /** - * Redirects to the configured URL where `` is mounted. + * Redirects to the configured URL where [``](https://clerk.com/docs/reference/components/organization/organization-profile) is mounted. This method uses the [`navigate()`](https://clerk.com/docs/reference/objects/clerk#navigate) method under the hood. + * + * Returns a promise that can be `await`ed in order to listen for the navigation to finish. The inner value should not be relied on, as it can change based on the framework it's used within. */ redirectToOrganizationProfile: () => Promise; /** - * Redirects to the configured URL where `` is mounted. + * Redirects to the configured URL where [``](https://clerk.com/docs/reference/components/organization/create-organization) is mounted. This method uses the [`navigate()`](https://clerk.com/docs/reference/objects/clerk#navigate) method under the hood. + * + * Returns a promise that can be `await`ed in order to listen for the navigation to finish. The inner value should not be relied on, as it can change based on the framework it's used within. */ redirectToCreateOrganization: () => Promise; /** - * Redirects to the configured afterSignIn URL. + * Redirects to the configured `afterSignIn` URL. */ redirectToAfterSignIn: () => void; /** - * Redirects to the configured afterSignUp URL. + * Redirects to the configured `afterSignUp` URL. */ redirectToAfterSignUp: () => void; /** - * Redirects to the configured afterSignOut URL. + * Redirects to the configured `afterSignOut` URL. */ redirectToAfterSignOut: () => void; /** - * Redirects to the configured URL where `` is mounted. + * Redirects to the configured URL where [``](https://clerk.com/docs/reference/components/authentication/waitlist) is mounted. */ redirectToWaitlist: () => void; /** - * Redirects to the configured URL where tasks are mounted. + * Redirects to the configured URL where [session tasks](https://clerk.com/docs/reference/objects/session#currenttask) are mounted. * - * @param opts - A {@link RedirectOptions} object + * @param opts - Options to control the redirect (e.g. redirect URL after tasks are complete). */ redirectToTasks(opts?: TasksRedirectOptions): Promise; /** - * Completes a Google One Tap redirection flow started by - * {@link Clerk.authenticateWithGoogleOneTap} + * Completes a Google One Tap redirection flow started by [`authenticateWithGoogleOneTap()`](https://clerk.com/reference/objects/clerk#authenticate-with-google-one-tap). This method should be called after the user is redirected back from visiting the Google One Tap prompt. + * + * @param signInOrUp - The resource returned from the initial `authenticateWithGoogleOneTap()` call (before redirect). + * @param params - Additional props that define where the user will be redirected to at the end of a successful Google One Tap flow. + * @param customNavigate - A function that overrides Clerk's default navigation behavior, allowing custom handling of navigation during sign-up and sign-in flows. */ handleGoogleOneTapCallback: ( signInOrUp: SignInResource | SignUpResource, @@ -929,8 +990,10 @@ export interface Clerk { ) => Promise; /** - * Completes an OAuth or SAML redirection flow started by - * {@link Clerk.client.signIn.authenticateWithRedirect} or {@link Clerk.client.signUp.authenticateWithRedirect} + * Completes a custom OAuth or SAML redirect flow that was started by calling [`SignIn.authenticateWithRedirect(params)`](https://clerk.com/docs/reference/objects/sign-in) or [`SignUp.authenticateWithRedirect(params)`](https://clerk.com/docs/reference/objects/sign-up). + * + * @param params - Additional props that define where the user will be redirected to at the end of a successful OAuth or SAML flow. + * @param customNavigate - A function that overrides Clerk's default navigation behavior, allowing custom handling of navigation during sign-up and sign-in flows. */ handleRedirectCallback: ( params: HandleOAuthCallbackParams | HandleSamlCallbackParams, @@ -938,7 +1001,9 @@ export interface Clerk { ) => Promise; /** - * Completes a Email Link flow started by {@link Clerk.client.signIn.createEmailLinkFlow} or {@link Clerk.client.signUp.createEmailLinkFlow} + * Completes an email link verification flow started by `Clerk.client.signIn.createEmailLinkFlow` or `Clerk.client.signUp.createEmailLinkFlow`, by processing the verification results from the redirect URL query parameters. This method should be called after the user is redirected back from visiting the verification link in their email. + * @param params - Allows you to define the URLs where the user should be redirected to on successful verification or pending/completed sign-up or sign-in attempts. If the email link is successfully verified on another device, there's a callback function parameter that allows custom code execution. + * @param customNavigate - A function that overrides Clerk's default navigation behavior, allowing custom handling of navigation during sign-up and sign-in flows. */ handleEmailLinkVerification: ( params: HandleEmailLinkVerificationParams, @@ -946,32 +1011,32 @@ export interface Clerk { ) => Promise; /** - * Authenticates user using their Metamask browser extension + * Starts a sign-in flow that uses the MetaMask browser extension to authenticate the user using their Metamask wallet address. */ authenticateWithMetamask: (params?: AuthenticateWithMetamaskParams) => Promise; /** - * Authenticates user using their Coinbase Smart Wallet and browser extension + * Starts a sign-in flow that uses the Coinbase Smart Wallet and browser extension to authenticate the user using their Coinbase wallet address. */ authenticateWithCoinbaseWallet: (params?: AuthenticateWithCoinbaseWalletParams) => Promise; /** - * Authenticates user using their OKX Wallet browser extension + * Starts a sign-in flow that uses the OKX Wallet browser extension to authenticate the user using their OKX wallet address. */ authenticateWithOKXWallet: (params?: AuthenticateWithOKXWalletParams) => Promise; /** - * Authenticates user using Base Account SDK + * Starts a sign-in flow that uses the Base Account SDK to authenticate the user using their Base wallet address. */ authenticateWithBase: (params?: AuthenticateWithBaseParams) => Promise; /** - * Authenticates user using their Solana supported Web3 wallet browser extension + * Starts a sign-in flow that uses a Solana supported Web3 wallet browser extension to authenticate the user using their Solana wallet address. */ authenticateWithSolana: (params: AuthenticateWithSolanaParams) => Promise; /** - * Authenticates user using their Web3 Wallet browser extension + * Starts a sign-in flow that uses a Web3 Wallet browser extension to authenticate the user using their Ethereum wallet address. */ authenticateWithWeb3: (params: ClerkAuthenticateWithWeb3Params) => Promise; @@ -983,20 +1048,28 @@ export interface Clerk { ) => Promise; /** - * Creates an Organization, adding the current user as admin. + * Creates an Organization programmatically, adding the current user as admin. Returns an [`Organization`](https://clerk.com/docs/reference/objects/organization) object. + * + * > [!NOTE] + * > For React-based apps, consider using the [``](https://clerk.com/docs/reference/components/organization/create-organization) component. */ createOrganization: (params: CreateOrganizationParams) => Promise; /** - * Retrieves a single Organization by ID. + * Retrieves a single [Organization](https://clerk.com/docs/reference/objects/organization) by ID. + * + * @param organizationId - The ID of the Organization to retrieve. */ getOrganization: (organizationId: string) => Promise; /** - * Handles a 401 response from Frontend API by refreshing the client and session object accordingly + * Handles a 401 response from the Frontend API by refreshing the [`Client`](https://clerk.com/docs/reference/objects/client) and [`Session`](https://clerk.com/docs/reference/objects/session) object accordingly. */ handleUnauthenticated: () => Promise; + /** + * Create a new waitlist entry programmatically. Requires that you set your app's sign-up mode to [**Waitlist**](https://clerk.com/docs/guides/secure/restricting-access#waitlist) in the Clerk Dashboard. + */ joinWaitlist: (params: JoinWaitlistParams) => Promise; /** @@ -1019,11 +1092,13 @@ export interface Clerk { /** * Internal flag indicating whether a `setActive` call is in progress. Used to prevent navigations from being * initiated outside of the Clerk class. + * + * @internal */ __internal_setActiveInProgress: boolean; /** - * API Keys Object + * The `APIKeys` object used for managing API keys. */ apiKeys: APIKeysNamespace; @@ -1036,44 +1111,42 @@ export interface Clerk { __experimental_checkout: __experimental_CheckoutFunction; } +/** @document */ export type HandleOAuthCallbackParams = TransferableOption & SignInForceRedirectUrl & SignInFallbackRedirectUrl & SignUpForceRedirectUrl & SignUpFallbackRedirectUrl & { /** - * Full URL or path where the SignIn component is mounted. + * The full URL or path where the [``](https://clerk.com/docs/reference/components/authentication/sign-in) component is mounted. */ signInUrl?: string; /** - * Full URL or path where the SignUp component is mounted. + * The full URL or path where the [``](https://clerk.com/docs/reference/components/authentication/sign-up) component is mounted. */ signUpUrl?: string; /** - * Full URL or path to navigate to during sign in, - * if identifier verification is required. + * The full URL or path to navigate to during sign in, if [first factor verification](!first-factor-verification) is required. */ firstFactorUrl?: string; /** - * Full URL or path to navigate to during sign in, - * if 2FA is enabled. + * The full URL or path to navigate to during sign in, if [multi-factor authentication](https://clerk.com/docs/guides/configure/auth-strategies/sign-up-sign-in-options#multi-factor-authentication) is enabled. */ secondFactorUrl?: string; /** - * Full URL or path to navigate to during sign in, - * if the user is required to reset their password. + * The full URL or path to navigate to during sign in, if the user is required to reset their password. */ resetPasswordUrl?: string; /** - * Full URL or path to navigate to after an incomplete sign up. + * The full URL or path to navigate to if the sign up requires additional information. */ continueSignUpUrl?: string | null; /** - * Full URL or path to navigate to after requesting email verification. + * The full URL or path to navigate to after requesting email verification. */ verifyEmailAddressUrl?: string | null; /** - * Full URL or path to navigate to after requesting phone verification. + * The full URL or path to navigate to after requesting phone verification. */ verifyPhoneNumberUrl?: string | null; /** @@ -1081,13 +1154,21 @@ export type HandleOAuthCallbackParams = TransferableOption & */ reloadResource?: 'signIn' | 'signUp'; /** - * Additional arbitrary metadata to be stored alongside the User object when a sign-up transfer occurs. + * Metadata that can be read and set from the frontend. Once the sign-up is complete, the value of this field will be automatically copied to the newly created user's unsafe metadata. One common use case for this attribute is to use it to implement custom fields that can be collected during sign-up and will automatically be attached to the created `User` object. */ unsafeMetadata?: SignUpUnsafeMetadata; }; export type HandleSamlCallbackParams = HandleOAuthCallbackParams; +/** + * A function used to navigate to a given URL after certain steps in the Clerk processes. + * + * @param to - The URL or relative path to navigate to. + * @param options - Optional configuration. + * @param options.replace? - If `true`, replace the current history entry instead of pushing a new one. + * @param options.metadata? - Optional router metadata. + */ export type CustomNavigation = (to: string, options?: NavigateOptions) => Promise | void; export type ClerkThemeOptions = DeepSnakeToCamel>; @@ -1285,11 +1366,21 @@ export type ClerkOptions = ClerkOptionsNavigation & taskUrls?: Partial>; }; +/** @inline */ export interface NavigateOptions { + /** + * If `true`, replace the current history entry instead of pushing a new one. + */ replace?: boolean; + /** + * Optional router metadata. + */ metadata?: RouterMetadata; } +/** + * @inline + */ export interface Resources { client: ClientResource; session?: SignedInSessionResource | null; @@ -1405,13 +1496,9 @@ export type SetActiveParams = { redirectUrl?: string; /** - * A custom navigation function to be called just before the session and/or Organization is set. + * A custom navigation function to be called just before the session and/or Organization is set. When provided, it takes precedence over the `redirectUrl` parameter for navigation. * - * When provided, it takes precedence over the `redirectUrl` parameter for navigation. - * - * The callback receives a `decorateUrl` function that should be used to wrap destination URLs. - * This enables Safari ITP cookie refresh when needed. The decorated URL may be an external URL - * (starting with `https://`) that requires `window.location.href` instead of client-side navigation. + * The callback receives a `decorateUrl` function that should be used to wrap destination URLs. This enables Safari ITP cookie refresh when needed. The decorated URL may be an external URL (starting with `https://`) that requires `window.location.href` instead of client-side navigation. See the [section on using the `navigate()` parameter](https://clerk.com/docs/reference/objects/clerk#using-the-navigate-parameter) for more details. * * @example * ```typescript @@ -1446,6 +1533,7 @@ export type RoutingOptions = | { path: string | undefined; routing?: Extract } | { path?: never; routing?: Extract }; +/** @document */ export type SignInProps = RoutingOptions & { /** * Full URL or path to navigate to after successful sign in. @@ -1473,7 +1561,7 @@ export type SignInProps = RoutingOptions & { */ signUpUrl?: string; /** - * Customisation options to fully match the Clerk components to your own brand. + * Customization options to fully match the Clerk components to your own brand. * These options serve as overrides and will be merged with the global `appearance` * prop of ClerkProvider (if one is provided) */ @@ -1512,10 +1600,12 @@ export type SignInProps = RoutingOptions & { SignUpFallbackRedirectUrl & AfterSignOutUrl; +/** + * @interface + */ export interface TransferableOption { /** - * Indicates whether or not sign in attempts are transferable to the sign up flow. - * When set to false, prevents opaque sign ups when a user attempts to sign in via OAuth with an email that doesn't exist. + * Indicates whether or not sign-in attempts are transferable to the sign-up flow. Defaults to `true`. When set to `false`, prevents [opaque sign-ups](!opaque-sign-up) when a user attempts to sign in via OAuth with an email that doesn't exist. * * @default true */ @@ -1557,7 +1647,7 @@ export type __internal_UserVerificationProps = RoutingOptions & { level?: SessionVerificationLevel; /** - * Customisation options to fully match the Clerk components to your own brand. + * Customization options to fully match the Clerk components to your own brand. * These options serve as overrides and will be merged with the global `appearance` * prop of ClerkProvider (if one is provided) */ @@ -1597,6 +1687,7 @@ export type __internal_AttemptToEnableEnvironmentSettingResult = { type GoogleOneTapRedirectUrlProps = SignInForceRedirectUrl & SignUpForceRedirectUrl; +/** @document */ export type GoogleOneTapProps = GoogleOneTapRedirectUrlProps & { /** * Whether to cancel the Google One Tap request if a user clicks outside the prompt. @@ -1622,6 +1713,7 @@ export type GoogleOneTapProps = GoogleOneTapRedirectUrlProps & { appearance?: ClerkAppearanceTheme; }; +/** @document */ export type SignUpProps = RoutingOptions & { /** * Full URL or path to navigate to after successful sign up. @@ -1644,7 +1736,7 @@ export type SignUpProps = RoutingOptions & { */ signInUrl?: string; /** - * Customisation options to fully match the Clerk components to your own brand. + * Customization options to fully match the Clerk components to your own brand. * These options serve as overrides and will be merged with the global `appearance` * prop of ClerkProvider (if one is provided) */ @@ -1688,9 +1780,10 @@ export type SignUpModalProps = WithoutRouting & { getContainer?: () => HTMLElement | null; }; +/** @document */ export type UserProfileProps = RoutingOptions & { /** - * Customisation options to fully match the Clerk components to your own brand. + * Customization options to fully match the Clerk components to your own brand. * These options serve as overrides and will be merged with the global `appearance` * prop of ClerkProvider (if one is provided) */ @@ -1737,6 +1830,7 @@ export type UserProfileModalProps = WithoutRouting & { getContainer?: () => HTMLElement | null; }; +/** @document */ export type OrganizationProfileProps = RoutingOptions & { /** * Full URL or path to navigate to after the user leaves the currently Active Organization. @@ -1745,7 +1839,7 @@ export type OrganizationProfileProps = RoutingOptions & { */ afterLeaveOrganizationUrl?: string; /** - * Customisation options to fully match the Clerk components to your own brand. + * Customization options to fully match the Clerk components to your own brand. * These options serve as overrides and will be merged with the global `appearance` * prop of ClerkProvider (if one is provided) */ @@ -1787,6 +1881,7 @@ export type OrganizationProfileModalProps = WithoutRouting HTMLElement | null; }; +/** @document */ export type CreateOrganizationProps = RoutingOptions & { /** * Full URL or path to navigate to after creating a new Organization. @@ -1804,13 +1899,14 @@ export type CreateOrganizationProps = RoutingOptions & { */ skipInvitationScreen?: boolean; /** - * Customisation options to fully match the Clerk components to your own brand. + * Customization options to fully match the Clerk components to your own brand. * These options serve as overrides and will be merged with the global `appearance` * prop of ClerkProvider (if one is provided) */ appearance?: ClerkAppearanceTheme; }; +/** @document */ export type CreateOrganizationModalProps = WithoutRouting & { /** * Function that returns the container element where portals should be rendered. @@ -1820,7 +1916,10 @@ export type CreateOrganizationModalProps = WithoutRouting HTMLElement | null; }; +/** @inline */ type UserProfileMode = 'modal' | 'navigation'; + +/** @document */ type UserButtonProfileMode = | { userProfileUrl?: never; @@ -1862,7 +1961,7 @@ export type UserButtonProps = UserButtonProfileMode & { */ afterSwitchSessionUrl?: string; /** - * Customisation options to fully match the Clerk components to your own brand. + * Customization options to fully match the Clerk components to your own brand. * These options serve as overrides and will be merged with the global `appearance` * prop of ClerkProvider (if one is provided) */ @@ -1899,6 +1998,7 @@ type CreateOrganizationMode = | { createOrganizationUrl: string; createOrganizationMode?: 'navigation' } | { createOrganizationUrl?: never; createOrganizationMode?: 'modal' }; +/** @document */ export type OrganizationSwitcherProps = CreateOrganizationMode & OrganizationProfileMode & { /** @@ -1963,7 +2063,7 @@ export type OrganizationSwitcherProps = CreateOrganizationMode & */ skipInvitationScreen?: boolean; /** - * Customisation options to fully match the Clerk components to your own brand. + * Customization options to fully match the Clerk components to your own brand. * These options serve as overrides and will be merged with the global `appearance` * prop of ClerkProvider(if one is provided) */ @@ -1975,6 +2075,7 @@ export type OrganizationSwitcherProps = CreateOrganizationMode & organizationProfileProps?: Pick; }; +/** @document */ export type OrganizationListProps = { /** * Full URL or path to navigate to after creating a new Organization. @@ -1994,7 +2095,7 @@ export type OrganizationListProps = { | ((organization: OrganizationResource) => string) | LooseExtractedParams>; /** - * Customisation options to fully match the Clerk components to your own brand. + * Customization options to fully match the Clerk components to your own brand. * These options serve as overrides and will be merged with the global `appearance` * prop of ClerkProvider (if one is provided) */ @@ -2024,13 +2125,14 @@ export type OrganizationListProps = { afterSelectPersonalUrl?: ((user: UserResource) => string) | LooseExtractedParams>; }; +/** @document */ export type WaitlistProps = { /** * Full URL or path to navigate to after join waitlist. */ afterJoinWaitlistUrl?: string; /** - * Customisation options to fully match the Clerk components to your own brand. + * Customization options to fully match the Clerk components to your own brand. * These options serve as overrides and will be merged with the global `appearance` * prop of ClerkProvided (if one is provided) */ @@ -2041,6 +2143,7 @@ export type WaitlistProps = { signInUrl?: string; }; +/** @document */ export type WaitlistModalProps = WaitlistProps & { /** * Function that returns the container element where portals should be rendered. @@ -2050,6 +2153,7 @@ export type WaitlistModalProps = WaitlistProps & { getContainer?: () => HTMLElement | null; }; +/** @document */ type PricingTableDefaultProps = { /** * The position of the CTA button. @@ -2071,6 +2175,7 @@ type PricingTableDefaultProps = { newSubscriptionRedirectUrl?: string; }; +/** @document */ type PricingTableBaseProps = { /** * The subscriber type to display plans for. @@ -2080,7 +2185,7 @@ type PricingTableBaseProps = { */ for?: ForPayerType; /** - * Customisation options to fully match the Clerk components to your own brand. + * Customization options to fully match the Clerk components to your own brand. * These options serve as overrides and will be merged with the global `appearance` * prop of ClerkProvider (if one is provided) */ @@ -2094,8 +2199,10 @@ type PricingTableBaseProps = { type PortalRoot = HTMLElement | null | undefined; +/** @document */ export type PricingTableProps = PricingTableBaseProps & PricingTableDefaultProps; +/** @document */ export type APIKeysProps = { /** * The number of API keys to show per page. @@ -2104,7 +2211,7 @@ export type APIKeysProps = { */ perPage?: number; /** - * Customisation options to fully match the Clerk components to your own brand. + * Customization options to fully match the Clerk components to your own brand. * These options serve as overrides and will be merged with the global `appearance` * prop of ClerkProvider (if one is provided) */ @@ -2117,20 +2224,47 @@ export type APIKeysProps = { showDescription?: boolean; }; +/** @document */ export type GetAPIKeysParams = ClerkPaginationParams<{ + /** + * The user or organization ID to query API keys by. If not provided, defaults to the [Active Organization](!active-organization), then the current User. + */ subject?: string; + /** + * A search query to filter API keys by name. + */ query?: string; }>; +/** @document */ export type CreateAPIKeyParams = { + /** + * The name of the API key. + */ name: string; + /** + * The user or organization ID to associate the API key with. If not provided, defaults to the [Active Organization](!active-organization), then the current User. + */ subject?: string; + /** + * The number of seconds until the API key expires. Set to `null` or omit to create a key that never expires. + */ secondsUntilExpiration?: number; + /** + * The description of the API key. + */ description?: string; }; +/** @document */ export type RevokeAPIKeyParams = { + /** + * The ID of the API key to revoke. + */ apiKeyID: string; + /** + * The reason for revoking the API key. + */ revocationReason?: string; }; @@ -2294,20 +2428,18 @@ export type __internal_OAuthConsentProps = { onDeny: () => void; }; +/** @document */ export interface HandleEmailLinkVerificationParams { /** - * Full URL or path to navigate to after successful magic link verification - * on completed sign up or sign in on the same device. + * The full URL to navigate to after successful email link verification on completed sign-up or sign-in on the same device. */ redirectUrlComplete?: string; /** - * Full URL or path to navigate to after successful magic link verification - * on the same device, but not completed sign in or sign up. + * The full URL to navigate to after successful email link verification on the same device, but without completing sign-in or sign-up. */ redirectUrl?: string; /** - * Callback function to be executed after successful magic link - * verification on another device. + * Callback function to be executed after successful email link verification on another device. */ onVerifiedOnOtherDevice?: () => void; } @@ -2350,37 +2482,63 @@ export type SignUpButtonProps = (SignUpButtonPropsModal | ButtonPropsRedirect) & | 'oauthFlow' >; +/** @document */ export type TaskChooseOrganizationProps = { /** * Full URL or path to navigate to after successfully resolving all tasks */ redirectUrlComplete: string; + /** + * Customization options to fully match the Clerk components to your own brand. + */ appearance?: ClerkAppearanceTheme; }; +/** @document */ export type TaskResetPasswordProps = { /** * Full URL or path to navigate to after successfully resolving all tasks */ redirectUrlComplete: string; + /** + * Customization options to fully match the Clerk components to your own brand. + */ appearance?: ClerkAppearanceTheme; }; +/** @document */ export type TaskSetupMFAProps = { /** * Full URL or path to navigate to after successfully resolving all tasks */ redirectUrlComplete: string; + /** + * Customization options to fully match the Clerk components to your own brand. + */ appearance?: ClerkAppearanceTheme; }; +/** @document */ export type CreateOrganizationInvitationParams = { + /** + * The email address of the user to invite. + */ emailAddress: string; + /** + * The role of the user to invite. + */ role: OrganizationCustomRoleKey; }; +/** @document */ export type CreateBulkOrganizationInvitationParams = { + /** + * The email addresses of the users to invite. + */ emailAddresses: string[]; + /** + * The role of the users to invite. + */ role: OrganizationCustomRoleKey; }; @@ -2398,60 +2556,175 @@ export interface CreateOrganizationParams { slug?: string; } +/** @document */ export interface ClerkAuthenticateWithWeb3Params { + /** + * A function that overrides Clerk's default navigation behavior, allowing custom handling of navigation during sign-up and sign-in flows. + */ customNavigate?: (to: string) => Promise; + /** + * The full URL or path to navigate to after a successful sign-in or sign-up. + */ redirectUrl?: string; + /** + * The URL to navigate to if the sign-up process is missing user information. + */ signUpContinueUrl?: string; + /** + * Metadata that can be read and set from the frontend. Once the sign-up is complete, the value of this field will be automatically copied to the newly created user's unsafe metadata. One common use case for this attribute is to use it to implement custom fields that can be collected during sign-up and will automatically be attached to the created `User` object. + */ unsafeMetadata?: SignUpUnsafeMetadata; + /** + * The strategy to use for the sign-in flow. + */ strategy: Web3Strategy; + /** + * A boolean indicating whether the user has agreed to the [legal compliance](https://clerk.com/docs/guides/secure/legal-compliance) documents. + */ legalAccepted?: boolean; + /** + * The URL to navigate to if [second factor](https://clerk.com/docs/guides/configure/auth-strategies/sign-up-sign-in-options#multi-factor-authentication) is required. + */ secondFactorUrl?: string; + /** + * The name of the wallet to use for authentication. + */ walletName?: string; } +/** @document */ export interface AuthenticateWithMetamaskParams { + /** + * A function that overrides Clerk's default navigation behavior, allowing custom handling of navigation during sign-up and sign-in flows. + */ customNavigate?: (to: string) => Promise; + /** + * The full URL or path to navigate to after a successful sign-in or sign-up. + */ redirectUrl?: string; + /** + * The URL to navigate to if the sign-up process is missing user information. + */ signUpContinueUrl?: string; + /** + * Metadata that can be read and set from the frontend. Once the sign-up is complete, the value of this field will be automatically copied to the newly created user's unsafe metadata. One common use case for this attribute is to use it to implement custom fields that can be collected during sign-up and will automatically be attached to the created `User` object. + */ unsafeMetadata?: SignUpUnsafeMetadata; + /** + * A boolean indicating whether the user has agreed to the [legal compliance](https://clerk.com/docs/guides/secure/legal-compliance) documents. + */ legalAccepted?: boolean; } +/** @document */ export interface AuthenticateWithCoinbaseWalletParams { + /** + * A function that overrides Clerk's default navigation behavior, allowing custom handling of navigation during sign-up and sign-in flows. + */ customNavigate?: (to: string) => Promise; + /** + * The full URL or path to navigate to after a successful sign-in or sign-up. + */ redirectUrl?: string; + /** + * The URL to navigate to if the sign-up process is missing user information. + */ signUpContinueUrl?: string; + /** + * Metadata that can be read and set from the frontend. Once the sign-up is complete, the value of this field will be automatically copied to the newly created user's unsafe metadata. One common use case for this attribute is to use it to implement custom fields that can be collected during sign-up and will automatically be attached to the created `User` object. + */ unsafeMetadata?: SignUpUnsafeMetadata; + /** + * A boolean indicating whether the user has agreed to the [legal compliance](https://clerk.com/docs/guides/secure/legal-compliance) documents. + */ legalAccepted?: boolean; } +/** @document */ export interface AuthenticateWithOKXWalletParams { + /** + * A function that overrides Clerk's default navigation behavior, allowing custom handling of navigation during sign-up and sign-in flows. + */ customNavigate?: (to: string) => Promise; + /** + * The full URL or path to navigate to after a successful sign-in or sign-up. + */ redirectUrl?: string; + /** + * The URL to navigate to if the sign-up process is missing user information. + */ signUpContinueUrl?: string; + /** + * Metadata that can be read and set from the frontend. Once the sign-up is complete, the value of this field will be automatically copied to the newly created user's unsafe metadata. One common use case for this attribute is to use it to implement custom fields that can be collected during sign-up and will automatically be attached to the created `User` object. + */ unsafeMetadata?: SignUpUnsafeMetadata; + /** + * A boolean indicating whether the user has agreed to the [legal compliance](https://clerk.com/docs/guides/secure/legal-compliance) documents. + */ legalAccepted?: boolean; } +/** @document */ export interface AuthenticateWithGoogleOneTapParams { + /** + * The Google credential token from the Google Identity Services response. + */ token: string; + /** + * A boolean indicating whether the user has agreed to the [legal compliance](https://clerk.com/docs/guides/secure/legal-compliance) documents. + */ legalAccepted?: boolean; } +/** @document */ export interface AuthenticateWithBaseParams { + /** + * A function that overrides Clerk's default navigation behavior, allowing custom handling of navigation during sign-up and sign-in flows. + */ customNavigate?: (to: string) => Promise; + /** + * The full URL or path to navigate to after a successful sign-in or sign-up. + */ redirectUrl?: string; + /** + * The URL to navigate to if the sign-up process is missing user information. + */ signUpContinueUrl?: string; + /** + * Metadata that can be read and set from the frontend. Once the sign-up is complete, the value of this field will be automatically copied to the newly created user's unsafe metadata. One common use case for this attribute is to use it to implement custom fields that can be collected during sign-up and will automatically be attached to the created `User` object. + */ unsafeMetadata?: SignUpUnsafeMetadata; + /** + * A boolean indicating whether the user has agreed to the [legal compliance](https://clerk.com/docs/guides/secure/legal-compliance) documents. + */ legalAccepted?: boolean; } +/** @document */ export interface AuthenticateWithSolanaParams { + /** + * A function that overrides Clerk's default navigation behavior, allowing custom handling of navigation during sign-up and sign-in flows. + */ customNavigate?: (to: string) => Promise; + /** + * The full URL or path to navigate to after a successful sign-in or sign-up. + */ redirectUrl?: string; + /** + * The URL to navigate to if the sign-up process is missing user information. + */ signUpContinueUrl?: string; + /** + * Metadata that can be read and set from the frontend. Once the sign-up is complete, the value of this field will be automatically copied to the newly created user's unsafe metadata. One common use case for this attribute is to use it to implement custom fields that can be collected during sign-up and will automatically be attached to the created `User` object. + */ unsafeMetadata?: SignUpUnsafeMetadata; + /** + * A boolean indicating whether the user has agreed to the [legal compliance](https://clerk.com/docs/guides/secure/legal-compliance) documents. + */ legalAccepted?: boolean; + /** + * The name of the Solana wallet to use for authentication. + */ walletName: string; } @@ -2464,6 +2737,11 @@ export interface BrowserClerkConstructor { } export interface HeadlessBrowserClerk extends Clerk { + /** + * Initializes the `Clerk` object and loads all necessary environment configuration and instance settings from the [Frontend API](/docs/reference/frontend-api){{ target: '_blank' }}. + * + * For the JavaScript SDK, you must call this method before using the `Clerk` object in your code. Refer to the [quickstart guide](/docs/js-frontend/getting-started/quickstart) for more information. + */ load: (opts?: Without) => Promise; updateClient: (client: ClientResource) => void; } diff --git a/packages/shared/src/types/client.ts b/packages/shared/src/types/client.ts index 1c89ed554aa..27bf97ef0ff 100644 --- a/packages/shared/src/types/client.ts +++ b/packages/shared/src/types/client.ts @@ -5,27 +5,106 @@ import type { SignInResource } from './signIn'; import type { SignUpResource } from './signUp'; import type { ClientJSONSnapshot } from './snapshots'; +/** + * The `Client` object keeps track of the authenticated sessions in the current device. The device can be a browser, a native application, or any other medium that is usually the requesting part in a request/response architecture. + * + * The `Client` object also holds information about any sign-in or sign-up attempts that might be in progress, tracking the sign-in or sign-up progress. + */ export interface ClientResource extends ClerkResource { + /** + * A list of sessions that have been created on this client. + */ sessions: SessionResource[]; + /** + * A list of sessions on this client where the user has completed the full sign-in flow. Sessions can be in one of the following states: + * + * - **active**: The user has completed the full sign-in flow and all pending tasks. + * - **pending**: The user has completed the sign-in flow but still needs to complete one or more required steps (**pending tasks**). + */ signedInSessions: SignedInSessionResource[]; + /** + * The current sign up attempt, or `null` if there is none. + */ signUp: SignUpResource; + /** + * The current sign in attempt, or `null` if there is none. + */ signIn: SignInResource; + /** + * Returns `true` if this client hasn't been saved (created) yet in the Frontend API. Returns `false` otherwise. + */ isNew: () => boolean; + /** + * Creates a new client for the current instance along with its cookie. + */ create: () => Promise; + /** + * + * Deletes the client. All sessions will be reset. + */ destroy: () => Promise; + /** + * + * Removes all sessions created on the client. + */ removeSessions: () => Promise; + /** + * + * Clears any locally cached session data for the current client. + */ clearCache: () => void; + /** + * + * Resets the current sign-in attempt. Clears the in-progress sign-in state on the client. + */ resetSignIn: () => void; + /** + * Resets the current sign-up attempt. Clears the in-progress sign-up state on the client. + */ resetSignUp: () => void; + /** + * + * Returns `true` if the client cookie is due to expire in 8 days or less. Returns `false` otherwise. + */ isEligibleForTouch: () => boolean; + /** + * + * Builds a URL that refreshes the current client's authentication state and then redirects the user to the specified URL. + * + */ buildTouchUrl: (params: { redirectUrl: URL }) => string; + /** + * The ID of the last active [`Session`](https://clerk.com/docs/reference/objects/session) on this client. + */ lastActiveSessionId: string | null; - /** Last authentication strategy used by this client; `null` when unknown or feature disabled. */ + /** + * Last authentication strategy used by this client; `null` when unknown or feature disabled. + */ lastAuthenticationStrategy: LastAuthenticationStrategy | null; + /** + * Indicates whether CAPTCHA checks are skipped for this client. + */ captchaBypass: boolean; + /** + * The date and time when the client's authentication cookie will expire. + */ cookieExpiresAt: Date | null; + /** + * The date and time when the client was created. + */ createdAt: Date | null; + /** + * The date and time when the client was last updated. + */ updatedAt: Date | null; + /** + * Sends a CAPTCHA token to the client. + * @internal + */ __internal_sendCaptchaToken: (params: unknown) => Promise; + /** + * Converts the client to a snapshot. + * @internal + */ __internal_toSnapshot: () => ClientJSONSnapshot; } diff --git a/packages/shared/src/types/instance.ts b/packages/shared/src/types/instance.ts index 617799635de..921926dd885 100644 --- a/packages/shared/src/types/instance.ts +++ b/packages/shared/src/types/instance.ts @@ -1 +1,4 @@ +/** + * @inline + */ export type InstanceType = 'production' | 'development'; diff --git a/packages/shared/src/types/multiDomain.ts b/packages/shared/src/types/multiDomain.ts index 3661e7132bf..de7bf6fe432 100644 --- a/packages/shared/src/types/multiDomain.ts +++ b/packages/shared/src/types/multiDomain.ts @@ -60,13 +60,16 @@ export type MultiDomainAndOrProxyPrimitives = domain?: never; }; +/** + * Only one of the two properties are allowed to be set at a time. + */ export type DomainOrProxyUrl = { /** - * **Required for applications that run behind a reverse proxy**. The URL that Clerk will proxy requests to. Can be either a relative path (`/__clerk`) or a full URL (`https:///__clerk`). + * **Required for applications that run behind a reverse proxy**. The URL that Clerk will proxy requests to. Can be either a relative path (`/__clerk`) or a full URL (`https:///__clerk`), or a function that will be called with a `URL` made from `window.location.href`. */ proxyUrl?: string | ((url: URL) => string); /** - * **Required if your application is a satellite application**. Sets the domain of the satellite application. + * **Required if your application is a satellite application**. Sets the domain of the satellite application. Can be either a relative path (`/__clerk`) or a full URL (`https:///__clerk`), or a function that will be called with a `URL` made from `window.location.href`. */ domain?: string | ((url: URL) => string); }; diff --git a/packages/shared/src/types/redirects.ts b/packages/shared/src/types/redirects.ts index bafcdf1db63..41e53ebc2cb 100644 --- a/packages/shared/src/types/redirects.ts +++ b/packages/shared/src/types/redirects.ts @@ -17,7 +17,6 @@ export type AfterMultiSessionSingleSignOutUrl = { /** * Redirect URLs for different actions. - * Mainly used to be used to type internal Clerk functions. */ export type RedirectOptions = SignInForceRedirectUrl & SignInFallbackRedirectUrl & @@ -25,6 +24,7 @@ export type RedirectOptions = SignInForceRedirectUrl & SignUpFallbackRedirectUrl & RedirectUrlProp; +/** @document */ export type AuthenticateWithRedirectParams = { /** * The full URL or path to the route that will complete the OAuth or SAML flow. @@ -82,6 +82,7 @@ export type AuthenticateWithRedirectParams = { export type AuthenticateWithPopupParams = AuthenticateWithRedirectParams & { popup: Window | null }; +/** @document */ export type RedirectUrlProp = { /** * Full URL or path to navigate to after a successful action. @@ -89,13 +90,15 @@ export type RedirectUrlProp = { redirectUrl?: string | null; }; +/** @document */ export type SignUpForceRedirectUrl = { /** - * This URL will always be redirected to after the user signs up. It's recommended to use the [environment variable](https://clerk.com/docs/guides/development/clerk-environment-variables#sign-in-and-sign-up-redirects) instead. + * If provided, this URL will always be redirected to after the user signs up. It's recommended to use the [environment variable](https://clerk.com/docs/guides/development/clerk-environment-variables#sign-in-and-sign-up-redirects) instead. */ signUpForceRedirectUrl?: string | null; }; +/** @document */ export type SignUpFallbackRedirectUrl = { /** * The fallback URL to redirect to after the user signs up, if there's no `redirect_url` in the path already. It's recommended to use the [environment variable](https://clerk.com/docs/guides/development/clerk-environment-variables#sign-in-and-sign-up-redirects) instead. @@ -105,6 +108,7 @@ export type SignUpFallbackRedirectUrl = { signUpFallbackRedirectUrl?: string | null; }; +/** @document */ export type SignInFallbackRedirectUrl = { /** * The fallback URL to redirect to after the user signs in, if there's no `redirect_url` in the path already. It's recommended to use the [environment variable](https://clerk.com/docs/guides/development/clerk-environment-variables#sign-in-and-sign-up-redirects) instead. @@ -114,9 +118,10 @@ export type SignInFallbackRedirectUrl = { signInFallbackRedirectUrl?: string | null; }; +/** @document */ export type SignInForceRedirectUrl = { /** - * This URL will always be redirected to after the user signs in. It's recommended to use the [environment variable](https://clerk.com/docs/guides/development/clerk-environment-variables#sign-in-and-sign-up-redirects) instead. + * If provided, this URL will always be redirected to after the user signs in. It's recommended to use the [environment variable](https://clerk.com/docs/guides/development/clerk-environment-variables#sign-in-and-sign-up-redirects) instead. */ signInForceRedirectUrl?: string | null; }; diff --git a/packages/shared/src/types/telemetry.ts b/packages/shared/src/types/telemetry.ts index bc518207818..14643c3ea65 100644 --- a/packages/shared/src/types/telemetry.ts +++ b/packages/shared/src/types/telemetry.ts @@ -57,6 +57,9 @@ export interface TelemetryLogEntry { readonly userId?: string; } +/** + * @inline + */ export interface TelemetryCollector { isEnabled: boolean; isDebug: boolean; diff --git a/packages/shared/src/types/waitlist.ts b/packages/shared/src/types/waitlist.ts index 52f16a427b6..74540af1692 100644 --- a/packages/shared/src/types/waitlist.ts +++ b/packages/shared/src/types/waitlist.ts @@ -23,6 +23,10 @@ export interface WaitlistResource extends ClerkResource { join: (params: JoinWaitlistParams) => Promise<{ error: ClerkError | null }>; } +/** @document */ export type JoinWaitlistParams = { + /** + * The email address of the user to add to the waitlist. + */ emailAddress: string; }; diff --git a/packages/ui/src/internal/appearance.ts b/packages/ui/src/internal/appearance.ts index cf4b8ac09d6..0a4d0197b2c 100644 --- a/packages/ui/src/internal/appearance.ts +++ b/packages/ui/src/internal/appearance.ts @@ -837,7 +837,7 @@ export type BaseTheme = (BaseThemeTaggedType | 'clerk' | 'simple') & { cssLayerN export type Theme = { /** * A theme used as the base theme for the components. - * For further customisation, you can use the {@link Theme.options}, {@link Theme.variables} and {@link Theme.elements} props. + * For further customization, you can use the {@link Theme.options}, {@link Theme.variables} and {@link Theme.elements} props. * * Supports both object-based themes and string-based themes: * diff --git a/turbo.json b/turbo.json index b595ebd4da0..3bc444f71a6 100644 --- a/turbo.json +++ b/turbo.json @@ -342,7 +342,13 @@ }, "//#typedoc:generate": { "dependsOn": ["@clerk/nextjs#build", "@clerk/react#build", "@clerk/shared#build"], - "inputs": ["tsconfig.typedoc.json", "typedoc.config.mjs"], + "inputs": [ + "tsconfig.typedoc.json", + "typedoc.config.mjs", + ".typedoc/reference-objects.mjs", + ".typedoc/extract-methods.mjs", + ".typedoc/extract-returns-and-params.mjs" + ], "outputs": [".typedoc/**"], "outputLogs": "new-only" },