Skip to content

chore(repo): Add eslint-plugin-jsdoc #5697

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions .changeset/tasty-parrots-teach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
6 changes: 6 additions & 0 deletions .typedoc/__tests__/__snapshots__/file-structure.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
"types/user-organization-invitation-resource.mdx",
"types/user-resource.mdx",
"types/without.mdx",
"shared/api-url-from-publishable-key.mdx",
Copy link
Member Author

Choose a reason for hiding this comment

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

Whenever we add JSDoc comments to previously undocumented functions we'll need to update this snapshot as Typedoc will only generate MDX files for documented, exported functions.

You can update the snapshot by running pnpm test:typedoc -u

"shared/build-clerk-js-script-attributes.mdx",
"shared/camel-to-snake.mdx",
"shared/clerk-js-script-url.mdx",
"shared/clerk-runtime-error.mdx",
"shared/create-path-matcher.mdx",
Expand All @@ -79,11 +81,13 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
"shared/fast-deep-merge-and-replace.mdx",
"shared/get-clerk-js-major-version-or-tag.mdx",
"shared/get-env-variable.mdx",
"shared/get-non-undefined-values.mdx",
"shared/get-script-url.mdx",
"shared/icon-image-url.mdx",
"shared/in-browser.mdx",
"shared/is-browser-online.mdx",
"shared/is-clerk-runtime-error.mdx",
"shared/is-ipv4-address.mdx",
"shared/is-publishable-key.mdx",
"shared/is-staging.mdx",
"shared/is-truthy.mdx",
Expand All @@ -96,6 +100,8 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
"shared/paginated-resources.mdx",
"shared/read-json-file.mdx",
"shared/set-clerk-js-loading-error-package-name.mdx",
"shared/snake-to-camel.mdx",
"shared/titleize.mdx",
"shared/to-sentence.mdx",
"shared/use-clerk.mdx",
"shared/use-organization-list-params.mdx",
Expand Down
40 changes: 26 additions & 14 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import pluginSimpleImportSort from 'eslint-plugin-simple-import-sort';
import pluginTurbo from 'eslint-plugin-turbo';
import pluginUnusedImports from 'eslint-plugin-unused-imports';
import pluginYml from 'eslint-plugin-yml';
import pluginJsDoc from 'eslint-plugin-jsdoc';
import globals from 'globals';
import tseslint from 'typescript-eslint';

Expand Down Expand Up @@ -419,21 +420,32 @@ export default tseslint.config([
'turbo/no-undeclared-env-vars': 'off',
},
},
...pluginYml.configs['flat/recommended'],
{
name: 'repo/.github',
// rules: {
// 'regex/invalid': [
// 'error',
// [
// {
// regex: '^(?!.*\\$TURBO_ARGS( |$)).*turbo \\S+',
// message: 'Invalid turbo CI command. Must contain `$TURBO_ARGS`',
// },
// ],
// ],
// },
name: 'repo/jsdoc',
...pluginJsDoc.configs['flat/recommended-typescript'],
files: ['packages/shared/src/**/*.{ts,tsx}'],
ignores: ['**/__tests__/**'],
plugins: {
jsdoc: pluginJsDoc,
},
rules: {
...pluginJsDoc.configs['flat/recommended-typescript'].rules,
'jsdoc/check-examples': 'off',
Copy link
Member Author

Choose a reason for hiding this comment

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

I don't want it to raise false positives in the examples, we want more control over it

'jsdoc/informative-docs': 'warn',
Copy link
Member Author

Choose a reason for hiding this comment

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

This ensures that this doesn't happen:

/** The user id. */
let userId;

You have to write a longer description, not restate the name

'jsdoc/check-tag-names': ['warn', { definedTags: ['inline', 'unionReturnHeadings'], typed: false }],
Copy link
Member Author

Choose a reason for hiding this comment

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

This will warn when an unknown/invalid tag name is used

'jsdoc/require-hyphen-before-param-description': 'warn',
'jsdoc/require-description': 'warn',
'jsdoc/require-description-complete-sentence': 'warn',
Copy link
Member Author

Choose a reason for hiding this comment

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

Sentences end with fullstop and start with capital letter

'jsdoc/require-param': ['warn', { ignoreWhenAllParamsMissing: true }],
'jsdoc/require-param-description': 'warn',
'jsdoc/require-returns': 'off',
Copy link
Member Author

Choose a reason for hiding this comment

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

Together with require-param I turned these off (unless you define at least one @param) since we use TypeScript types in most places. It doesn't hurt to add them but I don't think it's necessary in most places

'jsdoc/tag-lines': [
Copy link
Member Author

Choose a reason for hiding this comment

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

This ensures empty lines between tags/description, unless for multiple @param

'warn',
'always',
{ count: 1, applyToEndTag: false, startLines: 1, tags: { param: { lines: 'never' } } },
],
},
},

...pluginYml.configs['flat/recommended'],
configPrettier,
]);
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"eslint-import-resolver-typescript": "3.10.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-jest": "28.11.0",
"eslint-plugin-jsdoc": "50.6.9",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-playwright": "2.2.0",
"eslint-plugin-react": "7.37.5",
Expand Down
6 changes: 6 additions & 0 deletions packages/shared/src/apiUrlFromPublishableKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import {
} from './constants';
import { parsePublishableKey } from './keys';

/**
* Get the correct API url based on the publishable key.
*
* @param publishableKey - The publishable key to parse.
* @returns One of Clerk's API URLs.
*/
export const apiUrlFromPublishableKey = (publishableKey: string) => {
const frontendApi = parsePublishableKey(publishableKey)?.frontendApi;

Expand Down
39 changes: 34 additions & 5 deletions packages/shared/src/underscore.ts
Copy link
Member Author

Choose a reason for hiding this comment

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

I fixed this file as an exercise and check how the rules will apply

Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/**
* Converts an array of strings to a comma-separated sentence
* @param items {Array<string>}
* @returns {string} Returns a string with the items joined by a comma and the last item joined by ", or"
* Convert words to a sentence.
*
* @param items - An array of words to be joined.
* @returns A string with the items joined by a comma and the last item joined by ", or".
*/
export const toSentence = (items: string[]): string => {
// TODO: Once Safari supports it, use Intl.ListFormat
Expand All @@ -19,19 +20,41 @@ export const toSentence = (items: string[]): string => {
const IP_V4_ADDRESS_REGEX =
/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;

/**
* Checks if a string is a valid IPv4 address.
*
* @returns True if the string is a valid IPv4 address, false otherwise.
*/
export function isIPV4Address(str: string | undefined | null): boolean {
return IP_V4_ADDRESS_REGEX.test(str || '');
}

/**
* Converts the first character of a string to uppercase.
*
* @param str - The string to be converted.
* @returns The modified string with the rest of the string unchanged.
*
* @example
* ```ts
* titleize('hello world') // 'Hello world'
* ```
*/
export function titleize(str: string | undefined | null): string {
const s = str || '';
return s.charAt(0).toUpperCase() + s.slice(1);
}

/**
* Converts a string from snake_case to camelCase.
*/
export function snakeToCamel(str: string | undefined): string {
return str ? str.replace(/([-_][a-z])/g, match => match.toUpperCase().replace(/-|_/, '')) : '';
}

/**
* Converts a string from camelCase to snake_case.
*/
export function camelToSnake(str: string | undefined): string {
return str ? str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`) : '';
}
Expand Down Expand Up @@ -73,6 +96,7 @@ const createDeepObjectTransformer = (transform: any) => {
* Transforms camelCased objects/ arrays to snake_cased.
* This function recursively traverses all objects and arrays of the passed value
* camelCased keys are removed.
*
* @function
*/
export const deepCamelToSnake = createDeepObjectTransformer(camelToSnake);
Expand All @@ -81,13 +105,15 @@ export const deepCamelToSnake = createDeepObjectTransformer(camelToSnake);
* Transforms snake_cased objects/ arrays to camelCased.
* This function recursively traverses all objects and arrays of the passed value
* camelCased keys are removed.
*
* @function
*/
export const deepSnakeToCamel = createDeepObjectTransformer(snakeToCamel);

/**
* Returns true for `true`, true, positive numbers.
* Returns false for `false`, false, 0, negative integers and anything else.
* A function to determine if a value is truthy.
*
* @returns True for `true`, true, positive numbers. False for `false`, false, 0, negative integers and anything else.
*/
export function isTruthy(value: unknown): boolean {
// Return if Boolean
Expand Down Expand Up @@ -125,6 +151,9 @@ export function isTruthy(value: unknown): boolean {
return false;
}

/**
* Get all non-undefined values from an object.
*/
export function getNonUndefinedValues<T extends object>(obj: T): Partial<T> {
return Object.entries(obj).reduce((acc, [key, value]) => {
if (value !== undefined) {
Expand Down
Loading