mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-06 05:55:28 +08:00
packages/shared was never a real pnpm workspace package (no package.json); it was referenced only through the @shared TypeScript path alias. Relocate it under src/ via git mv (143 files, detected as pure renames).
Repoint the @shared alias and include globs to src/shared across electron.vite.config.ts, tsconfig.{json,node,web}.json and vitest.config.ts; update scripts/check-custom-exts.ts, scripts/update-languages.ts, the eslint.config.mjs generated-file globs, the data-classify generator output targets, .github/CODEOWNERS path rules, and CLAUDE.md/docs/source-comment references.
The @shared alias name is unchanged, so all 1403 @shared/* import sites resolve without modification. Verified with typecheck:node, typecheck:web and the full test suite (700 files, 9739 tests passing).
303 lines
11 KiB
JavaScript
303 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
|
|
/**
|
|
* Manually maintained boot config items that aren't sourced from
|
|
* classification.json. These get merged into the generator's normal
|
|
* extraction pipeline so they flow through the same sort/emit code as
|
|
* classification-derived items — the output schema is a single, flat,
|
|
* fully auto-generated file.
|
|
*
|
|
* When classification.json learns to model new source kinds (e.g. a
|
|
* 'configfile' category), these entries should move into classification.json
|
|
* and this constant can shrink.
|
|
*/
|
|
const MANUAL_BOOT_CONFIG_ITEMS = [
|
|
{
|
|
source: 'configfile',
|
|
sourceCategory: 'legacy-home',
|
|
originalKey: 'appDataPath',
|
|
targetKey: 'app.user_data_path',
|
|
type: 'Record<string, string>',
|
|
defaultValue: 'VALUE: {}',
|
|
jsdoc: [
|
|
'Custom user data directory, keyed by executable path.',
|
|
'',
|
|
'Conceptually a single setting ("where user data lives"); stored as a',
|
|
'Record so the same machine can host multiple installations (stable / dev /',
|
|
"portable) with independent user data locations — matching the v1 behavior",
|
|
"of ~/.cherrystudio/config/config.json's `appDataPath` array.",
|
|
'',
|
|
"Key: executable path (matches Electron's `app.getPath('exe')`).",
|
|
'Value: absolute path to the chosen userData directory.',
|
|
'',
|
|
'Migrated from v1 ~/.cherrystudio/config/config.json on first v1→v2 run',
|
|
"via the 'configfile' source in BootConfigMigrator."
|
|
]
|
|
},
|
|
{
|
|
source: 'preboot',
|
|
sourceCategory: 'transient',
|
|
originalKey: 'userDataRelocation',
|
|
targetKey: 'temp.user_data_relocation',
|
|
type:
|
|
"\n | { status: 'pending'; from: string; to: string }" +
|
|
"\n | { status: 'failed'; from: string; to: string; error: string; failedAt: string }" +
|
|
'\n | null',
|
|
defaultValue: null,
|
|
jsdoc: [
|
|
'In-flight relocation of the Electron userData directory tree',
|
|
"(the directory returned by `app.getPath('userData')`).",
|
|
'',
|
|
'Lives under the `temp.*` top-level namespace — reserved for ephemeral',
|
|
'runtime state: single in-flight operations meant to be cleared once',
|
|
'consumed. **Never** backed up or synced: restoring a stale temp.* entry',
|
|
'on a different machine or at a different time can cause silent data',
|
|
'corruption (e.g. re-executing a relocation that already happened).',
|
|
'',
|
|
'Lifecycle:',
|
|
' - null: no relocation in progress (default).',
|
|
" - { status: 'pending', from, to }: an IPC handler wrote this request",
|
|
' and the next preboot should execute the copy.',
|
|
" - { status: 'failed', from, to, error, failedAt }: a previous preboot",
|
|
' attempted the copy and it failed. The record stays in BootConfig',
|
|
' until a renderer recovery flow lets the user retry, abandon, or',
|
|
' investigate. The app continues running on the previous userData',
|
|
' location until then.',
|
|
'',
|
|
'Note: "userData" here means the Electron OS directory',
|
|
"(app.getPath('userData')), not the colloquial sense of user content.",
|
|
'The copy includes everything under that directory — user files,',
|
|
'Chromium runtime state, logs, etc.',
|
|
'',
|
|
'Consumer: src/main/core/preboot/userDataLocation.ts'
|
|
]
|
|
}
|
|
]
|
|
|
|
class BootConfigGenerator {
|
|
constructor() {
|
|
this.dataDir = path.resolve(__dirname, '../data')
|
|
this.targetFile = path.resolve(__dirname, '../../../../src/shared/data/bootConfig/bootConfigSchemas.ts')
|
|
this.classificationFile = path.join(this.dataDir, 'classification.json')
|
|
}
|
|
|
|
generate() {
|
|
console.log('Generating bootConfigSchemas.ts...')
|
|
|
|
const classification = this.loadClassification()
|
|
const bootConfigData = this.extractBootConfigData(classification)
|
|
const content = this.generateTypeScriptCode(bootConfigData)
|
|
this.writeFile(content)
|
|
|
|
console.log('bootConfigSchemas.ts generated!')
|
|
this.printSummary(bootConfigData)
|
|
}
|
|
|
|
loadClassification() {
|
|
if (!fs.existsSync(this.classificationFile)) {
|
|
throw new Error(`Classification file not found: ${this.classificationFile}`)
|
|
}
|
|
|
|
const content = fs.readFileSync(this.classificationFile, 'utf8')
|
|
return JSON.parse(content)
|
|
}
|
|
|
|
extractBootConfigData(classification) {
|
|
const allItems = []
|
|
const sources = ['electronStore', 'redux', 'localStorage', 'dexieSettings']
|
|
|
|
const extractItems = (items, source, category, parentKey = '') => {
|
|
if (!Array.isArray(items)) return
|
|
|
|
items.forEach((item) => {
|
|
if (item.children && Array.isArray(item.children)) {
|
|
extractItems(item.children, source, category, `${parentKey}${item.originalKey}.`)
|
|
return
|
|
}
|
|
|
|
if (item.category === 'bootConfig' && item.status === 'classified' && item.targetKey) {
|
|
allItems.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(`Extracted ${allItems.length} bootConfig items`)
|
|
|
|
// Deduplicate by targetKey: redux(4) > dexieSettings(3) > localStorage(2) > electronStore(1)
|
|
const targetKeyGroups = {}
|
|
allItems.forEach((item) => {
|
|
if (!targetKeyGroups[item.targetKey]) {
|
|
targetKeyGroups[item.targetKey] = []
|
|
}
|
|
targetKeyGroups[item.targetKey].push(item)
|
|
})
|
|
|
|
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(`Duplicate targetKey: ${targetKey}, ${items.length} items`)
|
|
items.forEach((item) => console.log(` - ${item.fullPath}`))
|
|
|
|
items.sort((a, b) => sourcePriority[b.source] - sourcePriority[a.source])
|
|
const selected = items[0]
|
|
console.log(` Selected: ${selected.fullPath}`)
|
|
deduplicatedData.push(selected)
|
|
} else {
|
|
deduplicatedData.push(items[0])
|
|
}
|
|
})
|
|
|
|
// Append manually maintained items (config-file source for v1 legacy
|
|
// home config). These bypass classification.json deliberately — see the
|
|
// MANUAL_BOOT_CONFIG_ITEMS comment at the top of this file.
|
|
for (const manual of MANUAL_BOOT_CONFIG_ITEMS) {
|
|
deduplicatedData.push({ ...manual, fullPath: `${manual.source}/${manual.sourceCategory}/${manual.originalKey}` })
|
|
}
|
|
|
|
console.log(`After deduplication + manual items: ${deduplicatedData.length} bootConfig items`)
|
|
return deduplicatedData
|
|
}
|
|
|
|
mapType(itemType, defaultValue) {
|
|
if (itemType && itemType !== 'unknown') {
|
|
if (itemType === 'boolean') return 'boolean'
|
|
if (itemType === 'string') return 'string'
|
|
if (itemType === 'number') return 'number'
|
|
return itemType
|
|
}
|
|
|
|
if (defaultValue !== null && defaultValue !== undefined) {
|
|
const valueType = typeof defaultValue
|
|
if (valueType === 'boolean' || valueType === 'string' || valueType === 'number') {
|
|
return valueType
|
|
}
|
|
}
|
|
|
|
return 'unknown'
|
|
}
|
|
|
|
formatDefaultValue(value) {
|
|
if (value === null || value === undefined) {
|
|
return 'null'
|
|
}
|
|
if (typeof value === 'string') {
|
|
if (value.startsWith('VALUE: ')) {
|
|
return value.substring(7)
|
|
}
|
|
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)
|
|
}
|
|
|
|
generateTypeScriptCode(bootConfigData) {
|
|
const sortedData = [...bootConfigData].sort((a, b) => a.targetKey.localeCompare(b.targetKey))
|
|
|
|
const header = `/**
|
|
* Auto-generated boot config schema
|
|
* Generated at: ${new Date().toISOString()}
|
|
*
|
|
* This file is automatically generated from classification.json (plus a
|
|
* small MANUAL_BOOT_CONFIG_ITEMS list in generate-boot-config.js for keys
|
|
* that don't fit classification.json's model yet, e.g. config-file sources).
|
|
*
|
|
* To update this file, either modify classification.json or the manual list
|
|
* in the generator, then run:
|
|
* node v2-refactor-temp/tools/data-classify/scripts/generate-boot-config.js
|
|
*
|
|
* === AUTO-GENERATED CONTENT START ===
|
|
*/`
|
|
|
|
let interfaceCode = 'export interface BootConfigSchema {\n'
|
|
sortedData.forEach((item, index) => {
|
|
const tsType = this.mapType(item.type, item.defaultValue)
|
|
// Optional JSDoc block (currently only emitted for manual items).
|
|
if (Array.isArray(item.jsdoc) && item.jsdoc.length > 0) {
|
|
if (index > 0) interfaceCode += '\n'
|
|
interfaceCode += ' /**\n'
|
|
for (const line of item.jsdoc) {
|
|
interfaceCode += line.length > 0 ? ` * ${line}\n` : ' *\n'
|
|
}
|
|
interfaceCode += ' */\n'
|
|
}
|
|
interfaceCode += ` // ${item.source}/${item.sourceCategory}/${item.originalKey}\n`
|
|
interfaceCode += ` '${item.targetKey}': ${tsType}\n`
|
|
})
|
|
interfaceCode += '}'
|
|
|
|
let defaultsCode = 'export const DefaultBootConfig: BootConfigSchema = {\n'
|
|
sortedData.forEach((item, index) => {
|
|
const defaultVal = this.formatDefaultValue(item.defaultValue)
|
|
const isLast = index === sortedData.length - 1
|
|
defaultsCode += ` '${item.targetKey}': ${defaultVal}${isLast ? '' : ','}\n`
|
|
})
|
|
defaultsCode += '}'
|
|
|
|
const footer = '// === AUTO-GENERATED CONTENT END ==='
|
|
|
|
return [header, '', interfaceCode, '', defaultsCode, '', footer, ''].join('\n')
|
|
}
|
|
|
|
writeFile(content) {
|
|
const targetDir = path.dirname(this.targetFile)
|
|
if (!fs.existsSync(targetDir)) {
|
|
fs.mkdirSync(targetDir, { recursive: true })
|
|
}
|
|
|
|
fs.writeFileSync(this.targetFile, content, 'utf8')
|
|
console.log(`Written to: ${this.targetFile}`)
|
|
}
|
|
|
|
printSummary(bootConfigData) {
|
|
console.log(`\nSummary:`)
|
|
console.log(`- Total items: ${bootConfigData.length}`)
|
|
console.log(`- electronStore: ${bootConfigData.filter((p) => p.source === 'electronStore').length}`)
|
|
console.log(`- redux: ${bootConfigData.filter((p) => p.source === 'redux').length}`)
|
|
console.log(`- localStorage: ${bootConfigData.filter((p) => p.source === 'localStorage').length}`)
|
|
console.log(`- dexieSettings: ${bootConfigData.filter((p) => p.source === 'dexieSettings').length}`)
|
|
console.log(`- Output: ${this.targetFile}`)
|
|
}
|
|
}
|
|
|
|
if (require.main === module) {
|
|
try {
|
|
const generator = new BootConfigGenerator()
|
|
generator.generate()
|
|
} catch (error) {
|
|
console.error('Generation failed:', error.message)
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
module.exports = BootConfigGenerator
|