Phase 007: migrate every remaining view from naive-ui to Arco Design Vue

The project now has exactly one UI library. Every naive-ui component in
packages/client/src is replaced by its Arco equivalent across 24 views and
roughly 80 components, and naive-ui is dropped from package.json along with
its transitive vueuc / @css-render / vdirs / seemly dependencies.

The migration is rendering-layer only. No store logic, no API contract, and
no /chat-run payload was touched.

Three classes of leftover were invisible to `grep naive-ui` and are now
covered by tests/client/no-naive-ui.test.ts, which asserts all three:
  - dead .n-* class names in style blocks
  - dead --n-* custom properties
  - naive-ui in package.json

Notable behavioural traps handled along the way, recorded in
docs/arco-migration-rules.md:
  - a-input-password defaults to masked; passing default-visibility="false"
    would have exposed API keys in clear text
  - a-upload fires a real XHR unless auto-upload is false
  - nested Trigger components fight over the first element child, so the
    group-chat settings dropdown was flattened
  - Arco renders tooltip default-slot text inline, which was silently
    collapsing the chat and group-chat toolbars
This commit is contained in:
2026-10-18 22:57:25 +08:00
parent c3eeea4187
commit 836d0228d2
115 changed files with 4067 additions and 3891 deletions
+77
View File
@@ -0,0 +1,77 @@
import { readdirSync, readFileSync, statSync } from 'fs'
import { join } from 'path'
import { describe, expect, it } from 'vitest'
const SRC_ROOT = 'packages/client/src'
const listFiles = (dir: string): string[] =>
readdirSync(dir).flatMap((name) => {
const full = join(dir, name)
return statSync(full).isDirectory() ? listFiles(full) : [full]
})
const sourceFiles = listFiles(SRC_ROOT).filter((file) =>
/\.(vue|ts|js)$/.test(file),
)
/**
* naive-ui 的类名在迁移时最容易漏:`grep naive-ui` 只匹配 import,
* 匹配不到样式块里的 `:deep(.n-button__content)` 这种写法,也不匹配
* JS 里的字符串字面量(`document.querySelector('.n-modal-mask')`)。
* 这两种都会在 Arco 下静默失效,不报错、测试也测不出来。
*/
const NAIVE_CLASS_PATTERN = /\.n-[a-z][\w-]*/i
const findOffenders = (pattern: RegExp) =>
sourceFiles
.filter((file) => pattern.test(readFileSync(file, 'utf8')))
.map((file) => file.replace(`${SRC_ROOT}/`, ''))
describe('naive-ui removal', () => {
it('has source files to scan', () => {
expect(sourceFiles.length).toBeGreaterThan(100)
})
it('never imports from naive-ui', () => {
expect(findOffenders(/from\s+['"]naive-ui['"]/)).toEqual([])
})
it('never imports naive-ui types or helpers by deep path', () => {
// packages/client/src 下没有深层导入,但防止将来有人从 @css-render 绕进来
expect(
findOffenders(/from\s+['"](vueuc|@css-render\/vue3-ssr|vdirs|seemly)['"]/),
).toEqual([])
})
it('never references naive-ui CSS class names', () => {
// 覆盖样式块里的 :deep(.n-*) 与 JS 字符串字面量两种写法
expect(findOffenders(NAIVE_CLASS_PATTERN)).toEqual([])
})
it('never registers naive-ui components globally', () => {
const main = readFileSync(join(SRC_ROOT, 'main.ts'), 'utf8')
expect(main).toContain('ArcoVue')
expect(main).not.toMatch(/N[A-Z][A-Za-z]*Provider/)
})
it('does not depend on naive-ui in package.json', () => {
const pkg = JSON.parse(readFileSync('package.json', 'utf8'))
const all = { ...pkg.dependencies, ...pkg.devDependencies }
expect(Object.keys(all)).not.toContain('naive-ui')
for (const dep of Object.keys(all)) {
expect(dep).not.toBe('vueuc')
expect(dep).not.toBe('vdirs')
}
})
it('does not chunk naive-ui in the vite config', () => {
const config = readFileSync('vite.config.ts', 'utf8')
expect(config).not.toMatch(/['"]naive-ui['"]/)
// Arco 的 scope 名含 "vue" 子串,判断必须排在 vue-vendor 之前,
// 否则整个 UI 库会被打进 vue chunk。见 vite.config.ts 的注释。
const arcoIndex = config.indexOf('@arco-design')
const vueIndex = config.indexOf('vue-vendor')
expect(arcoIndex).toBeGreaterThan(-1)
expect(arcoIndex).toBeLessThan(vueIndex)
})
})