mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-09 06:49:02 +08:00
293 lines
11 KiB
JavaScript
293 lines
11 KiB
JavaScript
import tseslint from '@electron-toolkit/eslint-config-ts'
|
|
import eslint from '@eslint/js'
|
|
import eslintReact from '@eslint-react/eslint-plugin'
|
|
import { defineConfig } from 'eslint/config'
|
|
import importZod from 'eslint-plugin-import-zod'
|
|
import oxlint from 'eslint-plugin-oxlint'
|
|
import reactHooks from 'eslint-plugin-react-hooks'
|
|
import simpleImportSort from 'eslint-plugin-simple-import-sort'
|
|
import unusedImports from 'eslint-plugin-unused-imports'
|
|
|
|
export default defineConfig([
|
|
eslint.configs.recommended,
|
|
tseslint.configs.recommended,
|
|
eslintReact.configs['recommended-typescript'],
|
|
reactHooks.configs['recommended-latest'],
|
|
{
|
|
plugins: {
|
|
'simple-import-sort': simpleImportSort,
|
|
'unused-imports': unusedImports,
|
|
'import-zod': importZod
|
|
},
|
|
rules: {
|
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
'@typescript-eslint/no-explicit-any': 'off',
|
|
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
|
|
'simple-import-sort/imports': 'error',
|
|
'simple-import-sort/exports': 'error',
|
|
'unused-imports/no-unused-imports': 'error',
|
|
'@eslint-react/no-prop-types': 'error',
|
|
'import-zod/prefer-zod-namespace': 'error'
|
|
}
|
|
},
|
|
// Configuration for ensuring compatibility with the original ESLint(8.x) rules
|
|
{
|
|
rules: {
|
|
'@typescript-eslint/no-require-imports': 'off',
|
|
'@typescript-eslint/no-unused-vars': ['error', { caughtErrors: 'none' }],
|
|
'@typescript-eslint/no-unused-expressions': 'off',
|
|
'@typescript-eslint/no-empty-object-type': 'off',
|
|
'@eslint-react/hooks-extra/no-direct-set-state-in-use-effect': 'off',
|
|
'@eslint-react/web-api/no-leaked-event-listener': 'off',
|
|
'@eslint-react/web-api/no-leaked-timeout': 'off',
|
|
'@eslint-react/no-unknown-property': 'off',
|
|
'@eslint-react/no-nested-component-definitions': 'off',
|
|
'@eslint-react/dom/no-dangerously-set-innerhtml': 'off',
|
|
'@eslint-react/no-array-index-key': 'off',
|
|
'@eslint-react/no-unstable-default-props': 'off',
|
|
'@eslint-react/no-unstable-context-value': 'off',
|
|
'@eslint-react/hooks-extra/prefer-use-state-lazy-initialization': 'off',
|
|
'@eslint-react/hooks-extra/no-unnecessary-use-prefix': 'off',
|
|
'@eslint-react/no-children-to-array': 'off'
|
|
}
|
|
},
|
|
{
|
|
ignores: [
|
|
'node_modules/**',
|
|
'build/**',
|
|
'dist/**',
|
|
'out/**',
|
|
'local/**',
|
|
'tests/**',
|
|
'.yarn/**',
|
|
'.gitignore',
|
|
'.conductor/**',
|
|
'scripts/cloudflare-worker.js',
|
|
'src/main/integration/nutstore/sso/lib/**',
|
|
'src/main/integration/cherryai/index.js',
|
|
'src/main/integration/nutstore/sso/lib/**',
|
|
'src/renderer/src/ui/**',
|
|
'src/renderer/src/routeTree.gen.ts',
|
|
'packages/**/dist'
|
|
]
|
|
},
|
|
// turn off oxlint supported rules.
|
|
...oxlint.configs['flat/eslint'],
|
|
...oxlint.configs['flat/typescript'],
|
|
...oxlint.configs['flat/unicorn'],
|
|
// Custom rules should be after oxlint to overwrite
|
|
// LoggerService Custom Rules - only apply to src directory
|
|
{
|
|
files: ['src/**/*.{ts,tsx,js,jsx}'],
|
|
ignores: ['src/**/__tests__/**', 'src/**/__mocks__/**', 'src/**/*.test.*', 'src/preload/**'],
|
|
rules: {
|
|
'no-restricted-syntax': [
|
|
process.env.PRCI ? 'error' : 'warn',
|
|
{
|
|
selector: 'CallExpression[callee.object.name="console"]',
|
|
message:
|
|
'❗CherryStudio uses unified LoggerService: 📖 docs/technical/how-to-use-logger-en.md\n❗CherryStudio 使用统一的日志服务:📖 docs/technical/how-to-use-logger-zh.md\n\n'
|
|
}
|
|
]
|
|
}
|
|
},
|
|
// i18n
|
|
{
|
|
files: ['**/*.{ts,tsx,js,jsx}'],
|
|
languageOptions: {
|
|
ecmaVersion: 2022,
|
|
sourceType: 'module'
|
|
},
|
|
plugins: {
|
|
i18n: {
|
|
rules: {
|
|
'no-template-in-t': {
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description: '⚠️不建议在 t() 函数中使用模板字符串,这样会导致渲染结果不可预料',
|
|
recommended: true
|
|
},
|
|
messages: {
|
|
noTemplateInT: '⚠️不建议在 t() 函数中使用模板字符串,这样会导致渲染结果不可预料'
|
|
}
|
|
},
|
|
create(context) {
|
|
return {
|
|
CallExpression(node) {
|
|
const { callee, arguments: args } = node
|
|
const isTFunction =
|
|
(callee.type === 'Identifier' && callee.name === 't') ||
|
|
(callee.type === 'MemberExpression' &&
|
|
callee.property.type === 'Identifier' &&
|
|
callee.property.name === 't')
|
|
|
|
if (isTFunction && args[0]?.type === 'TemplateLiteral') {
|
|
context.report({
|
|
node: args[0],
|
|
messageId: 'noTemplateInT'
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
rules: {
|
|
'i18n/no-template-in-t': 'warn'
|
|
}
|
|
},
|
|
// ui migration
|
|
{
|
|
// Component Rules - prevent importing antd components when migration completed
|
|
files: ['**/*.{ts,tsx,js,jsx}'],
|
|
ignores: [],
|
|
rules: {
|
|
'no-restricted-imports': [
|
|
'error',
|
|
{
|
|
paths: [
|
|
// {
|
|
// name: 'antd',
|
|
// importNames: ['Flex', 'Switch', 'message', 'Button', 'Tooltip'],
|
|
// message:
|
|
// '❌ Do not import this component from antd. Use our custom components instead: import { ... } from "@cherrystudio/ui"'
|
|
// },
|
|
{
|
|
name: 'antd',
|
|
importNames: ['Switch'],
|
|
message:
|
|
'❌ Do not import this component from antd. Use our custom components instead: import { ... } from "@cherrystudio/ui"'
|
|
},
|
|
{
|
|
name: '@heroui/react',
|
|
importNames: ['Switch'],
|
|
message:
|
|
'❌ Do not import the component from heroui directly. It\'s deprecated.'
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
},
|
|
// Schema key naming convention (cache & preferences)
|
|
// Supports both fixed keys and template keys:
|
|
// - Fixed: 'app.user.avatar', 'chat.multi_select_mode'
|
|
// - Template: 'scroll.position.${topicId}', 'entity.cache.${type}_${id}'
|
|
// Template keys must follow the same dot-separated pattern as fixed keys.
|
|
// When ${xxx} placeholders are treated as literal strings, the key must match: xxx.yyy.zzz_www
|
|
{
|
|
files: ['packages/shared/data/cache/cacheSchemas.ts', 'packages/shared/data/preference/preferenceSchemas.ts'],
|
|
plugins: {
|
|
'data-schema-key': {
|
|
rules: {
|
|
'valid-key': {
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description:
|
|
'Enforce schema key naming convention: namespace.sub.key_name (template placeholders treated as literal strings)',
|
|
recommended: true
|
|
},
|
|
messages: {
|
|
invalidKey:
|
|
'Schema key "{{key}}" must follow format: namespace.sub.key_name (e.g., app.user.avatar, scroll.position.${id}). Template ${xxx} is treated as a literal string segment.',
|
|
invalidTemplateVar:
|
|
'Template variable in "{{key}}" must be a valid identifier (e.g., ${id}, ${topicId}).'
|
|
}
|
|
},
|
|
create(context) {
|
|
/**
|
|
* Validates a schema key for correct naming convention.
|
|
*
|
|
* Both fixed keys and template keys must follow the same pattern:
|
|
* - Lowercase segments separated by dots
|
|
* - Each segment: starts with letter, contains letters/numbers/underscores
|
|
* - At least two segments (must have at least one dot)
|
|
*
|
|
* Template keys: ${xxx} placeholders are treated as literal string segments.
|
|
* Example valid: 'scroll.position.${id}', 'entity.cache.${type}_${id}'
|
|
* Example invalid: 'cache:${type}' (colon not allowed), '${id}' (no dot)
|
|
*
|
|
* @param {string} key - The schema key to validate
|
|
* @returns {{ valid: boolean, error?: 'invalidKey' | 'invalidTemplateVar' }}
|
|
*/
|
|
function validateKey(key) {
|
|
// Check if key contains template placeholders
|
|
const hasTemplate = key.includes('${')
|
|
|
|
if (hasTemplate) {
|
|
// Validate template variable names first
|
|
const templateVarPattern = /\$\{([^}]*)\}/g
|
|
let match
|
|
while ((match = templateVarPattern.exec(key)) !== null) {
|
|
const varName = match[1]
|
|
// Variable must be a valid identifier: start with letter, contain only alphanumeric and underscore
|
|
if (!varName || !/^[a-zA-Z][a-zA-Z0-9_]*$/.test(varName)) {
|
|
return { valid: false, error: 'invalidTemplateVar' }
|
|
}
|
|
}
|
|
|
|
// Replace template placeholders with a valid segment marker
|
|
// Use 'x' as placeholder since it's a valid segment character
|
|
const keyWithoutTemplates = key.replace(/\$\{[^}]+\}/g, 'x')
|
|
|
|
// Template key must follow the same pattern as fixed keys
|
|
// when ${xxx} is treated as a literal string
|
|
const fixedKeyPattern = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/
|
|
if (!fixedKeyPattern.test(keyWithoutTemplates)) {
|
|
return { valid: false, error: 'invalidKey' }
|
|
}
|
|
|
|
return { valid: true }
|
|
} else {
|
|
// Fixed key validation: standard dot-separated format
|
|
const fixedKeyPattern = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/
|
|
if (!fixedKeyPattern.test(key)) {
|
|
return { valid: false, error: 'invalidKey' }
|
|
}
|
|
return { valid: true }
|
|
}
|
|
}
|
|
|
|
return {
|
|
TSPropertySignature(node) {
|
|
if (node.key.type === 'Literal' && typeof node.key.value === 'string') {
|
|
const key = node.key.value
|
|
const result = validateKey(key)
|
|
if (!result.valid) {
|
|
context.report({
|
|
node: node.key,
|
|
messageId: result.error,
|
|
data: { key }
|
|
})
|
|
}
|
|
}
|
|
},
|
|
Property(node) {
|
|
if (node.key.type === 'Literal' && typeof node.key.value === 'string') {
|
|
const key = node.key.value
|
|
const result = validateKey(key)
|
|
if (!result.valid) {
|
|
context.report({
|
|
node: node.key,
|
|
messageId: result.error,
|
|
data: { key }
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
rules: {
|
|
'data-schema-key/valid-key': 'error'
|
|
}
|
|
}
|
|
])
|