Files
CherryHQ-cherry-studio/scripts/check-pr-style-reminders.ts
fullex 53a3577389 refactor(renderer): flatten src/renderer/src to src/renderer
Move all renderer source from src/renderer/src/* up one level to
src/renderer/*, removing the redundant nested src directory.

- Update path aliases (@renderer, @types, @logger, @data) and TanStack
  Router paths in electron.vite.config.ts; update tsconfig.{json,web,node}
  path mappings and include globs.
- Fix Vite root-relative script paths in the 8 renderer HTML entries.
- Update cross-process relative imports in main/preload (language,
  apiServer models, preload index) to drop the /src segment.
- Switch renderer test imports of the logger mock to the @test-mocks alias.
- Update hardcoded renderer paths in scripts and their fixtures, lint
  configs (eslint/oxlint/biome), CODEOWNERS, docs, and the data-classify tool.
- Convert deep (../../+) relative imports within the renderer to the
  @renderer alias (69 files, 108 imports); keep single-level relatives.
- Fix doc links broken by the move and correct one pre-existing broken
  link in naming-conventions.md.
2026-05-28 21:40:20 -07:00

362 lines
11 KiB
TypeScript

import { execFileSync } from 'node:child_process'
import * as fs from 'fs'
import * as path from 'path'
import { findLegacyVarsInLine, shouldIgnoreFile } from './check-legacy-css-vars'
import {
findTailwindCanonicalClassFindingsInContent,
loadTailwindDesignSystem,
type TailwindCanonicalClassFinding
} from './fix-tailwind-canonical-classes'
const REPO_ROOT = path.join(__dirname, '..')
const LEGACY_CHECK_EXTENSIONS = new Set(['.css', '.ts', '.tsx'])
const CANONICAL_CLASS_CHECK_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'])
const STYLE_REMINDERS_COMMENT_MARKER = '<!-- style-reminders-warning -->'
export interface PullRequestLegacyFinding {
file: string
line: number
variable: string
lineText: string
}
export interface PullRequestTailwindCanonicalFinding extends TailwindCanonicalClassFinding {
file: string
}
export interface CheckPullRequestStyleRemindersOptions {
baseRef: string
headRef: string
}
interface HunkRange {
newStart: number
}
function runGit(args: string[]): string {
return execFileSync('git', args, {
cwd: REPO_ROOT,
encoding: 'utf8'
})
}
function parseArgs(): { baseRef: string; headRef: string; markdownOutput?: string } {
const args = process.argv.slice(2)
let baseRef = process.env.BASE_SHA ?? process.env.GITHUB_BASE_REF ?? ''
let headRef = process.env.HEAD_SHA ?? process.env.GITHUB_SHA ?? 'HEAD'
let markdownOutput = process.env.STYLE_REMINDERS_PR_MARKDOWN_OUTPUT
for (let index = 0; index < args.length; index++) {
const arg = args[index]
if (arg === '--base' && args[index + 1]) {
baseRef = args[++index]
continue
}
if (arg === '--head' && args[index + 1]) {
headRef = args[++index]
continue
}
if (arg === '--markdown-output' && args[index + 1]) {
markdownOutput = args[++index]
}
}
if (!baseRef) {
throw new Error('Missing base ref. Pass --base <sha> or set BASE_SHA.')
}
return { baseRef, headRef, markdownOutput }
}
function isTrackedRendererFile(filePath: string): boolean {
if (!filePath.startsWith('src/renderer/')) return false
if (!LEGACY_CHECK_EXTENSIONS.has(path.extname(filePath))) return false
return !shouldIgnoreFile(path.join(REPO_ROOT, filePath))
}
function isCanonicalClassCheckFile(filePath: string): boolean {
if (!filePath.startsWith('src/renderer/')) return false
if (!CANONICAL_CLASS_CHECK_EXTENSIONS.has(path.extname(filePath))) return false
return !shouldIgnoreFile(path.join(REPO_ROOT, filePath))
}
function getChangedRendererFiles(baseRef: string, headRef: string): string[] {
const output = runGit(['diff', '--name-only', '--diff-filter=ACMR', baseRef, headRef, '--', 'src/renderer'])
return output
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.filter(isTrackedRendererFile)
}
function getChangedCanonicalClassFiles(baseRef: string, headRef: string): string[] {
const output = runGit(['diff', '--name-only', '--diff-filter=ACMR', baseRef, headRef, '--', 'src/renderer'])
return output
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.filter(isCanonicalClassCheckFile)
}
export function parseAddedLineNumbersFromDiff(diff: string): Set<number> {
const lines = diff.split(/\r?\n/)
const addedLineNumbers = new Set<number>()
let newLineNumber = 0
let inHunk = false
for (const line of lines) {
if (line.startsWith('@@')) {
const hunk = parseHunkRange(line)
if (!hunk) continue
newLineNumber = hunk.newStart
inHunk = true
continue
}
if (
!inHunk ||
line.startsWith('diff --git') ||
line.startsWith('index ') ||
line.startsWith('--- ') ||
line.startsWith('+++ ')
) {
continue
}
if (line.startsWith('+')) {
addedLineNumbers.add(newLineNumber)
newLineNumber += 1
continue
}
if (line.startsWith('-')) {
continue
}
newLineNumber += 1
}
return addedLineNumbers
}
function parseHunkRange(line: string): HunkRange | null {
const match = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/)
if (!match) return null
return {
newStart: Number(match[1])
}
}
export function parseAddedLegacyVarFindingsFromDiff(diff: string, filePath: string): PullRequestLegacyFinding[] {
const lines = diff.split(/\r?\n/)
const findings: PullRequestLegacyFinding[] = []
let newLineNumber = 0
let inHunk = false
for (const line of lines) {
if (line.startsWith('@@')) {
const hunk = parseHunkRange(line)
if (!hunk) continue
newLineNumber = hunk.newStart
inHunk = true
continue
}
if (
!inHunk ||
line.startsWith('diff --git') ||
line.startsWith('index ') ||
line.startsWith('--- ') ||
line.startsWith('+++ ')
) {
continue
}
if (line.startsWith('+')) {
const addedLine = line.slice(1)
for (const variable of findLegacyVarsInLine(addedLine)) {
findings.push({
file: filePath,
line: newLineNumber,
variable,
lineText: addedLine.trim()
})
}
newLineNumber += 1
continue
}
if (line.startsWith('-')) {
continue
}
newLineNumber += 1
}
return findings
}
export function checkPullRequestLegacyVars({
baseRef,
headRef
}: CheckPullRequestStyleRemindersOptions): PullRequestLegacyFinding[] {
const files = getChangedRendererFiles(baseRef, headRef)
return files.flatMap((filePath) => {
const diff = runGit(['diff', '--unified=0', '--no-color', baseRef, headRef, '--', filePath])
return parseAddedLegacyVarFindingsFromDiff(diff, filePath)
})
}
export async function checkPullRequestTailwindCanonicalClasses({
baseRef,
headRef
}: CheckPullRequestStyleRemindersOptions): Promise<PullRequestTailwindCanonicalFinding[]> {
const files = getChangedCanonicalClassFiles(baseRef, headRef)
if (files.length === 0) return []
const designSystem = await loadTailwindDesignSystem(REPO_ROOT)
return files.flatMap((filePath) => {
const diff = runGit(['diff', '--unified=0', '--no-color', baseRef, headRef, '--', filePath])
const addedLineNumbers = parseAddedLineNumbersFromDiff(diff)
if (addedLineNumbers.size === 0) return []
const content = fs.readFileSync(path.join(REPO_ROOT, filePath), 'utf8')
return findTailwindCanonicalClassFindingsInContent(content, filePath, designSystem)
.filter((finding) => addedLineNumbers.has(finding.line))
.map((finding) => ({
...finding,
file: filePath
}))
})
}
export function buildPullRequestStyleRemindersComment(
findings: PullRequestLegacyFinding[],
canonicalClassFindings: PullRequestTailwindCanonicalFinding[] = []
): string {
if (findings.length === 0 && canonicalClassFindings.length === 0) {
return ''
}
return [
STYLE_REMINDERS_COMMENT_MARKER,
'## Style Reminders',
'',
buildLegacyVarsCommentSection(findings),
buildCanonicalClassesCommentSection(canonicalClassFindings),
'',
'This is a migration reminder only and does not block the PR.'
]
.filter(Boolean)
.join('\n')
}
function buildLegacyVarsCommentSection(findings: PullRequestLegacyFinding[]): string {
if (findings.length === 0) return ''
const uniqueFiles = new Set(findings.map((finding) => finding.file)).size
const summary = new Map<string, number>()
for (const finding of findings) {
summary.set(finding.variable, (summary.get(finding.variable) ?? 0) + 1)
}
const summaryLines = [...summary.entries()]
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([variable, count]) => `- \`${variable}\`: ${count}`)
const sampleFindings = findings.slice(0, 20).map((finding) => {
return `- \`${finding.file}:${finding.line}\` uses \`${finding.variable}\`\n \`${finding.lineText}\``
})
const overflowLine =
findings.length > sampleFindings.length
? `\nThere are ${findings.length - sampleFindings.length} more matches in this PR.`
: ''
return [
'### Legacy CSS Variables Detected',
'',
`This PR adds new usages of deprecated renderer legacy CSS variables in ${uniqueFiles} file(s).`,
'Please migrate these changes to `@cherrystudio/ui` theme contract variables or Tailwind semantic utilities when possible.',
'',
'**Usage summary**',
...summaryLines,
'',
'**New usages introduced by this PR**',
...sampleFindings,
overflowLine
]
.filter(Boolean)
.join('\n')
}
function buildCanonicalClassesCommentSection(findings: PullRequestTailwindCanonicalFinding[]): string {
if (findings.length === 0) return ''
const uniqueFiles = new Set(findings.map((finding) => finding.file)).size
const sampleFindings = findings.slice(0, 20).map((finding) => {
return `- \`${finding.file}:${finding.line}\` can use \`${finding.canonical}\` instead of \`${finding.original}\`\n \`${finding.lineText}\``
})
const overflowLine =
findings.length > sampleFindings.length
? `\nThere are ${findings.length - sampleFindings.length} more Tailwind class matches in this PR.`
: ''
return [
'### Tailwind Canonical Classes Detected',
'',
`This PR adds Tailwind classes with shorter canonical forms in ${uniqueFiles} file(s).`,
'Run `pnpm styles:canonical <path>` locally to rewrite them automatically.',
'',
'**New canonical class suggestions introduced by this PR**',
...sampleFindings,
overflowLine
]
.filter(Boolean)
.join('\n')
}
function writeMarkdownOutput(markdownOutput: string | undefined, body: string): void {
if (!markdownOutput) return
const outputPath = path.resolve(REPO_ROOT, markdownOutput)
fs.mkdirSync(path.dirname(outputPath), { recursive: true })
fs.writeFileSync(outputPath, body, 'utf8')
}
async function main(): Promise<void> {
const { baseRef, headRef, markdownOutput } = parseArgs()
const findings = checkPullRequestLegacyVars({ baseRef, headRef })
const canonicalClassFindings = await checkPullRequestTailwindCanonicalClasses({ baseRef, headRef })
const body = buildPullRequestStyleRemindersComment(findings, canonicalClassFindings)
if (findings.length === 0 && canonicalClassFindings.length === 0) {
console.log(
'No new legacy renderer CSS variable usages or Tailwind canonical class suggestions were introduced in this PR.'
)
} else {
console.warn(
`Detected ${findings.length} new legacy renderer CSS variable usage(s) and ${canonicalClassFindings.length} Tailwind canonical class suggestion(s) in this PR.`
)
console.warn('')
console.warn(body)
}
writeMarkdownOutput(markdownOutput, body)
}
if (require.main === module) {
main().catch((error: unknown) => {
console.error(error instanceof Error ? error.message : error)
process.exitCode = 1
})
}