From 4cbd1f335e6fb743ecd8753293418f913ab40ea4 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sat, 27 Dec 2025 15:39:39 +0000 Subject: [PATCH] refactor: modularize select component and scripts --- .../src/components/molecules/form/Select.tsx | 141 ++-------------- .../molecules/form/SelectContent.tsx | 20 +++ .../components/molecules/form/SelectGroup.tsx | 21 +++ .../components/molecules/form/SelectItem.tsx | 20 +++ .../components/molecules/form/SelectLabel.tsx | 21 +++ .../molecules/form/SelectSeparator.tsx | 12 ++ .../molecules/form/SelectTrigger.tsx | 40 +++++ .../components/molecules/form/SelectValue.tsx | 21 +++ .../code/list-large-typescript-files.ts | 150 ++--------------- .../code/list-large-typescript-files/args.ts | 52 ++++++ .../file-scanner.ts | 47 ++++++ .../code/list-large-typescript-files/types.ts | 39 +++++ .../write-summary.ts | 15 ++ .../analysis/code/reports/large-ts-files.json | 2 +- tools/generation/generate-quality-summary.ts | 156 +----------------- .../generate-quality-summary/details.ts | 80 +++++++++ .../generate-quality-summary/overview.ts | 52 ++++++ .../quality-summary.ts | 34 ++++ .../generate-quality-summary/report-reader.ts | 12 ++ .../generate-quality-summary/sections.ts | 45 +++++ 20 files changed, 558 insertions(+), 422 deletions(-) create mode 100644 frontends/nextjs/src/components/molecules/form/SelectContent.tsx create mode 100644 frontends/nextjs/src/components/molecules/form/SelectGroup.tsx create mode 100644 frontends/nextjs/src/components/molecules/form/SelectItem.tsx create mode 100644 frontends/nextjs/src/components/molecules/form/SelectLabel.tsx create mode 100644 frontends/nextjs/src/components/molecules/form/SelectSeparator.tsx create mode 100644 frontends/nextjs/src/components/molecules/form/SelectTrigger.tsx create mode 100644 frontends/nextjs/src/components/molecules/form/SelectValue.tsx mode change 100755 => 100644 tools/analysis/code/list-large-typescript-files.ts create mode 100644 tools/analysis/code/list-large-typescript-files/args.ts create mode 100644 tools/analysis/code/list-large-typescript-files/file-scanner.ts create mode 100644 tools/analysis/code/list-large-typescript-files/types.ts create mode 100644 tools/analysis/code/list-large-typescript-files/write-summary.ts create mode 100644 tools/generation/generate-quality-summary/details.ts create mode 100644 tools/generation/generate-quality-summary/overview.ts create mode 100644 tools/generation/generate-quality-summary/quality-summary.ts create mode 100644 tools/generation/generate-quality-summary/report-reader.ts create mode 100644 tools/generation/generate-quality-summary/sections.ts diff --git a/frontends/nextjs/src/components/molecules/form/Select.tsx b/frontends/nextjs/src/components/molecules/form/Select.tsx index 2da8ff406..b6a71e2bc 100644 --- a/frontends/nextjs/src/components/molecules/form/Select.tsx +++ b/frontends/nextjs/src/components/molecules/form/Select.tsx @@ -1,19 +1,18 @@ 'use client' -import { forwardRef, ReactNode } from 'react' -import { - Select as MuiSelect, - SelectProps as MuiSelectProps, - MenuItem, - MenuItemProps, - FormControl, - InputLabel, - FormHelperText, - Box, -} from '@mui/material' import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' +import { FormControl, FormHelperText, InputLabel, Select as MuiSelect, SelectProps as MuiSelectProps } from '@mui/material' +import { forwardRef, ReactNode } from 'react' + +import { SelectContent } from './SelectContent' +import { SelectGroup } from './SelectGroup' +import { SelectItem } from './SelectItem' +import type { SelectItemProps } from './SelectItem' +import { SelectLabel } from './SelectLabel' +import { SelectSeparator } from './SelectSeparator' +import { SelectTrigger } from './SelectTrigger' +import { SelectValue } from './SelectValue' -// Select wrapper with FormControl export interface SelectProps extends Omit, 'onChange'> { onValueChange?: (value: string) => void helperText?: ReactNode @@ -42,119 +41,5 @@ const Select = forwardRef( ) Select.displayName = 'Select' -// SelectTrigger (shadcn compat - wraps select display) -interface SelectTriggerProps { - children: ReactNode - className?: string -} - -const SelectTrigger = forwardRef( - ({ children, ...props }, ref) => { - return ( - - {children} - - - ) - } -) -SelectTrigger.displayName = 'SelectTrigger' - -// SelectValue (placeholder display) -interface SelectValueProps { - placeholder?: string - children?: ReactNode -} - -const SelectValue = forwardRef( - ({ placeholder, children, ...props }, ref) => { - return ( - - {children || placeholder} - - ) - } -) -SelectValue.displayName = 'SelectValue' - -// SelectContent (dropdown container - just passes children in MUI) -const SelectContent = forwardRef( - ({ children, ...props }, ref) => { - return <>{children} - } -) -SelectContent.displayName = 'SelectContent' - -// SelectItem -export interface SelectItemProps extends MenuItemProps { - textValue?: string -} - -const SelectItem = forwardRef( - ({ value, children, textValue, ...props }, ref) => { - return ( - - {children} - - ) - } -) -SelectItem.displayName = 'SelectItem' - -// SelectGroup -const SelectGroup = forwardRef( - ({ children, ...props }, ref) => { - return {children} - } -) -SelectGroup.displayName = 'SelectGroup' - -// SelectLabel -const SelectLabel = forwardRef( - ({ children, ...props }, ref) => { - return ( - - {children} - - ) - } -) -SelectLabel.displayName = 'SelectLabel' - -// SelectSeparator -const SelectSeparator = forwardRef((props, ref) => { - return -}) -SelectSeparator.displayName = 'SelectSeparator' - -export { - Select, - SelectTrigger, - SelectValue, - SelectContent, - SelectItem, - SelectGroup, - SelectLabel, - SelectSeparator, -} +export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue } +export type { SelectItemProps } diff --git a/frontends/nextjs/src/components/molecules/form/SelectContent.tsx b/frontends/nextjs/src/components/molecules/form/SelectContent.tsx new file mode 100644 index 000000000..789148c6c --- /dev/null +++ b/frontends/nextjs/src/components/molecules/form/SelectContent.tsx @@ -0,0 +1,20 @@ +'use client' + +import { forwardRef, ReactNode } from 'react' + +interface SelectContentProps { + children: ReactNode + className?: string +} + +const SelectContent = forwardRef(({ children, ...props }, ref) => { + return ( +
+ {children} +
+ ) +}) + +SelectContent.displayName = 'SelectContent' + +export { SelectContent } diff --git a/frontends/nextjs/src/components/molecules/form/SelectGroup.tsx b/frontends/nextjs/src/components/molecules/form/SelectGroup.tsx new file mode 100644 index 000000000..cdc299bbe --- /dev/null +++ b/frontends/nextjs/src/components/molecules/form/SelectGroup.tsx @@ -0,0 +1,21 @@ +'use client' + +import { Box } from '@mui/material' +import { forwardRef, ReactNode } from 'react' + +interface SelectGroupProps { + children: ReactNode + className?: string +} + +const SelectGroup = forwardRef(({ children, ...props }, ref) => { + return ( + + {children} + + ) +}) + +SelectGroup.displayName = 'SelectGroup' + +export { SelectGroup } diff --git a/frontends/nextjs/src/components/molecules/form/SelectItem.tsx b/frontends/nextjs/src/components/molecules/form/SelectItem.tsx new file mode 100644 index 000000000..3306e78b0 --- /dev/null +++ b/frontends/nextjs/src/components/molecules/form/SelectItem.tsx @@ -0,0 +1,20 @@ +'use client' + +import { MenuItem, MenuItemProps } from '@mui/material' +import { forwardRef } from 'react' + +export interface SelectItemProps extends MenuItemProps { + textValue?: string +} + +const SelectItem = forwardRef(({ value, children, ...props }, ref) => { + return ( + + {children} + + ) +}) + +SelectItem.displayName = 'SelectItem' + +export { SelectItem } diff --git a/frontends/nextjs/src/components/molecules/form/SelectLabel.tsx b/frontends/nextjs/src/components/molecules/form/SelectLabel.tsx new file mode 100644 index 000000000..88bc3eb79 --- /dev/null +++ b/frontends/nextjs/src/components/molecules/form/SelectLabel.tsx @@ -0,0 +1,21 @@ +'use client' + +import { Box } from '@mui/material' +import { forwardRef, ReactNode } from 'react' + +interface SelectLabelProps { + children: ReactNode + className?: string +} + +const SelectLabel = forwardRef(({ children, ...props }, ref) => { + return ( + + {children} + + ) +}) + +SelectLabel.displayName = 'SelectLabel' + +export { SelectLabel } diff --git a/frontends/nextjs/src/components/molecules/form/SelectSeparator.tsx b/frontends/nextjs/src/components/molecules/form/SelectSeparator.tsx new file mode 100644 index 000000000..36acac27c --- /dev/null +++ b/frontends/nextjs/src/components/molecules/form/SelectSeparator.tsx @@ -0,0 +1,12 @@ +'use client' + +import { Box } from '@mui/material' +import { forwardRef } from 'react' + +const SelectSeparator = forwardRef((props, ref) => { + return +}) + +SelectSeparator.displayName = 'SelectSeparator' + +export { SelectSeparator } diff --git a/frontends/nextjs/src/components/molecules/form/SelectTrigger.tsx b/frontends/nextjs/src/components/molecules/form/SelectTrigger.tsx new file mode 100644 index 000000000..a5a5b9823 --- /dev/null +++ b/frontends/nextjs/src/components/molecules/form/SelectTrigger.tsx @@ -0,0 +1,40 @@ +'use client' + +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' +import { Box } from '@mui/material' +import { forwardRef, ReactNode } from 'react' + +interface SelectTriggerProps { + children: ReactNode + className?: string +} + +const SelectTrigger = forwardRef(({ children, ...props }, ref) => { + return ( + + {children} + + + ) +}) + +SelectTrigger.displayName = 'SelectTrigger' + +export { SelectTrigger } diff --git a/frontends/nextjs/src/components/molecules/form/SelectValue.tsx b/frontends/nextjs/src/components/molecules/form/SelectValue.tsx new file mode 100644 index 000000000..61cb560ac --- /dev/null +++ b/frontends/nextjs/src/components/molecules/form/SelectValue.tsx @@ -0,0 +1,21 @@ +'use client' + +import { Box } from '@mui/material' +import { forwardRef, ReactNode } from 'react' + +interface SelectValueProps { + placeholder?: string + children?: ReactNode +} + +const SelectValue = forwardRef(({ placeholder, children, ...props }, ref) => { + return ( + + {children || placeholder} + + ) +}) + +SelectValue.displayName = 'SelectValue' + +export { SelectValue } diff --git a/tools/analysis/code/list-large-typescript-files.ts b/tools/analysis/code/list-large-typescript-files.ts old mode 100755 new mode 100644 index 13e4c3fde..40fecc213 --- a/tools/analysis/code/list-large-typescript-files.ts +++ b/tools/analysis/code/list-large-typescript-files.ts @@ -1,145 +1,15 @@ #!/usr/bin/env tsx -import fs from 'fs/promises' -import path from 'path' +import { parseArgs } from './list-large-typescript-files/args' +import { walkFiles } from './list-large-typescript-files/file-scanner' +import { writeSummary } from './list-large-typescript-files/write-summary' +import type { Summary } from './list-large-typescript-files/types' -interface Options { - root: string - maxLines: number - ignore: Set - outFile?: string -} - -interface FileReport { - path: string - lines: number - recommendation: string -} - -interface Summary { - root: string - maxLines: number - ignored: string[] - scanned: number - overLimit: number - timestamp: string - files: FileReport[] -} - -const DEFAULT_IGNORE = [ - 'node_modules', - '.git', - '.next', - 'dist', - 'build', - 'coverage', - 'out', - 'tmp', - '.turbo' -] - -const usage = `Usage: tsx list-large-typescript-files.ts [--root ] [--max-lines ] [--out ] [--ignore ] - -Scans the repository for .ts/.tsx files longer than the threshold and suggests splitting them into a modular, lambda-per-file structure.` - -const parseArgs = (): Options => { - const args = process.argv.slice(2) - const options: Options = { - root: process.cwd(), - maxLines: 150, - ignore: new Set(DEFAULT_IGNORE), - } - - for (let i = 0; i < args.length; i += 1) { - const arg = args[i] - switch (arg) { - case '--root': - options.root = path.resolve(args[i + 1] ?? '.') - i += 1 - break - case '--max-lines': - options.maxLines = Number(args[i + 1] ?? options.maxLines) - i += 1 - break - case '--out': - options.outFile = args[i + 1] - i += 1 - break - case '--ignore': { - const extra = (args[i + 1] ?? '') - .split(',') - .map((entry) => entry.trim()) - .filter(Boolean) - extra.forEach((entry) => options.ignore.add(entry)) - i += 1 - break - } - case '--help': - console.log(usage) - process.exit(0) - default: - if (arg.startsWith('--')) { - console.warn(`Unknown option: ${arg}`) - } - } - } - - return options -} - -const countLines = async (filePath: string): Promise => { - const content = await fs.readFile(filePath, 'utf8') - return content.split(/\r?\n/).length -} - -const shouldSkip = (segment: string, ignore: Set): boolean => ignore.has(segment) - -const walkFiles = async (options: Options): Promise<{ reports: FileReport[]; total: number }> => { - const queue: string[] = [options.root] - const reports: FileReport[] = [] - let total = 0 - - while (queue.length > 0) { - const current = queue.pop() as string - const entries = await fs.readdir(current, { withFileTypes: true }) - for (const entry of entries) { - if (shouldSkip(entry.name, options.ignore)) continue - const fullPath = path.join(current, entry.name) - if (entry.isDirectory()) { - queue.push(fullPath) - continue - } - if (!entry.name.endsWith('.ts') && !entry.name.endsWith('.tsx')) continue - total += 1 - const lines = await countLines(fullPath) - if (lines > options.maxLines) { - const relativePath = path.relative(options.root, fullPath) - reports.push({ - path: relativePath, - lines, - recommendation: 'Split into focused modules; keep one primary lambda per file and extract helpers.', - }) - } - } - } - - return { reports: reports.sort((a, b) => b.lines - a.lines), total } -} - -const writeSummary = async (summary: Summary, destination?: string) => { - if (!destination) { - console.log(JSON.stringify(summary, null, 2)) - return - } - await fs.mkdir(path.dirname(destination), { recursive: true }) - await fs.writeFile(destination, `${JSON.stringify(summary, null, 2)}\n`, 'utf8') - console.log(`Report written to ${destination}`) -} - -const main = async () => { +const buildSummary = async (): Promise => { const options = parseArgs() const { reports, total } = await walkFiles(options) - const summary: Summary = { + + return { root: options.root, maxLines: options.maxLines, ignored: Array.from(options.ignore).sort(), @@ -147,9 +17,13 @@ const main = async () => { overLimit: reports.length, timestamp: new Date().toISOString(), files: reports, + outFile: options.outFile, } +} - await writeSummary(summary, options.outFile) +const main = async () => { + const summary = await buildSummary() + await writeSummary(summary, summary.outFile) } main().catch((error) => { diff --git a/tools/analysis/code/list-large-typescript-files/args.ts b/tools/analysis/code/list-large-typescript-files/args.ts new file mode 100644 index 000000000..561042e65 --- /dev/null +++ b/tools/analysis/code/list-large-typescript-files/args.ts @@ -0,0 +1,52 @@ +import path from 'path' + +import { DEFAULT_IGNORE, Options, usage } from './types' + +const extendIgnore = (options: Options, commaSeparated?: string) => { + const extra = (commaSeparated ?? '') + .split(',') + .map((entry) => entry.trim()) + .filter(Boolean) + + extra.forEach((entry) => options.ignore.add(entry)) +} + +export const parseArgs = (): Options => { + const args = process.argv.slice(2) + const options: Options = { + root: process.cwd(), + maxLines: 150, + ignore: new Set(DEFAULT_IGNORE), + } + + for (let i = 0; i < args.length; i += 1) { + const arg = args[i] + switch (arg) { + case '--root': + options.root = path.resolve(args[i + 1] ?? '.') + i += 1 + break + case '--max-lines': + options.maxLines = Number(args[i + 1] ?? options.maxLines) + i += 1 + break + case '--out': + options.outFile = args[i + 1] + i += 1 + break + case '--ignore': + extendIgnore(options, args[i + 1]) + i += 1 + break + case '--help': + console.log(usage) + process.exit(0) + default: + if (arg.startsWith('--')) { + console.warn(`Unknown option: ${arg}`) + } + } + } + + return options +} diff --git a/tools/analysis/code/list-large-typescript-files/file-scanner.ts b/tools/analysis/code/list-large-typescript-files/file-scanner.ts new file mode 100644 index 000000000..90ebbb1a4 --- /dev/null +++ b/tools/analysis/code/list-large-typescript-files/file-scanner.ts @@ -0,0 +1,47 @@ +import fs from 'fs/promises' +import path from 'path' + +import type { FileReport, Options } from './types' + +const countLines = async (filePath: string): Promise => { + const content = await fs.readFile(filePath, 'utf8') + return content.split(/\r?\n/).length +} + +const shouldSkip = (segment: string, ignore: Set): boolean => ignore.has(segment) + +export const walkFiles = async (options: Options): Promise<{ reports: FileReport[]; total: number }> => { + const queue: string[] = [options.root] + const reports: FileReport[] = [] + let total = 0 + + while (queue.length > 0) { + const current = queue.pop() as string + const entries = await fs.readdir(current, { withFileTypes: true }) + + for (const entry of entries) { + if (shouldSkip(entry.name, options.ignore)) continue + + const fullPath = path.join(current, entry.name) + if (entry.isDirectory()) { + queue.push(fullPath) + continue + } + + if (!entry.name.endsWith('.ts') && !entry.name.endsWith('.tsx')) continue + + total += 1 + const lines = await countLines(fullPath) + if (lines > options.maxLines) { + const relativePath = path.relative(options.root, fullPath) + reports.push({ + path: relativePath, + lines, + recommendation: 'Split into focused modules; keep one primary lambda per file and extract helpers.', + }) + } + } + } + + return { reports: reports.sort((a, b) => b.lines - a.lines), total } +} diff --git a/tools/analysis/code/list-large-typescript-files/types.ts b/tools/analysis/code/list-large-typescript-files/types.ts new file mode 100644 index 000000000..8a9930684 --- /dev/null +++ b/tools/analysis/code/list-large-typescript-files/types.ts @@ -0,0 +1,39 @@ +export interface Options { + root: string + maxLines: number + ignore: Set + outFile?: string +} + +export interface FileReport { + path: string + lines: number + recommendation: string +} + +export interface Summary { + root: string + maxLines: number + ignored: string[] + scanned: number + overLimit: number + timestamp: string + files: FileReport[] + outFile?: string +} + +export const DEFAULT_IGNORE = [ + 'node_modules', + '.git', + '.next', + 'dist', + 'build', + 'coverage', + 'out', + 'tmp', + '.turbo', +] + +export const usage = `Usage: tsx list-large-typescript-files.ts [--root ] [--max-lines ] [--out ] [--ignore ] + +Scans the repository for .ts/.tsx files longer than the threshold and suggests splitting them into a modular, lambda-per-file structure.` diff --git a/tools/analysis/code/list-large-typescript-files/write-summary.ts b/tools/analysis/code/list-large-typescript-files/write-summary.ts new file mode 100644 index 000000000..69009e87d --- /dev/null +++ b/tools/analysis/code/list-large-typescript-files/write-summary.ts @@ -0,0 +1,15 @@ +import fs from 'fs/promises' +import path from 'path' + +import type { Summary } from './types' + +export const writeSummary = async (summary: Summary, destination?: string) => { + if (!destination) { + console.log(JSON.stringify(summary, null, 2)) + return + } + + await fs.mkdir(path.dirname(destination), { recursive: true }) + await fs.writeFile(destination, `${JSON.stringify(summary, null, 2)}\n`, 'utf8') + console.log(`Report written to ${destination}`) +} diff --git a/tools/analysis/code/reports/large-ts-files.json b/tools/analysis/code/reports/large-ts-files.json index f89c5ae62..45a8bd46c 100644 --- a/tools/analysis/code/reports/large-ts-files.json +++ b/tools/analysis/code/reports/large-ts-files.json @@ -14,7 +14,7 @@ ], "scanned": 1381, "overLimit": 106, - "timestamp": "2025-12-27T15:30:28.307Z", + "timestamp": "2025-12-27T15:33:16.258Z", "files": [ { "path": "frontends/nextjs/src/lib/packages/core/package-catalog.ts", diff --git a/tools/generation/generate-quality-summary.ts b/tools/generation/generate-quality-summary.ts index e8071592c..670542cc0 100644 --- a/tools/generation/generate-quality-summary.ts +++ b/tools/generation/generate-quality-summary.ts @@ -1,159 +1,5 @@ #!/usr/bin/env tsx -import { execSync } from 'child_process' -import { existsSync, readFileSync } from 'fs' - -function generateQualitySummary(): string { - let markdown = '# Quality Metrics Summary\n\n' - - const reportPath = 'quality-reports/' - const sections = [ - { - name: '๐Ÿ” Code Quality', - files: ['code-quality-reports/code-quality-reports'], - icon: '๐Ÿ“Š' - }, - { - name: '๐Ÿงช Test Coverage', - files: ['coverage-reports/coverage-metrics.json'], - icon: 'โœ“' - }, - { - name: '๐Ÿ” Security', - files: ['security-reports/security-report.json'], - icon: '๐Ÿ›ก๏ธ' - }, - { - name: '๐Ÿ“š Documentation', - files: ['documentation-reports/jsdoc-report.json'], - icon: '๐Ÿ“–' - }, - { - name: 'โšก Performance', - files: ['performance-reports/bundle-analysis.json'], - icon: '๐Ÿš€' - }, - { - name: '๐Ÿ“ฆ Dependencies', - files: ['dependency-reports/license-report.json'], - icon: '๐Ÿ“ฆ' - }, - { - name: '๐ŸŽฏ Type Safety', - files: ['type-reports/ts-strict-report.json'], - icon: 'โœ”๏ธ' - } - ] - - markdown += '## Overview\n\n' - markdown += '| Metric | Status | Details |\n' - markdown += '|--------|--------|----------|\n' - - for (const section of sections) { - let status = 'โš ๏ธ No data' - let details = 'Report not available' - - for (const file of section.files) { - const fullPath = `${reportPath}${file}` - if (existsSync(fullPath)) { - try { - const content = readFileSync(fullPath, 'utf8') - const data = JSON.parse(content) - - if (data.coverage) { - status = data.coverage >= 80 ? 'โœ… Pass' : 'โš ๏ธ Warning' - details = `${data.coverage}% coverage` - } else if (data.totalIssues !== undefined) { - status = data.critical === 0 ? 'โœ… Pass' : 'โŒ Issues' - details = `${data.totalIssues} issues (${data.critical} critical)` - } else if (data.averageCoverage) { - status = data.averageCoverage >= 70 ? 'โœ… Good' : 'โš ๏ธ Needs work' - details = `${data.averageCoverage.toFixed(1)}% documented` - } - break - } catch (e) { - // Continue to next file - } - } - } - - markdown += `| ${section.icon} ${section.name} | ${status} | ${details} |\n` - } - - markdown += '\n## Detailed Metrics\n\n' - - // Code Complexity - markdown += '### Code Complexity\n\n' - const complexityPath = `${reportPath}code-quality-reports/code-quality-reports/complexity-report.json` - if (existsSync(complexityPath)) { - try { - const complexity = JSON.parse(readFileSync(complexityPath, 'utf8')) - markdown += `- **Total files analyzed**: ${complexity.totalFilesAnalyzed}\n` - markdown += `- **Average complexity**: ${complexity.avgMaxComplexity?.toFixed(2) || 'N/A'}\n` - markdown += `- **Violations**: ${complexity.totalViolations || 0}\n\n` - } catch (e) { - markdown += 'No data available\n\n' - } - } - - // Security Issues - markdown += '### Security Scan Results\n\n' - const securityPath = `${reportPath}security-reports/security-reports/security-report.json` - if (existsSync(securityPath)) { - try { - const security = JSON.parse(readFileSync(securityPath, 'utf8')) - markdown += `- **Critical Issues**: ${security.critical || 0} โŒ\n` - markdown += `- **High Severity**: ${security.high || 0} โš ๏ธ\n` - markdown += `- **Medium Severity**: ${security.medium || 0} โ„น๏ธ\n` - markdown += `- **Total Issues**: ${security.totalIssues || 0}\n\n` - } catch (e) { - markdown += 'No data available\n\n' - } - } - - // File Size Analysis - markdown += '### File Size Metrics\n\n' - const filesizePath = `${reportPath}size-reports/size-reports/file-sizes-report.json` - if (existsSync(filesizePath)) { - try { - const filesize = JSON.parse(readFileSync(filesizePath, 'utf8')) - markdown += `- **Files analyzed**: ${filesize.totalFilesAnalyzed}\n` - markdown += `- **Violations**: ${filesize.totalViolations || 0}\n` - if (filesize.largestFiles) { - markdown += `- **Largest file**: ${filesize.largestFiles[0]?.file || 'N/A'} (${filesize.largestFiles[0]?.lines || 0} lines)\n` - } - markdown += '\n' - } catch (e) { - markdown += 'No data available\n\n' - } - } - - // Performance Metrics - markdown += '### Performance Budget\n\n' - const perfPath = `${reportPath}performance-reports/performance-reports/bundle-analysis.json` - if (existsSync(perfPath)) { - try { - const perf = JSON.parse(readFileSync(perfPath, 'utf8')) - markdown += `- **Total bundle size**: ${perf.totalBundleSize?.mb || 'N/A'}MB\n` - markdown += `- **Gzip size**: ${perf.gzipSize?.mb || 'N/A'}MB\n` - markdown += `- **Status**: ${perf.budgetStatus?.status === 'ok' ? 'โœ…' : 'โš ๏ธ'}\n\n` - } catch (e) { - markdown += 'No data available\n\n' - } - } - - markdown += '---\n\n' - markdown += '## Recommendations\n\n' - markdown += '- ๐ŸŽฏ Maintain test coverage above 80%\n' - markdown += '- ๐Ÿ“š Add JSDoc comments to exported functions\n' - markdown += '- ๐Ÿ” Address complexity warnings for better maintainability\n' - markdown += '- โšก Monitor bundle size to prevent performance degradation\n' - markdown += '- ๐Ÿ” Fix any security issues before merging\n' - markdown += '- ๐Ÿ“– Keep documentation up to date\n\n' - - markdown += `**Report generated**: ${new Date().toISOString()}\n` - - return markdown -} +import { generateQualitySummary } from './generate-quality-summary/quality-summary' console.log(generateQualitySummary()) diff --git a/tools/generation/generate-quality-summary/details.ts b/tools/generation/generate-quality-summary/details.ts new file mode 100644 index 000000000..74c6b59b6 --- /dev/null +++ b/tools/generation/generate-quality-summary/details.ts @@ -0,0 +1,80 @@ +import { BASE_REPORT_PATH } from './sections' +import { readJsonFile } from './report-reader' + +const buildCodeComplexity = (): string => { + const complexityPath = `${BASE_REPORT_PATH}code-quality-reports/code-quality-reports/complexity-report.json` + const complexity = readJsonFile(complexityPath) + + if (!complexity) return 'No data available\n\n' + + return [ + `- **Total files analyzed**: ${complexity.totalFilesAnalyzed}`, + `- **Average complexity**: ${complexity.avgMaxComplexity?.toFixed(2) || 'N/A'}`, + `- **Violations**: ${complexity.totalViolations || 0}`, + '', + ].join('\n') +} + +const buildSecurity = (): string => { + const securityPath = `${BASE_REPORT_PATH}security-reports/security-reports/security-report.json` + const security = readJsonFile(securityPath) + + if (!security) return 'No data available\n\n' + + return [ + `- **Critical Issues**: ${security.critical || 0} โŒ`, + `- **High Severity**: ${security.high || 0} โš ๏ธ`, + `- **Medium Severity**: ${security.medium || 0} โ„น๏ธ`, + `- **Total Issues**: ${security.totalIssues || 0}`, + '', + ].join('\n') +} + +const buildFileSizeMetrics = (): string => { + const filesizePath = `${BASE_REPORT_PATH}size-reports/size-reports/file-sizes-report.json` + const filesize = readJsonFile(filesizePath) + + if (!filesize) return 'No data available\n\n' + + const largest = filesize.largestFiles?.[0] + const largestLabel = largest ? `${largest.file} (${largest.lines || 0} lines)` : 'N/A' + + return [ + `- **Files analyzed**: ${filesize.totalFilesAnalyzed}`, + `- **Violations**: ${filesize.totalViolations || 0}`, + `- **Largest file**: ${largestLabel}`, + '', + ].join('\n') +} + +const buildPerformance = (): string => { + const perfPath = `${BASE_REPORT_PATH}performance-reports/performance-reports/bundle-analysis.json` + const perf = readJsonFile(perfPath) + + if (!perf) return 'No data available\n\n' + + const status = perf.budgetStatus?.status === 'ok' ? 'โœ…' : 'โš ๏ธ' + + return [ + `- **Total bundle size**: ${perf.totalBundleSize?.mb || 'N/A'}MB`, + `- **Gzip size**: ${perf.gzipSize?.mb || 'N/A'}MB`, + `- **Status**: ${status}`, + '', + ].join('\n') +} + +export const buildDetailedMetrics = (): string => { + let markdown = '### Code Complexity\n\n' + markdown += `${buildCodeComplexity()}\n` + + markdown += '### Security Scan Results\n\n' + markdown += `${buildSecurity()}\n` + + markdown += '### File Size Metrics\n\n' + markdown += `${buildFileSizeMetrics()}\n` + + markdown += '### Performance Budget\n\n' + markdown += `${buildPerformance()}\n` + + return markdown +} diff --git a/tools/generation/generate-quality-summary/overview.ts b/tools/generation/generate-quality-summary/overview.ts new file mode 100644 index 000000000..50ba31b7d --- /dev/null +++ b/tools/generation/generate-quality-summary/overview.ts @@ -0,0 +1,52 @@ +import { BASE_REPORT_PATH, QUALITY_SECTIONS, SectionDefinition } from './sections' +import { readJsonFile } from './report-reader' + +type OverviewStatus = { + status: string + details: string +} + +const selectDetails = (data: any): OverviewStatus | undefined => { + if (data?.coverage !== undefined) { + return { + status: data.coverage >= 80 ? 'โœ… Pass' : 'โš ๏ธ Warning', + details: `${data.coverage}% coverage`, + } + } + + if (data?.totalIssues !== undefined) { + return { + status: data.critical === 0 ? 'โœ… Pass' : 'โŒ Issues', + details: `${data.totalIssues} issues (${data.critical} critical)`, + } + } + + if (data?.averageCoverage !== undefined) { + return { + status: data.averageCoverage >= 70 ? 'โœ… Good' : 'โš ๏ธ Needs work', + details: `${data.averageCoverage.toFixed(1)}% documented`, + } + } + + return undefined +} + +const evaluateSection = (section: SectionDefinition): OverviewStatus => { + for (const file of section.files) { + const fullPath = `${BASE_REPORT_PATH}${file}` + const data = readJsonFile(fullPath) + const chosen = selectDetails(data) + if (chosen) return chosen + } + + return { status: 'โš ๏ธ No data', details: 'Report not available' } +} + +export const buildOverviewTable = (): string => { + const rows = QUALITY_SECTIONS.map((section) => { + const { status, details } = evaluateSection(section) + return `| ${section.icon} ${section.name} | ${status} | ${details} |\n` + }) + + return rows.join('') +} diff --git a/tools/generation/generate-quality-summary/quality-summary.ts b/tools/generation/generate-quality-summary/quality-summary.ts new file mode 100644 index 000000000..e1f95f24e --- /dev/null +++ b/tools/generation/generate-quality-summary/quality-summary.ts @@ -0,0 +1,34 @@ +import { buildDetailedMetrics } from './details' +import { buildOverviewTable } from './overview' + +const buildRecommendations = () => { + return [ + '- ๐ŸŽฏ Maintain test coverage above 80%', + '- ๐Ÿ“š Add JSDoc comments to exported functions', + '- ๐Ÿ” Address complexity warnings for better maintainability', + '- โšก Monitor bundle size to prevent performance degradation', + '- ๐Ÿ” Fix any security issues before merging', + '- ๐Ÿ“– Keep documentation up to date', + '', + ].join('\n') +} + +export const generateQualitySummary = (): string => { + let markdown = '# Quality Metrics Summary\n\n' + + markdown += '## Overview\n\n' + markdown += '| Metric | Status | Details |\n' + markdown += '|--------|--------|----------|\n' + markdown += buildOverviewTable() + + markdown += '\n## Detailed Metrics\n\n' + markdown += buildDetailedMetrics() + + markdown += '---\n\n' + markdown += '## Recommendations\n\n' + markdown += `${buildRecommendations()}\n` + + markdown += `**Report generated**: ${new Date().toISOString()}\n` + + return markdown +} diff --git a/tools/generation/generate-quality-summary/report-reader.ts b/tools/generation/generate-quality-summary/report-reader.ts new file mode 100644 index 000000000..b835639b2 --- /dev/null +++ b/tools/generation/generate-quality-summary/report-reader.ts @@ -0,0 +1,12 @@ +import { existsSync, readFileSync } from 'fs' + +export const readJsonFile = (fullPath: string): T | undefined => { + if (!existsSync(fullPath)) return undefined + + try { + return JSON.parse(readFileSync(fullPath, 'utf8')) as T + } catch (error) { + console.warn(`Failed to parse JSON report at ${fullPath}:`, error) + return undefined + } +} diff --git a/tools/generation/generate-quality-summary/sections.ts b/tools/generation/generate-quality-summary/sections.ts new file mode 100644 index 000000000..ffd54e19f --- /dev/null +++ b/tools/generation/generate-quality-summary/sections.ts @@ -0,0 +1,45 @@ +export interface SectionDefinition { + name: string + files: string[] + icon: string +} + +export const BASE_REPORT_PATH = 'quality-reports/' + +export const QUALITY_SECTIONS: SectionDefinition[] = [ + { + name: '๐Ÿ” Code Quality', + files: ['code-quality-reports/code-quality-reports'], + icon: '๐Ÿ“Š', + }, + { + name: '๐Ÿงช Test Coverage', + files: ['coverage-reports/coverage-metrics.json'], + icon: 'โœ“', + }, + { + name: '๐Ÿ” Security', + files: ['security-reports/security-report.json'], + icon: '๐Ÿ›ก๏ธ', + }, + { + name: '๐Ÿ“š Documentation', + files: ['documentation-reports/jsdoc-report.json'], + icon: '๐Ÿ“–', + }, + { + name: 'โšก Performance', + files: ['performance-reports/bundle-analysis.json'], + icon: '๐Ÿš€', + }, + { + name: '๐Ÿ“ฆ Dependencies', + files: ['dependency-reports/license-report.json'], + icon: '๐Ÿ“ฆ', + }, + { + name: '๐ŸŽฏ Type Safety', + files: ['type-reports/ts-strict-report.json'], + icon: 'โœ”๏ธ', + }, +]