From b7b2a27a0254b1723527dfd25514f2cf57a78972 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Sat, 13 Jun 2026 13:04:06 +0800 Subject: [PATCH] chore(scripts): update legacy css variable checks (#16025) Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com> Signed-off-by: kangfenmao --- .../__tests__/check-legacy-css-vars.test.ts | 144 ++++++++++++- scripts/check-legacy-css-vars.ts | 194 +++++++++++++++--- 2 files changed, 313 insertions(+), 25 deletions(-) diff --git a/scripts/__tests__/check-legacy-css-vars.test.ts b/scripts/__tests__/check-legacy-css-vars.test.ts index 4b0beababc..121a7e688e 100644 --- a/scripts/__tests__/check-legacy-css-vars.test.ts +++ b/scripts/__tests__/check-legacy-css-vars.test.ts @@ -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 } { + 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('') + }) }) diff --git a/scripts/check-legacy-css-vars.ts b/scripts/check-legacy-css-vars.ts index 2933327889..cf432fe22e 100644 --- a/scripts/check-legacy-css-vars.ts +++ b/scripts/check-legacy-css-vars.ts @@ -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> = { + '--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 + +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() }