diff --git a/.changeset/typedoc-marker-slicer.md b/.changeset/typedoc-marker-slicer.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/typedoc-marker-slicer.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.typedoc/__tests__/__snapshots__/api-key-resource-methods-create.mdx b/.typedoc/__tests__/__snapshots__/api-key-resource-methods-create.mdx index 8a60ae22d3d..d1d6a9d49c3 100644 --- a/.typedoc/__tests__/__snapshots__/api-key-resource-methods-create.mdx +++ b/.typedoc/__tests__/__snapshots__/api-key-resource-methods-create.mdx @@ -18,4 +18,4 @@ function create(params: CreateAPIKeyParams): Promise | `description?` | `string` | The description of the API key. | | `name` | `string` | The name of the API key. | | `secondsUntilExpiration?` | `number` | The number of seconds until the API key expires. Set to `null` or omit to create a key that never expires. | -| `subject?` | `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 user or organization ID to associate the API key with. If not provided, defaults to the [Active Organization](!active-organization), then the current user. | diff --git a/.typedoc/__tests__/__snapshots__/clerk-properties.mdx b/.typedoc/__tests__/__snapshots__/clerk-properties.mdx index 571d450cf7a..9effbb43418 100644 --- a/.typedoc/__tests__/__snapshots__/clerk-properties.mdx +++ b/.typedoc/__tests__/__snapshots__/clerk-properties.mdx @@ -20,8 +20,8 @@ | `session` | undefined \| null \| [SignedInSessionResource](/docs/reference/objects/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`. | | `status` | "error" \| "degraded" \| "loading" \| "ready" | The status of the `Clerk` instance. Possible values are: | | `telemetry` | undefined \| \{ isDebug: boolean; isEnabled: boolean; record: void; recordLog: void; \} | [Telemetry](/docs/guides/how-clerk-works/security/clerk-telemetry) configuration. | -| `telemetry.isDebug` | `boolean` | If `true`, telemetry events are only logged to the console and not sent to Clerk. | -| `telemetry.isEnabled` | `boolean` | Indicates whether telemetry is enabled. | +| `telemetry.isDebug` | `boolean` | Whether telemetry events are only logged to the console and not sent to Clerk. | +| `telemetry.isEnabled` | `boolean` | Whether telemetry is enabled. | | `uiVersion` | undefined \| string | The version of `@clerk/ui` that is currently loaded, or `undefined` if the prebuilt UI has not been loaded yet. | | `user` | undefined \| null \| [UserResource](/docs/reference/objects/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. | | `version` | undefined \| string | The Clerk SDK version number. | diff --git a/.typedoc/__tests__/__snapshots__/clerk.mdx b/.typedoc/__tests__/__snapshots__/clerk.mdx index 8ea6508d37a..2bc7c984f96 100644 --- a/.typedoc/__tests__/__snapshots__/clerk.mdx +++ b/.typedoc/__tests__/__snapshots__/clerk.mdx @@ -1 +1,1699 @@ The `Clerk` class serves as the central interface for working with Clerk's authentication and user management functionality in your application. As a top-level class in the Clerk SDK, it provides access to key methods and properties for managing users, sessions, API keys, billing, organizations, and more. + +## `addListener()` + +Register a listener that triggers a callback whenever a change in the [`Client`](/docs/reference/objects/client), [`Session`](/docs/reference/objects/session), [`User`](/docs/reference/objects/user), or [`Organization`](/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`. + +```typescript +function addListener( + callback: (emission: { + client: ClientResource; + organization?: null | OrganizationResource; + session?: null | SignedInSessionResource; + user?: null | UserResource; + }) => void, + options?: { skipInitialEmit?: boolean }, +): UnsubscribeCallback; +``` + +### Parameters + +| Parameter | Type | Description | +| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| `callback` | (emission: \{ client: [ClientResource](/docs/reference/objects/client); organization?: null \| [OrganizationResource](/docs/reference/objects/organization); session?: null \| [SignedInSessionResource](/docs/reference/objects/session); user?: null \| [UserResource](/docs/reference/objects/user); \}) => void | The function to call when Clerk resources change. | +| `options?` | \{ skipInitialEmit?: boolean; \} | Optional configuration. | +| `options?.skipInitialEmit?` | `boolean` | Whether the callback will not be called immediately after registration. Defaults to `false`. | + +### Returns + +`UnsubscribeCallback` — - An `UnsubscribeCallback` function that can be used to remove the listener from the `Clerk` object. + +## `authenticateWithBase()` + +Starts a sign-in flow that uses Base to authenticate the user using their Web3 wallet address. + +```typescript +function authenticateWithBase( + params?: AuthenticateWithBaseParams, +): Promise; +``` + +### `AuthenticateWithBaseParams` + +| Property | Type | Description | +| --------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `customNavigate?` | (to: string) => Promise\ | A function that overrides Clerk's default navigation behavior, allowing custom handling of navigation during sign-up and sign-in flows. | +| `legalAccepted?` | `boolean` | A boolean indicating whether the user has agreed to the [legal compliance](/docs/guides/secure/legal-compliance) documents. | +| `redirectUrl?` | `string` | The full URL or path to navigate to after a successful sign-in or sign-up. | +| `signUpContinueUrl?` | `string` | The URL to navigate to if the sign-up process is missing user information. | +| `unsafeMetadata?` | [SignUpUnsafeMetadata](/docs/reference/types/metadata#sign-up-unsafe-metadata) | 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. | + +### Returns + +`Promise`\<`unknown`\> + +## `authenticateWithCoinbaseWallet()` + +Starts a sign-in flow that uses Coinbase Smart Wallet to authenticate the user using their Coinbase wallet address. + +```typescript +function authenticateWithCoinbaseWallet( + params?: AuthenticateWithCoinbaseWalletParams, +): Promise; +``` + +### `AuthenticateWithCoinbaseWalletParams` + +| Property | Type | Description | +| --------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `customNavigate?` | (to: string) => Promise\ | A function that overrides Clerk's default navigation behavior, allowing custom handling of navigation during sign-up and sign-in flows. | +| `legalAccepted?` | `boolean` | A boolean indicating whether the user has agreed to the [legal compliance](/docs/guides/secure/legal-compliance) documents. | +| `redirectUrl?` | `string` | The full URL or path to navigate to after a successful sign-in or sign-up. | +| `signUpContinueUrl?` | `string` | The URL to navigate to if the sign-up process is missing user information. | +| `unsafeMetadata?` | [SignUpUnsafeMetadata](/docs/reference/types/metadata#sign-up-unsafe-metadata) | 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. | + +### Returns + +`Promise`\<`unknown`\> + +## `authenticateWithGoogleOneTap()` + +Authenticates user using a Google token generated from Google identity services. + +```typescript +function authenticateWithGoogleOneTap( + params: AuthenticateWithGoogleOneTapParams, +): Promise; +``` + +### `AuthenticateWithGoogleOneTapParams` + +| Property | Type | Description | +| ------------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------- | +| `legalAccepted?` | `boolean` | A boolean indicating whether the user has agreed to the [legal compliance](/docs/guides/secure/legal-compliance) documents. | +| `token` | `string` | The Google credential token from the Google Identity Services response. | + +### Returns + +`Promise`\<[SignInResource](/docs/reference/objects/sign-in) \| [SignUpResource](/docs/reference/objects/sign-up)\> + +## `authenticateWithMetamask()` + +Starts a sign-in flow that uses MetaMask to authenticate the user using their Metamask wallet address. + +```typescript +function authenticateWithMetamask( + params?: AuthenticateWithMetamaskParams, +): Promise; +``` + +### `AuthenticateWithMetamaskParams` + +| Property | Type | Description | +| --------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `customNavigate?` | (to: string) => Promise\ | A function that overrides Clerk's default navigation behavior, allowing custom handling of navigation during sign-up and sign-in flows. | +| `legalAccepted?` | `boolean` | A boolean indicating whether the user has agreed to the [legal compliance](/docs/guides/secure/legal-compliance) documents. | +| `redirectUrl?` | `string` | The full URL or path to navigate to after a successful sign-in or sign-up. | +| `signUpContinueUrl?` | `string` | The URL to navigate to if the sign-up process is missing user information. | +| `unsafeMetadata?` | [SignUpUnsafeMetadata](/docs/reference/types/metadata#sign-up-unsafe-metadata) | 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. | + +### Returns + +`Promise`\<`unknown`\> + +## `authenticateWithOKXWallet()` + +Starts a sign-in flow that uses OKX Wallet to authenticate the user using their OKX wallet address. + +```typescript +function authenticateWithOKXWallet( + params?: AuthenticateWithOKXWalletParams, +): Promise; +``` + +### `AuthenticateWithOKXWalletParams` + +| Property | Type | Description | +| --------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `customNavigate?` | (to: string) => Promise\ | A function that overrides Clerk's default navigation behavior, allowing custom handling of navigation during sign-up and sign-in flows. | +| `legalAccepted?` | `boolean` | A boolean indicating whether the user has agreed to the [legal compliance](/docs/guides/secure/legal-compliance) documents. | +| `redirectUrl?` | `string` | The full URL or path to navigate to after a successful sign-in or sign-up. | +| `signUpContinueUrl?` | `string` | The URL to navigate to if the sign-up process is missing user information. | +| `unsafeMetadata?` | [SignUpUnsafeMetadata](/docs/reference/types/metadata#sign-up-unsafe-metadata) | 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. | + +### Returns + +`Promise`\<`unknown`\> + +## `authenticateWithSolana()` + +Starts a sign-in flow that uses Solana to authenticate the user using their Solana wallet address. + +```typescript +function authenticateWithSolana( + params: AuthenticateWithSolanaParams, +): Promise; +``` + +### `AuthenticateWithSolanaParams` + +| Property | Type | Description | +| --------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `customNavigate?` | (to: string) => Promise\ | A function that overrides Clerk's default navigation behavior, allowing custom handling of navigation during sign-up and sign-in flows. | +| `legalAccepted?` | `boolean` | A boolean indicating whether the user has agreed to the [legal compliance](/docs/guides/secure/legal-compliance) documents. | +| `redirectUrl?` | `string` | The full URL or path to navigate to after a successful sign-in or sign-up. | +| `signUpContinueUrl?` | `string` | The URL to navigate to if the sign-up process is missing user information. | +| `unsafeMetadata?` | [SignUpUnsafeMetadata](/docs/reference/types/metadata#sign-up-unsafe-metadata) | 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. | +| `walletName` | `string` | The name of the Solana wallet to use for authentication. | + +### Returns + +`Promise`\<`unknown`\> + +## `authenticateWithWeb3()` + +Starts a sign-in flow that uses a Web3 Wallet browser extension to authenticate the user using their Ethereum wallet address. + +```typescript +function authenticateWithWeb3( + params: ClerkAuthenticateWithWeb3Params, +): Promise; +``` + +### `ClerkAuthenticateWithWeb3Params` + +| Property | Type | Description | +| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `customNavigate?` | (to: string) => Promise\ | A function that overrides Clerk's default navigation behavior, allowing custom handling of navigation during sign-up and sign-in flows. | +| `legalAccepted?` | `boolean` | A boolean indicating whether the user has agreed to the [legal compliance](/docs/guides/secure/legal-compliance) documents. | +| `redirectUrl?` | `string` | The full URL or path to navigate to after a successful sign-in or sign-up. | +| `secondFactorUrl?` | `string` | The URL to navigate to if [second factor](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#multi-factor-authentication) is required. | +| `signUpContinueUrl?` | `string` | The URL to navigate to if the sign-up process is missing user information. | +| `strategy` | "web3_metamask_signature" \| "web3_base_signature" \| "web3_coinbase_wallet_signature" \| "web3_okx_wallet_signature" \| "web3_solana_signature" | The strategy to use for the sign-in flow. | +| `unsafeMetadata?` | [SignUpUnsafeMetadata](/docs/reference/types/metadata#sign-up-unsafe-metadata) | 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. | +| `walletName?` | `string` | The name of the wallet to use for authentication. | + +### Returns + +`Promise`\<`unknown`\> + +## `buildAfterMultiSessionSingleSignOutUrl()` + +Returns the configured `afterMultiSessionSingleSignOutUrl` of the instance. + +```typescript +function buildAfterMultiSessionSingleSignOutUrl(): string; +``` + +### Returns + +`string` + +## `buildAfterSignInUrl()` + +Returns the configured `afterSignInUrl` of the instance. + +```typescript +function buildAfterSignInUrl(params?: { params?: URLSearchParams }): string; +``` + +### Parameters + +| Parameter | Type | Description | +| --------- | -------------------------------------------- | ----------------------------------------------- | +| `params?` | \{ params?: URLSearchParams; \} | Optional query parameters to append to the URL. | + +### Returns + +`string` + +## `buildAfterSignOutUrl()` + +Returns the configured `afterSignOutUrl` of the instance. + +```typescript +function buildAfterSignOutUrl(): string; +``` + +### Returns + +`string` + +## `buildAfterSignUpUrl()` + +Returns the configured `afterSignUpUrl` of the instance. + +```typescript +function buildAfterSignUpUrl(params?: { params?: URLSearchParams }): string; +``` + +### Parameters + +| Parameter | Type | Description | +| --------- | -------------------------------------------- | ----------------------------------------------- | +| `params?` | \{ params?: URLSearchParams; \} | Optional query parameters to append to the URL. | + +### Returns + +`string` + +## `buildCreateOrganizationUrl()` + +Returns the configured URL where [``](/docs/reference/components/organization/create-organization) is mounted or a custom create-organization page is rendered. + +```typescript +function buildCreateOrganizationUrl(): string; +``` + +### Returns + +`string` + +## `buildNewSubscriptionRedirectUrl()` + +Returns the configured `newSubscriptionRedirectUrl` of the instance. + +```typescript +function buildNewSubscriptionRedirectUrl(): string; +``` + +### Returns + +`string` + +## `buildOrganizationProfileUrl()` + +Returns the configured URL where [``](/docs/reference/components/organization/organization-profile) is mounted or a custom organization-profile page is rendered. + +```typescript +function buildOrganizationProfileUrl(): string; +``` + +### Returns + +`string` + +## `buildSignInUrl()` + +Returns the configured URL where [``](/docs/reference/components/authentication/sign-in) is mounted or a custom sign-in page is rendered. + +```typescript +function buildSignInUrl(opts?: RedirectOptions): string; +``` + +### `RedirectOptions` + +| Property | Type | Description | +| ------------------------------------------------------------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `redirectUrl?` | string \| null | Full URL or path to navigate to after a successful action. | +| `signInFallbackRedirectUrl?` | string \| null | 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](/docs/guides/development/clerk-environment-variables#sign-in-and-sign-up-redirects) instead. Defaults to `'/'`. | +| `signInForceRedirectUrl?` | string \| null | If provided, this URL will always be redirected to after the user signs in. It's recommended to use the [environment variable](/docs/guides/development/clerk-environment-variables#sign-in-and-sign-up-redirects) instead. | +| `signUpFallbackRedirectUrl?` | string \| null | 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](/docs/guides/development/clerk-environment-variables#sign-in-and-sign-up-redirects) instead. Defaults to `'/'`. | +| `signUpForceRedirectUrl?` | string \| null | If provided, this URL will always be redirected to after the user signs up. It's recommended to use the [environment variable](/docs/guides/development/clerk-environment-variables#sign-in-and-sign-up-redirects) instead. | + +### Returns + +`string` + +## `buildSignUpUrl()` + +Returns the configured URL where [``](/docs/reference/components/authentication/sign-up) is mounted or a custom sign-up page is rendered. + +```typescript +function buildSignUpUrl(opts?: RedirectOptions): string; +``` + +### `RedirectOptions` + +| Property | Type | Description | +| ------------------------------------------------------------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `redirectUrl?` | string \| null | Full URL or path to navigate to after a successful action. | +| `signInFallbackRedirectUrl?` | string \| null | 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](/docs/guides/development/clerk-environment-variables#sign-in-and-sign-up-redirects) instead. Defaults to `'/'`. | +| `signInForceRedirectUrl?` | string \| null | If provided, this URL will always be redirected to after the user signs in. It's recommended to use the [environment variable](/docs/guides/development/clerk-environment-variables#sign-in-and-sign-up-redirects) instead. | +| `signUpFallbackRedirectUrl?` | string \| null | 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](/docs/guides/development/clerk-environment-variables#sign-in-and-sign-up-redirects) instead. Defaults to `'/'`. | +| `signUpForceRedirectUrl?` | string \| null | If provided, this URL will always be redirected to after the user signs up. It's recommended to use the [environment variable](/docs/guides/development/clerk-environment-variables#sign-in-and-sign-up-redirects) instead. | + +### Returns + +`string` + +## `buildTasksUrl()` + +Returns the configured URL where [session tasks](/docs/guides/configure/session-tasks) are mounted. + +```typescript +function buildTasksUrl(): string; +``` + +### Returns + +`string` + +## `buildUrlWithAuth()` + +Decorates the provided URL with the auth token for development instances. + +```typescript +function buildUrlWithAuth(to: string): string; +``` + +### Parameters + +| Parameter | Type | Description | +| --------- | -------- | ---------------------------------- | +| `to` | `string` | The route to create a URL towards. | + +### Returns + +`string` + +## `buildUserProfileUrl()` + +Returns the configured URL where [``](/docs/reference/components/user/user-profile) is mounted or a custom user-profile page is rendered. + +```typescript +function buildUserProfileUrl(): string; +``` + +### Returns + +`string` + +## `buildWaitlistUrl()` + +Returns the configured URL where [``](/docs/reference/components/authentication/waitlist) is mounted or a custom waitlist page is rendered. + +```typescript +function buildWaitlistUrl(opts?: { + initialValues?: Record; +}): string; +``` + +### Parameters + +| Parameter | Type | Description | +| ---------------------- | ------------------------------------------------------------ | -------------------------------------------- | +| `opts?` | \{ initialValues?: Record\; \} | Options to control the waitlist URL. | +| `opts?.initialValues?` | `Record`\<`string`, `string`\> | Initial values to prefill the waitlist form. | + +### Returns + +`string` + +## `closeCreateOrganization()` + +Closes the Clerk CreateOrganization modal. + +```typescript +function closeCreateOrganization(): void; +``` + +### Returns + +`void` + +## `closeGoogleOneTap()` + +Opens the Google One Tap component. +If the component is not already open, results in a noop. + +```typescript +function closeGoogleOneTap(): void; +``` + +### Returns + +`void` + +## `closeOrganizationProfile()` + +Closes the Clerk OrganizationProfile modal. + +```typescript +function closeOrganizationProfile(): void; +``` + +### Returns + +`void` + +## `closeSignIn()` + +Closes the Clerk SignIn modal. + +```typescript +function closeSignIn(): void; +``` + +### Returns + +`void` + +## `closeSignUp()` + +Closes the Clerk SignUp modal. + +```typescript +function closeSignUp(): void; +``` + +### Returns + +`void` + +## `closeUserProfile()` + +Closes the Clerk UserProfile modal. + +```typescript +function closeUserProfile(): void; +``` + +### Returns + +`void` + +## `closeWaitlist()` + +Closes the Clerk Waitlist modal. + +```typescript +function closeWaitlist(): void; +``` + +### Returns + +`void` + +## `createOrganization()` + +Creates an Organization programmatically, adding the current user as admin. Returns an [`Organization`](/docs/reference/objects/organization) object. + +> [!NOTE] +> For React-based apps, consider using the [``](/docs/reference/components/organization/create-organization) component. + +```typescript +function createOrganization( + params: CreateOrganizationParams, +): Promise; +``` + +### `CreateOrganizationParams` + +| Property | Type | Description | +| ------------------------- | -------- | ----------------------------- | +| `name` | `string` | The name of the Organization. | +| `slug?` | `string` | The slug of the Organization. | + +### Returns + +`Promise`\<[`OrganizationResource`](/docs/reference/objects/organization)\> + +## `getOrganization()` + +Gets a single [Organization](/docs/reference/objects/organization) by ID. + +```typescript +function getOrganization(organizationId: string): Promise; +``` + +### Parameters + +| Parameter | Type | Description | +| ---------------- | -------- | ---------------------------------- | +| `organizationId` | `string` | The ID of the Organization to get. | + +### Returns + +`Promise`\<[`OrganizationResource`](/docs/reference/objects/organization)\> + +## `handleEmailLinkVerification()` + +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. + +```typescript +function handleEmailLinkVerification( + params: { + onVerifiedOnOtherDevice?: () => void; + redirectUrl?: string; + redirectUrlComplete?: string; + }, + customNavigate?: (to: string) => Promise, +): Promise; +``` + +### Parameters + +| Parameter | Type | Description | +| --------------------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `params` | \{ onVerifiedOnOtherDevice?: () => void; redirectUrl?: string; redirectUrlComplete?: string; \} | 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. | +| `params.onVerifiedOnOtherDevice?` | () => void | Callback function to be executed after successful email link verification on another device. | +| `params.redirectUrl?` | `string` | The full URL to navigate to after successful email link verification on the same device, but without completing sign-in or sign-up. | +| `params.redirectUrlComplete?` | `string` | The full URL to navigate to after successful email link verification on completed sign-up or sign-in on the same device. | +| `customNavigate?` | (to: string) => Promise\ | A function that overrides Clerk's default navigation behavior, allowing custom handling of navigation during sign-up and sign-in flows. | + +### Returns + +`Promise`\<`unknown`\> + +## `handleGoogleOneTapCallback()` + +Completes a Google One Tap redirection flow started by [`authenticateWithGoogleOneTap()`](/docs/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. + +```typescript +function handleGoogleOneTapCallback( + signInOrUp: SignInResource | SignUpResource, + params: HandleOAuthCallbackParams, + customNavigate?: (to: string) => Promise, +): Promise; +``` + +### Parameters + +| Parameter | Type | Description | +| ----------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| `signInOrUp` | [SignInResource](/docs/reference/objects/sign-in) \| [SignUpResource](/docs/reference/objects/sign-up) | The resource returned from the initial `authenticateWithGoogleOneTap()` call (before redirect). | +| `params` | [`HandleOAuthCallbackParams`](/docs/reference/types/handle-o-auth-callback-params) | Additional props that define where the user will be redirected to at the end of a successful Google One Tap flow. | +| `customNavigate?` | (to: string) => Promise\ | A function that overrides Clerk's default navigation behavior, allowing custom handling of navigation during sign-up and sign-in flows. | + +### Returns + +`Promise`\<`unknown`\> + +## `handleRedirectCallback()` + +Completes a custom OAuth or SAML redirect flow that was started by calling [`SignIn.authenticateWithRedirect(params)`](/docs/reference/objects/sign-in) or [`SignUp.authenticateWithRedirect(params)`](/docs/reference/objects/sign-up). + +```typescript +function handleRedirectCallback( + params: HandleOAuthCallbackParams, + customNavigate?: (to: string) => Promise, +): Promise; +``` + +### Parameters + +| Parameter | Type | Description | +| ----------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| `params` | [`HandleOAuthCallbackParams`](/docs/reference/types/handle-o-auth-callback-params) | Additional props that define where the user will be redirected to at the end of a successful OAuth or SAML flow. | +| `customNavigate?` | (to: string) => Promise\ | A function that overrides Clerk's default navigation behavior, allowing custom handling of navigation during sign-up and sign-in flows. | + +### Returns + +`Promise`\<`unknown`\> + +## `handleUnauthenticated()` + +Handles a 401 response from the Frontend API by refreshing the [`Client`](/docs/reference/objects/client) and [`Session`](/docs/reference/objects/session) object accordingly. + +```typescript +function handleUnauthenticated(): Promise; +``` + +### Returns + +`Promise`\<`unknown`\> + +## `joinWaitlist()` + +Create a new waitlist entry programmatically. Requires that you set your app's sign-up mode to [**Waitlist**](/docs/guides/secure/restricting-access#waitlist) in the Clerk Dashboard. + +```typescript +function joinWaitlist(params: JoinWaitlistParams): Promise; +``` + +### `JoinWaitlistParams` + +| Property | Type | Description | +| ---------------------------------------- | -------- | ----------------------------------------------------- | +| `emailAddress` | `string` | The email address of the user to add to the waitlist. | + +### Returns + +`Promise`\<`WaitlistResource`\> + +## `mountAPIKeys()` + +Mount an API keys component at the target element. + +```typescript +function mountAPIKeys(targetNode: HTMLDivElement, props?: APIKeysProps): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | --------------------------------------- | -------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target to mount the APIKeys component. | +| `props?` | [`APIKeysProps`](../api-keys-props.mdx) | Configuration parameters. | + +### Returns + +`void` + +## `mountCreateOrganization()` + +Mount a CreateOrganization component at the target element. + +```typescript +function mountCreateOrganization( + targetNode: HTMLDivElement, + props?: CreateOrganizationProps, +): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | ------------------------------------------------------------- | ------------------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target to mount the CreateOrganization component. | +| `props?` | [`CreateOrganizationProps`](../create-organization-props.mdx) | Configuration parameters. | + +### Returns + +`void` + +## `mountOAuthConsent()` + +Mounts a OAuth consent component at the target element. + +```typescript +function mountOAuthConsent( + targetNode: HTMLDivElement, + oauthConsentProps?: OAuthConsentProps, +): void; +``` + +### Parameters + +| Parameter | Type | Description | +| -------------------- | ------------------- | ------------------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target node to mount the OAuth consent component. | +| `oauthConsentProps?` | `OAuthConsentProps` | OAuth consent configuration parameters. | + +### Returns + +`void` + +## `mountOrganizationList()` + +Mount an Organization list component at the target element. + +```typescript +function mountOrganizationList( + targetNode: HTMLDivElement, + props?: OrganizationListProps, +): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | --------------------------------------------------------- | ----------------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target to mount the OrganizationList component. | +| `props?` | [`OrganizationListProps`](../organization-list-props.mdx) | Configuration parameters. | + +### Returns + +`void` + +## `mountOrganizationProfile()` + +Mount an Organization profile component at the target element. + +```typescript +function mountOrganizationProfile( + targetNode: HTMLDivElement, + props?: OrganizationProfileProps, +): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | --------------------------------------------------------------- | -------------------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target to mount the OrganizationProfile component. | +| `props?` | [`OrganizationProfileProps`](../organization-profile-props.mdx) | Configuration parameters. | + +### Returns + +`void` + +## `mountOrganizationSwitcher()` + +Mount an Organization switcher component at the target element. + +```typescript +function mountOrganizationSwitcher( + targetNode: HTMLDivElement, + props?: OrganizationSwitcherProps, +): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | ----------------------------------------------------------------- | --------------------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target to mount the OrganizationSwitcher component. | +| `props?` | [`OrganizationSwitcherProps`](../organization-switcher-props.mdx) | Configuration parameters. | + +### Returns + +`void` + +## `mountPricingTable()` + +Mounts a pricing table component at the target element. + +```typescript +function mountPricingTable( + targetNode: HTMLDivElement, + props?: PricingTableProps, +): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | ------------------------------------------------- | ------------------------------------------------ | +| `targetNode` | `HTMLDivElement` | Target node to mount the PricingTable component. | +| `props?` | [`PricingTableProps`](../pricing-table-props.mdx) | configuration parameters. | + +### Returns + +`void` + +## `mountSignIn()` + +Mounts a sign in flow component at the target element. + +```typescript +function mountSignIn( + targetNode: HTMLDivElement, + signInProps?: SignInProps, +): void; +``` + +### Parameters + +| Parameter | Type | Description | +| -------------- | ------------------------------------- | ------------------------------------------ | +| `targetNode` | `HTMLDivElement` | Target node to mount the SignIn component. | +| `signInProps?` | [`SignInProps`](../sign-in-props.mdx) | sign in configuration parameters. | + +### Returns + +`void` + +## `mountSignUp()` + +Mounts a sign up flow component at the target element. + +```typescript +function mountSignUp( + targetNode: HTMLDivElement, + signUpProps?: SignUpProps, +): void; +``` + +### Parameters + +| Parameter | Type | Description | +| -------------- | ------------------------------------- | ------------------------------------------ | +| `targetNode` | `HTMLDivElement` | Target node to mount the SignUp component. | +| `signUpProps?` | [`SignUpProps`](../sign-up-props.mdx) | sign up configuration parameters. | + +### Returns + +`void` + +## `mountTaskChooseOrganization()` + +Mounts a TaskChooseOrganization component at the target element. + +```typescript +function mountTaskChooseOrganization( + targetNode: HTMLDivElement, + props?: TaskChooseOrganizationProps, +): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | ---------------------------------------------------------------------- | ---------------------------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target node to mount the TaskChooseOrganization component. | +| `props?` | [`TaskChooseOrganizationProps`](../task-choose-organization-props.mdx) | configuration parameters. | + +### Returns + +`void` + +## `mountTaskResetPassword()` + +Mounts a TaskResetPassword component at the target element. + +```typescript +function mountTaskResetPassword( + targetNode: HTMLDivElement, + props?: TaskResetPasswordProps, +): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | ------------------------------------------------------------ | ----------------------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target node to mount the TaskResetPassword component. | +| `props?` | [`TaskResetPasswordProps`](../task-reset-password-props.mdx) | configuration parameters. | + +### Returns + +`void` + +## `mountTaskSetupMFA()` + +Mounts a TaskSetupMFA component at the target element. +This component allows users to set up multi-factor authentication. + +```typescript +function mountTaskSetupMFA( + targetNode: HTMLDivElement, + props?: TaskSetupMFAProps, +): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | -------------------------------------------------- | ------------------------------------------------ | +| `targetNode` | `HTMLDivElement` | Target node to mount the TaskSetupMFA component. | +| `props?` | [`TaskSetupMFAProps`](../task-setup-mfa-props.mdx) | configuration parameters. | + +### Returns + +`void` + +## `mountUserAvatar()` + +Mount a user avatar component at the target element. + +```typescript +function mountUserAvatar( + targetNode: HTMLDivElement, + userAvatarProps?: UserAvatarProps, +): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------------ | ----------------- | ---------------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target node to mount the UserAvatar component. | +| `userAvatarProps?` | `UserAvatarProps` | - | + +### Returns + +`void` + +## `mountUserButton()` + +Mount a user button component at the target element. + +```typescript +function mountUserButton( + targetNode: HTMLDivElement, + userButtonProps?: UserButtonProps, +): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------------ | ----------------- | ---------------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target node to mount the UserButton component. | +| `userButtonProps?` | `UserButtonProps` | User button configuration parameters. | + +### Returns + +`void` + +## `mountUserProfile()` + +Mount a user profile component at the target element. + +```typescript +function mountUserProfile( + targetNode: HTMLDivElement, + userProfileProps?: UserProfileProps, +): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------------- | ----------------------------------------------- | ------------------------------------------ | +| `targetNode` | `HTMLDivElement` | Target to mount the UserProfile component. | +| `userProfileProps?` | [`UserProfileProps`](../user-profile-props.mdx) | User profile configuration parameters. | + +### Returns + +`void` + +## `mountWaitlist()` + +Mount a waitlist at the target element. + +```typescript +function mountWaitlist(targetNode: HTMLDivElement, props?: WaitlistProps): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | ---------------------------------------- | --------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target to mount the Waitlist component. | +| `props?` | [`WaitlistProps`](../waitlist-props.mdx) | Configuration parameters. | + +### Returns + +`void` + +## `navigate()` + +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. + +```typescript +function navigate( + to: string, + options?: { metadata?: RouterMetadata; replace?: boolean }, +): Promise | void; +``` + +### Parameters + +| Parameter | Type | Description | +| -------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------- | +| `to` | `string` | The URL or relative path to navigate to. | +| `options?` | \{ metadata?: RouterMetadata; replace?: boolean; \} | Optional configuration. | +| `options?.metadata?` | `RouterMetadata` | Router metadata. | +| `options?.replace?` | `boolean` | Whether to replace the current history entry instead of pushing a new one. | + +### Returns + +Promise\ \| void + +## `off()` + +Removes an event handler for a specific Clerk event. + +```typescript +function off(event: E, handler: EventHandler): void; +``` + +### Parameters + +| Parameter | Type | Description | +| --------- | --------------------- | ----------------------------------- | +| `event` | `E` | The event name to unsubscribe from. | +| `handler` | `EventHandler`\<`E`\> | The callback function to remove. | + +### Returns + +`void` + +## `on()` + +Registers an event handler for a specific Clerk event. + +```typescript +function on( + event: E, + handler: EventHandler, + opt?: { notify: boolean }, +): void; +``` + +### Parameters + +| Parameter | Type | Description | +| -------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `event` | `E` | The event name to subscribe to. | +| `handler` | `EventHandler`\<`E`\> | The callback function to execute when the event is dispatched. | +| `opt?` | \{ notify: boolean; \} | Optional configuration. | +| `opt?.notify?` | `boolean` | If true and the event was previously dispatched, handler will be called immediately with the latest payload. | + +### Returns + +`void` + +## `openCreateOrganization()` + +Opens the Clerk CreateOrganization modal. + +```typescript +function openCreateOrganization(props?: CreateOrganizationModalProps): void; +``` + +### `CreateOrganizationModalProps` + +| Property | Type | Description | +| --------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `getContainer?` | () => HTMLElement \| null | Function that returns the container element where portals should be rendered. This allows Clerk components to render inside external dialogs/popovers (e.g., Radix Dialog, React Aria Components) instead of document.body. | + +### Returns + +`void` + +## `openGoogleOneTap()` + +Opens the Google One Tap component. + +```typescript +function openGoogleOneTap(props?: GoogleOneTapProps): void; +``` + +### `GoogleOneTapProps` + +| Property | Type | Description | +| --------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `appearance?` | [`ClerkAppearanceTheme`](../clerk-appearance-theme.mdx) | 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` configuration (if one is provided). See the [`Appearance`](/docs/guides/customizing-clerk/appearance-prop/overview) docs for more information. | +| `cancelOnTapOutside?` | `boolean` | Whether to cancel the Google One Tap request if a user clicks outside the prompt. Defaults to `true`. | +| `fedCmSupport?` | `boolean` | FedCM enables more private sign-in flows without requiring the use of third-party cookies. The browser controls user settings, displays user prompts, and only contacts an Identity Provider such as Google after explicit user consent is given. Backwards compatible with browsers that still support third-party cookies. Defaults to `true`. | +| `itpSupport?` | `boolean` | Enables upgraded One Tap UX on ITP browsers. Turning this options off, would hide any One Tap UI in such browsers. Defaults to `true`. | + +### Returns + +`void` + +## `openOrganizationProfile()` + +Opens the Clerk OrganizationProfile modal. + +```typescript +function openOrganizationProfile(props?: OrganizationProfileModalProps): void; +``` + +### Parameters + +| Parameter | Type | Description | +| --------- | ------------------------------- | ------------------------------------------------------------------------ | +| `props?` | `OrganizationProfileModalProps` | Optional props that will be passed to the OrganizationProfile component. | + +### Returns + +`void` + +## `openSignIn()` + +Opens the Clerk SignIn component in a modal. + +```typescript +function openSignIn(props?: SignInModalProps): void; +``` + +### Parameters + +| Parameter | Type | Description | +| --------- | ------------------ | ------------------------------------------ | +| `props?` | `SignInModalProps` | Optional sign in configuration parameters. | + +### Returns + +`void` + +## `openSignUp()` + +Opens the Clerk SignUp component in a modal. + +```typescript +function openSignUp(props?: SignUpModalProps): void; +``` + +### Parameters + +| Parameter | Type | Description | +| --------- | ------------------ | ----------------------------------------------------------- | +| `props?` | `SignUpModalProps` | Optional props that will be passed to the SignUp component. | + +### Returns + +`void` + +## `openUserProfile()` + +Opens the Clerk UserProfile modal. + +```typescript +function openUserProfile(props?: UserProfileModalProps): void; +``` + +### Parameters + +| Parameter | Type | Description | +| --------- | ----------------------- | ---------------------------------------------------------------- | +| `props?` | `UserProfileModalProps` | Optional props that will be passed to the UserProfile component. | + +### Returns + +`void` + +## `openWaitlist()` + +Opens the Clerk Waitlist modal. + +```typescript +function openWaitlist(props?: WaitlistModalProps): void; +``` + +### `WaitlistModalProps` + +| Property | Type | Description | +| --------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `afterJoinWaitlistUrl?` | `string` | Full URL or path to navigate to after join waitlist. | +| `appearance?` | [`ClerkAppearanceTheme`](../clerk-appearance-theme.mdx) | 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` configuration (if one is provided). See the [`Appearance`](/docs/guides/customizing-clerk/appearance-prop/overview) docs for more information. | +| `getContainer?` | () => HTMLElement \| null | Function that returns the container element where portals should be rendered. This allows Clerk components to render inside external dialogs/popovers (e.g., Radix Dialog, React Aria Components) instead of document.body. | +| `signInUrl?` | `string` | Full URL or path where the SignIn component is mounted. | + +### Returns + +`void` + +## `redirectToAfterSignIn()` + +Redirects to the configured `afterSignIn` URL. + +```typescript +function redirectToAfterSignIn(): void; +``` + +### Returns + +`void` + +## `redirectToAfterSignOut()` + +Redirects to the configured `afterSignOut` URL. + +```typescript +function redirectToAfterSignOut(): void; +``` + +### Returns + +`void` + +## `redirectToAfterSignUp()` + +Redirects to the configured `afterSignUp` URL. + +```typescript +function redirectToAfterSignUp(): void; +``` + +### Returns + +`void` + +## `redirectToCreateOrganization()` + +Redirects to the configured URL where [``](/docs/reference/components/organization/create-organization) is mounted. This method uses the [`navigate()`](/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. + +```typescript +function redirectToCreateOrganization(): Promise; +``` + +### Returns + +`Promise`\<`unknown`\> + +## `redirectToOrganizationProfile()` + +Redirects to the configured URL where [``](/docs/reference/components/organization/organization-profile) is mounted. This method uses the [`navigate()`](/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. + +```typescript +function redirectToOrganizationProfile(): Promise; +``` + +### Returns + +`Promise`\<`unknown`\> + +## `redirectToSignIn()` + +Redirects to the sign-in URL, as configured in your application's instance settings. This method uses the [`navigate()`](/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. + +```typescript +function redirectToSignIn(opts?: SignInRedirectOptions): Promise; +``` + +### Parameters + +| Parameter | Type | Description | +| --------- | ----------------------------------------------------------------------- | -------------------------------- | +| `opts?` | [SignInRedirectOptions](/docs/reference/types/sign-in-redirect-options) | Options to control the redirect. | + +### Returns + +`Promise`\<`unknown`\> + +## `redirectToSignUp()` + +Redirects to the sign-up URL, as configured in your application's instance settings. This method uses the [`navigate()`](/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. + +```typescript +function redirectToSignUp(opts?: SignUpRedirectOptions): Promise; +``` + +### Parameters + +| Parameter | Type | Description | +| --------- | ----------------------------------------------------------------------- | -------------------------------- | +| `opts?` | [SignUpRedirectOptions](/docs/reference/types/sign-up-redirect-options) | Options to control the redirect. | + +### Returns + +`Promise`\<`unknown`\> + +## `redirectToTasks()` + +Redirects to the configured URL where [session tasks](/docs/reference/objects/session) are mounted. + +```typescript +function redirectToTasks(opts?: TasksRedirectOptions): Promise; +``` + +### Parameters + +| Parameter | Type | Description | +| --------- | -------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| `opts?` | [TasksRedirectOptions](/docs/reference/types/redirect-options) | Options to control the redirect (e.g. redirect URL after tasks are complete). | + +### Returns + +`Promise`\<`unknown`\> + +## `redirectToUserProfile()` + +Redirects to the configured URL where [``](/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. + +```typescript +function redirectToUserProfile(): Promise; +``` + +### Returns + +`Promise`\<`unknown`\> + +## `redirectToWaitlist()` + +Redirects to the configured URL where [``](/docs/reference/components/authentication/waitlist) is mounted. + +```typescript +function redirectToWaitlist(): void; +``` + +### Returns + +`void` + +## `redirectWithAuth()` + +Redirects to the provided URL after appending authentication credentials. For development instances, this method decorates the URL with an auth token to maintain authentication state. For production instances, the standard session cookie is used. + +Returns a promise that can be `await`ed in order to listen for the navigation to finish. The inner value should not be relied on, as it can change based on the framework it's used within. + +```typescript +function redirectWithAuth(to: string): Promise; +``` + +### Parameters + +| Parameter | Type | Description | +| --------- | -------- | ----------------------- | +| `to` | `string` | The URL to redirect to. | + +### Returns + +`Promise`\<`unknown`\> + +## `setActive()` + +A method used to set the current session and/or Organization for the client. Accepts a [`SetActiveParams`](/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. + +```typescript +function setActive(setActiveParams: SetActiveParams): Promise; +``` + +### `SetActiveParams` + +| Property | Type | Description | +| ----------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `navigate?` | `SetActiveNavigate` | 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. See the [section on using the `navigate()` parameter](/docs/reference/objects/clerk#using-the-navigate-parameter) for more details. | +| `organization?` | null \| string \| [OrganizationResource](/docs/reference/objects/organization) | The Organization resource or Organization ID/slug (string version) to be set as active in the current session. If `null`, the currently Active Organization is removed as active. | +| `redirectUrl?` | `string` | The full URL or path to redirect to just before the session and/or organization is set. | +| `session?` | null \| string \| [SignedInSessionResource](/docs/reference/objects/session) | The session resource or session ID (string version) to be set as active. If `null`, the current session is deleted. | + +### Returns + +`Promise`\<`void`\> + +## `signOut()` + +Signs out the current user on single-session instances, or all users on multi-session instances. + +```typescript +function signOut(options?: SignOutOptions): Promise; +``` + +### `SignOutOptions` + +| Property | Type | Description | +| --------------------------------------- | -------- | ------------------------------------------------------------------------------ | +| `redirectUrl?` | `string` | Specify a redirect URL to navigate to after sign-out is complete. | +| `sessionId?` | `string` | Specify a specific session to sign out. Useful for multi-session applications. | + +### Returns + +`Promise`\<`void`\> + +## `unmountAPIKeys()` + +Unmount an API keys component from the target element. +If there is no component mounted at the target node, results in a noop. + +```typescript +function unmountAPIKeys(targetNode: HTMLDivElement): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | ---------------- | -------------------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target node to unmount the APIKeys component from. | + +### Returns + +`void` + +## `unmountCreateOrganization()` + +Unmount the CreateOrganization component from the target node. + +```typescript +function unmountCreateOrganization(targetNode: HTMLDivElement): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | ---------------- | ------------------------------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target node to unmount the CreateOrganization component from. | + +### Returns + +`void` + +## `unmountOAuthConsent()` + +Unmounts a OAuth consent component from the target element. + +```typescript +function unmountOAuthConsent(targetNode: HTMLDivElement): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | ---------------- | -------------------------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target node to unmount the OAuth consent component from. | + +### Returns + +`void` + +## `unmountOrganizationList()` + +Unmount the Organization list component from the target node.\* + +```typescript +function unmountOrganizationList(targetNode: HTMLDivElement): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | ---------------- | ----------------------------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target node to unmount the OrganizationList component from. | + +### Returns + +`void` + +## `unmountOrganizationProfile()` + +Unmount the Organization profile component from the target node. + +```typescript +function unmountOrganizationProfile(targetNode: HTMLDivElement): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | ---------------- | -------------------------------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target node to unmount the OrganizationProfile component from. | + +### Returns + +`void` + +## `unmountOrganizationSwitcher()` + +Unmount the Organization switcher component from the target node.\* + +```typescript +function unmountOrganizationSwitcher(targetNode: HTMLDivElement): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | ---------------- | --------------------------------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target node to unmount the OrganizationSwitcher component from. | + +### Returns + +`void` + +## `unmountPricingTable()` + +Unmount a pricing table component from the target element. +If there is no component mounted at the target node, results in a noop. + +```typescript +function unmountPricingTable(targetNode: HTMLDivElement): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | ---------------- | ------------------------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target node to unmount the PricingTable component from. | + +### Returns + +`void` + +## `unmountSignIn()` + +Unmount a sign in flow component from the target element. +If there is no component mounted at the target node, results in a noop. + +```typescript +function unmountSignIn(targetNode: HTMLDivElement): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | ---------------- | ------------------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target node to unmount the SignIn component from. | + +### Returns + +`void` + +## `unmountSignUp()` + +Unmount a sign up flow component from the target element. +If there is no component mounted at the target node, results in a noop. + +```typescript +function unmountSignUp(targetNode: HTMLDivElement): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | ---------------- | ------------------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target node to unmount the SignUp component from. | + +### Returns + +`void` + +## `unmountTaskChooseOrganization()` + +Unmount a TaskChooseOrganization component from the target element. +If there is no component mounted at the target node, results in a noop. + +```typescript +function unmountTaskChooseOrganization(targetNode: HTMLDivElement): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | ---------------- | ----------------------------------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target node to unmount the TaskChooseOrganization component from. | + +### Returns + +`void` + +## `unmountTaskResetPassword()` + +Unmount a TaskResetPassword component from the target element. +If there is no component mounted at the target node, results in a noop. + +```typescript +function unmountTaskResetPassword(targetNode: HTMLDivElement): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | ---------------- | ------------------------------------------------------------ | +| `targetNode` | `HTMLDivElement` | Target node to unmount the TaskResetPassword component from. | + +### Returns + +`void` + +## `unmountTaskSetupMFA()` + +Unmount a TaskSetupMFA component from the target element. +If there is no component mounted at the target node, results in a noop. + +```typescript +function unmountTaskSetupMFA(targetNode: HTMLDivElement): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | ---------------- | ------------------------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target node to unmount the TaskSetupMFA component from. | + +### Returns + +`void` + +## `unmountUserAvatar()` + +Unmount a user avatar component at the target element. +If there is no component mounted at the target node, results in a noop. + +```typescript +function unmountUserAvatar(targetNode: HTMLDivElement): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | ---------------- | ----------------------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target node to unmount the UserAvatar component from. | + +### Returns + +`void` + +## `unmountUserButton()` + +Unmount a user button component at the target element. +If there is no component mounted at the target node, results in a noop. + +```typescript +function unmountUserButton(targetNode: HTMLDivElement): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | ---------------- | ----------------------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target node to unmount the UserButton component from. | + +### Returns + +`void` + +## `unmountUserProfile()` + +Unmount a user profile component at the target element. +If there is no component mounted at the target node, results in a noop. + +```typescript +function unmountUserProfile(targetNode: HTMLDivElement): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | ---------------- | ------------------------------------------------------ | +| `targetNode` | `HTMLDivElement` | Target node to unmount the UserProfile component from. | + +### Returns + +`void` + +## `unmountWaitlist()` + +Unmount the Waitlist component from the target node. + +```typescript +function unmountWaitlist(targetNode: HTMLDivElement): void; +``` + +### Parameters + +| Parameter | Type | Description | +| ------------ | ---------------- | --------------------------------------------------- | +| `targetNode` | `HTMLDivElement` | Target node to unmount the Waitlist component from. | + +### Returns + +`void` diff --git a/.typedoc/custom-plugin.mjs b/.typedoc/custom-plugin.mjs index ee160a04d23..205d3ec379f 100644 --- a/.typedoc/custom-plugin.mjs +++ b/.typedoc/custom-plugin.mjs @@ -434,19 +434,6 @@ 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 body (no heading) 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. diff --git a/.typedoc/custom-theme.mjs b/.typedoc/custom-theme.mjs index c2c0d1c79c3..262792785b2 100644 --- a/.typedoc/custom-theme.mjs +++ b/.typedoc/custom-theme.mjs @@ -1,15 +1,25 @@ // @ts-check -import { ArrayType, i18n, IntersectionType, ReferenceType, ReflectionKind, ReflectionType, UnionType } from 'typedoc'; -import { MarkdownTheme, MarkdownThemeContext } from 'typedoc-plugin-markdown'; +import { + ArrayType, + Comment, + i18n, + IntersectionType, + OptionalType, + ReferenceType, + ReflectionKind, + ReflectionType, + UnionType, +} from 'typedoc'; +import { MarkdownPageEvent, MarkdownTheme, MarkdownThemeContext } from 'typedoc-plugin-markdown'; import { applyTodoStrippingToComment } from './comment-utils.mjs'; +import { applyCatchAllMdReplacements, applyRelativeLinkReplacements } from './custom-plugin.mjs'; import { backTicks, heading, htmlTable, removeLineBreaks, table } from './markdown-helpers.mjs'; -import { REFERENCE_OBJECTS_LIST } from './reference-objects.mjs'; +import { BACKEND_API_CONFIG, REFERENCE_OBJECT_CONFIG, REFERENCE_OBJECTS_LIST } from './reference-objects.mjs'; +import { toFileSlug } from './slug.mjs'; import { isInlineModifierWithoutStandalonePage } from './standalone-page-tag.mjs'; import { unwrapOptional } from './type-utils.mjs'; -export { REFERENCE_OBJECTS_LIST }; - /** * Unwrap optional TypeDoc types so referenced object shapes are still found. * @@ -1065,11 +1075,636 @@ function clerkDeclaration(model, options = { headingLevel: 2, nested: false }) { return md.join('\n\n'); } +/** + * `'reference'` (default): each `methods/.mdx` opens with `### foo()` and uses an H4 `#### Parameters` heading — matches the reference-object pages that aggregate many methods. + * `'page'`: skip the method-name title and use an H2 `## Parameters` heading — for one-method-per-docs-page surfaces, like the backend API endpoints. + * @typedef {'reference' | 'page'} MethodFormat + */ + +/** @param {string} pageUrl */ +function methodFormatForPageUrl(pageUrl) { + return pageUrl in BACKEND_API_CONFIG + ? /** @type {MethodFormat} */ ('page') + : /** @type {MethodFormat} */ ('reference'); +} + +/** + * @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]; +} + +/** + * Must stay aligned with allowlisted `propertiesTable` filtering in `custom-theme.mjs` (`isCallableInterfaceProperty` and `@extractMethods`: extracted here, not listed as properties). Nested tables pass `applyAllowlistedPropertyTableRowFilters: false`. + * + * @param {import('typedoc').DeclarationReflection} decl + * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx + */ +function shouldExtractCallableMember(decl, ctx) { + if (decl.kind === ReflectionKind.Method) { + return true; + } + if ( + decl.kind === ReflectionKind.Property || + decl.kind === ReflectionKind.Accessor || + decl.kind === ReflectionKind.Variable + ) { + return isCallableInterfaceProperty(decl, ctx.helpers); + } + return false; +} + +/** + * @param {import('typedoc').DeclarationReflection} decl + */ +function hasExtractMethodsModifier(decl) { + return Boolean(decl.comment?.hasModifier('@extractMethods')); +} + +/** + * @param {import('typedoc-plugin-markdown').MarkdownPageEvent} output + * @returns {string | undefined} + */ +function matchReferenceObjectPageUrl(output) { + if (!output.url) { + return undefined; + } + const normalized = output.url.replace(/\\/g, '/'); + if (normalized in REFERENCE_OBJECT_CONFIG) return normalized; + if (normalized in BACKEND_API_CONFIG) return normalized; + return undefined; +} + +/** @param {string} pageUrl */ +function configEntryForPageUrl(pageUrl) { + return /** @type {import('./reference-objects.mjs').REFERENCE_OBJECT_CONFIG[keyof typeof REFERENCE_OBJECT_CONFIG]} */ ( + REFERENCE_OBJECT_CONFIG[/** @type {keyof typeof REFERENCE_OBJECT_CONFIG} */ (pageUrl)] ?? + BACKEND_API_CONFIG[/** @type {keyof typeof BACKEND_API_CONFIG} */ (pageUrl)] + ); +} + +/** + * Wrap the name portion of bare method headings on a parent page in backticks: + * `^## methodName()` → `` ^## `methodName()` ``. Applies to H2–H4 headings whose full text is + * an identifier followed by `()` with no other content — matches typedoc's natural rendering of + * callable signature titles on aggregator pages (e.g. `agent-task-api.mdx`). Idempotent: headings + * that already contain a backtick are left alone. + * + * @param {string} contents + * @returns {string} + */ +function addBackticksToBareMethodHeadings(contents) { + if (!contents) return contents; + return contents.replace(/^(#{2,4}) ([A-Za-z_$][A-Za-z0-9_$]*)\(\)\s*$/gm, '$1 `$2()`'); +} + +/** + * Replace each method's natural `### Parameters` (or `## Parameters` on single-callable pages) + * section with the canonical `parametersMarkdownTable` output for the same signature at the same + * heading level. Routes every parameters table through the same code path: + * + * - single nominal param (e.g. `create(params: CreateAgentTaskParams)`): section becomes + * `` `TypeName` `` (backticked) + the propertiesTable-based expanded rows. + * - multi-arg or non-nominal (e.g. `revoke(agentTaskId: string)`): section stays `Parameters` + * + parametersTable output. + * + * Handles two page shapes: + * 1. **Aggregator pages** (backend API parents, namespaces like `api-keys-namespace.mdx`): each + * method is a `## `methodName()` ` section with `### Parameters` inside. Iterates `decl.children`. + * 2. **Single-callable pages** (`server-get-token.mdx`, React hooks): no method heading, `## Parameters` + * at top-level. Uses `decl`'s own signature. + * + * Runs uniformly across all pages. `extract-returns-and-params.mjs` is responsible for renaming + * `` ## `TypeName` `` back to `## Parameters` on the react-package pages it post-processes (so the + * sibling `-params.mdx` extraction still finds the section) — this keeps theme output uniform and + * pushes downstream-consumer knowledge into the downstream tool. + * + * Applies link replacements manually because this listener runs after `custom-plugin.mjs`'s pass. + * + * @param {string} contents + * @param {import('typedoc').DeclarationReflection | import('typedoc').Reflection} decl + * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx + * @returns {string} + */ +function swapParentPageParametersSections(contents, decl, ctx) { + if (!contents) return contents; + let result = contents; + + /** @param {import('typedoc').SignatureReflection} sig @param {number} level */ + const renderParams = (sig, level) => { + const skip = + Boolean(/** @type {any} */ (sig).comment?.hasModifier?.('@skipParametersSection')) || + Boolean(/** @type {any} */ (sig).parent?.comment?.hasModifier?.('@skipParametersSection')); + if (skip) return undefined; + overlayParamCommentsFromSignatureBlockTags(sig); + const parent = /** @type {import('typedoc').DeclarationReflection} */ (sig.parent); + const instantiationMap = parent ? getGenericInstantiationMapFromCallableProperty(parent) : undefined; + const md = parametersMarkdownTable(sig, ctx, instantiationMap, level); + if (!md) return undefined; + return applyCatchAllMdReplacements(applyRelativeLinkReplacements(md)); + }; + + const declWithChildren = /** @type {import('typedoc').DeclarationReflection} */ (decl); + const callableChildren = (declWithChildren.children ?? []).filter( + /** @param {import('typedoc').DeclarationReflection} c */ c => { + if (c.name.startsWith('__')) return false; + return shouldExtractCallableMember(c, ctx); + }, + ); + + if (callableChildren.length > 0) { + // Aggregator page: iterate `## `methodName()` ` blocks, swap the nested params section. + for (const child of callableChildren) { + const sig = getPrimaryCallSignature(/** @type {import('typedoc').DeclarationReflection} */ (child)); + if (!sig) continue; + const heading = `## \`${child.name}()\``; + const startIdx = result.indexOf(heading); + if (startIdx === -1) continue; + const rest = result.slice(startIdx); + const paramsMatch = rest.match(/\n(##+) Parameters\n[\s\S]*?(?=\n(?:##+ |---)|$)/); + if (!paramsMatch || paramsMatch.index === undefined) continue; + const level = paramsMatch[1].length; + const newParamsMd = renderParams(sig, level); + if (!newParamsMd) continue; + const absStart = startIdx + paramsMatch.index; + const absEnd = absStart + paramsMatch[0].length; + result = `${result.slice(0, absStart)}\n${newParamsMd.trimEnd()}\n${result.slice(absEnd)}`; + } + return result; + } + + // Single-callable page: `decl` is (or wraps) the signature. Find the top-level Parameters section. + /** @type {import('typedoc').SignatureReflection | undefined} */ + const sig = + /** @type {any} */ (decl).signatures?.[0] ?? + /** @type {any} */ ((decl).kind && getPrimaryCallSignature(declWithChildren)) ?? + undefined; + if (!sig) return result; + const paramsMatch = result.match(/(^|\n)(##+) Parameters\n[\s\S]*?(?=\n(?:##+ |---)|$)/); + if (!paramsMatch || paramsMatch.index === undefined) return result; + const level = paramsMatch[2].length; + const newParamsMd = renderParams(sig, level); + if (!newParamsMd) return result; + const leadingNl = paramsMatch[1]; + const absStart = paramsMatch.index + leadingNl.length; + const absEnd = paramsMatch.index + paramsMatch[0].length; + result = `${result.slice(0, absStart)}${newParamsMd.trimEnd()}\n${result.slice(absEnd)}`; + return result; +} + +/** + * Check whether the page contents already include a `## methodName()` H2 heading (either bare or + * backticked) for the given method name. Used to avoid duplicating a synthesized aggregator section + * on pages where typedoc-plugin-markdown already emits per-method headings naturally (backend + * class-based API pages). + * + * @param {string} contents + * @param {string} name + * @returns {boolean} + */ +function contentsHaveMethodHeading(contents, name) { + if (!contents) return false; + const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const re = new RegExp(`(^|\\n)## \`?${escaped}\`?\\(\\)\`?[ \\t]*(\\n|$)`); + return re.test(contents); +} + +/** + * Render the canonical H2 property-table section for a parent page. Shape: + * + * ## `name` + * + * + * + * `namespacePropertyTable` entries list the non-callable members of an `@extractMethods` namespace + * (e.g. `## `verifications`` lists `emailAddress`, `phoneNumber`, ...). `memberPropertyTable` + * entries expand one non-callable member's object shape (e.g. `## `emailLink.verification``). + * + * Sub-doc equivalents produced by the marker-block path use H3 (`### `name``); text-slice demotes + * the H2 back to H3 on reshape, so the sub-doc is byte-identical to the marker-block output. + * + * @param {Extract} entry + * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx + * @returns {string} + */ +function renderAggregatorPropertyTableSection(entry, ctx) { + const displayName = entry.kind === 'namespacePropertyTable' ? entry.decl.name : entry.qualifiedName; + const description = commentSummaryAndBody(entry.decl.comment); + /** @type {import('typedoc').DeclarationReflection[]} */ + let propsUnsorted; + if (entry.kind === 'namespacePropertyTable') { + propsUnsorted = entry.nonCallableMembers; + } else { + propsUnsorted = resolveObjectShapeMembersForPropertyTable(entry.decl.type) ?? []; + } + if (!propsUnsorted.length) return ''; + const props = [...propsUnsorted].sort((a, b) => a.name.localeCompare(b.name)); + const tableMd = renderMemberTableOmittingExampleBlocks(props, ctx, () => + ctx.partials.propertiesTable( + props, + /** @type {Parameters[1]} */ + ({ + kind: ReflectionKind.Interface, + isEventProps: false, + applyAllowlistedPropertyTableRowFilters: false, + }), + ), + ); + if (!tableMd?.trim()) return ''; + const title = `## \`${displayName}\``; + const raw = [title, '', description, '', tableMd].filter(Boolean).join('\n\n'); + return `${applyCatchAllMdReplacements(applyRelativeLinkReplacements(raw)).trim()}\n`; +} + +/** + * Render one method's canonical "aggregator" H2 section for a parent page — the shape every parent + * page uses so `extract-methods.mjs` can slice it and reshape into the per-method extracted file + * with pure text ops (no reflection access needed downstream): + * + * ## `name()` + * (summary + non-@returns block tags) + * ```typescript + * function name(...): ReturnType + * ``` + * ### `TypeName` (nominal single-arg) OR ### Parameters + * + * ### Returns + * `Type` — (from ctx.partials.signatureReturns) + * + * Used to synthesize sections for callable members typedoc doesn't render as separate H2s — + * function-typed properties on interfaces (`end: () => Promise`) and members declared on + * `extraMethodInterfaces`. Sections synthesized here are indistinguishable from natural sections + * on the same page after all reshapers have run. + * + * Applies link replacements manually because this listener runs after `custom-plugin.mjs`'s pass. + * For property-table entries, delegates to {@link renderAggregatorPropertyTableSection}. + * + * @param {AggregatorMethodEntry} entry + * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx + * @returns {string} + */ +function renderAggregatorMethodSection(entry, ctx) { + if (entry.kind === 'namespacePropertyTable' || entry.kind === 'memberPropertyTable') { + return renderAggregatorPropertyTableSection(entry, ctx); + } + const { decl, qualifiedName } = entry; + const sig = getPrimaryCallSignature(decl); + if (!sig) return ''; + const displayName = qualifiedName ?? decl.name; + const title = `## \`${displayName}()\``; + const comment = decl.comment ?? sig.comment; + const c = comment ? (applyTodoStrippingToComment(comment) ?? comment) : undefined; + const summary = c ? displayPartsToString(c.summary).trim() : ''; + const block = + c?.blockTags + ?.filter(t => !BLOCK_TAGS_OMITTED_FROM_EXTRACTED_METHOD_PROSE.has(t.tag)) + .map(t => displayPartsToString(t.content).trim()) + .filter(Boolean) + .join('\n\n') ?? ''; + const description = [summary, block].filter(Boolean).join('\n\n'); + const instantiationMap = getGenericInstantiationMapFromCallableProperty(decl); + const signatureBlock = ['```typescript', formatTypeScriptSignature(sig, displayName, instantiationMap), '```'].join( + '\n', + ); + const skipParametersSection = + Boolean(decl.comment?.hasModifier('@skipParametersSection')) || + Boolean(sig.comment?.hasModifier('@skipParametersSection')); + let paramsMd = ''; + if (!skipParametersSection) { + overlayParamCommentsFromSignatureBlockTags(sig); + paramsMd = parametersMarkdownTable(sig, ctx, instantiationMap, 3); + } + // Function-typed properties (`create: (params) => Promise`) carry `@returns` on the property + // comment, not on the signature comment. `signatureReturns` reads sig.comment.@returns; overlay + // the decl comment's @returns onto sig so the rendered `### Returns` section carries the + // description text. + const declReturnsTag = decl.comment?.getTag('@returns'); + const sigReturnsTag = sig.comment?.getTag('@returns'); + if (declReturnsTag?.content?.length && !sigReturnsTag?.content?.length) { + if (!sig.comment) sig.comment = new Comment(); + sig.comment.blockTags = [...(sig.comment.blockTags ?? []), declReturnsTag]; + } + const returnsMd = ctx.partials.signatureReturns(sig, { headingLevel: 3 }); + // Apply link replacements to the prose chunks only. The typescript signature block must stay + // raw — `applyRelativeLinkReplacements` is line-based and would rewrite bare type identifiers + // inside the fenced code block into `[Type](...)` markdown link syntax, breaking downstream + // slicing. + const proseChunks = [title, description, paramsMd.trimEnd(), returnsMd] + .filter(s => s && s.length > 0) + .map(s => applyCatchAllMdReplacements(applyRelativeLinkReplacements(s))); + const chunks = [proseChunks[0], proseChunks[1], signatureBlock, ...proseChunks.slice(2)].filter( + s => s && s.length > 0, + ); + return chunks.join('\n\n'); +} + +/** + * Delete a `## methodName()` H2 section (heading through the next `## ` or end-of-file) from the + * page contents. Used when a natural typedoc-emitted section needs to be replaced by a synthesized + * one — e.g. overloaded backend-class methods where typedoc emits `### Call Signature` sub-headings + * per overload, but the aggregator synthesizes a single section for the primary signature only + * (via `getPrimaryCallSignature`). + * + * @param {string} contents + * @param {string} name + * @returns {string} + */ +function removeMethodH2Section(contents, name) { + if (!contents) return contents; + const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const headingRe = new RegExp(`(^|\\n)## \`?${escaped}\`?\\(\\)\`?[ \\t]*\\n`, ''); + const m = contents.match(headingRe); + if (!m || m.index === undefined) return contents; + const startIdx = m.index + (m[1] === '\n' ? 1 : 0); + const afterHeading = m.index + m[0].length; + const rest = contents.slice(afterHeading); + const nextH2 = rest.search(/\n## /); + const endIdx = nextH2 === -1 ? contents.length : afterHeading + nextH2 + 1; + // Also trim the trailing `***` typedoc separator that natural sections use between overloads. + let sliceEnd = endIdx; + const trailingSep = contents.slice(sliceEnd).match(/^\*+\s*\n+/); + if (trailingSep) sliceEnd += trailingSep[0].length; + return contents.slice(0, startIdx) + contents.slice(sliceEnd); +} + +/** + * Insert a `` ```typescript ... ``` `` signature code block into each natural (typedoc-emitted) + * `## methodName()` H2 section on the parent page. Positioned after the section's description + * prose and before the first `### ` subheading — matching the position `renderAggregatorMethodSection` + * uses for synthesized sections. This is what makes the parent's H2 sections a self-contained + * source for `extract-methods.mjs`: the extracted file's signature can be sliced out of the parent + * verbatim rather than regenerated downstream. + * + * Idempotent-ish: relies on `contentsHaveMethodHeading` matching the (possibly-backticked) title, + * and looks for the absence of a fenced ```typescript block already inside the section. + * + * @param {string} contents + * @param {import('typedoc').DeclarationReflection[]} callableDecls - direct callable members that + * may have natural sections. Callers pass the same list they enumerate for synthesis-vs-natural + * decisions. + * @returns {string} + */ +function insertSignatureBlocksIntoNaturalMethodSections(contents, callableDecls) { + if (!contents) return contents; + let result = contents; + for (const decl of callableDecls) { + const sig = getPrimaryCallSignature(decl); + if (!sig) continue; + const escaped = decl.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const headingRe = new RegExp(`(^|\\n)## \`?${escaped}\`?\\(\\)\`?[ \\t]*\\n`); + const headingMatch = result.match(headingRe); + if (!headingMatch || headingMatch.index === undefined) continue; + const sectionStart = headingMatch.index + headingMatch[0].length; + const rest = result.slice(sectionStart); + const nextH2 = rest.search(/\n## /); + const sectionEnd = nextH2 === -1 ? result.length : sectionStart + nextH2; + const section = result.slice(sectionStart, sectionEnd); + if (/```typescript\n/.test(section)) continue; + const firstSubheading = section.search(/\n### /); + const descriptionEnd = firstSubheading === -1 ? section.length : firstSubheading; + const description = section.slice(0, descriptionEnd); + const remainder = section.slice(descriptionEnd); + const instantiationMap = getGenericInstantiationMapFromCallableProperty(decl); + const signatureBlock = ['```typescript', formatTypeScriptSignature(sig, decl.name, instantiationMap), '```'].join( + '\n', + ); + const before = result.slice(0, sectionStart); + const rebuilt = `${description.trimEnd()}\n\n${signatureBlock}\n${remainder.startsWith('\n') ? remainder : `\n${remainder}`}`; + result = `${before}${rebuilt}${result.slice(sectionEnd)}`; + } + return result; +} + +/** + * @typedef {( + * | { kind?: 'method', decl: import('typedoc').DeclarationReflection, qualifiedName?: string } + * | { kind: 'namespacePropertyTable', decl: import('typedoc').DeclarationReflection, nonCallableMembers: import('typedoc').DeclarationReflection[] } + * | { kind: 'memberPropertyTable', decl: import('typedoc').DeclarationReflection, qualifiedName: string } + * )} AggregatorMethodEntry + */ + +/** + * Enumerate per-H2-section entries for the given parent page. Three shapes: + * - `method`: direct callable children, callables from `extraMethodInterfaces`, and callables + * inside `@extractMethods` namespaces (qualified names like `emailCode.sendCode`). + * - `namespacePropertyTable`: an `@extractMethods` namespace with at least one non-callable + * object-like member. Rendered as a `## `namespace`` H2 listing those non-callable members. + * - `memberPropertyTable`: a non-callable, object-like member inside an `@extractMethods` + * namespace. Rendered as a `## `namespace.member`` H2 expanding that member's object shape. + * + * @param {import('typedoc').DeclarationReflection} decl + * @param {import('typedoc').ProjectReflection | undefined} project + * @param {ReturnType} entry + * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx + * @returns {AggregatorMethodEntry[]} + */ +function enumerateAggregatorMethodDecls(decl, project, entry, ctx) { + /** @type {AggregatorMethodEntry[]} */ + const out = []; + /** @param {import('typedoc').DeclarationReflection} d */ + const pushCallables = d => { + for (const child of d.children ?? []) { + if (child.name.startsWith('__')) continue; + const cd = /** @type {import('typedoc').DeclarationReflection} */ (child); + if (hasExtractMethodsModifier(cd)) { + // `@extractMethods` namespace: enumerate its object-like members. Callables produce + // qualified-name method entries (`emailCode.sendCode`). Non-callable members with a + // resolvable object shape produce `memberPropertyTable` entries; if any such members + // exist, a `namespacePropertyTable` entry is also emitted for the namespace itself. + const members = resolveDeclarationWithObjectMembers(cd.type, project) ?? []; + /** @type {import('typedoc').DeclarationReflection[]} */ + const nonCallableMembers = []; + for (const nested of members) { + if (nested.name.startsWith('__')) continue; + const nd = /** @type {import('typedoc').DeclarationReflection} */ (nested); + if (shouldExtractCallableMember(nd, ctx)) { + out.push({ kind: 'method', decl: nd, qualifiedName: `${cd.name}.${nd.name}` }); + continue; + } + nonCallableMembers.push(nd); + if (resolveObjectShapeMembersForPropertyTable(nd.type)?.length) { + out.push({ kind: 'memberPropertyTable', decl: nd, qualifiedName: `${cd.name}.${nd.name}` }); + } + } + if (nonCallableMembers.length) { + out.push({ kind: 'namespacePropertyTable', decl: cd, nonCallableMembers }); + } + continue; + } + if (shouldExtractCallableMember(cd, ctx)) out.push({ kind: 'method', decl: cd }); + } + }; + pushCallables(decl); + const extraMethodInterfaces = entry && 'extraMethodInterfaces' in entry ? entry.extraMethodInterfaces : undefined; + if (Array.isArray(extraMethodInterfaces) && project) { + for (const extra of extraMethodInterfaces) { + const extraDecl = findInterfaceOrClass(project, extra.symbol, extra.declarationHint); + if (!extraDecl) { + console.warn(`[custom-theme] extraMethodInterfaces: could not find "${extra.symbol}"`); + continue; + } + pushCallables(extraDecl); + } + } + return out; +} + +/** + * Wrap the `## Properties` section body in the page contents with HTML-comment markers so the slicer can extract it after prettier runs. + * Idempotent: no-op if markers already exist. + * + * @param {string} contents + * @returns {string} + */ +function wrapPropertiesSectionWithMarkers(contents) { + if (!contents) return contents; + if (contents.includes('')) return contents; + const normalized = contents.replace(/\r\n/g, '\n'); + // Match `## Properties` whether it's at start-of-file or after a newline. The slicer's strip + // regex (in `extract-methods.mjs`) intentionally requires a leading `\n` so pages where + // `## Properties` is the very first line (e.g. `billing-namespace.mdx`) do NOT have the section + // stripped from the parent page — matching the pre-refactor `stripReferenceObjectPropertiesSection` + // behavior. Wrapping still happens (so `properties.mdx` gets written), which also matches + // pre-refactor (`extractPropertiesSectionBody` used the same `(^|\n)` prefix). + const m = normalized.match(/(^|\n)## Properties\n+/); + if (!m || m.index === undefined) return contents; + const headingEnd = m.index + m[0].length; + const rest = normalized.slice(headingEnd); + const nextH2 = rest.search(/\n## /); + const bodyEnd = nextH2 === -1 ? normalized.length : headingEnd + nextH2; + const before = normalized.slice(0, headingEnd); + const body = normalized.slice(headingEnd, bodyEnd); + const after = normalized.slice(bodyEnd); + return `${before}\n${body.trimEnd()}\n\n${after}`; +} + /** * @param {import('typedoc-plugin-markdown').MarkdownApplication} app */ export function load(app) { app.renderer.defineTheme('clerkTheme', ClerkMarkdownTheme); + + // Negative priority ensures this fires AFTER `custom-plugin.mjs`'s `MarkdownPageEvent.END` + // listener (which applies `applyRelativeLinkReplacements` + `applyCatchAllMdReplacements` at the + // default priority of 0). If we emitted first, custom-plugin's second pass would re-apply link + // replacements to synthesized aggregator sections we already processed — mangling the + // ```typescript``` signature fences (custom-plugin's replacements are line-based and do not + // skip fenced code blocks). `extract-methods.mjs`'s slicer registers at an even lower priority + // so it fires after this emit. + app.renderer.on( + MarkdownPageEvent.END, + output => { + const decl = /** @type {import('typedoc').DeclarationReflection | undefined} */ (output.model); + if (!decl) return; + const theme = /** @type {InstanceType | undefined} */ (app.renderer.theme); + if (!theme || typeof theme.getRenderContext !== 'function') return; + const ctx = /** @type {import('typedoc-plugin-markdown').MarkdownThemeContext} */ ( + theme.getRenderContext(output) + ); + + output.contents = addBackticksToBareMethodHeadings(output.contents ?? ''); + output.contents = swapParentPageParametersSections(output.contents ?? '', decl, ctx); + + const pageUrl = matchReferenceObjectPageUrl(output); + // Reference-object / backend-api specific work: + // 1. Synthesize per-H2 aggregator sections for callable and property-table members that + // typedoc-plugin-markdown doesn't render on its own (function-typed interface properties, + // `extraMethodInterfaces`, `@extractMethods` namespace members). + // 2. Wrap the `## Properties` section for post-prettier extraction into `properties.mdx`. + // `extract-methods.mjs` slices the resulting parent page into `methods/*.mdx` sub-docs. + if (pageUrl) { + const entry = configEntryForPageUrl(pageUrl); + const methodFormat = methodFormatForPageUrl(pageUrl); + const project = output.project; + if (decl.children && project) { + const allEntries = enumerateAggregatorMethodDecls(decl, project, entry, ctx); + // Property-table entries are always synthesized (typedoc emits nothing for them); + // handle them alongside `missing` methods below. Only the method entries participate + // in the natural-heading dance. + const methodEntries = allEntries.filter(e => !e.kind || e.kind === 'method'); + const propertyTableEntries = allEntries.filter( + e => e.kind === 'namespacePropertyTable' || e.kind === 'memberPropertyTable', + ); + // Overloaded methods on backend classes render natively as `### Call Signature` + // sub-sections per overload. To keep the parent-page shape uniform across single and + // multi-overload methods (so `extract-methods.mjs` can slice with one code path), remove + // the overloaded natural section and treat the method as missing — it'll be re-synthesized + // below using `getPrimaryCallSignature`. + for (const e of methodEntries) { + const nm = e.qualifiedName ?? e.decl.name; + if ((e.decl.signatures?.length ?? 0) > 1 && contentsHaveMethodHeading(output.contents ?? '', nm)) { + output.contents = removeMethodH2Section(output.contents ?? '', nm); + } + } + const naturalCallables = methodEntries.filter(e => + contentsHaveMethodHeading(output.contents ?? '', e.qualifiedName ?? e.decl.name), + ); + const missing = [ + ...methodEntries.filter( + e => !contentsHaveMethodHeading(output.contents ?? '', e.qualifiedName ?? e.decl.name), + ), + ...propertyTableEntries, + ]; + // Backend class pages: typedoc-plugin-markdown already emits `## methodName()` H2s. + // Inject a `` ```typescript ``` `` signature block after each natural section's + // description so the parent's section is self-contained (matches shape of synthesized + // sections below). Only applies to unqualified members — qualified-name entries from + // `@extractMethods` namespaces are never natural on the parent page. + const naturalUnqualified = naturalCallables.filter(e => !e.qualifiedName).map(e => e.decl); + if (naturalUnqualified.length) { + output.contents = insertSignatureBlocksIntoNaturalMethodSections(output.contents ?? '', naturalUnqualified); + } + if (missing.length) { + const sections = missing.map(e => renderAggregatorMethodSection(e, ctx)).filter(Boolean); + if (sections.length) { + // Insert synthesized sections BEFORE `## Properties` so extract-methods' strip + // (which drops `## Properties` and everything after during the transitional refactor) + // preserves them. Falls back to appending at end when the page has no properties + // section (backend class pages have methods only). + const block = sections.join('\n\n'); + const currentContents = output.contents ?? ''; + const propsIdx = currentContents.search(/(^|\n)## Properties\n/); + if (propsIdx === -1) { + output.contents = `${currentContents.trimEnd()}\n\n${block}\n`; + } else { + const before = currentContents.slice(0, propsIdx); + const after = currentContents.slice(propsIdx); + output.contents = `${before.trimEnd()}\n\n${block}\n\n${after.replace(/^\n+/, '')}`; + } + } + } + } + output.contents = wrapPropertiesSectionWithMarkers(output.contents ?? ''); + } + }, + -100, + ); } class ClerkMarkdownTheme extends MarkdownTheme { @@ -1860,85 +2495,1117 @@ function swap(arr, i, j) { return arr; } +// --------------------------------------------------------------------------- +// Extracted-methods helpers (moved from extract-methods.mjs). +// --------------------------------------------------------------------------- + /** - * @param {import('typedoc').DeclarationReflection} prop - * @param {import('typedoc-plugin-markdown').MarkdownThemeContext['helpers']} helpers + * @param {number} level + * @param {string} text */ -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()); +function markdownHeading(level, text) { + const l = Math.min(Math.max(level, 1), 6); + return `${'#'.repeat(l)} ${text}`; } /** - * 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. - * E.g. `navigate: CustomNavigation` in clerk.ts + * Heading whose visible title is a type/identifier (nominal single-parameter object sections) needs to get wrapped in backticks. + * + * @param {number} level + * @param {string} text + */ +function markdownHeadingInlineCode(level, text) { + const l = Math.min(Math.max(level, 1), 6); + const t = text.trim(); + return `${'#'.repeat(l)} \`${t}\``; +} + +/** + * 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, ' '); +} + +/** + * 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}\``; +} + +/** + * Build the description cell for a nested `param.field` row: summary text plus a leading `**Deprecated.**` marker (with the `@deprecated` tag's body) when that tag is present. Matches what typedoc-plugin-markdown's `parseParams` emits for inline `{ … }` params — fields documented only via `@deprecated` (no summary) otherwise rendered as `—` here, hiding important guidance. + * Returns `'—'` when both summary and `@deprecated` are empty. + * + * @param {import('typedoc').Comment | undefined} comment + */ +function renderNestedRowDescription(comment) { + const summaryText = comment?.summary?.length ? displayPartsToString(comment.summary).trim() : ''; + const deprecatedTag = comment?.blockTags?.find(t => t.tag === '@deprecated'); + const deprecatedText = deprecatedTag ? displayPartsToString(deprecatedTag.content).trim() : ''; + const deprecatedMd = deprecatedTag ? `**Deprecated.**${deprecatedText ? ` ${deprecatedText}` : ''}` : ''; + const combined = [summaryText, deprecatedMd].filter(Boolean).join(' '); + return combined || '—'; +} + +/** + * @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(''); +} + +/** + * Walk instantiated generic / alias chains (e.g. `CheckAuthorization` → `CheckAuthorizationFn` → `(…) => boolean`) until we find a {@link ReflectionType} call signature. Uses reflection IDs to avoid infinite loops. * * @param {import('typedoc').Type | undefined} t - * @param {import('typedoc-plugin-markdown').MarkdownThemeContext['helpers']} helpers - * @param {Set} seenReflectionIds - * @returns {boolean} + * @param {Set} visitedReflectionIds + * @returns {import('typedoc').SignatureReflection | undefined} */ -function isCallablePropertyValueType(t, helpers, seenReflectionIds) { - if (!t) { - return false; +function getCallSignatureFromType(t, visitedReflectionIds) { + if (!t || typeof t !== 'object') { + return undefined; } - if (t.type === 'optional' && 'elementType' in t) { - return isCallablePropertyValueType( + const tag = /** @type {{ type?: string }} */ (t).type; + if (tag === 'optional' && 'elementType' in t) { + return getCallSignatureFromType( /** @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)), + visitedReflectionIds, ); - 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.declaration?.signatures?.length) { + return t.declaration.signatures[0]; + } + return undefined; } 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 target = t.reflection; + if ( + target && + 'signatures' in target && + /** @type {{ signatures?: import('typedoc').SignatureReflection[] }} */ (target).signatures?.length + ) { + return /** @type {import('typedoc').DeclarationReflection} */ (target).signatures[0]; } - const refId = ref.id; - if (refId != null && seenReflectionIds.has(refId)) { - return false; + if (!target || !('kind' in target)) { + return undefined; } - if (refId != null) { - seenReflectionIds.add(refId); + const decl = /** @type {import('typedoc').DeclarationReflection} */ (target); + const id = decl.id; + if (id != null) { + if (visitedReflectionIds.has(id)) { + return undefined; + } + visitedReflectionIds.add(id); } 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 + if (decl.kind === ReflectionKind.TypeAlias && decl.type) { + return getCallSignatureFromType(decl.type, visitedReflectionIds); + } + } finally { + if (id != null) { + visitedReflectionIds.delete(id); + } + } + return undefined; + } + if (t instanceof UnionType) { + for (const arm of t.types) { + const sig = getCallSignatureFromType(arm, visitedReflectionIds); + if (sig) { + return sig; + } + } + return undefined; + } + if (t instanceof IntersectionType) { + for (const arm of t.types) { + const sig = getCallSignatureFromType(arm, visitedReflectionIds); + if (sig) { + return sig; + } + } + } + return undefined; +} + +/** + * @param {import('typedoc').SignatureReflection} sig + */ +function signatureIsDeprecated(sig) { + const c = sig.comment; + if (!c) return false; + if (typeof c.hasModifier === 'function' && c.hasModifier('@deprecated')) return true; + return c.blockTags?.some(t => t.tag === '@deprecated') ?? false; +} + +/** + * typedoc maps `@param paramName description` to `parameter.comment` during conversion of the + * signature that physically owns the JSDoc. With overloads + an implementation, the impl's `@param` + * tags don't transfer to overload parameters — even when typedoc copies the impl's comment onto + * each overload's `sig.comment.blockTags`. Without descriptions on `param.comment`, typedoc-plugin-markdown + * drops the Description column from the parameters table. + * + * Overlay missing parameter comments from any matching `@param` block tag on the signature comment + * so the rendered table shows the descriptions the author wrote on the implementation's JSDoc. + * + * @param {import('typedoc').SignatureReflection} sig + */ +function overlayParamCommentsFromSignatureBlockTags(sig) { + const params = sig.parameters; + if (!params?.length) return; + const blockTags = sig.comment?.blockTags; + if (!blockTags?.length) return; + for (const p of params) { + if (p.comment?.summary?.length) continue; + const tag = blockTags.find(t => t.tag === '@param' && t.name === p.name); + if (!tag?.content?.length) continue; + p.comment = new Comment(tag.content); + } +} + +/** + * @param {import('typedoc').DeclarationReflection} decl + * @returns {import('typedoc').SignatureReflection | undefined} + */ +function getPrimaryCallSignature(decl) { + if (decl.signatures?.length) { + // Prefer the first non-`@deprecated` overload. typedoc treats each overload as a separate + // signature, and selecting a deprecated one usually means rendering the form users shouldn't + // use — and (often) one whose JSDoc isn't where the canonical `@param` descriptions live. + const firstActive = decl.signatures.find(s => !signatureIsDeprecated(s)); + return firstActive ?? decl.signatures[0]; + } + const t = decl.type; + if (t && 'declaration' in t && t.declaration?.signatures?.length) { + return t.declaration.signatures[0]; + } + // E.g. `navigate: CustomNavigation` — for `type Fn = () => void`, signatures often live on the inner `declaration` of `alias.type` (ReflectionType), not on `alias.signatures` (see `custom-theme.mjs` `isCallablePropertyValueType`). + if (t && typeof t === 'object' && 'type' in t && /** @type {{ type?: string }} */ (t).type === 'reference') { + const ref = /** @type {import('typedoc').ReferenceType} */ (t); + const target = ref.reflection; + const sigs = + target && 'signatures' in target + ? /** @type {{ signatures?: import('typedoc').SignatureReflection[] }} */ (target).signatures + : undefined; + if (sigs?.length) { + return sigs[0]; + } + const aliasTarget = /** @type {import('typedoc').DeclarationReflection | undefined} */ ( + target && 'kind' in target ? target : undefined + ); + if (aliasTarget?.kind === ReflectionKind.TypeAlias && aliasTarget.type && 'declaration' in aliasTarget.type) { + const inner = /** @type {import('typedoc').ReflectionType} */ (aliasTarget.type).declaration; + if (inner?.signatures?.length) { + return inner.signatures[0]; + } + } + // `type X = SomeFn` — RHS is often ReferenceType (generic alias), not ReflectionType; recurse (e.g. `checkAuthorization: CheckAuthorization`). + if (aliasTarget?.kind === ReflectionKind.TypeAlias && aliasTarget.type) { + const fromRhs = getCallSignatureFromType(aliasTarget.type, new Set()); + if (fromRhs) { + return fromRhs; + } + } + const fromRef = getCallSignatureFromType(ref, new Set()); + if (fromRef) { + return fromRef; + } + } + return undefined; +} + +/** + * For `prop: OuterAlias` where `type OuterAlias = SomeFn`, maps generic parameter names on `SomeFn` to the instantiated type arguments (e.g. `Params` → `CheckAuthorizationParams`). + * + * @param {import('typedoc').DeclarationReflection} propertyDecl + * @returns {Map | undefined} + */ +function getGenericInstantiationMapFromCallableProperty(propertyDecl) { + const t = unwrapOptional(propertyDecl.type); + if (!(t instanceof ReferenceType) || !t.reflection) { + return undefined; + } + const alias = /** @type {import('typedoc').DeclarationReflection} */ (t.reflection); + if (!alias.kindOf(ReflectionKind.TypeAlias) || !alias.type) { + return undefined; + } + const inner = unwrapOptional(alias.type); + if (!(inner instanceof ReferenceType) || !inner.typeArguments?.length || !inner.reflection) { + return undefined; + } + const generic = /** @type {import('typedoc').DeclarationReflection} */ (inner.reflection); + const tpls = generic.typeParameters; + if (!tpls?.length) { + return undefined; + } + /** @type {Map} */ + const map = new Map(); + for (let i = 0; i < inner.typeArguments.length; i++) { + const tp = tpls[i]; + const arg = inner.typeArguments[i]; + if (tp?.name && arg) { + map.set(tp.name, arg); + } + } + return map.size ? map : undefined; +} + +/** + * Replace references to generic type parameters with instantiated types from {@link getGenericInstantiationMapFromCallableProperty}. + * + * @param {import('typedoc').Type | undefined} t + * @param {Map | undefined} map + * @returns {import('typedoc').Type | undefined} + */ +function substituteGenericParamRefsInType(t, map) { + if (!t || !map?.size) { + return t; + } + if (/** @type {{ type?: string }} */ (t).type === 'optional' && 'elementType' in t) { + const el = /** @type {{ elementType: import('typedoc').Type }} */ (t).elementType; + const next = substituteGenericParamRefsInType(el, map); + if (next && next !== el) { + return new OptionalType(/** @type {import('typedoc').SomeType} */ (/** @type {unknown} */ (next))); + } + return t; + } + if (t instanceof ReferenceType && map.has(t.name)) { + return map.get(t.name) ?? t; + } + return t; +} + +/** + * Resolve a property's type to its declared default when it references a `TypeParameter` reflection. + * Used when a generic alias is inlined into an intersection (e.g. `CreateParams = { … } & MetadataParams` where the `& MetadataParams` arm gets inlined as a reflection, losing the named reference) so the property table surfaces the resolved default type instead of the open type-parameter name (e.g. `TPublic` → `OrganizationPublicMetadata`). + * + * @param {import('typedoc').Type | undefined} t + * @returns {import('typedoc').Type | undefined} + */ +function substituteTypeParameterDefaultsInType(t) { + if (!t) { + return t; + } + if (/** @type {{ type?: string }} */ (t).type === 'optional' && 'elementType' in t) { + const el = /** @type {{ elementType: import('typedoc').Type }} */ (t).elementType; + const next = substituteTypeParameterDefaultsInType(el); + if (next && next !== el) { + return new OptionalType(/** @type {import('typedoc').SomeType} */ (/** @type {unknown} */ (next))); + } + return t; + } + if (t instanceof ReferenceType) { + const tp = /** @type {{ kindOf?: (k: number) => boolean, default?: import('typedoc').Type }} */ (t.reflection); + if (tp?.kindOf?.(ReflectionKind.TypeParameter) && tp.default) { + return tp.default; + } + } + return t; +} + +/** + * Clone each property and apply `substituteTypeParameterDefaultsInType` to its type — see comment on `substituteTypeParameterDefaultsInType` for the motivating case. + * + * @param {import('typedoc').DeclarationReflection[]} children + */ +function substituteTypeParameterDefaultsInChildren(children) { + return children.map(child => { + const next = substituteTypeParameterDefaultsInType(child.type); + if (next === child.type) { + return child; + } + return Object.assign(Object.create(Object.getPrototypeOf(child)), child, { type: next }); + }); +} + +/** + * @param {import('typedoc').SignatureReflection} sig + * @param {Map | undefined} instantiationMap + */ +function signatureWithInstantiation(sig, instantiationMap) { + if (!instantiationMap?.size) { + return sig; + } + const parameters = (sig.parameters ?? []).map(p => { + const newType = substituteGenericParamRefsInType(p.type, instantiationMap); + if (newType === p.type) { + return p; + } + return Object.assign(Object.create(Object.getPrototypeOf(p)), p, { type: newType }); + }); + const newReturn = substituteGenericParamRefsInType(sig.type, instantiationMap) ?? sig.type; + const out = Object.assign(Object.create(Object.getPrototypeOf(sig)), sig, { + parameters, + type: newReturn, + typeParameters: undefined, + }); + if (sig.project) { + out.project = sig.project; + } + return out; +} + +/** + * Object-literal (or single object arm of `T | null`) property rows for a properties table. + * + * @param {import('typedoc').SomeType | undefined} valueType + * @returns {import('typedoc').DeclarationReflection[] | undefined} + */ +function resolveObjectShapeMembersForPropertyTable(valueType) { + let t = unwrapOptional(valueType, { deep: true }); + if (t instanceof UnionType) { + const objectArms = t.types.filter(u => u instanceof ReflectionType && (u.declaration?.children?.length ?? 0) > 0); + if (objectArms.length !== 1) { + return undefined; + } + t = /** @type {import('typedoc').ReflectionType} */ (objectArms[0]); + } + if (!(t instanceof ReflectionType)) { + return undefined; + } + const kids = t.declaration?.children ?? []; + return kids.filter( + c => c.kind === ReflectionKind.Property || c.kind === ReflectionKind.Variable || c.kind === ReflectionKind.Accessor, + ); +} + +/** + * Plain TypeScript-like type text for ```typescript``` fences (no markdown / backticks from {@link MarkdownThemeContext.partials.someType}). + * + * @param {import('typedoc').Type | undefined} t + */ +function typeStringForTypeScriptFence(t) { + if (!t) { + return 'unknown'; + } + return removeLineBreaks(t.toString()); +} + +/** + * @param {import('typedoc').SignatureReflection} sig + * @param {string} memberName + * @param {Map | undefined} instantiationMap + */ +function formatTypeScriptSignature(sig, memberName, instantiationMap) { + const hideOuterTypeParams = Boolean(instantiationMap?.size) && (sig.typeParameters?.length ?? 0) > 0; + const typeParamStr = + !hideOuterTypeParams && sig.typeParameters?.length ? `<${sig.typeParameters.map(tp => tp.name).join(', ')}>` : ''; + const params = + sig.parameters?.map(p => { + const opt = p.flags.isOptional ? '?' : ''; + const rest = p.flags.isRest ? '...' : ''; + const t = substituteGenericParamRefsInType(p.type, instantiationMap) ?? p.type; + const typeStr = typeStringForTypeScriptFence(t); + return `${rest}${p.name}${opt}: ${typeStr}`; + }) ?? []; + const retT = substituteGenericParamRefsInType(sig.type, instantiationMap) ?? sig.type; + const ret = retT ? typeStringForTypeScriptFence(retT) : 'void'; + // Qualified names (`emailCode.sendCode`) aren't valid in `function foo.bar()` syntax; use the bare last segment — the parent is already in the heading above. + const displayName = memberName.includes('.') ? memberName.split('.').pop() : memberName; + return `function ${displayName}${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 c = applyTodoStrippingToComment(comment) ?? comment; + const summary = displayPartsToString(c.summary).trim(); + const block = c.blockTags + ?.filter(t => !BLOCK_TAGS_OMITTED_FROM_EXTRACTED_METHOD_PROSE.has(t.tag)) + .map(t => displayPartsToString(t.content).trim()) + .filter(Boolean) + .join('\n\n'); + const returnsLines = + c.blockTags + ?.filter(t => t.tag === '@returns') + .map(t => formatReturnsLineFromTag(t)) + .filter(Boolean) ?? []; + return [summary, block, ...returnsLines].filter(Boolean).join('\n\n'); +} + +/** + * @param {import('typedoc').DeclarationReflection} prop + */ +function propertyReflectionTypeIsNever(prop) { + const ty = unwrapOptional(prop.type, { deep: true }); + return ty?.type === 'intrinsic' && ty.name === 'never'; +} + +/** + * Union discriminators often use `otherProp?: never`. Prefer the branch with a documentable type. + * + * @param {import('typedoc').DeclarationReflection} existing + * @param {import('typedoc').DeclarationReflection} candidate + */ +function pickBetterUnionPropertyCandidate(existing, candidate) { + const existingNever = propertyReflectionTypeIsNever(existing); + const candidateNever = propertyReflectionTypeIsNever(candidate); + if (existingNever && !candidateNever) { + return candidate; + } + if (!existingNever && candidateNever) { + return existing; + } + const existingDoc = existing.comment?.summary?.length ?? 0; + const candidateDoc = candidate.comment?.summary?.length ?? 0; + return candidateDoc > existingDoc ? candidate : existing; +} + +/** + * Collect the set of string-literal keys from a type used as the second argument to `Omit` or `Pick` — a single literal (`'organizationId'`) or a union of literals (`'a' | 'b'`). Returns `undefined` if the type isn't a literal/literal-union (e.g. a `keyof` reference we can't resolve here), in which case callers should fall through to the generic-instantiation path. + * + * @param {import('typedoc').Type | undefined} t + * @returns {Set | undefined} + */ +function collectLiteralStringKeys(t) { + if (!t) { + return undefined; + } + if (t.type === 'literal' && typeof (/** @type {{ value: unknown }} */ (t).value) === 'string') { + return new Set([/** @type {{ value: string }} */ (t).value]); + } + if (t.type === 'union') { + const u = /** @type {import('typedoc').UnionType} */ (t); + /** @type {Set} */ + const keys = new Set(); + for (const inner of u.types) { + const got = collectLiteralStringKeys(inner); + if (!got) { + return undefined; + } + for (const k of got) keys.add(k); + } + return keys.size ? keys : undefined; + } + return undefined; +} + +/** + * Filter each arm to `Property` reflections and dedupe by name, returning a single sorted list + * (or `undefined` if every arm was empty). Used for intersection / union / generic-instantiation + * arm merges in {@link resolveDeclarationWithObjectMembers}. + * + * Default behavior is "later arm wins" overwrite (right for intersections + generic instantiations + * where every arm's properties are part of the final shape). For unions, set + * `{ skipNever: true, pickBetter: true }`: union arms often use `prop?: never` as a discriminator, + * so we drop those and keep the documentable branch when names collide. + * + * @param {Array} arms + * @param {{ skipNever?: boolean, pickBetter?: boolean }} [options] + * @returns {import('typedoc').DeclarationReflection[] | undefined} + */ +function mergePropertyArms(arms, options) { + /** @type {Map} */ + const byName = new Map(); + for (const arm of arms) { + if (!arm?.length) { + continue; + } + for (const c of arm) { + if (!c.kindOf(ReflectionKind.Property)) { + continue; + } + if (options?.skipNever && propertyReflectionTypeIsNever(c)) { + continue; + } + const existing = byName.get(c.name); + if (!existing) { + byName.set(c.name, c); + continue; + } + byName.set(c.name, options?.pickBetter ? pickBetterUnionPropertyCandidate(existing, c) : c); + } + } + if (byName.size === 0) { + return undefined; + } + const merged = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)); + return substituteTypeParameterDefaultsInChildren(merged); +} + +/** + * Resolve a parameter / property type to the list of `Property` reflections that should populate + * a nested rows table. TypeDoc applies `@param parent.prop` descriptions onto these reflections. + * + * Cases: + * - `reflection` (inline `{...}`): the declaration's own children. + * - `reference` to a named interface/alias: the target's children, or — for generic instantiations + * like `ClerkPaginationParams<{ status?: … }>` — the base properties merged with each typeArg's. + * - `intersection`: every `&` arm's properties combined (later arm wins on name collision). + * - `union`: every `|` arm's properties combined, dropping `prop?: never` discriminators and + * preferring the branch with more documentation on collisions. + * - `optional`: unwrap and recurse. + * + * Returns `undefined` when nothing resolves (so callers can `if (!children?.length)` cheaply). + * The children list may include non-`Property` kinds for direct `reflection` / `reference` cases — + * callers that need only `Property` should filter; merge cases (typeArgs / intersection / union) + * pre-filter via {@link mergePropertyArms}. + * + * @param {import('typedoc').SomeType | undefined} t + * @param {import('typedoc').ProjectReflection | undefined} [project] For resolving references when `ref.reflection` is missing (intersections like `Foo & WithOptionalOrgType<…>`). + * @returns {import('typedoc').DeclarationReflection[] | undefined} + */ +function resolveDeclarationWithObjectMembers(t, project) { + if (!t) { + return undefined; + } + if (t.type === 'optional') { + return resolveDeclarationWithObjectMembers(/** @type {import('typedoc').OptionalType} */ (t).elementType, project); + } + if (t.type === 'array') { + return resolveDeclarationWithObjectMembers(/** @type {import('typedoc').ArrayType} */ (t).elementType, project); + } + if (t.type === 'reflection') { + const children = t.declaration?.children; + return children?.length ? children : undefined; + } + if (t.type === 'reference') { + const ref = /** @type {import('typedoc').ReferenceType} */ (t); + /** + * `Omit` / `Pick` — TypeScript built-in utilities. They have no project reflection to look up, and falling through to the generic-instantiation path below would merge K's properties (zero, since K is a literal type) without applying the filter — Omit/Pick would silently behave like an identity. Resolve `X` to its property list, then keep/drop by the literal-string keys in `K`. Without this, `Array>` shows no nested rows because `decl` is undefined. + */ + if ((ref.name === 'Omit' || ref.name === 'Pick') && (ref.typeArguments?.length ?? 0) === 2) { + const baseChildren = resolveDeclarationWithObjectMembers(ref.typeArguments[0], project); + const keys = collectLiteralStringKeys(ref.typeArguments[1]); + if (baseChildren?.length && keys) { + const keep = ref.name === 'Pick' ? c => keys.has(c.name) : c => !keys.has(c.name); + return baseChildren.filter(keep); + } + } + let decl = + ref.reflection && 'kind' in ref.reflection + ? /** @type {import('typedoc').DeclarationReflection} */ (ref.reflection) + : undefined; + if (!decl && project && ref.name) { + decl = lookupInterfaceOrTypeAliasByName(project, ref.name); + } + if (!decl) { + return undefined; + } + /** + * Generic instantiation: TypeDoc often attaches pagination fields only to the target alias's + * own `children` and omits `decl.type`, so returning the base early drops the type argument + * object. Merge the base (`decl.type` if present, else `decl.children` as a fallback) with + * each type argument's properties. + */ + const typeArgs = ref.typeArguments ?? []; + if (typeArgs.length > 0) { + const baseFromType = decl.type ? resolveDeclarationWithObjectMembers(decl.type, project) : undefined; + const base = baseFromType ?? (decl.children?.length ? decl.children : undefined); + const argArms = typeArgs.map(ta => resolveDeclarationWithObjectMembers(ta, project)); + return mergePropertyArms([base, ...argArms]); + } + if (decl.children?.length) { + return substituteTypeParameterDefaultsInChildren(decl.children); + } + if (decl.type) { + return resolveDeclarationWithObjectMembers(decl.type, project); + } + return undefined; + } + if (t.type === 'intersection') { + const inter = /** @type {import('typedoc').IntersectionType} */ (t); + return mergePropertyArms(inter.types.map(inner => resolveDeclarationWithObjectMembers(inner, project))); + } + if (t.type === 'union') { + const u = /** @type {import('typedoc').UnionType} */ (t); + return mergePropertyArms( + u.types.map(inner => resolveDeclarationWithObjectMembers(inner, project)), + { skipNever: true, pickBetter: true }, + ); + } + return undefined; +} + +/** + * Build the name cell for a nominal-nested row. Uses `?.` when the parent param is optional (so `options?.foo` mirrors how it would be accessed at runtime) and `.` when required — same rule as `clerkParametersTable.flattenParams` in `custom-theme.mjs`. Appends `?` to the child name when the child property itself is optional, matching how the inline-flatten path renders `params.field?` via the standard parametersTable. + * + * @param {import('typedoc').ParameterReflection} parentParam + * @param {import('typedoc').DeclarationReflection} child + */ +function formatNestedParamNameColumn(parentParam, child) { + const sep = parentParam.flags?.isOptional ? '?.' : '.'; + const childOptional = child.flags?.isOptional ? '?' : ''; + return `\`${parentParam.name}${sep}${child.name}${childOptional}\``; +} + +/** + * When TypeDoc renders a parameter type as a markdown link to another generated `.mdx` file, that type has a dedicated page — omit nested `param?.prop` rows so readers follow the type link instead. + * `@inline` aliases are expanded by the theme and do not link to a standalone page unless `@standalonePage` is set (`standalone-page-tag.mjs`). + * + * @param {import('typedoc').SomeType | undefined} t + * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx + */ +function parameterTypeLinksToStandaloneMdxPage(t, ctx) { + const bare = unwrapOptional(t); + if (!bare) { + return false; + } + // `Array`: the standalone page documents the element shape, not the array signature. + // Surface both — the Type column links to the element page, and nested `params.` rows flatten the element so readers don't have to navigate just to see field names. + if (bare.type === 'array') { + return false; + } + if (bare.type === 'reference') { + const ref = /** @type {import('typedoc').ReferenceType} */ (bare); + if (isInlineModifierWithoutStandalonePage(ref.reflection)) { + return false; + } + } + const md = removeLineBreaksForTableCell(ctx.partials.someType(bare) ?? '') ?? ''; + return /\.mdx(?:#[^)]*)?\)/.test(md); +} + +/** + * Rows for object properties on a nominal param type (e.g. `HandleOAuthCallbackParams`), including from `@param parent.prop` on the method. + * Lists every property on the resolved shape; uses the property comment when present, otherwise `—` (intersection aliases often omit comments on some arms in the model). + * + * @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 []; + } + } + + if (parameterTypeLinksToStandaloneMdxPage(param.type, ctx)) { + return []; + } + + const project = /** @type {import('typedoc').ProjectReflection | undefined} */ (param.project ?? ctx.page?.project); + const children = resolveDeclarationWithObjectMembers(param.type, project); + if (!children?.length) { + return []; + } + const props = 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 typeCell = child.type ? removeLineBreaksForTableCell(ctx.partials.someType(child.type)) : '`unknown`'; + const nestedNameCol = formatNestedParamNameColumn(param, child); + const nestedDescRaw = renderNestedRowDescription(child.comment); + // Strip line breaks so multi-line `
    ` / paragraph descriptions don't shatter the markdown + // table row (which must be a single line). Matches the treatment already applied to typeCell. + const nestedDesc = removeLineBreaksForTableCell(nestedDescRaw) ?? '—'; + rows.push(`| ${nestedNameCol} | ${typeCell} | ${nestedDesc} |`); + } + return rows; +} + +/** + * Merged / external references sometimes leave {@link ReferenceType.reflection} unset; resolve by name. + * + * @param {import('typedoc').ProjectReflection} project + * @param {string} name + * @returns {import('typedoc').DeclarationReflection | undefined} + */ +function lookupInterfaceOrTypeAliasByName(project, name) { + /** @type {import('typedoc').DeclarationReflection[]} */ + const cands = []; + for (const r of Object.values(project.reflections)) { + if (r.name !== name) { + continue; + } + if (!r.kindOf(ReflectionKind.Interface) && !r.kindOf(ReflectionKind.TypeAlias)) { + continue; + } + cands.push(/** @type {import('typedoc').DeclarationReflection} */ (r)); + } + if (cands.length === 0) { + return undefined; + } + if (cands.length === 1) { + return cands[0]; + } + const withChildren = cands.find(c => c.children?.length); + return withChildren ?? cands[0]; +} + +/** + * Unwrap optional wrappers. When the parameter is a single named interface or type alias for an + * object shape, returns the section title (the type's name), the resolved property list, and the + * source `typeDecl` for `@experimental` / `@deprecated` checks. + * + * @param {import('typedoc').SomeType | undefined} t + * @param {import('typedoc').ProjectReflection} project + * @returns {{ sectionTitle: string, children: 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') { + return undefined; + } + const ref = /** @type {import('typedoc').ReferenceType} */ (t); + const 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, children: typeDecl.children, typeDecl }; + } + if (typeDecl.kindOf(ReflectionKind.TypeAlias)) { + // Prefer resolving `typeAlias.type` so intersections and generic instantiations (e.g. `ClerkPaginationParams<{ status?: … }>`) merge every `&` arm into one property list. + // Some aliases only attach members on `typeDecl.children` with no object shape on `.type`; keep that fallback (e.g. `SignOutOptions`, `JoinWaitlistParams`). + const fromResolvedType = typeDecl.type ? resolveDeclarationWithObjectMembers(typeDecl.type, project) : undefined; + const children = fromResolvedType?.length ? fromResolvedType : typeDecl.children; + if (!children?.length) { + return undefined; + } + return { sectionTitle: typeDecl.name, children, typeDecl }; + } + return undefined; +} + +/** + * Nominal param sections are skipped when there is no prose anywhere — avoids huge undocumented tables. + * Type-only aliases often use `@experimental` / `@deprecated` on the type with an empty summary; intersection params like `GetPaymentAttemptParams` still have documented arms (`id`, pagination) and must inline. + * + * @param {import('typedoc').DeclarationReflection} typeDecl + * @param {import('typedoc').DeclarationReflection[]} props + */ +function isNominalParamTypeDocumented(typeDecl, props) { + if (typeDecl.comment?.summary?.length) { + return true; + } + const blockTags = typeDecl.comment?.blockTags ?? []; + if (blockTags.some(t => t.tag !== '@inline')) { + return true; + } + return props.some(p => p.comment?.summary?.length); +} + +/** + * `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; + } + } +} + +/** Block tags omitted from extracted method prose (see `custom-theme.mjs` `comment` partial for theme output). */ +const BLOCK_TAGS_OMITTED_FROM_EXTRACTED_METHOD_PROSE = new Set(['@param', '@typeParam', '@returns', '@experimental']); + +/** + * @param {import('typedoc').SignatureReflection} sig + * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx + * @param {number} [headingLevel] Defaults to 4 (reference-object format); pass 2 for page format. + */ +function trySingleNominalParameterTypeSection(sig, ctx, headingLevel = 4) { + const params = sig.parameters ?? []; + if (params.length !== 1) { + return undefined; + } + const p = params[0]; + const project = sig.project ?? ctx.page?.project; + const nominal = resolveNominalObjectTypeForSingleParam(p.type, project); + if (!nominal) { + return undefined; + } + const props = nominal.children.filter(c => c.kindOf(ReflectionKind.Property)); + if (props.length === 0) { + return undefined; + } + if (!isNominalParamTypeDocumented(nominal.typeDecl, props)) { + return undefined; + } + const tableMd = renderMemberTableOmittingExampleBlocks(props, ctx, () => + ctx.partials.propertiesTable( + props, + /** @type {Parameters[1]} */ + ({ + kind: nominal.typeDecl.kind, + isEventProps: false, + applyAllowlistedPropertyTableRowFilters: false, + }), + ), + ); + if (!tableMd?.trim()) { + return undefined; + } + return [markdownHeadingInlineCode(headingLevel, nominal.sectionTitle), '', tableMd, ''].join('\n'); +} + +/** + * @param {import('typedoc').SignatureReflection} sig + * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx + * @param {Map | undefined} instantiationMap + * @param {number} [headingLevel] Defaults to 4 (reference-object format); pass 2 for page format. + */ +function parametersMarkdownTable(sig, ctx, instantiationMap, headingLevel = 4) { + const sigForDisplay = signatureWithInstantiation(sig, instantiationMap); + const params = sigForDisplay.parameters ?? []; + if (params.length === 0) { + return ''; + } + + const singleNominal = trySingleNominalParameterTypeSection(sigForDisplay, ctx, headingLevel); + if (singleNominal) { + return singleNominal; + } + + let tableMd = renderMemberTableOmittingExampleBlocks(params, ctx, () => ctx.partials.parametersTable(params)); + /** @type {string[]} */ + const nested = []; + for (const p of params) { + nested.push(...nestedParameterRowsFromDocumentedProperties(p, ctx)); + } + if (nested.length) { + // Nested rows are always 3-column (`| name | type | description |`). If the base table came + // back 2-column (no direct param has a comment, so `clerkParametersTable` omits the Description + // column), pad the base to 3 columns so the appended rows don't malform the table. + if (!parameterTableHasDescriptionColumn(tableMd)) { + tableMd = padParameterTableWithEmptyDescriptionColumn(tableMd); + } + tableMd = `${tableMd.trimEnd()}\n${nested.join('\n')}\n`; + } + + return [markdownHeading(headingLevel, ReflectionKind.pluralString(ReflectionKind.Parameter)), '', tableMd, ''].join( + '\n', + ); +} + +/** + * Detects whether a markdown parameter table (pipe-format) already has a `Description` column. + * HTML tables render on a single line — treat those as "already correct" (they carry their own + * column structure via `` cells and aren't concatenated with pipe-row nested output). + * + * @param {string} tableMd + */ +function parameterTableHasDescriptionColumn(tableMd) { + if (!tableMd) return true; + const firstLine = tableMd.split('\n', 1)[0].trim(); + if (!firstLine.startsWith('|')) return true; + return /\|\s*Description\s*\|/.test(firstLine); +} + +/** + * Append an empty Description column to a 2-column pipe-format parameter table so that appending + * 3-column nested-row output doesn't malform it. Header gets ` Description |`, divider gets + * ` ------ |`, data rows get ` - |`. + * + * @param {string} tableMd + */ +function padParameterTableWithEmptyDescriptionColumn(tableMd) { + const lines = tableMd.split('\n'); + let sawDivider = false; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (!line.startsWith('|') || !line.endsWith('|')) continue; + if (i === 0) { + lines[i] = line.replace(/\|\s*$/, ' Description |'); + } else if (!sawDivider && /^\|[\s\-|:]+\|$/.test(line)) { + lines[i] = line.replace(/\|\s*$/, ' ------ |'); + sawDivider = true; + } else { + lines[i] = line.replace(/\|\s*$/, ' - |'); + } + } + return lines.join('\n'); +} + +/** + * @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. + * E.g. `navigate: CustomNavigation` in clerk.ts + * + * @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) { @@ -1953,5 +3620,3 @@ function isCallablePropertyValueType(t, helpers, seenReflectionIds) { } return false; } - -export { isCallableInterfaceProperty }; diff --git a/.typedoc/extract-methods.mjs b/.typedoc/extract-methods.mjs index 731606fcd42..c9d01408e5d 100644 --- a/.typedoc/extract-methods.mjs +++ b/.typedoc/extract-methods.mjs @@ -1,1470 +1,367 @@ // @ts-check /** - * TypeDoc plugin that runs during the markdown render pass. For each reference-object page listed in {@link REFERENCE_OBJECT_CONFIG} (e.g. `shared/clerk/clerk.mdx`), this listener: + * TypeDoc plugin that runs during the markdown render pass. Acts as a text slicer for the + * parent-page shape that `custom-theme.mjs` produces: * - * - copies the body of the page's `## Properties` section (table only, no heading) into a sibling `properties.mdx`, - * - mutates `output.contents` to drop the `## Properties` section from the main page, - * - writes one `methods/.mdx` per callable child on the reflection (and on any `extraMethodInterfaces`), alongside the main page in that resource folder. + * - Synchronously (before prettier) slices `## `methodName()`` and `## `namespace`` H2 sections + * from the parent's `output.contents` and reshapes each into `methods/.mdx`. The parent's + * sections stay in place — the slicer only reads. + * - Queues a `preWriteAsyncJob` to run **after** typedoc-plugin-markdown's prettier job. Once + * `output.contents` is prettier-formatted, extract the Properties body from between the + * `` markers that `custom-theme.mjs` wraps around `## + * Properties`, and write it to sibling `properties.mdx` — inheriting prettier's column + * alignment. Strip the entire `## Properties` region + markers from the parent page contents. * - * Must load **after** `custom-plugin.mjs` so its `MarkdownPageEvent.END` listener — which applies link replacements to `output.contents` — runs first. The Properties body we copy out is then already in its final, replaced form. + * Must load **after** `custom-plugin.mjs` so its `MarkdownPageEvent.END` listener — which applies + * link replacements to `output.contents` — runs first (link replacements pass through HTML comment + * markers unchanged). * - * 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). The theme context comes from `theme.getRenderContext(output)` on the live page event — no second TypeDoc convert pass. - * - * Inline object namespaces tagged **`@extractMethods`** on the parent property are omitted from the main Properties table (see `custom-theme.mjs`). For each direct member: callables become `methods/-.mdx` via `buildMethodMdx`; non-callables become a heading + property table via `buildPropertyTableDocMdx`. + * Has zero reflection access: all generation lives in `custom-theme.mjs`. */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { - Comment, - IntersectionType, - OptionalType, - ReferenceType, - ReflectionKind, - ReflectionType, - UnionType, -} from 'typedoc'; import { MarkdownPageEvent } from 'typedoc-plugin-markdown'; -import { applyTodoStrippingToComment } from './comment-utils.mjs'; -import { - applyCatchAllMdReplacements, - applyRelativeLinkReplacements, - stripReferenceObjectPropertiesSection, -} from './custom-plugin.mjs'; -import { isCallableInterfaceProperty } from './custom-theme.mjs'; -import { removeLineBreaks } from './markdown-helpers.mjs'; import { BACKEND_API_CONFIG, REFERENCE_OBJECT_CONFIG } from './reference-objects.mjs'; - -/** - * `'reference'` (default): each `methods/.mdx` opens with `### foo()` and uses an H4 `#### Parameters` heading — matches the reference-object pages that aggregate many methods. - * `'page'`: skip the method-name title and use an H2 `## Parameters` heading — for one-method-per-docs-page surfaces, like the backend API endpoints. - * @typedef {'reference' | 'page'} MethodFormat - */ - -/** @param {string} pageUrl */ -function methodFormatForPageUrl(pageUrl) { - return pageUrl in BACKEND_API_CONFIG - ? /** @type {MethodFormat} */ ('page') - : /** @type {MethodFormat} */ ('reference'); -} import { toFileSlug } from './slug.mjs'; -import { isInlineModifierWithoutStandalonePage } from './standalone-page-tag.mjs'; -import { unwrapOptional } from './type-utils.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}`; -} - -/** - * Heading whose visible title is a type/identifier (nominal single-parameter object sections) needs to get wrapped in backticks. + * Parse a parent-page `## `methodName()`` H2 section into its logical parts. Handles both natural + * typedoc-plugin-markdown sections (backend classes) and synthesized aggregator sections (shared + * interfaces, extraMethodInterfaces). * - * @param {number} level - * @param {string} text - */ -function markdownHeadingInlineCode(level, text) { - const l = Math.min(Math.max(level, 1), 6); - const t = text.trim(); - return `${'#'.repeat(l)} \`${t}\``; -} - -/** - * Same as typedoc-plugin-markdown `removeLineBreaks` for table cells. + * Expected shape (mixture across natural/synthesized — reshape logic tolerates either): * - * @param {string | undefined} str - */ -function removeLineBreaksForTableCell(str) { - return str?.replace(/\r?\n/g, ' ').replace(/ {2,}/g, ' '); -} - -/** - * TypeDoc `code` display parts often already include backticks (same as {@link Comment.combineDisplayParts}). - * Wrapping again would produce `` `Client` `` in MDX. + * ## `name()` + * + * ```typescript + * function name(...) + * ``` + * ### `TypeName` (nominal single-arg) OR ### Parameters + * + * ### Returns + * `Type` — (or just `Type` with no ` — `) * - * @param {string} text + * @param {string} section */ -function codeDisplayPartToMarkdown(text) { - const trimmed = text.trim(); - if (trimmed.length >= 2 && trimmed.startsWith('`') && trimmed.endsWith('`')) { - return trimmed; - } - return `\`${text}\``; -} +function parseParentAggregatorSection(section) { + const lines = section.split('\n'); + // Method titles are either bare (`## methodName()`) or qualified (`## `emailCode.sendCode()``). + // Qualified names use `.` as separator (from `@extractMethods` namespaces). + const titleMatch = lines[0]?.match(/^## `?([A-Za-z_$][A-Za-z0-9_$.]*)`?\(\)`?/); + const name = titleMatch?.[1] ?? ''; -/** - * Build the description cell for a nested `param.field` row: summary text plus a leading `**Deprecated.**` marker (with the `@deprecated` tag's body) when that tag is present. Matches what typedoc-plugin-markdown's `parseParams` emits for inline `{ … }` params — fields documented only via `@deprecated` (no summary) otherwise rendered as `—` here, hiding important guidance. - * Returns `'—'` when both summary and `@deprecated` are empty. - * - * @param {import('typedoc').Comment | undefined} comment - */ -function renderNestedRowDescription(comment) { - const summaryText = comment?.summary?.length ? displayPartsToString(comment.summary).trim() : ''; - const deprecatedTag = comment?.blockTags?.find(t => t.tag === '@deprecated'); - const deprecatedText = deprecatedTag ? displayPartsToString(deprecatedTag.content).trim() : ''; - const deprecatedMd = deprecatedTag ? `**Deprecated.**${deprecatedText ? ` ${deprecatedText}` : ''}` : ''; - const combined = [summaryText, deprecatedMd].filter(Boolean).join(' '); - return combined || '—'; -} + const sigStart = lines.findIndex((l, i) => i > 0 && l.startsWith('```typescript')); + const sigEnd = sigStart === -1 ? -1 : lines.findIndex((l, i) => i > sigStart && l === '```'); -/** - * @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(''); -} + const descBeforeSig = sigStart === -1 ? lines.length : sigStart; + const descLines = lines.slice(1, descBeforeSig); + trimBlankBoundariesInPlace(descLines); + const description = descLines.join('\n'); -/** - * @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]; -} + const signature = sigStart !== -1 && sigEnd !== -1 ? lines.slice(sigStart, sigEnd + 1).join('\n') : ''; -/** - * Walk instantiated generic / alias chains (e.g. `CheckAuthorization` → `CheckAuthorizationFn` → `(…) => boolean`) until we find a {@link ReflectionType} call signature. Uses reflection IDs to avoid infinite loops. - * - * @param {import('typedoc').Type | undefined} t - * @param {Set} visitedReflectionIds - * @returns {import('typedoc').SignatureReflection | undefined} - */ -function getCallSignatureFromType(t, visitedReflectionIds) { - if (!t || typeof t !== 'object') { - return undefined; - } - const tag = /** @type {{ type?: string }} */ (t).type; - if (tag === 'optional' && 'elementType' in t) { - return getCallSignatureFromType( - /** @type {{ elementType: import('typedoc').Type }} */ (t).elementType, - visitedReflectionIds, - ); - } - if (t instanceof ReflectionType) { - if (t.declaration?.signatures?.length) { - return t.declaration.signatures[0]; - } - return undefined; - } - if (t instanceof ReferenceType) { - const target = t.reflection; - if ( - target && - 'signatures' in target && - /** @type {{ signatures?: import('typedoc').SignatureReflection[] }} */ (target).signatures?.length - ) { - return /** @type {import('typedoc').DeclarationReflection} */ (target).signatures[0]; - } - if (!target || !('kind' in target)) { - return undefined; - } - const decl = /** @type {import('typedoc').DeclarationReflection} */ (target); - const id = decl.id; - if (id != null) { - if (visitedReflectionIds.has(id)) { - return undefined; - } - visitedReflectionIds.add(id); - } - try { - if (decl.kind === ReflectionKind.TypeAlias && decl.type) { - return getCallSignatureFromType(decl.type, visitedReflectionIds); - } - } finally { - if (id != null) { - visitedReflectionIds.delete(id); - } - } - return undefined; - } - if (t instanceof UnionType) { - for (const arm of t.types) { - const sig = getCallSignatureFromType(arm, visitedReflectionIds); - if (sig) { - return sig; - } - } - return undefined; - } - if (t instanceof IntersectionType) { - for (const arm of t.types) { - const sig = getCallSignatureFromType(arm, visitedReflectionIds); - if (sig) { - return sig; - } - } - } - return undefined; -} - -/** - * @param {import('typedoc').SignatureReflection} sig - */ -function signatureIsDeprecated(sig) { - const c = sig.comment; - if (!c) return false; - if (typeof c.hasModifier === 'function' && c.hasModifier('@deprecated')) return true; - return c.blockTags?.some(t => t.tag === '@deprecated') ?? false; -} - -/** - * typedoc maps `@param paramName description` to `parameter.comment` during conversion of the - * signature that physically owns the JSDoc. With overloads + an implementation, the impl's `@param` - * tags don't transfer to overload parameters — even when typedoc copies the impl's comment onto - * each overload's `sig.comment.blockTags`. Without descriptions on `param.comment`, typedoc-plugin-markdown - * drops the Description column from the parameters table. - * - * Overlay missing parameter comments from any matching `@param` block tag on the signature comment - * so the rendered table shows the descriptions the author wrote on the implementation's JSDoc. - * - * @param {import('typedoc').SignatureReflection} sig - */ -function overlayParamCommentsFromSignatureBlockTags(sig) { - const params = sig.parameters; - if (!params?.length) return; - const blockTags = sig.comment?.blockTags; - if (!blockTags?.length) return; - for (const p of params) { - if (p.comment?.summary?.length) continue; - const tag = blockTags.find(t => t.tag === '@param' && t.name === p.name); - if (!tag?.content?.length) continue; - p.comment = new Comment(tag.content); - } -} - -/** - * @param {import('typedoc').DeclarationReflection} decl - * @returns {import('typedoc').SignatureReflection | undefined} - */ -function getPrimaryCallSignature(decl) { - if (decl.signatures?.length) { - // Prefer the first non-`@deprecated` overload. typedoc treats each overload as a separate - // signature, and selecting a deprecated one usually means rendering the form users shouldn't - // use — and (often) one whose JSDoc isn't where the canonical `@param` descriptions live. - const firstActive = decl.signatures.find(s => !signatureIsDeprecated(s)); - return firstActive ?? decl.signatures[0]; - } - const t = decl.type; - if (t && 'declaration' in t && t.declaration?.signatures?.length) { - return t.declaration.signatures[0]; - } - // E.g. `navigate: CustomNavigation` — for `type Fn = () => void`, signatures often live on the inner `declaration` of `alias.type` (ReflectionType), not on `alias.signatures` (see `custom-theme.mjs` `isCallablePropertyValueType`). - if (t && typeof t === 'object' && 'type' in t && /** @type {{ type?: string }} */ (t).type === 'reference') { - const ref = /** @type {import('typedoc').ReferenceType} */ (t); - const target = ref.reflection; - const sigs = - target && 'signatures' in target - ? /** @type {{ signatures?: import('typedoc').SignatureReflection[] }} */ (target).signatures - : undefined; - if (sigs?.length) { - return sigs[0]; - } - const aliasTarget = /** @type {import('typedoc').DeclarationReflection | undefined} */ ( - target && 'kind' in target ? target : undefined - ); - if (aliasTarget?.kind === ReflectionKind.TypeAlias && aliasTarget.type && 'declaration' in aliasTarget.type) { - const inner = /** @type {import('typedoc').ReflectionType} */ (aliasTarget.type).declaration; - if (inner?.signatures?.length) { - return inner.signatures[0]; - } - } - // `type X = SomeFn` — RHS is often ReferenceType (generic alias), not ReflectionType; recurse (e.g. `checkAuthorization: CheckAuthorization`). - if (aliasTarget?.kind === ReflectionKind.TypeAlias && aliasTarget.type) { - const fromRhs = getCallSignatureFromType(aliasTarget.type, new Set()); - if (fromRhs) { - return fromRhs; - } - } - const fromRef = getCallSignatureFromType(ref, new Set()); - if (fromRef) { - return fromRef; - } - } - return undefined; -} - -/** - * For `prop: OuterAlias` where `type OuterAlias = SomeFn`, maps generic parameter names on `SomeFn` to the instantiated type arguments (e.g. `Params` → `CheckAuthorizationParams`). - * - * @param {import('typedoc').DeclarationReflection} propertyDecl - * @returns {Map | undefined} - */ -function getGenericInstantiationMapFromCallableProperty(propertyDecl) { - const t = unwrapOptional(propertyDecl.type); - if (!(t instanceof ReferenceType) || !t.reflection) { - return undefined; - } - const alias = /** @type {import('typedoc').DeclarationReflection} */ (t.reflection); - if (!alias.kindOf(ReflectionKind.TypeAlias) || !alias.type) { - return undefined; - } - const inner = unwrapOptional(alias.type); - if (!(inner instanceof ReferenceType) || !inner.typeArguments?.length || !inner.reflection) { - return undefined; - } - const generic = /** @type {import('typedoc').DeclarationReflection} */ (inner.reflection); - const tpls = generic.typeParameters; - if (!tpls?.length) { - return undefined; - } - /** @type {Map} */ - const map = new Map(); - for (let i = 0; i < inner.typeArguments.length; i++) { - const tp = tpls[i]; - const arg = inner.typeArguments[i]; - if (tp?.name && arg) { - map.set(tp.name, arg); - } - } - return map.size ? map : undefined; -} - -/** - * Replace references to generic type parameters with instantiated types from {@link getGenericInstantiationMapFromCallableProperty}. - * - * @param {import('typedoc').Type | undefined} t - * @param {Map | undefined} map - * @returns {import('typedoc').Type | undefined} - */ -function substituteGenericParamRefsInType(t, map) { - if (!t || !map?.size) { - return t; - } - if (/** @type {{ type?: string }} */ (t).type === 'optional' && 'elementType' in t) { - const el = /** @type {{ elementType: import('typedoc').Type }} */ (t).elementType; - const next = substituteGenericParamRefsInType(el, map); - if (next && next !== el) { - return new OptionalType(/** @type {import('typedoc').SomeType} */ (/** @type {unknown} */ (next))); - } - return t; - } - if (t instanceof ReferenceType && map.has(t.name)) { - return map.get(t.name) ?? t; - } - return t; -} - -/** - * Resolve a property's type to its declared default when it references a `TypeParameter` reflection. - * Used when a generic alias is inlined into an intersection (e.g. `CreateParams = { … } & MetadataParams` where the `& MetadataParams` arm gets inlined as a reflection, losing the named reference) so the property table surfaces the resolved default type instead of the open type-parameter name (e.g. `TPublic` → `OrganizationPublicMetadata`). - * - * @param {import('typedoc').Type | undefined} t - * @returns {import('typedoc').Type | undefined} - */ -function substituteTypeParameterDefaultsInType(t) { - if (!t) { - return t; - } - if (/** @type {{ type?: string }} */ (t).type === 'optional' && 'elementType' in t) { - const el = /** @type {{ elementType: import('typedoc').Type }} */ (t).elementType; - const next = substituteTypeParameterDefaultsInType(el); - if (next && next !== el) { - return new OptionalType(/** @type {import('typedoc').SomeType} */ (/** @type {unknown} */ (next))); - } - return t; - } - if (t instanceof ReferenceType) { - const tp = /** @type {{ kindOf?: (k: number) => boolean, default?: import('typedoc').Type }} */ (t.reflection); - if (tp?.kindOf?.(ReflectionKind.TypeParameter) && tp.default) { - return tp.default; - } - } - return t; -} - -/** - * Clone each property and apply `substituteTypeParameterDefaultsInType` to its type — see comment on `substituteTypeParameterDefaultsInType` for the motivating case. - * - * @param {import('typedoc').DeclarationReflection[]} children - */ -function substituteTypeParameterDefaultsInChildren(children) { - return children.map(child => { - const next = substituteTypeParameterDefaultsInType(child.type); - if (next === child.type) { - return child; - } - return Object.assign(Object.create(Object.getPrototypeOf(child)), child, { type: next }); - }); -} - -/** - * @param {import('typedoc').SignatureReflection} sig - * @param {Map | undefined} instantiationMap - */ -function signatureWithInstantiation(sig, instantiationMap) { - if (!instantiationMap?.size) { - return sig; - } - const parameters = (sig.parameters ?? []).map(p => { - const newType = substituteGenericParamRefsInType(p.type, instantiationMap); - if (newType === p.type) { - return p; - } - return Object.assign(Object.create(Object.getPrototypeOf(p)), p, { type: newType }); - }); - const newReturn = substituteGenericParamRefsInType(sig.type, instantiationMap) ?? sig.type; - const out = Object.assign(Object.create(Object.getPrototypeOf(sig)), sig, { - parameters, - type: newReturn, - typeParameters: undefined, - }); - if (sig.project) { - out.project = sig.project; - } - return out; -} - -/** - * Must stay aligned with allowlisted `propertiesTable` filtering in `custom-theme.mjs` (`isCallableInterfaceProperty` and `@extractMethods`: extracted here, not listed as properties). Nested tables pass `applyAllowlistedPropertyTableRowFilters: false`. - * - * @param {import('typedoc').DeclarationReflection} decl - * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx - */ -function shouldExtractCallableMember(decl, ctx) { - if (decl.kind === ReflectionKind.Method) { - return true; - } - if ( - decl.kind === ReflectionKind.Property || - decl.kind === ReflectionKind.Accessor || - decl.kind === ReflectionKind.Variable - ) { - return isCallableInterfaceProperty(decl, ctx.helpers); - } - return false; -} - -/** - * Object-literal (or single object arm of `T | null`) property rows for a properties table. - * - * @param {import('typedoc').SomeType | undefined} valueType - * @returns {import('typedoc').DeclarationReflection[] | undefined} - */ -function resolveObjectShapeMembersForPropertyTable(valueType) { - let t = unwrapOptional(valueType, { deep: true }); - if (t instanceof UnionType) { - const objectArms = t.types.filter(u => u instanceof ReflectionType && (u.declaration?.children?.length ?? 0) > 0); - if (objectArms.length !== 1) { - return undefined; - } - t = /** @type {import('typedoc').ReflectionType} */ (objectArms[0]); - } - if (!(t instanceof ReflectionType)) { - return undefined; - } - const kids = t.declaration?.children ?? []; - return kids.filter( - c => c.kind === ReflectionKind.Property || c.kind === ReflectionKind.Variable || c.kind === ReflectionKind.Accessor, - ); -} - -/** - * @param {string} parentName - * @param {import('typedoc').DeclarationReflection} nestedDecl - * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx - */ -function buildPropertyTableDocMdx(parentName, nestedDecl, ctx) { - const qualifiedName = `${parentName}.${nestedDecl.name}`; - const title = `### \`${qualifiedName}\``; - const description = commentSummaryAndBody(nestedDecl.comment); - const propsUnsorted = resolveObjectShapeMembersForPropertyTable(nestedDecl.type); - if (!propsUnsorted?.length) { - return ''; - } - /** Match nominal param tables and merged intersection holders: stable A–Z by property name (TypeDoc inline literal `children` order is declaration order). */ - const props = [...propsUnsorted].sort((a, b) => a.name.localeCompare(b.name)); - const tableMd = renderMemberTableOmittingExampleBlocks(props, ctx, () => - ctx.partials.propertiesTable( - props, - /** @type {Parameters[1]} */ - ({ - kind: ReflectionKind.Interface, - isEventProps: false, - applyAllowlistedPropertyTableRowFilters: false, - }), - ), + const afterSig = sigEnd === -1 ? 1 + descBeforeSig : sigEnd + 1; + const returnsIdx = lines.findIndex((l, i) => i >= afterSig && /^### Returns\b/.test(l)); + const searchEnd = returnsIdx === -1 ? lines.length : returnsIdx; + const paramsIdx = lines.findIndex( + (l, i) => i >= afterSig && i < searchEnd && /^### /.test(l) && !/^### Returns\b/.test(l), ); - const chunks = [title, '', description, '', tableMd].filter(Boolean); - const raw = chunks.join('\n\n'); - return `${applyCatchAllMdReplacements(applyRelativeLinkReplacements(raw)).trim()}\n`; -} - -/** - * Parent-level property table for an `@extractMethods` namespace. - * Lists non-callable direct members on the namespace (e.g. `verifications.emailAddress`, `verifications.phoneNumber`). - * - * @param {import('typedoc').DeclarationReflection} parentDecl - * @param {import('typedoc').DeclarationReflection[]} nonCallableMembers - * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx - */ -function buildExtractMethodsNamespacePropertyTableMdx(parentDecl, nonCallableMembers, ctx) { - if (!nonCallableMembers.length) { - return ''; - } - const title = `### \`${parentDecl.name}\``; - const description = commentSummaryAndBody(parentDecl.comment); - const props = [...nonCallableMembers].sort((a, b) => a.name.localeCompare(b.name)); - const tableMd = renderMemberTableOmittingExampleBlocks(props, ctx, () => - ctx.partials.propertiesTable( - props, - /** @type {Parameters[1]} */ - ({ - kind: ReflectionKind.Interface, - isEventProps: false, - applyAllowlistedPropertyTableRowFilters: false, - }), - ), - ); - if (!tableMd?.trim()) { - return ''; - } - const raw = [title, '', description, '', tableMd].filter(Boolean).join('\n\n'); - return `${applyCatchAllMdReplacements(applyRelativeLinkReplacements(raw)).trim()}\n`; -} - -/** - * @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; -} - -/** - * Split the `## Properties` section out of page contents, returning the body (no heading) and the page contents with the Properties section removed. - * - * Operates on the in-memory `output.contents` of a `MarkdownPageEvent`; the caller writes `properties.mdx` and assigns the stripped string back to `output.contents`. The page's own END pipeline (link replacements) has already run by the time we get called, so the Properties body is in its final, replaced form — no re-application needed. - * - * @param {string} contents - * @returns {{ propertiesBody: string | undefined, stripped: string }} - */ -function splitPropertiesFromContents(contents) { - if (!contents) { - return { propertiesBody: undefined, stripped: contents }; - } - const propertiesBody = extractPropertiesSectionBody(contents); - const stripped = stripReferenceObjectPropertiesSection(contents); - return { propertiesBody, stripped }; -} - -/** - * Plain TypeScript-like type text for ```typescript``` fences (no markdown / backticks from {@link MarkdownThemeContext.partials.someType}). - * - * @param {import('typedoc').Type | undefined} t - */ -function typeStringForTypeScriptFence(t) { - if (!t) { - return 'unknown'; - } - return removeLineBreaks(t.toString()); -} - -/** - * @param {import('typedoc').SignatureReflection} sig - * @param {string} memberName - * @param {Map | undefined} instantiationMap - */ -function formatTypeScriptSignature(sig, memberName, instantiationMap) { - const hideOuterTypeParams = Boolean(instantiationMap?.size) && (sig.typeParameters?.length ?? 0) > 0; - const typeParamStr = - !hideOuterTypeParams && sig.typeParameters?.length ? `<${sig.typeParameters.map(tp => tp.name).join(', ')}>` : ''; - const params = - sig.parameters?.map(p => { - const opt = p.flags.isOptional ? '?' : ''; - const rest = p.flags.isRest ? '...' : ''; - const t = substituteGenericParamRefsInType(p.type, instantiationMap) ?? p.type; - const typeStr = typeStringForTypeScriptFence(t); - return `${rest}${p.name}${opt}: ${typeStr}`; - }) ?? []; - const retT = substituteGenericParamRefsInType(sig.type, instantiationMap) ?? sig.type; - const ret = retT ? typeStringForTypeScriptFence(retT) : 'void'; - // Qualified names (`emailCode.sendCode`) aren't valid in `function foo.bar()` syntax; use the bare last segment — the parent is already in the heading above. - const displayName = memberName.includes('.') ? memberName.split('.').pop() : memberName; - return `function ${displayName}${typeParamStr}(${params.join(', ')}): ${ret}`; -} - -/** - * `@returns - foo` is often stored with a leading dash, which renders as a bullet. Normalize to prose for "Returns …" lines. - * @param {string} body - */ -function normalizeReturnsBody(body) { - return body.replace(/^\s*[-*]\s+/, '').trim(); -} - -/** - * Lowercase the first character so the line reads "Returns an …" not "Returns An …". - * @param {string} body - */ -function lowercaseFirstCharacter(body) { - if (!body) { - return body; - } - return body.charAt(0).toLowerCase() + body.slice(1); -} - -/** - * @param {import('typedoc').CommentTag} tag - */ -function formatReturnsLineFromTag(tag) { - const raw = Comment.combineDisplayParts(tag.content).trim(); - if (!raw) { - return ''; - } - const body = lowercaseFirstCharacter(normalizeReturnsBody(raw)); - return `Returns ${body}`; -} -/** - * @param {import('typedoc').Comment | undefined} comment - */ -/** - * `typedoc-plugin-markdown` table partials include `@example` in Description cells. For extract-methods, we want to exclude examples from the generated output. - * - * Uses the same `getFlattenedDeclarations` list as `propertiesTable` so nested property rows omit examples too. - * - * @template T - * @param {import('typedoc').Reflection[]} roots - * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx - * @param {() => T} render - * @returns {T} - */ -function renderMemberTableOmittingExampleBlocks(roots, ctx, render) { - const flatten = - typeof ctx.helpers?.getFlattenedDeclarations === 'function' - ? ctx.helpers.getFlattenedDeclarations( - /** @type {import('typedoc').DeclarationReflection[]} */ (/** @type {unknown} */ (roots)), - ) - : roots; - /** @type {Set} */ - const processedComments = new Set(); - /** @type {{ ref: import('typedoc').Reflection; orig: import('typedoc').Comment }[]} */ - const restore = []; - for (const r of flatten) { - const c = 'comment' in r ? r.comment : undefined; - if (!c?.getTag('@example') || processedComments.has(c)) { - continue; - } - processedComments.add(c); - const next = c.clone(); - next.removeTags('@example'); - for (const ref of flatten) { - if (ref.comment === c) { - ref.comment = next; - restore.push({ ref, orig: c }); - } - } - } - try { - return render(); - } finally { - for (const { ref, orig } of restore) { - ref.comment = orig; - } + let paramsHeadingLine = ''; + let paramsBody = ''; + if (paramsIdx !== -1) { + paramsHeadingLine = lines[paramsIdx]; + const paramsBodyLines = lines.slice(paramsIdx + 1, searchEnd); + trimBlankBoundariesInPlace(paramsBodyLines); + paramsBody = paramsBodyLines.join('\n'); } -} - -/** Block tags omitted from extracted method prose (see `custom-theme.mjs` `comment` partial for theme output). */ -const BLOCK_TAGS_OMITTED_FROM_EXTRACTED_METHOD_PROSE = new Set(['@param', '@typeParam', '@returns', '@experimental']); -/** - * @param {import('typedoc').Comment | undefined} comment - */ -function commentSummaryAndBody(comment) { - if (!comment) { - return ''; + let returnsBody = ''; + if (returnsIdx !== -1) { + const returnsLines = lines.slice(returnsIdx + 1); + trimBlankBoundariesInPlace(returnsLines); + returnsBody = returnsLines.join('\n'); } - const c = applyTodoStrippingToComment(comment) ?? comment; - const summary = displayPartsToString(c.summary).trim(); - const block = c.blockTags - ?.filter(t => !BLOCK_TAGS_OMITTED_FROM_EXTRACTED_METHOD_PROSE.has(t.tag)) - .map(t => displayPartsToString(t.content).trim()) - .filter(Boolean) - .join('\n\n'); - const returnsLines = - c.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); + return { name, description, signature, paramsHeadingLine, paramsBody, returnsBody }; } -/** - * @param {import('typedoc').DeclarationReflection} prop - */ -function propertyReflectionTypeIsNever(prop) { - const ty = unwrapOptional(prop.type, { deep: true }); - return ty?.type === 'intrinsic' && ty.name === 'never'; +/** @param {string[]} arr */ +function trimBlankBoundariesInPlace(arr) { + while (arr.length && arr[0].trim() === '') arr.shift(); + while (arr.length && arr[arr.length - 1].trim() === '') arr.pop(); } /** - * Union discriminators often use `otherProp?: never`. Prefer the branch with a documentable type. + * Take the `### Returns` body and produce the extracted-file `Returns .` + * prose that `buildMethodMdx` writes in via `formatReturnsLineFromTag(@returns)`. Two shapes are + * possible depending on the theme's `signatureReturns` partial: * - * @param {import('typedoc').DeclarationReflection} existing - * @param {import('typedoc').DeclarationReflection} candidate - */ -function pickBetterUnionPropertyCandidate(existing, candidate) { - const existingNever = propertyReflectionTypeIsNever(existing); - const candidateNever = propertyReflectionTypeIsNever(candidate); - if (existingNever && !candidateNever) { - return candidate; - } - if (!existingNever && candidateNever) { - return existing; - } - const existingDoc = existing.comment?.summary?.length ?? 0; - const candidateDoc = candidate.comment?.summary?.length ?? 0; - return candidateDoc > existingDoc ? candidate : existing; -} - -/** - * Collect the set of string-literal keys from a type used as the second argument to `Omit` or `Pick` — a single literal (`'organizationId'`) or a union of literals (`'a' | 'b'`). Returns `undefined` if the type isn't a literal/literal-union (e.g. a `keyof` reference we can't resolve here), in which case callers should fall through to the generic-instantiation path. + * - Condensed: `` `Type` — `` — single line with em-dash separator. Description can + * extend across multiple lines (embedded `> [!WARNING]` callouts from `@returns` source). + * - Uncondensed: `` `Type`\n\n `` — default typedoc-plugin-markdown format when the + * theme's condense heuristic doesn't apply (typically when the return type prints on multiple + * lines, e.g. object literals). * - * @param {import('typedoc').Type | undefined} t - * @returns {Set | undefined} - */ -function collectLiteralStringKeys(t) { - if (!t) { - return undefined; - } - if (t.type === 'literal' && typeof (/** @type {{ value: unknown }} */ (t).value) === 'string') { - return new Set([/** @type {{ value: string }} */ (t).value]); - } - if (t.type === 'union') { - const u = /** @type {import('typedoc').UnionType} */ (t); - /** @type {Set} */ - const keys = new Set(); - for (const inner of u.types) { - const got = collectLiteralStringKeys(inner); - if (!got) { - return undefined; - } - for (const k of got) keys.add(k); - } - return keys.size ? keys : undefined; - } - return undefined; -} - -/** - * Filter each arm to `Property` reflections and dedupe by name, returning a single sorted list - * (or `undefined` if every arm was empty). Used for intersection / union / generic-instantiation - * arm merges in {@link resolveDeclarationWithObjectMembers}. + * Empty string when the returns body only shows a type (no `@returns` text) — matches + * `buildMethodMdx` which only emits `Returns …` when `@returns` tag content is present. * - * Default behavior is "later arm wins" overwrite (right for intersections + generic instantiations - * where every arm's properties are part of the final shape). For unions, set - * `{ skipNever: true, pickBetter: true }`: union arms often use `prop?: never` as a discriminator, - * so we drop those and keep the documentable branch when names collide. - * - * @param {Array} arms - * @param {{ skipNever?: boolean, pickBetter?: boolean }} [options] - * @returns {import('typedoc').DeclarationReflection[] | undefined} + * @param {string} returnsBody */ -function mergePropertyArms(arms, options) { - /** @type {Map} */ - const byName = new Map(); - for (const arm of arms) { - if (!arm?.length) { - continue; - } - for (const c of arm) { - if (!c.kindOf(ReflectionKind.Property)) { - continue; - } - if (options?.skipNever && propertyReflectionTypeIsNever(c)) { - continue; - } - const existing = byName.get(c.name); - if (!existing) { - byName.set(c.name, c); - continue; - } - byName.set(c.name, options?.pickBetter ? pickBetterUnionPropertyCandidate(existing, c) : c); +function extractReturnsProseFromReturnsBody(returnsBody) { + if (!returnsBody) return ''; + const dashIdx = returnsBody.indexOf(' — '); + let rightSide = ''; + if (dashIdx !== -1) { + rightSide = returnsBody.slice(dashIdx + 3); + } else { + const paragraphs = returnsBody.split('\n\n'); + if (paragraphs.length >= 2) { + rightSide = paragraphs.slice(1).join('\n\n'); } } - if (byName.size === 0) { - return undefined; - } - const merged = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)); - return substituteTypeParameterDefaultsInChildren(merged); + if (!rightSide) return ''; + // Strip leading `- ` or `* ` prefix (source `@returns - foo` renders with a leading dash) and + // lowercase the first character — matches custom-theme's `formatReturnsLineFromTag` / + // `normalizeReturnsBody`. + const normalized = rightSide.replace(/^\s*[-*]\s+/, '').trim(); + return `Returns ${normalized.charAt(0).toLowerCase()}${normalized.slice(1)}`; } /** - * Resolve a parameter / property type to the list of `Property` reflections that should populate - * a nested rows table. TypeDoc applies `@param parent.prop` descriptions onto these reflections. - * - * Cases: - * - `reflection` (inline `{...}`): the declaration's own children. - * - `reference` to a named interface/alias: the target's children, or — for generic instantiations - * like `ClerkPaginationParams<{ status?: … }>` — the base properties merged with each typeArg's. - * - `intersection`: every `&` arm's properties combined (later arm wins on name collision). - * - `union`: every `|` arm's properties combined, dropping `prop?: never` discriminators and - * preferring the branch with more documentation on collisions. - * - `optional`: unwrap and recurse. - * - * Returns `undefined` when nothing resolves (so callers can `if (!children?.length)` cheaply). - * The children list may include non-`Property` kinds for direct `reflection` / `reference` cases — - * callers that need only `Property` should filter; merge cases (typeArgs / intersection / union) - * pre-filter via {@link mergePropertyArms}. + * Reshape a parent-page property-table H2 section into the sub-doc shape produced by the + * marker-block path (`buildExtractMethodsNamespacePropertyTableMdx` / + * `buildPropertyTableDocMdx`). The parent's `## `name`` heading is demoted to `### `name``; + * everything after the heading (description + table body) is preserved verbatim so whitespace + * matches the marker-block output byte-for-byte. * - * @param {import('typedoc').SomeType | undefined} t - * @param {import('typedoc').ProjectReflection | undefined} [project] For resolving references when `ref.reflection` is missing (intersections like `Foo & WithOptionalOrgType<…>`). - * @returns {import('typedoc').DeclarationReflection[] | undefined} + * @param {string} section - full section text as returned by {@link findMethodH2Sections} + * @returns {string} */ -function resolveDeclarationWithObjectMembers(t, project) { - if (!t) { - return undefined; - } - if (t.type === 'optional') { - return resolveDeclarationWithObjectMembers(/** @type {import('typedoc').OptionalType} */ (t).elementType, project); - } - if (t.type === 'array') { - return resolveDeclarationWithObjectMembers(/** @type {import('typedoc').ArrayType} */ (t).elementType, project); - } - if (t.type === 'reflection') { - const children = t.declaration?.children; - return children?.length ? children : undefined; - } - if (t.type === 'reference') { - const ref = /** @type {import('typedoc').ReferenceType} */ (t); - /** - * `Omit` / `Pick` — TypeScript built-in utilities. They have no project reflection to look up, and falling through to the generic-instantiation path below would merge K's properties (zero, since K is a literal type) without applying the filter — Omit/Pick would silently behave like an identity. Resolve `X` to its property list, then keep/drop by the literal-string keys in `K`. Without this, `Array>` shows no nested rows because `decl` is undefined. - */ - if ((ref.name === 'Omit' || ref.name === 'Pick') && (ref.typeArguments?.length ?? 0) === 2) { - const baseChildren = resolveDeclarationWithObjectMembers(ref.typeArguments[0], project); - const keys = collectLiteralStringKeys(ref.typeArguments[1]); - if (baseChildren?.length && keys) { - const keep = ref.name === 'Pick' ? c => keys.has(c.name) : c => !keys.has(c.name); - return baseChildren.filter(keep); - } - } - let decl = - ref.reflection && 'kind' in ref.reflection - ? /** @type {import('typedoc').DeclarationReflection} */ (ref.reflection) - : undefined; - if (!decl && project && ref.name) { - decl = lookupInterfaceOrTypeAliasByName(project, ref.name); - } - if (!decl) { - return undefined; - } - /** - * Generic instantiation: TypeDoc often attaches pagination fields only to the target alias's - * own `children` and omits `decl.type`, so returning the base early drops the type argument - * object. Merge the base (`decl.type` if present, else `decl.children` as a fallback) with - * each type argument's properties. - */ - const typeArgs = ref.typeArguments ?? []; - if (typeArgs.length > 0) { - const baseFromType = decl.type ? resolveDeclarationWithObjectMembers(decl.type, project) : undefined; - const base = baseFromType ?? (decl.children?.length ? decl.children : undefined); - const argArms = typeArgs.map(ta => resolveDeclarationWithObjectMembers(ta, project)); - return mergePropertyArms([base, ...argArms]); - } - if (decl.children?.length) { - return substituteTypeParameterDefaultsInChildren(decl.children); - } - if (decl.type) { - return resolveDeclarationWithObjectMembers(decl.type, project); - } - return undefined; - } - if (t.type === 'intersection') { - const inter = /** @type {import('typedoc').IntersectionType} */ (t); - return mergePropertyArms(inter.types.map(inner => resolveDeclarationWithObjectMembers(inner, project))); - } - if (t.type === 'union') { - const u = /** @type {import('typedoc').UnionType} */ (t); - return mergePropertyArms( - u.types.map(inner => resolveDeclarationWithObjectMembers(inner, project)), - { skipNever: true, pickBetter: true }, - ); - } - return undefined; +function reshapeAggregatorPropertyTableSectionToExtractedFile(section) { + const nlIdx = section.indexOf('\n'); + if (nlIdx === -1) return `${section.replace(/^## /, '### ').trimEnd()}\n`; + const rest = section.slice(nlIdx + 1); + const demotedTitle = section.slice(0, nlIdx).replace(/^## /, '### '); + return `${demotedTitle}\n${rest.trimEnd()}\n`; } /** - * Build the name cell for a nominal-nested row. Uses `?.` when the parent param is optional (so `options?.foo` mirrors how it would be accessed at runtime) and `.` when required — same rule as `clerkParametersTable.flattenParams` in `custom-theme.mjs`. Appends `?` to the child name when the child property itself is optional, matching how the inline-flatten path renders `params.field?` via the standard parametersTable. + * Reshape a parent-page `## `name()`` H2 section into the per-method extracted-file shape produced + * by `buildMethodMdx` (which the marker-block path historically wrote). The output is intended to + * be byte-identical to the marker-block output so we can drop the marker block emission entirely. * - * @param {import('typedoc').ParameterReflection} parentParam - * @param {import('typedoc').DeclarationReflection} child - */ -function formatNestedParamNameColumn(parentParam, child) { - const sep = parentParam.flags?.isOptional ? '?.' : '.'; - const childOptional = child.flags?.isOptional ? '?' : ''; - return `\`${parentParam.name}${sep}${child.name}${childOptional}\``; -} - -/** - * When TypeDoc renders a parameter type as a markdown link to another generated `.mdx` file, that type has a dedicated page — omit nested `param?.prop` rows so readers follow the type link instead. - * `@inline` aliases are expanded by the theme and do not link to a standalone page unless `@standalonePage` is set (`standalone-page-tag.mjs`). + * - **`reference` format** (shared reference-object pages): keeps an `### `name()`` H3 title. + * Params heading is demoted to H4 (`#### `TypeName`` or `#### Parameters`). + * - **`page` format** (backend API pages, one method per docs page): no title. Params heading + * is promoted to H2 (`## `TypeName`` or `## Parameters`). * - * @param {import('typedoc').SomeType | undefined} t - * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx - */ -function parameterTypeLinksToStandaloneMdxPage(t, ctx) { - const bare = unwrapOptional(t); - if (!bare) { - return false; - } - // `Array`: the standalone page documents the element shape, not the array signature. - // Surface both — the Type column links to the element page, and nested `params.` rows flatten the element so readers don't have to navigate just to see field names. - if (bare.type === 'array') { - return false; - } - if (bare.type === 'reference') { - const ref = /** @type {import('typedoc').ReferenceType} */ (bare); - if (isInlineModifierWithoutStandalonePage(ref.reflection)) { - return false; - } - } - const md = removeLineBreaksForTableCell(ctx.partials.someType(bare) ?? '') ?? ''; - return /\.mdx(?:#[^)]*)?\)/.test(md); -} - -/** - * Rows for object properties on a nominal param type (e.g. `HandleOAuthCallbackParams`), including from `@param parent.prop` on the method. - * Lists every property on the resolved shape; uses the property comment when present, otherwise `—` (intersection aliases often omit comments on some arms in the model). + * Params heading is always followed by `\n\n` (blank line + blank line, i.e. `\n\n\n`) before the + * table body — matches the buildMethodMdx output. Every consecutive chunk is separated by a + * single blank line. * - * @param {import('typedoc').ParameterReflection} param - * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx + * @param {ReturnType} parts + * @param {'reference' | 'page'} methodFormat + * @returns {string} */ -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 []; - } - } +function reshapeAggregatorSectionToExtractedFile(parts, methodFormat) { + const returnsProse = extractReturnsProseFromReturnsBody(parts.returnsBody); + const descriptionWithReturns = returnsProse + ? parts.description + ? `${parts.description}\n\n${returnsProse}` + : returnsProse + : parts.description; - if (parameterTypeLinksToStandaloneMdxPage(param.type, ctx)) { - return []; + let outputParamsHeading = ''; + if (parts.paramsHeadingLine) { + outputParamsHeading = + methodFormat === 'reference' + ? parts.paramsHeadingLine.replace(/^### /, '#### ') + : parts.paramsHeadingLine.replace(/^### /, '## '); } - const project = /** @type {import('typedoc').ProjectReflection | undefined} */ (param.project ?? ctx.page?.project); - const children = resolveDeclarationWithObjectMembers(param.type, project); - if (!children?.length) { - return []; - } - const props = 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 typeCell = child.type ? removeLineBreaksForTableCell(ctx.partials.someType(child.type)) : '`unknown`'; - const nestedNameCol = formatNestedParamNameColumn(param, child); - const nestedDescRaw = renderNestedRowDescription(child.comment); - // Strip line breaks so multi-line `
      ` / paragraph descriptions don't shatter the markdown - // table row (which must be a single line). Matches the treatment already applied to typeCell. - const nestedDesc = removeLineBreaksForTableCell(nestedDescRaw) ?? '—'; - rows.push(`| ${nestedNameCol} | ${typeCell} | ${nestedDesc} |`); + const chunks = []; + if (methodFormat === 'reference') chunks.push(`### \`${parts.name}()\``); + if (descriptionWithReturns) chunks.push(descriptionWithReturns); + if (parts.signature) chunks.push(parts.signature); + if (outputParamsHeading && parts.paramsBody) { + // Match buildMethodMdx: params heading is followed by a blank + blank before the table body. + chunks.push(`${outputParamsHeading}\n\n\n${parts.paramsBody}`); + } else if (outputParamsHeading) { + chunks.push(outputParamsHeading); } - return rows; + return chunks.join('\n\n').trim() + '\n'; } /** - * Merged / external references sometimes leave {@link ReferenceType.reflection} unset; resolve by name. + * Iterate `## `name()`` (method) and `## `name`` (property-table) H2 sections in the parent + * contents, returning each as `{ name, section, shape }` in document order. Sections extend from + * a `## ` heading until the next `## ` heading or end-of-file. Skips `## Properties` and any + * other H2s that aren't identifier-shaped headings (e.g. `## Overview`). * - * @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 the section title (the type's name), the resolved property list, and the - * source `typeDecl` for `@experimental` / `@deprecated` checks. + * A section is classified `propertyTable` when its title has no `()` — matches what + * `renderAggregatorPropertyTableSection` emits for `@extractMethods` namespace and non-callable + * object-shape members. * - * @param {import('typedoc').SomeType | undefined} t - * @param {import('typedoc').ProjectReflection} project - * @returns {{ sectionTitle: string, children: 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') { - return undefined; - } - const ref = /** @type {import('typedoc').ReferenceType} */ (t); - const 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; + * @param {string} contents + * @returns {{ name: string, section: string, shape: 'method' | 'propertyTable' }[]} + */ +function findMethodH2Sections(contents) { + if (!contents) return []; + const lines = contents.split('\n'); + /** @type {{ start: number, name: string, shape: 'method' | 'propertyTable' }[]} */ + const starts = []; + for (let i = 0; i < lines.length; i++) { + const method = lines[i].match(/^## `?([A-Za-z_$][A-Za-z0-9_$.]*)`?\(\)`?[ \t]*$/); + if (method) { + starts.push({ start: i, name: method[1], shape: 'method' }); + continue; } - return { sectionTitle: typeDecl.name, children: typeDecl.children, typeDecl }; - } - if (typeDecl.kindOf(ReflectionKind.TypeAlias)) { - // Prefer resolving `typeAlias.type` so intersections and generic instantiations (e.g. `ClerkPaginationParams<{ status?: … }>`) merge every `&` arm into one property list. - // Some aliases only attach members on `typeDecl.children` with no object shape on `.type`; keep that fallback (e.g. `SignOutOptions`, `JoinWaitlistParams`). - const fromResolvedType = typeDecl.type ? resolveDeclarationWithObjectMembers(typeDecl.type, project) : undefined; - const children = fromResolvedType?.length ? fromResolvedType : typeDecl.children; - if (!children?.length) { - return undefined; + const propTable = lines[i].match(/^## `([A-Za-z_$][A-Za-z0-9_$.]*)`[ \t]*$/); + if (propTable) { + starts.push({ start: i, name: propTable[1], shape: 'propertyTable' }); + } + } + /** @type {{ name: string, section: string, shape: 'method' | 'propertyTable' }[]} */ + const out = []; + for (let i = 0; i < starts.length; i++) { + const startLine = starts[i].start; + const nextInList = i + 1 < starts.length ? starts[i + 1].start : -1; + // Also stop at any other `## ` H2 that isn't in our list (e.g. `## Properties`). + let endLine = lines.length; + for (let j = startLine + 1; j < lines.length; j++) { + if (lines[j].startsWith('## ')) { + endLine = j; + break; + } } - return { sectionTitle: typeDecl.name, children, typeDecl }; + if (nextInList !== -1 && nextInList < endLine) endLine = nextInList; + // Trim trailing separator lines like `***` typedoc places between natural sections. + let sectionLines = lines.slice(startLine, endLine); + while (sectionLines.length && /^(\s|\*+\s*)*$/.test(sectionLines[sectionLines.length - 1])) sectionLines.pop(); + out.push({ name: starts[i].name, section: sectionLines.join('\n'), shape: starts[i].shape }); } - return undefined; -} - -/** - * Nominal param sections are skipped when there is no prose anywhere — avoids huge undocumented tables. - * Type-only aliases often use `@experimental` / `@deprecated` on the type with an empty summary; intersection params like `GetPaymentAttemptParams` still have documented arms (`id`, pagination) and must inline. - * - * @param {import('typedoc').DeclarationReflection} typeDecl - * @param {import('typedoc').DeclarationReflection[]} props - */ -function isNominalParamTypeDocumented(typeDecl, props) { - if (typeDecl.comment?.summary?.length) { - return true; - } - const blockTags = typeDecl.comment?.blockTags ?? []; - if (blockTags.some(t => t.tag !== '@inline')) { - return true; - } - return props.some(p => p.comment?.summary?.length); + return out; } /** - * Single parameter that is a named object type (interface / type alias): one section titled after the type, table lists every property (not the outer `params` row). Uses the same `propertiesTable` partial as TypeDoc. + * Extract the Properties body from between markers. Called AFTER prettier so the returned body + * is column-aligned, matching pre-refactor behavior where the properties.mdx was sliced from the + * already-prettified page. * - * @param {import('typedoc').SignatureReflection} sig - * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx + * @param {string} contents * @returns {string | undefined} */ -/** - * @param {import('typedoc').SignatureReflection} sig - * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx - * @param {number} [headingLevel] Defaults to 4 (reference-object format); pass 2 for page format. - */ -function trySingleNominalParameterTypeSection(sig, ctx, headingLevel = 4) { - const params = sig.parameters ?? []; - if (params.length !== 1) { - return undefined; - } - const p = params[0]; - const project = sig.project ?? ctx.page?.project; - const nominal = resolveNominalObjectTypeForSingleParam(p.type, project); - if (!nominal) { - return undefined; - } - const props = nominal.children.filter(c => c.kindOf(ReflectionKind.Property)); - if (props.length === 0) { - return undefined; - } - if (!isNominalParamTypeDocumented(nominal.typeDecl, props)) { - return undefined; - } - const tableMd = renderMemberTableOmittingExampleBlocks(props, ctx, () => - ctx.partials.propertiesTable( - props, - /** @type {Parameters[1]} */ - ({ - kind: nominal.typeDecl.kind, - isEventProps: false, - applyAllowlistedPropertyTableRowFilters: false, - }), - ), - ); - if (!tableMd?.trim()) { - return undefined; - } - return [markdownHeadingInlineCode(headingLevel, nominal.sectionTitle), '', tableMd, ''].join('\n'); -} - -/** - * @param {import('typedoc').SignatureReflection} sig - * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx - * @param {Map | undefined} instantiationMap - * @param {number} [headingLevel] Defaults to 4 (reference-object format); pass 2 for page format. - */ -function parametersMarkdownTable(sig, ctx, instantiationMap, headingLevel = 4) { - const sigForDisplay = signatureWithInstantiation(sig, instantiationMap); - const params = sigForDisplay.parameters ?? []; - if (params.length === 0) { - return ''; - } - - const singleNominal = trySingleNominalParameterTypeSection(sigForDisplay, ctx, headingLevel); - if (singleNominal) { - return singleNominal; - } - - let tableMd = renderMemberTableOmittingExampleBlocks(params, ctx, () => ctx.partials.parametersTable(params)); - /** @type {string[]} */ - const nested = []; - for (const p of params) { - nested.push(...nestedParameterRowsFromDocumentedProperties(p, ctx)); - } - if (nested.length) { - tableMd = `${tableMd.trimEnd()}\n${nested.join('\n')}\n`; - } - - return [markdownHeading(headingLevel, ReflectionKind.pluralString(ReflectionKind.Parameter)), '', tableMd, ''].join( - '\n', - ); -} - -/** - * @param {import('typedoc').DeclarationReflection} decl - * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx - * @param {{ qualifiedName?: string, methodFormat?: MethodFormat }} [options] Nested namespace methods use `parent.child` for headings / signatures. - */ -function buildMethodMdx(decl, ctx, options = {}) { - const name = options.qualifiedName ?? decl.name; - const methodFormat = options.methodFormat ?? 'reference'; - const sig = getPrimaryCallSignature(decl); - if (!sig) { - return ''; - } - // `'page'` format renders one method per docs page, so the page is anchored by a containing file/heading upstream — no in-page `### foo()` title needed. `'reference'` format aggregates many methods on one page and uses `### foo()` to disambiguate them. - const title = methodFormat === 'page' ? '' : `### \`${name}()\``; - const paramsHeadingLevel = methodFormat === 'page' ? 2 : 4; - /** 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 = comment === sig.comment ? '' : appendSignatureOnlyReturns(decl.comment, sig.comment); - if (sigReturns) { - description = [description, sigReturns].filter(Boolean).join('\n\n'); - } - const instantiationMap = getGenericInstantiationMapFromCallableProperty(decl); - const ts = ['```typescript', formatTypeScriptSignature(sig, name, instantiationMap), '```'].join('\n'); - const skipParametersSection = - Boolean(decl.comment?.hasModifier('@skipParametersSection')) || - Boolean(sig.comment?.hasModifier('@skipParametersSection')); - if (!skipParametersSection) { - overlayParamCommentsFromSignatureBlockTags(sig); - } - const paramsMd = skipParametersSection ? '' : parametersMarkdownTable(sig, ctx, instantiationMap, paramsHeadingLevel); - - // 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 headSource = title ? [title, '', description].join('\n') : description; - const head = applyCatchAllMdReplacements(applyRelativeLinkReplacements(headSource)); - const paramsProcessed = paramsMd ? applyCatchAllMdReplacements(applyRelativeLinkReplacements(paramsMd)) : ''; - const chunks = head ? [head, ts] : [ts]; - if (paramsProcessed) { - chunks.push(paramsProcessed); - } - return chunks.join('\n\n').trim() + '\n'; -} - -/** - * @param {import('typedoc').DeclarationReflection} decl - */ -function hasExtractMethodsModifier(decl) { - return Boolean(decl.comment?.hasModifier('@extractMethods')); +function extractPropertiesBetweenMarkers(contents) { + const m = contents.match(/\n?([\s\S]*?)\n?/); + if (!m) return undefined; + const body = m[1].trim(); + return body.length ? body : undefined; } /** - * @typedef {{ filePath: string, content: string }} ExtractedFile - */ - -/** - * Collect `methods/-.mdx` content for each direct member of an `@extractMethods` object-like type: callables via {@link buildMethodMdx}, non-callables with a resolvable object shape via {@link buildPropertyTableDocMdx}. Plus a `.mdx` index for non-callable members. + * Remove the entire `## Properties` region plus its markers, producing output equivalent to the + * pre-refactor `stripReferenceObjectPropertiesSection` (which did + * `contents.replace(/\n## Properties\n+[\s\S]*$/, '').trimEnd() + '\n'`). * - * Supports inline object literals and named references (`interface` / object-like `type` aliases) via {@link resolveDeclarationWithObjectMembers}. + * Pages where `## Properties` is the very first line (no leading `\n`) intentionally do NOT get + * their section stripped — matches pre-refactor behavior. In that case we still remove the wrapper + * markers themselves so they don't leak into the final MDX. * - * @param {import('typedoc').DeclarationReflection} parentDecl - * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx - * @param {string} outDir - * @returns {ExtractedFile[]} - */ -function processExtractMethodsNamespace(parentDecl, ctx, outDir) { - const project = ctx.page?.project; - const members = resolveDeclarationWithObjectMembers(parentDecl.type, project) ?? []; - if (members.length === 0) { - console.warn( - `[extract-methods] @extractMethods on "${parentDecl.name}" requires an object-like type with members; skipping nested extraction`, + * @param {string} contents + * @returns {string} + */ +function stripPropertiesSectionWithMarkers(contents) { + if (!contents.includes('')) { + return contents; + } + // Case 1: leading `\n## Properties` — strip the entire section (heading + markers + body) and + // any trailing content, then re-add a single trailing newline. Mirrors the pre-refactor regex. + if (/\n## Properties\n+/.test(contents)) { + const stripped = contents.replace( + /\n## Properties\n+[\s\S]*?[\s\S]*$/, + '', ); - return []; - } - const parentName = parentDecl.name; - /** @type {ExtractedFile[]} */ - const collected = []; - /** @type {import('typedoc').DeclarationReflection[]} */ - const nonCallableMembers = []; - for (const nested of members) { - if (nested.name.startsWith('__')) { - continue; - } - const nd = /** @type {import('typedoc').DeclarationReflection} */ (nested); - const fileSlug = `${toFileSlug(parentName)}-${toFileSlug(nd.name)}`; - const filePath = path.join(outDir, `${fileSlug}.mdx`); - if (shouldExtractCallableMember(nd, ctx)) { - const mdx = buildMethodMdx(nd, ctx, { qualifiedName: `${parentName}.${nd.name}` }); - if (!mdx) { - continue; - } - collected.push({ filePath, content: mdx }); - continue; - } - nonCallableMembers.push(nd); - const propTableMdx = buildPropertyTableDocMdx(parentName, nd, ctx); - if (!propTableMdx) { - continue; - } - collected.push({ filePath, content: propTableMdx }); - } - if (nonCallableMembers.length) { - const namespaceMdx = buildExtractMethodsNamespacePropertyTableMdx(parentDecl, nonCallableMembers, ctx); - if (namespaceMdx) { - const namespacePath = path.join(outDir, `${toFileSlug(parentName)}.mdx`); - collected.push({ filePath: namespacePath, content: namespaceMdx }); - } - } - return collected; -} - -/** - * Collect (path, content) pairs for each callable/`@extractMethods` child on `decl`. Callers are responsible for writing — see {@link load} which prettifies then writes. - * - * @param {import('typedoc').DeclarationReflection} decl - * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx - * @param {string} outDir - * @param {MethodFormat} [methodFormat] - * @returns {ExtractedFile[]} - */ -function extractCallableMembersFromDeclaration(decl, ctx, outDir, methodFormat = 'reference') { - if (!decl.children) { - return []; - } - /** @type {ExtractedFile[]} */ - const collected = []; - for (const child of decl.children) { - if (child.name.startsWith('__')) { - continue; - } - const childDecl = /** @type {import('typedoc').DeclarationReflection} */ (child); - - if (hasExtractMethodsModifier(childDecl)) { - collected.push(...processExtractMethodsNamespace(childDecl, ctx, outDir)); - continue; - } - - if (shouldExtractCallableMember(childDecl, ctx)) { - const mdx = buildMethodMdx(childDecl, ctx, { methodFormat }); - if (mdx) { - const fileName = `${toFileSlug(child.name)}.mdx`; - const filePath = path.join(outDir, fileName); - collected.push({ filePath, content: mdx }); - } - } - } - return collected; -} - -/** - * @param {import('typedoc-plugin-markdown').MarkdownPageEvent} output - * @returns {string | undefined} - */ -function matchReferenceObjectPageUrl(output) { - if (!output.url) { - return undefined; + return stripped.trimEnd() + '\n'; } - const normalized = output.url.replace(/\\/g, '/'); - if (normalized in REFERENCE_OBJECT_CONFIG) return normalized; - if (normalized in BACKEND_API_CONFIG) return normalized; - return undefined; -} - -/** @param {string} pageUrl */ -function configEntryForPageUrl(pageUrl) { - return /** @type {import('./reference-objects.mjs').REFERENCE_OBJECT_CONFIG[keyof typeof REFERENCE_OBJECT_CONFIG]} */ ( - REFERENCE_OBJECT_CONFIG[/** @type {keyof typeof REFERENCE_OBJECT_CONFIG} */ (pageUrl)] ?? - BACKEND_API_CONFIG[/** @type {keyof typeof BACKEND_API_CONFIG} */ (pageUrl)] - ); + // Case 2: `## Properties` at start-of-file (billing-namespace pattern). Pre-refactor left the + // section in place. Strip just the marker comments plus any adjacent blank lines prettier may + // have inserted around them. + return contents + .replace(/\n+/g, '') + .replace(/\n+\n*/g, '\n'); } /** - * Plugin entry: registers a `MarkdownPageEvent.END` listener that, for each page in {@link REFERENCE_OBJECT_CONFIG}, queues a `preWriteAsyncJob` to extract Properties + methods. - * - * The job runs **after** typedoc-plugin-markdown's own prettier job (also a `preWriteAsyncJob`, queued during `renderDocument`) — so by the time we read `output.contents`, the Properties table is already prettier-formatted, and our `properties.mdx` inherits that formatting. Method files are written raw (matching the pre-refactor behavior, where extract-methods.mjs also bypassed prettier for `methods/*.mdx`). - * - * Must be loaded **after** `custom-plugin.mjs` so its END listener (link replacements + heading filtering) runs first. + * Plugin entry: registers a `MarkdownPageEvent.END` listener that text-slices per-H2 sections + * from the parent contents synchronously (pre-prettier), then defers Properties extraction / + * marker stripping until after prettier via `preWriteAsyncJobs`. * * @param {import('typedoc-plugin-markdown').MarkdownApplication} app */ export function load(app) { - app.renderer.on(MarkdownPageEvent.END, output => { - const pageUrl = matchReferenceObjectPageUrl(output); - if (!pageUrl) { - return; - } - const entry = configEntryForPageUrl(pageUrl); - const methodFormat = methodFormatForPageUrl(pageUrl); - const decl = /** @type {import('typedoc').DeclarationReflection | undefined} */ (output.model); - if (!decl?.children) { - console.warn(`[extract-methods] No children on reflection for ${pageUrl}, skipping`); - return; - } - const project = output.project; - if (!project) { - console.warn(`[extract-methods] No project on page event for ${pageUrl}, skipping`); - return; - } - const theme = /** @type {InstanceType | undefined} */ ( - app.renderer.theme - ); - if (!theme || typeof theme.getRenderContext !== 'function') { - console.warn(`[extract-methods] Renderer theme not ready for ${pageUrl}, skipping`); - return; - } - const ctx = /** @type {import('typedoc-plugin-markdown').MarkdownThemeContext} */ (theme.getRenderContext(output)); - - const objectDir = path.dirname(output.filename); - const outDir = path.join(objectDir, 'methods'); - - /** @type {ExtractedFile[]} */ - const methodFiles = extractCallableMembersFromDeclaration(decl, ctx, outDir, methodFormat); - const extraMethodInterfaces = 'extraMethodInterfaces' in entry ? entry.extraMethodInterfaces : undefined; - if (Array.isArray(extraMethodInterfaces)) { - for (const extra of extraMethodInterfaces) { - const extraDecl = findInterfaceOrClass(project, extra.symbol, extra.declarationHint); - if (!extraDecl?.children) { - console.warn(`[extract-methods] extraMethodInterfaces: could not find "${extra.symbol}" for ${pageUrl}`); - continue; + // Priority -200: fire after `custom-theme.mjs`'s emit listener (registered at -100), which in + // turn fires after `custom-plugin.mjs`'s link replacements (default priority 0). By the time + // this slicer runs, aggregator sections have been synthesized and the Properties section has + // been wrapped — and no other END listener will touch our contents after us. + app.renderer.on( + MarkdownPageEvent.END, + output => { + const contents = output.contents ?? ''; + const normUrl = (output.url ?? '').replace(/\\/g, '/'); + const pageUrl = output.url ?? '(unknown)'; + const objectDir = path.dirname(output.filename); + const methodsDir = path.join(objectDir, 'methods'); + + // Slice `## `methodName()`` and `## `namespace`` H2 sections from the parent's pre-prettier + // content and reshape into per-method / per-property-table extracted files. Handles every + // callable member (including `@extractMethods` namespace callables emitted with qualified + // names) and every `@extractMethods` namespace / non-callable-with-object-shape property. + const methodFormat = + normUrl in REFERENCE_OBJECT_CONFIG ? 'reference' : normUrl in BACKEND_API_CONFIG ? 'page' : undefined; + if (methodFormat) { + const h2Sections = findMethodH2Sections(contents); + if (h2Sections.length) { + fs.mkdirSync(methodsDir, { recursive: true }); + for (const { name, section, shape } of h2Sections) { + const extracted = + shape === 'propertyTable' + ? reshapeAggregatorPropertyTableSectionToExtractedFile(section) + : reshapeAggregatorSectionToExtractedFile(parseParentAggregatorSection(section), methodFormat); + const filename = `${name.split('.').map(toFileSlug).join('-')}.mdx`; + fs.writeFileSync(path.join(methodsDir, filename), extracted, 'utf-8'); + } + console.log(`[extract-methods] ${pageUrl}: text-slice wrote ${h2Sections.length} method file(s)`); } - methodFiles.push(...extractCallableMembersFromDeclaration(extraDecl, ctx, outDir, methodFormat)); - } - } - - output.preWriteAsyncJobs.push(() => { - fs.mkdirSync(objectDir, { recursive: true }); - - // `output.contents` is already prettier-formatted by typedoc-plugin-markdown's earlier - // pre-write job. Extract the Properties body from it (also formatted), write it out, - // then strip the section so the main page no longer ships it. - const { propertiesBody, stripped } = splitPropertiesFromContents(output.contents ?? ''); - if (propertiesBody) { - const propertiesPath = path.join(objectDir, 'properties.mdx'); - fs.writeFileSync(propertiesPath, `${propertiesBody.trimEnd()}\n`, 'utf-8'); - console.log(`[extract-methods] Wrote ${path.relative(path.join(__dirname, '..'), propertiesPath)}`); - } - if (stripped && stripped !== output.contents) { - output.contents = stripped; } - if (methodFiles.length === 0) { + if (!contents.includes('')) { return; } - fs.mkdirSync(outDir, { recursive: true }); - for (const { filePath, content } of methodFiles) { - fs.writeFileSync(filePath, content, 'utf-8'); - console.log(`[extract-methods] Wrote ${path.relative(path.join(__dirname, '..'), filePath)}`); - } - console.log(`[extract-methods] ${pageUrl}: wrote ${methodFiles.length} method file(s)`); - }); - }); + + output.preWriteAsyncJobs.push(() => { + const post = output.contents ?? ''; + + // Extract the (now prettier-formatted) Properties body from between its markers, write it + // out, then strip the whole `## Properties` region (heading + markers + body) from the + // parent page. + const propertiesBody = extractPropertiesBetweenMarkers(post); + if (propertiesBody) { + fs.mkdirSync(objectDir, { recursive: true }); + const propertiesPath = path.join(objectDir, 'properties.mdx'); + fs.writeFileSync(propertiesPath, `${propertiesBody.trimEnd()}\n`, 'utf-8'); + console.log(`[extract-methods] Wrote ${path.relative(path.join(__dirname, '..'), propertiesPath)}`); + } + output.contents = stripPropertiesSectionWithMarkers(post); + }); + }, + -200, + ); } diff --git a/.typedoc/extract-returns-and-params.mjs b/.typedoc/extract-returns-and-params.mjs index 4cec2a53cee..28c39dcc65c 100644 --- a/.typedoc/extract-returns-and-params.mjs +++ b/.typedoc/extract-returns-and-params.mjs @@ -6,8 +6,36 @@ import { fileURLToPath } from 'node:url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +/** + * Packages whose pages this script post-processes (Returns + Parameters extracted into sibling + * `-return.mdx` / `-params.mdx` files). Pages under these packages get their nominal-type + * parameter heading normalized back to the generic `## Parameters` before extraction — see + * {@link normalizeNominalParametersHeading}. + */ +const EXTRACT_RETURNS_AND_PARAMS_PACKAGES = ['react']; + const LEGACY_HOOK_NAMES = new Set(['use-sign-in-1', 'use-sign-up-1']); +/** + * `custom-theme.mjs` uniformly swaps a method's `## Parameters` heading to `` ## `TypeName` `` + * when the sole argument is a nominal object type — so every parameters table in the docs uses + * one code path. On the pages this script processes, callers still expect the sibling + * `-params.mdx` extraction to find a section keyed on the literal `## Parameters` heading, so we + * rename the nominal H2 heading back to `## Parameters` in place before extraction. + * + * Only H2 headings without parentheses are treated as params-type headings: `` ## `TypeName` `` ✔, + * `` ## `methodName()` `` ✘ (that's a method name on an aggregator page). Rewrites the first match + * only — hook/single-callable pages have at most one such heading per file. + * + * @param {string} content + * @returns {string} + */ +function normalizeNominalParametersHeading(content) { + return content.replace(/(^|\n)## `([A-Za-z_$][A-Za-z0-9_$]*)`(\s*)(\n|$)/, (_m, before, _name, trailing, after) => { + return `${before}## Parameters${trailing}${after}`; + }); +} + /** * Returns legacy hook output info or null if not a legacy hook. * @param {string} filePath @@ -203,8 +231,7 @@ function getAllMdxFiles(dir) { * Main function to process all files from the react package */ function main() { - const packages = ['react']; - const dirs = packages.map(folder => path.join(__dirname, 'temp-docs', folder)); + const dirs = EXTRACT_RETURNS_AND_PARAMS_PACKAGES.map(folder => path.join(__dirname, 'temp-docs', folder)); for (const dir of dirs) { if (!fs.existsSync(dir)) { @@ -219,7 +246,14 @@ function main() { let paramsCount = 0; for (const filePath of mdxFiles) { - const content = fs.readFileSync(filePath, 'utf-8'); + const original = fs.readFileSync(filePath, 'utf-8'); + // Normalize the theme's uniform `` ## `TypeName` `` back to `## Parameters` so both the + // sibling extraction below AND downstream consumers reading the page see the generic + // heading. Persist the rewrite when it changed anything. + const content = normalizeNominalParametersHeading(original); + if (content !== original) { + fs.writeFileSync(filePath, content, 'utf-8'); + } const legacyTarget = getLegacyHookTarget(filePath); // Extract Returns sections