From 4a447b8f10a2e038f936c3fee3f3e5aa9754e029 Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:35:08 -0700 Subject: [PATCH 01/13] Drop functions from property tables; add JSDoc comments --- .typedoc/custom-plugin.mjs | 5 ++ .typedoc/custom-theme.mjs | 119 ++++++++++++++++++++++++- packages/shared/src/types/apiKeys.ts | 48 ++++++++++ packages/shared/src/types/clerk.ts | 96 +++++++++++++++----- packages/shared/src/types/instance.ts | 3 + packages/shared/src/types/telemetry.ts | 3 + 6 files changed, 252 insertions(+), 22 deletions(-) diff --git a/.typedoc/custom-plugin.mjs b/.typedoc/custom-plugin.mjs index d2f31f4270e..a4e5cbbb96f 100644 --- a/.typedoc/custom-plugin.mjs +++ b/.typedoc/custom-plugin.mjs @@ -99,6 +99,7 @@ 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'], ]; /** @@ -127,6 +128,10 @@ function getRelativeLinkReplacements() { function getCatchAllReplacements() { return [ + { + pattern: /(?/g, replace: '[`Appearance`](/docs/guides/customizing-clerk/appearance-prop/overview)', diff --git a/.typedoc/custom-theme.mjs b/.typedoc/custom-theme.mjs index ec1c32fa2dd..68302fb4349 100644 --- a/.typedoc/custom-theme.mjs +++ b/.typedoc/custom-theme.mjs @@ -1,5 +1,5 @@ // @ts-check -import { ReflectionKind, ReflectionType, UnionType } from 'typedoc'; +import { IntersectionType, ReferenceType, ReflectionKind, ReflectionType, UnionType } from 'typedoc'; import { MarkdownTheme, MarkdownThemeContext } from 'typedoc-plugin-markdown'; /** @@ -48,6 +48,21 @@ class ClerkMarkdownThemeContext extends MarkdownThemeContext { this.partials = { ...superPartials, + /** + * Drop function-valued interface/class properties from the properties table (they remain TypeScript "Property" + * members because they use property syntax). Keeps data fields and object namespaces in the table. + */ + /** + * @param {import('typedoc').DeclarationReflection[]} model + * @param {Parameters[1]} [options] + */ + propertiesTable: (model, options) => { + if (!Array.isArray(model)) { + return superPartials.propertiesTable(/** @type {any} */ (model), options); + } + const filtered = model.filter(prop => !isCallableInterfaceProperty(prop, this.helpers)); + 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 @@ -637,3 +652,105 @@ 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; +} 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..bb7584f5125 100644 --- a/packages/shared/src/types/clerk.ts +++ b/packages/shared/src/types/clerk.ts @@ -214,27 +214,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 +245,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,17 +294,26 @@ 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; /** @@ -329,11 +341,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 +355,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 +369,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 +383,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 +403,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 +687,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 +695,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; @@ -779,7 +804,7 @@ export interface Clerk { setActive: SetActive; /** - * Function used to commit a navigation after certain steps in the Clerk processes. + * A function used to commit a navigation after certain steps in the Clerk processes. */ navigate: CustomNavigation; @@ -1019,11 +1044,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; @@ -1088,6 +1115,9 @@ export type HandleOAuthCallbackParams = TransferableOption & export type HandleSamlCallbackParams = HandleOAuthCallbackParams; +/** + * A function used to navigate to a given URL after certain steps in the Clerk processes. + */ export type CustomNavigation = (to: string, options?: NavigateOptions) => Promise | void; export type ClerkThemeOptions = DeepSnakeToCamel>; @@ -2118,19 +2148,43 @@ export type APIKeysProps = { }; 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; }>; 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; }; export type RevokeAPIKeyParams = { + /** + * The ID of the API key to revoke. + */ apiKeyID: string; + /** + * The reason for revoking the API key. + */ revocationReason?: string; }; 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/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; From 91009769986f874cc9704e53bef62349faad783b Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:29:29 -0700 Subject: [PATCH 02/13] undo changes about removing functions from property tables --- .typedoc/custom-theme.mjs | 119 +------------------------------------- 1 file changed, 1 insertion(+), 118 deletions(-) diff --git a/.typedoc/custom-theme.mjs b/.typedoc/custom-theme.mjs index 68302fb4349..ec1c32fa2dd 100644 --- a/.typedoc/custom-theme.mjs +++ b/.typedoc/custom-theme.mjs @@ -1,5 +1,5 @@ // @ts-check -import { IntersectionType, ReferenceType, ReflectionKind, ReflectionType, UnionType } from 'typedoc'; +import { ReflectionKind, ReflectionType, UnionType } from 'typedoc'; import { MarkdownTheme, MarkdownThemeContext } from 'typedoc-plugin-markdown'; /** @@ -48,21 +48,6 @@ class ClerkMarkdownThemeContext extends MarkdownThemeContext { this.partials = { ...superPartials, - /** - * Drop function-valued interface/class properties from the properties table (they remain TypeScript "Property" - * members because they use property syntax). Keeps data fields and object namespaces in the table. - */ - /** - * @param {import('typedoc').DeclarationReflection[]} model - * @param {Parameters[1]} [options] - */ - propertiesTable: (model, options) => { - if (!Array.isArray(model)) { - return superPartials.propertiesTable(/** @type {any} */ (model), options); - } - const filtered = model.filter(prop => !isCallableInterfaceProperty(prop, this.helpers)); - 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 @@ -652,105 +637,3 @@ 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; -} From ea3d2eb6f20f83cfc4ac8090380dbcb27aa6b6ab Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:34:22 -0700 Subject: [PATCH 03/13] Revert "undo changes about removing functions from property tables" This reverts commit 91009769986f874cc9704e53bef62349faad783b. --- .typedoc/custom-theme.mjs | 119 +++++++++++++++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 1 deletion(-) diff --git a/.typedoc/custom-theme.mjs b/.typedoc/custom-theme.mjs index ec1c32fa2dd..68302fb4349 100644 --- a/.typedoc/custom-theme.mjs +++ b/.typedoc/custom-theme.mjs @@ -1,5 +1,5 @@ // @ts-check -import { ReflectionKind, ReflectionType, UnionType } from 'typedoc'; +import { IntersectionType, ReferenceType, ReflectionKind, ReflectionType, UnionType } from 'typedoc'; import { MarkdownTheme, MarkdownThemeContext } from 'typedoc-plugin-markdown'; /** @@ -48,6 +48,21 @@ class ClerkMarkdownThemeContext extends MarkdownThemeContext { this.partials = { ...superPartials, + /** + * Drop function-valued interface/class properties from the properties table (they remain TypeScript "Property" + * members because they use property syntax). Keeps data fields and object namespaces in the table. + */ + /** + * @param {import('typedoc').DeclarationReflection[]} model + * @param {Parameters[1]} [options] + */ + propertiesTable: (model, options) => { + if (!Array.isArray(model)) { + return superPartials.propertiesTable(/** @type {any} */ (model), options); + } + const filtered = model.filter(prop => !isCallableInterfaceProperty(prop, this.helpers)); + 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 @@ -637,3 +652,105 @@ 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; +} From 0292f5fec0b4be3de8a439d0cc9aac10372124fc Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:41:36 -0700 Subject: [PATCH 04/13] only remove functions from property tables if output page listed in allowlist --- .typedoc/custom-theme.mjs | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/.typedoc/custom-theme.mjs b/.typedoc/custom-theme.mjs index 68302fb4349..779dce24a3c 100644 --- a/.typedoc/custom-theme.mjs +++ b/.typedoc/custom-theme.mjs @@ -29,6 +29,28 @@ class ClerkMarkdownTheme extends MarkdownTheme { */ const unionCommentMap = new Map(); +/** + * Only for these output pages do we remove function-valued members from **property** tables. + * Match {@link import('typedoc-plugin-markdown').MarkdownPageEvent#url} as TypeDoc emits it (relative to `out`), + * e.g. `shared/clerk.mdx` — not the path after `cpy` to `.typedoc/docs` (that does not change `page.url`). + */ +const PROPERTY_TABLE_EXCLUDE_FUNCTIONS_ALLOWLIST = ['shared/clerk.mdx']; + +/** + * @param {string | undefined} pageUrl + * @param {readonly string[]} allowlist + */ +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 @@ -49,10 +71,9 @@ class ClerkMarkdownThemeContext extends MarkdownThemeContext { this.partials = { ...superPartials, /** - * Drop function-valued interface/class properties from the properties table (they remain TypeScript "Property" - * members because they use property syntax). Keeps data fields and object namespaces in the table. - */ - /** + * 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] */ @@ -60,7 +81,11 @@ class ClerkMarkdownThemeContext extends MarkdownThemeContext { if (!Array.isArray(model)) { return superPartials.propertiesTable(/** @type {any} */ (model), options); } - const filtered = model.filter(prop => !isCallableInterfaceProperty(prop, this.helpers)); + const allowlisted = pageMatchesPropertyTableFunctionFilterAllowlist( + this.page?.url, + PROPERTY_TABLE_EXCLUDE_FUNCTIONS_ALLOWLIST, + ); + const filtered = allowlisted ? model.filter(prop => !isCallableInterfaceProperty(prop, this.helpers)) : model; return superPartials.propertiesTable(filtered, options); }, /** From 8b9b1d443d596a6a3063f5fc771942ac4f963d7f Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:55:25 -0700 Subject: [PATCH 05/13] begin building extract-methods.mjs --- .typedoc/custom-plugin.mjs | 4 + .typedoc/custom-theme.mjs | 22 +- .typedoc/extract-methods.mjs | 578 +++++++++++++++++++++++ .typedoc/reference-objects.mjs | 15 + package.json | 2 +- packages/clerk-js/src/core/clerk.ts | 8 + packages/shared/src/types/clerk.ts | 373 +++++++++++---- packages/shared/src/types/client.ts | 81 +++- packages/shared/src/types/multiDomain.ts | 7 +- packages/shared/src/types/redirects.ts | 6 +- packages/shared/src/types/waitlist.ts | 3 + turbo.json | 8 +- 12 files changed, 1006 insertions(+), 101 deletions(-) create mode 100644 .typedoc/extract-methods.mjs create mode 100644 .typedoc/reference-objects.mjs diff --git a/.typedoc/custom-plugin.mjs b/.typedoc/custom-plugin.mjs index a4e5cbbb96f..90437859175 100644 --- a/.typedoc/custom-plugin.mjs +++ b/.typedoc/custom-plugin.mjs @@ -219,6 +219,10 @@ function getCatchAllReplacements() { replace: '[`OrganizationMembershipPublicMetadata`](/docs/reference/types/metadata#organization-membership-public-metadata)', }, + { + pattern: /`RedirectOptions`/g, + replace: '[`RedirectOptions`](/docs/reference/types/redirect-options)', + }, { pattern: /(? !isCallableInterfaceProperty(prop, this.helpers)) : model; return superPartials.propertiesTable(filtered, options); }, diff --git a/.typedoc/extract-methods.mjs b/.typedoc/extract-methods.mjs new file mode 100644 index 00000000000..deff922d20c --- /dev/null +++ b/.typedoc/extract-methods.mjs @@ -0,0 +1,578 @@ +// @ts-check +/** + * For each entry in REFERENCE_OBJECTS_LIST, finds callable members on the mapped interface/class via TypeDoc + * and writes one .mdx per method (kebab file name) next to the main reference page output. + * + * Run after `typedoc` (same cwd as repo root). Uses a second TypeDoc convert pass to read reflections. + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { Application, Comment, ReflectionKind } from 'typedoc'; + +import typedocConfig from '../typedoc.config.mjs'; +import { REFERENCE_OBJECTS_LIST, REFERENCE_OBJECT_PAGE_SYMBOLS } from './reference-objects.mjs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +/** + * 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]; + } + return undefined; +} + +/** + * @param {import('typedoc').DeclarationReflection} decl + */ +function isCallableMember(decl) { + if (decl.kind === ReflectionKind.Method) { + return true; + } + if (decl.kind === ReflectionKind.Property) { + return !!getPrimaryCallSignature(decl); + } + return false; +} + +/** + * @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 + */ +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); +} + +/** + * @param {import('typedoc').SignatureReflection} sig + * @param {import('typedoc').ParameterReflection} param + * @param {import('typedoc').DeclarationReflection} decl + */ +function getParamDescription(sig, param, decl) { + if (param.comment?.summary?.length) { + return displayPartsToString(param.comment.summary).trim(); + } + const tag = + sig.comment?.getIdentifiedTag(param.name, '@param') ?? decl.comment?.getIdentifiedTag(param.name, '@param'); + if (tag?.content?.length) { + return Comment.combineDisplayParts(tag.content).trim(); + } + return ''; +} + +/** + * 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 + * @param {boolean} parentOptional + */ +function formatNestedParamNameColumn(baseName, pathSegments, parentOptional) { + const chain = pathSegments.join('.'); + const inner = parentOptional ? `${baseName}?.${chain}` : `${baseName}.${chain}`; + return `\`${inner}\``; +} + +/** + * 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[]} + */ +function nestedParameterRowsFromDocumentedProperties(param) { + 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 nestedTypeRaw = child.type?.toString(); + const nestedTypeStr = nestedTypeRaw ? `\`${nestedTypeRaw.replace(/\|/g, '\\|')}\`` : '`unknown`'; + const nestedNameCol = formatNestedParamNameColumn(param.name, [child.name], param.flags.isOptional); + const nestedDesc = displayPartsToString(summary).trim() || '—'; + rows.push(`| ${nestedNameCol} | ${nestedTypeStr} | ${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)) { + const holder = resolveDeclarationWithObjectMembers(typeDecl.type); + 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); +} + +/** + * @param {import('typedoc').DeclarationReflection} holder + * @returns {string[]} + */ +function propertyTableRowsForDeclaration(holder) { + const props = (holder.children ?? []).filter(c => c.kindOf(ReflectionKind.Property)); + props.sort((a, b) => a.name.localeCompare(b.name)); + /** @type {string[]} */ + const rows = []; + for (const child of props) { + const opt = child.flags.isOptional ? '?' : ''; + const nameCol = `\`${child.name}${opt}\``; + const nestedTypeRaw = child.type?.toString(); + const nestedTypeStr = nestedTypeRaw ? `\`${nestedTypeRaw.replace(/\|/g, '\\|')}\`` : '`unknown`'; + const nestedDesc = child.comment?.summary?.length ? displayPartsToString(child.comment.summary).trim() : '—'; + rows.push(`| ${nameCol} | ${nestedTypeStr} | ${nestedDesc} |`); + } + return rows; +} + +/** + * 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). + * + * @param {import('typedoc').SignatureReflection} sig + * @returns {string | undefined} + */ +function trySingleNominalParameterTypeSection(sig) { + 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 rows = propertyTableRowsForDeclaration(nominal.holder); + if (rows.length === 0) { + return undefined; + } + return [`#### ${nominal.sectionTitle}`, '', '| Name | Type | Description |', '| --- | --- | --- |', ...rows, ''].join( + '\n', + ); +} + +/** + * @param {import('typedoc').SignatureReflection} sig + * @param {import('typedoc').DeclarationReflection} decl + */ +function parametersMarkdownTable(sig, decl) { + const params = sig.parameters ?? []; + if (params.length === 0) { + return ''; + } + + const singleNominal = trySingleNominalParameterTypeSection(sig); + if (singleNominal) { + return singleNominal; + } + + /** @type {string[]} */ + const rows = []; + for (const p of params) { + const typeStr = p.type ? `\`${p.type.toString().replace(/\|/g, '\\|')}\`` : '`unknown`'; + const opt = p.flags.isOptional ? '?' : ''; + const nameCol = `\`${p.name}${opt}\``; + const desc = getParamDescription(sig, p, decl); + rows.push(`| ${nameCol} | ${typeStr} | ${desc || '—'} |`); + rows.push(...nestedParameterRowsFromDocumentedProperties(p)); + } + + return ['#### Parameters', '', '| Name | Type | Description |', '| --- | --- | --- |', ...rows, ''].join('\n'); +} + +/** + * @param {import('typedoc').DeclarationReflection} decl + */ +function buildMethodMdx(decl) { + 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, decl); + + const parts = [title, '', description, '', ts, '']; + if (paramsMd) { + parts.push(paramsMd); + } + return parts.join('\n').trim() + '\n'; +} + +/** + * @param {string} pageUrl + * @param {import('typedoc').ProjectReflection} project + */ +function extractMethodsForPage(pageUrl, project) { + 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; + } + + const outDir = path.join(__dirname, 'temp-docs', path.dirname(pageUrl), `${path.basename(pageUrl, '.mdx')}-methods`); + fs.mkdirSync(outDir, { recursive: true }); + + let count = 0; + for (const child of decl.children) { + if (child.name.startsWith('__')) { + continue; + } + if (!isCallableMember(/** @type {import('typedoc').DeclarationReflection} */ (child))) { + continue; + } + const mdx = buildMethodMdx(/** @type {import('typedoc').DeclarationReflection} */ (child)); + 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); + } + + let total = 0; + for (const pageUrl of REFERENCE_OBJECTS_LIST) { + total += extractMethodsForPage(pageUrl, project); + } + console.log(`[extract-methods] Wrote ${total} method files total`); +} + +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/.typedoc/reference-objects.mjs b/.typedoc/reference-objects.mjs new file mode 100644 index 00000000000..db12e70bbb7 --- /dev/null +++ b/.typedoc/reference-objects.mjs @@ -0,0 +1,15 @@ +// @ts-check +/** + * Shared between the markdown theme and extract-methods.mjs. + * `page.url` values are relative to TypeDoc `out` (e.g. `.typedoc/temp-docs`). + */ + +export const REFERENCE_OBJECTS_LIST = ['shared/clerk.mdx', 'shared/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.mdx': 'Clerk', + 'shared/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/clerk.ts b/packages/shared/src/types/clerk.ts index bb7584f5125..ba992ec780b 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; @@ -755,27 +764,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; @@ -783,7 +794,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; @@ -796,7 +807,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. @@ -804,148 +815,167 @@ export interface Clerk { setActive: SetActive; /** - * A 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 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 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. */ 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. * - * @param to + * 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 - 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. + * + * 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. */ 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. * - * @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. */ 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()`](#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, @@ -954,8 +984,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, @@ -963,7 +995,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 {@link Clerk.client.signIn.createEmailLinkFlow} or {@link 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, @@ -971,32 +1005,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; @@ -1008,20 +1042,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; /** @@ -1063,44 +1105,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; /** @@ -1108,7 +1148,7 @@ 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; }; @@ -1117,6 +1157,12 @@ export type HandleSamlCallbackParams = HandleOAuthCallbackParams; /** * A function used to navigate to a given URL after certain steps in the Clerk processes. + * @inline + * @param to - The URL or relative path to navigate to. + * @param options - Optional configuration. `NavigateOptions` is an object that accepts the following properties: + * + * - `replace?` (boolean): If `true`, replace the current history entry instead of pushing a new one. + * - `metadata?` (RouterMetadata): Optional router metadata. */ export type CustomNavigation = (to: string, options?: NavigateOptions) => Promise | void; @@ -1320,6 +1366,9 @@ export interface NavigateOptions { metadata?: RouterMetadata; } +/** + * @inline + */ export interface Resources { client: ClientResource; session?: SignedInSessionResource | null; @@ -1476,6 +1525,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. @@ -1544,8 +1594,7 @@ export type SignInProps = RoutingOptions & { 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 */ @@ -1627,6 +1676,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. @@ -1652,6 +1702,7 @@ export type GoogleOneTapProps = GoogleOneTapRedirectUrlProps & { appearance?: ClerkAppearanceTheme; }; +/** @document */ export type SignUpProps = RoutingOptions & { /** * Full URL or path to navigate to after successful sign up. @@ -1718,6 +1769,7 @@ export type SignUpModalProps = WithoutRouting & { getContainer?: () => HTMLElement | null; }; +/** @document */ export type UserProfileProps = RoutingOptions & { /** * Customisation options to fully match the Clerk components to your own brand. @@ -1767,6 +1819,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. @@ -1817,6 +1870,7 @@ export type OrganizationProfileModalProps = WithoutRouting HTMLElement | null; }; +/** @document */ export type CreateOrganizationProps = RoutingOptions & { /** * Full URL or path to navigate to after creating a new Organization. @@ -1841,6 +1895,7 @@ export type CreateOrganizationProps = RoutingOptions & { appearance?: ClerkAppearanceTheme; }; +/** @document */ export type CreateOrganizationModalProps = WithoutRouting & { /** * Function that returns the container element where portals should be rendered. @@ -1850,7 +1905,10 @@ export type CreateOrganizationModalProps = WithoutRouting HTMLElement | null; }; +/** @inline */ type UserProfileMode = 'modal' | 'navigation'; + +/** @document */ type UserButtonProfileMode = | { userProfileUrl?: never; @@ -1929,6 +1987,7 @@ type CreateOrganizationMode = | { createOrganizationUrl: string; createOrganizationMode?: 'navigation' } | { createOrganizationUrl?: never; createOrganizationMode?: 'modal' }; +/** @document */ export type OrganizationSwitcherProps = CreateOrganizationMode & OrganizationProfileMode & { /** @@ -2005,6 +2064,7 @@ export type OrganizationSwitcherProps = CreateOrganizationMode & organizationProfileProps?: Pick; }; +/** @document */ export type OrganizationListProps = { /** * Full URL or path to navigate to after creating a new Organization. @@ -2054,6 +2114,7 @@ export type OrganizationListProps = { afterSelectPersonalUrl?: ((user: UserResource) => string) | LooseExtractedParams>; }; +/** @document */ export type WaitlistProps = { /** * Full URL or path to navigate to after join waitlist. @@ -2071,6 +2132,7 @@ export type WaitlistProps = { signInUrl?: string; }; +/** @document */ export type WaitlistModalProps = WaitlistProps & { /** * Function that returns the container element where portals should be rendered. @@ -2080,6 +2142,7 @@ export type WaitlistModalProps = WaitlistProps & { getContainer?: () => HTMLElement | null; }; +/** @document */ type PricingTableDefaultProps = { /** * The position of the CTA button. @@ -2101,6 +2164,7 @@ type PricingTableDefaultProps = { newSubscriptionRedirectUrl?: string; }; +/** @document */ type PricingTableBaseProps = { /** * The subscriber type to display plans for. @@ -2124,8 +2188,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. @@ -2147,6 +2213,7 @@ 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. @@ -2158,6 +2225,7 @@ export type GetAPIKeysParams = ClerkPaginationParams<{ query?: string; }>; +/** @document */ export type CreateAPIKeyParams = { /** * The name of the API key. @@ -2177,6 +2245,7 @@ export type CreateAPIKeyParams = { description?: string; }; +/** @document */ export type RevokeAPIKeyParams = { /** * The ID of the API key to revoke. @@ -2348,20 +2417,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; } @@ -2404,37 +2471,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; }; @@ -2452,60 +2545,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; } @@ -2518,6 +2726,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/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..5bcd3ea7f3c 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. @@ -91,7 +91,7 @@ export type RedirectUrlProp = { 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; }; @@ -116,7 +116,7 @@ export type SignInFallbackRedirectUrl = { 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/waitlist.ts b/packages/shared/src/types/waitlist.ts index 52f16a427b6..0a88d19eaaf 100644 --- a/packages/shared/src/types/waitlist.ts +++ b/packages/shared/src/types/waitlist.ts @@ -24,5 +24,8 @@ export interface WaitlistResource extends ClerkResource { } export type JoinWaitlistParams = { + /** + * The email address of the user to add to the waitlist. + */ emailAddress: string; }; 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" }, From ac8f1331ad6fae920e7017e6955ec47935337c99 Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:58:49 -0700 Subject: [PATCH 06/13] use custom-plugin in extract-methods for link replacements --- .typedoc/custom-plugin.mjs | 27 ++++++++++++++++++++------- .typedoc/extract-methods.mjs | 12 ++++++++---- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/.typedoc/custom-plugin.mjs b/.typedoc/custom-plugin.mjs index 90437859175..83e1ab6d8eb 100644 --- a/.typedoc/custom-plugin.mjs +++ b/.typedoc/custom-plugin.mjs @@ -260,6 +260,24 @@ function getCatchAllReplacements() { ]; } +/** + * Same replacement pass as the catch-all loop in `MarkdownPageEvent.END` (after relative links). + * Used by `extract-methods.mjs`, which writes MDX outside TypeDoc and never hits that hook. + * + * @param {string} contents + */ +export function applyCatchAllMdReplacements(contents) { + if (!contents) { + return contents; + } + let out = contents; + for (const { pattern, replace } of getCatchAllReplacements()) { + // @ts-ignore — string | function + out = out.replace(pattern, replace); + } + return out; +} + /** * @param {import('typedoc-plugin-markdown').MarkdownApplication} app */ @@ -274,13 +292,8 @@ export function load(app) { } } - 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/extract-methods.mjs b/.typedoc/extract-methods.mjs index deff922d20c..55ecec63388 100644 --- a/.typedoc/extract-methods.mjs +++ b/.typedoc/extract-methods.mjs @@ -11,6 +11,7 @@ import { fileURLToPath } from 'node:url'; import { Application, Comment, ReflectionKind } from 'typedoc'; import typedocConfig from '../typedoc.config.mjs'; +import { applyCatchAllMdReplacements } from './custom-plugin.mjs'; import { REFERENCE_OBJECTS_LIST, REFERENCE_OBJECT_PAGE_SYMBOLS } from './reference-objects.mjs'; const __filename = fileURLToPath(import.meta.url); @@ -501,11 +502,14 @@ function buildMethodMdx(decl) { const ts = ['```typescript', formatTypeScriptSignature(sig, name), '```'].join('\n'); const paramsMd = parametersMarkdownTable(sig, decl); - const parts = [title, '', description, '', ts, '']; - if (paramsMd) { - parts.push(paramsMd); + // Same catch-all pass as `custom-plugin.mjs` — not run automatically because this MDX bypasses TypeDoc's renderer. Skip the ```typescript``` fence so signatures stay plain code. + const head = applyCatchAllMdReplacements([title, '', description].join('\n')); + const paramsProcessed = paramsMd ? applyCatchAllMdReplacements(paramsMd) : ''; + const chunks = [head, ts]; + if (paramsProcessed) { + chunks.push(paramsProcessed); } - return parts.join('\n').trim() + '\n'; + return chunks.join('\n\n').trim() + '\n'; } /** From 9139e2725ab481ed1f51ed5b1269a645d04cf92c Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Thu, 9 Apr 2026 19:08:55 -0700 Subject: [PATCH 07/13] catchall link replacements should never replace text in headings --- .typedoc/custom-plugin.mjs | 85 +++++++++++++++++--------- packages/shared/src/types/clerk.ts | 53 ++++++++-------- packages/shared/src/types/waitlist.ts | 1 + packages/ui/src/internal/appearance.ts | 2 +- 4 files changed, 87 insertions(+), 54 deletions(-) diff --git a/.typedoc/custom-plugin.mjs b/.typedoc/custom-plugin.mjs index 83e1ab6d8eb..763245ab221 100644 --- a/.typedoc/custom-plugin.mjs +++ b/.typedoc/custom-plugin.mjs @@ -129,102 +129,115 @@ function getRelativeLinkReplacements() { 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 (ATX_HEADING_LINE.test(line.replace(/\r$/, ''))) { + return line; + } + let out = line; + for (const { pattern, replace } of getCatchAllReplacements()) { + // @ts-ignore — string | function + out = out.replace(pattern, replace); + } + return out; + }) + .join('\n'); } /** diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts index ba992ec780b..1799b8abc12 100644 --- a/packages/shared/src/types/clerk.ts +++ b/packages/shared/src/types/clerk.ts @@ -891,6 +891,9 @@ export interface Clerk { /** * 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; @@ -930,7 +933,7 @@ export interface Clerk { redirectToUserProfile: () => Promise; /** - * Redirects to the configured URL where [``](https://clerk.com/docs/reference/components/organization/organization-profile) is mounted. This method uses the [`navigate()`](#navigate) method under the hood. + * 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. */ @@ -1157,12 +1160,11 @@ export type HandleSamlCallbackParams = HandleOAuthCallbackParams; /** * A function used to navigate to a given URL after certain steps in the Clerk processes. - * @inline - * @param to - The URL or relative path to navigate to. - * @param options - Optional configuration. `NavigateOptions` is an object that accepts the following properties: * - * - `replace?` (boolean): If `true`, replace the current history entry instead of pushing a new one. - * - `metadata?` (RouterMetadata): Optional router metadata. + * @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; @@ -1361,8 +1363,15 @@ 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; } @@ -1484,13 +1493,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. * * @example * ```typescript @@ -1553,7 +1558,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) */ @@ -1636,7 +1641,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) */ @@ -1725,7 +1730,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) */ @@ -1772,7 +1777,7 @@ export type SignUpModalProps = WithoutRouting & { /** @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) */ @@ -1828,7 +1833,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) */ @@ -1888,7 +1893,7 @@ 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) */ @@ -1950,7 +1955,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) */ @@ -2052,7 +2057,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) */ @@ -2084,7 +2089,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) */ @@ -2121,7 +2126,7 @@ export type WaitlistProps = { */ 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) */ @@ -2174,7 +2179,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) */ @@ -2200,7 +2205,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) */ diff --git a/packages/shared/src/types/waitlist.ts b/packages/shared/src/types/waitlist.ts index 0a88d19eaaf..74540af1692 100644 --- a/packages/shared/src/types/waitlist.ts +++ b/packages/shared/src/types/waitlist.ts @@ -23,6 +23,7 @@ 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. 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: * From 2978152f8fbe614dba0c04907b3d48e706149a11 Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Thu, 9 Apr 2026 19:22:31 -0700 Subject: [PATCH 08/13] protectPipeDelimitedInlineCodeSpans; handle if members live on alias with no typeDecl.type --- .typedoc/custom-plugin.mjs | 41 ++++++++++++++++++++++++++++-- .typedoc/extract-methods.mjs | 8 +++++- packages/shared/src/types/clerk.ts | 17 +++++++------ 3 files changed, 55 insertions(+), 11 deletions(-) diff --git a/.typedoc/custom-plugin.mjs b/.typedoc/custom-plugin.mjs index 763245ab221..744c96814fa 100644 --- a/.typedoc/custom-plugin.mjs +++ b/.typedoc/custom-plugin.mjs @@ -276,6 +276,39 @@ function getCatchAllReplacements() { /** CommonMark ATX heading: optional indent, 1–6 `#`, then space or end — entire line is left unchanged. */ const ATX_HEADING_LINE = /^\s{0,3}#{1,6}(?:\s|$)/; +/** Private-use placeholders — must not appear in real MDX and must not match catch-all patterns. */ +const PIPE_CODE_PH = /\uE000(\d+)\uE001/g; + +/** + * Inline code that contains a pipe (e.g. `` `a \\| b` `` or `` `a | b` ``) cannot receive per-token + * link replacements without breaking MDX. Replace those whole spans with placeholders, run catch-alls, + * then restore. + * + * @param {string} line + * @returns {{ text: string, placeholders: string[] }} + */ +function protectPipeDelimitedInlineCodeSpans(line) { + /** @type {string[]} */ + const placeholders = []; + const text = line.replace(/`([^`\n]*)`/g, (full, inner) => { + if (!inner.includes('|')) { + return full; + } + const id = placeholders.length; + placeholders.push(full); + return `\uE000${id}\uE001`; + }); + return { text, placeholders }; +} + +/** + * @param {string} text + * @param {string[]} placeholders + */ +function restoreProtectedInlineCodeSpans(text, placeholders) { + return text.replace(PIPE_CODE_PH, (_, /** @type {string} */ i) => placeholders[Number(i)] ?? ''); +} + /** * Same replacement pass as the catch-all loop in `MarkdownPageEvent.END` (after relative links). * Used by `extract-methods.mjs`, which writes MDX outside TypeDoc and never hits that hook. @@ -283,6 +316,9 @@ const ATX_HEADING_LINE = /^\s{0,3}#{1,6}(?:\s|$)/; * Skips ATX heading lines (`#` … `######`) so titles like `#### SetActiveParams` are never linkified. * (A lone `(? 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; }; @@ -326,11 +331,7 @@ export interface Clerk { __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; From adb43df69a972d9d481671ae2f3425e14a312f00 Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Thu, 9 Apr 2026 19:33:11 -0700 Subject: [PATCH 09/13] add client resource to link replacements --- .typedoc/custom-plugin.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/.typedoc/custom-plugin.mjs b/.typedoc/custom-plugin.mjs index 744c96814fa..a21550e4840 100644 --- a/.typedoc/custom-plugin.mjs +++ b/.typedoc/custom-plugin.mjs @@ -100,6 +100,7 @@ const LINK_REPLACEMENTS = [ ['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'], ]; /** From a0a9731e546d70f7299cb1ce855d7ae2505671a9 Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:42:25 -0700 Subject: [PATCH 10/13] refactor extract-methods to use existing plugins to create tables,etc --- .typedoc/custom-plugin.mjs | 38 ++- .typedoc/extract-methods.mjs | 350 +++++++++++++++++++------ .typedoc/prepare-markdown-renderer.mjs | 118 +++++++++ packages/shared/src/types/clerk.ts | 13 +- packages/shared/src/types/redirects.ts | 5 + 5 files changed, 434 insertions(+), 90 deletions(-) create mode 100644 .typedoc/prepare-markdown-renderer.mjs diff --git a/.typedoc/custom-plugin.mjs b/.typedoc/custom-plugin.mjs index a21550e4840..b0884c5ad8a 100644 --- a/.typedoc/custom-plugin.mjs +++ b/.typedoc/custom-plugin.mjs @@ -101,6 +101,8 @@ const LINK_REPLACEMENTS = [ ['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-call-back-params', '/docs/reference/types/handle-o-auth-call-back-params'], ]; /** @@ -127,6 +129,24 @@ 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 [ { @@ -180,7 +200,10 @@ function getCatchAllReplacements() { pattern: /(? { 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); } if (output.contents) { diff --git a/.typedoc/extract-methods.mjs b/.typedoc/extract-methods.mjs index d57dea75491..8da026b81d2 100644 --- a/.typedoc/extract-methods.mjs +++ b/.typedoc/extract-methods.mjs @@ -4,19 +4,185 @@ * and writes one .mdx per method (kebab file name) next to the main reference page output. * * 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, ReflectionKind } from 'typedoc'; +import { Application, Comment, PageKind, ReflectionKind } from 'typedoc'; +import { MarkdownPageEvent, MarkdownTheme } from 'typedoc-plugin-markdown'; import typedocConfig from '../typedoc.config.mjs'; -import { applyCatchAllMdReplacements } from './custom-plugin.mjs'; +import { applyCatchAllMdReplacements, applyRelativeLinkReplacements } 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.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. @@ -177,6 +343,55 @@ function formatReturnsLineFromTag(tag) { 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 */ @@ -214,23 +429,6 @@ function appendSignatureOnlyReturns(declComment, sigComment) { return formatReturnsLineFromTag(tag); } -/** - * @param {import('typedoc').SignatureReflection} sig - * @param {import('typedoc').ParameterReflection} param - * @param {import('typedoc').DeclarationReflection} decl - */ -function getParamDescription(sig, param, decl) { - if (param.comment?.summary?.length) { - return displayPartsToString(param.comment.summary).trim(); - } - const tag = - sig.comment?.getIdentifiedTag(param.name, '@param') ?? decl.comment?.getIdentifiedTag(param.name, '@param'); - if (tag?.content?.length) { - return Comment.combineDisplayParts(tag.content).trim(); - } - return ''; -} - /** * Object / type-literal declaration for a parameter type (reference, inlined reflection, intersection). * TypeDoc applies `@param parent.prop` descriptions onto property reflections under this declaration. @@ -278,12 +476,10 @@ function resolveDeclarationWithObjectMembers(t) { /** * @param {string} baseName * @param {string[]} pathSegments - * @param {boolean} parentOptional */ -function formatNestedParamNameColumn(baseName, pathSegments, parentOptional) { - const chain = pathSegments.join('.'); - const inner = parentOptional ? `${baseName}?.${chain}` : `${baseName}.${chain}`; - return `\`${inner}\``; +function formatNestedParamNameColumn(baseName, pathSegments) { + const pathChain = pathSegments.join('?.'); + return `\`${baseName}?.${pathChain}\``; } /** @@ -293,7 +489,20 @@ function formatNestedParamNameColumn(baseName, pathSegments, parentOptional) { * @param {import('typedoc').ParameterReflection} param * @returns {string[]} */ -function nestedParameterRowsFromDocumentedProperties(param) { +/** + * @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 []; @@ -307,11 +516,10 @@ function nestedParameterRowsFromDocumentedProperties(param) { if (!summary?.length) { continue; } - const nestedTypeRaw = child.type?.toString(); - const nestedTypeStr = nestedTypeRaw ? `\`${nestedTypeRaw.replace(/\|/g, '\\|')}\`` : '`unknown`'; - const nestedNameCol = formatNestedParamNameColumn(param.name, [child.name], param.flags.isOptional); + 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} | ${nestedTypeStr} | ${nestedDesc} |`); + rows.push(`| ${nestedNameCol} | ${typeCell} | ${nestedDesc} |`); } return rows; } @@ -406,34 +614,15 @@ function isNominalParamTypeDocumented(typeDecl, props) { return props.some(p => p.comment?.summary?.length); } -/** - * @param {import('typedoc').DeclarationReflection} holder - * @returns {string[]} - */ -function propertyTableRowsForDeclaration(holder) { - const props = (holder.children ?? []).filter(c => c.kindOf(ReflectionKind.Property)); - props.sort((a, b) => a.name.localeCompare(b.name)); - /** @type {string[]} */ - const rows = []; - for (const child of props) { - const opt = child.flags.isOptional ? '?' : ''; - const nameCol = `\`${child.name}${opt}\``; - const nestedTypeRaw = child.type?.toString(); - const nestedTypeStr = nestedTypeRaw ? `\`${nestedTypeRaw.replace(/\|/g, '\\|')}\`` : '`unknown`'; - const nestedDesc = child.comment?.summary?.length ? displayPartsToString(child.comment.summary).trim() : '—'; - rows.push(`| ${nameCol} | ${nestedTypeStr} | ${nestedDesc} |`); - } - return rows; -} - /** * 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). + * 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) { +function trySingleNominalParameterTypeSection(sig, ctx) { const params = sig.parameters ?? []; if (params.length !== 1) { return undefined; @@ -450,48 +639,53 @@ function trySingleNominalParameterTypeSection(sig) { if (!isNominalParamTypeDocumented(nominal.typeDecl, props)) { return undefined; } - const rows = propertyTableRowsForDeclaration(nominal.holder); - if (rows.length === 0) { + const tableMd = renderMemberTableOmittingExampleBlocks(props, ctx, () => + ctx.partials.propertiesTable(props, { + kind: nominal.typeDecl.kind, + isEventProps: false, + }), + ); + if (!tableMd?.trim()) { return undefined; } - return [`#### ${nominal.sectionTitle}`, '', '| Name | Type | Description |', '| --- | --- | --- |', ...rows, ''].join( - '\n', - ); + return [markdownHeading(4, nominal.sectionTitle), '', tableMd, ''].join('\n'); } /** * @param {import('typedoc').SignatureReflection} sig - * @param {import('typedoc').DeclarationReflection} decl + * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx */ -function parametersMarkdownTable(sig, decl) { +function parametersMarkdownTable(sig, ctx) { const params = sig.parameters ?? []; if (params.length === 0) { return ''; } - const singleNominal = trySingleNominalParameterTypeSection(sig); + const singleNominal = trySingleNominalParameterTypeSection(sig, ctx); if (singleNominal) { return singleNominal; } + let tableMd = renderMemberTableOmittingExampleBlocks(params, ctx, () => ctx.partials.parametersTable(params)); /** @type {string[]} */ - const rows = []; + const nested = []; for (const p of params) { - const typeStr = p.type ? `\`${p.type.toString().replace(/\|/g, '\\|')}\`` : '`unknown`'; - const opt = p.flags.isOptional ? '?' : ''; - const nameCol = `\`${p.name}${opt}\``; - const desc = getParamDescription(sig, p, decl); - rows.push(`| ${nameCol} | ${typeStr} | ${desc || '—'} |`); - rows.push(...nestedParameterRowsFromDocumentedProperties(p)); + nested.push(...nestedParameterRowsFromDocumentedProperties(p, ctx)); + } + if (nested.length) { + tableMd = appendMarkdownTableRows(tableMd, nested); } - return ['#### Parameters', '', '| Name | Type | Description |', '| --- | --- | --- |', ...rows, ''].join('\n'); + 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) { +function buildMethodMdx(decl, ctx) { const name = decl.name; const sig = getPrimaryCallSignature(decl); if (!sig) { @@ -506,11 +700,12 @@ function buildMethodMdx(decl) { description = [description, sigReturns].filter(Boolean).join('\n\n'); } const ts = ['```typescript', formatTypeScriptSignature(sig, name), '```'].join('\n'); - const paramsMd = parametersMarkdownTable(sig, decl); + const paramsMd = parametersMarkdownTable(sig, ctx); - // Same catch-all pass as `custom-plugin.mjs` — not run automatically because this MDX bypasses TypeDoc's renderer. Skip the ```typescript``` fence so signatures stay plain code. - const head = applyCatchAllMdReplacements([title, '', description].join('\n')); - const paramsProcessed = paramsMd ? applyCatchAllMdReplacements(paramsMd) : ''; + // 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); @@ -521,8 +716,9 @@ function buildMethodMdx(decl) { /** * @param {string} pageUrl * @param {import('typedoc').ProjectReflection} project + * @param {import('typedoc').Application} app */ -function extractMethodsForPage(pageUrl, project) { +function extractMethodsForPage(pageUrl, project, app) { const symbol = /** @type {Record} */ (/** @type {unknown} */ (REFERENCE_OBJECT_PAGE_SYMBOLS))[ pageUrl ]; @@ -538,6 +734,8 @@ function extractMethodsForPage(pageUrl, project) { return 0; } + const ctx = createThemeContextForReferencePage(app, project, pageUrl, decl); + const outDir = path.join(__dirname, 'temp-docs', path.dirname(pageUrl), `${path.basename(pageUrl, '.mdx')}-methods`); fs.mkdirSync(outDir, { recursive: true }); @@ -549,7 +747,7 @@ function extractMethodsForPage(pageUrl, project) { if (!isCallableMember(/** @type {import('typedoc').DeclarationReflection} */ (child))) { continue; } - const mdx = buildMethodMdx(/** @type {import('typedoc').DeclarationReflection} */ (child)); + const mdx = buildMethodMdx(/** @type {import('typedoc').DeclarationReflection} */ (child), ctx); if (!mdx) { continue; } @@ -575,9 +773,11 @@ async function main() { process.exit(1); } + prepareMarkdownRenderer(app, project); + let total = 0; for (const pageUrl of REFERENCE_OBJECTS_LIST) { - total += extractMethodsForPage(pageUrl, project); + total += extractMethodsForPage(pageUrl, project, app); } console.log(`[extract-methods] Wrote ${total} method files total`); } 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/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts index 13e770af47c..35a1c0b51e2 100644 --- a/packages/shared/src/types/clerk.ts +++ b/packages/shared/src/types/clerk.ts @@ -865,13 +865,15 @@ export interface Clerk { /** * Returns the configured `afterSignInUrl` of the instance. - * @param params - Optional query parameters to append to the URL. + * @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. - * @param params - Optional query parameters to append to the URL. + * @param params - Optional configuration. + * @param params.params - Optional query parameters to append to the URL. */ buildAfterSignUpUrl({ params }?: { params?: URLSearchParams }): string; @@ -999,7 +1001,7 @@ export interface Clerk { ) => Promise; /** - * Completes an email link verification flow started by {@link Clerk.client.signIn.createEmailLinkFlow} or {@link 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. + * 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. */ @@ -1496,7 +1498,7 @@ export type SetActiveParams = { /** * 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. * - * 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 @@ -1598,6 +1600,9 @@ export type SignInProps = RoutingOptions & { SignUpFallbackRedirectUrl & AfterSignOutUrl; +/** + * @interface + */ export interface TransferableOption { /** * 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. diff --git a/packages/shared/src/types/redirects.ts b/packages/shared/src/types/redirects.ts index 5bcd3ea7f3c..41e53ebc2cb 100644 --- a/packages/shared/src/types/redirects.ts +++ b/packages/shared/src/types/redirects.ts @@ -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,6 +90,7 @@ export type RedirectUrlProp = { redirectUrl?: string | null; }; +/** @document */ export type SignUpForceRedirectUrl = { /** * 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. @@ -96,6 +98,7 @@ export type SignUpForceRedirectUrl = { 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,6 +118,7 @@ export type SignInFallbackRedirectUrl = { signInFallbackRedirectUrl?: string | null; }; +/** @document */ export type SignInForceRedirectUrl = { /** * 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. From c9129a46177551608a1cd4a4d8024d88486634d0 Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:00:27 -0700 Subject: [PATCH 11/13] type intersections should merge all type information into the generated file --- .typedoc/custom-theme.mjs | 249 +++++++++++++++++++++++++++++++++++++- 1 file changed, 246 insertions(+), 3 deletions(-) diff --git a/.typedoc/custom-theme.mjs b/.typedoc/custom-theme.mjs index 7d58e428401..0cd40c37df5 100644 --- a/.typedoc/custom-theme.mjs +++ b/.typedoc/custom-theme.mjs @@ -6,6 +6,237 @@ 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 */ @@ -355,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, @@ -363,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; From 2d1ceecc851416a91e66f3fa83aad748e8fa8dfd Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:55:56 -0700 Subject: [PATCH 12/13] objects get their own folders with -properties file and -methods folder --- .typedoc/__tests__/file-structure.test.ts | 4 ++ .typedoc/custom-plugin.mjs | 40 +++++++++---- .typedoc/custom-router.mjs | 22 +++++++ .typedoc/extract-methods.mjs | 72 +++++++++++++++++++++-- .typedoc/reference-objects.mjs | 10 +++- 5 files changed, 127 insertions(+), 21 deletions(-) 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 b0884c5ad8a..018e3d02a42 100644 --- a/.typedoc/custom-plugin.mjs +++ b/.typedoc/custom-plugin.mjs @@ -329,6 +329,20 @@ 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. @@ -347,18 +361,20 @@ export function applyCatchAllMdReplacements(contents) { } return contents .split('\n') - .map(line => { - if (ATX_HEADING_LINE.test(line.replace(/\r$/, ''))) { - return line; - } - const { text: withPh, placeholders } = protectPipeDelimitedInlineCodeSpans(line); - let out = withPh; - for (const { pattern, replace } of getCatchAllReplacements()) { - // @ts-ignore — string | function - out = out.replace(pattern, replace); - } - return restoreProtectedInlineCodeSpans(out, placeholders); - }) + .map( + /** @param {string} line */ line => { + if (ATX_HEADING_LINE.test(line.replace(/\r$/, ''))) { + return line; + } + const { text: withPh, placeholders } = protectPipeDelimitedInlineCodeSpans(line); + let out = withPh; + for (const { pattern, replace } of getCatchAllReplacements()) { + // @ts-ignore — string | function + out = out.replace(pattern, replace); + } + return restoreProtectedInlineCodeSpans(out, placeholders); + }, + ) .join('\n'); } 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/extract-methods.mjs b/.typedoc/extract-methods.mjs index 8da026b81d2..a75189fe027 100644 --- a/.typedoc/extract-methods.mjs +++ b/.typedoc/extract-methods.mjs @@ -1,7 +1,6 @@ // @ts-check /** - * For each entry in REFERENCE_OBJECTS_LIST, finds callable members on the mapped interface/class via TypeDoc - * and writes one .mdx per method (kebab file name) next to the main reference page output. + * 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. * @@ -17,7 +16,11 @@ import { Application, Comment, PageKind, ReflectionKind } from 'typedoc'; import { MarkdownPageEvent, MarkdownTheme } from 'typedoc-plugin-markdown'; import typedocConfig from '../typedoc.config.mjs'; -import { applyCatchAllMdReplacements, applyRelativeLinkReplacements } from './custom-plugin.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'; @@ -167,7 +170,7 @@ function formatMethodParametersTable(tableMd) { /** * @param {import('typedoc').Application} app * @param {import('typedoc').ProjectReflection} project - * @param {string} pageUrl e.g. `shared/clerk.mdx` + * @param {string} pageUrl e.g. `shared/clerk/index.mdx` * @param {import('typedoc').DeclarationReflection} interfaceDecl */ function createThemeContextForReferencePage(app, project, pageUrl, interfaceDecl) { @@ -277,12 +280,64 @@ function isCallableMember(decl) { if (decl.kind === ReflectionKind.Method) { return true; } - if (decl.kind === ReflectionKind.Property) { + if (decl.kind === ReflectionKind.Property || decl.kind === ReflectionKind.Accessor) { return !!getPrimaryCallSignature(decl); } 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 */ @@ -734,9 +789,14 @@ function extractMethodsForPage(pageUrl, project, app) { return 0; } + extractPropertiesAndTrimSourcePage(pageUrl); + const ctx = createThemeContextForReferencePage(app, project, pageUrl, decl); - const outDir = path.join(__dirname, 'temp-docs', path.dirname(pageUrl), `${path.basename(pageUrl, '.mdx')}-methods`); + 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; diff --git a/.typedoc/reference-objects.mjs b/.typedoc/reference-objects.mjs index db12e70bbb7..aa4e76bbdf4 100644 --- a/.typedoc/reference-objects.mjs +++ b/.typedoc/reference-objects.mjs @@ -4,12 +4,16 @@ * `page.url` values are relative to TypeDoc `out` (e.g. `.typedoc/temp-docs`). */ -export const REFERENCE_OBJECTS_LIST = ['shared/clerk.mdx', 'shared/client-resource.mdx']; +/** + * 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.mdx': 'Clerk', - 'shared/client-resource.mdx': 'ClientResource', + 'shared/clerk/clerk.mdx': 'Clerk', + 'shared/client-resource/client-resource.mdx': 'ClientResource', }; From 6eedb5b4b3d707621afdc862f3e826f41dc1bb02 Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:18:37 -0700 Subject: [PATCH 13/13] include typealias functions in the methods generation --- .typedoc/custom-plugin.mjs | 2 +- .typedoc/custom-theme.mjs | 2 ++ .typedoc/extract-methods.mjs | 41 ++++++++++++++++++++++++++++++++---- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/.typedoc/custom-plugin.mjs b/.typedoc/custom-plugin.mjs index 018e3d02a42..c6a8dde0f3a 100644 --- a/.typedoc/custom-plugin.mjs +++ b/.typedoc/custom-plugin.mjs @@ -102,7 +102,7 @@ const LINK_REPLACEMENTS = [ ['billing-namespace', '/docs/reference/objects/billing'], ['client-resource', '/docs/reference/objects/client'], ['redirect-options', '/docs/reference/types/redirect-options'], - ['handle-o-auth-call-back-params', '/docs/reference/types/handle-o-auth-call-back-params'], + ['handle-o-auth-callback-params', '/docs/reference/types/handle-o-auth-callback-params'], ]; /** diff --git a/.typedoc/custom-theme.mjs b/.typedoc/custom-theme.mjs index 0cd40c37df5..a1b29dc10c1 100644 --- a/.typedoc/custom-theme.mjs +++ b/.typedoc/custom-theme.mjs @@ -1018,3 +1018,5 @@ function isCallablePropertyValueType(t, helpers, seenReflectionIds) { } return false; } + +export { isCallableInterfaceProperty }; diff --git a/.typedoc/extract-methods.mjs b/.typedoc/extract-methods.mjs index a75189fe027..c17ebd5ed1c 100644 --- a/.typedoc/extract-methods.mjs +++ b/.typedoc/extract-methods.mjs @@ -16,6 +16,7 @@ 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, @@ -270,18 +271,50 @@ function getPrimaryCallSignature(decl) { 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 isCallableMember(decl) { +function shouldExtractCallableMember(decl, ctx) { if (decl.kind === ReflectionKind.Method) { return true; } - if (decl.kind === ReflectionKind.Property || decl.kind === ReflectionKind.Accessor) { - return !!getPrimaryCallSignature(decl); + if ( + decl.kind === ReflectionKind.Property || + decl.kind === ReflectionKind.Accessor || + decl.kind === ReflectionKind.Variable + ) { + return isCallableInterfaceProperty(decl, ctx.helpers); } return false; } @@ -804,7 +837,7 @@ function extractMethodsForPage(pageUrl, project, app) { if (child.name.startsWith('__')) { continue; } - if (!isCallableMember(/** @type {import('typedoc').DeclarationReflection} */ (child))) { + if (!shouldExtractCallableMember(/** @type {import('typedoc').DeclarationReflection} */ (child), ctx)) { continue; } const mdx = buildMethodMdx(/** @type {import('typedoc').DeclarationReflection} */ (child), ctx);