Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/green-donuts-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/shared': patch
'@clerk/clerk-react': patch
'@clerk/types': patch
---

Improve JSDoc documentation
73 changes: 71 additions & 2 deletions .typedoc/custom-plugin.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,43 @@
// @ts-check
import { MarkdownRendererEvent } from 'typedoc-plugin-markdown';
import { MarkdownPageEvent, MarkdownRendererEvent } from 'typedoc-plugin-markdown';

/**
* A list of files where we want to remove any headings
*/
const FILES_WITHOUT_HEADINGS = [
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of the files only contain the contents of a single interface.

For example:

# Parameters

Table goes here

So since I want to use the table contents as a partial/include somewhere else I need to get rid off the heading. But since I don't want to remove headings everywhere I'm removing them only from the files I want to use in such way.

'use-organization-return.mdx',
'use-organization-params.mdx',
'paginated-resources.mdx',
'pages-or-infinite-options.mdx',
'pages-or-infinite-options.mdx',
'paginated-hook-config.mdx',
'use-organization-list-return.mdx',
'use-organization-list-params.mdx',
];

/**
* An array of tuples where the first element is the file name and the second element is the new path.
*/
const LINK_REPLACEMENTS = [
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sometimes there are relative links like [PaginatedResponse](../types/paginated-response.mdx) in the files. The link to the also generated file through typedoc. But we already have that page manually created and want to link to it. So this replaces relative links with links to our existing docs.

['clerk-paginated-response', '/docs/references/javascript/types/clerk-paginated-response'],
['paginated-resources', '#paginated-resources'],
];

/**
* Inside the generated MDX files are links to other generated MDX files. These relative links need to be replaced with absolute links to pages that exist on clerk.com.
* For example, `[Foobar](../../foo/bar.mdx)` needs to be replaced with `[Foobar](/docs/foo/bar)`.
* It also shouldn't matter how level deep the relative link is.
*
* This function returns an array of `{ pattern: string, replace: string }` to pass into the `typedoc-plugin-replace-text` plugin.
*/
function getRelativeLinkReplacements() {
return LINK_REPLACEMENTS.map(([fileName, newPath]) => {
return {
pattern: new RegExp(`\\((?:\\.{1,2}\\/)+.*?${fileName}\\.mdx\\)`, 'g'),
replace: `(${newPath})`,
};
});
}

/**
* @param {string} str
Expand All @@ -13,8 +51,9 @@ function toKebabCase(str) {
*/
export function load(app) {
app.renderer.on(MarkdownRendererEvent.BEGIN, output => {
// Do not output README.mdx files
// Modify the output object
output.urls = output.urls
// Do not output README.mdx files
?.filter(e => !e.url.endsWith('README.mdx'))
.map(e => {
// Convert URLs (by default camelCase) to kebab-case
Expand All @@ -23,7 +62,37 @@ export function load(app) {
e.url = kebabUrl;
e.model.url = kebabUrl;

/**
* For the `@clerk/shared` package it outputs the hooks as for example: shared/react/hooks/use-clerk/functions/use-clerk.mdx.
* It also places the interfaces as shared/react/hooks/use-organization/interfaces/use-organization-return.mdx
* Group all those .mdx files under shared/react/hooks
*/
if (e.url.includes('shared/react/hooks')) {
e.url = e.url.replace(/\/[^/]+\/(functions|interfaces)\//, '/');
e.model.url = e.url;
}

return e;
});
});

app.renderer.on(MarkdownPageEvent.END, output => {
const fileName = output.url.split('/').pop();
const linkReplacements = getRelativeLinkReplacements();

for (const { pattern, replace } of linkReplacements) {
if (output.contents) {
output.contents = output.contents.replace(pattern, replace);
}
}

if (fileName) {
if (FILES_WITHOUT_HEADINGS.includes(fileName)) {
if (output.contents) {
// Remove any headings from the file, irrespective of the level
output.contents = output.contents.replace(/^#+\s.+/gm, '');
}
}
}
});
}
112 changes: 110 additions & 2 deletions .typedoc/custom-theme.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @ts-check
import { ReflectionKind } from 'typedoc';
import { ArrayType, IntersectionType, ReflectionKind, ReflectionType, UnionType } from 'typedoc';
import { MarkdownTheme, MarkdownThemeContext } from 'typedoc-plugin-markdown';

/**
Expand Down Expand Up @@ -36,7 +36,7 @@ class ClerkMarkdownThemeContext extends MarkdownThemeContext {
this.partials = {
...superPartials,
/**
* Copied from default theme / source code. This hides the return type from the output
* Copied from default theme / source code. This hides the return type heading over the table from the output
* https://github.com/typedoc2md/typedoc-plugin-markdown/blob/179a54c502b318cd4f3951e5e8b90f7f7a4752d8/packages/typedoc-plugin-markdown/src/theme/context/partials/member.signatureReturns.ts
* @param {import('typedoc').SignatureReflection} model
* @param {{ headingLevel: number }} options
Expand Down Expand Up @@ -235,6 +235,114 @@ class ClerkMarkdownThemeContext extends MarkdownThemeContext {

md.push(this.partials.body(model, { headingLevel: options.headingLevel }));

return md.join('\n\n');
},
/**
* Copied from default theme / source code. This hides the "Type parameters" section and the declaration title from the output
* https://github.com/typedoc2md/typedoc-plugin-markdown/blob/e798507a3c04f9ddf7710baf4cc7836053e438ff/packages/typedoc-plugin-markdown/src/theme/context/partials/member.declaration.ts
* @param {import('typedoc').DeclarationReflection} model
* @param {{ headingLevel: number, nested?: boolean }} options
*/
declaration: (model, options = { headingLevel: 2, nested: false }) => {
const md = [];

const opts = {
nested: false,
...options,
};

if (!opts.nested && model.sources && !this.options.getValue('disableSources')) {
md.push(this.partials.sources(model));
}

if (model?.documents) {
md.push(
this.partials.documents(model, {
headingLevel: options.headingLevel,
}),
);
}

/**
* @type any
*/
const modelType = model.type;
/**
* @type {import('typedoc').DeclarationReflection}
*/
let typeDeclaration = modelType?.declaration;

if (model.type instanceof ArrayType && model.type?.elementType instanceof ReflectionType) {
typeDeclaration = model.type?.elementType?.declaration;
}

const hasTypeDeclaration =
Boolean(typeDeclaration) ||
(model.type instanceof UnionType && model.type?.types.some(type => type instanceof ReflectionType));

if (model.comment) {
md.push(
this.partials.comment(model.comment, {
headingLevel: opts.headingLevel,
showSummary: true,
showTags: false,
}),
);
}

if (model.type instanceof IntersectionType) {
model.type?.types?.forEach(intersectionType => {
if (intersectionType instanceof ReflectionType && !intersectionType.declaration.signatures) {
if (intersectionType.declaration.children) {
md.push(heading(opts.headingLevel, this.i18n.theme_type_declaration()));

md.push(
this.partials.typeDeclaration(intersectionType.declaration, {
headingLevel: opts.headingLevel,
}),
);
}
}
});
}

if (hasTypeDeclaration) {
if (model.type instanceof UnionType) {
if (this.helpers.hasUsefulTypeDetails(model.type)) {
md.push(heading(opts.headingLevel, this.i18n.theme_type_declaration()));

model.type.types.forEach(type => {
if (type instanceof ReflectionType) {
md.push(this.partials.someType(type, { forceCollapse: true }));
md.push(this.partials.typeDeclarationContainer(model, type.declaration, options));
} else {
md.push(`${this.partials.someType(type)}`);
}
});
}
} else {
const useHeading =
typeDeclaration?.children?.length &&
(model.kind !== ReflectionKind.Property || this.helpers.useTableFormat('properties'));
if (useHeading) {
md.push(heading(opts.headingLevel, this.i18n.theme_type_declaration()));
}
md.push(this.partials.typeDeclarationContainer(model, typeDeclaration, options));
}
}
if (model.comment) {
md.push(
this.partials.comment(model.comment, {
headingLevel: opts.headingLevel,
showSummary: false,
showTags: true,
showReturns: true,
}),
);
}

md.push(this.partials.inheritance(model, { headingLevel: opts.headingLevel }));

return md.join('\n\n');
},
};
Expand Down
8 changes: 8 additions & 0 deletions .typedoc/typedoc-prettier-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"printWidth": 120,
"useTabs": false,
"bracketSpacing": true
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"test:integration:tanstack-start": "E2E_APP_ID=tanstack.start pnpm test:integration:base --grep @tanstack-start",
"test:integration:vue": "E2E_APP_ID=vue.vite pnpm test:integration:base --grep @vue",
"turbo:clean": "turbo daemon clean",
"typedoc:generate": "typedoc --tsconfig tsconfig.typedoc.json",
"typedoc:generate": "pnpm build:declarations && typedoc --tsconfig tsconfig.typedoc.json",
"version-packages": "changeset version && pnpm install --lockfile-only --engine-strict=false",
"version-packages:canary": "./scripts/canary.mjs",
"version-packages:snapshot": "./scripts/snapshot.mjs",
Expand Down
1 change: 1 addition & 0 deletions packages/react/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/*/
!/src/
!/docs/
43 changes: 43 additions & 0 deletions packages/react/docs/use-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!-- #region nextjs-01 -->

```tsx {{ filename: 'app/external-data/page.tsx' }}
'use client';

import { useAuth } from '@clerk/nextjs';

export default function ExternalDataPage() {
const { userId, sessionId, getToken, isLoaded, isSignedIn } = useAuth();

const fetchExternalData = async () => {
const token = await getToken();

// Fetch data from an external API
const response = await fetch('https://api.example.com/data', {
headers: {
Authorization: `Bearer ${token}`,
},
});

return response.json();
};

if (!isLoaded) {
return <div>Loading...</div>;
}

if (!isSignedIn) {
return <div>Sign in to view this page</div>;
}

return (
<div>
<p>
Hello, {userId}! Your current active session is {sessionId}.
</p>
<button onClick={fetchExternalData}>Fetch Data</button>
</div>
);
}
```

<!-- #endregion nextjs-01 -->
20 changes: 20 additions & 0 deletions packages/react/docs/use-sign-in.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!-- #region nextjs-01 -->

```tsx {{ filename: 'app/sign-in/page.tsx' }}
'use client';

import { useSignIn } from '@clerk/nextjs';

export default function SignInPage() {
const { isLoaded, signIn } = useSignIn();

if (!isLoaded) {
// Handle loading state
return null;
}

return <div>The current sign-in attempt status is {signIn?.status}.</div>;
}
```

<!-- #endregion nextjs-01 -->
20 changes: 20 additions & 0 deletions packages/react/docs/use-sign-up.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!-- #region nextjs-01 -->

```tsx {{ filename: 'app/sign-up/page.tsx' }}
'use client';

import { useSignUp } from '@clerk/nextjs';

export default function SignUpPage() {
const { isLoaded, signUp } = useSignUp();

if (!isLoaded) {
// Handle loading state
return null;
}

return <div>The current sign-up attempt status is {signUp?.status}.</div>;
}
```

<!-- #endregion nextjs-01 -->
11 changes: 11 additions & 0 deletions packages/react/src/hooks/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import { createGetToken, createSignOut } from './utils';
*
* The following example demonstrates how to use the `useAuth()` hook to access the current auth state, like whether the user is signed in or not. It also includes a basic example for using the `getToken()` method to retrieve a session token for fetching data from an external resource.
*
* <Tabs items='React,Next.js'>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you use these MDX components (they are not resolved, just passed along) in such way, they won't be rendered in IntelliSense

* <Tab>
*
* ```tsx {{ filename: 'src/pages/ExternalDataPage.tsx' }}
* import { useAuth } from '@clerk/clerk-react'
*
Expand Down Expand Up @@ -58,6 +61,14 @@ import { createGetToken, createSignOut } from './utils';
* )
* }
* ```
*
* </Tab>
* <Tab>
*
* {@include ../../docs/use-auth.md#nextjs-01}
*
* </Tab>
* </Tabs>
*/
export const useAuth = (initialAuthState: any = {}): UseAuthReturn => {
useAssertWrappedByClerkProvider('useAuth');
Expand Down
11 changes: 11 additions & 0 deletions packages/react/src/hooks/useSignIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvid
*
* The following example uses the `useSignIn()` hook to access the [`SignIn`](https://clerk.com/docs/references/javascript/sign-in/sign-in) object, which contains the current sign-in attempt status and methods to create a new sign-in attempt. The `isLoaded` property is used to handle the loading state.
*
* <Tabs items='React,Next.js'>
* <Tab>
*
* ```tsx {{ filename: 'src/pages/SignInPage.tsx' }}
* import { useSignIn } from '@clerk/clerk-react'
*
Expand All @@ -28,6 +31,14 @@ import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvid
* }
* ```
*
* </Tab>
* <Tab>
*
* {@include ../../docs/use-sign-in.md#nextjs-01}
*
* </Tab>
* </Tabs>
*
* @example
* ### Create a custom sign-in flow with `useSignIn()`
*
Expand Down
Loading