Files
CherryHQ-cherry-studio/scripts/__tests__/fix-tailwind-canonical-classes.test.ts
亢奋猫 29a3750ac3 chore(style-reminders): add Tailwind canonical checks (#14862)
### 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>
2026-05-11 14:48:55 +08:00

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')
})
})