diff --git a/.changeset/thick-singers-pick.md b/.changeset/thick-singers-pick.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/thick-singers-pick.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.typedoc/__tests__/__snapshots__/organization-resource-methods-create-domain.mdx b/.typedoc/__tests__/__snapshots__/organization-resource-methods-create-domain.mdx new file mode 100644 index 00000000000..882997f821f --- /dev/null +++ b/.typedoc/__tests__/__snapshots__/organization-resource-methods-create-domain.mdx @@ -0,0 +1,21 @@ +### `createDomain()` + +Creates a new domain. + +Returns an [`OrganizationDomainResource`](/docs/reference/types/organization-domain-resource) object. + +> [!WARNING] +> You must have [**Verified domains**](/docs/guides/organizations/add-members/verified-domains) enabled in your app's settings in the Clerk Dashboard. + +```typescript +function createDomain(domainName: string, params?: Pick): Promise +``` + +#### Parameters + + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `domainName` | `string` | The name of the domain to create. | +| `params?` | `Pick`\<`CreateOrganizationDomainParams`, `"enrollmentMode"`\> | Optional parameters, including the `enrollmentMode` to assign to the new domain. | +| `params?.enrollmentMode?` | "manual_invitation" \| "automatic_invitation" \| "automatic_suggestion" \| "enterprise_sso" | The enrollment mode that determines how matching users are added to the Organization. Defaults to `manual_invitation`. | diff --git a/.typedoc/__tests__/custom-theme.test.mts b/.typedoc/__tests__/custom-theme.test.mts new file mode 100644 index 00000000000..4d286bc462e --- /dev/null +++ b/.typedoc/__tests__/custom-theme.test.mts @@ -0,0 +1,111 @@ +import { ReflectionKind, type Type } from 'typedoc'; +import { describe, expect, it } from 'vitest'; + +import { getParameterObjectShapeDeclaration, getPickPropertyNames } from '../custom-theme.mjs'; + +const literal = (value: unknown) => ({ type: 'literal', value }) as unknown as Type; +const union = (...types: unknown[]) => ({ type: 'union', types }) as unknown as Type; + +describe('getPickPropertyNames', () => { + it('returns the single key for a string literal', () => { + expect(getPickPropertyNames(literal('enrollmentMode'))).toEqual(['enrollmentMode']); + }); + + it('flattens a union of string literals', () => { + expect(getPickPropertyNames(union(literal('a'), literal('b')))).toEqual(['a', 'b']); + }); + + it('recurses into nested unions', () => { + expect(getPickPropertyNames(union(literal('a'), union(literal('b'), literal('c'))))).toEqual(['a', 'b', 'c']); + }); + + it('bails on a non-string literal key', () => { + expect(getPickPropertyNames(literal(0))).toBeUndefined(); + }); + + it('bails when any union arm is not a string literal', () => { + expect(getPickPropertyNames(union(literal('a'), { type: 'reference', name: 'Foo' }))).toBeUndefined(); + }); +}); + +describe('getParameterObjectShapeDeclaration', () => { + const sourceInterface = (...names: string[]) => ({ + reflection: { + kind: ReflectionKind.Interface, + name: 'Src', + children: names.map(name => ({ name })), + }, + }); + + const pick = (source: unknown, keys: unknown) => + ({ + type: 'reference', + name: 'Pick', + package: 'typescript', + typeArguments: [{ type: 'reference', ...(source as object) }, keys], + }) as unknown as Type; + + it('does not resolve Pick sources whose declaration kind is unsupported', () => { + const sourceType = { + type: 'reference', + name: 'ExampleClass', + reflection: { + kind: ReflectionKind.Class, + children: [ + { + name: 'someMethodName', + kind: ReflectionKind.Method, + signatures: [{}], + }, + ], + }, + } as unknown as Type; + const pickType = { + type: 'reference', + name: 'Pick', + package: 'typescript', + typeArguments: [sourceType, { type: 'literal', value: 'someMethodName' }], + } as unknown as Type; + + expect(getParameterObjectShapeDeclaration(pickType)).toBeUndefined(); + }); + + it('selects only the picked properties from a multi-key Pick', () => { + const decl = getParameterObjectShapeDeclaration( + pick(sourceInterface('name', 'enrollmentMode', 'other'), union(literal('name'), literal('enrollmentMode'))), + ); + expect(decl?.children?.map(child => child.name)).toEqual(['name', 'enrollmentMode']); + }); + + it('does not mutate the source declaration', () => { + const source = sourceInterface('name', 'enrollmentMode'); + getParameterObjectShapeDeclaration(pick(source, literal('enrollmentMode'))); + expect(source.reflection.children.map(child => child.name)).toEqual(['name', 'enrollmentMode']); + }); + + it('fails closed when a picked key is not a property of the source', () => { + expect(getParameterObjectShapeDeclaration(pick(sourceInterface('name'), literal('missing')))).toBeUndefined(); + }); + + it('fails closed for non-literal keys', () => { + expect( + getParameterObjectShapeDeclaration( + pick(sourceInterface('name', 'enrollmentMode'), { type: 'reference', name: 'keyof Src' }), + ), + ).toBeUndefined(); + }); + + it('does not treat Omit as a flattenable builtin', () => { + const omit = { + type: 'reference', + name: 'Omit', + package: 'typescript', + reflection: undefined, + typeArguments: [ + { type: 'reference', ...sourceInterface('name', 'enrollmentMode') }, + { type: 'literal', value: 'name' }, + ], + } as unknown as Type; + expect(getParameterObjectShapeDeclaration(omit)).toBeUndefined(); + }); +}); diff --git a/.typedoc/__tests__/extract-methods.test.ts b/.typedoc/__tests__/extract-methods.test.ts index d62f715d917..3d0b6fe0882 100644 --- a/.typedoc/__tests__/extract-methods.test.ts +++ b/.typedoc/__tests__/extract-methods.test.ts @@ -13,6 +13,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/create-domain.mdx` – `Pick` parameter flattened to only the selected property * - `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`) @@ -45,6 +46,11 @@ describe('extract-methods snapshots', () => { await expect(content).toMatchFileSnapshot('./__snapshots__/clerk-methods-handle-email-link-verification.mdx'); }); + it('Pick parameter includes only selected properties without linking the full type: organization.createDomain()', async () => { + const content = await readGenerated('shared/organization-resource/methods/create-domain.mdx'); + await expect(content).toMatchFileSnapshot('./__snapshots__/organization-resource-methods-create-domain.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 c2c0d1c79c3..9b10a4651e7 100644 --- a/.typedoc/custom-theme.mjs +++ b/.typedoc/custom-theme.mjs @@ -446,7 +446,51 @@ function hasDefaultValuesForParameters(parameters) { } /** - * Object shape for a parameter: inline `{ … }`, optional-wrapped, or reference to a type alias / interface. + * Collects string literal members from a type used as `Pick`'s key argument. + * + * @param {import('typedoc').Type | undefined} t + * @returns {string[] | undefined} + */ +function getPickPropertyNames(t) { + const unwrapped = unwrapOptional(t); + if (!unwrapped || typeof unwrapped !== 'object') { + return undefined; + } + if (unwrapped.type === 'literal') { + const literal = /** @type {import('typedoc').LiteralType} */ (unwrapped); + if (typeof literal.value === 'string') { + return [literal.value]; + } + return undefined; + } + if (!isUnionTypeDoc(unwrapped)) { + return undefined; + } + const names = []; + const union = /** @type {import('typedoc').UnionType} */ (unwrapped); + for (const type of union.types) { + const nestedNames = getPickPropertyNames(type); + if (!nestedNames) { + return undefined; + } + names.push(...nestedNames); + } + return names; +} + +/** + * @param {import('typedoc').Type | undefined} t + * @returns {boolean} + */ +function isPickReferenceType(t) { + if (!isReferenceTypeDoc(t)) { + return false; + } + return t.name === 'Pick' && t.package === 'typescript' && t.typeArguments?.length === 2; +} + +/** + * Object shape for a parameter: inline `{ … }`, optional-wrapped, reference to a type alias / interface, or `Pick` with literal keys. * * @param {import('typedoc').Type | undefined} t * @returns {import('typedoc').DeclarationReflection | undefined} @@ -470,6 +514,37 @@ function getParameterObjectShapeDeclaration(t) { } if (o.type === 'reference') { const ref = /** @type {import('typedoc').ReferenceType} */ (t); + if (isPickReferenceType(ref)) { + const [sourceType, keysType] = /** @type {[import('typedoc').Type, import('typedoc').Type]} */ ( + ref.typeArguments + ); + const propertyNames = getPickPropertyNames(keysType); + if (!propertyNames?.length) { + return undefined; + } + const sourceDecl = getParameterObjectShapeDeclaration(sourceType); + const sourceRef = sourceType.type === 'reference' ? sourceType.reflection : undefined; + const sourceWithChildren = + sourceDecl ?? + (sourceRef && + (sourceRef.kind === ReflectionKind.TypeAlias || sourceRef.kind === ReflectionKind.Interface) && + 'children' in sourceRef + ? /** @type {import('typedoc').DeclarationReflection} */ (sourceRef) + : undefined); + if (!sourceWithChildren?.children?.length) { + return undefined; + } + const selected = new Set(propertyNames); + const children = sourceWithChildren.children.filter(child => selected.has(child.name)); + if (children.length !== selected.size) { + return undefined; + } + return /** @type {import('typedoc').DeclarationReflection} */ ({ + ...sourceWithChildren, + kind: ReflectionKind.TypeLiteral, + children, + }); + } const sym = ref.reflection; if (!sym) { return undefined; @@ -504,6 +579,27 @@ function shouldFlattenInlineObjectParameter(decl) { return Boolean(only?.comment?.hasVisibleComponent()); } +/** + * Whether a parameter is a built-in `Pick` that will be flattened into nested rows. Its source type should not + * link to the full unpicked declaration, which documents properties the parameter does not accept. + * + * @param {import('typedoc').Type | undefined} t + */ +function isFlattenedPickParameter(t) { + const unwrapped = unwrapOptional(t); + if (!isPickReferenceType(unwrapped)) { + return false; + } + return shouldFlattenInlineObjectParameter(getParameterObjectShapeDeclaration(t)); +} + +/** + * @param {string} value + */ +function stripMarkdownLinks(value) { + return value.replace(/\[([^\[\]]*)\]\((.*?)\)/gm, '$1'); +} + /** * Same as typedoc-plugin-markdown `member.parametersTable`, with `shouldFlattenInlineObjectParameter` and `getParameterObjectShapeDeclaration`. * @@ -581,12 +677,13 @@ function clerkParametersTable(model) { const optional = isOptional ? '?' : ''; row.push(`${rest}${backTicks(`${parameter.name}${optional}`)}`); if (parameter.type) { - const displayType = + const renderedType = parameter.type instanceof ReflectionType ? this.partials.reflectionType(parameter.type, { forceCollapse: true, }) : this.partials.someType(parameter.type); + const displayType = isFlattenedPickParameter(parameter.type) ? stripMarkdownLinks(renderedType) : renderedType; row.push(removeLineBreaks(displayType)); } if (showDefaults) { @@ -1954,4 +2051,4 @@ function isCallablePropertyValueType(t, helpers, seenReflectionIds) { return false; } -export { isCallableInterfaceProperty }; +export { getParameterObjectShapeDeclaration, getPickPropertyNames, isCallableInterfaceProperty }; diff --git a/packages/localizations/src/he-IL.ts b/packages/localizations/src/he-IL.ts index 39fa6e8bb4c..b87cd52df23 100644 --- a/packages/localizations/src/he-IL.ts +++ b/packages/localizations/src/he-IL.ts @@ -935,23 +935,21 @@ export const heIL: LocalizationResource = { membershipRole__basicMember: 'חבר', membershipRole__guestMember: 'אורח', oauthConsent: { - action__allow: "אישור", - action__deny: "דחייה", - offlineAccessNotice: "תישאר מחובר עד שתתנתק או תבטל את הגישה.", - redirectNotice: "אם תאשר גישה, האפליקציה תעביר אותך אל {{domainAction}}.", + action__allow: 'אישור', + action__deny: 'דחייה', + offlineAccessNotice: 'תישאר מחובר עד שתתנתק או תבטל את הגישה.', + redirectNotice: 'אם תאשר גישה, האפליקציה תעביר אותך אל {{domainAction}}.', redirectUriModal: { - subtitle: - "ודא שאתה סומך על {{applicationName}} ושכתובת URL זו שייכת ל-{{applicationName}}.", - title: "כתובת להפניה", + subtitle: 'ודא שאתה סומך על {{applicationName}} ושכתובת URL זו שייכת ל-{{applicationName}}.', + title: 'כתובת להפניה', }, scopeList: { - title: "פעולה זו תאפשר ל-{{applicationName}} גישה אל:", privateMetadata: undefined, + title: 'פעולה זו תאפשר ל-{{applicationName}} גישה אל:', }, - subtitle: "מבקש גישה ל-{{applicationName}} בשם {{identifier}}", - viewFullUrl: "הצג כתובת מלאה", - warning: - "ודא שאתה סומך על {{applicationName}} ({{domainAction}}). ייתכן שתשתף מידע רגיש עם אתר או אפליקציה זו.", + subtitle: 'מבקש גישה ל-{{applicationName}} בשם {{identifier}}', + viewFullUrl: 'הצג כתובת מלאה', + warning: 'ודא שאתה סומך על {{applicationName}} ({{domainAction}}). ייתכן שתשתף מידע רגיש עם אתר או אפליקציה זו.', }, oauthDeviceVerification: { action__tryAnotherCode: undefined,