Files
CherryHQ-cherry-studio/v2-refactor-temp/tools/data-classify/scripts/generate-preferences.js
fullex 1c6ff30a18 refactor(shared): dissolve config/ and move logic out of types/ into utils/
Dissolve the by-kind @shared/config junk drawer per shared-layer governance: route each member by shape and actual consumer process — cross-process slices into types//utils//ai/, single-process code back into main/renderer (Invariant 1.1). Confirm each item's real consumer process rather than trusting the directional plan (API_SERVER_DEFAULTS is renderer-only, MIN_WINDOW_* is cross-process, providers.ts is renderer-only), and drop dead consts (ZOOM_LEVELS/ZOOM_OPTIONS, bookExts, thirdPartyApplicationExts).

Purge runtime logic from types/ so the bucket holds only declarations: move serializeError + AI-SDK error guards to utils/error.ts, the FileHandle factories/guards to utils/file/handle.ts, isSerializable + SerializableSchema to utils/serializable.ts, and the tab-instance guard/normalizer to utils/tabInstanceMetadata.ts. Tests follow the logic to utils/__tests__; the type-level ipc contract test is retired (its invariants kept as a breadcrumb for the future IpcApi Zod schema). types/ is now logic-free and test-free.

Also: remove the dead @shared mock from the packages/ui code-editor test so packages/ui no longer references production code; fix the data-classify preference generator prompts import and the update-languages output path to the relocated modules.

Update shared-layer-architecture (3.1 type/util test rule, 5/6 config dissolution) and renderer-architecture cross-references.
2026-06-19 20:41:18 -07:00

489 lines
16 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
const fs = require('fs')
const path = require('path')
class PreferencesGenerator {
constructor() {
this.dataDir = path.resolve(__dirname, '../data')
this.targetFile = path.resolve(__dirname, '../../../../src/shared/data/preference/preferenceSchemas.ts')
this.classificationFile = path.join(this.dataDir, 'classification.json')
this.targetKeyDefinitionsFile = path.join(this.dataDir, 'target-key-definitions.json')
}
generate() {
console.log('开始生成 preferences.ts...')
// 读取分类数据
const classification = this.loadClassification()
// 提取preferences相关数据
const classificationData = this.extractPreferencesData(classification)
// 读取target-key-definitions.json复杂映射的target key定义
const targetKeyDefinitions = this.loadTargetKeyDefinitions()
// 合并数据target-key-definitions 覆盖 classification
const preferencesData = this.mergeDataSources(classificationData, targetKeyDefinitions)
// 构建类型结构
const typeStructure = this.buildTypeStructure(preferencesData)
// 生成TypeScript代码
const content = this.generateTypeScriptCode(typeStructure, preferencesData)
// 写入文件
this.writePreferencesFile(content)
console.log('preferences.ts 生成完成!')
this.printSummary(preferencesData)
}
/**
* Load target-key-definitions.json for complex mapping target keys
* These definitions override or extend classification.json
*/
loadTargetKeyDefinitions() {
if (!fs.existsSync(this.targetKeyDefinitionsFile)) {
console.log('target-key-definitions.json 不存在,跳过')
return { definitions: [] }
}
const content = fs.readFileSync(this.targetKeyDefinitionsFile, 'utf8')
const data = JSON.parse(content)
console.log(`读取 target-key-definitions.json: ${data.definitions?.length || 0} 项定义`)
return data
}
/**
* Merge classification data with target-key-definitions
* Target-key-definitions take priority (can override or disable keys)
*/
mergeDataSources(classificationData, targetKeyDefinitions) {
// Use Map to deduplicate by targetKey, definitions take priority
const targetKeyMap = new Map()
// First add classification data
for (const item of classificationData) {
if (item.targetKey) {
targetKeyMap.set(item.targetKey, {
...item,
_source: 'classification'
})
}
}
const definitionsCount = {
added: 0,
overridden: 0,
disabled: 0
}
// Then process target-key-definitions (override or disable)
for (const def of targetKeyDefinitions.definitions || []) {
if (def.status === 'classified') {
const existed = targetKeyMap.has(def.targetKey)
targetKeyMap.set(def.targetKey, {
targetKey: def.targetKey,
type: def.type,
defaultValue: def.defaultValue,
source: 'target-key-definitions',
sourceCategory: def.source || 'complex',
originalKey: def.source || 'complex',
fullPath: `target-key-definitions/${def.targetKey}`,
_source: 'target-key-definitions'
})
if (existed) {
definitionsCount.overridden++
console.log(` 覆盖: ${def.targetKey}`)
} else {
definitionsCount.added++
console.log(` 新增: ${def.targetKey}`)
}
} else if (def.status === 'pending' && targetKeyMap.has(def.targetKey)) {
// status: pending can disable keys from classification
targetKeyMap.delete(def.targetKey)
definitionsCount.disabled++
console.log(` 禁用: ${def.targetKey}`)
}
}
if (definitionsCount.added + definitionsCount.overridden + definitionsCount.disabled > 0) {
console.log(
`target-key-definitions 处理完成: 新增 ${definitionsCount.added}, 覆盖 ${definitionsCount.overridden}, 禁用 ${definitionsCount.disabled}`
)
}
return Array.from(targetKeyMap.values())
}
loadClassification() {
if (!fs.existsSync(this.classificationFile)) {
throw new Error(`分类文件不存在: ${this.classificationFile}`)
}
const content = fs.readFileSync(this.classificationFile, 'utf8')
return JSON.parse(content)
}
extractPreferencesData(classification) {
const allPreferencesData = []
const sources = ['electronStore', 'redux', 'localStorage', 'dexieSettings']
// 递归提取项目包括children
const extractItems = (items, source, category, parentKey = '') => {
if (!Array.isArray(items)) return
items.forEach((item) => {
// 处理有children的项目
if (item.children && Array.isArray(item.children)) {
console.log(`处理children项: ${source}/${category}/${item.originalKey}`)
extractItems(item.children, source, category, `${parentKey}${item.originalKey}.`)
return
}
// 处理普通项目
if (item.category === 'preferences' && item.status === 'classified' && item.targetKey) {
allPreferencesData.push({
...item,
source,
sourceCategory: category,
originalKey: parentKey + item.originalKey, // 包含父级路径
fullPath: `${source}/${category}/${parentKey}${item.originalKey}`
})
}
})
}
sources.forEach((source) => {
if (classification.classifications[source]) {
Object.keys(classification.classifications[source]).forEach((category) => {
const items = classification.classifications[source][category]
extractItems(items, source, category)
})
}
})
console.log(`提取到 ${allPreferencesData.length} 个preferences项包含children`)
// 处理重复的targetKey优先使用redux数据
const targetKeyGroups = {}
allPreferencesData.forEach((item) => {
if (!targetKeyGroups[item.targetKey]) {
targetKeyGroups[item.targetKey] = []
}
targetKeyGroups[item.targetKey].push(item)
})
// 去重按redux > dexieSettings > localStorage > electronStore优先级选择
const sourcePriority = { redux: 4, dexieSettings: 3, localStorage: 2, electronStore: 1 }
const deduplicatedData = []
Object.keys(targetKeyGroups).forEach((targetKey) => {
const items = targetKeyGroups[targetKey]
if (items.length > 1) {
console.log(`发现重复targetKey: ${targetKey},共${items.length}`)
items.forEach((item) => console.log(` - ${item.fullPath}`))
// 按优先级排序,选择最高优先级的项
items.sort((a, b) => sourcePriority[b.source] - sourcePriority[a.source])
const selected = items[0]
console.log(` 选择: ${selected.fullPath}`)
deduplicatedData.push(selected)
} else {
deduplicatedData.push(items[0])
}
})
console.log(`去重后剩余 ${deduplicatedData.length} 个preferences项`)
return deduplicatedData
}
buildTypeStructure(preferencesData) {
const structure = { default: {} }
preferencesData.forEach((item) => {
if (!item.targetKey) return
// 直接使用targetKey作为键不进行拆分
structure.default[item.targetKey] = {
type: this.mapType(item.type, item.defaultValue),
defaultValue: item.defaultValue,
description: `${item.source}/${item.sourceCategory}/${item.originalKey}`,
originalItem: item
}
})
return structure
}
mapType(itemType, defaultValue) {
// 优先使用明确定义的类型只有当type为unknown时才进行类型推断
// 'VALUE: null' is a special marker to indicate the value should be null and not overwritten
const isNullable = defaultValue === null || defaultValue === undefined || defaultValue === 'VALUE: null'
// 如果type不是unknown直接使用定义好的类型
if (itemType && itemType !== 'unknown') {
// 处理简单的基础类型
if (itemType === 'boolean') {
return isNullable ? 'boolean | null' : 'boolean'
}
if (itemType === 'string') {
return isNullable ? 'string | null' : 'string'
}
if (itemType === 'number') {
return isNullable ? 'number | null' : 'number'
}
// 处理数组类型支持string[]、number[]等格式)
if (itemType.endsWith('[]')) {
return isNullable ? `${itemType} | null` : itemType
}
// 处理array泛型类型
if (itemType === 'array') {
// 尝试从默认值推断数组元素类型
if (Array.isArray(defaultValue) && defaultValue.length > 0) {
const elementType = typeof defaultValue[0]
return `${elementType}[]`
}
return isNullable ? 'unknown[] | null' : 'unknown[]'
}
// 处理object类型
if (itemType === 'object') {
return isNullable ? 'Record<string, unknown> | null' : 'Record<string, unknown>'
}
// 对于其他明确定义的类型,直接使用
return isNullable ? `${itemType} | null` : itemType
}
// 只有当type为unknown或未定义时才基于默认值进行类型推断
if (defaultValue !== null && defaultValue !== undefined) {
const valueType = typeof defaultValue
if (valueType === 'boolean' || valueType === 'string' || valueType === 'number') {
return valueType
}
if (Array.isArray(defaultValue)) {
return 'unknown[]'
}
if (valueType === 'object') {
return 'Record<string, unknown>'
}
}
return 'unknown | null'
}
generateTypeScriptCode(structure, preferencesData) {
const header = `/**
* Auto-generated preferences configuration
* Generated at: ${new Date().toISOString()}
*
* This file is automatically generated from classification.json
* To update this file, modify classification.json and run:
* node v2-refactor-temp/tools/data-classify/scripts/generate-preferences.js
*
* ## Key Naming Convention
*
* All preference keys MUST follow the format: \`namespace.sub.key_name\`
*
* Rules:
* - At least 2 segments separated by dots (.)
* - Each segment uses lowercase letters, numbers, and underscores only
* - Pattern: /^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+$/
*
* Examples:
* - 'app.user.avatar' (valid)
* - 'chat.multi_select_mode' (valid)
* - 'userAvatar' (invalid - missing dot separator)
* - 'App.user' (invalid - uppercase not allowed)
*
* This convention is enforced by ESLint rule: data-schema-key/valid-key
*
* === AUTO-GENERATED CONTENT START ===
*/
import { TRANSLATE_PROMPT } from '@shared/ai/prompts'
import * as PreferenceTypes from '@shared/data/preference/preferenceTypes'
/* eslint @typescript-eslint/member-ordering: ["error", {
"interfaces": { "order": "alphabetically" },
"typeLiterals": { "order": "alphabetically" }
}] */`
// 生成接口定义
const interfaceCode = this.generateInterface(structure)
// 生成默认值对象
const defaultsCode = this.generateDefaults(structure)
const footer = `
// === AUTO-GENERATED CONTENT END ===
/**
* 生成统计:
* - 总配置项: ${preferencesData.length}
* - electronStore项: ${preferencesData.filter((p) => p.source === 'electronStore').length}
* - redux项: ${preferencesData.filter((p) => p.source === 'redux').length}
* - localStorage项: ${preferencesData.filter((p) => p.source === 'localStorage').length}
* - dexieSettings项: ${preferencesData.filter((p) => p.source === 'dexieSettings').length}
*/`
return [header, interfaceCode, defaultsCode, footer].join('\n\n')
}
generateInterface(structure, depth = 0) {
const indent = ' '.repeat(depth)
if (depth === 0) {
// 顶层接口
let code = `export interface PreferenceSchemas {\n`
Object.keys(structure)
.sort()
.forEach((scope) => {
code += `${indent} ${scope}: {\n`
code += this.generateInterfaceProperties(structure[scope], depth + 2)
code += `${indent} }\n`
})
code += `}`
return code
}
}
generateInterfaceProperties(obj, depth) {
const indent = ' '.repeat(depth)
let code = ''
// 获取所有键并排序
const keys = Object.keys(obj).sort()
keys.forEach((key) => {
const value = obj[key]
if (value.type) {
// 叶子节点 - 实际的配置项直接使用targetKey
const comment = value.description ? `${indent}// ${value.description}\n` : ''
code += `${comment}${indent}'${key}': ${value.type}\n`
} else {
// 中间节点 - 嵌套对象
code += `${indent}'${key}': {\n`
code += this.generateInterfaceProperties(value, depth + 1)
code += `${indent}}\n`
}
})
return code
}
generateDefaults(structure) {
const header = `/* eslint sort-keys: ["error", "asc", {"caseSensitive": true, "natural": false}] */
export const DefaultPreferences: PreferenceSchemas = {`
let code = header + '\n'
Object.keys(structure)
.sort()
.forEach((scope) => {
code += ` ${scope}: {\n`
code += this.generateDefaultsProperties(structure[scope], 2)
code += ' }\n'
})
code += '}'
return code
}
generateDefaultsProperties(obj, depth) {
const indent = ' '.repeat(depth)
let code = ''
// 获取所有键并排序
const keys = Object.keys(obj).sort()
keys.forEach((key, index) => {
const value = obj[key]
const isLast = index === keys.length - 1
if (value.type) {
// 叶子节点 - 实际的配置项直接使用targetKey
const defaultVal = this.formatDefaultValue(value.defaultValue)
code += `${indent}'${key}': ${defaultVal}${isLast ? '' : ','}\n`
} else {
// 中间节点 - 嵌套对象
code += `${indent}'${key}': {\n`
code += this.generateDefaultsProperties(value, depth + 1)
code += `${indent}}${isLast ? '' : ','}\n`
}
})
return code
}
formatDefaultValue(value) {
if (value === null || value === undefined) {
return 'null'
}
if (typeof value === 'string') {
// Handle special "VALUE: xxxx" format - use xxxx directly without quotes
if (value.startsWith('VALUE: ')) {
return value.substring(7) // Remove "VALUE: " prefix and don't add quotes
}
return `'${value.replace(/'/g, "\\'")}'`
}
if (typeof value === 'boolean' || typeof value === 'number') {
return String(value)
}
if (Array.isArray(value)) {
return `[${value.map((item) => this.formatDefaultValue(item)).join(', ')}]`
}
if (typeof value === 'object') {
const entries = Object.entries(value).map(([k, v]) => `${k}: ${this.formatDefaultValue(v)}`)
return `{ ${entries.join(', ')} }`
}
return JSON.stringify(value)
}
writePreferencesFile(content) {
const targetDir = path.dirname(this.targetFile)
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true })
}
fs.writeFileSync(this.targetFile, content, 'utf8')
}
printSummary(preferencesData) {
console.log(`\n生成摘要:`)
console.log(`- 总配置项: ${preferencesData.length}`)
console.log(`- electronStore项: ${preferencesData.filter((p) => p.source === 'electronStore').length}`)
console.log(`- redux项: ${preferencesData.filter((p) => p.source === 'redux').length}`)
console.log(`- localStorage项: ${preferencesData.filter((p) => p.source === 'localStorage').length}`)
console.log(`- dexieSettings项: ${preferencesData.filter((p) => p.source === 'dexieSettings').length}`)
console.log(`- 输出文件: ${this.targetFile}`)
// 显示一些示例targetKey
const sampleKeys = preferencesData
.slice(0, 5)
.map((p) => p.targetKey)
.filter(Boolean)
if (sampleKeys.length > 0) {
console.log(`\n示例配置键:`)
sampleKeys.forEach((key) => console.log(` - ${key}`))
}
}
}
// 主执行逻辑
if (require.main === module) {
try {
const generator = new PreferencesGenerator()
generator.generate()
} catch (error) {
console.error('生成失败:', error.message)
process.exit(1)
}
}
module.exports = PreferencesGenerator