mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-25 06:14:59 +00:00
Merge pull request #116 from johndoe6345789/codex/refactor-typescript-files-to-modular-structure
refactor: modularize select component and scripts
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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
150
tools/analysis/code/list-large-typescript-files.ts
Executable file → Normal 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) => {
|
||||
|
||||
52
tools/analysis/code/list-large-typescript-files/args.ts
Normal file
52
tools/analysis/code/list-large-typescript-files/args.ts
Normal 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
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
39
tools/analysis/code/list-large-typescript-files/types.ts
Normal file
39
tools/analysis/code/list-large-typescript-files/types.ts
Normal 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.`
|
||||
@@ -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}`)
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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())
|
||||
|
||||
80
tools/generation/generate-quality-summary/details.ts
Normal file
80
tools/generation/generate-quality-summary/details.ts
Normal 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
|
||||
}
|
||||
52
tools/generation/generate-quality-summary/overview.ts
Normal file
52
tools/generation/generate-quality-summary/overview.ts
Normal 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('')
|
||||
}
|
||||
34
tools/generation/generate-quality-summary/quality-summary.ts
Normal file
34
tools/generation/generate-quality-summary/quality-summary.ts
Normal 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
|
||||
}
|
||||
12
tools/generation/generate-quality-summary/report-reader.ts
Normal file
12
tools/generation/generate-quality-summary/report-reader.ts
Normal 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
|
||||
}
|
||||
}
|
||||
45
tools/generation/generate-quality-summary/sections.ts
Normal file
45
tools/generation/generate-quality-summary/sections.ts
Normal 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: '✔️',
|
||||
},
|
||||
]
|
||||
Reference in New Issue
Block a user