import { BettererFileTest } from '@betterer/betterer'; import { ESLint, Linter } from 'eslint'; import { promises as fs } from 'fs'; // Why are we ignoring these? // They're all deprecated/being removed so doesn't make sense to fix types const eslintPathsToIgnore = [ 'packages/grafana-ui/src/graveyard', // will be removed alongside angular in Grafana 12 'public/app/angular', // will be removed in Grafana 12 'public/app/plugins/panel/graph', // will be removed alongside angular in Grafana 12 'public/app/plugins/panel/table-old', // will be removed alongside angular in Grafana 12 'e2e/test-plugins', ]; // Avoid using functions that report the position of the issues, as this causes a lot of merge conflicts export default { 'better eslint': () => countEslintErrors() .include('**/*.{ts,tsx}') .exclude(new RegExp(eslintPathsToIgnore.join('|'))), 'no undocumented stories': () => countUndocumentedStories().include('**/!(*.internal).story.tsx'), 'no gf-form usage': () => regexp(/gf-form/gm, 'gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.') .include('**/*.{ts,tsx,html}') .exclude(new RegExp('packages/grafana-ui/src/themes/GlobalStyles')), }; function countUndocumentedStories() { return new BettererFileTest(async (filePaths, fileTestResult) => { await Promise.all( filePaths.map(async (filePath) => { // look for .mdx import in the story file const mdxImportRegex = new RegExp("^import.*\\.mdx';$", 'gm'); // Looks for the "autodocs" string in the file const autodocsStringRegex = /autodocs/; const fileText = await fs.readFile(filePath, 'utf8'); const hasMdxImport = mdxImportRegex.test(fileText); const hasAutodocsString = autodocsStringRegex.test(fileText); // If both .mdx import and autodocs string are missing, add an issue if (!hasMdxImport && !hasAutodocsString) { // In this case the file contents don't matter: const file = fileTestResult.addFile(filePath, ''); // Add the issue to the first character of the file: file.addIssue(0, 0, 'No undocumented stories are allowed, please add an .mdx file with some documentation'); } }) ); }); } /** * Generic regexp pattern matcher, similar to @betterer/regexp. * The only difference is that the positions of the errors are not reported, as this may cause a lot of merge conflicts. */ function regexp(pattern: RegExp, issueMessage: string) { return new BettererFileTest(async (filePaths, fileTestResult) => { await Promise.all( filePaths.map(async (filePath) => { const fileText = await fs.readFile(filePath, 'utf8'); const matches = fileText.match(pattern); if (matches) { // File contents doesn't matter, since we're not reporting the position const file = fileTestResult.addFile(filePath, ''); matches.forEach(() => { file.addIssue(0, 0, issueMessage); }); } }) ); }); } function countEslintErrors() { return new BettererFileTest(async (filePaths, fileTestResult, resolver) => { // Just bail early if there's no files to test. Prevents trying to get the base config from failing if (filePaths.length === 0) { return; } const { baseDirectory } = resolver; const baseRules: Partial = { '@typescript-eslint/no-explicit-any': 'error', '@grafana/no-aria-label-selectors': 'error', 'no-restricted-imports': [ 'error', { patterns: [ { group: ['@grafana/ui*', '*/Layout/*'], importNames: ['Layout', 'HorizontalGroup', 'VerticalGroup'], message: 'Use Stack component instead.', }, ], }, ], }; const config: Linter.Config[] = [ { files: ['**/*.{js,jsx,ts,tsx}'], rules: baseRules, }, { files: ['**/*.{ts,tsx}'], ignores: ['**/*.{test,spec}.{ts,tsx}', '**/__mocks__/**', '**/public/test/**'], rules: { '@typescript-eslint/consistent-type-assertions': ['error', { assertionStyle: 'never' }], }, }, { files: ['public/app/**/*.{ts,tsx}'], rules: { 'no-barrel-files/no-barrel-files': 'error', }, }, { files: ['public/**/*.tsx', 'packages/grafana-ui/**/*.tsx'], ignores: [ 'public/app/plugins/**', '**/*.story.tsx', '**/*.{test,spec}.{ts,tsx}', '**/__mocks__/', 'public/test', ], rules: { '@grafana/no-untranslated-strings': 'error', }, }, ]; const runner = new ESLint({ overrideConfig: config, cwd: baseDirectory, warnIgnored: false, }); const lintResults = await runner.lintFiles(Array.from(filePaths)); lintResults.forEach(({ messages, filePath }) => { const file = fileTestResult.addFile(filePath, ''); messages.forEach((message, index) => { file.addIssue(0, 0, message.message, `${index}`); }); }); }); }