diff --git a/.prettierignore b/.prettierignore index f469f34fac0..55550f719f1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -24,3 +24,5 @@ packages/clerk-js/src/core/resources/index.ts packages/shared/src/compiled /**/CHANGELOG.md renovate.json5 +# Frozen snapshots of TypeDoc-generated MDX; must match raw `extract-methods.mjs` output. +.typedoc/__tests__/__snapshots__/ diff --git a/.typedoc/__tests__/__snapshots__/api-key-resource-methods-create.mdx b/.typedoc/__tests__/__snapshots__/api-key-resource-methods-create.mdx new file mode 100644 index 00000000000..8a60ae22d3d --- /dev/null +++ b/.typedoc/__tests__/__snapshots__/api-key-resource-methods-create.mdx @@ -0,0 +1,21 @@ +### `create()` + +Creates a new API key. + +Returns an [`APIKeyResource`](/docs/reference/types/api-key-resource) object that includes the `secret` property. +> [!WARNING] +> Make sure to store the API key secret immediately after creation, as it will not be available again. + +```typescript +function create(params: CreateAPIKeyParams): Promise +``` + +#### `CreateAPIKeyParams` + + +| Property | Type | Description | +| ------ | ------ | ------ | +| `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. | diff --git a/.typedoc/__tests__/__snapshots__/clerk-methods-handle-email-link-verification.mdx b/.typedoc/__tests__/__snapshots__/clerk-methods-handle-email-link-verification.mdx new file mode 100644 index 00000000000..9622ed9de65 --- /dev/null +++ b/.typedoc/__tests__/__snapshots__/clerk-methods-handle-email-link-verification.mdx @@ -0,0 +1,18 @@ +### `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. | diff --git a/.typedoc/__tests__/__snapshots__/clerk-methods-handle-redirect-callback.mdx b/.typedoc/__tests__/__snapshots__/clerk-methods-handle-redirect-callback.mdx new file mode 100644 index 00000000000..6c50c100af8 --- /dev/null +++ b/.typedoc/__tests__/__snapshots__/clerk-methods-handle-redirect-callback.mdx @@ -0,0 +1,15 @@ +### `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. | diff --git a/.typedoc/__tests__/__snapshots__/clerk-methods-join-waitlist.mdx b/.typedoc/__tests__/__snapshots__/clerk-methods-join-waitlist.mdx new file mode 100644 index 00000000000..cb76cd60a96 --- /dev/null +++ b/.typedoc/__tests__/__snapshots__/clerk-methods-join-waitlist.mdx @@ -0,0 +1,14 @@ +### `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. | diff --git a/.typedoc/__tests__/__snapshots__/clerk-methods-sign-out.mdx b/.typedoc/__tests__/__snapshots__/clerk-methods-sign-out.mdx new file mode 100644 index 00000000000..ea575168ea9 --- /dev/null +++ b/.typedoc/__tests__/__snapshots__/clerk-methods-sign-out.mdx @@ -0,0 +1,15 @@ +### `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. | diff --git a/.typedoc/__tests__/__snapshots__/clerk-properties.mdx b/.typedoc/__tests__/__snapshots__/clerk-properties.mdx new file mode 100644 index 00000000000..104f1a4f92f --- /dev/null +++ b/.typedoc/__tests__/__snapshots__/clerk-properties.mdx @@ -0,0 +1,21 @@ +| Property | Type | Description | +| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `apiKeys` | [`APIKeysNamespace`](/docs/reference/objects/api-keys) | The `APIKeys` object used for managing API keys. | +| `billing` | [`BillingNamespace`](/docs/reference/objects/billing) | The `Billing` object used for managing billing. | +| `client` | undefined \| [ClientResource](/docs/reference/objects/client) | The `Client` object for the current window. | +| `domain` | `string` | The current Clerk app's domain. Prefixed with `clerk.` on production if not already prefixed. Returns `""` when ran on the server. | +| `instanceType` | undefined \| "production" \| "development" | Indicates if the Clerk instance is running in a production or development environment. | +| `isSatellite` | `boolean` | Indicates if the instance is a satellite app. | +| `isSignedIn` | `boolean` | Indicates whether the current user has a valid signed-in client session. | +| `isStandardBrowser` | undefined \| boolean | Indicates if the instance is being loaded in a standard browser environment. Set to `false` on native platforms where cookies cannot be set. When `undefined`, Clerk assumes a standard browser. | +| `loaded` | `boolean` | Indicates if the `Clerk` object is ready for use. Set to `false` when the `status` is `"loading"`. Set to `true` when the `status` is `"ready"` or `"degraded"`. | +| `oauthApplication` | [`OAuthApplicationNamespace`](../o-auth-application-namespace.mdx) | OAuth application helpers (e.g. consent metadata for custom consent UIs). | +| `organization` | undefined \| null \| [OrganizationResource](/docs/reference/objects/organization) | A shortcut to the last active `Session.user.organizationMemberships` which holds an instance of a `Organization` object. If the session is `null` or `undefined`, the user field will match. | +| `proxyUrl` | undefined \| string | **Required for applications that run behind a reverse proxy**. Your Clerk app's proxy URL. Can be either a relative path (`/__clerk`) or a full URL (`https:///__clerk`). | +| `publishableKey` | `string` | Your Clerk [Publishable Key](!publishable-key). | +| `sdkMetadata` | undefined \| \{ environment?: string; name: string; version: string; \} | If present, contains information about the SDK that the host application is using. For example, if Clerk is loaded through `@clerk/nextjs`, this would be `{ name: '@clerk/nextjs', version: '1.0.0' }`. You don't need to set this value yourself unless you're [developing an SDK](/docs/guides/development/sdk-development/overview). | +| `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:
  • `"error"`: Set when hotloading `clerk-js` or `Clerk.load()` failed.
  • `"loading"`: Set during initialization.
  • `"ready"`: Set when Clerk is fully operational.
  • `"degraded"`: Set when Clerk is partially operational.
| +| `telemetry` | undefined \| \{ isDebug: boolean; isEnabled: boolean; record: void; recordLog: void; \} | [Telemetry](/docs/guides/how-clerk-works/security/clerk-telemetry) configuration. | +| `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 new file mode 100644 index 00000000000..8ea6508d37a --- /dev/null +++ b/.typedoc/__tests__/__snapshots__/clerk.mdx @@ -0,0 +1 @@ +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. diff --git a/.typedoc/__tests__/__snapshots__/session-resource-methods-check-authorization.mdx b/.typedoc/__tests__/__snapshots__/session-resource-methods-check-authorization.mdx new file mode 100644 index 00000000000..0e0568beadd --- /dev/null +++ b/.typedoc/__tests__/__snapshots__/session-resource-methods-check-authorization.mdx @@ -0,0 +1,7 @@ +### `checkAuthorization()` + +Checks if the user is [authorized for the specified Role, Permission, Feature, or Plan](/docs/guides/secure/authorization-checks) or requires the user to [reverify their credentials](/docs/guides/secure/reverification) if their last verification is older than allowed. + +```typescript +function checkAuthorization(isAuthorizedParams: CheckAuthorizationParams): boolean +``` diff --git a/.typedoc/__tests__/__snapshots__/sign-in-future-resource-methods-email-code-send-code.mdx b/.typedoc/__tests__/__snapshots__/sign-in-future-resource-methods-email-code-send-code.mdx new file mode 100644 index 00000000000..b3d241d759e --- /dev/null +++ b/.typedoc/__tests__/__snapshots__/sign-in-future-resource-methods-email-code-send-code.mdx @@ -0,0 +1,15 @@ +### `emailCode.sendCode()` + +Sends an email code to sign-in. + +```typescript +function emailCode.sendCode(params?: SignInFutureEmailCodeSendParams): Promise<{ error: null | ClerkError }> +``` + +#### `SignInFutureEmailCodeSendParams` + + +| Property | Type | Description | +| ------ | ------ | ------ | +| `emailAddress?` | `string` | The user's email address. Only supported if [Email address](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#email) is enabled. Provide either `emailAddress` or `emailAddressId`, not both. Omit both when a sign-in already exists. | +| `emailAddressId?` | `string` | The ID for the user's email address that will receive an email with the one-time authentication code. Provide either `emailAddress` or `emailAddressId`, not both. Omit both when a sign-in already exists. | diff --git a/.typedoc/__tests__/__snapshots__/sign-in-future-resource-methods-email-link.mdx b/.typedoc/__tests__/__snapshots__/sign-in-future-resource-methods-email-link.mdx new file mode 100644 index 00000000000..b2f588fd6fb --- /dev/null +++ b/.typedoc/__tests__/__snapshots__/sign-in-future-resource-methods-email-link.mdx @@ -0,0 +1,6 @@ +### `emailLink` + + +| Property | Type | Description | +| ------ | ------ | ------ | +| `verification` | null \| \{ createdSessionId: string; status: "expired" \| "failed" \| "verified" \| "client_mismatch"; verifiedFromTheSameClient: boolean; \} | The verification status of the email link. This property is populated by reading query parameters from the URL after the user visits the email link. Returns `null` if no verification status is available. | diff --git a/.typedoc/__tests__/__snapshots__/user-resource-properties.mdx b/.typedoc/__tests__/__snapshots__/user-resource-properties.mdx new file mode 100644 index 00000000000..87898993fc0 --- /dev/null +++ b/.typedoc/__tests__/__snapshots__/user-resource-properties.mdx @@ -0,0 +1,36 @@ +| Property | Type | Description | +| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `backupCodeEnabled` | `boolean` | Indicates whether the user has enabled backup codes. | +| `createdAt` | null \| Date | The date and time when the user was created. | +| `createOrganizationEnabled` | `boolean` | Indicates whether the user can create organizations. | +| `createOrganizationsLimit` | null \| number | The maximum number of organizations the user can create. | +| `deleteSelfEnabled` | `boolean` | Indicates whether the user can delete their own account. | +| `emailAddresses` | [EmailAddressResource](/docs/reference/types/email-address)[] | An array of all the `EmailAddress` objects associated with the user. Includes the primary. | +| `enterpriseAccounts` | [EnterpriseAccountResource](/docs/reference/types/enterprise-account)[] | An array of all the `EnterpriseAccount` objects associated with the user via enterprise SSO. | +| `externalAccounts` | [ExternalAccountResource](/docs/reference/types/external-account)[] | An array of all the `ExternalAccount` objects associated with the user via OAuth. | +| `externalId` | null \| string | The user's ID as used in your external systems. Must be unique across your instance. | +| `firstName` | null \| string | The user's first name. | +| `fullName` | null \| string | The user's full name. | +| `hasImage` | `boolean` | Indicates whether the user has uploaded an image or one was copied from OAuth. Returns `false` if Clerk is displaying an avatar for the user. | +| `id` | `string` | The unique identifier of the user. | +| `imageUrl` | `string` | Holds the default avatar or user's uploaded profile image. Compatible with Clerk's [Image Optimization](/docs/guides/development/image-optimization). | +| `lastName` | null \| string | The user's last name. | +| `lastSignInAt` | null \| Date | The date and time when the user last signed in. | +| `legalAcceptedAt` | null \| Date | The date and time when the user accepted the legal compliance documents. `null` if [**Require express consent to legal documents**](/docs/guides/secure/legal-compliance) is not enabled. | +| `organizationMemberships` | [OrganizationMembershipResource](/docs/reference/types/organization-membership)[] | An array of all the `OrganizationMembership` objects associated with the user. | +| `passkeys` | [PasskeyResource](/docs/reference/types/passkey-resource)[] | An array of all the `Passkey` objects associated with the user. | +| `passwordEnabled` | `boolean` | Indicates whether the user has a password on their account. | +| `phoneNumbers` | [PhoneNumberResource](/docs/reference/types/phone-number)[] | An array of all the `PhoneNumber` objects associated with the user. Includes the primary. | +| `primaryEmailAddress` | null \| [EmailAddressResource](/docs/reference/types/email-address) | The user's primary email address. | +| `primaryEmailAddressId` | null \| string | The ID of the user's primary email address. | +| `primaryPhoneNumber` | null \| [PhoneNumberResource](/docs/reference/types/phone-number) | The user's primary phone number. | +| `primaryPhoneNumberId` | null \| string | The ID of the user's primary phone number. | +| `primaryWeb3Wallet` | null \| [Web3WalletResource](/docs/reference/types/web3-wallet) | The user's primary Web3 wallet. | +| `primaryWeb3WalletId` | null \| string | The ID of the user's primary Web3 wallet. | +| `publicMetadata` | [UserPublicMetadata](/docs/reference/types/metadata#user-public-metadata) | Metadata that can be read from the Frontend API and Backend API and can be set only from the Backend API. | +| `totpEnabled` | `boolean` | Indicates whether the user has enabled TOTP. | +| `twoFactorEnabled` | `boolean` | Indicates whether the user has enabled two-factor authentication. | +| `unsafeMetadata` | [UserUnsafeMetadata](/docs/reference/types/metadata#user-unsafe-metadata) | Metadata that can be read and set from the Frontend API. It's considered unsafe because it can be modified from the frontend. There is also an `unsafeMetadata` attribute in the [`SignUp`](/docs/reference/objects/sign-up-future) object. The value of that field will be automatically copied to the user's unsafe metadata once the sign up is complete. | +| `updatedAt` | null \| Date | The date and time when the user was last updated. | +| `username` | null \| string | The user's username. | +| `web3Wallets` | [Web3WalletResource](/docs/reference/types/web3-wallet)[] | An array of all the `Web3Wallet` objects associated with the user. Includes the primary. | diff --git a/.typedoc/__tests__/extract-methods.test.ts b/.typedoc/__tests__/extract-methods.test.ts new file mode 100644 index 00000000000..6992eb3dc2e --- /dev/null +++ b/.typedoc/__tests__/extract-methods.test.ts @@ -0,0 +1,88 @@ +import { readFile } from 'fs/promises'; +import { join } from 'path'; +import { describe, expect, it } from 'vitest'; + +/** + * Snapshots for `extract-methods.mjs` output. Each `.mdx` under `__snapshots__/` is a frozen copy of a representative file produced by `typedoc:generate`. Refactors to the plugin or its helpers should leave these files byte-identical; a diff means the change is observable in the published docs and needs a human decision. + * + * Run `pnpm typedoc:generate` first to populate `.typedoc/docs/`, then `vitest run` here. + * To intentionally update a snapshot after reviewing the diff: `vitest run -u`. + * + * Coverage targets one case per non-trivial code path: + * + * - `methods/sign-out.mdx` – simple zero-arg callable + * - `methods/handle-redirect-callback.mdx` – multi-param `parametersTable` with nested rows + * - `methods/handle-email-link-verification.mdx` – required parent (`params`) flattened to `.` + * - `methods/join-waitlist.mdx` – single nominal-param section (`JoinWaitlistParams`) + * - `methods/create.mdx` (api-key) – another single-nominal-param case + warning callout + * - `methods/check-authorization.mdx` – generic instantiation (`CheckAuthorization`) + * - `methods/email-code-send-code.mdx` – qualified name from `@extractMethods` parent + * - `methods/email-link.mdx` – `@extractMethods` namespace index (non-callables) + * - `properties.mdx` (clerk) – properties table sliced from already-prettified page + * - `clerk.mdx` – main page after Properties has been stripped + * - `properties.mdx` (user-resource) – properties with external type links and metadata + */ +const DOCS_DIR = join(process.cwd(), 'docs'); + +async function readGenerated(relPath: string) { + return readFile(join(DOCS_DIR, relPath), 'utf-8'); +} + +describe('extract-methods snapshots', () => { + it('simple callable: clerk.signOut()', async () => { + const content = await readGenerated('shared/clerk/methods/sign-out.mdx'); + await expect(content).toMatchFileSnapshot('./__snapshots__/clerk-methods-sign-out.mdx'); + }); + + it('multi-param method with nested rows: clerk.handleRedirectCallback()', async () => { + const content = await readGenerated('shared/clerk/methods/handle-redirect-callback.mdx'); + await expect(content).toMatchFileSnapshot('./__snapshots__/clerk-methods-handle-redirect-callback.mdx'); + }); + + it('required-parent flatten uses `.` not `?.`: clerk.handleEmailLinkVerification()', async () => { + const content = await readGenerated('shared/clerk/methods/handle-email-link-verification.mdx'); + await expect(content).toMatchFileSnapshot('./__snapshots__/clerk-methods-handle-email-link-verification.mdx'); + }); + + it('single nominal-param section: clerk.joinWaitlist()', async () => { + const content = await readGenerated('shared/clerk/methods/join-waitlist.mdx'); + await expect(content).toMatchFileSnapshot('./__snapshots__/clerk-methods-join-waitlist.mdx'); + }); + + it('single nominal-param + warning callout: apiKeys.create()', async () => { + const content = await readGenerated('shared/api-key-resource/methods/create.mdx'); + await expect(content).toMatchFileSnapshot('./__snapshots__/api-key-resource-methods-create.mdx'); + }); + + it('generic instantiation: session.checkAuthorization()', async () => { + const content = await readGenerated('shared/session-resource/methods/check-authorization.mdx'); + await expect(content).toMatchFileSnapshot('./__snapshots__/session-resource-methods-check-authorization.mdx'); + }); + + it('@extractMethods child: signInFuture.emailCode.sendCode()', async () => { + const content = await readGenerated('shared/sign-in-future-resource/methods/email-code-send-code.mdx'); + await expect(content).toMatchFileSnapshot( + './__snapshots__/sign-in-future-resource-methods-email-code-send-code.mdx', + ); + }); + + it('@extractMethods namespace index: signInFuture.emailLink', async () => { + const content = await readGenerated('shared/sign-in-future-resource/methods/email-link.mdx'); + await expect(content).toMatchFileSnapshot('./__snapshots__/sign-in-future-resource-methods-email-link.mdx'); + }); + + it('properties extracted + prettier-aligned: clerk', async () => { + const content = await readGenerated('shared/clerk/properties.mdx'); + await expect(content).toMatchFileSnapshot('./__snapshots__/clerk-properties.mdx'); + }); + + it('main page after Properties strip: clerk', async () => { + const content = await readGenerated('shared/clerk/clerk.mdx'); + await expect(content).toMatchFileSnapshot('./__snapshots__/clerk.mdx'); + }); + + it('properties with external type links: user-resource', async () => { + const content = await readGenerated('shared/user-resource/properties.mdx'); + await expect(content).toMatchFileSnapshot('./__snapshots__/user-resource-properties.mdx'); + }); +}); diff --git a/.typedoc/__tests__/file-structure.test.ts b/.typedoc/__tests__/file-structure.test.ts index ad1c3770971..5aca0e62a03 100644 --- a/.typedoc/__tests__/file-structure.test.ts +++ b/.typedoc/__tests__/file-structure.test.ts @@ -2,10 +2,7 @@ import { readdir } from 'fs/promises'; import { join, relative } from 'path'; import { describe, expect, it } from 'vitest'; -// Same function as in custom-router.mjs -function toKebabCase(str: string) { - return str.replace(/((?<=[a-z\d])[A-Z]|(?<=[A-Z\d])[A-Z](?=[a-z]))/g, '-$1').toLowerCase(); -} +import { toUrlSlug } from '../slug.mjs'; const OUTPUT_LOCATION = `${process.cwd()}/docs`; @@ -82,7 +79,7 @@ describe('Typedoc output', () => { }); it('should only contain kebab-cased files', async () => { const files = await scanDirectory('file'); - const incorrectFiles = files.filter(file => file !== toKebabCase(file)); + const incorrectFiles = files.filter(file => file !== toUrlSlug(file)); expect(incorrectFiles).toHaveLength(0); }); diff --git a/.typedoc/comment-utils.mjs b/.typedoc/comment-utils.mjs index 842047b19d7..dd08a110e87 100644 --- a/.typedoc/comment-utils.mjs +++ b/.typedoc/comment-utils.mjs @@ -22,22 +22,6 @@ export function commentContainsTodo(comment) { return chunks.some(text => TODO_WORD.test(text)); } -/** - * Truncate at the first word "TODO" (case-insensitive). Used when flattening display parts to a string. - * - * @param {string} text - */ -export function stripTextAfterTodo(text) { - if (!text) { - return ''; - } - const m = TODO_WORD.exec(text); - if (!m) { - return text; - } - return text.slice(0, m.index).trimEnd(); -} - /** * Drop display parts from the first `TODO` onward; truncate the containing text part if `TODO` appears mid-string. * diff --git a/.typedoc/custom-router.mjs b/.typedoc/custom-router.mjs index 0fca4b91220..0e6cf9a5c7c 100644 --- a/.typedoc/custom-router.mjs +++ b/.typedoc/custom-router.mjs @@ -4,6 +4,7 @@ import { MemberRouter } from 'typedoc-plugin-markdown'; import { isInlineModifierWithoutStandalonePage } from './standalone-page-tag.mjs'; import { REFERENCE_OBJECT_PAGE_SYMBOLS } from './reference-objects.mjs'; +import { toUrlSlug } from './slug.mjs'; /** @type {Set} */ const REFERENCE_OBJECT_SYMBOL_NAMES = new Set(Object.values(REFERENCE_OBJECT_PAGE_SYMBOLS)); @@ -20,13 +21,6 @@ function flattenDirName(filePath) { return filePath; } -/** - * @param {string} str - */ -function toKebabCase(str) { - return str.replace(/((?<=[a-z\d])[A-Z]|(?<=[A-Z\d])[A-Z](?=[a-z]))/g, '-$1').toLowerCase(); -} - /** * @param {import('typedoc-plugin-markdown').MarkdownApplication} app */ @@ -79,7 +73,7 @@ class ClerkRouter extends MemberRouter { getIdealBaseName(reflection) { const original = super.getIdealBaseName(reflection); // Convert URLs (by default camelCase) to kebab-case - let filePath = toKebabCase(original); + let filePath = toUrlSlug(original); /** * By default, the paths are deeply nested, e.g.: @@ -100,7 +94,7 @@ class ClerkRouter extends MemberRouter { (reflection.kind === ReflectionKind.Interface || reflection.kind === ReflectionKind.Class) && REFERENCE_OBJECT_SYMBOL_NAMES.has(reflection.name) ) { - const kebab = toKebabCase(reflection.name); + const kebab = toUrlSlug(reflection.name); const m = filePath.match(/^([^/]+)\/([^/]+)$/); if (m) { const [, pkg] = m; diff --git a/.typedoc/custom-theme.mjs b/.typedoc/custom-theme.mjs index 31b29544bef..ab6274042ac 100644 --- a/.typedoc/custom-theme.mjs +++ b/.typedoc/custom-theme.mjs @@ -1,14 +1,7 @@ // @ts-check import { ArrayType, i18n, IntersectionType, ReferenceType, ReflectionKind, ReflectionType, UnionType } from 'typedoc'; import { MarkdownTheme, MarkdownThemeContext } from 'typedoc-plugin-markdown'; -import { - backTicks, - heading, - htmlTable, - table, -} from '../node_modules/typedoc-plugin-markdown/dist/libs/markdown/index.js'; -import { removeLineBreaks } from '../node_modules/typedoc-plugin-markdown/dist/libs/utils/index.js'; -import { TypeDeclarationVisibility } from '../node_modules/typedoc-plugin-markdown/dist/options/maps.js'; +import { backTicks, heading, htmlTable, removeLineBreaks, table } from './markdown-helpers.mjs'; import { applyTodoStrippingToComment } from './comment-utils.mjs'; import { isInlineModifierWithoutStandalonePage } from './standalone-page-tag.mjs'; @@ -23,8 +16,7 @@ export { REFERENCE_OBJECTS_LIST }; * @returns {import('typedoc').Type} */ /** - * Prefer structural checks over `instanceof` so we still match when multiple TypeDoc copies are loaded - * (otherwise `instanceof IntersectionType` is false at render time). + * Prefer structural checks over `instanceof` so we still match when multiple TypeDoc copies are loaded (otherwise `instanceof IntersectionType` is false at render time). * * @param {import('typedoc').Type | undefined} t * @returns {t is import('typedoc').IntersectionType} @@ -164,8 +156,7 @@ function findOAuthStrategyDeclaration(project) { } /** - * Stock `someType` uses `instanceof UnionType`; duplicate Typedoc copies in the tree break that check and unions - * fall through to `backTicks(model.toString())`, bypassing {@link unionType} entirely (including OAuth collapse). + * Stock `someType` uses `instanceof UnionType`; duplicate Typedoc copies in the tree break that check and unions fall through to `backTicks(model.toString())`, bypassing {@link unionType} entirely (including OAuth collapse). * * @param {import('typedoc').Type | undefined} model * @returns {import('typedoc').UnionType | undefined} @@ -185,12 +176,9 @@ function coerceUnionTypeIfNeeded(model) { } /** - * TypeScript normalizes `OAuthStrategy` to a large union of `oauth_*` string literals plus - * `` `oauth_custom_${string}` ``. That is not a {@link ReferenceType}, so the theme prints every literal. - * Collapse **only** when the union clearly matches that expanded Clerk shape, then render a link to `OAuthStrategy`. + * TypeScript normalizes `OAuthStrategy` to a large union of `oauth_*` string literals plus `` `oauth_custom_${string}` ``. That is not a {@link ReferenceType}, so the theme prints every literal. Collapse **only** when the union clearly matches that expanded Clerk shape, then render a link to `OAuthStrategy`. * - * Guards (all must pass): many `oauth_` literals, fingerprint literals present, optional `oauth_custom_` template arm, - * `OAuthStrategy` exists and is not `@inline`. Skips ambiguous cases so other unions are unchanged. + * Guards (all must pass): many `oauth_` literals, fingerprint literals present, optional `oauth_custom_` template arm, `OAuthStrategy` exists and is not `@inline`. Skips ambiguous cases so other unions are unchanged. * * @param {import('typedoc').Type | undefined} t * @returns {import('typedoc').Type[]} @@ -553,11 +541,14 @@ function clerkParametersTable(model) { return shouldFlatten ? [...acc, current, ...flattenParams(current)] : [...acc, current]; }; /** + * Joins flattened names with `?.` when the parent is optional (so `options?.foo` reflects the type at runtime) and `.` when required (`options.foo`). Same logic recurses for deeper inline shapes: separator between each level depends on **that** level's optionality. + * * @param {import('typedoc').ParameterReflection} current * @returns {import('typedoc').ParameterReflection[]} */ const flattenParams = current => { const decl = getParameterObjectShapeDeclaration(current.type); + const separator = current.flags?.isOptional ? '?.' : '.'; return ( decl?.children?.reduce( /** @@ -568,7 +559,7 @@ function clerkParametersTable(model) { (acc, child) => { const childObj = { ...child, - name: `${current.name}.${child.name}`, + name: `${current.name}${separator}${child.name}`, }; return parseParams( /** @type {import('typedoc').ParameterReflection} */ (/** @type {unknown} */ (childObj)), @@ -706,7 +697,8 @@ function clerkTypeDeclarationTable(model, options) { this.options.getValue('tableColumnSettings') ?? {} ); const leftAlignHeadings = tableColumnsOptions.leftAlignHeaders; - const isCompact = this.options.getValue('typeDeclarationVisibility') === TypeDeclarationVisibility.Compact; + // typedoc-plugin-markdown's `TypeDeclarationVisibility.Compact` is just the string `'compact'`. + const isCompact = this.options.getValue('typeDeclarationVisibility') === 'compact'; const hasSources = !tableColumnsOptions.hideSources && !this.options.getValue('disableSources'); const headers = []; const baseDeclarations = this.helpers.getFlattenedDeclarations(model, { @@ -1087,9 +1079,7 @@ class ClerkMarkdownThemeContext extends MarkdownThemeContext { ); }, /** - * Stock `comments.comment` prints every {@link Comment.modifierTags} as **`TitleCase`** before the summary - * (it does not consult `notRenderedTags`; that option only filters block tags). `@inline` / `@inlineType` are - * router/type hints; `@experimental` is SDK-only guidance — none of these must appear in property tables or prose. + * Stock `comments.comment` prints every {@link Comment.modifierTags} as **`TitleCase`** before the summary (it does not consult `notRenderedTags`; that option only filters block tags). `@inline` / `@inlineType` are router/type hints; `@experimental` is SDK-only guidance — none of these must appear in property tables or prose. * * @param {import('typedoc').Comment} model * @param {Parameters[1]} [options] @@ -1857,9 +1847,7 @@ function isCallablePropertyValueType(t, helpers, seenReflectionIds) { 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. + * 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 = diff --git a/.typedoc/extract-methods.mjs b/.typedoc/extract-methods.mjs index 07bf80faf29..3696204342f 100644 --- a/.typedoc/extract-methods.mjs +++ b/.typedoc/extract-methods.mjs @@ -1,10 +1,14 @@ // @ts-check /** - * For each entry in REFERENCE_OBJECTS_LIST, reads the TypeDoc output (e.g. `shared/clerk/clerk.mdx`), strips **Properties** from the main generated file and copies the section body (table only, no `## Properties` heading) into `properties.mdx`, and writes one .mdx per method under `methods/` (alongside the main page in that resource folder). + * 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: * - * Run after `typedoc` (same cwd as repo root). Uses a second TypeDoc convert pass to read reflections. + * - 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. * - * Like `extract-returns-and-params.mjs`, parameter tables are not hand-built: they use the same `MarkdownThemeContext.partials` as TypeDoc markdown output (`parametersTable` / `propertiesTable`, which call `someType` and therefore pick up `custom-theme.mjs` union/`<code>` behavior). Router + theme are prepared via `prepare-markdown-renderer.mjs` (same idea as `typedoc-plugin-markdown` `render()`). + * 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. + * + * 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`. */ @@ -12,20 +16,17 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { - Application, Comment, IntersectionType, OptionalType, - PageKind, ReferenceType, ReflectionKind, ReflectionType, UnionType, } from 'typedoc'; import { MarkdownPageEvent, MarkdownTheme } from 'typedoc-plugin-markdown'; -import { removeLineBreaks } from '../node_modules/typedoc-plugin-markdown/dist/libs/utils/index.js'; +import { removeLineBreaks } from './markdown-helpers.mjs'; -import typedocConfig from '../typedoc.config.mjs'; import { isCallableInterfaceProperty } from './custom-theme.mjs'; import { applyCatchAllMdReplacements, @@ -33,9 +34,9 @@ import { stripReferenceObjectPropertiesSection, } from './custom-plugin.mjs'; import { isInlineModifierWithoutStandalonePage } from './standalone-page-tag.mjs'; -import { prepareMarkdownRenderer } from './prepare-markdown-renderer.mjs'; import { applyTodoStrippingToComment } from './comment-utils.mjs'; -import { REFERENCE_OBJECTS_LIST, REFERENCE_OBJECT_CONFIG } from './reference-objects.mjs'; +import { REFERENCE_OBJECT_CONFIG } from './reference-objects.mjs'; +import { toFileSlug } from './slug.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -70,144 +71,6 @@ function removeLineBreaksForTableCell(str) { return str?.replace(/\r?\n/g, ' ').replace(/ {2,}/g, ' '); } -/** - * Append data rows to a markdown table string (header + separator + rows). - * - * @param {string} tableMd - * @param {string[]} rowLines Lines like `| a | b | c |` - */ -function appendMarkdownTableRows(tableMd, rowLines) { - if (!rowLines.length) { - return tableMd; - } - return `${tableMd.trimEnd()}\n${rowLines.join('\n')}\n`; -} - -/** - * Post-process the theme’s parameters markdown table. TypeDoc flattens object params as `parent.child` and may interleave those rows with other parameters. Here we (1) move each `parent.*` block directly under `parent`, and (2) rewrite dotted paths in the name column to optional-chaining (`parent?.child`, `a?.b?.c`). Top-level names are unchanged (`foo?`, `exa`). - * - * @param {string} tableMd - */ -function formatMethodParametersTable(tableMd) { - const leadingNewlines = (tableMd.match(/^\n+/) ?? [''])[0]; - const nonEmpty = tableMd.split('\n').filter(l => l.trim().length); - if (nonEmpty.length < 3) { - return tableMd; - } - const header = nonEmpty[0]; - const sep = nonEmpty[1]; - const dataLines = nonEmpty.slice(2).filter(l => l.trim().startsWith('|')); - if (dataLines.length <= 1) { - return tableMd; - } - - /** @param {string} line */ - const firstName = line => { - const m = line.match(/^\|\s*(?:<\/a>\s*)?`([^`]+)`/); - return m ? m[1] : ''; - }; - /** `parent.child` / `parent?.child` → grouping key `parent` (matches top-level `parent` or `parent?` via fallback below). */ - /** @param {string} raw */ - const parentOfNested = raw => { - const j = raw.indexOf('?.'); - if (j !== -1) { - return raw.slice(0, j); - } - const i = raw.indexOf('.'); - return i === -1 ? '' : raw.slice(0, i); - }; - /** `a.b.c` → `a?.b?.c`; leave `foo?` and names without `.` alone. */ - /** @param {string} raw */ - const nameForDisplay = raw => (!raw.includes('.') || raw.includes('?.') ? raw : raw.split('.').join('?.')); - /** @param {string} line @param {string} name */ - const replaceFirstName = (line, name) => - line.replace(/^(\|\s*(?:<\/a>\s*)?)`[^`]+`/, `$1\`${name}\``); - - const topLevelOrder = []; - const seenTop = new Set(); - /** @type {Map} */ - const childrenOf = new Map(); - - for (const line of dataLines) { - const raw = firstName(line); - if (!raw) { - continue; - } - if (!raw.includes('.')) { - if (!seenTop.has(raw)) { - seenTop.add(raw); - topLevelOrder.push(raw); - } - continue; - } - const p = parentOfNested(raw); - if (!p) { - continue; - } - let bucket = childrenOf.get(p); - if (!bucket) { - bucket = []; - childrenOf.set(p, bucket); - } - bucket.push(line); - } - - for (const lines of childrenOf.values()) { - lines.sort((a, b) => firstName(a).localeCompare(firstName(b))); - } - - /** @param {string} top */ - const rowsForParent = top => - childrenOf.get(top) ?? (top.endsWith('?') ? childrenOf.get(top.slice(0, -1)) : undefined); - - const body = []; - const emitted = new Set(); - - for (const top of topLevelOrder) { - const topLine = dataLines.find(l => firstName(l) === top); - if (topLine) { - const r = firstName(topLine); - body.push(replaceFirstName(topLine, nameForDisplay(r))); - emitted.add(topLine); - } - const kids = rowsForParent(top); - if (kids) { - for (const line of kids) { - body.push(replaceFirstName(line, nameForDisplay(firstName(line)))); - emitted.add(line); - } - } - } - - for (const line of dataLines) { - if (!emitted.has(line)) { - const r = firstName(line); - body.push(r ? replaceFirstName(line, nameForDisplay(r)) : line); - } - } - - return `${leadingNewlines}${[header, sep, ...body].join('\n')}\n`; -} - -/** - * @param {import('typedoc').Application} app - * @param {import('typedoc').ProjectReflection} project - * @param {string} pageUrl e.g. `shared/clerk/index.mdx` - * @param {import('typedoc').DeclarationReflection} interfaceDecl - */ -function createThemeContextForReferencePage(app, project, pageUrl, interfaceDecl) { - const page = new MarkdownPageEvent(interfaceDecl); - page.url = pageUrl; - page.filename = path.join(app.options.getValue('out') ?? '', pageUrl); - page.pageKind = PageKind.Reflection; - page.project = project; - const theme = /** @type {InstanceType | undefined} */ (app.renderer.theme); - if (!theme || typeof theme.getRenderContext !== 'function') { - throw new Error('[extract-methods] Renderer theme is not ready; call prepareMarkdownRenderer(app) after convert'); - } - return /** @type {import('typedoc-plugin-markdown').MarkdownThemeContext} */ (theme.getRenderContext(page)); -} - /** * TypeDoc `code` display parts often already include backticks (same as {@link Comment.combineDisplayParts}). * Wrapping again would produce `` `Client` `` in MDX. @@ -403,16 +266,29 @@ function getPrimaryCallSignature(decl) { } /** - * @param {import('typedoc').Type | undefined} t + * Strip one (or, with `{ deep: true }`, all) `OptionalType` layers and return the inner type. Returns `t` unchanged when it isn't an `OptionalType`, or when `t` is nullish. + * + * Typed loosely (`Type` ⊕ `SomeType`) so callers in either type domain can use the same helper; the runtime check is structural (`type === 'optional' && 'elementType' in t`). + * + * @template {import('typedoc').Type | import('typedoc').SomeType | undefined} T + * @param {T} t + * @param {{ deep?: boolean }} [options] + * @returns {T} */ -function unwrapOptionalType(t) { - if (!t || typeof t !== 'object') { - return t; - } - if (/** @type {{ type?: string }} */ (t).type === 'optional' && 'elementType' in t) { - return /** @type {{ elementType: import('typedoc').Type }} */ (t).elementType; +function unwrapOptional(t, options) { + let cur = t; + while ( + cur && + typeof cur === 'object' && + /** @type {{ type?: string }} */ (cur).type === 'optional' && + 'elementType' in cur + ) { + cur = /** @type {T} */ (/** @type {{ elementType: import('typedoc').Type }} */ (cur).elementType); + if (!options?.deep) { + break; + } } - return t; + return cur; } /** @@ -422,7 +298,7 @@ function unwrapOptionalType(t) { * @returns {Map | undefined} */ function getGenericInstantiationMapFromCallableProperty(propertyDecl) { - const t = unwrapOptionalType(propertyDecl.type); + const t = unwrapOptional(propertyDecl.type); if (!(t instanceof ReferenceType) || !t.reflection) { return undefined; } @@ -430,7 +306,7 @@ function getGenericInstantiationMapFromCallableProperty(propertyDecl) { if (!alias.kindOf(ReflectionKind.TypeAlias) || !alias.type) { return undefined; } - const inner = unwrapOptionalType(alias.type); + const inner = unwrapOptional(alias.type); if (!(inner instanceof ReferenceType) || !inner.typeArguments?.length || !inner.reflection) { return undefined; } @@ -523,23 +399,6 @@ function shouldExtractCallableMember(decl, ctx) { return false; } -/** - * @param {import('typedoc').SomeType | undefined} t - * @returns {import('typedoc').SomeType | undefined} - */ -function unwrapOptionalLayersSomeType(t) { - let cur = /** @type {import('typedoc').SomeType | undefined} */ (t); - while ( - cur && - typeof cur === 'object' && - /** @type {{ type?: string }} */ (cur).type === 'optional' && - 'elementType' in cur - ) { - cur = /** @type {import('typedoc').SomeType} */ (/** @type {import('typedoc').OptionalType} */ (cur).elementType); - } - return cur; -} - /** * Object-literal (or single object arm of `T | null`) property rows for a properties table. * @@ -547,7 +406,7 @@ function unwrapOptionalLayersSomeType(t) { * @returns {import('typedoc').DeclarationReflection[] | undefined} */ function resolveObjectShapeMembersForPropertyTable(valueType) { - let t = unwrapOptionalLayersSomeType(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) { @@ -647,46 +506,20 @@ function extractPropertiesSectionBody(markdown) { } /** - * @param {string} pageUrl e.g. `shared/clerk/clerk.mdx` + * 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 extractPropertiesAndTrimSourcePage(pageUrl) { - const sourcePath = path.join(__dirname, 'temp-docs', pageUrl); - if (!fs.existsSync(sourcePath)) { - console.warn(`[extract-methods] Expected TypeDoc output missing: ${sourcePath}`); - return; - } - const raw = fs.readFileSync(sourcePath, 'utf-8'); - const body = extractPropertiesSectionBody(raw); - const pageDir = path.dirname(pageUrl); - const objectDir = path.join(__dirname, 'temp-docs', pageDir); - fs.mkdirSync(objectDir, { recursive: true }); - - if (body) { - const propertiesDoc = `${body.trimEnd()}\n`; - const propertiesPath = path.join(objectDir, 'properties.mdx'); - fs.writeFileSync( - propertiesPath, - applyCatchAllMdReplacements(applyRelativeLinkReplacements(propertiesDoc)), - 'utf-8', - ); - console.log(`[extract-methods] Wrote ${path.relative(path.join(__dirname, '..'), propertiesPath)}`); - } - - const stripped = stripReferenceObjectPropertiesSection(raw); - if (stripped !== raw) { - fs.writeFileSync(sourcePath, stripped, 'utf-8'); - console.log(`[extract-methods] Stripped Properties from ${path.relative(path.join(__dirname, '..'), sourcePath)}`); +function splitPropertiesFromContents(contents) { + if (!contents) { + return { propertiesBody: undefined, stripped: contents }; } -} - -/** - * @param {string} name - */ -function toKebabCase(name) { - return name - .replace(/([a-z\d])([A-Z])/g, '$1-$2') - .replace(/[\s_]+/g, '-') - .toLowerCase(); + const propertiesBody = extractPropertiesSectionBody(contents); + const stripped = stripReferenceObjectPropertiesSection(contents); + return { propertiesBody, stripped }; } /** @@ -848,10 +681,7 @@ function appendSignatureOnlyReturns(declComment, sigComment) { * @param {import('typedoc').DeclarationReflection} prop */ function propertyReflectionTypeIsNever(prop) { - let ty = prop.type; - while (ty?.type === 'optional') { - ty = /** @type {import('typedoc').OptionalType} */ (ty).elementType; - } + const ty = unwrapOptional(prop.type, { deep: true }); return ty?.type === 'intrinsic' && ty.name === 'never'; } @@ -1026,25 +856,14 @@ function resolveDeclarationWithObjectMembers(t, project) { } /** - * @param {string} baseName - * @param {string[]} pathSegments - */ -function formatNestedParamNameColumn(baseName, pathSegments) { - const pathChain = pathSegments.join('?.'); - return `\`${baseName}?.${pathChain}\``; -} - -/** - * This function unwraps a TypeDoc parameter type if it is an optional type. If the provided type is of type "optional", it returns the underlying element type (the real type being wrapped). If it is not optional or is undefined, it returns the type as-is. + * 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`. * - * @param {import('typedoc').SomeType | undefined} t - * @returns {import('typedoc').SomeType | undefined} + * @param {import('typedoc').ParameterReflection} parentParam + * @param {string} childName */ -function unwrapOptionalParamType(t) { - if (t?.type === 'optional') { - return /** @type {import('typedoc').OptionalType} */ (t).elementType; - } - return t; +function formatNestedParamNameColumn(parentParam, childName) { + const sep = parentParam.flags?.isOptional ? '?.' : '.'; + return `\`${parentParam.name}${sep}${childName}\``; } /** @@ -1055,7 +874,7 @@ function unwrapOptionalParamType(t) { * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx */ function parameterTypeLinksToStandaloneMdxPage(t, ctx) { - const bare = unwrapOptionalParamType(t); + const bare = unwrapOptional(t); if (!bare) { return false; } @@ -1102,7 +921,7 @@ function nestedParameterRowsFromDocumentedProperties(param, ctx) { for (const child of props) { const summary = child.comment?.summary; const typeCell = child.type ? removeLineBreaksForTableCell(ctx.partials.someType(child.type)) : '`unknown`'; - const nestedNameCol = formatNestedParamNameColumn(param.name, [child.name]); + const nestedNameCol = formatNestedParamNameColumn(param, child.name); const nestedDesc = summary?.length ? displayPartsToString(summary).trim() || '—' : '—'; rows.push(`| ${nestedNameCol} | ${typeCell} | ${nestedDesc} |`); } @@ -1272,11 +1091,9 @@ function parametersMarkdownTable(sig, ctx, instantiationMap) { nested.push(...nestedParameterRowsFromDocumentedProperties(p, ctx)); } if (nested.length) { - tableMd = appendMarkdownTableRows(tableMd, nested); + tableMd = `${tableMd.trimEnd()}\n${nested.join('\n')}\n`; } - tableMd = formatMethodParametersTable(tableMd); - return [markdownHeading(4, ReflectionKind.pluralString(ReflectionKind.Parameter)), '', tableMd, ''].join('\n'); } @@ -1325,17 +1142,18 @@ function hasExtractMethodsModifier(decl) { } /** - * Writes `methods/-.mdx` for each direct member of an `@extractMethods` object-like type: - * callables via {@link buildMethodMdx}, non-callables with a resolvable object shape via - * {@link buildPropertyTableDocMdx}. + * @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. * - * Supports inline object literals and named references (`interface` / object-like `type` aliases) by resolving - * the holder with {@link resolveDeclarationWithObjectMembers}. + * Supports inline object literals and named references (`interface` / object-like `type` aliases) via {@link resolveDeclarationWithObjectMembers}. * * @param {import('typedoc').DeclarationReflection} parentDecl * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx * @param {string} outDir - * @returns {number} Number of files written + * @returns {ExtractedFile[]} */ function processExtractMethodsNamespace(parentDecl, ctx, outDir) { const project = ctx.page?.project; @@ -1345,10 +1163,11 @@ function processExtractMethodsNamespace(parentDecl, ctx, outDir) { console.warn( `[extract-methods] @extractMethods on "${parentDecl.name}" requires an object-like type with members; skipping nested extraction`, ); - return 0; + return []; } const parentName = parentDecl.name; - let count = 0; + /** @type {ExtractedFile[]} */ + const collected = []; /** @type {import('typedoc').DeclarationReflection[]} */ const nonCallableMembers = []; for (const nested of members) { @@ -1356,16 +1175,14 @@ function processExtractMethodsNamespace(parentDecl, ctx, outDir) { continue; } const nd = /** @type {import('typedoc').DeclarationReflection} */ (nested); - const fileSlug = `${toKebabCase(parentName)}-${toKebabCase(nd.name)}`; + 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; } - fs.writeFileSync(filePath, mdx, 'utf-8'); - console.log(`[extract-methods] Wrote ${path.relative(path.join(__dirname, '..'), filePath)}`); - count++; + collected.push({ filePath, content: mdx }); continue; } nonCallableMembers.push(nd); @@ -1373,32 +1190,32 @@ function processExtractMethodsNamespace(parentDecl, ctx, outDir) { if (!propTableMdx) { continue; } - fs.writeFileSync(filePath, propTableMdx, 'utf-8'); - console.log(`[extract-methods] Wrote ${path.relative(path.join(__dirname, '..'), filePath)}`); - count++; + collected.push({ filePath, content: propTableMdx }); } if (nonCallableMembers.length) { const namespaceMdx = buildExtractMethodsNamespacePropertyTableMdx(parentDecl, nonCallableMembers, ctx); if (namespaceMdx) { - const namespacePath = path.join(outDir, `${toKebabCase(parentName)}.mdx`); - fs.writeFileSync(namespacePath, namespaceMdx, 'utf-8'); - console.log(`[extract-methods] Wrote ${path.relative(path.join(__dirname, '..'), namespacePath)}`); - count++; + const namespacePath = path.join(outDir, `${toFileSlug(parentName)}.mdx`); + collected.push({ filePath: namespacePath, content: namespaceMdx }); } } - return count; + 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 + * @returns {ExtractedFile[]} */ function extractCallableMembersFromDeclaration(decl, ctx, outDir) { - let count = 0; if (!decl.children) { - return 0; + return []; } + /** @type {ExtractedFile[]} */ + const collected = []; for (const child of decl.children) { if (child.name.startsWith('__')) { continue; @@ -1406,92 +1223,111 @@ function extractCallableMembersFromDeclaration(decl, ctx, outDir) { const childDecl = /** @type {import('typedoc').DeclarationReflection} */ (child); if (hasExtractMethodsModifier(childDecl)) { - count += processExtractMethodsNamespace(childDecl, ctx, outDir); + collected.push(...processExtractMethodsNamespace(childDecl, ctx, outDir)); continue; } if (shouldExtractCallableMember(childDecl, ctx)) { const mdx = buildMethodMdx(childDecl, ctx); if (mdx) { - const fileName = `${toKebabCase(child.name)}.mdx`; + const fileName = `${toFileSlug(child.name)}.mdx`; const filePath = path.join(outDir, fileName); - fs.writeFileSync(filePath, mdx, 'utf-8'); - console.log(`[extract-methods] Wrote ${path.relative(path.join(__dirname, '..'), filePath)}`); - count++; + collected.push({ filePath, content: mdx }); } } } - return count; + return collected; } /** - * @param {string} pageUrl - * @param {import('typedoc').ProjectReflection} project - * @param {import('typedoc').Application} app + * @param {import('typedoc-plugin-markdown').MarkdownPageEvent} output + * @returns {keyof typeof REFERENCE_OBJECT_CONFIG | undefined} */ -function extractMethodsForPage(pageUrl, project, app) { - const entry = REFERENCE_OBJECT_CONFIG[/** @type {keyof typeof REFERENCE_OBJECT_CONFIG} */ (pageUrl)]; - if (!entry) { - console.warn(`[extract-methods] No symbol mapping for ${pageUrl}, skipping`); - return 0; - } - - const { symbol, declarationHint } = entry; - const extraMethodInterfaces = 'extraMethodInterfaces' in entry ? entry.extraMethodInterfaces : undefined; - const decl = findInterfaceOrClass(project, symbol, declarationHint); - if (!decl?.children) { - console.warn(`[extract-methods] Could not find interface/class "${symbol}"`); - return 0; +function matchReferenceObjectPageUrl(output) { + if (!output.url) { + return undefined; } + const normalized = output.url.replace(/\\/g, '/'); + return normalized in REFERENCE_OBJECT_CONFIG + ? /** @type {keyof typeof REFERENCE_OBJECT_CONFIG} */ (normalized) + : undefined; +} - extractPropertiesAndTrimSourcePage(pageUrl); - - const ctx = createThemeContextForReferencePage(app, project, pageUrl, decl); - - const pageDir = path.dirname(pageUrl); - const objectDir = path.join(__dirname, 'temp-docs', pageDir); - const outDir = path.join(objectDir, 'methods'); - fs.mkdirSync(outDir, { recursive: true }); - - let count = extractCallableMembersFromDeclaration(decl, ctx, outDir); - - 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; +/** + * 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. + * + * @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 = REFERENCE_OBJECT_CONFIG[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); + 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; + } + methodFiles.push(...extractCallableMembersFromDeclaration(extraDecl, ctx, outDir)); } - count += extractCallableMembersFromDeclaration(extraDecl, ctx, outDir); } - } - return count; -} + output.preWriteAsyncJobs.push(async () => { + 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; + } -async function main() { - const app = await Application.bootstrapWithPlugins({ - ...typedocConfig, - // Avoid writing markdown twice; we only need reflections. - out: path.join(__dirname, 'temp-docs-unused'), + if (methodFiles.length === 0) { + 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)`); + }); }); - - const project = await app.convert(); - if (!project) { - console.error('[extract-methods] TypeDoc conversion failed'); - process.exit(1); - } - - prepareMarkdownRenderer(app, project); - - let total = 0; - for (const pageUrl of REFERENCE_OBJECTS_LIST) { - total += extractMethodsForPage(pageUrl, project, app); - } - console.log(`[extract-methods] Wrote ${total} method files total`); } - -main().catch(err => { - console.error(err); - process.exit(1); -}); diff --git a/.typedoc/markdown-helpers.mjs b/.typedoc/markdown-helpers.mjs new file mode 100644 index 00000000000..91ef4df2b5b --- /dev/null +++ b/.typedoc/markdown-helpers.mjs @@ -0,0 +1,117 @@ +// @ts-check +/** + * Small markdown utilities. These are inlined from `typedoc-plugin-markdown`'s + * internal `dist/libs/markdown/` and `dist/libs/utils/` modules — the plugin's + * public API doesn't re-export them, and reaching into `dist/` directly breaks + * when the dependency updates. + * + * Keep these byte-equivalent to the upstream behavior so generated markdown + * stays consistent with what typedoc-plugin-markdown produces. + * + * @see https://github.com/typedoc2md/typedoc-plugin-markdown/blob/main/packages/typedoc-plugin-markdown/src/libs/markdown/ + * @see https://github.com/typedoc2md/typedoc-plugin-markdown/blob/main/packages/typedoc-plugin-markdown/src/libs/utils/ + */ + +/** + * Escape characters with special meaning in MDX so they render literally. + * + * @param {string} str + */ +export function escapeChars(str) { + return str + .replace(/>/g, '\\>') + .replace(/ 6 ? 6 : level; + return `${'#'.repeat(l)} ${text}`; +} + +/** + * Collapse newlines and excess whitespace so a string is safe to use as a + * single markdown table cell. + * + * @param {string} str + */ +export function removeLineBreaks(str) { + return str?.replace(/\r?\n/g, ' ').replace(/ {2,}/g, ' '); +} + +/** + * Sanitize a markdown table cell: flatten newlines, unwrap any fenced code + * block into inline backticks, and collapse runs of spaces. + * + * @param {string} str + */ +function formatTableCell(str) { + return str + .replace(/\r?\n/g, ' ') + .replace(/```(\w+\s)?([\s\S]*?)```/gs, (_match, _lang, body) => `\`${body.trim()}\``) + .replace(/ +/g, ' ') + .trim(); +} + +/** + * Render a markdown pipe-table. + * + * @param {string[]} headers + * @param {string[][]} rows + * @param {boolean} [headerLeftAlign] + */ +export function table(headers, rows, headerLeftAlign = false) { + const sep = headers.map(() => `${headerLeftAlign ? ':' : ''}------`).join(' | '); + const body = rows.map(row => `| ${row.map(cell => formatTableCell(cell)).join(' | ')} |\n`).join(''); + return `\n| ${headers.join(' | ')} |\n| ${sep} |\n${body}`; +} + +/** + * Render an HTML `` (used when MDX needs richer cell content than the + * pipe-table syntax can express). + * + * @param {string[]} headers + * @param {string[][]} rows + * @param {boolean} [leftAlignHeadings] + */ +export function htmlTable(headers, rows, leftAlignHeadings = false) { + const align = leftAlignHeadings ? ' align="left"' : ''; + const head = headers.map(h => `\n${h}`).join(''); + const body = rows + .map(row => { + const cells = row.map(cell => `\n`).join(''); + return `\n${cells}\n`; + }) + .join(''); + return `
\n\n${cell === '-' ? '‐' : cell}\n\n
\n\n${head}\n\n\n${body}\n\n
`; +} diff --git a/.typedoc/prepare-markdown-renderer.mjs b/.typedoc/prepare-markdown-renderer.mjs deleted file mode 100644 index bbf373c63f7..00000000000 --- a/.typedoc/prepare-markdown-renderer.mjs +++ /dev/null @@ -1,118 +0,0 @@ -// @ts-check -/** - * Mirrors `prepareRouter` + `prepareTheme` from `typedoc-plugin-markdown` `render()` so code outside the - * markdown render pass can build a `MarkdownThemeContext` (same `partials` as generated pages). - * - * Only `member`, `module`, and plugin-registered routers (e.g. `clerk-router`) are supported — matching this repo's - * TypeDoc config. - * - * @see https://github.com/typedoc2md/typedoc-plugin-markdown/blob/main/packages/typedoc-plugin-markdown/src/renderer/render.ts - */ -import { MarkdownTheme, MemberRouter, ModuleRouter } from 'typedoc-plugin-markdown'; - -/** - * @param {import('typedoc').Renderer} renderer - * @returns {string} - */ -function getRouterName(renderer) { - const routerOption = renderer.application.options.getValue('router'); - if (!renderer.application.options.isSet('router')) { - if (renderer.application.options.isSet('outputFileStrategy')) { - const outputFileStrategy = renderer.application.options.getValue('outputFileStrategy'); - return outputFileStrategy === 'modules' ? 'module' : 'member'; - } - return 'member'; - } - return routerOption; -} - -/** - * TypeDoc types `Renderer['routers']` as private; at runtime plugins register routers on this map (e.g. `clerk-router`). - * - * @param {import('typedoc').Renderer} renderer - * @param {string} routerName - * @returns {typeof MemberRouter | typeof ModuleRouter | (new (application: import('typedoc').Application) => import('typedoc').Router) | undefined} - */ -function getRouterConstructor(renderer, routerName) { - if (routerName === 'member') { - return MemberRouter; - } - if (routerName === 'module') { - return ModuleRouter; - } - const routers = - /** @type {{ routers: Map import('typedoc').Router> }} */ ( - /** @type {unknown} */ (renderer) - ).routers; - return routers.get(routerName); -} - -/** - * Same situation as {@link getRouterConstructor}: `themes` is public at runtime but typed private. - * - * @param {import('typedoc').Renderer} renderer - * @returns {Map import('typedoc').Theme>} - */ -function getThemeRegistry(renderer) { - return /** @type {{ themes: Map import('typedoc').Theme> }} */ ( - /** @type {unknown} */ (renderer) - ).themes; -} - -/** - * @param {import('typedoc').Renderer} renderer - */ -function prepareRouter(renderer) { - const routerName = getRouterName(renderer); - const RouterCtor = getRouterConstructor(renderer, routerName); - if (!RouterCtor) { - throw new Error( - `[prepare-markdown-renderer] Router "${routerName}" is not registered (expected member, module, or a custom router from a plugin)`, - ); - } - renderer.router = new RouterCtor(renderer.application); -} - -/** - * @param {import('typedoc').Renderer} renderer - */ -function getThemeName(renderer) { - const themeOption = renderer.application.options.getValue('theme'); - return themeOption === 'default' ? 'markdown' : themeOption; -} - -/** - * @param {import('typedoc').Renderer} renderer - */ -function prepareTheme(renderer) { - const themes = getThemeRegistry(renderer); - const themeName = getThemeName(renderer); - const ThemeCtor = themes.get(themeName); - if (!ThemeCtor) { - throw new Error(`[prepare-markdown-renderer] Theme "${themeName}" is not registered`); - } - const theme = new ThemeCtor(renderer); - if (!(theme instanceof MarkdownTheme)) { - renderer.application.logger.warn( - `[prepare-markdown-renderer] Theme "${themeName}" is not MarkdownTheme; falling back to built-in markdown theme`, - ); - renderer.theme = new /** @type {typeof MarkdownTheme} */ (themes.get('markdown'))(renderer); - return; - } - renderer.theme = theme; -} - -/** - * @param {import('typedoc').Application} app - * @param {import('typedoc').ProjectReflection} project - */ -export function prepareMarkdownRenderer(app, project) { - prepareRouter(app.renderer); - prepareTheme(app.renderer); - // Required so `referenceType` / links can resolve (`getFullUrl`); same as `render()` before each page. - const router = app.renderer.router; - if (!router) { - throw new Error('[prepare-markdown-renderer] Router was not set after prepareRouter'); - } - router.buildPages(project); -} diff --git a/.typedoc/reference-objects.mjs b/.typedoc/reference-objects.mjs index ecb196dac00..424ca40172f 100644 --- a/.typedoc/reference-objects.mjs +++ b/.typedoc/reference-objects.mjs @@ -53,7 +53,6 @@ export const REFERENCE_OBJECT_CONFIG = { 'shared/billing-namespace/billing-namespace.mdx': { symbol: 'BillingNamespace', declarationHint: 'types/billing', - extraMethodInterfaces: [{ symbol: 'BillingNamespace', declarationHint: 'types/billing' }], }, }; diff --git a/.typedoc/slug.mjs b/.typedoc/slug.mjs new file mode 100644 index 00000000000..dcc274db8f5 --- /dev/null +++ b/.typedoc/slug.mjs @@ -0,0 +1,35 @@ +// @ts-check +/** + * Two kebab-case flavors. They produce different output for acronym-heavy names (`mountOAuthConsent`, `authenticateWithOKXWallet`, …) and the published docs depend on both styles existing — do not consolidate them without changing the output. + * + * | input | toFileSlug | toUrlSlug | + * | --------------------------- | ----------------------- | ------------------------- | + * | `mountOAuthConsent` | `mount-oauth-consent` | `mount-o-auth-consent` | + * | `authenticateWithOKXWallet` | `authenticate-with-okxwallet` | `authenticate-with-okx-wallet` | + * | `OAuthCallback` | `oauth-callback` | `o-auth-callback` | + * + * `toFileSlug` is what `extract-methods.mjs` uses for `methods/.mdx` filenames — the existing clerk.com docs link to `oauth-…` slugs (see `mount-oauth-consent.mdx`). + * + * `toUrlSlug` is what `custom-router.mjs` uses for page URLs and what cross-page link replacements (`o-auth-strategy`, `o-auth-consent-info` in `custom-plugin.mjs`) match — the published docs link to those `o-auth-…` slugs. + */ + +/** + * Inserts a dash before every uppercase that immediately follows a lowercase or digit, then lowercases. Treats runs of uppercase letters (acronyms) as a single token: `OKXWallet` → `okxwallet`. Used for `methods/.mdx` filenames. + * + * @param {string} name + */ +export function toFileSlug(name) { + return name + .replace(/([a-z\d])([A-Z])/g, '$1-$2') + .replace(/[\s_]+/g, '-') + .toLowerCase(); +} + +/** + * Splits acronyms by also inserting a dash between adjacent uppercase letters when the second one is followed by a lowercase: `OKXWallet` → `okx-wallet`. Used for page URLs. + * + * @param {string} str + */ +export function toUrlSlug(str) { + return str.replace(/((?<=[a-z\d])[A-Z]|(?<=[A-Z\d])[A-Z](?=[a-z]))/g, '-$1').toLowerCase(); +} diff --git a/package.json b/package.json index 6a73d099f74..ce54cf75eb3 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "test:typedoc": "pnpm typedoc:generate && cd ./.typedoc && vitest run", "turbo:clean": "turbo daemon clean", "typedoc:generate": "pnpm build && pnpm typedoc:generate:skip-build", - "typedoc:generate:skip-build": "typedoc --tsconfig tsconfig.typedoc.json && node .typedoc/extract-returns-and-params.mjs && node .typedoc/extract-methods.mjs && rimraf .typedoc/docs && cpy '.typedoc/temp-docs/**' '.typedoc/docs' && rimraf .typedoc/temp-docs", + "typedoc:generate:skip-build": "typedoc --tsconfig tsconfig.typedoc.json && node .typedoc/extract-returns-and-params.mjs && rimraf .typedoc/docs && cpy '.typedoc/temp-docs/**' '.typedoc/docs' && rimraf .typedoc/temp-docs", "version-packages": "changeset version && pnpm install --lockfile-only --engine-strict=false", "version-packages:canary": "./scripts/canary.mjs", "version-packages:canary-core3": "./scripts/canary-core3.mjs", diff --git a/typedoc.config.mjs b/typedoc.config.mjs index c206e1c92e4..e3449e9d9d8 100644 --- a/typedoc.config.mjs +++ b/typedoc.config.mjs @@ -81,6 +81,8 @@ const config = { './.typedoc/custom-router.mjs', './.typedoc/custom-theme.mjs', './.typedoc/custom-plugin.mjs', + /** Must load after custom-plugin.mjs so its END listener (link replacements) fires first. */ + './.typedoc/extract-methods.mjs', ], theme: 'clerkTheme', router: 'clerk-router',