diff --git a/.changeset/few-years-stare.md b/.changeset/few-years-stare.md new file mode 100644 index 0000000000..95b86c83ff --- /dev/null +++ b/.changeset/few-years-stare.md @@ -0,0 +1,5 @@ +--- +'@lg-tools/meta': patch +--- + +Adds `getPackageJson` utility diff --git a/.changeset/popular-timers-jam.md b/.changeset/popular-timers-jam.md new file mode 100644 index 0000000000..6c478153d1 --- /dev/null +++ b/.changeset/popular-timers-jam.md @@ -0,0 +1,5 @@ +--- +'@lg-tools/eslint-plugin': minor +--- + +First pre-release of `eslint-plugin` diff --git a/.changeset/rude-scissors-deliver.md b/.changeset/rude-scissors-deliver.md new file mode 100644 index 0000000000..d95a0fc79b --- /dev/null +++ b/.changeset/rude-scissors-deliver.md @@ -0,0 +1,5 @@ +--- +'@lg-tools/validate': patch +--- + +Updates the logic to check whether a dependency is used in a development file diff --git a/.changeset/sour-zebras-exercise.md b/.changeset/sour-zebras-exercise.md new file mode 100644 index 0000000000..4bf2fb75fd --- /dev/null +++ b/.changeset/sour-zebras-exercise.md @@ -0,0 +1,5 @@ +--- +'@lg-tools/lint': minor +--- + +Adds `@lg-tools/eslint-plugin` diff --git a/.eslintignore b/.eslintignore index c242e49bb4..6a7e27b4d2 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,7 @@ node_modules dist lib +coverage/ packages/icon/src/glyphs/*.svg packages/icon/src/generated/*.tsx storybook-static diff --git a/charts/line-chart/package.json b/charts/line-chart/package.json index 44f57a82a2..b2a6fb442a 100644 --- a/charts/line-chart/package.json +++ b/charts/line-chart/package.json @@ -16,7 +16,7 @@ }, "dependencies": { "@leafygreen-ui/emotion": "^4.0.7", - "@leafygreen-ui/lib": "^12.0.0", + "@leafygreen-ui/lib": "^13.7.0", "@leafygreen-ui/palette": "^4.1.1", "@leafygreen-ui/tokens": "^2.11.0", "@lg-tools/storybook-utils": "^0.1.1", diff --git a/chat/rich-links/package.json b/chat/rich-links/package.json index 3c8c5ed727..f3673193f4 100644 --- a/chat/rich-links/package.json +++ b/chat/rich-links/package.json @@ -19,7 +19,7 @@ "@leafygreen-ui/emotion": "^4.0.7", "@leafygreen-ui/icon": "^12.6.0", "@leafygreen-ui/leafygreen-provider": "^3.1.12", - "@leafygreen-ui/lib": "^12.0.0", + "@leafygreen-ui/lib": "^13.7.0", "@leafygreen-ui/palette": "^4.1.0", "@leafygreen-ui/polymorphic": "^2.0.2", "@leafygreen-ui/tokens": "^2.11.0", diff --git a/tools/eslint-plugin/README.md b/tools/eslint-plugin/README.md new file mode 100644 index 0000000000..24f7a1b384 --- /dev/null +++ b/tools/eslint-plugin/README.md @@ -0,0 +1,110 @@ +# eslint-plugin-leafygreen + +Lint Rules for LeafyGreen UI + +## Installation + +You'll first need to install [ESLint](https://eslint.org/): + +```sh +yarn add -D eslint +``` + +Next, install `@lg-tools/eslint-plugin`: + +```sh +yarn add -D @lg-tools/eslint-plugin +``` + +## Usage + +Add `@lg-tools` to the plugins section of your `.eslintrc` configuration file. (You can omit the `/eslint-plugin` suffix): + +```json +{ + "plugins": ["@lg-tools"], + "extends": ["plugin:@lg-tools/internal"] +} +``` + +Optionally configure the rules you want to use under the rules section. + +```json +{ + "rules": { + "@lg-tools/some-rule": ["warn"] + } +} +``` + +## Rules + + + +⚠️ Configurations set to warn in.\ +🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\ +💡 Manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions). + +| Name                | Description | ⚠️ | 🔧 | 💡 | +| :------------------------------------------------------- | :----------------------------------------------------------------------- | :------------------ | :-- | :-- | +| [boolean-verb-prefix](docs/rules/boolean-verb-prefix.md) | Enforce prefixing boolean variables & properties with a conditional verb | ![badge-internal][] | | 💡 | +| [no-indirect-imports](docs/rules/no-indirect-imports.md) | Forbid indirect imports from `src/` or `packages/` | ![badge-internal][] | 🔧 | | +| [standard-testid](docs/rules/standard-testid.md) | Enforce a consistent prefix for hard-coded `data-testid` attributes | ![badge-internal][] | 🔧 | | + + + +## Contributing + +To create a new rule: + +### 1. Run the `create-rule` script + +```sh +yarn workspace @lg-tools/eslint-plugin run create-rule +``` + +or, with npm: + +```sh +cd tools/eslint-plugin && npm run create-rule +``` + +This will create a new file in `src/rules`, and a test file in `src/tests` + +### 2. Add AST Listeners + +Inside your new `src/rules/.ts` file, add the appropriate metadata, and write AST listeners. + +The return object of a rule's `create` method should return at least one AST listener. + +These work similar to CSS selectors, and run a function when the ESLint static analyzer hits a specific AST node. + +See [ESLint Docs](https://eslint.org/docs/latest/extend/custom-rules) for more details. + +```ts +export const exampleRule = createRule({ + // ... + create: context => { + return { + VariableDeclaration: node => { + // Executes on any variable declaration + // e.g. const myVar = 5; + }, + }; + }, +}); +``` + +## 3. Add tests + +Inside the new `src/test/.spec.ts` file, add valid and invalid test cases. + +See [ESLint RuleTester docs](https://eslint.org/docs/latest/integrate/nodejs-api#ruletester) for more details. + +### Useful Resources + +- [ESLint Docs](https://eslint.org/docs/latest/extend/custom-rules) +- [ESLint RuleTester](https://eslint.org/docs/latest/integrate/nodejs-api#ruletester) +- [ESTree Spec](https://github.com/estree/estree/blob/master/README.md) +- [JSX Spec](https://github.com/facebook/jsx/blob/main/README.md) +- [TSESLint Listener Definitions](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/utils/src/ts-eslint/Rule.ts#L293) diff --git a/tools/eslint-plugin/docs/rules/boolean-verb-prefix.md b/tools/eslint-plugin/docs/rules/boolean-verb-prefix.md new file mode 100644 index 0000000000..facdd26ac6 --- /dev/null +++ b/tools/eslint-plugin/docs/rules/boolean-verb-prefix.md @@ -0,0 +1,18 @@ +# Enforce prefixing boolean variables & properties with a conditional verb (`@lg-tools/boolean-verb-prefix`) + +⚠️ This rule _warns_ in the `internal` config. + +💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions). + + + +## Options + + + +| Name | Description | Type | Default | +| :---------------- | :-------------------------------------------------------------- | :---- | :------ | +| `additionalVerbs` | Additional verbs to allow as prefixes to boolean variable names | Array | `` | +| `allowVarNames` | Un-prefixed variable names that should be allowed | Array | `` | + + diff --git a/tools/eslint-plugin/docs/rules/no-indirect-imports.md b/tools/eslint-plugin/docs/rules/no-indirect-imports.md new file mode 100644 index 0000000000..52c382b83d --- /dev/null +++ b/tools/eslint-plugin/docs/rules/no-indirect-imports.md @@ -0,0 +1,7 @@ +# Forbid indirect imports from `src/` or `packages/` (`@lg-tools/no-indirect-imports`) + +⚠️ This rule _warns_ in the `internal` config. + +🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + + diff --git a/tools/eslint-plugin/docs/rules/standard-testid.md b/tools/eslint-plugin/docs/rules/standard-testid.md new file mode 100644 index 0000000000..fa47e111f4 --- /dev/null +++ b/tools/eslint-plugin/docs/rules/standard-testid.md @@ -0,0 +1,17 @@ +# Enforce a consistent prefix for hard-coded `data-testid` attributes (`@lg-tools/standard-testid`) + +⚠️ This rule _warns_ in the `internal` config. + +🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + + + +## Options + + + +| Name | Description | Type | Default | +| :------- | :---------------------------------- | :----- | :------ | +| `prefix` | Prefix for `data-testid` attributes | String | `lg-` | + + diff --git a/tools/eslint-plugin/package.json b/tools/eslint-plugin/package.json new file mode 100644 index 0000000000..c8a72103eb --- /dev/null +++ b/tools/eslint-plugin/package.json @@ -0,0 +1,41 @@ +{ + "name": "@lg-tools/eslint-plugin", + "version": "0.0.1", + "description": "ESLint Rules for LeafyGreen", + "keywords": [ + "eslint", + "eslintplugin", + "eslint-plugin", + "leafygreen" + ], + "main": "./dist/index.js", + "exports": "./dist/index.js", + "scripts": { + "postinstall": "npx ts-node ./scripts/buildRulesIndex.ts", + "build": "lg-internal-build-package", + "tsc": "tsc --build tsconfig.json", + "docs": "eslint-doc-generator", + "lint": "npm-run-all \"lint:*\"", + "lint:eslint-docs": "npm-run-all \"update:eslint-docs -- --check\"", + "lint:js": "eslint .", + "create-rule": "npx ts-node ./scripts/createNewRule.ts" + }, + "dependencies": { + "@typescript-eslint/types": "6.9.0", + "@typescript-eslint/utils": "6.9.0", + "eslint": "8.43.0", + "lodash": "^4.17.21" + }, + "devDependencies": { + "@lg-tools/build": "0.6.0", + "@types/eslint": "^8.44.6", + "@typescript-eslint/rule-tester": "6.9.0", + "commander": "^11.0.0", + "eslint-doc-generator": "^1.5.2", + "fs-extra": "11.1.1" + }, + "peerDependencies": { + "eslint": "^8.43.0" + }, + "license": "Apache-2.0" +} diff --git a/tools/eslint-plugin/rollup.config.mjs b/tools/eslint-plugin/rollup.config.mjs new file mode 100644 index 0000000000..6a6c680842 --- /dev/null +++ b/tools/eslint-plugin/rollup.config.mjs @@ -0,0 +1,8 @@ +import { umdConfig } from '@lg-tools/build/config/rollup.config.mjs'; + +const config = { + ...umdConfig, + external: [...umdConfig.external, /^@typescript-eslint\//], +}; + +export default [config]; diff --git a/tools/eslint-plugin/scripts/buildRulesIndex.ts b/tools/eslint-plugin/scripts/buildRulesIndex.ts new file mode 100644 index 0000000000..d3837a5619 --- /dev/null +++ b/tools/eslint-plugin/scripts/buildRulesIndex.ts @@ -0,0 +1,43 @@ +import fse from 'fs-extra'; +import path from 'path'; + +import { makeId, makeVarName } from './utils'; + +buildRulesIndexFile(); + +/** + * Utility function that creates or updates the Rules index file + * after a new Rule is created + */ +export function buildRulesIndexFile() { + const rulesDir = path.resolve(__dirname, '../src/rules'); + fse.readdir(rulesDir).then(files => { + files = files.filter(file => file !== 'index.ts'); + const importStatements = files + .map(fileName => { + const fileId = makeId(fileName.replace('.ts', '')); + return `import { ${makeVarName(fileId)} } from './${fileId}';`; + }) + .join('\n'); + + const declarations = files + .map(fileName => { + const fileId = makeId(fileName.replace('.ts', '')); + return ` '${fileId}': ${makeVarName(fileId)},`; + }) + .join('\n'); + + const indexContent = `/** + * DO NOT MODIFY THIS FILE + * ANY CHANGES WILL BE REMOVED ON THE NEXT BUILD + */ +${importStatements} + +export const rules = { +${declarations} +}; +`; + + fse.writeFile(path.resolve(rulesDir, 'index.ts'), indexContent); + }); +} diff --git a/tools/eslint-plugin/scripts/createNewRule.ts b/tools/eslint-plugin/scripts/createNewRule.ts new file mode 100644 index 0000000000..7b2d26a49e --- /dev/null +++ b/tools/eslint-plugin/scripts/createNewRule.ts @@ -0,0 +1,77 @@ +import { Command } from 'commander'; +import fse from 'fs-extra'; +import path from 'path'; + +import { buildRulesIndexFile } from './buildRulesIndex'; +import { + makeDefaultMessageId, + makeFileName, + makeId, + makeVarName, +} from './utils'; + +const cli = new Command(''); +cli.argument('', 'The name of the rule'); +cli.action(createNewRule); +cli.parse(process.argv); + +/** + * Creates a new Rule within `eslint-plugin/src/rules` + */ +function createNewRule(ruleName: string) { + const rulesDir = path.resolve(__dirname, '../src/rules'); + const testsDir = path.resolve(__dirname, '../src/tests'); + + const varName = makeVarName(ruleName); + const ruleId = makeId(ruleName); + const fileName = makeFileName(ruleName); + const msgId = makeDefaultMessageId(ruleName); + + const ruleTemplate = ` +import { createRule } from '../utils/createRule'; + +export const ${varName} = createRule({ + name: '${ruleId}', + meta: { + type: 'suggestion', + messages: { + '${msgId}': '', + }, + schema: [], + docs: { + description: '', + } + }, + defaultOptions: [], + create: context => { + return {} + } +}); +`; + + const testTemplate = ` +import { ${varName} } from '../rules/${fileName}'; + +import { ruleTester } from './utils/ruleTester.testutils'; + +ruleTester.run('${ruleId}', ${varName}, { + valid: [{ + code: \`\`, // valid code snippet + }], + invalid: [{ + code: \`\`, // code with lint errors + // output: '', // fixed code + errors: [{ + messageId: '${msgId}', + }] + }] +}); +`; + + fse.writeFileSync(path.resolve(rulesDir, `${fileName}.ts`), ruleTemplate); + fse.writeFileSync( + path.resolve(testsDir, `${fileName}.spec.ts`), + testTemplate, + ); + buildRulesIndexFile(); +} diff --git a/tools/eslint-plugin/scripts/tsconfig.json b/tools/eslint-plugin/scripts/tsconfig.json new file mode 100644 index 0000000000..4cc95120b8 --- /dev/null +++ b/tools/eslint-plugin/scripts/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "noEmit": true, + "tsBuildInfoFile": "./tsconfig.tsbuildinfo", + "incremental": true, + "target": "ES5", + "jsx": "react", + "allowJs": true, + "pretty": true, + "strictNullChecks": true, + "noUnusedLocals": false, + "esModuleInterop": true, + "strict": true, + "allowSyntheticDefaultImports": true, + "moduleResolution": "node", + "baseUrl": ".", + "skipLibCheck": true, + "resolveJsonModule": true, + } +} diff --git a/tools/eslint-plugin/scripts/utils.ts b/tools/eslint-plugin/scripts/utils.ts new file mode 100644 index 0000000000..19e30852dc --- /dev/null +++ b/tools/eslint-plugin/scripts/utils.ts @@ -0,0 +1,6 @@ +import { camelCase, kebabCase } from 'lodash'; + +export const makeVarName = (str: string) => camelCase(str) + 'Rule'; +export const makeId = (str: string) => kebabCase(str); +export const makeFileName = (str: string) => kebabCase(str); +export const makeDefaultMessageId = (str: string) => 'issue:' + camelCase(str); diff --git a/tools/eslint-plugin/src/index.ts b/tools/eslint-plugin/src/index.ts new file mode 100644 index 0000000000..25e906ef43 --- /dev/null +++ b/tools/eslint-plugin/src/index.ts @@ -0,0 +1,38 @@ +import { RuleModule } from '@typescript-eslint/utils/ts-eslint'; +import { ESLint } from 'eslint'; + +import { rules } from './rules'; + +//------------------------------------------------------------------------------ +// Plugin Definition +//------------------------------------------------------------------------------ + +type RuleKey = keyof typeof rules; + +interface Plugin extends Omit { + rules: Record>; +} + +const plugin: Plugin = { + meta: { + name: '@lg-tools/eslint-plugin', + version: '0.0.1', + }, + rules, + configs: { + internal: { + plugins: ['@lg-tools'], + rules: { + '@lg-tools/no-indirect-imports': ['error'], + '@lg-tools/boolean-verb-prefix': ['off'], + '@lg-tools/standard-testid': ['off'], + }, + }, + external: { + plugins: ['@lg-tools'], + rules: {}, + }, + }, +}; + +export default plugin; diff --git a/tools/eslint-plugin/src/rules/boolean-verb-prefix.ts b/tools/eslint-plugin/src/rules/boolean-verb-prefix.ts new file mode 100644 index 0000000000..4cd31a72e9 --- /dev/null +++ b/tools/eslint-plugin/src/rules/boolean-verb-prefix.ts @@ -0,0 +1,185 @@ +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; +import upperFirst from 'lodash/upperFirst'; + +import { createRule } from '../utils/createRule'; +import { isTestFile } from '../utils/isTestFile'; +import { RuleContext } from '../utils/RuleContext'; + +const VERBS = ['is', 'are', 'has', 'should', 'did', 'does', 'will', 'use']; +const BOOLEAN_COMPARATORS = ['===', '==', '>', '>=', '<', '<=', '!=', '!==']; + +type BooleanVerbPrefixOptions = [ + { + additionalVerbs: Array; + allowVarNames: Array; + }, +]; + +type BooleanVerbPrefixMessages = + | 'issue:ambiguousVariableName' + | 'issue:ambiguousKey' + | 'fix:addVerbIs'; + +export const booleanVerbPrefixRule = createRule< + BooleanVerbPrefixOptions, + BooleanVerbPrefixMessages +>({ + name: 'boolean-verb-prefix', + meta: { + docs: { + description: `Enforce prefixing boolean variables & properties with a conditional verb`, + }, + type: 'suggestion', + hasSuggestions: true, + messages: { + 'issue:ambiguousVariableName': + 'Ambiguous boolean variable `{{var}}`. Boolean variables must start with a verb', + 'issue:ambiguousKey': + 'Ambiguous type key `{{var}}`. Boolean props must start with a verb', + 'fix:addVerbIs': 'Prefix this variable with `is`', + }, + schema: [ + { + type: 'object', + properties: { + additionalVerbs: { + type: 'array', + description: + 'Additional verbs to allow as prefixes to boolean variable names', + default: [], + }, + allowVarNames: { + type: 'array', + description: 'Un-prefixed variable names that should be allowed', + default: [], + }, + }, + additionalProperties: false, + }, + ], + }, + defaultOptions: [ + { + additionalVerbs: [], + allowVarNames: [], + }, + ], + create: context => { + return { + VariableDeclarator: node => { + if (!isTestFile(context.filename)) { + if (node.id.type === AST_NODE_TYPES.Identifier && node.init) { + if (node.init.type === AST_NODE_TYPES.Literal) { + if (typeof node.init.value === 'boolean') { + lintBooleanDeclaration(context, node); + } + } + + if ( + node.init.type === AST_NODE_TYPES.BinaryExpression && + BOOLEAN_COMPARATORS.includes(node.init.operator) + ) { + lintBooleanDeclaration(context, node); + } + } + } + }, + TSPropertySignature: node => { + if ( + node.typeAnnotation?.typeAnnotation.type === + AST_NODE_TYPES.TSBooleanKeyword + ) { + lintInterfaceProperty(context, node); + } + }, + }; + }, +}); + +/** + * Merges default verbs with additional verbs from context + */ +function getVerbs( + context: RuleContext, +): Array { + const additionalVerbs = context.options[0]?.additionalVerbs ?? []; + return [...VERBS, ...additionalVerbs]; +} + +/** Returns the list of allowed variable names */ +function getAllowedNames( + context: RuleContext, +): Array { + const allowVarNames = context.options[0]?.allowVarNames ?? []; + return allowVarNames; +} + +function lintBooleanDeclaration( + context: RuleContext, + node: TSESTree.VariableDeclarator, +) { + const variableName = (node.id as TSESTree.Identifier).name; + + const startsWithVerb = getVerbs(context).some(verb => + variableName.startsWith(verb), + ); + + if (!startsWithVerb) { + context.report({ + node, + messageId: 'issue:ambiguousVariableName', + data: { + var: variableName, + }, + suggest: [ + { + messageId: 'fix:addVerbIs', + fix: fixer => { + return fixer.replaceText(node.id, `is${upperFirst(variableName)}`); + }, + }, + ], + }); + } +} + +function lintInterfaceProperty( + context: RuleContext, + node: TSESTree.TSPropertySignature, +) { + const propName = (() => { + if (node.key.type === AST_NODE_TYPES.Identifier) { + return node.key.name; + } else if (node.key.type === AST_NODE_TYPES.MemberExpression) { + if (node.key.property.type === AST_NODE_TYPES.Identifier) { + return node.key.property.name; + } + } + })(); + + if (propName) { + const propNameStartsWithVerb = getVerbs(context).some(verb => + propName.startsWith(verb), + ); + + const isNameAllowed = getAllowedNames(context).includes(propName); + + if (!propNameStartsWithVerb && !isNameAllowed) { + context.report({ + node, + messageId: 'issue:ambiguousKey', + data: { + var: propName, + }, + suggest: [ + { + messageId: 'fix:addVerbIs', + fix: fixer => { + return fixer.replaceText(node.key, `is${upperFirst(propName)}`); + }, + }, + ], + }); + } + } +} diff --git a/tools/eslint-plugin/src/rules/index.ts b/tools/eslint-plugin/src/rules/index.ts new file mode 100644 index 0000000000..45c8f0aa5b --- /dev/null +++ b/tools/eslint-plugin/src/rules/index.ts @@ -0,0 +1,13 @@ +/** + * DO NOT MODIFY THIS FILE + * ANY CHANGES WILL BE REMOVED ON THE NEXT BUILD + */ +import { booleanVerbPrefixRule } from './boolean-verb-prefix'; +import { noIndirectImportsRule } from './no-indirect-imports'; +import { standardTestidRule } from './standard-testid'; + +export const rules = { + 'boolean-verb-prefix': booleanVerbPrefixRule, + 'no-indirect-imports': noIndirectImportsRule, + 'standard-testid': standardTestidRule, +}; diff --git a/tools/eslint-plugin/src/rules/no-indirect-imports.ts b/tools/eslint-plugin/src/rules/no-indirect-imports.ts new file mode 100644 index 0000000000..372d27ea14 --- /dev/null +++ b/tools/eslint-plugin/src/rules/no-indirect-imports.ts @@ -0,0 +1,63 @@ +import { createRule } from '../utils/createRule'; + +export const noIndirectImportsRule = createRule({ + name: 'no-indirect-imports', + meta: { + docs: { + description: 'Forbid indirect imports from `src/` or `packages/`', + }, + fixable: 'code', + type: 'suggestion', + messages: { + 'issue:importFromPackages': 'Do not import from the `packages` directory', + 'issue:importFromSrc': "Do not import from a package's `src` directory", + }, + schema: [], + }, + defaultOptions: [], + create: context => { + return { + ImportDeclaration: node => { + const importSource = node.source.value; + + const isImportingFromPackages = importSource.includes('packages/'); + const isImportingFromSrc = importSource.startsWith('src/'); + + if (isImportingFromPackages) { + context.report({ + node, + messageId: 'issue:importFromPackages', + fix: fixer => + fixer.replaceText( + node.source, + `'${importSource.replace( + /(\.+\/)*packages/g, + '@leafygreen-ui', + )}'`, + ), + }); + } + + if (isImportingFromSrc) { + const relativePath = context.filename.split('src/')[1]; + const levelsToSrc = relativePath?.split('/').length - 1 ?? 0; + + const pathToSource = + levelsToSrc > 0 + ? new Array(levelsToSrc).fill('../').join('') + : './'; + + context.report({ + node, + messageId: 'issue:importFromSrc', + fix: fixer => + fixer.replaceText( + node.source, + `'${importSource.replace('src/', pathToSource)}'`, + ), + }); + } + }, + }; + }, +}); diff --git a/tools/eslint-plugin/src/rules/standard-testid.ts b/tools/eslint-plugin/src/rules/standard-testid.ts new file mode 100644 index 0000000000..413789e16b --- /dev/null +++ b/tools/eslint-plugin/src/rules/standard-testid.ts @@ -0,0 +1,123 @@ +import { TSESTree } from '@typescript-eslint/types'; + +import { createRule } from '../utils/createRule'; +import { isTestFile } from '../utils/isTestFile'; + +const PREFIX = 'lg-'; + +export const standardTestidRule = createRule({ + name: 'standard-testid', + meta: { + docs: { + description: + 'Enforce a consistent prefix for hard-coded `data-testid` attributes', + }, + fixable: 'code', + type: 'suggestion', + messages: { + 'issue:namespace': + 'Hard-coded `data-testid` attributes should be namespaced with {{prefix}}', + 'issue:structure': + 'Hard-coded `data-testid` attributes should match the component structure', + }, + schema: [ + { + type: 'object', + properties: { + prefix: { + type: 'string', + description: 'Prefix for `data-testid` attributes', + default: PREFIX, + }, + }, + }, + ], + }, + defaultOptions: [ + { + prefix: PREFIX, + }, + ], + create: context => { + return { + JSXAttribute: node => { + const nodeName = node.name.name; + const value = (node.value as TSESTree.Literal)?.value; + + if (typeof value !== 'string') { + return; + } + + if (nodeName === 'data-testid' && !isTestFile(context.filename)) { + lintTestIdPrefix(context, node); + } + }, + }; + }, +}); + +// eslint-disable-next-line @typescript-eslint/no-unused-vars + +type ThisRuleContext = Parameters<(typeof standardTestidRule)['create']>[0]; + +/** + * Checks whether the `data-testid` attribute starts with `lg-` + */ +function lintTestIdPrefix( + context: ThisRuleContext, + node: TSESTree.JSXAttribute, +) { + const prefix = context.options[0]?.prefix ?? PREFIX; + const value = (node.value as TSESTree.Literal)?.value; + + if (typeof value !== 'string') return; + + if (!value.startsWith(prefix)) { + context.report({ + node, + messageId: 'issue:namespace', + fix: fixer => { + return fixer.replaceText( + node.value as TSESTree.StringLiteral, + `"${prefix}${value}"`, + ); + }, + }); + } +} + +/** + * Checks whether the `data-testid` attribute is named according to its file path + */ +// function lintTestIdStructure( +// context: ThisRuleContext, +// node: TSESTree.JSXAttribute, +// ) { +// const value = (node.value as TSESTree.Literal)?.value; + +// if (typeof value !== 'string') return; + +// const relativePath = context.filename.split('packages')[1]; +// const pathSegments = uniq( +// relativePath +// .split('/') +// .filter(segment => segment.length > 0 && segment !== 'src') +// .map(segment => segment.replace(/(\.tsx$)/, '')) +// .map(segment => snakeCase(segment)), +// ); + +// const expectedId = ['lg', ...pathSegments].join('-'); + +// if (!value.startsWith(expectedId)) { +// context.report({ +// node, +// messageId: 'issue:structure', +// fix: fixer => { +// return fixer.replaceText( +// node.value as TSESTree.StringLiteral, +// `"${expectedId}"`, +// ); +// }, +// }); +// } +// } diff --git a/tools/eslint-plugin/src/tests/boolean-verb-prefix.spec.ts b/tools/eslint-plugin/src/tests/boolean-verb-prefix.spec.ts new file mode 100644 index 0000000000..487a048838 --- /dev/null +++ b/tools/eslint-plugin/src/tests/boolean-verb-prefix.spec.ts @@ -0,0 +1,54 @@ +import { booleanVerbPrefixRule } from '../rules/boolean-verb-prefix'; + +import { ruleTester } from './utils/ruleTester.testutils'; + +ruleTester.run( + 'tools/eslint-plugin/boolean-verb-prefix', + booleanVerbPrefixRule, + { + valid: [ + { + code: `const isDisabled = false`, + }, + { + code: `const isDisabled = getDisabled()`, + }, + { + code: `const hasLabel = !!label`, + }, + { + code: `interface MyInterface { isDisabled: boolean; }`, + }, + ], + invalid: [ + { + code: `const disabled = false`, + errors: [ + { + messageId: 'issue:ambiguousVariableName', + suggestions: [ + { + messageId: 'fix:addVerbIs', + output: `const isDisabled = false`, + }, + ], + }, + ], + }, + { + code: `interface MyInterface { disabled: boolean; }`, + errors: [ + { + messageId: 'issue:ambiguousKey', + suggestions: [ + { + messageId: 'fix:addVerbIs', + output: `interface MyInterface { isDisabled: boolean; }`, + }, + ], + }, + ], + }, + ], + }, +); diff --git a/tools/eslint-plugin/src/tests/no-indirect-imports.spec.ts b/tools/eslint-plugin/src/tests/no-indirect-imports.spec.ts new file mode 100644 index 0000000000..ab981fd3bd --- /dev/null +++ b/tools/eslint-plugin/src/tests/no-indirect-imports.spec.ts @@ -0,0 +1,48 @@ +import { noIndirectImportsRule } from '../rules/no-indirect-imports'; + +import { ruleTester } from './utils/ruleTester.testutils'; + +ruleTester.run( + 'tools/eslint-plugin/no-indirect-imports', + noIndirectImportsRule, + { + valid: [ + { + code: `import Button from '@leafygreen-ui/button'`, + }, + { + code: `import { Combobox } from '@leafygreen-ui/combobox'`, + }, + ], + invalid: [ + { + code: `import Button from 'packages/button'`, + output: `import Button from '@leafygreen-ui/button'`, + errors: [ + { + messageId: 'issue:importFromPackages', + }, + ], + }, + { + code: `import Button from '../packages/button'`, + output: `import Button from '@leafygreen-ui/button'`, + errors: [ + { + messageId: 'issue:importFromPackages', + }, + ], + }, + { + code: `import { InternalToast } from 'src/InternalToast'`, + output: `import { InternalToast } from '../InternalToast'`, + filename: 'packages/toast/src/ToastContainer/ToastContainer.tsx', + errors: [ + { + messageId: 'issue:importFromSrc', + }, + ], + }, + ], + }, +); diff --git a/tools/eslint-plugin/src/tests/standard-testid.spec.ts b/tools/eslint-plugin/src/tests/standard-testid.spec.ts new file mode 100644 index 0000000000..3d5d5c30d5 --- /dev/null +++ b/tools/eslint-plugin/src/tests/standard-testid.spec.ts @@ -0,0 +1,22 @@ +import { standardTestidRule } from '../rules/standard-testid'; + +import { ruleTester } from './utils/ruleTester.testutils'; + +ruleTester.run('tools/eslint-plugin/standard-testid', standardTestidRule, { + valid: [ + { + code: `
`, + }, + ], + invalid: [ + { + code: `
`, + output: `
`, + errors: [ + { + messageId: 'issue:namespace', + }, + ], + }, + ], +}); diff --git a/tools/eslint-plugin/src/tests/utils/ruleTester.testutils.ts b/tools/eslint-plugin/src/tests/utils/ruleTester.testutils.ts new file mode 100644 index 0000000000..130eb10def --- /dev/null +++ b/tools/eslint-plugin/src/tests/utils/ruleTester.testutils.ts @@ -0,0 +1,10 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; + +export const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, +}); diff --git a/tools/eslint-plugin/src/utils/RuleContext.d.ts b/tools/eslint-plugin/src/utils/RuleContext.d.ts new file mode 100644 index 0000000000..3995b75811 --- /dev/null +++ b/tools/eslint-plugin/src/utils/RuleContext.d.ts @@ -0,0 +1,3 @@ +export type RuleContext = Parameters< + TModule['create'] +>[0]; diff --git a/tools/eslint-plugin/src/utils/createRule.ts b/tools/eslint-plugin/src/utils/createRule.ts new file mode 100644 index 0000000000..a7e16c0594 --- /dev/null +++ b/tools/eslint-plugin/src/utils/createRule.ts @@ -0,0 +1,3 @@ +import { ESLintUtils } from '../utils/typescript-eslint'; + +export const createRule = ESLintUtils.RuleCreator(n => n); diff --git a/tools/eslint-plugin/src/utils/deepOmit.ts b/tools/eslint-plugin/src/utils/deepOmit.ts new file mode 100644 index 0000000000..4d71c8b4a4 --- /dev/null +++ b/tools/eslint-plugin/src/utils/deepOmit.ts @@ -0,0 +1,16 @@ +import isObject from 'lodash/isObject'; +import omit from 'lodash/omit'; + +export function deepOmit(obj: Record, paths: Array) { + const omittedObject: Record = omit(obj, paths); + + for (const key in omittedObject) { + const value = omittedObject[key]; + + if (isObject(value)) { + omittedObject[key] = deepOmit(omittedObject[key], paths); + } + } + + return omittedObject; +} diff --git a/tools/eslint-plugin/src/utils/isTestFile.ts b/tools/eslint-plugin/src/utils/isTestFile.ts new file mode 100644 index 0000000000..8a9842f6c4 --- /dev/null +++ b/tools/eslint-plugin/src/utils/isTestFile.ts @@ -0,0 +1,8 @@ +/** + * Returns whether the string is a spec, story or testutils file + */ +export const isTestFile = (filename: string): boolean => { + const testFileRegex = /.+\.((spec)|(story)|(testutils))\.tsx?$/; + const isTestFile = testFileRegex.test(filename); + return isTestFile; +}; diff --git a/tools/eslint-plugin/src/utils/typescript-eslint.ts b/tools/eslint-plugin/src/utils/typescript-eslint.ts new file mode 100644 index 0000000000..eef53237cd --- /dev/null +++ b/tools/eslint-plugin/src/utils/typescript-eslint.ts @@ -0,0 +1,2 @@ +// Re-exporting these to ensure they're imported consistently by rules +export { ESLintUtils, TSESLint } from '@typescript-eslint/utils'; diff --git a/tools/eslint-plugin/tsconfig.json b/tools/eslint-plugin/tsconfig.json new file mode 100644 index 0000000000..0ba1acc9d9 --- /dev/null +++ b/tools/eslint-plugin/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "@lg-tools/build/config/package.tsconfig.json", + "//": "See https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/tsconfig.build.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "composite": false, + "declaration": false, + "declarationMap": false, + "emitDeclarationOnly": false, + "outDir": "./dist", + "rootDir": "./src", + "baseUrl": ".", + "noUnusedLocals": false + }, + "include": ["src/**/*"], + "exclude": ["**/*.spec.*"], + "references": [ + { + "path": "../build" + }, + { + "path": "../meta" + } + ] +} diff --git a/tools/lint/config/eslint.config.js b/tools/lint/config/eslint.config.js index 009df6e8bf..7a5cbc919d 100644 --- a/tools/lint/config/eslint.config.js +++ b/tools/lint/config/eslint.config.js @@ -1,14 +1,14 @@ module.exports = { parser: '@babel/eslint-parser', - plugins: ['@emotion', 'simple-import-sort'], + plugins: ['@emotion', '@lg-tools', 'simple-import-sort'], extends: [ 'eslint:recommended', - 'plugin:react/recommended', - 'plugin:react-hooks/recommended', 'plugin:jest/recommended', - 'prettier', 'plugin:jsx-a11y/recommended', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', 'plugin:storybook/recommended', + 'prettier', ], parserOptions: { sourceType: 'module', diff --git a/tools/lint/package.json b/tools/lint/package.json index 218888ae10..002e6b1ef1 100644 --- a/tools/lint/package.json +++ b/tools/lint/package.json @@ -17,6 +17,7 @@ "@babel/eslint-parser": "7.22.15", "@emotion/eslint-plugin": "11.11.0", "@lg-tools/build": "^0.6.0", + "@lg-tools/eslint-plugin": "^0.0.1", "@types/cross-spawn": "6.0.2", "@typescript-eslint/eslint-plugin": "5.60.0", "@typescript-eslint/parser": "5.60.0", diff --git a/tools/test/config/jest.config.js b/tools/test/config/jest.config.js index 3213e2bb57..cc99f86a6a 100644 --- a/tools/test/config/jest.config.js +++ b/tools/test/config/jest.config.js @@ -49,7 +49,7 @@ module.exports = { testEnvironment: 'jsdom', // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped - testPathIgnorePatterns: ['/node_modules/'], + testPathIgnorePatterns: ['/node_modules/', '/dist/'], // The regexp pattern Jest uses to detect test files testRegex: '.spec.[jt]sx?$', diff --git a/tools/test/config/react17/jest.config.js b/tools/test/config/react17/jest.config.js index af4ad01e1d..c1334ff179 100644 --- a/tools/test/config/react17/jest.config.js +++ b/tools/test/config/react17/jest.config.js @@ -42,7 +42,7 @@ module.exports = { testEnvironment: 'jsdom', // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped - testPathIgnorePatterns: ['/node_modules/'], + testPathIgnorePatterns: ['/node_modules/', '/tools/eslint-plugin'], // The regexp pattern Jest uses to detect test files testRegex: '.spec.[jt]sx?$', diff --git a/turbo.json b/turbo.json index 46ca2b86f2..0014783d6c 100644 --- a/turbo.json +++ b/turbo.json @@ -5,7 +5,7 @@ "dependsOn": ["^prebuild"] }, "build": { - "dependsOn": ["^build"], + "dependsOn": ["^build", "prebuild"], "inputs": ["$TURBO_DEFAULT$", "!**/*.stories.{tsx,jsx,mdx}"], "outputs": ["dist/**", "stories.js"] }, diff --git a/yarn.lock b/yarn.lock index 9b9f0fd600..4fc6433ae3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3298,7 +3298,7 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.19.tgz#6105755d7097e0d7e22f893c3e62f143d8137bd0" integrity sha512-z69jhyG20Gq4QL5JKPLqUT+eREuqnDAFItLbza4JCmpvUnIlY73YNjd5djlO7kBiiZnvTnJuAbOjIoZIOa1GjA== -"@eslint-community/eslint-utils@^4.2.0": +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== @@ -3784,15 +3784,6 @@ resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60" integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA== -"@leafygreen-ui/lib@^12.0.0": - version "12.0.0" - resolved "https://registry.yarnpkg.com/@leafygreen-ui/lib/-/lib-12.0.0.tgz#1ab22c541435e6c0060e21d7097dd65892da24a1" - integrity sha512-nhaxi4oBesnizxO0YK7XwcmiLL9U5QuN7lkZdWGDdmoJgNNL+aRju4W5vmZc7vcazSHfr3gAL+NFAGaAuopyRA== - dependencies: - "@storybook/csf" "^0.1.0" - lodash "^4.17.21" - prop-types "^15.7.2" - "@lg-tools/build@0.5.0": version "0.5.0" resolved "https://registry.yarnpkg.com/@lg-tools/build/-/build-0.5.0.tgz#b24c35f5845fc368dac603cf178ab95535a9e51f" @@ -5894,6 +5885,14 @@ "@types/estree" "*" "@types/json-schema" "*" +"@types/eslint@^8.44.6": + version "8.56.12" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.12.tgz#1657c814ffeba4d2f84c0d4ba0f44ca7ea1ca53a" + integrity sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + "@types/estree@*", "@types/estree@^1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" @@ -6062,6 +6061,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== +"@types/json-schema@^7.0.12": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -6355,6 +6359,17 @@ "@typescript-eslint/typescript-estree" "5.60.0" debug "^4.3.4" +"@typescript-eslint/rule-tester@6.9.0": + version "6.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/rule-tester/-/rule-tester-6.9.0.tgz#f7e74d520621c6c27c76df78227b50fafca3e285" + integrity sha512-hkgcWEAFxQnQZ2GoBxXp8hJtjCStUDnws1oEvERq3bExkC+IAKd36xG42Y5wMbkDmL5dIKiuO2zSX0Zbn7q6Cw== + dependencies: + "@typescript-eslint/typescript-estree" "6.9.0" + "@typescript-eslint/utils" "6.9.0" + ajv "^6.10.0" + lodash.merge "4.6.2" + semver "^7.5.4" + "@typescript-eslint/scope-manager@5.60.0": version "5.60.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.60.0.tgz#ae511967b4bd84f1d5e179bb2c82857334941c1c" @@ -6371,6 +6386,14 @@ "@typescript-eslint/types" "5.62.0" "@typescript-eslint/visitor-keys" "5.62.0" +"@typescript-eslint/scope-manager@6.9.0": + version "6.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.9.0.tgz#2626e9a7fe0e004c3e25f3b986c75f584431134e" + integrity sha512-1R8A9Mc39n4pCCz9o79qRO31HGNDvC7UhPhv26TovDsWPBDx+Sg3rOZdCELIA3ZmNoWAuxaMOT7aWtGRSYkQxw== + dependencies: + "@typescript-eslint/types" "6.9.0" + "@typescript-eslint/visitor-keys" "6.9.0" + "@typescript-eslint/type-utils@5.60.0": version "5.60.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.60.0.tgz#69b09087eb12d7513d5b07747e7d47f5533aa228" @@ -6391,6 +6414,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== +"@typescript-eslint/types@6.9.0": + version "6.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.9.0.tgz#86a0cbe7ac46c0761429f928467ff3d92f841098" + integrity sha512-+KB0lbkpxBkBSiVCuQvduqMJy+I1FyDbdwSpM3IoBS7APl4Bu15lStPjgBIdykdRqQNYqYNMa8Kuidax6phaEw== + "@typescript-eslint/typescript-estree@5.60.0": version "5.60.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.0.tgz#4ddf1a81d32a850de66642d9b3ad1e3254fb1600" @@ -6417,6 +6445,19 @@ semver "^7.3.7" tsutils "^3.21.0" +"@typescript-eslint/typescript-estree@6.9.0": + version "6.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.9.0.tgz#d0601b245be873d8fe49f3737f93f8662c8693d4" + integrity sha512-NJM2BnJFZBEAbCfBP00zONKXvMqihZCrmwCaik0UhLr0vAgb6oguXxLX1k00oQyD+vZZ+CJn3kocvv2yxm4awQ== + dependencies: + "@typescript-eslint/types" "6.9.0" + "@typescript-eslint/visitor-keys" "6.9.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + "@typescript-eslint/utils@5.60.0": version "5.60.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.60.0.tgz#4667c5aece82f9d4f24a667602f0f300864b554c" @@ -6431,7 +6472,20 @@ eslint-scope "^5.1.1" semver "^7.3.7" -"@typescript-eslint/utils@^5.10.0", "@typescript-eslint/utils@^5.45.0": +"@typescript-eslint/utils@6.9.0": + version "6.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.9.0.tgz#5bdac8604fca4823f090e4268e681c84d3597c9f" + integrity sha512-5Wf+Jsqya7WcCO8me504FBigeQKVLAMPmUzYgDbWchINNh1KJbxCgVya3EQ2MjvJMVeXl3pofRmprqX6mfQkjQ== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "6.9.0" + "@typescript-eslint/types" "6.9.0" + "@typescript-eslint/typescript-estree" "6.9.0" + semver "^7.5.4" + +"@typescript-eslint/utils@^5.10.0", "@typescript-eslint/utils@^5.38.1", "@typescript-eslint/utils@^5.45.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ== @@ -6461,6 +6515,14 @@ "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" +"@typescript-eslint/visitor-keys@6.9.0": + version "6.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.9.0.tgz#cc69421c10c4ac997ed34f453027245988164e80" + integrity sha512-dGtAfqjV6RFOtIP8I0B4ZTBRrlTT8NHHlZZSchQx3qReaoDeXhYM++M4So2AgFK9ZB0emRPA6JI1HkafzA2Ibg== + dependencies: + "@typescript-eslint/types" "6.9.0" + eslint-visitor-keys "^3.4.1" + "@vue/compiler-core@3.3.4": version "3.3.4" resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.3.4.tgz#7fbf591c1c19e1acd28ffd284526e98b4f581128" @@ -6805,6 +6867,16 @@ ajv@^8.0.0, ajv@^8.9.0: require-from-string "^2.0.2" uri-js "^4.2.2" +ajv@^8.11.2: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + ansi-colors@^4.1.1, ansi-colors@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" @@ -7365,6 +7437,11 @@ boolbase@^1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== +boolean@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b" + integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw== + bplist-parser@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/bplist-parser/-/bplist-parser-0.2.0.tgz#43a9d183e5bf9d545200ceac3e712f79ebbe8d0e" @@ -7801,6 +7878,11 @@ comma-separated-tokens@^2.0.0: resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== +commander@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + commander@^11.0.0: version "11.0.0" resolved "https://registry.yarnpkg.com/commander/-/commander-11.0.0.tgz#43e19c25dbedc8256203538e8d7e9346877a6f67" @@ -7960,6 +8042,16 @@ cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: path-type "^4.0.0" yaml "^1.10.0" +cosmiconfig@^8.0.0: + version "8.3.6" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" + integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== + dependencies: + import-fresh "^3.3.0" + js-yaml "^4.1.0" + parse-json "^5.2.0" + path-type "^4.0.0" + cosmiconfig@^8.1.3, cosmiconfig@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.2.0.tgz#f7d17c56a590856cd1e7cee98734dca272b0d8fd" @@ -8556,6 +8648,13 @@ dot-case@^3.0.4: no-case "^3.0.4" tslib "^2.0.3" +dot-prop@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-7.2.0.tgz#468172a3529779814d21a779c1ba2f6d76609809" + integrity sha512-Ol/IPXUARn9CSbkrdV4VJo7uCy1I3VuSiWCaFSg+8BdUOzF9n3jefIpcgAydvUZbTdEBZs2vEiTiS9m61ssiDA== + dependencies: + type-fest "^2.11.2" + dotenv-expand@^10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37" @@ -8895,6 +8994,24 @@ eslint-config-prettier@8.8.0: resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348" integrity sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA== +eslint-doc-generator@^1.5.2: + version "1.7.1" + resolved "https://registry.yarnpkg.com/eslint-doc-generator/-/eslint-doc-generator-1.7.1.tgz#c758c802a23f9a21b2134d9c3f57d5e5c13c3aea" + integrity sha512-i1Zjl+Xcy712SZhbceCeMVaIdhbFqY27i8d7f9gyb9P/6AQNnPA0VCWynAFVGYa0hpeR5kwUI09+GBELgC2nnA== + dependencies: + "@typescript-eslint/utils" "^5.38.1" + ajv "^8.11.2" + boolean "^3.2.0" + commander "^10.0.0" + cosmiconfig "^8.0.0" + deepmerge "^4.2.2" + dot-prop "^7.2.0" + jest-diff "^29.2.1" + json-schema-traverse "^1.0.0" + markdown-table "^3.0.3" + no-case "^3.0.4" + type-fest "^3.0.0" + eslint-import-resolver-node@^0.3.7: version "0.3.8" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.8.tgz#be719e72f5e96dcef7a60f74147c842db0c74b06" @@ -9313,6 +9430,11 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-uri@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.3.tgz#892a1c91802d5d7860de728f18608a0573142241" + integrity sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw== + fastq@^1.6.0: version "1.15.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" @@ -10192,7 +10314,7 @@ immutable@^4.0.0: resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.2.tgz#f89d910f8dfb6e15c03b2cae2faaf8c1f66455fe" integrity sha512-oGXzbEDem9OOpDWZu88jGiYCvIsLHMvGw+8OXlpsvTFvIQplQbjg1B1cvKg8f7Hoch6+NGjpPsH1Fr+Mc2D1aA== -import-fresh@^3.0.0, import-fresh@^3.2.1: +import-fresh@^3.0.0, import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -11716,7 +11838,7 @@ map-or-similar@^1.5.0: resolved "https://registry.yarnpkg.com/map-or-similar/-/map-or-similar-1.5.0.tgz#6de2653174adfb5d9edc33c69d3e92a1b76faf08" integrity sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg== -markdown-table@^3.0.0: +markdown-table@^3.0.0, markdown-table@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd" integrity sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw== @@ -14307,6 +14429,11 @@ semver@^7.0.0, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semve dependencies: lru-cache "^6.0.0" +semver@^7.5.4: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -15068,6 +15195,11 @@ trough@^2.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-2.1.0.tgz#0f7b511a4fde65a46f18477ab38849b22c554876" integrity sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g== +ts-api-utils@^1.0.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" + integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== + ts-dedent@^2.0.0, ts-dedent@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5" @@ -15236,12 +15368,12 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-fest@^2.0.0, type-fest@^2.13.0, type-fest@^2.19.0, type-fest@^2.5.0, type-fest@~2.19: +type-fest@^2.0.0, type-fest@^2.11.2, type-fest@^2.13.0, type-fest@^2.19.0, type-fest@^2.5.0, type-fest@~2.19: version "2.19.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== -type-fest@^3.1.0, type-fest@^3.12.0, type-fest@^3.9.0: +type-fest@^3.0.0, type-fest@^3.1.0, type-fest@^3.12.0, type-fest@^3.9.0: version "3.13.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.13.1.tgz#bb744c1f0678bea7543a2d1ec24e83e68e8c8706" integrity sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==