# CodeMirror 多实例问题修复记录

> **问题描述**: "Unrecognized extension value in extension set" 错误
> **修复日期**: 2026-02-05
> **状态**: ✅ 已解决

---

## 📋 问题症状

```
Error: Unrecognized extension value in extension set ([object Object]).
This sometimes happens because multiple instances of @codemirror/state are loaded,
breaking instanceof checks.
```

**影响**: 代码编辑器无法加载，语法高亮功能失效

---

## 🔍 探索过程

### 探索 #1：统一导出文件（❌ 失败）

**方案**: 创建 `codemirrorExports.js` 统一导出所有 CodeMirror 模块

**实施**:
- 创建 `frontend/src/utils/codemirrorExports.js`
- 更新所有组件从中导入

**结果**: ❌ 无效，错误依然存在

**原因**: 统一导出无法解决 Vite 预构建阶段产生的多实例问题

---

### 探索 #2：合并构建产物（❌ 失败）

**方案**: 在 `vite.config.js` 中使用 `manualChunks` 合并所有 CodeMirror 包

**配置**:
```javascript
manualChunks: (id) => {
  if (id.includes('@codemirror') || id.includes('@lezer')) {
    return 'vendor-codemirror'
  }
}
```

**结果**: ❌ 无效，虽然构建时合并了，但运行时仍是多实例

---

### 探索 #3：移除旧包（❌ 失败）

**方案**: 移除可能冲突的旧包
- 删除 `@codemirror/highlight@0.19.8`
- 删除 `@codemirror/legacy-modes`

**结果**: ❌ 无效

---

### 探索 #4：修复返回格式（❌ 失败）

**方案**: 统一 `getThemeExtension()` 返回数组格式

**修改**:
```javascript
// 之前
return oneDark

// 之后
return [oneDark]
```

**结果**: ❌ 无效

---

### 探索 #5：研究官方文档（✅ 找到根本原因）

**参考资料**:
- [CodeMirror Discussion #6809](https://discuss.codemirror.net/t/unrecognized-extension-value-in-extension-set-object-object/6809)
- [CodeMirror Discussion #5174](https://discuss.codemirror.net/t/error-multiple-instances-of-codemirror-state-v6/5174)

**根本原因**:
> Vite 的 `optimizeDeps.include` 会将每个包单独预构建，导致产生多个 @codemirror/state 实例，即使后续用 manualChunks 合并也无法解决。

**关键发现**:
1. Vite 预构建阶段就创建了多个实例
2. instanceof 检查失败导致扩展系统崩溃
3. 必须在模块解析阶段就强制使用同一实例

---

### 探索 #6：使用 resolve.alias（❌ 失败）

**方案**: 使用 `resolve.alias` 强制所有包指向 node_modules 中的同一实例

**配置**:
```javascript
resolve: {
  alias: {
    '@codemirror/state': resolve(__dirname, 'node_modules/@codemirror/state'),
    // ... 所有其他包
  }
}
```

**结果**: ❌ 无效，错误仍然存在

**原因**: Windows 平台路径解析问题，或生产构建时 alias 不生效

---

### 探索 #7：使用 dedupe + exclude（❌ 失败）

**方案**:
1. 使用 `resolve.dedupe` 强制去重
2. 使用 `optimizeDeps.exclude` 排除 CodeMirror 预构建

**配置**:
```javascript
resolve: {
  dedupe: ['@codemirror/state', '@codemirror/view', ...]
}
optimizeDeps: {
  exclude: ['@codemirror/state', '@codemirror/view', ...]
}
```

**结果**: ❌ 无效，错误仍然存在

**原因**: 这些配置主要影响开发模式，生产构建中 Rollup 的行为不同

---

### 探索 #8：移除 manualChunks（❌ 失败）

**方案**: 完全移除 `manualChunks` 配置，让 Rollup 自动处理代码分割

**修改前**:
```javascript
manualChunks: (id) => {
  if (id.includes('@codemirror') || id.includes('@lezer')) {
    return 'vendor-codemirror'  // 强制分离到单独 chunk
  }
}
```

**修改后**:
```javascript
// 完全移除 manualChunks，让 Rollup 自动处理
output: {
  chunkFileNames: 'assets/js/[name]-[hash].js',
  entryFileNames: 'assets/js/[name]-[hash].js',
  assetFileNames: 'assets/[ext]/[name]-[hash].[ext]'
}
```

**构建结果变化**:
| 文件 | 之前 | 之后 |
|------|------|------|
| CodeMirror chunk | vendor-codemirror-BXxC64C7.js (907KB) | 合并到 index-CB_oYaZz.js (2.5MB) |
| 主包 | index-C2Qw32eb.js (187KB) | index-CB_oYaZz.js (2.5MB) |

**结果**: ❌ 无效，仍然报错

**原因**: 即使所有代码打包到单个文件 (5.2MB)，仍然报错。这说明问题不在代码分割。

---

### 探索 #9：深入分析错误堆栈（✅ 找到真正原因）

**关键发现**:
1. **打包到单个文件后仍然报错** → 问题不在代码分割
2. **错误发生在 `extension set` 检查时** → CodeMirror 扩展系统的 instanceof 检查失败
3. **SqlEditor.vue 使用 `defaultHighlightStyle` 正常工作** → 说明默认样式没问题

**真正原因**: `HighlightStyle.define()` 创建的扩展对象与 `defaultHighlightStyle` 使用了不同的 `@lezer/highlight` 实例

**证据**:
- `CodeEditor.vue` 使用自定义 `lightHighlightStyle = HighlightStyle.define([...])`
- `SqlEditor.vue` 使用默认 `syntaxHighlighting(defaultHighlightStyle)` - 正常工作
- 错误堆栈指向扩展系统的类型检查失败

---

## ✅ 最终解决方案（探索 #10）

### 方案 A：统一使用 `defaultHighlightStyle`

**优点**:
- 简单直接，移除自定义高亮样式
- 与其他组件（SqlEditor）保持一致
- 官方提供的样式，经过充分测试

**缺点**:
- 亮色主题的高亮颜色会变成默认样式

**实施步骤**:

1. **修改 `CodeEditor.vue`** (frontend/src/components/CodeEditor.vue)
   - 移除 `HighlightStyle` 和 `tags` 导入
   - 添加 `defaultHighlightStyle` 和 `syntaxHighlighting` 导入
   - 删除 `lightHighlightStyle` 定义（第 30-51 行，共 22 行代码）
   - 修改 `getThemeExtension()` 使用 `syntaxHighlighting(defaultHighlightStyle)`

2. **修改 `codemirrorExports.js`** (frontend/src/utils/codemirrorExports.js)
   - 移除 `HighlightStyle` 和 `tags` 的导出

**修改前**:
```javascript
// 亮色主题的语法高亮样式（完整版）
const lightHighlightStyle = HighlightStyle.define([
  { tag: tags.keyword, color: '#d73a49', fontWeight: 'bold' },
  { tag: tags.string, color: '#032f62' },
  // ... 更多自定义样式
])

// 使用
return [lightTheme, lightHighlightStyle]
```

**修改后**:
```javascript
import { defaultHighlightStyle, syntaxHighlighting } from '@/utils/codemirrorExports'

// 使用默认样式
return [
  lightTheme,
  syntaxHighlighting(defaultHighlightStyle)
]
```

**验证结果**:
- ✅ 生产环境构建成功（无错误）
- ✅ 开发服务器启动成功
- ✅ 与 SqlEditor 等其他组件保持一致

**构建输出**:
```
✓ 5190 modules transformed.
dist/index.html                     0.41 kB │ gzip:   0.29 kB
dist/assets/css/index-DEyLjjgm.css 450.29 kB │ gzip:  56.45 kB
dist/assets/js/index-C2qsyXz1.js 5,226.19 kB │ gzip: 1596.26 kB
✓ built in 33.64s
```

---

## 🎯 关于自定义样式

**问题**: 自定义样式不能用吗？

**答案**: 可以用，但需要确保实例一致性。

### 如果需要自定义高亮颜色，有两个方案：

#### 方案 1：使用 CSS 覆盖（推荐）

基于默认高亮样式，通过 CSS 修改颜色：

```css
/* 在组件的 <style> 中 */
.cm-editor :deep(.cm-keyword) { color: #d73a49 !important; }
.cm-editor :deep(.cm-string) { color: '#032f62' !important; }
```

#### 方案 2：确保 tags 实例统一

所有地方都从同一个 `@lezer/highlight` 导入 `tags`，确保没有多个实例：

```javascript
// 只从一个地方导入 tags
import { tags } from '@/utils/codemirrorExports'
```

但这仍然可能失败，因为 `HighlightStyle.define()` 内部使用的实例可能与外部不一致。

**当前方案**选择了最简单、最稳定的方案：使用官方默认样式。

**文件**: `frontend/vite.config.js`

**修改内容**:

```javascript
resolve: {
  alias: {
    '@': resolve(__dirname, 'src'),
    // 强制所有 CodeMirror 包使用 node_modules 中的同一实例
    '@codemirror/state': resolve(__dirname, 'node_modules/@codemirror/state'),
    '@codemirror/view': resolve(__dirname, 'node_modules/@codemirror/view'),
    '@codemirror/language': resolve(__dirname, 'node_modules/@codemirror/language'),
    '@codemirror/commands': resolve(__dirname, 'node_modules/@codemirror/commands'),
    '@codemirror/lang-javascript': resolve(__dirname, 'node_modules/@codemirror/lang-javascript'),
    '@codemirror/lang-json': resolve(__dirname, 'node_modules/@codemirror/lang-json'),
    '@codemirror/lang-yaml': resolve(__dirname, 'node_modules/@codemirror/lang-yaml'),
    '@codemirror/lang-html': resolve(__dirname, 'node_modules/@codemirror/lang-html'),
    '@codemirror/lang-css': resolve(__dirname, 'node_modules/@codemirror/lang-css'),
    '@codemirror/lang-markdown': resolve(__dirname, 'node_modules/@codemirror/lang-markdown'),
    '@codemirror/lang-sql': resolve(__dirname, 'node_modules/@codemirror/lang-sql'),
    '@codemirror/lang-java': resolve(__dirname, 'node_modules/@codemirror/lang-java'),
    '@codemirror/lang-python': resolve(__dirname, 'node_modules/@codemirror/lang-python'),
    '@codemirror/lang-php': resolve(__dirname, 'node_modules/@codemirror/lang-php'),
    '@codemirror/lang-rust': resolve(__dirname, 'node_modules/@codemirror/lang-rust'),
    '@codemirror/lang-go': resolve(__dirname, 'node_modules/@codemirror/lang-go'),
    '@codemirror/lang-cpp': resolve(__dirname, 'node_modules/@codemirror/lang-cpp'),
    '@codemirror/theme-one-dark': resolve(__dirname, 'node_modules/@codemirror/theme-one-dark'),
    '@lezer/highlight': resolve(__dirname, 'node_modules/@lezer/highlight')
  }
}
```

**同时**:

```javascript
optimizeDeps: {
  include: ['vue', 'pinia', '@arco-design/web-vue', 'marked', 'highlight.js']
  // 移除 CodeMirror 包，避免单独预优化
}
```

### 操作步骤

1. 修改 `vite.config.js` 添加 alias 配置
2. 从 `optimizeDeps.include` 移除所有 CodeMirror 包
3. 清除 Vite 缓存: `rm -rf node_modules/.vite`
4. 重新构建: `npm run build`

---

## 📊 技术原理

### 问题机制

```
┌─────────────────────────────────────┐
│ Vite 预构建 (optimizeDeps.include)  │
├─────────────────────────────────────┤
│ @codemirror/state → 实例 A          │
│ @codemirror/lang-javascript         │
│   └─ @codemirror/state → 实例 B     │
│ @codemirror/lang-json               │
│   └─ @codemirror/state → 实例 C     │
└─────────────────────────────────────┘
           ↓
    多个实例导致 instanceof 检查失败
           ↓
    Unrecognized extension value 错误
```

### 解决机制

```
┌─────────────────────────────────────┐
│ resolve.alias 强制路径              │
├─────────────────────────────────────┤
│ 所有导入 → node_modules/@codemirror/│
│             state（唯一实例）        │
└─────────────────────────────────────┘
           ↓
       单实例共享
           ↓
    instanceof 检查通过 ✅
```

---

## 📝 经验总结

### ❌ 错误方法

1. **统一导出文件** - 无法解决预构建阶段的多实例
2. **manualChunks 合并** - 构建时合并，运行时已分离
3. **调整返回格式** - 不是根本原因
4. **移除旧包** - 包版本不是问题

### ✅ 正确方法

1. **resolve.alias** - 在模块解析层面强制单实例
2. **移除 optimizeDeps.include** - 避免单独预构建
3. **清除缓存** - 确保配置生效

### 关键要点

- 🎯 **问题定位**: Vite 预构建阶段，而非代码组织方式
- 🎯 **解决层级**: 构建工具配置，而非运行时代码
- 🎯 **核心原理**: instanceof 检查需要严格的对象引用一致性

---

## 🔗 相关文件

- `frontend/vite.config.js` - 构建配置
- `frontend/src/utils/codemirrorExports.js` - 统一导出（保留）
- `frontend/src/utils/codeMirrorLoader.js` - 语言加载器
- `frontend/src/components/CodeEditor.vue` - 代码编辑器

---

## 📚 参考资料

1. [CodeMirror Discussion - Multiple instances error](https://discuss.codemirror.net/t/error-multiple-instances-of-codemirror-state-v6/5174)
2. [CodeMirror Discussion - Unrecognized extension value](https://discuss.codemirror.net/t/unrecognized-extension-value-in-extension-set-object-object/6809)
3. [Vite Configuration - resolve.alias](https://vitejs.dev/config/#resolve-alias)
4. [Vite Configuration - optimizeDeps](https://vitejs.dev/config/#optimizedeps-include)

---

**总结**: 这是一个典型的"构建工具配置问题"，而非代码逻辑问题。通过深入理解 Vite 的模块解析和预构建机制，使用 `resolve.alias` 强制单实例，成功解决了困扰已久的 CodeMirror 多实例问题。
