chore(scripts): update legacy css variable checks (#16025)

Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com>
Signed-off-by: kangfenmao <kangfenmao@qq.com>
This commit is contained in:
亢奋猫
2026-06-13 13:04:06 +08:00
committed by GitHub
parent b0dbef7656
commit b7b2a27a02
2 changed files with 313 additions and 25 deletions

View File

@@ -1,6 +1,33 @@
import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
import { describe, expect, it } from 'vitest'
import { findLegacyVarHitsInContent, isCommentLine } from '../check-legacy-css-vars'
import {
collectTargetFiles,
findLegacyVarHitsInContent,
fixLegacyVarsInContent,
isCommentLine,
runCli
} from '../check-legacy-css-vars'
function makeTempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), 'legacy-css-vars-'))
}
function createCaptureStream(): { output: () => string; stream: Pick<typeof process.stdout, 'write'> } {
let output = ''
return {
output: () => output,
stream: {
write: (chunk: string | Uint8Array): boolean => {
output += chunk.toString()
return true
}
}
}
}
describe('check-legacy-css-vars', () => {
it('identifies comment lines', () => {
@@ -37,4 +64,119 @@ describe('check-legacy-css-vars', () => {
expect(findings.map((finding) => finding.variable)).toEqual(['--color-text-1', '--color-text-2'])
expect(findings.map((finding) => finding.line)).toEqual([3, 6])
})
it('uses the renderer source directory as the default target', () => {
const files = collectTargetFiles()
expect(files.length).toBeGreaterThan(0)
expect(files.every((file) => file.includes(`${path.sep}src${path.sep}renderer${path.sep}`))).toBe(true)
})
it('collects only the specified source file', () => {
const tempDir = makeTempDir()
const targetFile = path.join(tempDir, 'Component.tsx')
const ignoredFile = path.join(tempDir, 'Component.test.tsx')
fs.writeFileSync(targetFile, 'color: var(--color-text-1);')
fs.writeFileSync(ignoredFile, 'color: var(--color-text-2);')
expect(collectTargetFiles(targetFile)).toEqual([targetFile])
expect(collectTargetFiles(ignoredFile)).toEqual([])
})
it('collects matching files recursively from the specified directory', () => {
const tempDir = makeTempDir()
const nestedDir = path.join(tempDir, 'nested')
const sourceFile = path.join(tempDir, 'style.css')
const nestedSourceFile = path.join(nestedDir, 'Component.tsx')
fs.mkdirSync(nestedDir)
fs.writeFileSync(sourceFile, 'color: var(--color-text-1);')
fs.writeFileSync(nestedSourceFile, 'color: var(--color-text-2);')
fs.writeFileSync(path.join(tempDir, 'README.md'), 'var(--color-text-3)')
expect(collectTargetFiles(tempDir).sort()).toEqual([sourceFile, nestedSourceFile].sort())
})
it('returns an error when the specified path does not exist', () => {
const stdout = createCaptureStream()
const stderr = createCaptureStream()
const exitCode = runCli(['missing-legacy-css-vars-path'], { stdout: stdout.stream, stderr: stderr.stream })
expect(exitCode).toBe(1)
expect(stdout.output()).toBe('')
expect(stderr.output()).toContain('Path does not exist: missing-legacy-css-vars-path')
})
it('returns a strict-mode error when the specified path contains legacy vars', () => {
const tempDir = makeTempDir()
const targetFile = path.join(tempDir, 'style.css')
const stdout = createCaptureStream()
const stderr = createCaptureStream()
fs.writeFileSync(targetFile, 'color: var(--color-text-1);')
const exitCode = runCli([targetFile, '--strict'], { stdout: stdout.stream, stderr: stderr.stream })
expect(exitCode).toBe(1)
expect(stdout.output()).toBe('')
expect(stderr.output()).toContain(targetFile)
expect(stderr.output()).toContain('--color-text-1')
})
it('honors LEGACY_CSS_VARS_STRICT for specified paths', () => {
const tempDir = makeTempDir()
const targetFile = path.join(tempDir, 'style.css')
const stdout = createCaptureStream()
const stderr = createCaptureStream()
fs.writeFileSync(targetFile, 'color: var(--color-text-1);')
const exitCode = runCli([targetFile], {
env: { LEGACY_CSS_VARS_STRICT: 'true' },
stdout: stdout.stream,
stderr: stderr.stream
})
expect(exitCode).toBe(1)
expect(stdout.output()).toBe('')
expect(stderr.output()).toContain(targetFile)
})
it('auto-fixes mapped legacy variables in code lines only', () => {
const content = [
'const className = "text-(--color-text-2) bg-(--color-background-soft)"',
'const linkStyle = { color: "var(--color-link)" }',
'// var(--color-text-1)',
':root {',
' --color-text-1: var(--color-foreground);',
'}'
].join('\n')
const result = fixLegacyVarsInContent(content)
expect(result.replacements).toBe(3)
expect(result.content).toContain('text-(--color-foreground-secondary) bg-(--color-muted)')
expect(result.content).toContain('var(--color-primary)')
expect(result.content).toContain('// var(--color-text-1)')
expect(result.content).toContain('--color-text-1: var(--color-foreground);')
})
it('writes auto-fixes for the specified path before strict validation', () => {
const tempDir = makeTempDir()
const targetFile = path.join(tempDir, 'style.css')
const stdout = createCaptureStream()
const stderr = createCaptureStream()
fs.writeFileSync(targetFile, '.title { color: var(--color-text-1); }')
const exitCode = runCli([targetFile, '--fix', '--strict'], { stdout: stdout.stream, stderr: stderr.stream })
expect(exitCode).toBe(0)
expect(fs.readFileSync(targetFile, 'utf8')).toBe('.title { color: var(--color-foreground); }')
expect(stdout.output()).toContain('changed 1 files, replaced 1 usages')
expect(stdout.output()).toContain('No legacy renderer CSS variable usages found.')
expect(stderr.output()).toBe('')
})
})

View File

@@ -1,7 +1,8 @@
import * as fs from 'fs'
import * as path from 'path'
const RENDERER_DIR = path.join(__dirname, '../src/renderer')
const REPO_ROOT = path.resolve(__dirname, '..')
const RENDERER_DIR = path.join(REPO_ROOT, 'src/renderer')
const CHECK_EXTENSIONS = new Set(['.css', '.ts', '.tsx'])
const IGNORED_DIR_NAMES = new Set(['node_modules', 'dist', 'out'])
const IGNORED_FILE_PATTERNS = [/\.test\.(ts|tsx)$/, /\.spec\.(ts|tsx)$/, /\.snap$/]
@@ -67,7 +68,54 @@ export const LEGACY_VARS = [
const LEGACY_VAR_SET = new Set(LEGACY_VARS)
const OCCURRENCE_PATTERN = new RegExp(`(${LEGACY_VARS.map(escapeRegExp).join('|')})(?![\\w-])`, 'g')
const STRICT = process.argv.includes('--strict') || process.env.LEGACY_CSS_VARS_STRICT === 'true'
const AUTO_FIX_REPLACEMENTS: Partial<Record<(typeof LEGACY_VARS)[number], string>> = {
'--color-text-1': '--color-foreground',
'--color-text-2': '--color-foreground-secondary',
'--color-text-3': '--color-foreground-muted',
'--color-text': '--color-foreground',
'--color-text-secondary': '--color-foreground-secondary',
'--color-text-soft': '--color-foreground-secondary',
'--color-text-light': '--color-foreground',
'--color-background-soft': '--color-muted',
'--color-background-mute': '--color-accent',
'--color-background-opacity': '--color-background',
'--color-border-soft': '--color-border',
'--color-border-mute': '--color-border',
'--color-error': '--color-error-base',
'--color-link': '--color-primary',
'--color-primary-bg': '--color-primary-soft',
'--color-fill-secondary': '--color-muted',
'--color-fill-2': '--color-muted',
'--color-bg-base': '--color-background',
'--color-bg-1': '--color-muted',
'--color-hover': '--color-accent',
'--color-active': '--color-muted',
'--color-frame-border': '--color-border',
'--color-group-background': '--color-muted',
'--color-reference': '--color-primary-soft',
'--color-reference-text': '--color-primary',
'--color-reference-background': '--color-primary-soft',
'--color-list-item': '--color-background',
'--color-list-item-hover': '--color-accent',
'--navbar-background': '--color-background',
'--modal-background': '--color-card',
'--chat-background-user': '--color-muted',
'--chat-text-user': '--color-foreground',
'--list-item-border-radius': '--radius-lg',
'--color-primary-1': '--color-primary-soft',
'--color-primary-6': '--color-primary',
'--color-status-success': '--color-success',
'--color-status-error': '--color-error-base',
'--color-status-warning': '--color-warning'
}
type WritableStream = Pick<typeof process.stdout, 'write'>
interface RunCliOptions {
env?: NodeJS.ProcessEnv
stdout?: WritableStream
stderr?: WritableStream
}
export interface Finding {
file: string
@@ -76,6 +124,11 @@ export interface Finding {
lineText: string
}
export interface FixSummary {
filesChanged: number
replacements: number
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
@@ -109,6 +162,20 @@ function collectFiles(dir: string): string[] {
return files
}
export function collectTargetFiles(targetPath = RENDERER_DIR): string[] {
const stats = fs.statSync(targetPath)
if (stats.isDirectory()) {
return collectFiles(targetPath)
}
if (!stats.isFile()) return []
if (!CHECK_EXTENSIONS.has(path.extname(targetPath))) return []
if (shouldIgnoreFile(targetPath)) return []
return [targetPath]
}
function isVariableDefinitionLine(line: string, variable: string): boolean {
return new RegExp(`^\\s*${escapeRegExp(variable)}\\s*:`).test(line)
}
@@ -153,18 +220,52 @@ export function findLegacyVarHitsInContent(content: string, filePath: string): F
return findings
}
export function fixLegacyVarsInContent(content: string): { content: string; replacements: number } {
const lines = content.split(/\r?\n/)
let replacements = 0
const nextLines = lines.map((line) => {
const variables = new Set(findLegacyVarsInLine(line))
let nextLine = line
for (const variable of variables) {
const replacement = AUTO_FIX_REPLACEMENTS[variable as (typeof LEGACY_VARS)[number]]
if (!replacement) continue
const pattern = new RegExp(`${escapeRegExp(variable)}(?![\\w-])`, 'g')
nextLine = nextLine.replace(pattern, () => {
replacements += 1
return replacement
})
}
return nextLine
})
return { content: nextLines.join('\n'), replacements }
}
function findLegacyVarHits(filePath: string): Finding[] {
const content = fs.readFileSync(filePath, 'utf8')
return findLegacyVarHitsInContent(content, filePath)
}
function toRepoRelative(filePath: string): string {
return path.relative(path.join(__dirname, '..'), filePath)
function fixLegacyVarHits(filePath: string): number {
const content = fs.readFileSync(filePath, 'utf8')
const result = fixLegacyVarsInContent(content)
if (result.replacements > 0) {
fs.writeFileSync(filePath, result.content)
}
return result.replacements
}
function printResults(findings: Finding[]): void {
function toRepoRelative(filePath: string): string {
return path.relative(REPO_ROOT, filePath)
}
function printResults(findings: Finding[], stdout: WritableStream, stderr: WritableStream): void {
if (findings.length === 0) {
console.log('No legacy renderer CSS variable usages found.')
stdout.write('No legacy renderer CSS variable usages found.\n')
return
}
@@ -174,39 +275,84 @@ function printResults(findings: Finding[]): void {
byVariable.set(finding.variable, (byVariable.get(finding.variable) ?? 0) + 1)
}
console.warn('Legacy renderer CSS variable usages detected:')
console.warn('')
stderr.write('Legacy renderer CSS variable usages detected:\n')
stderr.write('\n')
for (const finding of findings) {
console.warn(` ${toRepoRelative(finding.file)}:${finding.line}`)
console.warn(` ${finding.variable}`)
console.warn(` ${finding.lineText}`)
stderr.write(` ${toRepoRelative(finding.file)}:${finding.line}\n`)
stderr.write(` ${finding.variable}\n`)
stderr.write(` ${finding.lineText}\n`)
}
console.warn('')
console.warn('Usage summary:')
stderr.write('\n')
stderr.write('Usage summary:\n')
for (const [variable, count] of [...byVariable.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
console.warn(` ${variable}: ${count}`)
stderr.write(` ${variable}: ${count}\n`)
}
console.warn('')
console.warn(
'Prefer @cherrystudio/ui theme contract variables and Tailwind semantic utilities instead of adding new legacy var usages.'
stderr.write('\n')
stderr.write(
'Prefer @cherrystudio/ui theme contract variables and Tailwind semantic utilities instead of adding new legacy var usages.\n'
)
}
function main(): void {
const files = collectFiles(RENDERER_DIR)
function printUsage(stderr: WritableStream): void {
stderr.write('Usage: pnpm styles:legacy-vars [path] [--strict] [--fix]\n')
}
function printFixSummary(summary: FixSummary, stdout: WritableStream): void {
stdout.write(
`Legacy renderer CSS variable auto-fix: changed ${summary.filesChanged} files, replaced ${summary.replacements} usages.\n`
)
}
export function runCli(argv = process.argv.slice(2), options: RunCliOptions = {}): number {
const stdout = options.stdout ?? process.stdout
const stderr = options.stderr ?? process.stderr
const env = options.env ?? process.env
const strict = argv.includes('--strict') || env.LEGACY_CSS_VARS_STRICT === 'true'
const fix = argv.includes('--fix')
const pathArgs = argv.filter((arg) => arg !== '--strict' && arg !== '--fix')
if (pathArgs.length > 1) {
printUsage(stderr)
return 1
}
const targetInput = pathArgs[0]
const targetPath = targetInput ? path.resolve(REPO_ROOT, targetInput) : RENDERER_DIR
if (!fs.existsSync(targetPath)) {
stderr.write(`Path does not exist: ${targetInput}\n`)
return 1
}
const files = collectTargetFiles(targetPath)
if (fix) {
const fixSummary: FixSummary = {
filesChanged: 0,
replacements: 0
}
for (const file of files) {
const replacements = fixLegacyVarHits(file)
if (replacements === 0) continue
fixSummary.filesChanged += 1
fixSummary.replacements += replacements
}
printFixSummary(fixSummary, stdout)
}
const findings = files.flatMap(findLegacyVarHits)
printResults(findings)
printResults(findings, stdout, stderr)
if (STRICT && findings.length > 0) {
process.exitCode = 1
}
return strict && findings.length > 0 ? 1 : 0
}
if (require.main === module) {
main()
process.exitCode = runCli()
}