mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-03 12:27:41 +08:00
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:
@@ -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('')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user