From 0d17df8b7a061914a179f56de51f26ab6d97fefd Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Fri, 22 May 2026 16:03:50 -0700 Subject: [PATCH 01/11] refactor(typedoc): run extract-methods inside the markdown render pass Folds extract-methods.mjs into a MarkdownPageEvent.END listener that queues a preWriteAsyncJob, removing the second TypeDoc convert pass and its prepare-markdown-renderer.mjs workaround. Output is byte-identical to the prior two-pass flow (verified by full `diff -r`, 695/695 files); adds 10 file-snapshot tests to lock that contract for future refactors. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../api-key-resource-methods-create.mdx | 21 ++ ...clerk-methods-handle-redirect-callback.mdx | 17 + .../clerk-methods-join-waitlist.mdx | 13 + .../__snapshots__/clerk-methods-sign-out.mdx | 14 + .../__snapshots__/clerk-properties.mdx | 21 ++ .typedoc/__tests__/__snapshots__/clerk.mdx | 1 + ...n-resource-methods-check-authorization.mdx | 7 + ...-resource-methods-email-code-send-code.mdx | 14 + ...-in-future-resource-methods-email-link.mdx | 5 + .../user-resource-properties.mdx | 36 +++ .typedoc/__tests__/extract-methods.test.ts | 85 +++++ .typedoc/extract-methods.mjs | 294 +++++++++--------- .typedoc/prepare-markdown-renderer.mjs | 118 ------- package.json | 2 +- typedoc.config.mjs | 2 + 15 files changed, 389 insertions(+), 261 deletions(-) create mode 100644 .typedoc/__tests__/__snapshots__/api-key-resource-methods-create.mdx create mode 100644 .typedoc/__tests__/__snapshots__/clerk-methods-handle-redirect-callback.mdx create mode 100644 .typedoc/__tests__/__snapshots__/clerk-methods-join-waitlist.mdx create mode 100644 .typedoc/__tests__/__snapshots__/clerk-methods-sign-out.mdx create mode 100644 .typedoc/__tests__/__snapshots__/clerk-properties.mdx create mode 100644 .typedoc/__tests__/__snapshots__/clerk.mdx create mode 100644 .typedoc/__tests__/__snapshots__/session-resource-methods-check-authorization.mdx create mode 100644 .typedoc/__tests__/__snapshots__/sign-in-future-resource-methods-email-code-send-code.mdx create mode 100644 .typedoc/__tests__/__snapshots__/sign-in-future-resource-methods-email-link.mdx create mode 100644 .typedoc/__tests__/__snapshots__/user-resource-properties.mdx create mode 100644 .typedoc/__tests__/extract-methods.test.ts delete mode 100644 .typedoc/prepare-markdown-renderer.mjs 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..5fa277f1664 --- /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-redirect-callback.mdx b/.typedoc/__tests__/__snapshots__/clerk-methods-handle-redirect-callback.mdx new file mode 100644 index 00000000000..54710015415 --- /dev/null +++ b/.typedoc/__tests__/__snapshots__/clerk-methods-handle-redirect-callback.mdx @@ -0,0 +1,17 @@ +### `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..3d21fda5618 --- /dev/null +++ b/.typedoc/__tests__/__snapshots__/clerk-methods-join-waitlist.mdx @@ -0,0 +1,13 @@ +### `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..74f878a5ae7 --- /dev/null +++ b/.typedoc/__tests__/__snapshots__/clerk-methods-sign-out.mdx @@ -0,0 +1,14 @@ +### `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..08039761271 --- /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..8c6dcf3bfc8 --- /dev/null +++ b/.typedoc/__tests__/__snapshots__/sign-in-future-resource-methods-email-code-send-code.mdx @@ -0,0 +1,14 @@ +### `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..313c306c427 --- /dev/null +++ b/.typedoc/__tests__/__snapshots__/sign-in-future-resource-methods-email-link.mdx @@ -0,0 +1,5 @@ +### `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..53bee1507a6 --- /dev/null +++ b/.typedoc/__tests__/extract-methods.test.ts @@ -0,0 +1,85 @@ +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/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('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/extract-methods.mjs b/.typedoc/extract-methods.mjs index 07bf80faf29..a40b3a58c0b 100644 --- a/.typedoc/extract-methods.mjs +++ b/.typedoc/extract-methods.mjs @@ -1,22 +1,36 @@ // @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. * - * 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`. + * 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`. */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { - Application, Comment, IntersectionType, OptionalType, - PageKind, ReferenceType, ReflectionKind, ReflectionType, @@ -25,7 +39,6 @@ import { import { MarkdownPageEvent, MarkdownTheme } from 'typedoc-plugin-markdown'; import { removeLineBreaks } from '../node_modules/typedoc-plugin-markdown/dist/libs/utils/index.js'; -import typedocConfig from '../typedoc.config.mjs'; import { isCallableInterfaceProperty } from './custom-theme.mjs'; import { applyCatchAllMdReplacements, @@ -33,9 +46,8 @@ 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'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -189,25 +201,6 @@ function formatMethodParametersTable(tableMd) { 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. @@ -647,36 +640,24 @@ 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 }; } + const propertiesBody = extractPropertiesSectionBody(contents); + const stripped = stripReferenceObjectPropertiesSection(contents); + return { propertiesBody, stripped }; } /** @@ -1325,17 +1306,21 @@ 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 +1330,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) { @@ -1363,9 +1349,7 @@ function processExtractMethodsNamespace(parentDecl, ctx, outDir) { 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 +1357,33 @@ 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++; + 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,7 +1391,7 @@ 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; } @@ -1415,83 +1400,108 @@ function extractCallableMembersFromDeclaration(decl, ctx, outDir) { if (mdx) { const fileName = `${toKebabCase(child.name)}.mdx`; const filePath = path.join(outDir, fileName); - fs.writeFileSync(filePath, mdx, 'utf-8'); - console.log(`[extract-methods] Wrote ${path.relative(path.join(__dirname, '..'), filePath)}`); - count++; + 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/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/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', From 33e16d0969f2dd5a78da4868f5d08314d6988acb Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Fri, 22 May 2026 16:27:04 -0700 Subject: [PATCH 02/11] refactor(typedoc): consolidate unwrap-optional + slug helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces three unwrap-optional helpers + a while-loop with a single `unwrapOptional(t, { deep })`. Extracts the two `toKebabCase` flavors into `.typedoc/slug.mjs` — `toFileSlug` (method filenames) and `toUrlSlug` (page URLs) intentionally differ on acronyms, so the module exports both rather than unifying. Also adds the snapshot dir to `.prettierignore`: lint-staged was silently transforming the committed snapshots after `vitest run` passed, leaving them out of sync with the raw output `extract-methods.mjs` writes. Regenerates them. Output is byte-identical to the pre-refactor baseline (`diff -r`, 695/695 files). Co-Authored-By: Claude Opus 4.7 (1M context) --- .prettierignore | 2 + .../api-key-resource-methods-create.mdx | 16 ++-- ...clerk-methods-handle-redirect-callback.mdx | 14 ++- .../clerk-methods-join-waitlist.mdx | 7 +- .../__snapshots__/clerk-methods-sign-out.mdx | 11 ++- ...n-resource-methods-check-authorization.mdx | 2 +- ...-resource-methods-email-code-send-code.mdx | 9 +- ...-in-future-resource-methods-email-link.mdx | 5 +- .typedoc/__tests__/file-structure.test.ts | 7 +- .typedoc/custom-router.mjs | 12 +-- .typedoc/extract-methods.mjs | 91 +++++++------------ .typedoc/slug.mjs | 43 +++++++++ 12 files changed, 115 insertions(+), 104 deletions(-) create mode 100644 .typedoc/slug.mjs 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 index 5fa277f1664..8a60ae22d3d 100644 --- a/.typedoc/__tests__/__snapshots__/api-key-resource-methods-create.mdx +++ b/.typedoc/__tests__/__snapshots__/api-key-resource-methods-create.mdx @@ -3,19 +3,19 @@ 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; +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. | + +| 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-redirect-callback.mdx b/.typedoc/__tests__/__snapshots__/clerk-methods-handle-redirect-callback.mdx index 54710015415..6c50c100af8 100644 --- a/.typedoc/__tests__/__snapshots__/clerk-methods-handle-redirect-callback.mdx +++ b/.typedoc/__tests__/__snapshots__/clerk-methods-handle-redirect-callback.mdx @@ -3,15 +3,13 @@ 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; +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. | + +| 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 index 3d21fda5618..cb76cd60a96 100644 --- a/.typedoc/__tests__/__snapshots__/clerk-methods-join-waitlist.mdx +++ b/.typedoc/__tests__/__snapshots__/clerk-methods-join-waitlist.mdx @@ -3,11 +3,12 @@ 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; +function joinWaitlist(params: JoinWaitlistParams): Promise ``` #### `JoinWaitlistParams` -| Property | Type | Description | -| ---------------------------------------- | -------- | ----------------------------------------------------- | + +| 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 index 74f878a5ae7..ea575168ea9 100644 --- a/.typedoc/__tests__/__snapshots__/clerk-methods-sign-out.mdx +++ b/.typedoc/__tests__/__snapshots__/clerk-methods-sign-out.mdx @@ -3,12 +3,13 @@ Signs out the current user on single-session instances, or all users on multi-session instances. ```typescript -function signOut(options?: SignOutOptions): Promise; +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. | + +| 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__/session-resource-methods-check-authorization.mdx b/.typedoc/__tests__/__snapshots__/session-resource-methods-check-authorization.mdx index 08039761271..0e0568beadd 100644 --- a/.typedoc/__tests__/__snapshots__/session-resource-methods-check-authorization.mdx +++ b/.typedoc/__tests__/__snapshots__/session-resource-methods-check-authorization.mdx @@ -3,5 +3,5 @@ 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; +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 index 8c6dcf3bfc8..b3d241d759e 100644 --- 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 @@ -8,7 +8,8 @@ function emailCode.sendCode(params?: SignInFutureEmailCodeSendParams): Promise<{ #### `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. | + +| 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 index 313c306c427..b2f588fd6fb 100644 --- a/.typedoc/__tests__/__snapshots__/sign-in-future-resource-methods-email-link.mdx +++ b/.typedoc/__tests__/__snapshots__/sign-in-future-resource-methods-email-link.mdx @@ -1,5 +1,6 @@ ### `emailLink` -| Property | Type | Description | -| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + +| 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__/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/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/extract-methods.mjs b/.typedoc/extract-methods.mjs index a40b3a58c0b..336eb0cd35d 100644 --- a/.typedoc/extract-methods.mjs +++ b/.typedoc/extract-methods.mjs @@ -48,6 +48,7 @@ import { import { isInlineModifierWithoutStandalonePage } from './standalone-page-tag.mjs'; import { applyTodoStrippingToComment } from './comment-utils.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); @@ -396,16 +397,31 @@ 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; } /** @@ -415,7 +431,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; } @@ -423,7 +439,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; } @@ -516,23 +532,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. * @@ -540,7 +539,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) { @@ -660,16 +659,6 @@ function splitPropertiesFromContents(contents) { return { propertiesBody, stripped }; } -/** - * @param {string} name - */ -function toKebabCase(name) { - return name - .replace(/([a-z\d])([A-Z])/g, '$1-$2') - .replace(/[\s_]+/g, '-') - .toLowerCase(); -} - /** * Plain TypeScript-like type text for ```typescript``` fences (no markdown / backticks from {@link MarkdownThemeContext.partials.someType}). * @@ -829,10 +818,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'; } @@ -1015,19 +1001,6 @@ function formatNestedParamNameColumn(baseName, pathSegments) { 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. - * - * @param {import('typedoc').SomeType | undefined} t - * @returns {import('typedoc').SomeType | undefined} - */ -function unwrapOptionalParamType(t) { - if (t?.type === 'optional') { - return /** @type {import('typedoc').OptionalType} */ (t).elementType; - } - return t; -} - /** * 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`). @@ -1036,7 +1009,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; } @@ -1342,7 +1315,7 @@ 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}` }); @@ -1362,7 +1335,7 @@ function processExtractMethodsNamespace(parentDecl, ctx, outDir) { if (nonCallableMembers.length) { const namespaceMdx = buildExtractMethodsNamespacePropertyTableMdx(parentDecl, nonCallableMembers, ctx); if (namespaceMdx) { - const namespacePath = path.join(outDir, `${toKebabCase(parentName)}.mdx`); + const namespacePath = path.join(outDir, `${toFileSlug(parentName)}.mdx`); collected.push({ filePath: namespacePath, content: namespaceMdx }); } } @@ -1398,7 +1371,7 @@ function extractCallableMembersFromDeclaration(decl, ctx, outDir) { 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); collected.push({ filePath, content: mdx }); } diff --git a/.typedoc/slug.mjs b/.typedoc/slug.mjs new file mode 100644 index 00000000000..3688240d014 --- /dev/null +++ b/.typedoc/slug.mjs @@ -0,0 +1,43 @@ +// @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(); +} From 03896b2f193c7091129656c3e0abf87f3b1ec4d3 Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Fri, 22 May 2026 16:48:30 -0700 Subject: [PATCH 03/11] refactor(typedoc): flatten params with optionality-aware separator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `clerkParametersTable` now joins flattened parent.child names with `?.` when the parent is optional and `.` when required, mirroring how the property would be accessed at runtime. `nestedParameterRowsFromDocumentedProperties` applies the same rule. With the theme correct, the regex post-process in `formatMethodParametersTable` (reorder + blanket `.` → `?.` rename) is no longer needed — removed (-116 LOC), along with `appendMarkdownTableRows`. Fixes 8 files where the old blanket rule was wrong in one direction or the other: - `shared/clerk/methods/handle-email-link-verification.mdx`: parent `params` is required, children now render as `params.foo` (was `params?.foo`). - `backend/get-auth-fn.mdx`, `backend/get-auth-fn-no-request.mdx`, `nextjs/current-user.mdx`, `shared/api-keys-namespace.mdx`, `shared/custom-navigation.mdx`, `shared/get-memberships.mdx`, `shared/on-event-listener.mdx`: parent is optional, children now render as `parent?.foo` (was `parent.foo`). Adds a snapshot for `handle-email-link-verification.mdx` to lock in the required-parent rendering. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...methods-handle-email-link-verification.mdx | 18 +++ .typedoc/__tests__/extract-methods.test.ts | 6 + .typedoc/custom-theme.mjs | 7 +- .typedoc/extract-methods.mjs | 139 ++---------------- 4 files changed, 41 insertions(+), 129 deletions(-) create mode 100644 .typedoc/__tests__/__snapshots__/clerk-methods-handle-email-link-verification.mdx 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__/extract-methods.test.ts b/.typedoc/__tests__/extract-methods.test.ts index 53bee1507a6..271d6fd0766 100644 --- a/.typedoc/__tests__/extract-methods.test.ts +++ b/.typedoc/__tests__/extract-methods.test.ts @@ -15,6 +15,7 @@ import { describe, expect, it } from 'vitest'; * * - `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`) @@ -41,6 +42,11 @@ describe('extract-methods snapshots', () => { 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'); diff --git a/.typedoc/custom-theme.mjs b/.typedoc/custom-theme.mjs index 31b29544bef..4bbcc398f20 100644 --- a/.typedoc/custom-theme.mjs +++ b/.typedoc/custom-theme.mjs @@ -553,11 +553,16 @@ 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 +573,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)), diff --git a/.typedoc/extract-methods.mjs b/.typedoc/extract-methods.mjs index 336eb0cd35d..d0ef2046a69 100644 --- a/.typedoc/extract-methods.mjs +++ b/.typedoc/extract-methods.mjs @@ -83,125 +83,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`; -} - /** * TypeDoc `code` display parts often already include backticks (same as {@link Comment.combineDisplayParts}). * Wrapping again would produce `` `Client` `` in MDX. @@ -993,12 +874,16 @@ function resolveDeclarationWithObjectMembers(t, project) { } /** - * @param {string} baseName - * @param {string[]} pathSegments + * 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').ParameterReflection} parentParam + * @param {string} childName */ -function formatNestedParamNameColumn(baseName, pathSegments) { - const pathChain = pathSegments.join('?.'); - return `\`${baseName}?.${pathChain}\``; +function formatNestedParamNameColumn(parentParam, childName) { + const sep = parentParam.flags?.isOptional ? '?.' : '.'; + return `\`${parentParam.name}${sep}${childName}\``; } /** @@ -1056,7 +941,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} |`); } @@ -1226,11 +1111,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'); } From f9182c91d13d70444b09201b850f45d868dfdfce Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Fri, 22 May 2026 17:00:58 -0700 Subject: [PATCH 04/11] format --- .typedoc/__tests__/extract-methods.test.ts | 5 +- .typedoc/custom-theme.mjs | 25 +++------ .typedoc/extract-methods.mjs | 64 ++++++---------------- .typedoc/slug.mjs | 18 ++---- 4 files changed, 30 insertions(+), 82 deletions(-) diff --git a/.typedoc/__tests__/extract-methods.test.ts b/.typedoc/__tests__/extract-methods.test.ts index 271d6fd0766..6992eb3dc2e 100644 --- a/.typedoc/__tests__/extract-methods.test.ts +++ b/.typedoc/__tests__/extract-methods.test.ts @@ -3,10 +3,7 @@ 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. + * 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`. diff --git a/.typedoc/custom-theme.mjs b/.typedoc/custom-theme.mjs index 4bbcc398f20..09688d5200b 100644 --- a/.typedoc/custom-theme.mjs +++ b/.typedoc/custom-theme.mjs @@ -23,8 +23,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 +163,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 +183,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,9 +548,7 @@ 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. + * 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[]} @@ -1092,9 +1085,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] @@ -1862,9 +1853,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 d0ef2046a69..ed36527bb1d 100644 --- a/.typedoc/extract-methods.mjs +++ b/.typedoc/extract-methods.mjs @@ -1,28 +1,16 @@ // @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. For each reference-object page listed in {@link REFERENCE_OBJECT_CONFIG} (e.g. `shared/clerk/clerk.mdx`), this listener: * - * - copies the body of the page's `## Properties` section (table only, no heading) into a - * sibling `properties.mdx`, + * - 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. + * - writes one `methods/.mdx` per callable child on the reflection (and on any `extraMethodInterfaces`), alongside the main page in that resource folder. * - * 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. 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. + * 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`. + * 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`. */ import fs from 'node:fs'; import path from 'node:path'; @@ -278,11 +266,9 @@ function getPrimaryCallSignature(decl) { } /** - * 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. + * 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`). + * 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 @@ -520,13 +506,9 @@ function extractPropertiesSectionBody(markdown) { } /** - * Split the `## Properties` section out of page contents, returning the body (no heading) - * and the page contents with the Properties section removed. + * 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. + * 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 }} @@ -874,9 +856,7 @@ function resolveDeclarationWithObjectMembers(t, project) { } /** - * 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`. + * 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').ParameterReflection} parentParam * @param {string} childName @@ -1166,12 +1146,9 @@ function hasExtractMethodsModifier(decl) { */ /** - * 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. + * 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) - * via {@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 @@ -1226,8 +1203,7 @@ function processExtractMethodsNamespace(parentDecl, ctx, outDir) { } /** - * Collect (path, content) pairs for each callable/`@extractMethods` child on `decl`. Callers - * are responsible for writing — see {@link load} which prettifies then writes. + * 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 @@ -1278,17 +1254,11 @@ function matchReferenceObjectPageUrl(output) { } /** - * Plugin entry: registers a `MarkdownPageEvent.END` listener that, for each page in - * {@link REFERENCE_OBJECT_CONFIG}, queues a `preWriteAsyncJob` to extract Properties + methods. + * 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`). + * 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. + * Must be loaded **after** `custom-plugin.mjs` so its END listener (link replacements + heading filtering) runs first. * * @param {import('typedoc-plugin-markdown').MarkdownApplication} app */ diff --git a/.typedoc/slug.mjs b/.typedoc/slug.mjs index 3688240d014..dcc274db8f5 100644 --- a/.typedoc/slug.mjs +++ b/.typedoc/slug.mjs @@ -1,8 +1,6 @@ // @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. + * 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 | * | --------------------------- | ----------------------- | ------------------------- | @@ -10,18 +8,13 @@ * | `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`). + * `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. + * `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. + * 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 */ @@ -33,8 +26,7 @@ export function toFileSlug(name) { } /** - * 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. + * 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 */ From ff3595c80cba5c019702ea7b121803db7f5e1b29 Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Fri, 22 May 2026 17:53:01 -0700 Subject: [PATCH 05/11] refactor(typedoc): inline markdown helpers, drop node_modules imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `custom-theme.mjs` and `extract-methods.mjs` were reaching into `node_modules/typedoc-plugin-markdown/dist/libs/...` for `backTicks`, `heading`, `htmlTable`, `table`, `removeLineBreaks`, and the `TypeDeclarationVisibility` enum — none of which the plugin exports publicly. That coupling breaks on any dependency update. Inlines the five utilities (plus the `escapeChars` / `formatTableCell` helpers they call) into `.typedoc/markdown-helpers.mjs`, and replaces the enum check with the underlying string literal (`'compact'`). Generated output is byte-identical to the upstream behavior (`diff -r` shows zero deltas beyond the unrelated step-4 changes; 16 snapshot tests pass). Co-Authored-By: Claude Opus 4.7 (1M context) --- .typedoc/custom-theme.mjs | 12 +--- .typedoc/extract-methods.mjs | 2 +- .typedoc/markdown-helpers.mjs | 117 ++++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 .typedoc/markdown-helpers.mjs diff --git a/.typedoc/custom-theme.mjs b/.typedoc/custom-theme.mjs index 09688d5200b..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'; @@ -704,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, { diff --git a/.typedoc/extract-methods.mjs b/.typedoc/extract-methods.mjs index ed36527bb1d..3696204342f 100644 --- a/.typedoc/extract-methods.mjs +++ b/.typedoc/extract-methods.mjs @@ -25,7 +25,7 @@ import { 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 { isCallableInterfaceProperty } from './custom-theme.mjs'; import { 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
`; +} From dc7260cf91602cebf7163674837c4dcb76151558 Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Fri, 22 May 2026 17:56:42 -0700 Subject: [PATCH 06/11] fix billing config --- .typedoc/reference-objects.mjs | 1 - 1 file changed, 1 deletion(-) 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' }], }, }; From 829534f3d836f31f4adaf8829a3991007ae28cee Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Fri, 22 May 2026 18:01:24 -0700 Subject: [PATCH 07/11] remove unused stripTextAfterTodo helper --- .typedoc/comment-utils.mjs | 16 ---------------- 1 file changed, 16 deletions(-) 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. * From d6565701fb05de2cf786797a73beb19fb97067dc Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Tue, 26 May 2026 15:57:08 -0700 Subject: [PATCH 08/11] refactor(typedoc): consolidate property merge across intersection/union/generic arms Three branches of `resolveDeclarationWithObjectMembers` duplicated the same shape: iterate each arm's children, filter to `Property`, dedupe by name into a `Map`, and return a sorted list. Each branch also wrapped that list in a synthetic `DeclarationReflection` (`__intersectionMerged` / `__unionMerged` / `__referenceMerged`) just so callers could read `.children`. Extracts `mergePropertyArms(arms, options)` for the shared logic. Union arms pass `{ skipNever: true, pickBetter: true }` to drop `prop?: never` discriminators and prefer the more-documented branch on name collisions; intersection and generic-instantiation arms use the default "later arm wins" overwrite. `resolveDeclarationWithObjectMembers` now returns `DeclarationReflection[] | undefined` directly, removing the fake holders. Callers (`nestedParameterRowsFromDocumentedProperties`, `resolveNominalObjectTypeForSingleParam`, `trySingleNominalParameterTypeSection`, `processExtractMethodsNamespace`) consume the array directly. Generated output is byte-identical to the prior commit (`diff -rq` shows zero deltas across 695 files; 16 snapshot tests pass). Co-Authored-By: Claude Opus 4.7 (1M context) --- .typedoc/extract-methods.mjs | 274 ++++++++++++++++------------------- 1 file changed, 122 insertions(+), 152 deletions(-) diff --git a/.typedoc/extract-methods.mjs b/.typedoc/extract-methods.mjs index 3696204342f..de675b72cda 100644 --- a/.typedoc/extract-methods.mjs +++ b/.typedoc/extract-methods.mjs @@ -706,27 +706,82 @@ function pickBetterUnionPropertyCandidate(existing, candidate) { } /** - * Object / type-literal declaration for a parameter type (reference, inlined reflection, intersection). - * TypeDoc applies `@param parent.prop` descriptions onto property reflections under this declaration. + * 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; + } + return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)); +} + +/** + * 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} + * @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 === 'reflection') { - const d = t.declaration; - if (d.children?.length) { - return d; - } + const children = t.declaration?.children; + return children?.length ? children : undefined; } if (t.type === 'reference') { const ref = /** @type {import('typedoc').ReferenceType} */ (t); - const typeArgs = ref.typeArguments ?? []; - let decl = ref.reflection && 'kind' in ref.reflection ? /** @type {import('typedoc').DeclarationReflection} */ (ref.reflection) @@ -734,124 +789,41 @@ function resolveDeclarationWithObjectMembers(t, project) { if (!decl && project && ref.name) { decl = lookupInterfaceOrTypeAliasByName(project, ref.name); } - if (decl) { - /** - * Generic aliases like `ClerkPaginationParams<{ status?: … }>` are a reference with `typeArguments`. - * TypeDoc often puts pagination fields only on the target alias `children` and omits `decl.type`, so returning `decl` early drops the type argument object. Merge base + each type argument's properties. - */ - if (typeArgs.length > 0) { - /** @type {Map} */ - const byName = new Map(); - if (decl.type) { - const fromType = resolveDeclarationWithObjectMembers(decl.type, project); - if (fromType?.children?.length) { - for (const c of fromType.children) { - if (c.kindOf(ReflectionKind.Property)) { - byName.set(c.name, c); - } - } - } - } - if (byName.size === 0 && decl.children?.length) { - for (const c of decl.children) { - if (c.kindOf(ReflectionKind.Property)) { - byName.set(c.name, c); - } - } - } - for (const ta of typeArgs) { - const fromArg = resolveDeclarationWithObjectMembers(ta, project); - if (fromArg?.children?.length) { - for (const c of fromArg.children) { - if (c.kindOf(ReflectionKind.Property)) { - byName.set(c.name, c); - } - } - } - } - if (byName.size > 0) { - return /** @type {import('typedoc').DeclarationReflection} */ ( - /** @type {unknown} */ ({ - children: [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)), - kind: ReflectionKind.TypeLiteral, - name: '__referenceMerged', - }) - ); - } - } - - if (decl.children?.length) { - return decl; - } - if (decl.type) { - return resolveDeclarationWithObjectMembers(decl.type, project); - } + 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 decl.children; + } + if (decl.type) { + return resolveDeclarationWithObjectMembers(decl.type, project); + } + return undefined; } if (t.type === 'intersection') { const inter = /** @type {import('typedoc').IntersectionType} */ (t); - /** @type {Map} */ - const byName = new Map(); - for (const inner of inter.types) { - const res = resolveDeclarationWithObjectMembers(inner, project); - if (res?.children?.length) { - for (const c of res.children) { - if (c.kindOf(ReflectionKind.Property)) { - byName.set(c.name, c); - } - } - } - } - if (byName.size === 0) { - return undefined; - } - // Synthetic holder so nominal param sections list every `&` arm (e.g. `RedirectOptions`). - return /** @type {import('typedoc').DeclarationReflection} */ ( - /** @type {unknown} */ ({ - children: [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)), - kind: ReflectionKind.TypeLiteral, - name: '__intersectionMerged', - }) - ); + return mergePropertyArms(inter.types.map(inner => resolveDeclarationWithObjectMembers(inner, project))); } if (t.type === 'union') { const u = /** @type {import('typedoc').UnionType} */ (t); - /** @type {Map} */ - const byName = new Map(); - for (const inner of u.types) { - const res = resolveDeclarationWithObjectMembers(inner, project); - if (!res?.children?.length) { - continue; - } - for (const c of res.children) { - if (!c.kindOf(ReflectionKind.Property)) { - continue; - } - if (propertyReflectionTypeIsNever(c)) { - continue; - } - const existing = byName.get(c.name); - if (!existing) { - byName.set(c.name, c); - } else { - byName.set(c.name, pickBetterUnionPropertyCandidate(existing, c)); - } - } - } - if (byName.size === 0) { - return undefined; - } - return /** @type {import('typedoc').DeclarationReflection} */ ( - /** @type {unknown} */ ({ - children: [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)), - kind: ReflectionKind.TypeLiteral, - name: '__unionMerged', - }) + return mergePropertyArms( + u.types.map(inner => resolveDeclarationWithObjectMembers(inner, project)), + { skipNever: true, pickBetter: true }, ); } - if (t.type === 'optional') { - return resolveDeclarationWithObjectMembers(/** @type {import('typedoc').OptionalType} */ (t).elementType, project); - } return undefined; } @@ -910,11 +882,11 @@ function nestedParameterRowsFromDocumentedProperties(param, ctx) { } const project = /** @type {import('typedoc').ProjectReflection | undefined} */ (param.project ?? ctx.page?.project); - const holder = resolveDeclarationWithObjectMembers(param.type, project); - if (!holder?.children?.length) { + const children = resolveDeclarationWithObjectMembers(param.type, project); + if (!children?.length) { return []; } - const props = holder.children.filter(c => c.kindOf(ReflectionKind.Property)); + const props = children.filter(c => c.kindOf(ReflectionKind.Property)); props.sort((a, b) => a.name.localeCompare(b.name)); /** @type {string[]} */ const rows = []; @@ -958,11 +930,13 @@ function lookupInterfaceOrTypeAliasByName(project, name) { } /** - * Unwrap optional wrappers. When the parameter is a single named interface or type alias for an object shape, returns that name and the declaration holding object properties. + * 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, holder: import('typedoc').DeclarationReflection, typeDecl: import('typedoc').DeclarationReflection } | undefined} + * @returns {{ sectionTitle: string, children: import('typedoc').DeclarationReflection[], typeDecl: import('typedoc').DeclarationReflection } | undefined} */ function resolveNominalObjectTypeForSingleParam(t, project) { if (!t) { @@ -974,35 +948,32 @@ function resolveNominalObjectTypeForSingleParam(t, project) { project, ); } - if (t.type === 'reference') { - const ref = /** @type {import('typedoc').ReferenceType} */ (t); - let typeDecl = - ref.reflection && 'kind' in ref.reflection - ? /** @type {import('typedoc').DeclarationReflection} */ (ref.reflection) - : lookupInterfaceOrTypeAliasByName(project, ref.name); - if (!typeDecl) { + 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; } - if (typeDecl.kindOf(ReflectionKind.Interface)) { - if (!typeDecl.children?.length) { - return undefined; - } - return { sectionTitle: typeDecl.name, holder: typeDecl, 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 holder = fromResolvedType?.children?.length - ? fromResolvedType - : typeDecl.children?.length - ? typeDecl - : undefined; - if (!holder?.children?.length) { - return undefined; - } - return { sectionTitle: typeDecl.name, holder, typeDecl }; + 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; } @@ -1043,7 +1014,7 @@ function trySingleNominalParameterTypeSection(sig, ctx) { if (!nominal) { return undefined; } - const props = (nominal.holder.children ?? []).filter(c => c.kindOf(ReflectionKind.Property)); + const props = nominal.children.filter(c => c.kindOf(ReflectionKind.Property)); if (props.length === 0) { return undefined; } @@ -1157,8 +1128,7 @@ function hasExtractMethodsModifier(decl) { */ function processExtractMethodsNamespace(parentDecl, ctx, outDir) { const project = ctx.page?.project; - const holder = resolveDeclarationWithObjectMembers(parentDecl.type, project); - const members = holder?.children ?? []; + 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`, From 79d22c0b6a7bce3c14ac8762ecc26cdaefdd1708 Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Wed, 27 May 2026 10:54:34 -0700 Subject: [PATCH 09/11] chore(typedoc): clear lint errors in touched files - drop unused runtime `MarkdownTheme` import; use JSDoc-only `import('typedoc-plugin-markdown').MarkdownTheme` in the cast - pre-write job body is sync (`fs.mkdirSync`/`fs.writeFileSync`); drop `async` to satisfy `require-await` - brace single-line `if (...) return false;` for `curly` - autofix import order Output verified byte-identical via `pnpm typedoc:generate:skip-build` + `diff -rq`; `.typedoc` vitest still 16/16. Co-Authored-By: Claude Opus 4.7 (1M context) --- .typedoc/custom-router.mjs | 2 +- .typedoc/custom-theme.mjs | 8 +++++--- .typedoc/extract-methods.mjs | 17 ++++++++++------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/.typedoc/custom-router.mjs b/.typedoc/custom-router.mjs index 0e6cf9a5c7c..b56f1eb57e8 100644 --- a/.typedoc/custom-router.mjs +++ b/.typedoc/custom-router.mjs @@ -2,9 +2,9 @@ import { ReflectionKind } from 'typedoc'; 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'; +import { isInlineModifierWithoutStandalonePage } from './standalone-page-tag.mjs'; /** @type {Set} */ const REFERENCE_OBJECT_SYMBOL_NAMES = new Set(Object.values(REFERENCE_OBJECT_PAGE_SYMBOLS)); diff --git a/.typedoc/custom-theme.mjs b/.typedoc/custom-theme.mjs index ab6274042ac..c4e1f6af21a 100644 --- a/.typedoc/custom-theme.mjs +++ b/.typedoc/custom-theme.mjs @@ -1,11 +1,11 @@ // @ts-check import { ArrayType, i18n, IntersectionType, ReferenceType, ReflectionKind, ReflectionType, UnionType } from 'typedoc'; import { MarkdownTheme, MarkdownThemeContext } from 'typedoc-plugin-markdown'; -import { backTicks, heading, htmlTable, removeLineBreaks, table } from './markdown-helpers.mjs'; import { applyTodoStrippingToComment } from './comment-utils.mjs'; -import { isInlineModifierWithoutStandalonePage } from './standalone-page-tag.mjs'; +import { backTicks, heading, htmlTable, removeLineBreaks, table } from './markdown-helpers.mjs'; import { REFERENCE_OBJECTS_LIST } from './reference-objects.mjs'; +import { isInlineModifierWithoutStandalonePage } from './standalone-page-tag.mjs'; export { REFERENCE_OBJECTS_LIST }; @@ -1203,7 +1203,9 @@ class ClerkMarkdownThemeContext extends MarkdownThemeContext { // Find the immediate next heading after '## Parameters' const nextHeadingIndex = splitOutput.findIndex((item, index) => { // Skip the items before the parameters - if (index <= parametersIndex) return false; + if (index <= parametersIndex) { + return false; + } // Find the next heading return item.startsWith('##') || item.startsWith('\n##'); }); diff --git a/.typedoc/extract-methods.mjs b/.typedoc/extract-methods.mjs index de675b72cda..c5200fcbc2a 100644 --- a/.typedoc/extract-methods.mjs +++ b/.typedoc/extract-methods.mjs @@ -15,6 +15,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; + import { Comment, IntersectionType, @@ -24,19 +25,19 @@ import { ReflectionType, UnionType, } from 'typedoc'; -import { MarkdownPageEvent, MarkdownTheme } from 'typedoc-plugin-markdown'; -import { removeLineBreaks } from './markdown-helpers.mjs'; +import { MarkdownPageEvent } from 'typedoc-plugin-markdown'; -import { isCallableInterfaceProperty } from './custom-theme.mjs'; +import { applyTodoStrippingToComment } from './comment-utils.mjs'; import { applyCatchAllMdReplacements, applyRelativeLinkReplacements, stripReferenceObjectPropertiesSection, } from './custom-plugin.mjs'; -import { isInlineModifierWithoutStandalonePage } from './standalone-page-tag.mjs'; -import { applyTodoStrippingToComment } from './comment-utils.mjs'; +import { isCallableInterfaceProperty } from './custom-theme.mjs'; +import { removeLineBreaks } from './markdown-helpers.mjs'; import { REFERENCE_OBJECT_CONFIG } from './reference-objects.mjs'; import { toFileSlug } from './slug.mjs'; +import { isInlineModifierWithoutStandalonePage } from './standalone-page-tag.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -1249,7 +1250,9 @@ export function load(app) { console.warn(`[extract-methods] No project on page event for ${pageUrl}, skipping`); return; } - const theme = /** @type {InstanceType | undefined} */ (app.renderer.theme); + 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; @@ -1273,7 +1276,7 @@ export function load(app) { } } - output.preWriteAsyncJobs.push(async () => { + output.preWriteAsyncJobs.push(() => { fs.mkdirSync(objectDir, { recursive: true }); // `output.contents` is already prettier-formatted by typedoc-plugin-markdown's earlier From 3f45eb58e9b925ad3bc6ebef82ac5d8eb8bc67f0 Mon Sep 17 00:00:00 2001 From: Michael Novotny Date: Wed, 27 May 2026 22:11:37 -0500 Subject: [PATCH 10/11] refactor(repo): address co-review findings - Hoist `unwrapOptional` into shared `.typedoc/type-utils.mjs`; drop the duplicate `unwrapOptionalType` from `custom-theme.mjs`. - `formatTypeScriptSignature`: use the bare last segment for dotted method names so the code fence is valid TS (`function sendCode(...)` instead of `function emailCode.sendCode(...)`). Heading still shows the qualified name. Snapshot refreshed. - Add a description to `// @ts-check` in the two new helper files to satisfy `@typescript-eslint/ban-ts-comment`. - Add empty changeset per AGENTS.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/neat-cobras-press.md | 2 ++ ...-resource-methods-email-code-send-code.mdx | 2 +- .typedoc/custom-theme.mjs | 21 ++----------- .typedoc/extract-methods.mjs | 31 +++---------------- .typedoc/markdown-helpers.mjs | 2 +- .typedoc/slug.mjs | 2 +- .typedoc/type-utils.mjs | 27 ++++++++++++++++ 7 files changed, 39 insertions(+), 48 deletions(-) create mode 100644 .changeset/neat-cobras-press.md create mode 100644 .typedoc/type-utils.mjs diff --git a/.changeset/neat-cobras-press.md b/.changeset/neat-cobras-press.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/neat-cobras-press.md @@ -0,0 +1,2 @@ +--- +--- 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 index b3d241d759e..6715e7476f4 100644 --- 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 @@ -3,7 +3,7 @@ Sends an email code to sign-in. ```typescript -function emailCode.sendCode(params?: SignInFutureEmailCodeSendParams): Promise<{ error: null | ClerkError }> +function sendCode(params?: SignInFutureEmailCodeSendParams): Promise<{ error: null | ClerkError }> ``` #### `SignInFutureEmailCodeSendParams` diff --git a/.typedoc/custom-theme.mjs b/.typedoc/custom-theme.mjs index c4e1f6af21a..dc06bc65608 100644 --- a/.typedoc/custom-theme.mjs +++ b/.typedoc/custom-theme.mjs @@ -6,6 +6,7 @@ import { applyTodoStrippingToComment } from './comment-utils.mjs'; import { backTicks, heading, htmlTable, removeLineBreaks, table } from './markdown-helpers.mjs'; import { REFERENCE_OBJECTS_LIST } from './reference-objects.mjs'; import { isInlineModifierWithoutStandalonePage } from './standalone-page-tag.mjs'; +import { unwrapOptional } from './type-utils.mjs'; export { REFERENCE_OBJECTS_LIST }; @@ -51,22 +52,6 @@ function isUnionTypeDoc(/** @type {import('typedoc').Type | undefined} */ t) { return Boolean(o && typeof o === 'object' && o.type === 'union' && Array.isArray(o.types)); } -/** - * @param {import('typedoc').Type | undefined} t - */ -function unwrapOptionalType(t) { - if ( - t && - typeof t === 'object' && - 'type' in t && - /** @type {{ type: string }} */ (t).type === 'optional' && - 'elementType' in t - ) { - return /** @type {{ elementType: import('typedoc').Type }} */ (t).elementType; - } - return t; -} - /** * Stock `typedoc-plugin-markdown` `arrayType` only wraps `elementType.type === 'union'`. * For `T | T[]` where `T` is an `@inline` alias to a union, the element is still a `reference` in the model but renders as `"a" \| "b"`, producing `"a" \| "b"[]` (wrong binding). Instead, parens the array type whenever the reference inlines to a union RHS so it produces `("a" \| "b")[]`. @@ -287,7 +272,7 @@ function tryCollapseExpandedOAuthStrategyUnion(model, ctx) { * @returns {import('typedoc').DeclarationReflection[]} */ function collectPropertyReflectionsFromIntersectionArm(t, visitedReflectionIds, project) { - const unwrapped = unwrapOptionalType(t); + const unwrapped = unwrapOptional(t); if (!unwrapped) { return []; } @@ -401,7 +386,7 @@ function mergeIntersectionPropertyReflections(intersection, project) { * @returns {import('typedoc').DeclarationReflection[]} */ function collectPropertyReflectionsFromUnionObjectArms(t, visitedReflectionIds, project) { - const unwrapped = unwrapOptionalType(t); + const unwrapped = unwrapOptional(t); if (!unwrapped || /** @type {{ type?: string }} */ (unwrapped).type !== 'union') { return []; } diff --git a/.typedoc/extract-methods.mjs b/.typedoc/extract-methods.mjs index c5200fcbc2a..138a388fa9d 100644 --- a/.typedoc/extract-methods.mjs +++ b/.typedoc/extract-methods.mjs @@ -38,6 +38,7 @@ import { removeLineBreaks } from './markdown-helpers.mjs'; import { REFERENCE_OBJECT_CONFIG } from './reference-objects.mjs'; 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); @@ -266,32 +267,6 @@ function getPrimaryCallSignature(decl) { return undefined; } -/** - * 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 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 cur; -} - /** * For `prop: OuterAlias` where `type OuterAlias = SomeFn`, maps generic parameter names on `SomeFn` to the instantiated type arguments (e.g. `Params` → `CheckAuthorizationParams`). * @@ -554,7 +529,9 @@ function formatTypeScriptSignature(sig, memberName, instantiationMap) { }) ?? []; const retT = substituteGenericParamRefsInType(sig.type, instantiationMap) ?? sig.type; const ret = retT ? typeStringForTypeScriptFence(retT) : 'void'; - return `function ${memberName}${typeParamStr}(${params.join(', ')}): ${ret}`; + // 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}`; } /** diff --git a/.typedoc/markdown-helpers.mjs b/.typedoc/markdown-helpers.mjs index 91ef4df2b5b..9924671bf4b 100644 --- a/.typedoc/markdown-helpers.mjs +++ b/.typedoc/markdown-helpers.mjs @@ -1,4 +1,4 @@ -// @ts-check +// @ts-check — JSDoc-typed plugin helpers. /** * Small markdown utilities. These are inlined from `typedoc-plugin-markdown`'s * internal `dist/libs/markdown/` and `dist/libs/utils/` modules — the plugin's diff --git a/.typedoc/slug.mjs b/.typedoc/slug.mjs index dcc274db8f5..f4cebb54e64 100644 --- a/.typedoc/slug.mjs +++ b/.typedoc/slug.mjs @@ -1,4 +1,4 @@ -// @ts-check +// @ts-check — JSDoc-typed plugin helpers. /** * 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. * diff --git a/.typedoc/type-utils.mjs b/.typedoc/type-utils.mjs new file mode 100644 index 00000000000..38ba45c4e5f --- /dev/null +++ b/.typedoc/type-utils.mjs @@ -0,0 +1,27 @@ +// @ts-check — JSDoc-typed plugin helpers shared between custom-theme.mjs and extract-methods.mjs. + +/** + * 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} + */ +export 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 cur; +} From 41e2d79d96943058cc2a5a85f5a73050593ae846 Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Thu, 28 May 2026 13:10:30 -0700 Subject: [PATCH 11/11] remove changeset --- .changeset/neat-cobras-press.md | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .changeset/neat-cobras-press.md diff --git a/.changeset/neat-cobras-press.md b/.changeset/neat-cobras-press.md deleted file mode 100644 index a845151cc84..00000000000 --- a/.changeset/neat-cobras-press.md +++ /dev/null @@ -1,2 +0,0 @@ ---- ----