Merge pull request #116 from johndoe6345789/codex/refactor-typescript-files-to-modular-structure

refactor: modularize select component and scripts
This commit is contained in:
2025-12-27 15:40:24 +00:00
committed by GitHub
20 changed files with 558 additions and 422 deletions

View File

@@ -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<MuiSelectProps<string>, 'onChange'> {
onValueChange?: (value: string) => void
helperText?: ReactNode
@@ -42,119 +41,5 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(
)
Select.displayName = 'Select'
// SelectTrigger (shadcn compat - wraps select display)
interface SelectTriggerProps {
children: ReactNode
className?: string
}
const SelectTrigger = forwardRef<HTMLDivElement, SelectTriggerProps>(
({ children, ...props }, ref) => {
return (
<Box
ref={ref}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: 1.5,
py: 1,
border: 1,
borderColor: 'divider',
borderRadius: 1,
cursor: 'pointer',
'&:hover': {
borderColor: 'text.secondary',
},
}}
{...props}
>
{children}
<KeyboardArrowDownIcon fontSize="small" sx={{ ml: 1, color: 'text.secondary' }} />
</Box>
)
}
)
SelectTrigger.displayName = 'SelectTrigger'
// SelectValue (placeholder display)
interface SelectValueProps {
placeholder?: string
children?: ReactNode
}
const SelectValue = forwardRef<HTMLSpanElement, SelectValueProps>(
({ placeholder, children, ...props }, ref) => {
return (
<Box component="span" ref={ref} sx={{ color: children ? 'text.primary' : 'text.secondary' }} {...props}>
{children || placeholder}
</Box>
)
}
)
SelectValue.displayName = 'SelectValue'
// SelectContent (dropdown container - just passes children in MUI)
const SelectContent = forwardRef<HTMLDivElement, { children: ReactNode; className?: string }>(
({ children, ...props }, ref) => {
return <>{children}</>
}
)
SelectContent.displayName = 'SelectContent'
// SelectItem
export interface SelectItemProps extends MenuItemProps {
textValue?: string
}
const SelectItem = forwardRef<HTMLLIElement, SelectItemProps>(
({ value, children, textValue, ...props }, ref) => {
return (
<MenuItem ref={ref} value={value} {...props}>
{children}
</MenuItem>
)
}
)
SelectItem.displayName = 'SelectItem'
// SelectGroup
const SelectGroup = forwardRef<HTMLDivElement, { children: ReactNode; className?: string }>(
({ children, ...props }, ref) => {
return <Box ref={ref} {...props}>{children}</Box>
}
)
SelectGroup.displayName = 'SelectGroup'
// SelectLabel
const SelectLabel = forwardRef<HTMLDivElement, { children: ReactNode; className?: string }>(
({ children, ...props }, ref) => {
return (
<Box
ref={ref}
sx={{ px: 2, py: 1, fontSize: '0.75rem', fontWeight: 600, color: 'text.secondary' }}
{...props}
>
{children}
</Box>
)
}
)
SelectLabel.displayName = 'SelectLabel'
// SelectSeparator
const SelectSeparator = forwardRef<HTMLHRElement>((props, ref) => {
return <Box ref={ref} component="hr" sx={{ my: 0.5, borderColor: 'divider' }} {...props} />
})
SelectSeparator.displayName = 'SelectSeparator'
export {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
SelectGroup,
SelectLabel,
SelectSeparator,
}
export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue }
export type { SelectItemProps }

View File

@@ -0,0 +1,20 @@
'use client'
import { forwardRef, ReactNode } from 'react'
interface SelectContentProps {
children: ReactNode
className?: string
}
const SelectContent = forwardRef<HTMLDivElement, SelectContentProps>(({ children, ...props }, ref) => {
return (
<div ref={ref} {...props}>
{children}
</div>
)
})
SelectContent.displayName = 'SelectContent'
export { SelectContent }

View File

@@ -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<HTMLDivElement, SelectGroupProps>(({ children, ...props }, ref) => {
return (
<Box ref={ref} {...props}>
{children}
</Box>
)
})
SelectGroup.displayName = 'SelectGroup'
export { SelectGroup }

View File

@@ -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<HTMLLIElement, SelectItemProps>(({ value, children, ...props }, ref) => {
return (
<MenuItem ref={ref} value={value} {...props}>
{children}
</MenuItem>
)
})
SelectItem.displayName = 'SelectItem'
export { SelectItem }

View File

@@ -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<HTMLDivElement, SelectLabelProps>(({ children, ...props }, ref) => {
return (
<Box ref={ref} sx={{ px: 2, py: 1, fontSize: '0.75rem', fontWeight: 600, color: 'text.secondary' }} {...props}>
{children}
</Box>
)
})
SelectLabel.displayName = 'SelectLabel'
export { SelectLabel }

View File

@@ -0,0 +1,12 @@
'use client'
import { Box } from '@mui/material'
import { forwardRef } from 'react'
const SelectSeparator = forwardRef<HTMLHRElement>((props, ref) => {
return <Box ref={ref} component="hr" sx={{ my: 0.5, borderColor: 'divider' }} {...props} />
})
SelectSeparator.displayName = 'SelectSeparator'
export { SelectSeparator }

View File

@@ -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<HTMLDivElement, SelectTriggerProps>(({ children, ...props }, ref) => {
return (
<Box
ref={ref}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: 1.5,
py: 1,
border: 1,
borderColor: 'divider',
borderRadius: 1,
cursor: 'pointer',
'&:hover': {
borderColor: 'text.secondary',
},
}}
{...props}
>
{children}
<KeyboardArrowDownIcon fontSize="small" sx={{ ml: 1, color: 'text.secondary' }} />
</Box>
)
})
SelectTrigger.displayName = 'SelectTrigger'
export { SelectTrigger }

View File

@@ -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<HTMLSpanElement, SelectValueProps>(({ placeholder, children, ...props }, ref) => {
return (
<Box component="span" ref={ref} sx={{ color: children ? 'text.primary' : 'text.secondary' }} {...props}>
{children || placeholder}
</Box>
)
})
SelectValue.displayName = 'SelectValue'
export { SelectValue }

150
tools/analysis/code/list-large-typescript-files.ts Executable file → Normal file
View File

@@ -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<string>
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 <path>] [--max-lines <number>] [--out <path>] [--ignore <dirA,dirB>]
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<number> => {
const content = await fs.readFile(filePath, 'utf8')
return content.split(/\r?\n/).length
}
const shouldSkip = (segment: string, ignore: Set<string>): 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<Summary> => {
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) => {

View File

@@ -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
}

View File

@@ -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<number> => {
const content = await fs.readFile(filePath, 'utf8')
return content.split(/\r?\n/).length
}
const shouldSkip = (segment: string, ignore: Set<string>): 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 }
}

View File

@@ -0,0 +1,39 @@
export interface Options {
root: string
maxLines: number
ignore: Set<string>
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 <path>] [--max-lines <number>] [--out <path>] [--ignore <dirA,dirB>]
Scans the repository for .ts/.tsx files longer than the threshold and suggests splitting them into a modular, lambda-per-file structure.`

View File

@@ -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}`)
}

View File

@@ -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",

View File

@@ -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())

View File

@@ -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<any>(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<any>(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<any>(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<any>(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
}

View File

@@ -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('')
}

View File

@@ -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
}

View File

@@ -0,0 +1,12 @@
import { existsSync, readFileSync } from 'fs'
export const readJsonFile = <T = unknown>(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
}
}

View File

@@ -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: '✔️',
},
]