Files
canvas-lms/script/techdebt_stats.js
Aaron Shafovaloff 20c9e03daf remove vestiges of sinon and qunit
Change-Id: I77aedaa2a488a91b1a39e7711dc4994b237cbe65
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/381954
Reviewed-by: Aaron Shafovaloff <ashafovaloff@instructure.com>
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Michael Hulse <michael.hulse@instructure.com>
QA-Review: Aaron Shafovaloff <ashafovaloff@instructure.com>
Product-Review: Aaron Shafovaloff <ashafovaloff@instructure.com>
2025-06-20 19:26:27 +00:00

834 lines
26 KiB
JavaScript

/*
* Copyright (C) 2024 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const {execSync, exec} = require('child_process')
const util = require('util')
const path = require('path')
const {ESLint} = require('eslint')
const pluginReactCompiler = require('eslint-plugin-react-compiler')
const execAsync = util.promisify(exec)
const projectRoot = path.resolve(__dirname, '..')
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
gray: '\x1b[90m',
bold: '\x1b[1m',
white: '\x1b[37m',
}
function colorize(color, text) {
return `${colors[color]}${text}${colors.reset}`
}
function bold(text) {
return colorize('bold', text)
}
// Parse command line arguments
function parseArgs() {
const args = process.argv.slice(2)
const options = {
sections: [],
verbose: false,
help: false,
}
for (let i = 0; i < args.length; i++) {
const arg = args[i]
switch (arg) {
case '-h':
case '--help':
options.help = true
break
case '-v':
case '--verbose':
options.verbose = true
break
case '-s':
case '--section':
if (i + 1 < args.length) {
// Split by comma to support multiple sections
const sectionArg = args[i + 1]
options.sections = sectionArg.split(',').map(s => s.trim())
i++ // Skip the next argument since we used it
}
break
}
}
return options
}
function printHelp() {
console.log(`
Usage: node techdebt_stats.js [options]
Options:
-h, --help Show this help message
-v, --verbose Show all files instead of just examples
-s, --section <n> Show only specific section(s), comma-separated (e.g., skipped,proptypes)
Available sections:
skipped - Skipped tests
proptypes - PropTypes usage
defaultprops - DefaultProps usage
handlebars - Handlebars files
jquery - jQuery imports
reactdom - ReactDOM.render files
class - React class components
javascript - JavaScript files
typescript - TypeScript suppressions
outdated - Outdated packages
react-compiler - React Compiler Rule Violations
`)
process.exit(0)
}
const normalizePath = filePath => filePath.replace(/\/+/g, '/')
// Helper function to get random examples
function getRandomExamples(files, count = 3) {
if (files.length === 0) return []
if (files.length <= count) return files.map(normalizePath)
const shuffled = [...files].sort(() => 0.5 - Math.random())
return shuffled.slice(0, count).map(normalizePath)
}
async function getMatchingFiles(searchPattern, verbose = false) {
try {
const {stdout} = await execAsync(
`git ls-files "ui/" "packages/" | grep -E "${searchPattern}"`,
{cwd: projectRoot},
)
return stdout.trim().split('\n').filter(Boolean)
} catch (error) {
return []
}
}
async function getGrepMatchingFiles(filePattern, grepPattern, verbose = false) {
try {
const cmd = `git ls-files "ui/" "packages/" | grep -E "${filePattern}" | xargs grep -l "${grepPattern}"`
const {stdout} = await execAsync(cmd, {cwd: projectRoot})
return stdout.trim().split('\n').filter(Boolean)
} catch (error) {
return []
}
}
async function countAndShowFiles(searchPattern, description, verbose = false) {
try {
const files = await getMatchingFiles(searchPattern, verbose)
const fileCount = files.length
if (fileCount > 0) {
console.log(colorize('yellow', `- ${description}: ${bold(fileCount)}`))
if (verbose) {
files.sort().forEach(file => {
console.log(colorize('gray', ` ${file}`))
})
} else {
const examples = getRandomExamples(files, 3)
examples.forEach(file => {
console.log(colorize('gray', ` Example: ${file}`))
})
}
} else {
console.log(colorize('yellow', `- ${description}: ${colorize('green', 'None')}`))
}
} catch (error) {
console.error(colorize('red', `Error searching for ${description}: ${error.message}`))
}
}
async function countTsSuppressions(type, verbose = false) {
try {
const {stdout} = await execAsync(
`git ls-files "ui/" | grep -E "\\.(ts|tsx)$" | xargs grep -l "@${type}" | wc -l`,
{cwd: projectRoot},
)
return Number.parseInt(stdout.trim(), 10)
} catch (error) {
console.error(colorize('red', `Error counting @${type}: ${error.message}`))
return 0
}
}
async function getRandomTsSuppressionFiles(type, verbose = false) {
try {
const {stdout} = await execAsync(
`git ls-files "ui/" | grep -E "\\.(ts|tsx)$" | xargs grep -l "@${type}"`,
{cwd: projectRoot},
)
const files = stdout.trim().split('\n').filter(Boolean)
return getRandomExamples(files, 3)
} catch (error) {
console.error(colorize('red', `Error finding @${type} examples: ${error.message}`))
}
return []
}
async function showTsSuppressionStats(type, verbose = false) {
const count = await countTsSuppressions(type, verbose)
console.log(colorize('yellow', `- Total files with @${type}: ${bold(count)}`))
if (count > 0) {
const files = await getGrepMatchingFiles('\\.(ts|tsx)$', `@${type}`, verbose)
if (verbose) {
files.sort().forEach(file => {
console.log(colorize('gray', ` ${file}`))
})
} else {
const examples = await getRandomTsSuppressionFiles(type, verbose)
examples.forEach(file => {
console.log(colorize('gray', ` Example: ${file}`))
})
}
}
}
async function countJqueryImports(verbose = false) {
try {
const cmd =
'git ls-files "ui/" | grep -E "\\.(js|jsx|ts|tsx)$" | ' +
'xargs grep -l "from [\'\\"]jquery[\'\\"]"'
const {stdout} = await execAsync(cmd, {cwd: projectRoot})
return Number.parseInt(stdout.trim().split('\n').filter(Boolean).length, 10)
} catch (error) {
console.error(colorize('red', `Error counting jQuery imports: ${error.message}`))
return 0
}
}
async function getRandomJqueryImportFiles(verbose = false) {
try {
const cmd =
'git ls-files "ui/" | grep -E "\\.(js|jsx|ts|tsx)$" | ' +
'xargs grep -l "from [\'\\"]jquery[\'\\"]"'
const {stdout} = await execAsync(cmd, {cwd: projectRoot})
const files = stdout.trim().split('\n').filter(Boolean)
return getRandomExamples(files, 3)
} catch (error) {
console.error(colorize('red', `Error finding jQuery import examples: ${error.message}`))
}
return []
}
async function showJqueryImportStats(verbose = false) {
const count = await countJqueryImports(verbose)
console.log(colorize('yellow', `- Files with jQuery imports: ${bold(count)}`))
if (count > 0) {
if (verbose) {
const cmd =
'git ls-files "ui/" "packages/" | grep -E "\\.(js|jsx|ts|tsx)$" | ' +
'xargs grep -l "\\$\\|\\bjQuery\\b\\|\\bimport.*jquery\\b"'
const {stdout} = await execAsync(cmd, {cwd: projectRoot})
const files = stdout.trim().split('\n').filter(Boolean)
files.sort().forEach(file => {
console.log(colorize('gray', ` ${file}`))
})
} else {
const examples = await getRandomJqueryImportFiles(verbose)
examples.forEach(file => {
console.log(colorize('gray', ` Example: ${file}`))
})
}
}
}
async function countSkippedTests(verbose = false) {
try {
let itSkipFiles = []
let describeSkipFiles = []
try {
const {stdout: itSkipStdout} = await execAsync(
`git ls-files "ui/" "packages/" | xargs grep -l 'it\\.skip(' 2>/dev/null || true`,
{cwd: projectRoot},
)
itSkipFiles = itSkipStdout.trim().split('\n').filter(Boolean)
} catch (e) {
// grep returns exit code 1 when no matches found
}
try {
const {stdout: describeSkipStdout} = await execAsync(
`git ls-files "ui/" "packages/" | xargs grep -l 'describe\\.skip(' 2>/dev/null || true`,
{cwd: projectRoot},
)
describeSkipFiles = describeSkipStdout.trim().split('\n').filter(Boolean)
} catch (e) {
// grep returns exit code 1 when no matches found
}
// Combine and deduplicate files
const allFiles = [...new Set([...itSkipFiles, ...describeSkipFiles])]
const fileCount = allFiles.length
if (fileCount > 0) {
console.log(colorize('yellow', `- Total files with skipped tests: ${bold(fileCount)}`))
if (verbose) {
allFiles.sort().forEach(file => {
console.log(colorize('gray', ` ${file}`))
})
} else {
const examples = getRandomExamples(allFiles, 3)
examples.forEach(file => {
console.log(colorize('gray', ` Example: ${file}`))
})
}
} else {
console.log(
colorize('yellow', `- Total files with skipped tests: ${colorize('green', 'None')}`),
)
}
} catch (error) {
if (error.code === 1 && !error.stdout) {
// grep returns exit code 1 when no matches are found
console.log(
colorize('yellow', `- Total files with skipped tests: ${colorize('green', 'None')}`),
)
} else {
console.error(colorize('red', `Error counting skipped test files: ${error.message}`))
}
}
}
async function checkOutdatedPackages(verbose = false) {
try {
const output = execSync('npm outdated --json', {
cwd: projectRoot,
stdio: ['pipe', 'pipe', 'pipe'],
encoding: 'utf8',
}).toString()
handleOutdatedPackages(output, verbose)
} catch (error) {
// npm outdated exits with code 1 when it finds outdated packages
// This is expected behavior, so we should still try to parse the output
if (error.stdout) {
handleOutdatedPackages(error.stdout, verbose)
} else {
console.error(colorize('red', `Error running npm outdated: ${error.message}`))
if (error.stderr) {
console.error(colorize('red', `stderr: ${error.stderr}`))
}
if (error.status) {
console.error(colorize('red', `exit code: ${error.status}`))
}
if (error.signal) {
console.error(colorize('red', `signal: ${error.signal}`))
}
}
}
}
function handleOutdatedPackages(output, verbose = false) {
if (output.trim()) {
const outdatedData = JSON.parse(output)
const majorOutdated = []
for (const packageName in outdatedData) {
const pkg = outdatedData[packageName]
// Skip if we don't have all the version information
if (!pkg.current || !pkg.latest) continue
const currentMajor = Number.parseInt((pkg.current || '0').split('.')[0], 10)
const latestMajor = Number.parseInt((pkg.latest || '0').split('.')[0], 10)
if (!Number.isNaN(currentMajor) && !Number.isNaN(latestMajor) && latestMajor > currentMajor) {
majorOutdated.push({
packageName,
current: pkg.current,
wanted: pkg.wanted || pkg.current,
latest: pkg.latest,
})
}
}
if (majorOutdated.length > 0) {
console.log(
colorize('yellow', `- Packages outdated by major version: ${bold(majorOutdated.length)}`),
)
if (verbose) {
majorOutdated.forEach(pkg => {
console.log(
colorize(
'gray',
` ${pkg.packageName} (current: ${pkg.current}, wanted: ${pkg.wanted}, latest: ${pkg.latest})`,
),
)
})
} else {
// Fix: Pass the formatted string directly, not the object
const examples = getRandomExamples(
majorOutdated.map(
pkg =>
`${pkg.packageName} (current: ${pkg.current}, wanted: ${pkg.wanted}, latest: ${pkg.latest})`,
),
3,
)
examples.forEach(example => {
console.log(colorize('gray', ` Example: ${example}`))
})
}
} else {
console.log(
colorize('yellow', `- Packages outdated by major version: ${colorize('green', 'None')}`),
)
}
} else {
console.log(
colorize('yellow', `- Packages outdated by major version: ${colorize('green', 'None')}`),
)
}
}
async function countReactDomRenderFiles(verbose = false) {
try {
// Find files containing ReactDOM.render
const {stdout} = await execAsync(
`git ls-files "ui/" "packages/" | xargs grep -l "ReactDOM.render"`,
{cwd: projectRoot},
)
const files = stdout.trim().split('\n').filter(Boolean)
const fileCount = files.length
if (fileCount > 0) {
console.log(colorize('yellow', `- Total files with ReactDOM.render: ${bold(fileCount)}`))
if (verbose) {
files.sort().forEach(file => {
console.log(colorize('gray', ` ${file}`))
})
} else {
const examples = getRandomExamples(files, 3)
examples.forEach(file => {
console.log(colorize('gray', ` Example: ${file}`))
})
}
} else {
console.log(
colorize('yellow', `- Total files with ReactDOM.render: ${colorize('green', 'None')}`),
)
}
} catch (error) {
if (error.code === 1 && !error.stdout) {
// grep returns exit code 1 when no matches are found
console.log(
colorize('yellow', `- Total files with ReactDOM.render: ${colorize('green', 'None')}`),
)
} else {
console.error(colorize('red', `Error counting ReactDOM.render files: ${error.message}`))
}
}
}
async function countReactClassComponentFiles(verbose = false) {
try {
let reactComponentFiles = []
let componentFiles = []
// Find files containing class components using both patterns
try {
const {stdout: reactComponentStdout} = await execAsync(
`git ls-files "ui/" "packages/" | xargs grep -l "extends React.Component" 2>/dev/null || true`,
{cwd: projectRoot},
)
reactComponentFiles = reactComponentStdout.trim().split('\n').filter(Boolean)
} catch (e) {
// grep returns exit code 1 when no matches found
}
try {
const {stdout: componentStdout} = await execAsync(
`git ls-files "ui/" "packages/" | xargs grep -l "extends Component" 2>/dev/null || true`,
{cwd: projectRoot},
)
componentFiles = componentStdout.trim().split('\n').filter(Boolean)
} catch (e) {
// grep returns exit code 1 when no matches found
}
// Combine and deduplicate files
const allFiles = [...new Set([...reactComponentFiles, ...componentFiles])]
const fileCount = allFiles.length
if (fileCount > 0) {
console.log(colorize('yellow', `- Total files with class components: ${bold(fileCount)}`))
if (verbose) {
allFiles.sort().forEach(file => {
console.log(colorize('gray', ` ${file}`))
})
} else {
const examples = getRandomExamples(allFiles, 3)
examples.forEach(file => {
console.log(colorize('gray', ` Example: ${file}`))
})
}
} else {
console.log(
colorize('yellow', `- Total files with class components: ${colorize('green', 'None')}`),
)
}
} catch (error) {
if (error.code === 1 && !error.stdout) {
// grep returns exit code 1 when no matches are found
console.log(
colorize('yellow', `- Total files with class components: ${colorize('green', 'None')}`),
)
} else {
console.error(colorize('red', `Error counting class component files: ${error.message}`))
}
}
}
async function countPropTypesFiles(verbose = false) {
try {
const cmd =
'git ls-files "ui/" "packages/" | grep -E "\\.(js|jsx|ts|tsx)$" | ' +
'xargs grep -l "\\.propTypes\\s*="'
const {stdout} = await execAsync(cmd, {cwd: projectRoot})
return Number.parseInt(stdout.trim().split('\n').filter(Boolean).length, 10)
} catch (error) {
console.error(colorize('red', `Error counting PropTypes files: ${error.message}`))
return 0
}
}
async function getRandomPropTypesFiles(verbose = false) {
try {
const cmd =
'git ls-files "ui/" "packages/" | grep -E "\\.(js|jsx|ts|tsx)$" | ' +
'xargs grep -l "\\.propTypes\\s*="'
const {stdout} = await execAsync(cmd, {cwd: projectRoot})
const files = stdout.trim().split('\n').filter(Boolean)
return getRandomExamples(files, 3)
} catch (error) {
console.error(colorize('red', `Error finding PropTypes examples: ${error.message}`))
}
return []
}
async function showPropTypesStats(verbose = false) {
const count = await countPropTypesFiles(verbose)
console.log(colorize('yellow', `- Files with PropTypes: ${bold(count)}`))
if (count > 0) {
if (verbose) {
const cmd =
'git ls-files "ui/" "packages/" | grep -E "\\.(js|jsx)$" | ' +
'xargs grep -l "PropTypes\\."'
const {stdout} = await execAsync(cmd, {cwd: projectRoot})
const files = stdout.trim().split('\n').filter(Boolean)
files.sort().forEach(file => {
console.log(colorize('gray', ` ${file}`))
})
} else {
const examples = await getRandomPropTypesFiles(verbose)
examples.forEach(file => {
console.log(colorize('gray', ` Example: ${file}`))
})
}
}
}
async function countDefaultPropsFiles(verbose = false) {
try {
const cmd =
'git ls-files "ui/" "packages/" | grep -E "\\.(js|jsx|ts|tsx)$" | ' +
'xargs grep -l "\\.defaultProps\\s*="'
const {stdout} = await execAsync(cmd, {cwd: projectRoot})
return Number.parseInt(stdout.trim().split('\n').filter(Boolean).length, 10)
} catch (error) {
console.error(colorize('red', `Error counting defaultProps files: ${error.message}`))
return 0
}
}
async function getRandomDefaultPropsFiles(verbose = false) {
try {
const cmd =
'git ls-files "ui/" "packages/" | grep -E "\\.(js|jsx|ts|tsx)$" | ' +
'xargs grep -l "\\.defaultProps\\s*="'
const {stdout} = await execAsync(cmd, {cwd: projectRoot})
const files = stdout.trim().split('\n').filter(Boolean)
return getRandomExamples(files, 3)
} catch (error) {
console.error(colorize('red', `Error finding defaultProps examples: ${error.message}`))
}
return []
}
async function showDefaultPropsStats(verbose = false) {
const count = await countDefaultPropsFiles(verbose)
console.log(colorize('yellow', `- Files with defaultProps: ${bold(count)}`))
if (count > 0) {
if (verbose) {
const cmd =
'git ls-files "ui/" "packages/" | grep -E "\\.(js|jsx)$" | ' +
'xargs grep -l "\\.defaultProps\\s*="'
const {stdout} = await execAsync(cmd, {cwd: projectRoot})
const files = stdout.trim().split('\n').filter(Boolean)
files.sort().forEach(file => {
console.log(colorize('gray', ` ${file}`))
})
} else {
const examples = await getRandomDefaultPropsFiles(verbose)
examples.forEach(file => {
console.log(colorize('gray', ` Example: ${file}`))
})
}
}
}
// Add loading indicator helpers
function startSpinner(message) {
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
let i = 0
process.stdout.write('\r' + frames[0] + ' ' + message)
return setInterval(() => {
i = (i + 1) % frames.length
process.stdout.write('\r' + frames[i] + ' ' + message)
}, 80)
}
function stopSpinner(interval) {
clearInterval(interval)
process.stdout.write('\r\x1b[K') // Clear the line
}
function createReactCompilerESLint() {
return new ESLint({
cache: true,
baseConfig: {
plugins: {
'react-compiler': pluginReactCompiler,
},
rules: {
'react-compiler/react-compiler': 'warn',
},
},
})
}
async function countReactCompilerViolations() {
try {
const spinner = startSpinner('Analyzing files in `ui/` for react-compiler violations...')
const eslint = createReactCompilerESLint()
const results = await eslint.lintFiles(['ui/**/*.{js,jsx,ts,tsx}'])
stopSpinner(spinner)
const violations = results.flatMap(result =>
result.messages.filter(msg => msg.ruleId === 'react-compiler/react-compiler'),
)
return violations.length
} catch (error) {
console.error(colorize('red', `Error counting react-compiler violations: ${error.message}`))
return 0
}
}
async function getRandomReactCompilerViolationFiles() {
try {
const spinner = startSpinner('Finding files with react-compiler violations...')
const eslint = createReactCompilerESLint()
const results = await eslint.lintFiles(['ui/**/*.{js,jsx,ts,tsx}'])
stopSpinner(spinner)
const filesWithViolations = results
.filter(result => result.messages.some(msg => msg.ruleId === 'react-compiler/react-compiler'))
.map(result => {
const message = result.messages.find(msg => msg.ruleId === 'react-compiler/react-compiler')
return (
normalizePath(path.relative(projectRoot, result.filePath)) +
`:${message.line}:${message.column}`
)
})
if (filesWithViolations.length === 0) {
return []
}
return getRandomExamples(filesWithViolations, 3)
} catch (error) {
console.error(
colorize('red', `Error finding react-compiler violation examples: ${error.message}`),
)
return []
}
}
async function showReactCompilerViolationStats(verbose = false) {
const count = await countReactCompilerViolations()
console.log(colorize('yellow', `- Files with react-compiler violations: ${bold(count)}`))
if (count > 0) {
if (verbose) {
const spinner = startSpinner('Getting detailed list of react-compiler violations...')
const eslint = createReactCompilerESLint()
const results = await eslint.lintFiles(['ui/**/*.{js,jsx,ts,tsx}'])
stopSpinner(spinner)
const filesWithViolations = results
.filter(result =>
result.messages.some(msg => msg.ruleId === 'react-compiler/react-compiler'),
)
.map(result => {
const message = result.messages.find(
msg => msg.ruleId === 'react-compiler/react-compiler',
)
return normalizePath(
`${path.relative(projectRoot, result.filePath)}:${message.line}:${message.column}`,
)
})
.sort()
filesWithViolations.forEach(file => {
console.log(colorize('gray', ` ${file}`))
})
} else {
const examples = await getRandomReactCompilerViolationFiles()
if (examples.length > 0) {
examples.forEach(file => {
console.log(colorize('gray', ` Example: ${file}`))
})
} else {
console.log(colorize('green', `No violations found!`))
}
}
}
}
function getSectionTitle(section) {
const titles = {
class: ['React Class Component Files', '(convert to function components)'],
defaultprops: ['DefaultProps Usage', '(use default parameters/TypeScript defaults)'],
handlebars: ['Handlebars Files', '(convert to React)'],
javascript: ['JavaScript Files', '(convert to TypeScript)'],
jquery: ['JQuery Imports', '(use native DOM)'],
outdated: ['Outdated Packages', ''],
proptypes: ['PropTypes Usage', '(use TypeScript interfaces/types)'],
reactCompiler: ['React Compiler Rule Violations', ''],
reactdom: ['ReactDOM.render Files', '(convert to createRoot)'],
skipped: ['Skipped Tests', '(fix or remove)'],
typescript: ['TypeScript Suppressions', ''],
}
const [title, note] = titles[section] || [section, '']
return note
? colorize('white', bold(title)) + ' ' + colorize('gray', note)
: colorize('white', bold(title))
}
async function printDashboard() {
try {
const options = parseArgs()
if (options.help) {
printHelp()
}
console.log(bold(colorize('green', '\nTech Debt Summary\n')))
const selectedSections = options.sections
const verbose = options.verbose
if (selectedSections.length === 0 || selectedSections.includes('skipped')) {
console.log(getSectionTitle('skipped'))
await countSkippedTests(verbose)
console.log()
}
if (selectedSections.length === 0 || selectedSections.includes('reactdom')) {
console.log(getSectionTitle('reactdom'))
await countReactDomRenderFiles(verbose)
console.log()
}
if (selectedSections.length === 0 || selectedSections.includes('defaultprops')) {
console.log(getSectionTitle('defaultprops'))
await showDefaultPropsStats(verbose)
console.log()
}
if (selectedSections.length === 0 || selectedSections.includes('handlebars')) {
console.log(getSectionTitle('handlebars'))
await countAndShowFiles('\\.handlebars$', 'Total Handlebars files', verbose)
console.log()
}
if (selectedSections.length === 0 || selectedSections.includes('class')) {
console.log(getSectionTitle('class'))
await countReactClassComponentFiles(verbose)
console.log()
}
if (selectedSections.length === 0 || selectedSections.includes('proptypes')) {
console.log(getSectionTitle('proptypes'))
await showPropTypesStats(verbose)
console.log()
}
if (selectedSections.length === 0 || selectedSections.includes('jquery')) {
console.log(getSectionTitle('jquery'))
await showJqueryImportStats(verbose)
console.log()
}
if (selectedSections.length === 0 || selectedSections.includes('javascript')) {
console.log(getSectionTitle('javascript'))
await countAndShowFiles('\\.(js|jsx)$', 'Total JavaScript files', verbose)
console.log()
}
if (selectedSections.length === 0 || selectedSections.includes('typescript')) {
console.log(getSectionTitle('typescript'))
await showTsSuppressionStats('ts-nocheck', verbose)
await showTsSuppressionStats('ts-ignore', verbose)
await showTsSuppressionStats('ts-expect-error', verbose)
console.log()
}
if (selectedSections.length === 0 || selectedSections.includes('outdated')) {
console.log(getSectionTitle('outdated'))
await checkOutdatedPackages(verbose)
console.log()
}
if (selectedSections.includes('react-compiler')) {
console.log(getSectionTitle('reactCompiler'))
await showReactCompilerViolationStats(verbose)
}
} catch (error) {
console.error(colorize('red', `Error: ${error.message}`))
process.exit(1)
}
}
printDashboard().catch(error => {
console.error(colorize('red', `Error: ${error.message}`))
process.exit(1)
})