mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-05 21:50:46 +08:00
### What this PR does Before this PR: Tailwind canonical class suggestions such as `w-[420px] -> w-105` had to be fixed manually, and the PR style reminder workflow only reported newly introduced legacy renderer CSS variables. After this PR: Adds `pnpm styles:canonical <path>` to rewrite static Tailwind class strings to their canonical Tailwind v4 forms. The PR style reminders workflow now comments on both newly introduced legacy CSS variables and Tailwind canonical class suggestions. <!-- (optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*: --> Fixes # ### Why we need it and why it was done in this way The following tradeoffs were made: The canonical class fixer is conservative: it only rewrites static JSX `class` / `className` strings and static `cn(...)` string inputs, leaving dynamic template literals untouched. It uses Tailwind's own design system canonicalization instead of maintaining a manual mapping table. The following alternatives were considered: A regex-only implementation was avoided because Tailwind canonicalization depends on Tailwind v4 parsing and theme behavior. A separate PR workflow comment was also avoided so the style reminders comment remains the single bot comment. Links to places where the discussion took place: N/A ### Breaking changes None. ### Special notes for your reviewer Compatibility aliases and legacy marker/env fallback were removed; the PR workflow now uses the `style-reminders` script, marker, and output naming. Validation performed: - `pnpm test:scripts -- scripts/__tests__/check-pr-style-reminders.test.ts scripts/__tests__/fix-tailwind-canonical-classes.test.ts` - `pnpm build:check` - `git diff --check` ### Checklist This checklist is not enforcing, but it's a reminder of items that could be relevant to every PR. Approvers are expected to review this list. - [x] PR: The PR description is expressive enough and will help future contributors - [x] Code: [Write code that humans can understand](https://en.wikiquote.org/wiki/Martin_Fowler#code-for-humans) and [Keep it simple](https://en.wikipedia.org/wiki/KISS_principle) - [x] Refactor: You have [left the code cleaner than you found it (Boy Scout Rule)](https://learning.oreilly.com/library/view/97-things-every/9780596809515/ch08.html) - [x] Upgrade: Impact of this change on upgrade flows was considered and addressed if required - [ ] Documentation: A [user-guide update](https://docs.cherry-ai.com) was considered and is present (link) or not required. Check this only when the PR introduces or changes a user-facing feature or behavior. - [x] Self-review: I have reviewed my own code (e.g., via [`/gh-pr-review`](/.claude/skills/gh-pr-review/SKILL.md), `gh pr diff`, or GitHub UI) before requesting review from others ### Release note <!-- Write your release note: 1. Enter your extended release note in the below block. If the PR requires additional action from users switching to the new release, include the string "action required". 2. If no release note is required, just write "NONE". 3. Only include user-facing changes (new features, bug fixes visible to users, UI changes, behavior changes). For CI, maintenance, internal refactoring, build tooling, or other non-user-facing work, write "NONE". --> ```release-note NONE ``` --------- Signed-off-by: kangfenmao <kangfenmao@qq.com>
94 lines
3.1 KiB
TypeScript
94 lines
3.1 KiB
TypeScript
import fs from 'node:fs/promises'
|
|
import os from 'node:os'
|
|
import path from 'node:path'
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import { loadTailwindDesignSystem, processPath, runCli } from '../fix-tailwind-canonical-classes'
|
|
|
|
async function createTempDir(): Promise<string> {
|
|
return fs.mkdtemp(path.join(os.tmpdir(), 'tailwind-canonical-'))
|
|
}
|
|
|
|
async function writeSource(tempDir: string, relativePath: string, source: string): Promise<string> {
|
|
const filePath = path.join(tempDir, relativePath)
|
|
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
await fs.writeFile(filePath, source)
|
|
return filePath
|
|
}
|
|
|
|
describe('fix-tailwind-canonical-classes', () => {
|
|
it('rewrites canonical Tailwind suggestions in JSX className strings', async () => {
|
|
const tempDir = await createTempDir()
|
|
const filePath = await writeSource(
|
|
tempDir,
|
|
'Component.tsx',
|
|
`export function Component() {
|
|
return (
|
|
<div className="w-[420px] sm:max-w-[480px] min-h-[72px] !w-fit text-[var(--color-foreground-secondary)]" />
|
|
)
|
|
}
|
|
`
|
|
)
|
|
|
|
const designSystem = await loadTailwindDesignSystem()
|
|
const summary = await processPath(tempDir, designSystem)
|
|
const source = await fs.readFile(filePath, 'utf8')
|
|
|
|
expect(summary).toEqual({ scannedFiles: 1, changedFiles: 1, replacements: 5 })
|
|
expect(source).toContain('className="w-105 sm:max-w-120 min-h-18 w-fit! text-(--color-foreground-secondary)"')
|
|
})
|
|
|
|
it('rewrites static cn string arguments and object keys', async () => {
|
|
const tempDir = await createTempDir()
|
|
const filePath = await writeSource(
|
|
tempDir,
|
|
'Component.tsx',
|
|
`const value = cn('w-[420px]', { 'min-h-[72px]': enabled }, \`text-[var(--color-foreground-secondary)]\`)
|
|
`
|
|
)
|
|
|
|
const designSystem = await loadTailwindDesignSystem()
|
|
const summary = await processPath(filePath, designSystem)
|
|
const source = await fs.readFile(filePath, 'utf8')
|
|
|
|
expect(summary).toEqual({ scannedFiles: 1, changedFiles: 1, replacements: 3 })
|
|
expect(source).toBe("const value = cn('w-105', { 'min-h-18': enabled }, `text-(--color-foreground-secondary)`)\n")
|
|
})
|
|
|
|
it('leaves dynamic template literals unchanged', async () => {
|
|
const tempDir = await createTempDir()
|
|
const filePath = await writeSource(
|
|
tempDir,
|
|
'Component.tsx',
|
|
`const value = cn(\`w-[420px] \${active ? 'min-h-[72px]' : ''}\`)
|
|
`
|
|
)
|
|
|
|
const designSystem = await loadTailwindDesignSystem()
|
|
const summary = await processPath(filePath, designSystem)
|
|
const source = await fs.readFile(filePath, 'utf8')
|
|
|
|
expect(summary).toEqual({ scannedFiles: 1, changedFiles: 0, replacements: 0 })
|
|
expect(source).toBe("const value = cn(`w-[420px] ${active ? 'min-h-[72px]' : ''}`)\n")
|
|
})
|
|
|
|
it('fails when the path argument is missing', async () => {
|
|
let stderr = ''
|
|
const exitCode = await runCli([], {
|
|
stderr: {
|
|
write: (chunk: string | Uint8Array) => {
|
|
stderr += String(chunk)
|
|
return true
|
|
}
|
|
},
|
|
stdout: {
|
|
write: () => true
|
|
}
|
|
})
|
|
|
|
expect(exitCode).toBe(1)
|
|
expect(stderr).toBe('Usage: pnpm styles:canonical <path>\n')
|
|
})
|
|
})
|